use std::{
borrow::Cow,
collections::BTreeMap,
fmt::{Debug, Formatter},
};
use async_generic::async_generic;
use c2pa_raw_crypto::RawSignatureValidationError;
use serde::{Deserialize, Serialize};
use serde_bytes::ByteBuf;
use crate::{
context::Context,
crypto::cose::{parse_cose_sign1, CertificateTrustPolicy, CoseError, Verifier},
dynamic_assertion::PartialClaim,
identity::{
claim_aggregation::IcaSignatureVerifier,
identity_assertion::{
report::{
IdentityAssertionReport, IdentityAssertionsForManifest,
IdentityAssertionsForManifestStore, SignerPayloadReport,
},
signer_payload::SignerPayload,
},
internal::debug_byte_slice::DebugByteSlice,
x509::{X509SignatureInfo, X509StatusRemapGuard},
SignatureVerifier, ToCredentialSummary, ValidationError,
},
jumbf::labels::to_assertion_uri,
log_current_item, log_item,
settings::TrustListKind,
status_tracker::StatusTracker,
validation_status::{CAWG_X509_SIGNATURE_MISMATCH, CAWG_X509_SIGNATURE_VALIDATED},
Manifest, Reader,
};
#[derive(Deserialize, Serialize)]
pub struct IdentityAssertion {
pub(crate) signer_payload: SignerPayload,
#[serde(with = "serde_bytes")]
pub(crate) signature: Vec<u8>,
#[serde(with = "serde_bytes")]
pub(crate) pad1: Vec<u8>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) pad2: Option<ByteBuf>,
#[serde(skip)]
pub(crate) label: Option<String>,
}
#[allow(unused)] impl IdentityAssertion {
pub(crate) fn from_manifest<'a>(
manifest: &'a Manifest,
status_tracker: &'a mut StatusTracker,
) -> impl Iterator<Item = Result<Self, crate::Error>> + use<'a> {
manifest
.assertions()
.iter()
.filter(|a| a.label() == "cawg.identity" || a.label().starts_with("cawg.identity__"))
.map(|a| {
let mut ia: Result<Self, crate::Error> = a.to_assertion();
if let Ok(ref mut ia) = ia {
if let Some(manifest_label) = manifest.label() {
ia.label = Some(to_assertion_uri(manifest_label, a.label()));
}
}
(a.label().to_owned(), ia)
})
.inspect(|(label, r)| {
let mut label = label.to_owned();
if let Err(err) = r {
if let Some(manifest_label) = manifest.label() {
label = to_assertion_uri(manifest_label, &label);
}
log_item!(label, "invalid CBOR", "IdentityAssertion::from_manifest")
.validation_status("cawg.identity.cbor.invalid")
.failure_no_throw(
status_tracker,
crate::Error::AssertionSpecificError(err.to_string()),
);
}
})
.map(move |(_label, r)| r)
}
pub(crate) async fn to_summary<SV: SignatureVerifier>(
&self,
manifest: &Manifest,
status_tracker: &mut StatusTracker,
verifier: &SV,
) -> impl Serialize
where
<SV as SignatureVerifier>::Output: 'static,
{
self.to_summary_impl(manifest, status_tracker, verifier)
.await
}
pub(crate) async fn to_summary_impl<SV: SignatureVerifier>(
&self,
manifest: &Manifest,
status_tracker: &mut StatusTracker,
verifier: &SV,
) -> IdentityAssertionReport<
<<SV as SignatureVerifier>::Output as ToCredentialSummary>::CredentialSummary,
>
where
<SV as SignatureVerifier>::Output: 'static,
{
match self.validate(manifest, status_tracker, verifier).await {
Ok(named_actor) => {
let summary = named_actor.to_summary();
IdentityAssertionReport {
signer_payload: SignerPayloadReport::from_signer_payload(&self.signer_payload),
named_actor: Some(summary),
}
}
Err(_err) => {
todo!("Handle summary report for failure case");
}
}
}
pub(crate) async fn summarize_all<SV: SignatureVerifier>(
manifest: &Manifest,
status_tracker: &mut StatusTracker,
verifier: &SV,
) -> impl Serialize {
Self::summarize_all_impl(manifest, status_tracker, verifier).await
}
pub(crate) async fn summarize_all_impl<SV: SignatureVerifier>(
manifest: &Manifest,
status_tracker: &mut StatusTracker,
verifier: &SV,
) -> IdentityAssertionsForManifest<
<<SV as SignatureVerifier>::Output as ToCredentialSummary>::CredentialSummary,
> {
let mut reports: Vec<
IdentityAssertionReport<
<<SV as SignatureVerifier>::Output as ToCredentialSummary>::CredentialSummary,
>,
> = vec![];
let assertion_results: Vec<Result<IdentityAssertion, crate::Error>> =
Self::from_manifest(manifest, status_tracker).collect();
for assertion in assertion_results {
let report = match assertion {
Ok(assertion) => {
assertion
.to_summary_impl(manifest, status_tracker, verifier)
.await
}
Err(_) => {
todo!("Handle assertion failed to parse case");
}
};
reports.push(report);
}
IdentityAssertionsForManifest::<
<<SV as SignatureVerifier>::Output as ToCredentialSummary>::CredentialSummary,
> {
assertion_reports: reports,
}
}
pub(crate) async fn summarize_from_reader<SV: SignatureVerifier>(
reader: &Reader,
status_tracker: &mut StatusTracker,
verifier: &SV,
) -> impl Serialize {
let mut reports: BTreeMap<
String,
IdentityAssertionsForManifest<
<<SV as SignatureVerifier>::Output as ToCredentialSummary>::CredentialSummary,
>,
> = BTreeMap::new();
for (id, manifest) in reader.manifests() {
let report = Self::summarize_all_impl(manifest, status_tracker, verifier).await;
reports.insert(id.clone(), report);
}
IdentityAssertionsForManifestStore::<
<<SV as SignatureVerifier>::Output as ToCredentialSummary>::CredentialSummary,
> {
assertions_for_manifest: reports,
}
}
pub(crate) async fn validate<SV: SignatureVerifier>(
&self,
manifest: &Manifest,
status_tracker: &mut StatusTracker,
verifier: &SV,
) -> Result<SV::Output, ValidationError<SV::Error>> {
if let Some(ref label) = self.label {
status_tracker.push_current_uri(label);
}
let result = self.validate_imp(manifest, status_tracker, verifier).await;
if self.label.is_some() {
status_tracker.pop_current_uri();
}
result
}
async fn validate_imp<SV: SignatureVerifier>(
&self,
manifest: &Manifest,
status_tracker: &mut StatusTracker,
verifier: &SV,
) -> Result<SV::Output, ValidationError<SV::Error>> {
self.check_padding(status_tracker)?;
self.signer_payload
.check_against_manifest(manifest, status_tracker)?;
verifier
.check_signature_async(&self.signer_payload, &self.signature, status_tracker)
.await
}
#[async_generic]
pub(crate) fn validate_partial_claim(
&self,
partial_claim: &PartialClaim,
status_tracker: &mut StatusTracker,
context: &Context,
) -> Result<serde_json::Value, ValidationError<String>> {
let settings = context.settings();
self.check_padding(status_tracker)?;
self.signer_payload
.check_against_partial_claim(partial_claim, status_tracker)?;
let sig_type = self.signer_payload.sig_type.as_str();
if sig_type == "cawg.x509.cose" {
let mut ctp = CertificateTrustPolicy::default();
if let Some(anchors) = &settings.trust.anchors_for_trust_kind(TrustListKind::CAWG) {
for anchor in anchors {
let _ = ctp.add_trust_anchors(
anchor.trust_anchors.as_bytes(),
anchor.trust_uri.as_deref().unwrap_or(""),
anchor.trust_kind.clone().into(),
anchor.trust_config.clone(),
);
if let Some(al) = &anchor.allowed_list {
let _ = ctp.add_end_entity_credentials(al.as_bytes());
}
}
}
let cose_verifier = Verifier::VerifyTrustPolicy(Cow::Owned(ctp));
let mut signer_payload_cbor: Vec<u8> = vec![];
c2pa_cbor::to_writer(&mut signer_payload_cbor, &self.signer_payload).map_err(|_| {
ValidationError::InternalError("CBOR serialization error".to_string())
})?;
let signature_result = {
let mut remap_guard = X509StatusRemapGuard::new(status_tracker);
let status_tracker = remap_guard.status_tracker();
match parse_cose_sign1(&self.signature, &signer_payload_cbor, status_tracker) {
Ok(cose_sign1) => {
let verify_result = if _sync {
cose_verifier.verify_signature(
&self.signature,
&signer_payload_cbor,
&[],
None,
status_tracker,
)
} else {
cose_verifier
.verify_signature_async(
&self.signature,
&signer_payload_cbor,
&[],
None,
status_tracker,
)
.await
};
verify_result
.map(|cert_info| (cose_sign1, cert_info))
.map_err(|e| match e {
CoseError::RawSignatureValidationError(
RawSignatureValidationError::SignatureMismatch,
) => {
log_current_item!(
"signature mismatch",
"validate_partial_claim"
)
.validation_status(CAWG_X509_SIGNATURE_MISMATCH)
.failure_no_throw(
status_tracker,
ValidationError::<String>::SignatureMismatch,
);
ValidationError::SignatureMismatch
}
e => ValidationError::SignatureError(e.to_string()),
})
}
Err(e) => Err(ValidationError::SignatureError(e.to_string())),
}
};
let (cose_sign1, cert_info) = signature_result?;
log_current_item!(
"X.509 identity assertion signature validated",
"validate_partial_claim"
)
.validation_status(CAWG_X509_SIGNATURE_VALIDATED)
.success(status_tracker);
let info = X509SignatureInfo {
signer_payload: self.signer_payload.clone(),
cose_sign1,
cert_info,
};
let result = info.to_summary();
log_current_item!(
"CAWG X.509 identity signature valid",
"validate_partial_claim"
)
.validation_status("cawg.identity.well-formed")
.success(status_tracker);
serde_json::to_value(result)
.map_err(|e| ValidationError::UnknownSignatureType(e.to_string()))
} else if sig_type == "cawg.identity_claims_aggregation" {
let verifier = IcaSignatureVerifier::new(context);
let result = if _sync {
verifier
.check_signature(&self.signer_payload, &self.signature, status_tracker)
.map(|v| v.to_summary())
.map_err(|e| ValidationError::UnknownSignatureType(e.to_string()))
} else {
verifier
.check_signature_async(&self.signer_payload, &self.signature, status_tracker)
.await
.map(|v| v.to_summary())
.map_err(|e| ValidationError::UnknownSignatureType(e.to_string()))
}?;
serde_json::to_value(result)
.map_err(|e| ValidationError::UnknownSignatureType(e.to_string()))
} else {
log_current_item!("unsupported signature type", "validate_partial_claim")
.validation_status("cawg.identity.sig_type.unknown")
.failure_no_throw(
status_tracker,
ValidationError::<String>::UnknownSignatureType(sig_type.to_string()),
);
Err(ValidationError::UnknownSignatureType(sig_type.to_string()))
}
}
fn check_padding<E: Debug>(
&self,
status_tracker: &mut StatusTracker,
) -> Result<(), ValidationError<E>> {
if !self.pad1.iter().all(|b| *b == 0) {
log_current_item!(
"invalid value in pad fields",
"SignerPayload::check_padding"
)
.validation_status("cawg.identity.pad.invalid")
.failure(status_tracker, ValidationError::<E>::InvalidPadding)?;
return Ok(());
}
if let Some(pad2) = self.pad2.as_ref() {
if !pad2.iter().all(|b| *b == 0) {
log_current_item!(
"invalid value in pad fields",
"SignerPayload::check_padding"
)
.validation_status("cawg.identity.pad.invalid")
.failure(status_tracker, ValidationError::<E>::InvalidPadding)?;
}
}
Ok(())
}
pub fn signer_payload(&self) -> &SignerPayload {
&self.signer_payload
}
}
impl Debug for IdentityAssertion {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> {
f.debug_struct("IdentityAssertion")
.field("signer_payload", &self.signer_payload)
.field("signature", &DebugByteSlice(&self.signature))
.field("label", &self.label)
.finish()
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::panic)]
#![allow(clippy::unwrap_used)]
use std::io::{Cursor, Seek};
use c2pa_macros::c2pa_test_async;
#[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
use wasm_bindgen_test::wasm_bindgen_test;
use super::*;
use crate::{
dynamic_assertion::PartialClaim,
identity::{
builder::{IdentityAssertionBuilder, IdentityAssertionSigner},
tests::{
fixtures::{cert_chain_and_private_key_for_alg, manifest_json, parent_json},
read_manifest,
},
x509::X509CredentialHolder,
},
Builder, SigningAlg,
};
const TEST_IMAGE: &[u8] = include_bytes!("../../../tests/fixtures/CA.jpg");
const TEST_THUMBNAIL: &[u8] = include_bytes!("../../../tests/fixtures/thumbnail.jpg");
#[c2pa_test_async]
async fn x509_signature_mismatch_is_reported() {
let format = "image/jpeg";
let mut source = Cursor::new(TEST_IMAGE);
let mut dest = Cursor::new(Vec::new());
let mut builder = Builder::default().with_definition(manifest_json()).unwrap();
builder
.add_ingredient_from_stream(parent_json(), format, &mut source)
.unwrap();
builder
.add_resource("thumbnail.jpg", Cursor::new(TEST_THUMBNAIL))
.unwrap();
let mut c2pa_signer = IdentityAssertionSigner::from_test_credentials(SigningAlg::Ps256);
let (cawg_cert_chain, cawg_private_key) =
cert_chain_and_private_key_for_alg(SigningAlg::Ed25519);
let cawg_raw_signer =
c2pa_raw_crypto::signer_from_private_key(&cawg_private_key, SigningAlg::Ed25519)
.unwrap();
let x509_holder = X509CredentialHolder::from_raw_signer(
cawg_raw_signer,
crate::crypto::cert_chain_pem_to_der(&cawg_cert_chain).unwrap(),
);
let iab = IdentityAssertionBuilder::for_credential_holder(x509_holder);
c2pa_signer.add_identity_assertion(iab);
builder
.sign(&c2pa_signer, format, &mut source, &mut dest)
.unwrap();
dest.rewind().unwrap();
let manifest_store = read_manifest(format, &mut dest).await;
assert_eq!(manifest_store.validation_status(), None);
let manifest = manifest_store.active_manifest().unwrap();
let mut st = StatusTracker::default();
let mut ia_iter = IdentityAssertion::from_manifest(manifest, &mut st);
let ia = ia_iter.next().unwrap().unwrap();
assert!(ia_iter.next().is_none());
drop(ia_iter);
let mut partial_claim = PartialClaim::default();
for referenced_assertion in &ia.signer_payload.referenced_assertions {
partial_claim.add_assertion(referenced_assertion);
}
let mut tampered_signer_payload = ia.signer_payload.clone();
tampered_signer_payload.roles.push("tampered".to_string());
let tampered_ia = IdentityAssertion {
signer_payload: tampered_signer_payload,
signature: ia.signature.clone(),
pad1: ia.pad1.clone(),
pad2: ia.pad2.clone(),
label: None,
};
let context = Context::new();
let err = tampered_ia
.validate_partial_claim_async(&partial_claim, &mut st, &context)
.await
.unwrap_err();
assert_eq!(err.to_string(), "signature is invalid");
let log = st
.logged_items()
.iter()
.find(|item| item.description == "signature mismatch")
.unwrap();
assert_eq!(log.kind, crate::status_tracker::LogKind::Failure);
assert_eq!(
log.validation_status.as_ref().unwrap().as_ref() as &str,
CAWG_X509_SIGNATURE_MISMATCH
);
let malformed_ia = IdentityAssertion {
signer_payload: ia.signer_payload.clone(),
signature: b"not a COSE_Sign1 structure".to_vec(),
pad1: ia.pad1.clone(),
pad2: ia.pad2.clone(),
label: None,
};
let mut malformed_st = StatusTracker::default();
let malformed_err = malformed_ia
.validate_partial_claim_async(&partial_claim, &mut malformed_st, &context)
.await
.unwrap_err();
assert!(matches!(malformed_err, ValidationError::SignatureError(_)));
assert!(malformed_st.has_status(CAWG_X509_SIGNATURE_MISMATCH));
}
}