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));
     }