use std::{collections::HashSet, fmt::Debug, sync::LazyLock};
use regex::Regex;
use serde::{Deserialize, Serialize};
use crate::{
assertions::labels::is_hard_binding_label,
dynamic_assertion::PartialClaim,
identity::ValidationError,
log_current_item,
status_tracker::StatusTracker,
validation_status::{
CAWG_IDENTITY_ASSERTION_DUPLICATE, CAWG_IDENTITY_ASSERTION_MISMATCH,
CAWG_IDENTITY_HARD_BINDING_MISSING,
},
HashedUri, Manifest,
};
#[derive(Clone, Debug, Deserialize, Eq, Serialize, PartialEq)]
pub struct SignerPayload {
pub referenced_assertions: Vec<HashedUri>,
pub sig_type: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
#[serde(rename = "role")]
pub roles: Vec<String>,
}
impl SignerPayload {
pub(super) fn check_against_partial_claim<E: Debug>(
&self,
partial_claim: &PartialClaim,
status_tracker: &mut StatusTracker,
) -> Result<(), ValidationError<E>> {
for ref_assertion in self.referenced_assertions.iter() {
if let Some(claim_assertion) = partial_claim.assertions().find(|a| {
let url = a.url();
if url == ref_assertion.url() {
return true;
}
let url = ABSOLUTE_URL_PREFIX.replace(&url, "");
url == ref_assertion.url()
}) {
if claim_assertion.hash() != ref_assertion.hash() {
log_current_item!(
"referenced assertion hash mismatch",
"SignerPayload::check_against_partial_claim"
)
.validation_status("cawg.identity.assertion.mismatch")
.failure(
status_tracker,
ValidationError::<E>::AssertionMismatch(ref_assertion.url().to_owned()),
)?;
}
} else {
log_current_item!(
"referenced assertion not in claim",
"SignerPayload::check_against_manifest"
)
.validation_status(CAWG_IDENTITY_ASSERTION_MISMATCH)
.failure(
status_tracker,
ValidationError::<E>::AssertionNotInClaim(ref_assertion.url().to_owned()),
)?;
}
}
let ref_assertion_labels: Vec<String> = self
.referenced_assertions
.iter()
.map(|ra| ra.url().to_owned())
.collect();
if !ref_assertion_labels.iter().any(|ra| {
if let Some((_jumbf_prefix, label)) = ra.rsplit_once('/') {
is_hard_binding_label(label)
} else {
false
}
}) {
log_current_item!(
"no hard binding assertion",
"SignerPayload::check_against_manifest"
)
.validation_status(CAWG_IDENTITY_HARD_BINDING_MISSING)
.failure(status_tracker, ValidationError::<E>::NoHardBindingAssertion)?;
}
let mut labels = HashSet::<String>::new();
for label in &ref_assertion_labels {
let label = label.clone();
if labels.contains(&label) {
log_current_item!(
"multiple references to same assertion",
"SignerPayload::check_against_manifest"
)
.validation_status(CAWG_IDENTITY_ASSERTION_DUPLICATE)
.failure(
status_tracker,
ValidationError::<E>::DuplicateAssertionReference(label.clone()),
)?;
}
labels.insert(label);
}
Ok(())
}
pub(super) fn check_against_manifest<E: Debug>(
&self,
manifest: &Manifest,
status_tracker: &mut StatusTracker,
) -> Result<(), ValidationError<E>> {
for ref_assertion in self.referenced_assertions.iter() {
if let Some(claim_assertion) = manifest.assertion_references().find(|a| {
let url = a.url();
if url == ref_assertion.url() {
return true;
}
let url = ABSOLUTE_URL_PREFIX.replace(&url, "");
url == ref_assertion.url()
}) {
if claim_assertion.hash() != ref_assertion.hash() {
log_current_item!(
"referenced assertion hash mismatch",
"SignerPayload::check_against_manifest"
)
.validation_status("cawg.identity.assertion.mismatch")
.failure(
status_tracker,
ValidationError::<E>::AssertionMismatch(ref_assertion.url().to_owned()),
)?;
}
} else {
log_current_item!(
"referenced assertion not in claim",
"SignerPayload::check_against_manifest"
)
.validation_status(CAWG_IDENTITY_ASSERTION_MISMATCH)
.failure(
status_tracker,
ValidationError::<E>::AssertionNotInClaim(ref_assertion.url().to_owned()),
)?;
}
}
let ref_assertion_labels: Vec<String> = self
.referenced_assertions
.iter()
.map(|ra| ra.url().to_owned())
.collect();
if !ref_assertion_labels.iter().any(|ra| {
if let Some((_jumbf_prefix, label)) = ra.rsplit_once('/') {
is_hard_binding_label(label)
} else {
false
}
}) {
log_current_item!(
"no hard binding assertion",
"SignerPayload::check_against_manifest"
)
.validation_status(CAWG_IDENTITY_HARD_BINDING_MISSING)
.failure(status_tracker, ValidationError::<E>::NoHardBindingAssertion)?;
}
let mut labels = HashSet::<String>::new();
for label in &ref_assertion_labels {
let label = label.clone();
if labels.contains(&label) {
log_current_item!(
"multiple references to same assertion",
"SignerPayload::check_against_manifest"
)
.validation_status(CAWG_IDENTITY_ASSERTION_DUPLICATE)
.failure(
status_tracker,
ValidationError::<E>::DuplicateAssertionReference(label.clone()),
)?;
}
labels.insert(label);
}
Ok(())
}
}
#[allow(clippy::unwrap_used)]
static ABSOLUTE_URL_PREFIX: LazyLock<Regex> = LazyLock::new(|| Regex::new("/c2pa/[^/]+/").unwrap());
#[cfg(test)]
mod tests {
#![allow(clippy::panic)]
#![allow(clippy::unwrap_used)]
use hex_literal::hex;
#[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
use wasm_bindgen_test::wasm_bindgen_test;
use crate::{
dynamic_assertion::PartialClaim,
identity::{SignerPayload, ValidationError},
status_tracker::{ErrorBehavior, StatusTracker},
HashedUri,
};
#[test]
fn mismatch_is_logged_and_propagated() {
let referenced = HashedUri::new(
"self#jumbf=c2pa.assertions/c2pa.hash.data".to_owned(),
Some("sha256".to_owned()),
&[0u8; 32],
);
let claim_reference = HashedUri::new(
"self#jumbf=c2pa.assertions/c2pa.hash.data".to_owned(),
Some("sha256".to_owned()),
&[255u8; 32],
);
let mut partial_claim = PartialClaim::default();
partial_claim.add_assertion(&claim_reference);
let mut status_tracker =
StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
let signer_payload = SignerPayload {
referenced_assertions: vec![referenced],
roles: vec![],
sig_type: "cawg.x509.cose".to_owned(),
};
let err: ValidationError<String> = signer_payload
.check_against_partial_claim(&partial_claim, &mut status_tracker)
.unwrap_err();
assert!(matches!(err, ValidationError::AssertionMismatch(_)));
assert_eq!(status_tracker.logged_items().len(), 1);
assert_eq!(
status_tracker.logged_items()[0]
.validation_status
.as_deref()
.unwrap(),
"cawg.identity.assertion.mismatch"
);
}
#[test]
#[cfg_attr(
all(target_arch = "wasm32", not(target_os = "wasi")),
wasm_bindgen_test
)]
fn impl_clone() {
let data_hash_ref = HashedUri::new(
"self#jumbf=c2pa/urn:uuid:F9168C5E-CEB2-4faa-B6BF-329BF39FA1E4/c2pa.assertions/c2pa.hash.data".to_owned(),
Some("sha256".to_owned()),
&hex!("53d1b2cf4e6d9a97ed9281183fa5d836c32751b9d2fca724b40836befee7d67f"));
let signer_payload = SignerPayload {
referenced_assertions: vec![{ data_hash_ref }],
roles: vec![],
sig_type: "NONSENSE".to_owned(),
};
assert_eq!(signer_payload, signer_payload.clone());
}
}