From 0f923b9553af7fe47270a391d0250fdb06204475 Mon Sep 17 00:00:00 2001 From: ok-nick Date: Fri, 14 Nov 2025 16:31:49 -0500 Subject: [PATCH 01/15] feat: setting to enable timestamp assertions for the parent ingredient in update manifests --- sdk/src/assertions/mod.rs | 2 +- sdk/src/assertions/timestamp.rs | 19 +------ sdk/src/builder.rs | 92 ++++++++++++++++++++++++++++----- sdk/src/settings/builder.rs | 16 +++++- sdk/src/settings/mod.rs | 1 - sdk/src/store.rs | 11 ++-- 6 files changed, 99 insertions(+), 42 deletions(-) diff --git a/sdk/src/assertions/mod.rs b/sdk/src/assertions/mod.rs index 728c27c8c..a21d27350 100644 --- a/sdk/src/assertions/mod.rs +++ b/sdk/src/assertions/mod.rs @@ -70,7 +70,7 @@ mod thumbnail; pub(crate) use thumbnail::Thumbnail; mod timestamp; -pub(crate) use timestamp::TimeStamp; +pub use timestamp::TimeStamp; mod user; pub(crate) use user::User; diff --git a/sdk/src/assertions/timestamp.rs b/sdk/src/assertions/timestamp.rs index 30c158310..80709ed53 100644 --- a/sdk/src/assertions/timestamp.rs +++ b/sdk/src/assertions/timestamp.rs @@ -21,14 +21,13 @@ use crate::{ assertion::{Assertion, AssertionBase, AssertionCbor}, assertions::labels, error::Result, - http::{AsyncGenericResolver, AsyncHttpResolver, SyncGenericResolver, SyncHttpResolver}, + http::{AsyncHttpResolver, SyncHttpResolver}, }; /// Helper class to create Timestamp assertions #[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq)] pub struct TimeStamp(HashMap); -#[allow(dead_code)] impl TimeStamp { /// Label prefix for an [`Timestamp`] assertion. /// @@ -50,26 +49,12 @@ impl TimeStamp { self.0.get(manifest_id).map(|buf| buf.as_ref()) } - #[async_generic] - pub fn send_timestamp_token_request(tsa_url: &str, message: &[u8]) -> Result> { - if _sync { - Self::send_timestamp_token_request_impl(tsa_url, message, &SyncGenericResolver::new()) - } else { - Self::send_timestamp_token_request_impl_async( - tsa_url, - message, - &AsyncGenericResolver::new(), - ) - .await - } - } - #[async_generic(async_signature( tsa_url: &str, message: &[u8], http_resolver: &impl AsyncHttpResolver, ))] - pub(crate) fn send_timestamp_token_request_impl( + pub(crate) fn send_timestamp_token_request( tsa_url: &str, message: &[u8], http_resolver: &impl SyncHttpResolver, diff --git a/sdk/src/builder.rs b/sdk/src/builder.rs index fe3271fca..eb18b028b 100644 --- a/sdk/src/builder.rs +++ b/sdk/src/builder.rs @@ -37,7 +37,7 @@ use crate::{ }, claim::Claim, error::{Error, Result}, - http::{AsyncGenericResolver, SyncGenericResolver}, + http::{AsyncGenericResolver, AsyncHttpResolver, SyncGenericResolver, SyncHttpResolver}, jumbf_io, resource_store::{ResourceRef, ResourceResolver, ResourceStore}, settings::{self, Settings}, @@ -1351,12 +1351,7 @@ impl Builder { } #[cfg(feature = "add_thumbnails")] - fn maybe_add_thumbnail( - &mut self, - format: &str, - stream: &mut R, - settings: &Settings, - ) -> Result<&mut Self> + fn maybe_add_thumbnail(&mut self, format: &str, stream: &mut R) -> Result<&mut Self> where R: Read + Seek + ?Sized, { @@ -1366,7 +1361,7 @@ impl Builder { } // check settings to see if we should auto generate a thumbnail - let auto_thumbnail = settings.builder.thumbnail.enabled; + let auto_thumbnail = self.settings.builder.thumbnail.enabled; if self.definition.thumbnail.is_none() && auto_thumbnail { stream.rewind()?; @@ -1376,7 +1371,7 @@ impl Builder { crate::utils::thumbnail::make_thumbnail_bytes_from_stream( format, &mut stream, - settings, + &self.settings, )? { stream.rewind()?; @@ -1418,6 +1413,52 @@ impl Builder { Ok(self) } + #[async_generic(async_signature( + &mut self, + timestamp_authority_url: &str, + store: &mut Store, + http_resolver: &impl AsyncHttpResolver, + ))] + fn maybe_add_timestamp( + &mut self, + timestamp_authority_url: &str, + store: &mut Store, + http_resolver: &impl SyncHttpResolver, + ) -> Result<()> { + if self.intent() == Some(BuilderIntent::Update) + && self.definition.ingredients.iter().any(|i| i.is_parent()) + && self.settings.builder.add_parent_timestamp_assertion + { + // TODO: or err? + #[allow(clippy::unwrap_used)] + let manifest_id = store.provenance_path().unwrap(); + + let manifest_ids = vec![manifest_id.as_ref()]; + let timestamp_assertion = if _sync { + store.get_timestamp_assertion( + &manifest_ids, + timestamp_authority_url, + http_resolver, + )? + } else { + store + .get_timestamp_assertion_async( + &manifest_ids, + timestamp_authority_url, + http_resolver, + ) + .await? + }; + + // TODO: or err? + #[allow(clippy::unwrap_used)] + let claim = store.provenance_claim_mut().unwrap(); + claim.add_assertion(×tamp_assertion)?; + } + + Ok(()) + } + // Find an assertion in the manifest. pub(crate) fn find_assertion(&self, label: &str) -> Result { if let Some(manifest_assertion) = self @@ -1571,7 +1612,6 @@ impl Builder { R: Read + Seek + Send, W: Write + Read + Seek + Send, { - let settings = crate::settings::get_settings().unwrap_or_default(); let http_resolver = if _sync { SyncGenericResolver::new() } else { @@ -1593,17 +1633,41 @@ impl Builder { // generate thumbnail if we don't already have one #[cfg(feature = "add_thumbnails")] - self.maybe_add_thumbnail(&format, source, &settings)?; + self.maybe_add_thumbnail(&format, source)?; // convert the manifest to a store - let mut store = self.to_store(&settings)?; + let mut store = self.to_store(&self.settings)?; + + // add timestamp if conditions allow + if let Some(timestamp_authority_url) = signer.time_authority_url() { + if _sync { + self.maybe_add_timestamp(×tamp_authority_url, &mut store, &http_resolver)?; + } else { + self.maybe_add_timestamp_async(×tamp_authority_url, &mut store, &http_resolver) + .await? + } + } // sign and write our store to to the output image file if _sync { - store.save_to_stream(&format, source, dest, signer, &http_resolver, &settings) + store.save_to_stream( + &format, + source, + dest, + signer, + &http_resolver, + &self.settings, + ) } else { store - .save_to_stream_async(&format, source, dest, signer, &http_resolver, &settings) + .save_to_stream_async( + &format, + source, + dest, + signer, + &http_resolver, + &self.settings, + ) .await } } diff --git a/sdk/src/settings/builder.rs b/sdk/src/settings/builder.rs index 12616a0cc..07ef1700d 100644 --- a/sdk/src/settings/builder.rs +++ b/sdk/src/settings/builder.rs @@ -467,10 +467,24 @@ pub struct BuilderSettings { /// See more information on the difference between created vs gathered assertions in the spec here: /// pub created_assertion_labels: Option>, - /// Whether to generate a C2PA archive (instead of zip) when writing the manifest builder. /// This will eventually become the default behavior. pub generate_c2pa_archive: Option, + /// Whether to auto-generate a [`TimeStamp`] assertion for the parent ingredient. + /// + /// Useful when a manifest was signed offline and you want to attach a trusted timestamp to it later. + /// + /// Note that for this setting to take effect, a time stamp authority URL must be set in the + /// [`Signer::time_authority_url`]. If the signer is acquired from settings via [`Settings::signer`], + /// the URL can be set in [`SignerSettings`]. + /// + /// The default value is false. + /// + /// [`TimeStamp`]: crate::assertions::TimeStamp + /// [`Signer::time_authority_url`]: crate::Signer::time_authority_url + /// [`Settings::signer`]: crate::settings::signer + /// [`SignerSettings`]: crate::settings::signer::SignerSettings + pub add_parent_timestamp_assertion: bool, } /// The scope of which manifests to fetch for OCSP. diff --git a/sdk/src/settings/mod.rs b/sdk/src/settings/mod.rs index effc2ebca..1d7e1e734 100644 --- a/sdk/src/settings/mod.rs +++ b/sdk/src/settings/mod.rs @@ -305,7 +305,6 @@ pub struct Verify { /// [`Ingredient`]: crate::Ingredient /// [`Builder`]: crate::Builder pub remote_manifest_fetch: bool, - /// /// Whether to skip ingredient conflict resolution when multiple ingredients have the same /// manifest identifier. This settings is only applicable for C2PA v2 validation. /// diff --git a/sdk/src/store.rs b/sdk/src/store.rs index c1cd24a5e..d1a3fb214 100644 --- a/sdk/src/store.rs +++ b/sdk/src/store.rs @@ -500,7 +500,6 @@ impl Store { /// Creates a TimeStamp (c2pa.time-stamp) assertion containing the TimeStampTokens for each /// specified manifest_id. If any time stamp request fails the assertion is not created. - #[allow(dead_code)] #[async_generic(async_signature( &self, manifest_ids: &[&str], @@ -521,14 +520,10 @@ impl Store { .ok_or(Error::ClaimMissingSignatureBox)?; let timestamp_token = if _sync { - TimeStamp::send_timestamp_token_request_impl(tsa_url, &signature, http_resolver)? + TimeStamp::send_timestamp_token_request(tsa_url, &signature, http_resolver)? } else { - TimeStamp::send_timestamp_token_request_impl_async( - tsa_url, - &signature, - http_resolver, - ) - .await? + TimeStamp::send_timestamp_token_request_async(tsa_url, &signature, http_resolver) + .await? }; timestamp_assertion.add_timestamp(manifest_id, ×tamp_token); From c344c1b482617b5711b939696532a83c81d2bdb5 Mon Sep 17 00:00:00 2001 From: ok-nick Date: Mon, 17 Nov 2025 10:01:49 -0500 Subject: [PATCH 02/15] feat: add Claim::parent_claim and change timestamp setting to apply only to update manifests --- sdk/src/builder.rs | 52 +++++++++++++++++++------------------ sdk/src/claim.rs | 15 +++++++++++ sdk/src/settings/builder.rs | 4 +-- sdk/src/store.rs | 23 +++++----------- 4 files changed, 51 insertions(+), 43 deletions(-) diff --git a/sdk/src/builder.rs b/sdk/src/builder.rs index eb18b028b..aff4c5531 100644 --- a/sdk/src/builder.rs +++ b/sdk/src/builder.rs @@ -1425,35 +1425,37 @@ impl Builder { store: &mut Store, http_resolver: &impl SyncHttpResolver, ) -> Result<()> { - if self.intent() == Some(BuilderIntent::Update) - && self.definition.ingredients.iter().any(|i| i.is_parent()) - && self.settings.builder.add_parent_timestamp_assertion - { - // TODO: or err? - #[allow(clippy::unwrap_used)] - let manifest_id = store.provenance_path().unwrap(); - - let manifest_ids = vec![manifest_id.as_ref()]; - let timestamp_assertion = if _sync { - store.get_timestamp_assertion( - &manifest_ids, - timestamp_authority_url, - http_resolver, - )? - } else { - store - .get_timestamp_assertion_async( + if self.settings.builder.update_manifest_timestamp_assertion { + // TODO: better errors here + let provenance_claim = store.provenance_claim().ok_or(Error::NotFound)?; + if provenance_claim.update_manifest() { + // TODO: better errors here + let parent_claim_id = provenance_claim + .parent_claim()? + .ok_or(Error::NotFound)? + .label(); + + let manifest_ids = vec![parent_claim_id]; + let timestamp_assertion = if _sync { + store.get_timestamp_assertion( &manifest_ids, timestamp_authority_url, http_resolver, - ) - .await? - }; + )? + } else { + store + .get_timestamp_assertion_async( + &manifest_ids, + timestamp_authority_url, + http_resolver, + ) + .await? + }; - // TODO: or err? - #[allow(clippy::unwrap_used)] - let claim = store.provenance_claim_mut().unwrap(); - claim.add_assertion(×tamp_assertion)?; + // TODO: better errors here + let claim = store.provenance_claim_mut().ok_or(Error::NotFound)?; + claim.add_assertion(×tamp_assertion)?; + } } Ok(()) diff --git a/sdk/src/claim.rs b/sdk/src/claim.rs index 7759aeadd..b9fb95424 100644 --- a/sdk/src/claim.rs +++ b/sdk/src/claim.rs @@ -3396,6 +3396,21 @@ impl Claim { self.ingredients_store.get_mut(claim_guid) } + /// Return the claim of the ingredient with a ParentOf relationship. + pub fn parent_claim(&self) -> Result> { + for i in self.ingredient_assertions() { + let ingredient = Ingredient::from_assertion(i.assertion())?; + if ingredient.relationship == Relationship::ParentOf { + if let Some(parent_uri) = ingredient.c2pa_manifest() { + return Ok(manifest_label_from_uri(&parent_uri.url()) + .and_then(|parent_label| self.ingredients_store.get(&parent_label))); + } + } + } + + Ok(None) + } + /// Adds ingredients, this data will be written out during commit of the Claim /// redactions are full uris since they refer to external assertions pub(crate) fn add_ingredient_data( diff --git a/sdk/src/settings/builder.rs b/sdk/src/settings/builder.rs index 07ef1700d..0baeec0e0 100644 --- a/sdk/src/settings/builder.rs +++ b/sdk/src/settings/builder.rs @@ -470,7 +470,7 @@ pub struct BuilderSettings { /// Whether to generate a C2PA archive (instead of zip) when writing the manifest builder. /// This will eventually become the default behavior. pub generate_c2pa_archive: Option, - /// Whether to auto-generate a [`TimeStamp`] assertion for the parent ingredient. + /// Whether to auto-generate a [`TimeStamp`] assertion for an update manifest. /// /// Useful when a manifest was signed offline and you want to attach a trusted timestamp to it later. /// @@ -484,7 +484,7 @@ pub struct BuilderSettings { /// [`Signer::time_authority_url`]: crate::Signer::time_authority_url /// [`Settings::signer`]: crate::settings::signer /// [`SignerSettings`]: crate::settings::signer::SignerSettings - pub add_parent_timestamp_assertion: bool, + pub update_manifest_timestamp_assertion: bool, } /// The scope of which manifests to fetch for OCSP. diff --git a/sdk/src/store.rs b/sdk/src/store.rs index d1a3fb214..62ed18d4e 100644 --- a/sdk/src/store.rs +++ b/sdk/src/store.rs @@ -64,8 +64,8 @@ use crate::{ self, boxes::*, labels::{ - manifest_label_from_uri, manifest_label_to_parts, to_assertion_uri, to_manifest_uri, - ASSERTIONS, CREDENTIALS, DATABOXES, SIGNATURE, + manifest_label_to_parts, to_assertion_uri, to_manifest_uri, ASSERTIONS, CREDENTIALS, + DATABOXES, SIGNATURE, }, }, jumbf_io::{ @@ -3941,20 +3941,11 @@ impl Store { } // walk the update manifests until you find an acceptable claim - for i in claim.ingredient_assertions() { - let ingredient = Ingredient::from_assertion(i.assertion()).ok()?; - if ingredient.relationship == Relationship::ParentOf { - if let Some(parent_uri) = ingredient.c2pa_manifest() { - let parent_label = manifest_label_from_uri(&parent_uri.url())?; - if let Some(parent) = self.get_claim(&parent_label) { - // recurse until we find - if parent.update_manifest() { - self.get_hash_binding_manifest(parent); - } else if !parent.hash_assertions().is_empty() { - return Some(parent.label().to_owned()); - } - } - } + if let Some(parent) = claim.parent_claim().ok()? { + if parent.update_manifest() { + self.get_hash_binding_manifest(parent); + } else if !parent.hash_assertions().is_empty() { + return Some(parent.label().to_owned()); } } None From fc0777bcb3365a2e3b88776bb521efdc7c6af875 Mon Sep 17 00:00:00 2001 From: ok-nick Date: Mon, 17 Nov 2025 10:56:43 -0500 Subject: [PATCH 03/15] fix: return uri instead of claim inside of claim since they can be uncommitted --- sdk/src/builder.rs | 13 ++++++++----- sdk/src/claim.rs | 29 ++++++++++++++--------------- sdk/src/store.rs | 18 +++++++++++------- 3 files changed, 33 insertions(+), 27 deletions(-) diff --git a/sdk/src/builder.rs b/sdk/src/builder.rs index aff4c5531..c1e4831a5 100644 --- a/sdk/src/builder.rs +++ b/sdk/src/builder.rs @@ -38,6 +38,7 @@ use crate::{ claim::Claim, error::{Error, Result}, http::{AsyncGenericResolver, AsyncHttpResolver, SyncGenericResolver, SyncHttpResolver}, + jumbf::labels::manifest_label_from_uri, jumbf_io, resource_store::{ResourceRef, ResourceResolver, ResourceStore}, settings::{self, Settings}, @@ -1430,12 +1431,14 @@ impl Builder { let provenance_claim = store.provenance_claim().ok_or(Error::NotFound)?; if provenance_claim.update_manifest() { // TODO: better errors here - let parent_claim_id = provenance_claim - .parent_claim()? - .ok_or(Error::NotFound)? - .label(); + let parent_claim_id = manifest_label_from_uri( + &provenance_claim + .parent_claim_uri()? + .ok_or(Error::NotFound)?, + ) + .ok_or(Error::NotFound)?; - let manifest_ids = vec![parent_claim_id]; + let manifest_ids = vec![parent_claim_id.as_ref()]; let timestamp_assertion = if _sync { store.get_timestamp_assertion( &manifest_ids, diff --git a/sdk/src/claim.rs b/sdk/src/claim.rs index b9fb95424..f79aaf965 100644 --- a/sdk/src/claim.rs +++ b/sdk/src/claim.rs @@ -3396,21 +3396,6 @@ impl Claim { self.ingredients_store.get_mut(claim_guid) } - /// Return the claim of the ingredient with a ParentOf relationship. - pub fn parent_claim(&self) -> Result> { - for i in self.ingredient_assertions() { - let ingredient = Ingredient::from_assertion(i.assertion())?; - if ingredient.relationship == Relationship::ParentOf { - if let Some(parent_uri) = ingredient.c2pa_manifest() { - return Ok(manifest_label_from_uri(&parent_uri.url()) - .and_then(|parent_label| self.ingredients_store.get(&parent_label))); - } - } - } - - Ok(None) - } - /// Adds ingredients, this data will be written out during commit of the Claim /// redactions are full uris since they refer to external assertions pub(crate) fn add_ingredient_data( @@ -3937,6 +3922,20 @@ impl Claim { } } + /// Return the claim JUMBF URI of the ingredient with a ParentOf relationship. + pub fn parent_claim_uri(&self) -> Result> { + for i in self.ingredient_assertions() { + let ingredient = Ingredient::from_assertion(i.assertion())?; + if ingredient.relationship == Relationship::ParentOf { + return Ok(ingredient + .c2pa_manifest() + .map(|hashed_uri| hashed_uri.url())); + } + } + + Ok(None) + } + /// Checks whether or not ocsp values are present in claim pub fn has_ocsp_vals(&self) -> bool { if !self.certificate_status_assertions().is_empty() { diff --git a/sdk/src/store.rs b/sdk/src/store.rs index 62ed18d4e..3ff087bf4 100644 --- a/sdk/src/store.rs +++ b/sdk/src/store.rs @@ -64,8 +64,8 @@ use crate::{ self, boxes::*, labels::{ - manifest_label_to_parts, to_assertion_uri, to_manifest_uri, ASSERTIONS, CREDENTIALS, - DATABOXES, SIGNATURE, + manifest_label_from_uri, manifest_label_to_parts, to_assertion_uri, to_manifest_uri, + ASSERTIONS, CREDENTIALS, DATABOXES, SIGNATURE, }, }, jumbf_io::{ @@ -3941,13 +3941,17 @@ impl Store { } // walk the update manifests until you find an acceptable claim - if let Some(parent) = claim.parent_claim().ok()? { - if parent.update_manifest() { - self.get_hash_binding_manifest(parent); - } else if !parent.hash_assertions().is_empty() { - return Some(parent.label().to_owned()); + if let Some(parent_uri) = claim.parent_claim_uri().ok()? { + let parent_label = manifest_label_from_uri(&parent_uri)?; + if let Some(parent) = self.get_claim(&parent_label) { + if parent.update_manifest() { + self.get_hash_binding_manifest(parent); + } else if !parent.hash_assertions().is_empty() { + return Some(parent.label().to_owned()); + } } } + None } From ba21217d736d0ba3b75b8a50c7773b4ac35ad51c Mon Sep 17 00:00:00 2001 From: ok-nick Date: Mon, 17 Nov 2025 11:49:53 -0500 Subject: [PATCH 04/15] test: update manifest timestamp assertions --- sdk/src/builder.rs | 8 ++-- sdk/src/settings/builder.rs | 2 +- sdk/tests/test_builder.rs | 90 +++++++++++++++++++++++++++++++++++-- 3 files changed, 92 insertions(+), 8 deletions(-) diff --git a/sdk/src/builder.rs b/sdk/src/builder.rs index 89327a190..c1c30202f 100644 --- a/sdk/src/builder.rs +++ b/sdk/src/builder.rs @@ -1441,13 +1441,13 @@ impl Builder { #[async_generic(async_signature( &mut self, - timestamp_authority_url: &str, + time_authority_url: &str, store: &mut Store, http_resolver: &impl AsyncHttpResolver, ))] fn maybe_add_timestamp( &mut self, - timestamp_authority_url: &str, + time_authority_url: &str, store: &mut Store, http_resolver: &impl SyncHttpResolver, ) -> Result<()> { @@ -1467,14 +1467,14 @@ impl Builder { let timestamp_assertion = if _sync { store.get_timestamp_assertion( &manifest_ids, - timestamp_authority_url, + time_authority_url, http_resolver, )? } else { store .get_timestamp_assertion_async( &manifest_ids, - timestamp_authority_url, + time_authority_url, http_resolver, ) .await? diff --git a/sdk/src/settings/builder.rs b/sdk/src/settings/builder.rs index 8ab9fc072..235e36b5a 100644 --- a/sdk/src/settings/builder.rs +++ b/sdk/src/settings/builder.rs @@ -474,7 +474,7 @@ pub struct BuilderSettings { /// /// Useful when a manifest was signed offline and you want to attach a trusted timestamp to it later. /// - /// Note that for this setting to take effect, a time stamp authority URL must be set in the + /// Note that for this setting to take effect, a time authority URL must be set in the /// [`Signer::time_authority_url`]. If the signer is acquired from settings via [`Settings::signer`], /// the URL can be set in [`SignerSettings`]. /// diff --git a/sdk/tests/test_builder.rs b/sdk/tests/test_builder.rs index 4bfa0a588..19eec817f 100644 --- a/sdk/tests/test_builder.rs +++ b/sdk/tests/test_builder.rs @@ -11,11 +11,13 @@ // specific language governing permissions and limitations under // each license. -use std::io::{self, Cursor}; +use std::io::{self, Cursor, Seek}; use c2pa::{ - settings::Settings, validation_status, Builder, BuilderIntent, ManifestAssertionKind, Reader, - Result, ValidationState, + assertions::{self, TimeStamp}, + settings::Settings, + validation_status, Builder, BuilderIntent, ManifestAssertionKind, Reader, Result, Signer, + ValidationState, }; mod common; @@ -23,6 +25,88 @@ mod common; use common::compare_stream_to_known_good; use common::test_signer; +#[test] +fn test_update_manifest_timestamp_assertion() { + const TEST_IMAGE: &[u8] = include_bytes!("fixtures/CA.jpg"); + const FORMAT: &str = "image/jpeg"; + + // Basic wrapper around a Signer to include a time authority URL. + struct WrappedTsaSigner(Box); + + impl Signer for WrappedTsaSigner { + fn sign(&self, data: &[u8]) -> Result> { + self.0.sign(data) + } + + fn alg(&self) -> c2pa::SigningAlg { + self.0.alg() + } + + fn certs(&self) -> Result>> { + self.0.certs() + } + + fn reserve_size(&self) -> usize { + self.0.reserve_size() + } + + fn time_authority_url(&self) -> Option { + Some("http://timestamp.digicert.com".to_owned()) + } + } + + Settings::from_toml(include_str!("../tests/fixtures/test_settings.toml")).unwrap(); + Settings::from_toml( + &toml::toml! { + [builder] + update_manifest_timestamp_assertion = true + } + .to_string(), + ) + .unwrap(); + + let mut child_image = Cursor::new(Vec::new()); + + let mut builder = Builder::new(); + builder + .sign( + &Settings::signer().unwrap(), + FORMAT, + &mut Cursor::new(TEST_IMAGE), + &mut child_image, + ) + .unwrap(); + + child_image.rewind().unwrap(); + + let mut parent_image = Cursor::new(Vec::new()); + + let mut builder = Builder::new(); + builder.set_intent(BuilderIntent::Update); + builder + .sign( + &WrappedTsaSigner(Settings::signer().unwrap()), + FORMAT, + &mut child_image, + &mut parent_image, + ) + .unwrap(); + + parent_image.rewind().unwrap(); + + let reader = Reader::from_stream(FORMAT, parent_image).unwrap(); + let timestamp_assertion: TimeStamp = reader + .active_manifest() + .unwrap() + .find_assertion(assertions::labels::TIMESTAMP) + .unwrap(); + + let active_manifest_label = reader.active_label().unwrap(); + assert!(timestamp_assertion + .get_timestamp(active_manifest_label) + .is_some()); +} + #[test] #[cfg(all(feature = "add_thumbnails", feature = "file_io"))] fn test_builder_ca_jpg() -> Result<()> { From 5d2b845b7d5f227f5fe31b617f15a4a11b8df6e5 Mon Sep 17 00:00:00 2001 From: ok-nick Date: Mon, 17 Nov 2025 12:12:07 -0500 Subject: [PATCH 05/15] test: use child manifest label instead of active when verifying timestamp assertion --- sdk/tests/test_builder.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/sdk/tests/test_builder.rs b/sdk/tests/test_builder.rs index 19eec817f..4baa82341 100644 --- a/sdk/tests/test_builder.rs +++ b/sdk/tests/test_builder.rs @@ -24,6 +24,7 @@ mod common; #[cfg(all(feature = "add_thumbnails", feature = "file_io"))] use common::compare_stream_to_known_good; use common::test_signer; +use serde_with::TimestampSeconds; #[test] fn test_update_manifest_timestamp_assertion() { @@ -101,9 +102,11 @@ fn test_update_manifest_timestamp_assertion() { .find_assertion(assertions::labels::TIMESTAMP) .unwrap(); - let active_manifest_label = reader.active_label().unwrap(); + let child_manifest_label = reader.active_manifest().unwrap().ingredients()[0] + .active_manifest() + .unwrap(); assert!(timestamp_assertion - .get_timestamp(active_manifest_label) + .get_timestamp(child_manifest_label) .is_some()); } From 86c505104ff9db73395e7d4c66f694d9712a64a3 Mon Sep 17 00:00:00 2001 From: ok-nick Date: Mon, 17 Nov 2025 12:15:06 -0500 Subject: [PATCH 06/15] docs: clarify update_manifest_timestamp_assertion setting --- sdk/src/settings/builder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/src/settings/builder.rs b/sdk/src/settings/builder.rs index 235e36b5a..f2063051d 100644 --- a/sdk/src/settings/builder.rs +++ b/sdk/src/settings/builder.rs @@ -470,7 +470,7 @@ pub struct BuilderSettings { /// Whether to generate a C2PA archive (instead of zip) when writing the manifest builder. /// This will eventually become the default behavior. pub generate_c2pa_archive: Option, - /// Whether to auto-generate a [`TimeStamp`] assertion for an update manifest. + /// Whether to auto-generate a [`TimeStamp`] assertion for update manifests. /// /// Useful when a manifest was signed offline and you want to attach a trusted timestamp to it later. /// From 5c81c84d52e329ddce71c06aa928ad973e44f2a3 Mon Sep 17 00:00:00 2001 From: ok-nick Date: Mon, 17 Nov 2025 12:17:39 -0500 Subject: [PATCH 07/15] fix: align new error cases with same errors used in other places --- sdk/src/builder.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/sdk/src/builder.rs b/sdk/src/builder.rs index c1c30202f..cd65781e4 100644 --- a/sdk/src/builder.rs +++ b/sdk/src/builder.rs @@ -1452,16 +1452,14 @@ impl Builder { http_resolver: &impl SyncHttpResolver, ) -> Result<()> { if self.settings.builder.update_manifest_timestamp_assertion { - // TODO: better errors here - let provenance_claim = store.provenance_claim().ok_or(Error::NotFound)?; + let provenance_claim = store.provenance_claim().ok_or(Error::ClaimEncoding)?; if provenance_claim.update_manifest() { - // TODO: better errors here let parent_claim_id = manifest_label_from_uri( &provenance_claim .parent_claim_uri()? - .ok_or(Error::NotFound)?, + .ok_or(Error::ClaimEncoding)?, ) - .ok_or(Error::NotFound)?; + .ok_or(Error::ClaimEncoding)?; let manifest_ids = vec![parent_claim_id.as_ref()]; let timestamp_assertion = if _sync { @@ -1480,8 +1478,7 @@ impl Builder { .await? }; - // TODO: better errors here - let claim = store.provenance_claim_mut().ok_or(Error::NotFound)?; + let claim = store.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; claim.add_assertion(×tamp_assertion)?; } } From ca5c35a924c0db9c6f9ff735d6bd47f190d80705 Mon Sep 17 00:00:00 2001 From: ok-nick Date: Mon, 17 Nov 2025 12:54:59 -0500 Subject: [PATCH 08/15] style: fix clippy lints --- sdk/tests/test_builder.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/sdk/tests/test_builder.rs b/sdk/tests/test_builder.rs index 4baa82341..0ac1e341f 100644 --- a/sdk/tests/test_builder.rs +++ b/sdk/tests/test_builder.rs @@ -24,7 +24,6 @@ mod common; #[cfg(all(feature = "add_thumbnails", feature = "file_io"))] use common::compare_stream_to_known_good; use common::test_signer; -use serde_with::TimestampSeconds; #[test] fn test_update_manifest_timestamp_assertion() { From 4f48857114856040db044de1d61349d0e8b1e8ee Mon Sep 17 00:00:00 2001 From: ok-nick Date: Mon, 17 Nov 2025 13:52:42 -0500 Subject: [PATCH 09/15] test: fix test_bogus_cert test by moving settings changes before creating the builder --- sdk/src/store.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/sdk/src/store.rs b/sdk/src/store.rs index 3ff087bf4..72a70e4db 100644 --- a/sdk/src/store.rs +++ b/sdk/src/store.rs @@ -8839,6 +8839,11 @@ pub mod tests { #[cfg(feature = "file_io")] fn test_bogus_cert() { use crate::builder::{Builder, BuilderIntent}; + + // bypass auto sig check + crate::settings::set_settings_value("verify.verify_after_sign", false).unwrap(); + crate::settings::set_settings_value("verify.verify_trust", false).unwrap(); + let png = include_bytes!("../tests/fixtures/libpng-test.png"); // Randomly generated local Ed25519 let ed25519 = include_bytes!("../tests/fixtures/certs/ed25519.pem"); let certs = include_bytes!("../tests/fixtures/certs/es256.pub"); @@ -8849,10 +8854,6 @@ pub mod tests { crate::create_signer::from_keys(certs, ed25519, SigningAlg::Ed25519, None).unwrap(); let mut dst = Cursor::new(Vec::new()); - // bypass auto sig check - crate::settings::set_settings_value("verify.verify_after_sign", false).unwrap(); - crate::settings::set_settings_value("verify.verify_trust", false).unwrap(); - builder .sign(&signer, "image/png", &mut Cursor::new(png), &mut dst) .unwrap(); From cd48c517977ba4e301ac5e09c85e8aefd5f2c564 Mon Sep 17 00:00:00 2001 From: ok-nick Date: Wed, 19 Nov 2025 14:32:05 -0500 Subject: [PATCH 10/15] fix: use existing timestamp assertion if it exists and move timestamp logic into assertion --- sdk/src/assertions/timestamp.rs | 54 +++++++++++++++++++++++++++------ sdk/src/builder.rs | 43 ++++++++++++++++++-------- sdk/src/settings/builder.rs | 3 +- sdk/src/store.rs | 35 +-------------------- 4 files changed, 79 insertions(+), 56 deletions(-) diff --git a/sdk/src/assertions/timestamp.rs b/sdk/src/assertions/timestamp.rs index 80709ed53..52b4a35bf 100644 --- a/sdk/src/assertions/timestamp.rs +++ b/sdk/src/assertions/timestamp.rs @@ -20,8 +20,13 @@ use serde_bytes::ByteBuf; use crate::{ assertion::{Assertion, AssertionBase, AssertionCbor}, assertions::labels, + crypto::cose::CertificateTrustPolicy, error::Result, http::{AsyncHttpResolver, SyncHttpResolver}, + settings::Settings, + status_tracker::StatusTracker, + store::Store, + Error, }; /// Helper class to create Timestamp assertions @@ -50,26 +55,57 @@ impl TimeStamp { } #[async_generic(async_signature( - tsa_url: &str, + &mut self, + store: &Store, + time_authority_url: &str, + manifest_id: &str, + http_resolver: &impl AsyncHttpResolver, + ))] + pub(crate) fn refresh_timestamp( + &mut self, + store: &Store, + time_authority_url: &str, + manifest_id: &str, + http_resolver: &impl SyncHttpResolver, + ) -> Result<&[u8]> { + let signature = store + .get_cose_sign1_signature(manifest_id) + .ok_or(Error::ClaimMissingSignatureBox)?; + + let timestamp_token = if _sync { + TimeStamp::send_timestamp_token_request(time_authority_url, &signature, http_resolver)? + } else { + TimeStamp::send_timestamp_token_request_async( + time_authority_url, + &signature, + http_resolver, + ) + .await? + }; + + Ok(self + .0 + .entry(manifest_id.to_owned()) + .or_insert(ByteBuf::from(timestamp_token)) + .as_slice()) + } + + #[async_generic(async_signature( + time_authority_url: &str, message: &[u8], http_resolver: &impl AsyncHttpResolver, ))] pub(crate) fn send_timestamp_token_request( - tsa_url: &str, + time_authority_url: &str, message: &[u8], http_resolver: &impl SyncHttpResolver, ) -> Result> { - use crate::{ - crypto::cose::CertificateTrustPolicy, settings::Settings, - status_tracker::StatusTracker, Error, - }; - let body = crate::crypto::time_stamp::default_rfc3161_message(message)?; let headers = None; let bytes = if _sync { crate::crypto::time_stamp::default_rfc3161_request( - tsa_url, + time_authority_url, headers, &body, message, @@ -77,7 +113,7 @@ impl TimeStamp { ) } else { crate::crypto::time_stamp::default_rfc3161_request_async( - tsa_url, + time_authority_url, headers, &body, message, diff --git a/sdk/src/builder.rs b/sdk/src/builder.rs index a546feafd..10c7f5686 100644 --- a/sdk/src/builder.rs +++ b/sdk/src/builder.rs @@ -32,8 +32,8 @@ use crate::{ assertion::{AssertionBase, AssertionDecodeError}, assertions::{ c2pa_action, labels, Action, ActionTemplate, Actions, AssertionMetadata, BmffHash, BoxHash, - DataHash, DigitalSourceType, EmbeddedData, Exif, Metadata, SoftwareAgent, Thumbnail, User, - UserCbor, + DataHash, DigitalSourceType, EmbeddedData, Exif, Metadata, SoftwareAgent, Thumbnail, + TimeStamp, User, UserCbor, }, claim::Claim, error::{Error, Result}, @@ -1440,22 +1440,41 @@ impl Builder { ) .ok_or(Error::ClaimEncoding)?; - let manifest_ids = vec![parent_claim_id.as_ref()]; - let timestamp_assertion = if _sync { - store.get_timestamp_assertion( - &manifest_ids, + // First check if a timestamp assertion already exists. + let timestamp_assertions = provenance_claim.timestamp_assertions(); + let mut timestamp_assertion = if !timestamp_assertions.is_empty() { + // There can only be one timestamp assertion per the spec. + let timestamp_assertion = + TimeStamp::from_assertion(timestamp_assertions[0].assertion())?; + if timestamp_assertion + .get_timestamp(&parent_claim_id) + .is_some() + { + return Ok(()); + } + + timestamp_assertion + } else { + TimeStamp::new() + }; + + if _sync { + timestamp_assertion.refresh_timestamp( + store, time_authority_url, + &parent_claim_id, http_resolver, - )? + )?; } else { - store - .get_timestamp_assertion_async( - &manifest_ids, + timestamp_assertion + .refresh_timestamp_async( + store, time_authority_url, + &parent_claim_id, http_resolver, ) - .await? - }; + .await?; + } let claim = store.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; claim.add_assertion(×tamp_assertion)?; diff --git a/sdk/src/settings/builder.rs b/sdk/src/settings/builder.rs index f2063051d..9453c7fbf 100644 --- a/sdk/src/settings/builder.rs +++ b/sdk/src/settings/builder.rs @@ -470,7 +470,8 @@ pub struct BuilderSettings { /// Whether to generate a C2PA archive (instead of zip) when writing the manifest builder. /// This will eventually become the default behavior. pub generate_c2pa_archive: Option, - /// Whether to auto-generate a [`TimeStamp`] assertion for update manifests. + /// Whether to auto-generate a [`TimeStamp`] assertion for parent of the update manifest if one does not + /// exist already. /// /// Useful when a manifest was signed offline and you want to attach a trusted timestamp to it later. /// diff --git a/sdk/src/store.rs b/sdk/src/store.rs index 72a70e4db..3e1ca744b 100644 --- a/sdk/src/store.rs +++ b/sdk/src/store.rs @@ -485,7 +485,7 @@ impl Store { placeholder } - fn get_cose_sign1_signature(&self, manifest_id: &str) -> Option> { + pub fn get_cose_sign1_signature(&self, manifest_id: &str) -> Option> { let manifest = self.get_claim(manifest_id)?; let sig = manifest.signature_val(); @@ -498,39 +498,6 @@ impl Store { Some(sign1.signature) } - /// Creates a TimeStamp (c2pa.time-stamp) assertion containing the TimeStampTokens for each - /// specified manifest_id. If any time stamp request fails the assertion is not created. - #[async_generic(async_signature( - &self, - manifest_ids: &[&str], - tsa_url: &str, - http_resolver: &impl AsyncHttpResolver - ))] - pub fn get_timestamp_assertion( - &self, - manifest_ids: &[&str], - tsa_url: &str, - http_resolver: &impl SyncHttpResolver, - ) -> Result { - let mut timestamp_assertion = TimeStamp::new(); - for manifest_id in manifest_ids { - // lets add a timestamp for old manifest - let signature = self - .get_cose_sign1_signature(manifest_id) - .ok_or(Error::ClaimMissingSignatureBox)?; - - let timestamp_token = if _sync { - TimeStamp::send_timestamp_token_request(tsa_url, &signature, http_resolver)? - } else { - TimeStamp::send_timestamp_token_request_async(tsa_url, &signature, http_resolver) - .await? - }; - - timestamp_assertion.add_timestamp(manifest_id, ×tamp_token); - } - Ok(timestamp_assertion) - } - /// Return OCSP info if available // Currently only called from manifest_store behind a feature flag but this is allowable // anywhere so allow dead code here for future uses to compile From 7cbf4ca7288de8ac96e4ef1b1535817b061676f0 Mon Sep 17 00:00:00 2001 From: ok-nick Date: Wed, 19 Nov 2025 14:37:23 -0500 Subject: [PATCH 11/15] fix: replace existing timestamp assertion rather than adding another --- sdk/src/builder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/src/builder.rs b/sdk/src/builder.rs index 10c7f5686..cf340c772 100644 --- a/sdk/src/builder.rs +++ b/sdk/src/builder.rs @@ -1477,7 +1477,7 @@ impl Builder { } let claim = store.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; - claim.add_assertion(×tamp_assertion)?; + claim.replace_assertion(timestamp_assertion.to_assertion()?)?; } } From c853bbb8b45c6c4c4d79e59476473c0424d6b2cc Mon Sep 17 00:00:00 2001 From: ok-nick Date: Wed, 19 Nov 2025 14:45:56 -0500 Subject: [PATCH 12/15] fix: add_assertion if it doesn't already exist instead of replace --- sdk/src/builder.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sdk/src/builder.rs b/sdk/src/builder.rs index cf340c772..968c64f0d 100644 --- a/sdk/src/builder.rs +++ b/sdk/src/builder.rs @@ -1477,7 +1477,11 @@ impl Builder { } let claim = store.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; - claim.replace_assertion(timestamp_assertion.to_assertion()?)?; + if claim.timestamp_assertions().is_empty() { + claim.add_assertion(×tamp_assertion)?; + } else { + claim.replace_assertion(timestamp_assertion.to_assertion()?)?; + } } } From 8ef0806a137bdfacb30eda2a956424439f014b2c Mon Sep 17 00:00:00 2001 From: ok-nick Date: Fri, 21 Nov 2025 10:09:44 -0500 Subject: [PATCH 13/15] fix: simplify timestamp function signatures, add docs, and add more context to errors --- sdk/src/assertions/timestamp.rs | 75 ++++++++++++----------- sdk/src/builder.rs | 37 ++++++----- sdk/src/crypto/cose/sigtst.rs | 17 ++++- sdk/src/crypto/time_stamp/http_request.rs | 17 ++++- sdk/src/crypto/time_stamp/verify.rs | 5 +- sdk/src/store.rs | 39 ++++++------ 6 files changed, 109 insertions(+), 81 deletions(-) diff --git a/sdk/src/assertions/timestamp.rs b/sdk/src/assertions/timestamp.rs index 52b4a35bf..f17ca4e1b 100644 --- a/sdk/src/assertions/timestamp.rs +++ b/sdk/src/assertions/timestamp.rs @@ -23,84 +23,93 @@ use crate::{ crypto::cose::CertificateTrustPolicy, error::Result, http::{AsyncHttpResolver, SyncHttpResolver}, - settings::Settings, status_tracker::StatusTracker, - store::Store, Error, }; -/// Helper class to create Timestamp assertions +/// Helper class to create a `TimeStamp` assertion. +/// +/// #[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq)] pub struct TimeStamp(HashMap); impl TimeStamp { - /// Label prefix for an [`Timestamp`] assertion. + /// Label prefix for an [`TimeStamp`] assertion. /// /// See . pub const LABEL: &'static str = labels::TIMESTAMP; + /// Construct a new, empty [`TimeStamp`] assertion. pub fn new() -> Self { TimeStamp(HashMap::new()) } - // + /// Add a timestamp token for the given manifest id. pub fn add_timestamp(&mut self, manifest_id: &str, timestamp: &[u8]) { self.0 .insert(manifest_id.to_string(), ByteBuf::from(timestamp.to_vec())); } - /// Get the timestamp for a given manifest id + /// Get the timestamp token for a given manifest id. pub fn get_timestamp(&self, manifest_id: &str) -> Option<&[u8]> { self.0.get(manifest_id).map(|buf| buf.as_ref()) } + /// Refresh the timstamp token for a given manifest id. + /// + /// The signature is expected to be the `signature` field of the `COSE_Sign1_Tagged` structure + /// found in the C2PA claim signature box of the manifest corresponding to the `manifest_id`. + // + // The `signature` is normally obtained from [`Store::get_cose_sign1_signature`]. + // + // [`Store::get_cose_sign1_signature`][crate::store::Store::get_cose_sign1_structure]. #[async_generic(async_signature( &mut self, - store: &Store, time_authority_url: &str, manifest_id: &str, + signature: &[u8], http_resolver: &impl AsyncHttpResolver, ))] pub(crate) fn refresh_timestamp( &mut self, - store: &Store, time_authority_url: &str, manifest_id: &str, + signature: &[u8], http_resolver: &impl SyncHttpResolver, - ) -> Result<&[u8]> { - let signature = store - .get_cose_sign1_signature(manifest_id) - .ok_or(Error::ClaimMissingSignatureBox)?; - + ) -> Result<()> { let timestamp_token = if _sync { - TimeStamp::send_timestamp_token_request(time_authority_url, &signature, http_resolver)? + TimeStamp::send_timestamp_token_request(time_authority_url, signature, http_resolver)? } else { TimeStamp::send_timestamp_token_request_async( time_authority_url, - &signature, + signature, http_resolver, ) .await? }; - Ok(self - .0 - .entry(manifest_id.to_owned()) - .or_insert(ByteBuf::from(timestamp_token)) - .as_slice()) + self.0 + .insert(manifest_id.to_owned(), ByteBuf::from(timestamp_token)); + + Ok(()) } + /// Send a timestamp token request to the `time_authority_url` with the given `signature`. + /// + /// This function will verify the structure of the returned response but not the trust. + /// + /// See [`TimeStamp::refresh_timestamp`] for more information. #[async_generic(async_signature( time_authority_url: &str, - message: &[u8], + signature: &[u8], http_resolver: &impl AsyncHttpResolver, ))] pub(crate) fn send_timestamp_token_request( time_authority_url: &str, - message: &[u8], + signature: &[u8], http_resolver: &impl SyncHttpResolver, ) -> Result> { - let body = crate::crypto::time_stamp::default_rfc3161_message(message)?; + let body = crate::crypto::time_stamp::default_rfc3161_message(signature)?; let headers = None; let bytes = if _sync { @@ -108,7 +117,7 @@ impl TimeStamp { time_authority_url, headers, &body, - message, + signature, http_resolver, ) } else { @@ -116,37 +125,31 @@ impl TimeStamp { time_authority_url, headers, &body, - message, + signature, http_resolver, ) .await } - .map_err(|_e| Error::OtherError("timestamp token not found".into()))?; + .map_err(|err| Error::OtherError(format!("timestamp token not found: {err:?}").into()))?; // make sure it is a good response let ctp = CertificateTrustPolicy::passthrough(); let mut tracker = StatusTracker::default(); - - // TODO: separate verifying time stamp and verifying time stamp trust into separate functions? - // do we need to pass settings here at all if `ctp` is set to pasthrough anyways? - let mut settings = Settings::default(); - settings.verify.verify_timestamp_trust = false; - if _sync { crate::crypto::time_stamp::verify_time_stamp( &bytes, - message, + signature, &ctp, &mut tracker, - &settings, + false, )?; } else { crate::crypto::time_stamp::verify_time_stamp_async( &bytes, - message, + signature, &ctp, &mut tracker, - &settings, + false, ) .await?; } diff --git a/sdk/src/builder.rs b/sdk/src/builder.rs index 968c64f0d..913ff9dfa 100644 --- a/sdk/src/builder.rs +++ b/sdk/src/builder.rs @@ -1458,22 +1458,27 @@ impl Builder { TimeStamp::new() }; - if _sync { - timestamp_assertion.refresh_timestamp( - store, - time_authority_url, - &parent_claim_id, - http_resolver, - )?; - } else { - timestamp_assertion - .refresh_timestamp_async( - store, - time_authority_url, - &parent_claim_id, - http_resolver, - ) - .await?; + match store.get_cose_sign1_signature(&parent_claim_id)? { + Some(signature) => { + if _sync { + timestamp_assertion.refresh_timestamp( + time_authority_url, + &parent_claim_id, + &signature, + http_resolver, + )?; + } else { + timestamp_assertion + .refresh_timestamp_async( + time_authority_url, + &parent_claim_id, + &signature, + http_resolver, + ) + .await?; + } + } + None => return Err(Error::ClaimMissingSignatureBox), } let claim = store.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; diff --git a/sdk/src/crypto/cose/sigtst.rs b/sdk/src/crypto/cose/sigtst.rs index 88cf64387..923f6dee6 100644 --- a/sdk/src/crypto/cose/sigtst.rs +++ b/sdk/src/crypto/cose/sigtst.rs @@ -144,9 +144,22 @@ pub(crate) fn parse_and_validate_sigtst( let tbs = cose_countersign_data(data, p_header); let tst_info_res = if _sync { - verify_time_stamp(&token.val, &tbs, ctp, validation_log, settings) + verify_time_stamp( + &token.val, + &tbs, + ctp, + validation_log, + settings.verify.verify_timestamp_trust, + ) } else { - verify_time_stamp_async(&token.val, &tbs, ctp, validation_log, settings).await + verify_time_stamp_async( + &token.val, + &tbs, + ctp, + validation_log, + settings.verify.verify_timestamp_trust, + ) + .await }; if let Ok(tst_info) = tst_info_res { diff --git a/sdk/src/crypto/time_stamp/http_request.rs b/sdk/src/crypto/time_stamp/http_request.rs index 3d234aa3e..dfab2386e 100644 --- a/sdk/src/crypto/time_stamp/http_request.rs +++ b/sdk/src/crypto/time_stamp/http_request.rs @@ -75,9 +75,22 @@ pub fn default_rfc3161_request( // Make sure the time stamp is valid before we return it. if _sync { - verify_time_stamp(&ts, message, &ctp, &mut local_log, &settings)?; + verify_time_stamp( + &ts, + message, + &ctp, + &mut local_log, + settings.verify.verify_timestamp_trust, + )?; } else { - verify_time_stamp_async(&ts, message, &ctp, &mut local_log, &settings).await?; + verify_time_stamp_async( + &ts, + message, + &ctp, + &mut local_log, + settings.verify.verify_timestamp_trust, + ) + .await?; } Ok(ts) diff --git a/sdk/src/crypto/time_stamp/verify.rs b/sdk/src/crypto/time_stamp/verify.rs index b373b0ab2..2d8a00646 100644 --- a/sdk/src/crypto/time_stamp/verify.rs +++ b/sdk/src/crypto/time_stamp/verify.rs @@ -35,7 +35,6 @@ use crate::{ }, }, log_item, - settings::Settings, status_tracker::StatusTracker, validation_status::{ TIMESTAMP_MALFORMED, TIMESTAMP_MISMATCH, TIMESTAMP_OUTSIDE_VALIDITY, TIMESTAMP_TRUSTED, @@ -65,7 +64,7 @@ pub fn verify_time_stamp( data: &[u8], ctp: &CertificateTrustPolicy, validation_log: &mut StatusTracker, - settings: &Settings, + verify_trust: bool, ) -> Result { // Get the signed data frorm the timestamp data let Ok(Some(sd)) = signed_data_from_time_stamp_response(ts) else { @@ -531,8 +530,6 @@ pub fn verify_time_stamp( } // the certificate must be on the trust list to be considered valid - let verify_trust = settings.verify.verify_timestamp_trust; - if verify_trust { // per the spec TSA trust can only be checked against the system trust list not the user trust list let mut adjusted_ctp = ctp.clone(); diff --git a/sdk/src/store.rs b/sdk/src/store.rs index 3e1ca744b..25176fa8b 100644 --- a/sdk/src/store.rs +++ b/sdk/src/store.rs @@ -485,17 +485,22 @@ impl Store { placeholder } - pub fn get_cose_sign1_signature(&self, manifest_id: &str) -> Option> { - let manifest = self.get_claim(manifest_id)?; - - let sig = manifest.signature_val(); - let data = manifest.data().ok()?; - let mut validation_log = - StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError); - - let sign1 = parse_cose_sign1(sig, &data, &mut validation_log).ok()?; + /// Return the COSE Sign1 signature found in the claim signature of the given `manifest_id`. + /// + /// This function will return `None` if there is no claim corresponding to the `manifest_id`. + pub fn get_cose_sign1_signature(&self, manifest_id: &str) -> Result>> { + match self.get_claim(manifest_id) { + Some(claim) => { + let sig = claim.signature_val(); + let data = claim.data()?; + let mut validation_log = + StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError); - Some(sign1.signature) + let sign1 = parse_cose_sign1(sig, &data, &mut validation_log)?; + Ok(Some(sign1.signature)) + } + None => Ok(None), + } } /// Return OCSP info if available @@ -1969,7 +1974,6 @@ impl Store { claim: &'a Claim, asset_data: &mut ClaimAssetData<'_>, validation_log: &mut StatusTracker, - settings: &Settings, ) -> Result> { let mut svi = StoreValidationInfo::default(); Store::get_claim_referenced_manifests(claim, self, &mut svi, true, validation_log)?; @@ -2054,26 +2058,19 @@ impl Store { // save the valid timestamps stored in the StoreValidationInfo // we only use valid timestamps, otherwise just ignore - let mut adjusted_settings = settings.clone(); - let original_trust_val = adjusted_settings.verify.verify_timestamp_trust; for (referenced_claim, time_stamp_token) in timestamp_assertion.as_ref() { if let Some(rc) = svi.manifest_map.get(referenced_claim) { - if rc.version() == 1 { - // no trust checks for leagacy timestamps - adjusted_settings.verify.verify_timestamp_trust = false; - } - if let Ok(tst_info) = verify_time_stamp( time_stamp_token, rc.signature_val(), &self.ctp, validation_log, - &adjusted_settings, + // no trust checks for leagacy timestamps + rc.version() != 1, ) { svi.timestamps.insert(rc.label().to_owned(), tst_info); } } - adjusted_settings.verify.verify_timestamp_trust = original_trust_val; } } @@ -2133,7 +2130,7 @@ impl Store { }; // get info needed to complete validation - let svi = store.get_store_validation_info(claim, asset_data, validation_log, settings)?; + let svi = store.get_store_validation_info(claim, asset_data, validation_log)?; if _sync { // verify the provenance claim From e75c94bf3b29197393c92c13897d7a2e3e73a9c5 Mon Sep 17 00:00:00 2001 From: ok-nick Date: Fri, 21 Nov 2025 10:22:15 -0500 Subject: [PATCH 14/15] fix: add more context to timestamp errors --- sdk/src/assertions/timestamp.rs | 6 ++++-- sdk/src/crypto/cose/sigtst.rs | 33 ++++++++++++++++++++++----------- sdk/src/store.rs | 5 +++-- 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/sdk/src/assertions/timestamp.rs b/sdk/src/assertions/timestamp.rs index f17ca4e1b..34298dcb1 100644 --- a/sdk/src/assertions/timestamp.rs +++ b/sdk/src/assertions/timestamp.rs @@ -154,8 +154,10 @@ impl TimeStamp { .await?; } - let token = crate::crypto::cose::timestamptoken_from_timestamprsp(&bytes) - .ok_or(Error::OtherError("timestamp token not found".into()))?; + let token = + crate::crypto::cose::timestamptoken_from_timestamprsp(&bytes).map_err(|err| { + Error::OtherError(format!("timestamp token not found: {err:?}").into()) + })?; Ok(token) } diff --git a/sdk/src/crypto/cose/sigtst.rs b/sdk/src/crypto/cose/sigtst.rs index 923f6dee6..74bfc9d96 100644 --- a/sdk/src/crypto/cose/sigtst.rs +++ b/sdk/src/crypto/cose/sigtst.rs @@ -29,7 +29,7 @@ use crate::{ log_item, settings::Settings, status_tracker::StatusTracker, - validation_status, + validation_status, Result, }; /// Given a COSE signature, retrieve the `sigTst` header from it and validate @@ -245,9 +245,11 @@ pub(crate) fn add_sigtst_header( if tss == TimeStampStorage::V2_sigTst2_CTT { // In `sigTst2`, we use only the `TimeStampToken` and not `TimeStampRsp` for // sigTst2 - cts = timestamptoken_from_timestamprsp(&cts).ok_or(CoseError::CborGenerationError( - "unable to generate time stamp token".to_string(), - ))?; + cts = timestamptoken_from_timestamprsp(&cts).map_err(|err| { + CoseError::CborGenerationError(format!( + "unable to generate time stamp token: {err:?}" + )) + })?; } let cts = make_cose_timestamp(&cts); @@ -285,26 +287,35 @@ fn make_cose_timestamp(ts_data: &[u8]) -> TstContainer { } /// Return DER encoded TimeStampToken used by sigTst2 from TimeStampResponse. -pub fn timestamptoken_from_timestamprsp(ts: &[u8]) -> Option> { +pub fn timestamptoken_from_timestamprsp(ts: &[u8]) -> Result> { let ts_resp = TimeStampResponse( - Constructed::decode(ts, bcder::Mode::Der, TimeStampResp::take_from).ok()?, + Constructed::decode(ts, bcder::Mode::Der, TimeStampResp::take_from).map_err(|err| { + CoseError::InternalError(format!("invalid timestamp response: {err:?}")) + })?, ); - let tst = ts_resp.0.time_stamp_token?; + let tst = ts_resp + .0 + .time_stamp_token + .ok_or_else(|| CoseError::InternalError(format!("invalid timestamp token")))?; - let a: Result, CoseError> = tst + let a = tst .content_type .iter() .map(|v| { v.to_u32() .ok_or(CoseError::InternalError("invalid component".to_string())) }) - .collect(); + .collect::, CoseError>>()?; let ci = ContentInfo { - content_type: rasn::types::ObjectIdentifier::new(a.ok()?)?, + content_type: rasn::types::ObjectIdentifier::new(a).ok_or(CoseError::InternalError( + "invalid object identifier for timestamp response".to_string(), + ))?, content: rasn::types::Any::new(tst.content.as_bytes().to_vec()), }; - rasn::der::encode(&ci).ok() + Ok(rasn::der::encode(&ci).map_err(|err| { + CoseError::InternalError(format!("failed to encode timestamp token: {err:?}")) + })?) } diff --git a/sdk/src/store.rs b/sdk/src/store.rs index 25176fa8b..3f520aa75 100644 --- a/sdk/src/store.rs +++ b/sdk/src/store.rs @@ -485,9 +485,10 @@ impl Store { placeholder } - /// Return the COSE Sign1 signature found in the claim signature of the given `manifest_id`. + /// Returns the `signature` field of the `COSE_Sign1_Tagged` structure found in the claim signature + /// box of the manifest corresponding to the `manifest_id`. /// - /// This function will return `None` if there is no claim corresponding to the `manifest_id`. + /// This function will return `Ok(None)` if there is no claim corresponding to the `manifest_id`. pub fn get_cose_sign1_signature(&self, manifest_id: &str) -> Result>> { match self.get_claim(manifest_id) { Some(claim) => { From d35960e3dbd8e32c707f01987515c6744af598c3 Mon Sep 17 00:00:00 2001 From: ok-nick Date: Fri, 21 Nov 2025 10:26:57 -0500 Subject: [PATCH 15/15] style: fix clippy lints --- sdk/src/crypto/cose/sigtst.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/src/crypto/cose/sigtst.rs b/sdk/src/crypto/cose/sigtst.rs index 74bfc9d96..88c074b7a 100644 --- a/sdk/src/crypto/cose/sigtst.rs +++ b/sdk/src/crypto/cose/sigtst.rs @@ -297,7 +297,7 @@ pub fn timestamptoken_from_timestamprsp(ts: &[u8]) -> Result> { let tst = ts_resp .0 .time_stamp_token - .ok_or_else(|| CoseError::InternalError(format!("invalid timestamp token")))?; + .ok_or_else(|| CoseError::InternalError("invalid timestamp token".to_string()))?; let a = tst .content_type