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..34298dcb1 100644 --- a/sdk/src/assertions/timestamp.rs +++ b/sdk/src/assertions/timestamp.rs @@ -20,118 +20,144 @@ use serde_bytes::ByteBuf; use crate::{ assertion::{Assertion, AssertionBase, AssertionCbor}, assertions::labels, + crypto::cose::CertificateTrustPolicy, error::Result, - http::{AsyncGenericResolver, AsyncHttpResolver, SyncGenericResolver, SyncHttpResolver}, + http::{AsyncHttpResolver, SyncHttpResolver}, + status_tracker::StatusTracker, + 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); -#[allow(dead_code)] 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()) } - #[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()) + /// 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, + time_authority_url: &str, + manifest_id: &str, + signature: &[u8], + http_resolver: &impl AsyncHttpResolver, + ))] + pub(crate) fn refresh_timestamp( + &mut self, + time_authority_url: &str, + manifest_id: &str, + signature: &[u8], + http_resolver: &impl SyncHttpResolver, + ) -> Result<()> { + let timestamp_token = if _sync { + TimeStamp::send_timestamp_token_request(time_authority_url, signature, http_resolver)? } else { - Self::send_timestamp_token_request_impl_async( - tsa_url, - message, - &AsyncGenericResolver::new(), + TimeStamp::send_timestamp_token_request_async( + time_authority_url, + signature, + http_resolver, ) - .await - } + .await? + }; + + 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( - tsa_url: &str, - message: &[u8], + time_authority_url: &str, + signature: &[u8], http_resolver: &impl AsyncHttpResolver, ))] - pub(crate) fn send_timestamp_token_request_impl( - tsa_url: &str, - message: &[u8], + pub(crate) fn send_timestamp_token_request( + time_authority_url: &str, + signature: &[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 body = crate::crypto::time_stamp::default_rfc3161_message(signature)?; let headers = None; let bytes = if _sync { crate::crypto::time_stamp::default_rfc3161_request( - tsa_url, + time_authority_url, headers, &body, - message, + signature, http_resolver, ) } else { crate::crypto::time_stamp::default_rfc3161_request_async( - tsa_url, + 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?; } - 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/builder.rs b/sdk/src/builder.rs index c2b0b1718..913ff9dfa 100644 --- a/sdk/src/builder.rs +++ b/sdk/src/builder.rs @@ -32,12 +32,13 @@ 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}, - http::{AsyncGenericResolver, SyncGenericResolver}, + http::{AsyncGenericResolver, AsyncHttpResolver, SyncGenericResolver, SyncHttpResolver}, + jumbf::labels::manifest_label_from_uri, jumbf_io, resource_store::{ResourceRef, ResourceResolver, ResourceStore}, settings::{self, Settings}, @@ -1355,12 +1356,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, { @@ -1370,7 +1366,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()?; @@ -1380,7 +1376,7 @@ impl Builder { crate::utils::thumbnail::make_thumbnail_bytes_from_stream( format, &mut stream, - settings, + &self.settings, )? { stream.rewind()?; @@ -1422,6 +1418,81 @@ impl Builder { Ok(self) } + #[async_generic(async_signature( + &mut self, + time_authority_url: &str, + store: &mut Store, + http_resolver: &impl AsyncHttpResolver, + ))] + fn maybe_add_timestamp( + &mut self, + time_authority_url: &str, + store: &mut Store, + http_resolver: &impl SyncHttpResolver, + ) -> Result<()> { + if self.settings.builder.update_manifest_timestamp_assertion { + let provenance_claim = store.provenance_claim().ok_or(Error::ClaimEncoding)?; + if provenance_claim.update_manifest() { + let parent_claim_id = manifest_label_from_uri( + &provenance_claim + .parent_claim_uri()? + .ok_or(Error::ClaimEncoding)?, + ) + .ok_or(Error::ClaimEncoding)?; + + // 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() + }; + + 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)?; + if claim.timestamp_assertions().is_empty() { + claim.add_assertion(×tamp_assertion)?; + } else { + claim.replace_assertion(timestamp_assertion.to_assertion()?)?; + } + } + } + + Ok(()) + } + // Find an assertion in the manifest. pub(crate) fn find_assertion(&self, label: &str) -> Result { if let Some(manifest_assertion) = self @@ -1575,7 +1646,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 { @@ -1597,17 +1667,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/claim.rs b/sdk/src/claim.rs index 7759aeadd..f79aaf965 100644 --- a/sdk/src/claim.rs +++ b/sdk/src/claim.rs @@ -3922,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/crypto/cose/sigtst.rs b/sdk/src/crypto/cose/sigtst.rs index 88cf64387..88c074b7a 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 @@ -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 { @@ -232,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); @@ -272,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("invalid timestamp token".to_string()))?; - 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/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 20b2fa196..6aca3fa15 100644 --- a/sdk/src/crypto/time_stamp/verify.rs +++ b/sdk/src/crypto/time_stamp/verify.rs @@ -34,7 +34,6 @@ use crate::{ }, }, log_item, - settings::Settings, status_tracker::StatusTracker, validation_status::{ TIMESTAMP_MALFORMED, TIMESTAMP_MISMATCH, TIMESTAMP_OUTSIDE_VALIDITY, TIMESTAMP_TRUSTED, @@ -64,7 +63,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/settings/builder.rs b/sdk/src/settings/builder.rs index 234e1de05..9453c7fbf 100644 --- a/sdk/src/settings/builder.rs +++ b/sdk/src/settings/builder.rs @@ -467,10 +467,25 @@ 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 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. + /// + /// 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`]. + /// + /// 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 update_manifest_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 f271a062c..e6bf13b59 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..3f520aa75 100644 --- a/sdk/src/store.rs +++ b/sdk/src/store.rs @@ -485,55 +485,23 @@ impl Store { placeholder } - 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()?; - - 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. - #[allow(dead_code)] - #[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_impl(tsa_url, &signature, http_resolver)? - } else { - TimeStamp::send_timestamp_token_request_impl_async( - tsa_url, - &signature, - http_resolver, - ) - .await? - }; + /// 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 `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) => { + let sig = claim.signature_val(); + let data = claim.data()?; + let mut validation_log = + StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError); - timestamp_assertion.add_timestamp(manifest_id, ×tamp_token); + let sign1 = parse_cose_sign1(sig, &data, &mut validation_log)?; + Ok(Some(sign1.signature)) + } + None => Ok(None), } - Ok(timestamp_assertion) } /// Return OCSP info if available @@ -2007,7 +1975,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)?; @@ -2092,26 +2059,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; } } @@ -2171,7 +2131,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 @@ -3946,22 +3906,17 @@ 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_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 } @@ -8849,6 +8804,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"); @@ -8859,10 +8819,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(); diff --git a/sdk/tests/test_builder.rs b/sdk/tests/test_builder.rs index 4bfa0a588..0ac1e341f 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,90 @@ 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 child_manifest_label = reader.active_manifest().unwrap().ingredients()[0] + .active_manifest() + .unwrap(); + assert!(timestamp_assertion + .get_timestamp(child_manifest_label) + .is_some()); +} + #[test] #[cfg(all(feature = "add_thumbnails", feature = "file_io"))] fn test_builder_ca_jpg() -> Result<()> {