From f1f356b7624a6eb31db7a17a14abd7ba868fedfb Mon Sep 17 00:00:00 2001 From: Eric Scouten <scouten@adobe.com> Date: Mon, 23 Dec 2024 14:44:27 -0800 Subject: [PATCH] feat: Introduce `c2pa_crypto::cose::Verifier` (#797) --- internal/crypto/src/cose/mod.rs | 3 + internal/crypto/src/cose/verify.rs | 120 +++++++++++++++++++++++++++++ sdk/src/cose_validator.rs | 105 +++++++------------------ sdk/src/store.rs | 4 +- 4 files changed, 151 insertions(+), 81 deletions(-) create mode 100644 internal/crypto/src/cose/verify.rs diff --git a/internal/crypto/src/cose/mod.rs b/internal/crypto/src/cose/mod.rs index 85b2ab3c0..a89aaeeb5 100644 --- a/internal/crypto/src/cose/mod.rs +++ b/internal/crypto/src/cose/mod.rs @@ -37,3 +37,6 @@ pub use sigtst::{ cose_countersign_data, parse_and_validate_sigtst, parse_and_validate_sigtst_async, validate_cose_tst_info, validate_cose_tst_info_async, TstToken, }; + +mod verify; +pub use verify::Verifier; diff --git a/internal/crypto/src/cose/verify.rs b/internal/crypto/src/cose/verify.rs new file mode 100644 index 000000000..e872260d2 --- /dev/null +++ b/internal/crypto/src/cose/verify.rs @@ -0,0 +1,120 @@ +// Copyright 2022 Adobe. All rights reserved. +// This file is licensed to you under the Apache License, +// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) +// or the MIT license (http://opensource.org/licenses/MIT), +// at your option. + +// Unless required by applicable law or agreed to in writing, +// this software is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or +// implied. See the LICENSE-MIT and LICENSE-APACHE files for the +// specific language governing permissions and limitations under +// each license. + +use async_generic::async_generic; +use c2pa_status_tracker::{ + log_item, + validation_codes::{TIMESTAMP_MISMATCH, TIMESTAMP_OUTSIDE_VALIDITY}, + StatusTracker, +}; +use coset::CoseSign1; + +use crate::{ + asn1::rfc3161::TstInfo, + cose::{cert_chain_from_sign1, check_certificate_profile, CertificateTrustPolicy, CoseError}, + time_stamp::TimeStampError, +}; + +/// A `Verifier` reads a COSE signature and reports on its validity. +/// +/// It can provide different levels of verification depending on the enum value +/// chosen. +#[derive(Debug)] +pub enum Verifier<'a> { + /// Use a [`CertificateTrustPolicy`] to validate the signing certificate's + /// profile against C2PA requirements _and_ validate the certificate's + /// membership against a trust configuration. + VerifyTrustPolicy(&'a CertificateTrustPolicy), + + /// Validate the certificate's membership against a trust configuration, but + /// do not against any trust list. The [`CertificateTrustPolicy`] is used to + /// enforce EKU (Extended Key Usage) policy only. + VerifyCertificateProfileOnly(&'a CertificateTrustPolicy), + + /// Ignore both trust configuration and trust lists. + IgnoreProfileAndTrustPolicy, +} + +impl Verifier<'_> { + /// Verify certificate profile if so configured. + /// + /// TO DO: This might not need to be public after refactoring. + #[async_generic] + pub fn verify_profile( + &self, + sign1: &CoseSign1, + tst_info_res: &Result<TstInfo, CoseError>, + validation_log: &mut impl StatusTracker, + ) -> Result<(), CoseError> { + let ctp = match self { + Self::VerifyTrustPolicy(ctp) => *ctp, + Self::VerifyCertificateProfileOnly(ctp) => *ctp, + Self::IgnoreProfileAndTrustPolicy => { + return Ok(()); + } + }; + + let certs = cert_chain_from_sign1(sign1)?; + let end_entity_cert_der = &certs[0]; + + match tst_info_res { + Ok(tst_info) => Ok(check_certificate_profile( + end_entity_cert_der, + ctp, + validation_log, + Some(tst_info), + )?), + + Err(CoseError::NoTimeStampToken) => Ok(check_certificate_profile( + end_entity_cert_der, + ctp, + validation_log, + None, + )?), + + Err(CoseError::TimeStampError(TimeStampError::InvalidData)) => { + log_item!( + "Cose_Sign1", + "timestamp did not match signed data", + "verify_cose" + ) + .validation_status(TIMESTAMP_MISMATCH) + .failure_no_throw(validation_log, TimeStampError::InvalidData); + + Err(TimeStampError::InvalidData.into()) + } + + Err(CoseError::TimeStampError(TimeStampError::ExpiredCertificate)) => { + log_item!( + "Cose_Sign1", + "timestamp certificate outside of validity", + "verify_cose" + ) + .validation_status(TIMESTAMP_OUTSIDE_VALIDITY) + .failure_no_throw(validation_log, TimeStampError::ExpiredCertificate); + + Err(TimeStampError::ExpiredCertificate.into()) + } + + Err(e) => { + log_item!("Cose_Sign1", "error parsing timestamp", "verify_cose") + .failure_no_throw(validation_log, e); + + // Frustratingly, we can't clone CoseError. The likely cases are already handled + // above, so we'll call this an internal error. + + Err(CoseError::InternalError(e.to_string())) + } + } + } +} diff --git a/sdk/src/cose_validator.rs b/sdk/src/cose_validator.rs index 3accd72e1..2d40eaa0c 100644 --- a/sdk/src/cose_validator.rs +++ b/sdk/src/cose_validator.rs @@ -17,14 +17,13 @@ use async_generic::async_generic; use c2pa_crypto::{ asn1::rfc3161::TstInfo, cose::{ - cert_chain_from_sign1, check_certificate_profile, parse_cose_sign1, signing_alg_from_sign1, - validate_cose_tst_info, validate_cose_tst_info_async, CertificateTrustError, - CertificateTrustPolicy, CoseError, OcspFetchPolicy, + cert_chain_from_sign1, parse_cose_sign1, signing_alg_from_sign1, validate_cose_tst_info, + validate_cose_tst_info_async, CertificateTrustError, CertificateTrustPolicy, + OcspFetchPolicy, Verifier, }, ocsp::OcspResponse, p1363::parse_ec_der_sig, raw_signature::{validator_for_signing_alg, RawSignatureValidator}, - time_stamp::TimeStampError, SigningAlg, ValidationInfo, }; use c2pa_status_tracker::{log_item, validation_codes::*, StatusTracker}; @@ -255,42 +254,21 @@ pub(crate) async fn verify_cose_async( let tst_info_res = validate_cose_tst_info_async(&sign1, &data).await; - // verify cert matches requested algorithm - if cert_check { - // verify certs - match &tst_info_res { - Ok(tst_info) => { - check_certificate_profile(der_bytes, ctp, validation_log, Some(tst_info))? - } - - Err(CoseError::NoTimeStampToken) => { - check_certificate_profile(der_bytes, ctp, validation_log, None)? - } - - Err(CoseError::TimeStampError(TimeStampError::InvalidData)) => { - log_item!( - "Cose_Sign1", - "timestamp message imprint did not match", - "verify_cose" - ) - .validation_status(TIMESTAMP_MISMATCH) - .failure(validation_log, Error::CoseTimeStampMismatch)?; - } - - Err(CoseError::TimeStampError(TimeStampError::ExpiredCertificate)) => { - log_item!("Cose_Sign1", "timestamp outside of validity", "verify_cose") - .validation_status(TIMESTAMP_OUTSIDE_VALIDITY) - .failure(validation_log, Error::CoseTimeStampValidity)?; - } - - _ => { - log_item!("Cose_Sign1", "error parsing timestamp", "verify_cose") - .failure_no_throw(validation_log, Error::CoseInvalidTimeStamp); - - return Err(Error::CoseInvalidTimeStamp); - } + let verifier = if cert_check { + match get_settings_value::<bool>("verify.verify_trust") { + Ok(true) => Verifier::VerifyTrustPolicy(ctp), + _ => Verifier::VerifyCertificateProfileOnly(ctp), } + } else { + Verifier::IgnoreProfileAndTrustPolicy + }; + verifier + .verify_profile_async(&sign1, &tst_info_res, validation_log) + .await?; + + // verify cert matches requested algorithm + if cert_check { // is the certificate trusted #[cfg(target_arch = "wasm32")] check_trust_async( @@ -461,49 +439,18 @@ pub(crate) fn verify_cose( let tst_info_res = validate_cose_tst_info(&sign1, data); - if cert_check { - // verify certs - match &tst_info_res { - Ok(tst_info) => { - check_certificate_profile(der_bytes, ctp, validation_log, Some(tst_info))? - } - - Err(CoseError::NoTimeStampToken) => { - check_certificate_profile(der_bytes, ctp, validation_log, None)? - } - - Err(CoseError::TimeStampError(TimeStampError::InvalidData)) => { - log_item!( - "Cose_Sign1", - "timestamp did not match signed data", - "verify_cose" - ) - .validation_status(TIMESTAMP_MISMATCH) - .failure_no_throw(validation_log, Error::CoseTimeStampMismatch); - - return Err(Error::CoseTimeStampMismatch); - } - - Err(CoseError::TimeStampError(TimeStampError::ExpiredCertificate)) => { - log_item!( - "Cose_Sign1", - "timestamp certificate outside of validity", - "verify_cose" - ) - .validation_status(TIMESTAMP_OUTSIDE_VALIDITY) - .failure_no_throw(validation_log, Error::CoseTimeStampValidity); - - return Err(Error::CoseTimeStampValidity); - } - - _ => { - log_item!("Cose_Sign1", "error parsing timestamp", "verify_cose") - .failure_no_throw(validation_log, Error::CoseInvalidTimeStamp); - - return Err(Error::CoseInvalidTimeStamp); - } + let verifier = if cert_check { + match get_settings_value::<bool>("verify.verify_trust") { + Ok(true) => Verifier::VerifyTrustPolicy(ctp), + _ => Verifier::VerifyCertificateProfileOnly(ctp), } + } else { + Verifier::IgnoreProfileAndTrustPolicy + }; + verifier.verify_profile(&sign1, &tst_info_res, validation_log)?; + + if cert_check { // is the certificate trusted check_trust( ctp, diff --git a/sdk/src/store.rs b/sdk/src/store.rs index a10194a5a..d994b3d06 100644 --- a/sdk/src/store.rs +++ b/sdk/src/store.rs @@ -3595,7 +3595,7 @@ pub mod tests { use std::io::Write; - use c2pa_crypto::SigningAlg; + use c2pa_crypto::{time_stamp::TimeStampError, SigningAlg}; use c2pa_status_tracker::StatusTracker; use memchr::memmem; use serde::Serialize; @@ -4895,7 +4895,7 @@ pub mod tests { // replace the title that is inside the claim data - should cause signature to not match let report = patch_and_report("C.jpg", b"C.jpg", b"X.jpg"); assert!(!report.logged_items().is_empty()); - assert!(report.has_error(Error::CoseTimeStampMismatch)); + assert!(report.has_error(TimeStampError::InvalidData)); assert!(report.has_status(validation_status::TIMESTAMP_MISMATCH)); }