use std::io::Write;
use asn1_rs::FromDer;
use async_generic::async_generic;
use c2pa_status_tracker::{
log_item,
validation_codes::{
ALGORITHM_UNSUPPORTED, SIGNING_CREDENTIAL_INVALID, SIGNING_CREDENTIAL_TRUSTED,
SIGNING_CREDENTIAL_UNTRUSTED, TIMESTAMP_MISMATCH, TIMESTAMP_OUTSIDE_VALIDITY,
},
StatusTracker,
};
use coset::CoseSign1;
use x509_parser::prelude::X509Certificate;
use crate::{
asn1::rfc3161::TstInfo,
base64::encode,
cose::{
cert_chain_from_sign1, check_certificate_profile, parse_cose_sign1, signing_alg_from_sign1,
validate_cose_tst_info, validate_cose_tst_info_async, CertificateInfo,
CertificateTrustError, CertificateTrustPolicy, CoseError,
},
ec_utils::parse_ec_der_sig,
raw_signature::{validator_for_signing_alg, SigningAlg},
time_stamp::TimeStampError,
};
#[derive(Debug)]
#[non_exhaustive]
pub enum Verifier<'a> {
VerifyTrustPolicy(&'a CertificateTrustPolicy),
VerifyCertificateProfileOnly(&'a CertificateTrustPolicy),
IgnoreProfileAndTrustPolicy,
}
impl Verifier<'_> {
#[async_generic]
pub fn verify_signature(
&self,
cose_sign1: &[u8],
data: &[u8],
additional_data: &[u8],
validation_log: &mut impl StatusTracker,
) -> Result<CertificateInfo, CoseError> {
let mut sign1 = parse_cose_sign1(cose_sign1, data, validation_log)?;
let Ok(alg) = signing_alg_from_sign1(&sign1) else {
log_item!(
"Cose_Sign1",
"unsupported or missing Cose algorithm",
"verify_cose"
)
.validation_status(ALGORITHM_UNSUPPORTED)
.failure_no_throw(validation_log, CoseError::UnsupportedSigningAlgorithm);
return Err(CoseError::UnsupportedSigningAlgorithm);
};
let tst_info_res = if _sync {
validate_cose_tst_info(&sign1, data)
} else {
validate_cose_tst_info_async(&sign1, data).await
};
match alg {
SigningAlg::Es256 | SigningAlg::Es384 | SigningAlg::Es512 => {
if parse_ec_der_sig(&sign1.signature).is_ok() {
log_item!(
"Cose_Sign1",
"unsupported signature format (EC signature should be in P1363 r|s format)",
"verify_cose"
)
.validation_status(SIGNING_CREDENTIAL_INVALID)
.failure_no_throw(validation_log, CoseError::InvalidEcdsaSignature);
return Err(CoseError::InvalidEcdsaSignature);
}
}
_ => (),
}
if _sync {
self.verify_profile(&sign1, &tst_info_res, validation_log)?;
self.verify_trust(&sign1, &tst_info_res, validation_log)?;
} else {
self.verify_profile_async(&sign1, &tst_info_res, validation_log)
.await?;
self.verify_trust_async(&sign1, &tst_info_res, validation_log)
.await?;
}
sign1.payload = Some(data.to_vec());
let tbs = sign1.tbs_data(additional_data);
let certs = cert_chain_from_sign1(&sign1)?;
let end_entity_cert_der = &certs[0];
let (_rem, sign_cert) = X509Certificate::from_der(end_entity_cert_der)
.map_err(|_| CoseError::CborParsingError("invalid X509 certificate".to_string()))?;
let pk = sign_cert.public_key();
let pk_der = pk.raw;
#[allow(unused_mut)] let mut validated = false;
if _async {
#[cfg(target_arch = "wasm32")]
if let Some(validator) = crate::raw_signature::async_validator_for_signing_alg(alg) {
validator
.validate_async(&sign1.signature, &tbs, pk_der)
.await?;
validated = true;
}
}
if !validated {
let Some(validator) = validator_for_signing_alg(alg) else {
return Err(CoseError::UnsupportedSigningAlgorithm);
};
validator.validate(&sign1.signature, &tbs, pk_der)?;
}
let subject = sign_cert
.subject()
.iter_organization()
.map(|attr| attr.as_str())
.last()
.ok_or(CoseError::MissingSigningCertificateChain)?
.map(|attr| attr.to_string())
.map_err(|_| CoseError::MissingSigningCertificateChain)?;
Ok(CertificateInfo {
alg: Some(alg),
date: tst_info_res.map(|t| t.gen_time.into()).ok(),
cert_serial_number: Some(sign_cert.serial.clone()),
issuer_org: Some(subject),
validated: true,
cert_chain: dump_cert_chain(&certs)?,
revocation_status: Some(true),
})
}
#[async_generic]
pub(crate) 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);
Err(CoseError::InternalError(e.to_string()))
}
}
}
#[async_generic]
pub(crate) fn verify_trust(
&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) => {
return Ok(());
}
Self::IgnoreProfileAndTrustPolicy => {
return Ok(());
}
};
let certs = cert_chain_from_sign1(sign1)?;
let end_entity_cert_der = &certs[0];
let chain_der = &certs[1..];
let signing_time_epoch = tst_info_res.as_ref().ok().map(|tst_info| {
let dt: chrono::DateTime<chrono::Utc> = tst_info.gen_time.clone().into();
dt.timestamp()
});
let verify_result = if _sync {
ctp.check_certificate_trust(chain_der, end_entity_cert_der, signing_time_epoch)
} else {
ctp.check_certificate_trust_async(chain_der, end_entity_cert_der, signing_time_epoch)
.await
};
match verify_result {
Ok(()) => {
log_item!("Cose_Sign1", "signing certificate trusted", "verify_cose")
.validation_status(SIGNING_CREDENTIAL_TRUSTED)
.success(validation_log);
Ok(())
}
Err(CertificateTrustError::CertificateNotTrusted) => {
log_item!("Cose_Sign1", "signing certificate untrusted", "verify_cose")
.validation_status(SIGNING_CREDENTIAL_UNTRUSTED)
.failure_no_throw(validation_log, CertificateTrustError::CertificateNotTrusted);
Err(CertificateTrustError::CertificateNotTrusted.into())
}
Err(e) => {
log_item!("Cose_Sign1", "signing certificate untrusted", "verify_cose")
.validation_status(SIGNING_CREDENTIAL_UNTRUSTED)
.failure_no_throw(validation_log, &e);
Err(e.into())
}
}
}
}
fn dump_cert_chain(certs: &[Vec<u8>]) -> Result<Vec<u8>, CoseError> {
let mut writer = Vec::new();
let line_len = 64;
let cert_begin = "-----BEGIN CERTIFICATE-----";
let cert_end = "-----END CERTIFICATE-----";
for der_bytes in certs {
let cert_base_str = encode(der_bytes);
let cert_lines = cert_base_str
.chars()
.collect::<Vec<char>>()
.chunks(line_len)
.map(|chunk| chunk.iter().collect::<String>())
.collect::<Vec<_>>();
writer
.write_fmt(format_args!("{}\n", cert_begin))
.map_err(|_e| CoseError::InternalError("could not write PEM".to_string()))?;
for l in cert_lines {
writer
.write_fmt(format_args!("{}\n", l))
.map_err(|_e| CoseError::InternalError("could not write PEM".to_string()))?;
}
writer
.write_fmt(format_args!("{}\n", cert_end))
.map_err(|_e| CoseError::InternalError("could not write PEM".to_string()))?;
}
Ok(writer)
}