Skip to content

Commit

Permalink
feat: Introduce c2pa_crypto::cose::Verifier (#797)
Browse files Browse the repository at this point in the history
  • Loading branch information
scouten-adobe authored Dec 23, 2024
1 parent 5b13bb2 commit f1f356b
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 81 deletions.
3 changes: 3 additions & 0 deletions internal/crypto/src/cose/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
120 changes: 120 additions & 0 deletions internal/crypto/src/cose/verify.rs
Original file line number Diff line number Diff line change
@@ -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()))
}
}
}
}
105 changes: 26 additions & 79 deletions sdk/src/cose_validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions sdk/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
}

Expand Down

0 comments on commit f1f356b

Please sign in to comment.