use std::{collections::HashSet, fmt::Debug, sync::LazyLock};
use regex::Regex;
use serde::{Deserialize, Serialize};
use crate::{
dynamic_assertion::PartialClaim, identity::ValidationError, log_current_item,
status_tracker::StatusTracker, 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() {
return Err(ValidationError::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('/') {
label.starts_with("c2pa.hash.")
} 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() {
return Err(ValidationError::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('/') {
label.starts_with("c2pa.hash.")
} 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::{identity::SignerPayload, HashedUri};
#[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());
}
}