#![allow(clippy::expect_used)]
#![allow(clippy::unwrap_used)]
#![allow(deprecated)]
use serde_json::{json, Value};
use super::claim_core::{GH_FULL_VERSION_LIST, SPEC_VERSION_F};
use crate::{
assertion::{Assertion, AssertionBase, AssertionData},
assertions::{self, c2pa_action, Action, Actions, Ingredient, Relationship},
claim::{AssertionStoreJsonFormat, Claim, ClaimAssertionType},
context::Context,
error::{Error, Result},
hashed_uri::HashedUri,
jumbf::labels::{to_assertion_uri, to_manifest_uri, to_signature_uri},
resource_store::UriOrResource,
settings::Settings,
status_tracker::{ErrorBehavior, StatusTracker},
store::StoreValidationInfo,
utils::test::{create_test_claim, create_test_claim_v1},
validation_status, ClaimGeneratorInfo, DigitalSourceType,
};
#[test]
fn test_build_claim() {
let mut claim = create_test_claim().expect("create test claim");
claim.build().expect("bad claim");
let orig_binary = claim.data().expect("failure returning data");
let restored_claim =
Claim::from_data("as_adbe_1", &orig_binary).expect("could not restore from binary");
let restored_binary = restored_claim.data().expect("failure returning data");
assert_eq!(orig_binary, restored_binary);
println!("Restored Claim: {restored_claim:?}");
assert_eq!(orig_binary, restored_claim.original_bytes.unwrap());
let json_str = claim
.to_json(AssertionStoreJsonFormat::OrderedList, true)
.expect("could not generate json");
println!("Claim: {json_str}");
}
#[test]
fn test_assertion_store_json_formats() -> Result<()> {
let mut claim = Claim::new("test", Some("test"), 1);
let binary = vec![0xde, 0xad, 0xbe, 0xef];
claim.add_assertion(&assertions::User::new("test.json", r#"{"value":7}"#))?;
claim.add_assertion(&Actions::new().add_action(Action::new(c2pa_action::CREATED)))?;
claim.add_assertion(&assertions::EmbeddedData::new(
"test.binary",
"application/octet-stream",
binary.clone(),
))?;
claim.add_assertion(&assertions::Uuid::new(
"test.uuid",
"deadbeefdeadbeefdeadbeefdeadbeef".to_string(),
binary.clone(),
))?;
for pretty in [false, true] {
for (format, ordered, omit_binary) in [
(AssertionStoreJsonFormat::KeyValue, false, false),
(AssertionStoreJsonFormat::KeyValueNoBinary, false, true),
(AssertionStoreJsonFormat::OrderedList, true, false),
(AssertionStoreJsonFormat::OrderedListNoBinary, true, true),
] {
let report: Value = serde_json::from_str(&claim.to_json(format, pretty)?)?;
assert_eq!(report["label"], claim.label());
let store = &report["assertion_store"];
let expected_binary = if omit_binary {
"omitted".to_string()
} else {
crate::crypto::base64::encode(&binary)
};
if ordered {
let entries = store.as_array().expect("ordered assertion store");
assert_eq!(entries.len(), 4);
assert_eq!(entries[0]["data"], json!({"value": 7}));
assert_eq!(entries[0]["is_binary"], false);
assert_eq!(
entries[1]["data"]["actions"][0]["action"],
c2pa_action::CREATED
);
assert_eq!(entries[2]["data"], expected_binary);
assert_eq!(entries[2]["is_binary"], true);
assert_eq!(entries[3]["data"]["data"], expected_binary);
assert_eq!(
entries[3]["data"]["uuid"],
"deadbeefdeadbeefdeadbeefdeadbeef"
);
assert_eq!(entries[3]["is_binary"], true);
} else {
assert_eq!(store["test.json"], json!({"value": 7}));
assert_eq!(
store["c2pa.actions.v2"]["actions"][0]["action"],
c2pa_action::CREATED
);
assert_eq!(store["test.binary"], expected_binary);
assert_eq!(store["test.uuid"]["data"], expected_binary);
assert_eq!(
store["test.uuid"]["uuid"],
"deadbeefdeadbeefdeadbeefdeadbeef"
);
assert!(report["vc_store"].is_array());
}
}
}
let report: Value =
serde_json::from_str(&claim.to_json(AssertionStoreJsonFormat::None, false)?)?;
assert!(report.get("assertion_store").is_none());
Ok(())
}
#[test]
fn test_action_ingredient_relationships() -> Result<()> {
for action_name in [
c2pa_action::OPENED,
c2pa_action::PLACED,
c2pa_action::REMOVED,
c2pa_action::TRANSCODED,
c2pa_action::REPACKAGED,
] {
for relationship in [Relationship::ParentOf, Relationship::ComponentOf] {
for plural in [false, true] {
for missing in [false, true] {
let mut claim = Claim::new("test", Some("test"), 2);
let ingredient = Ingredient::new_v3(relationship.clone())
.set_title("source.jpg")
.set_format("image/jpeg");
let reference = claim.add_assertion(&ingredient)?;
let reference = if missing {
HashedUri::new(
"self#jumbf=c2pa.assertions/c2pa.ingredient__99".to_string(),
None,
&[0; 32],
)
} else {
reference
};
let action = if plural {
Action::new(action_name).set_parameter("ingredients", vec![reference])?
} else {
Action::new(action_name).set_parameter("ingredient", reference)?
};
let actions = if action_name == c2pa_action::OPENED {
Actions::new()
} else {
Actions::new().add_action(
Action::new(c2pa_action::CREATED)
.set_source_type(DigitalSourceType::Empty),
)
};
claim.add_assertion(&actions.add_action(action))?;
let mut log =
StatusTracker::with_error_behavior(ErrorBehavior::ContinueWhenPossible);
Claim::verify_actions(
&claim,
&StoreValidationInfo::default(),
&mut log,
&Settings::default(),
)?;
let expected_relationship = if action_name == c2pa_action::PLACED
|| action_name == c2pa_action::REMOVED
{
Relationship::ComponentOf
} else {
Relationship::ParentOf
};
let mismatch = missing || relationship != expected_relationship;
let failures: Vec<_> = log
.logged_items()
.iter()
.filter(|item| {
item.validation_status.as_deref()
== Some(validation_status::ASSERTION_ACTION_INGREDIENT_MISMATCH)
})
.collect();
assert_eq!(
!failures.is_empty(),
mismatch,
"{action_name}, {relationship:?}, plural={plural}, missing={missing}: {log:?}"
);
if mismatch {
assert!(failures[0].description.contains(if missing {
"not found"
} else {
"source.jpg"
}));
} else {
assert!(log.logged_items().is_empty(), "{log:?}");
}
}
}
}
}
Ok(())
}
#[test]
fn test_action_missing_ingredient_parameters() -> Result<()> {
for action_name in [
c2pa_action::OPENED,
c2pa_action::PLACED,
c2pa_action::REMOVED,
] {
for parameters in [None, Some(json!({})), Some(json!({"ingredients": []}))] {
let mut claim = Claim::new("test", Some("test"), 2);
let mut action = json!({"action": action_name});
if let Some(parameters) = parameters {
action["parameters"] = parameters;
}
let action: Action = serde_json::from_value(action)?;
let actions = if action_name == c2pa_action::OPENED {
Actions::new()
} else {
Actions::new().add_action(
Action::new(c2pa_action::CREATED).set_source_type(DigitalSourceType::Empty),
)
};
claim.add_assertion(&actions.add_action(action))?;
let mut log = StatusTracker::with_error_behavior(ErrorBehavior::ContinueWhenPossible);
Claim::verify_actions(
&claim,
&StoreValidationInfo::default(),
&mut log,
&Settings::default(),
)?;
assert!(
log.logged_items()
.iter()
.any(|item| item.validation_status.as_deref()
== Some(validation_status::ASSERTION_ACTION_INGREDIENT_MISMATCH)),
"{action_name}: {log:?}"
);
}
}
Ok(())
}
#[test]
fn test_action_validation_v1_strictness_and_empty_actions() -> Result<()> {
for strict in [false, true] {
let mut claim = Claim::new("test", Some("test"), 1);
claim.add_assertion(&Actions::new())?;
let mut settings = Settings::default();
settings.verify.strict_v1_validation = strict;
let mut log = StatusTracker::with_error_behavior(ErrorBehavior::ContinueWhenPossible);
let result =
Claim::verify_actions(&claim, &StoreValidationInfo::default(), &mut log, &settings);
assert_eq!(result.is_err(), strict);
assert_eq!(log.logged_items().is_empty(), !strict);
if strict {
assert_eq!(
log.logged_items()[0].validation_status.as_deref(),
Some(validation_status::ASSERTION_ACTION_MALFORMED)
);
}
}
Ok(())
}
#[test]
fn test_action_validation_malformed_actions() -> Result<()> {
for (actions, description) in [
(Actions::new(), "Actions missing action array"),
(
Actions::new().add_action(Action::new("c2pa.edited")),
"first action must be created or opened",
),
(
Actions::new().add_action(Action::new(c2pa_action::CREATED)),
"c2pa.created action must have a digitalSourceType",
),
(
Actions::new()
.add_action(
Action::new(c2pa_action::CREATED).set_source_type(DigitalSourceType::Empty),
)
.add_action(Action::new("")),
"action must have an action.",
),
] {
let mut claim = Claim::new("test", Some("test"), 2);
claim.add_assertion(&actions)?;
for (behavior, stop_on_error) in [
(ErrorBehavior::StopOnFirstError, true),
(ErrorBehavior::ContinueWhenPossible, false),
] {
let mut log = StatusTracker::with_error_behavior(behavior);
let result = Claim::verify_actions(
&claim,
&StoreValidationInfo::default(),
&mut log,
&Settings::default(),
);
assert!(
log.logged_items()
.iter()
.any(|item| item.validation_status.as_deref()
== Some(validation_status::ASSERTION_ACTION_MALFORMED)
&& item.description == description),
"{description}: {log:?}"
);
let should_throw = description == "Actions missing action array"
|| (stop_on_error
&& description != "c2pa.created action must have a digitalSourceType");
assert_eq!(result.is_err(), should_throw, "{description}");
}
}
Ok(())
}
#[test]
fn test_action_validation_v1_rejects_multiple_assertions() -> Result<()> {
let mut claim = Claim::new("test", Some("test"), 1);
claim.add_assertion(&Actions::new().add_action(Action::new(c2pa_action::CREATED)))?;
claim.add_assertion(&Actions::new().add_action(Action::new("c2pa.edited")))?;
let mut log = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
assert!(Claim::verify_actions(
&claim,
&StoreValidationInfo::default(),
&mut log,
&Settings::default(),
)
.is_err());
assert_eq!(
log.logged_items()[0].validation_status.as_deref(),
Some(validation_status::ASSERTION_ACTION_MALFORMED)
);
assert_eq!(
log.logged_items()[0].description,
"only one action assertion allowed for v1 claims"
);
Ok(())
}
#[test]
fn test_build_claim_generator_hints() {
let mut claim = create_test_claim().expect("create test claim");
claim.add_claim_generator_hint(
GH_FULL_VERSION_LIST,
Value::String(r#""user app";v="2.3.4""#.to_string()),
);
claim.add_claim_generator_hint(
GH_FULL_VERSION_LIST,
Value::String(r#""some toolkit";v="1.0.0""#.to_string()),
);
let expected_value = r#""user app";v="2.3.4", "some toolkit";v="1.0.0""#;
let cg_map = claim.get_claim_generator_hint_map().unwrap();
let value = &cg_map[GH_FULL_VERSION_LIST];
assert_eq!(expected_value, value.as_str().unwrap());
}
#[test]
fn test_build_claim_generator_info() {
let mut claim = create_test_claim().expect("create test claim");
let mut info = ClaimGeneratorInfo::new("test app");
info.version = Some("2.3.4".to_string());
info.icon = Some(UriOrResource::HashedUri(HashedUri::new(
"self#jumbf=c2pa.databoxes.data_box".to_string(),
None,
b"hashed",
)));
info.insert("something", "else");
claim.add_claim_generator_info(info);
let cgi = claim.claim_generator_info().unwrap();
assert_eq!(&cgi[0].name, "test app");
assert_eq!(cgi[0].version.as_deref(), Some("2.3.4"));
if let UriOrResource::HashedUri(r) = cgi[1].icon.as_ref().unwrap() {
assert_eq!(r.hash(), b"hashed");
}
}
#[test]
fn test_new_with_user_guid() {
Claim::new_with_user_guid(
"claim_generator",
"acme:urn:uuid:3fad1ead-8ed5-44d0-873b-ea5f58adea82",
1,
)
.unwrap();
Claim::new_with_user_guid(
"claim_generator",
"urn:c2pa:3fad1ead-8ed5-44d0-873b-ea5f58adea82:acme",
2,
)
.unwrap();
let c2 = Claim::new_with_user_guid(
"claim_generator",
"urn:c2pa:3fad1ead-8ed5-44d0-873b-ea5f58adea82:acme",
1,
);
assert!(c2.is_err());
let c3 = Claim::new_with_user_guid(
"claim_generator",
"acme:urn:uuid:3fad1ead-8ed5-44d0-873b-ea5f58adea82",
2,
);
assert!(c3.is_err());
let c4 = Claim::new_with_user_guid(
"claim_generator",
"urn:c2pa:3fad1ead-8ed5-44d0-873b-ea5f58adea82:acme",
1,
);
assert!(c4.is_err());
let c5 = Claim::new_with_user_guid(
"claim_generator",
"urn:blahblah:3fad1ead-8ed5-44d0-873b-ea5f58adea82:acme",
2,
);
assert!(c5.is_err());
}
#[test]
fn test_add_assertion_variants() -> Result<()> {
let mut claim = crate::utils::test::create_min_test_claim()?;
const MY_METADATA: &str = "my.metadata";
let data = json!({
"@context" : {
"dc" : "http://purl.org/dc/elements/1.1/"
},
"dc:created": "2025 August 13",
"dc:creator": [
"John Doe"
]
})
.to_string();
let metadata = assertions::Metadata::new(MY_METADATA, &data)?;
claim.add_assertion(&metadata)?;
claim.add_assertion(&metadata)?;
claim.add_assertion(&metadata)?;
let instances = claim.count_instances(MY_METADATA);
assert_eq!(instances, 3);
for i in 0..instances {
let ca = claim
.get_claim_assertion(MY_METADATA, i)
.expect("should find assertion");
assert_eq!(ca.instance(), i);
assert_eq!(ca.label_raw(), MY_METADATA);
assert_eq!(ca.assertion().label(), MY_METADATA);
assert_eq!(ca.assertion_type(), ClaimAssertionType::Gathered);
}
Ok(())
}
#[test]
fn test_redact_assertion_exact_label_match_with_shared_prefix() {
use crate::{
assertions::{labels, EmbeddedData},
jumbf::labels::to_assertion_uri,
};
let mut claim = Claim::new("unit test", Some("test"), 2);
let thumb0 = EmbeddedData::new(labels::INGREDIENT_THUMBNAIL, "image/jpeg", vec![0u8; 4]);
let thumb1 = EmbeddedData::new(labels::INGREDIENT_THUMBNAIL, "image/jpeg", vec![1u8; 4]);
claim.add_assertion(&thumb0).expect("add thumb0");
claim.add_assertion(&thumb1).expect("add thumb1");
assert!(
claim
.get_assertion(labels::INGREDIENT_THUMBNAIL, 0)
.is_some(),
"instance 0 should exist"
);
assert!(
claim
.get_assertion(labels::INGREDIENT_THUMBNAIL, 1)
.is_some(),
"instance 1 should exist"
);
let manifest_label = claim.label();
let uri_base = to_assertion_uri(manifest_label, labels::INGREDIENT_THUMBNAIL);
let uri_instance1 = to_assertion_uri(
manifest_label,
&format!("{}__1", labels::INGREDIENT_THUMBNAIL),
);
claim
.redact_assertion(&uri_instance1)
.expect("redact instance 1 should succeed");
assert!(
claim
.get_assertion(labels::INGREDIENT_THUMBNAIL, 0)
.is_some(),
"instance 0 must still exist after redacting instance 1"
);
claim
.redact_assertion(&uri_base)
.expect("redact base instance should succeed");
assert!(
claim
.get_assertion(labels::INGREDIENT_THUMBNAIL, 0)
.is_none(),
"instance 0 should be gone"
);
assert!(
claim
.get_assertion(labels::INGREDIENT_THUMBNAIL, 1)
.is_none(),
"instance 1 should be gone"
);
}
#[test]
fn test_redact_databox_exact_label_match_with_shared_prefix() {
let mut claim = Claim::new("unit test", Some("test"), 2);
let uri0 = claim
.add_databox("application/octet-stream", vec![0u8; 4], None)
.expect("add databox 0");
let uri1 = claim
.add_databox("application/octet-stream", vec![1u8; 4], None)
.expect("add databox 1");
let has_databox = |claim: &Claim, uri: &HashedUri| {
claim.databoxes().iter().any(|(x, _d)| x.url() == uri.url())
};
assert!(has_databox(&claim, &uri0), "databox 0 should exist");
assert!(has_databox(&claim, &uri1), "databox 1 should exist");
assert!(
uri0.url().ends_with("/c2pa.data"),
"expected databox 0 to be labeled `c2pa.data`, got {}",
uri0.url()
);
assert!(
uri1.url().ends_with("/c2pa.data__1"),
"expected databox 1 to be labeled `c2pa.data__1`, got {}",
uri1.url()
);
claim
.redact_assertion(&uri1.url())
.expect("redact databox 1 should succeed");
assert!(
has_databox(&claim, &uri0),
"databox 0 must still exist after redacting databox 1"
);
assert!(!has_databox(&claim, &uri1), "databox 1 should be gone");
claim
.redact_assertion(&uri0.url())
.expect("redact databox 0 should succeed");
assert!(!has_databox(&claim, &uri0), "databox 0 should be gone");
assert!(!has_databox(&claim, &uri1), "databox 1 should be gone");
}
#[test]
fn test_redact_databox_from_other_manifest_returns_not_found() {
use crate::jumbf::labels::to_databox_uri;
let mut claim = Claim::new("unit test", Some("test"), 2);
let uri0 = claim
.add_databox("application/octet-stream", vec![0u8; 4], None)
.expect("add databox 0");
let foreign_uri = to_databox_uri("urn:uuid:00000000-0000-0000-0000-000000000000", "c2pa.data");
assert_ne!(foreign_uri, uri0.url(), "test setup: URIs must differ");
let result = claim.redact_assertion(&foreign_uri);
assert!(
matches!(result, Err(Error::AssertionRedactionNotFound)),
"cross-manifest redaction should fail, got {result:?}"
);
assert!(
claim
.databoxes()
.iter()
.any(|(x, _d)| x.url() == uri0.url()),
"databox 0 should have survived a redaction targeting another manifest"
);
}
#[test]
fn test_redact_assertion_instance_labels_are_exact() {
use crate::{assertions::UserCbor, jumbf::labels::to_assertion_uri};
const LABEL: &str = "com.example.a";
let mut claim = Claim::new("unit test", Some("test"), 2);
for n in 0..3 {
claim
.add_assertion(&UserCbor::new(
LABEL,
c2pa_cbor::to_vec(&format!("payload {n}")).unwrap(),
))
.expect("add assertion");
}
let stored = |claim: &Claim| -> Vec<String> {
claim
.claim_assertion_store()
.iter()
.map(|x| Claim::label_with_instance(&x.label_raw(), x.instance()))
.collect()
};
assert_eq!(
stored(&claim),
vec!["com.example.a", "com.example.a__1", "com.example.a__2"],
"test setup: three instances of the same label"
);
claim
.redact_assertion(&to_assertion_uri(claim.label(), "com.example.a__2"))
.expect("redact instance 2");
assert_eq!(
stored(&claim),
vec!["com.example.a", "com.example.a__1"],
"only instance 2 should have been removed"
);
claim
.redact_assertion(&to_assertion_uri(claim.label(), "com.example.a__1"))
.expect("redact instance 1");
assert_eq!(
stored(&claim),
vec!["com.example.a"],
"only instance 1 should have been removed"
);
claim
.redact_assertion(&to_assertion_uri(claim.label(), LABEL))
.expect("redact base instance");
assert!(claim.claim_assertion_store().is_empty());
}
#[test]
fn test_redact_assertion_prefix_is_not_a_match() {
use crate::{assertions::UserCbor, jumbf::labels::to_assertion_uri};
let mut claim = Claim::new("unit test", Some("test"), 2);
claim
.add_assertion(&UserCbor::new(
"com.example.metadata",
c2pa_cbor::to_vec(&"payload").unwrap(),
))
.expect("add assertion");
let uri = to_assertion_uri(claim.label(), "com.example.meta");
let result = claim.redact_assertion(&uri);
assert!(
matches!(result, Err(Error::AssertionRedactionNotFound)),
"a prefix of a stored label must not match, got {result:?}"
);
assert_eq!(
claim.claim_assertion_store().len(),
1,
"the assertion must survive a non-matching redaction"
);
}
#[test]
fn test_redact_assertion_suffix_label_is_not_a_match() {
use crate::{assertions::UserCbor, jumbf::labels::to_assertion_uri};
let mut claim = Claim::new("unit test", Some("test"), 2);
for label in ["example.a", "org.example.a"] {
claim
.add_assertion(&UserCbor::new(
label,
c2pa_cbor::to_vec(&"payload").unwrap(),
))
.expect("add assertion");
}
claim
.redact_assertion(&to_assertion_uri(claim.label(), "org.example.a"))
.expect("redact org.example.a");
let remaining: Vec<String> = claim
.claim_assertion_store()
.iter()
.map(|x| Claim::label_with_instance(&x.label_raw(), x.instance()))
.collect();
assert_eq!(
remaining,
vec!["example.a"],
"redacting `org.example.a` must not remove the `example.a` assertion"
);
}
#[test]
fn test_redact_assertion_label_from_link_round_trip() {
for label in [
"com.example.a",
"com.example.a__1",
"c2pa.thumbnail.ingredient__3.jpeg",
"c2pa.thumbnail.claim.jpeg__1",
"stds.schema-org.CreativeWork__2",
"c2pa.data",
"c2pa.data__1",
] {
let (parsed, instance) = Claim::assertion_label_from_link(label);
assert_eq!(
Claim::label_with_instance(&parsed, instance),
label,
"label normalization must round-trip for {label}"
);
}
}
fn make_soft_binding(alg: Option<&str>) -> assertions::SoftBinding {
use crate::assertions::{SoftBindingBlock, SoftBindingScope};
let mut sb = assertions::SoftBinding::default();
sb.alg = alg.map(str::to_string);
sb.blocks = vec![SoftBindingBlock {
scope: SoftBindingScope::default(),
value: "abc123".as_bytes().to_vec(),
}];
sb
}
#[test]
fn test_verify_soft_binding_alg_valid() {
let settings = Settings::new()
.with_json(
r#"
{
"soft_binding": {
"soft_binding_algorithms": ["com.adobe.trustmark.Q", "com.adobe.trustmark.C", "com.digimarc.validate.1"]
}
}
"#,
)
.unwrap();
let mut validation_log = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
let mut claim = create_test_claim().expect("create test claim");
claim
.add_assertion(&make_soft_binding(Some("com.digimarc.validate.1")))
.expect("add soft binding");
assert!(
Claim::verify_soft_binding_alg(
&claim,
settings
.soft_binding
.soft_binding_algorithms
.as_deref()
.unwrap_or(&[]),
&mut validation_log
)
.is_ok(),
"known algorithm should pass"
);
assert!(validation_log.logged_items().is_empty());
}
#[test]
#[ignore = "validation disabled for C2PA 2.4 forward"]
fn test_verify_soft_binding_alg_unknown() {
let settings = Settings::new()
.with_json(
r#"
{
"soft_binding": {
"soft_binding_algorithms": ["com.adobe.trustmark.Q", "com.adobe.trustmark.C", "com.digimarc.validate.1"]
}
}
"#,
)
.unwrap();
let mut validation_log = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
let mut claim = create_test_claim().expect("create test claim");
claim
.add_assertion(&make_soft_binding(Some("com.unknown.watermark.99")))
.expect("add soft binding");
let result = Claim::verify_soft_binding_alg(
&claim,
settings
.soft_binding
.soft_binding_algorithms
.as_deref()
.unwrap_or(&[]),
&mut validation_log,
);
assert!(result.is_err(), "unknown algorithm should fail");
assert!(
validation_log
.logged_items()
.iter()
.any(|item| item.validation_status.as_deref()
== Some(validation_status::ALGORITHM_UNSUPPORTED)),
"should log ALGORITHM_UNSUPPORTED"
);
}
#[test]
#[ignore = "validation disabled for C2PA 2.4 forward"]
fn test_verify_soft_binding_alg_missing_alg() {
let settings = Settings::new()
.with_json(
r#"
{
"soft_binding": {
"soft_binding_algorithms": ["com.adobe.trustmark.Q", "com.adobe.trustmark.C", "com.digimarc.validate.1"]
}
}
"#,
)
.unwrap();
let mut validation_log = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
let mut claim = create_test_claim().expect("create test claim");
claim
.add_assertion(&make_soft_binding(None))
.expect("add soft binding");
let result = Claim::verify_soft_binding_alg(
&claim,
settings
.soft_binding
.soft_binding_algorithms
.as_deref()
.unwrap_or(&[]),
&mut validation_log,
);
assert!(result.is_err(), "missing algorithm should fail");
assert!(
validation_log
.logged_items()
.iter()
.any(|item| item.validation_status.as_deref()
== Some(validation_status::CLAIM_MALFORMED)),
"should log CLAIM_MALFORMED"
);
}
#[test]
fn test_verify_soft_binding_alg_fallback_to_alg_soft() {
let settings = Settings::new()
.with_json(
r#"
{
"soft_binding": {
"soft_binding_algorithms": ["io.iscc.v0"]
}
}
"#,
)
.unwrap();
let mut validation_log = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
let mut claim = create_test_claim().expect("create test claim");
claim.alg_soft = Some("io.iscc.v0".to_string());
claim
.add_assertion(&make_soft_binding(None))
.expect("add soft binding");
assert!(
Claim::verify_soft_binding_alg(
&claim,
settings
.soft_binding
.soft_binding_algorithms
.as_deref()
.unwrap_or(&[]),
&mut validation_log
)
.is_ok(),
"valid alg_soft fallback should pass"
);
assert!(validation_log.logged_items().is_empty());
}
#[test]
#[ignore = "validation disabled for C2PA 2.4 forward"]
fn test_verify_soft_binding_alg_undecodable_does_not_abort() {
struct MalformedSoftBinding;
impl AssertionBase for MalformedSoftBinding {
const LABEL: &'static str = assertions::SoftBinding::LABEL;
fn to_assertion(&self) -> Result<Assertion> {
Ok(Assertion::new(
Self::LABEL,
None,
AssertionData::Json(r#"{"alg":"com.example.sha256","value":"abc123"}"#.to_string()),
)
.set_content_type("application/json"))
}
fn from_assertion(_assertion: &Assertion) -> Result<Self> {
unimplemented!("not needed for this test")
}
}
let settings = Settings::new()
.with_json(
r#"
{
"soft_binding": {
"soft_binding_algorithms": ["com.example.sha256"]
}
}
"#,
)
.unwrap();
let mut validation_log = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
let mut claim = create_test_claim().expect("create test claim");
claim
.add_assertion(&MalformedSoftBinding)
.expect("add malformed soft binding");
assert!(
Claim::verify_soft_binding_alg(
&claim,
settings
.soft_binding
.soft_binding_algorithms
.as_deref()
.unwrap_or(&[]),
&mut validation_log
)
.is_ok(),
"an undecodable soft binding assertion should not abort verification"
);
assert!(
validation_log
.logged_items()
.iter()
.any(|item| item.validation_status.as_deref()
== Some(validation_status::CLAIM_MALFORMED)),
"should log CLAIM_MALFORMED for the undecodable assertion"
);
}
#[test]
#[ignore = "validation disabled for C2PA 2.4 forward"]
fn test_verify_soft_binding_alg_continues_past_malformed_assertion() {
struct MalformedSoftBinding;
impl AssertionBase for MalformedSoftBinding {
const LABEL: &'static str = assertions::SoftBinding::LABEL;
fn to_assertion(&self) -> Result<Assertion> {
Ok(Assertion::new(
Self::LABEL,
None,
AssertionData::Json(r#"{"alg":"com.example.sha256","value":"abc123"}"#.to_string()),
)
.set_content_type("application/json"))
}
fn from_assertion(_assertion: &Assertion) -> Result<Self> {
unimplemented!("not needed for this test")
}
}
let settings = Settings::new()
.with_json(
r#"
{
"soft_binding": {
"soft_binding_algorithms": ["com.example.sha256"]
}
}
"#,
)
.unwrap();
let mut validation_log = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
let mut claim = create_test_claim().expect("create test claim");
claim
.add_assertion(&MalformedSoftBinding)
.expect("add malformed soft binding");
claim
.add_assertion(&make_soft_binding(Some("com.example.sha256")))
.expect("add valid soft binding");
claim
.add_assertion(&make_soft_binding(Some("com.unknown.watermark.99")))
.expect("add soft binding with unsupported algorithm");
let result = Claim::verify_soft_binding_alg(
&claim,
settings
.soft_binding
.soft_binding_algorithms
.as_deref()
.unwrap_or(&[]),
&mut validation_log,
);
assert!(
result.is_err(),
"the unsupported algorithm on the third assertion must still be fatal"
);
assert!(
validation_log
.logged_items()
.iter()
.any(|item| item.validation_status.as_deref()
== Some(validation_status::CLAIM_MALFORMED)),
"should log CLAIM_MALFORMED for the first, undecodable assertion"
);
assert!(
validation_log
.logged_items()
.iter()
.any(|item| item.validation_status.as_deref()
== Some(validation_status::ALGORITHM_UNSUPPORTED)),
"should still reach and log ALGORITHM_UNSUPPORTED for the third assertion"
);
}
#[test]
fn test_verify_claim_generator_info_valid() {
let mut validation_log = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
let claim = create_test_claim().expect("create test claim");
assert!(
Claim::verify_claim_generator_info(&claim, &mut validation_log).is_ok(),
"valid claim_generator_info should pass"
);
assert!(
validation_log.logged_items().is_empty(),
"no errors should be logged for a valid claim"
);
}
#[test]
fn test_verify_claim_generator_info_missing() {
let mut validation_log = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
let mut claim = Claim::new("test", Some("test"), 2);
claim.claim_generator_info = None;
let result = Claim::verify_claim_generator_info(&claim, &mut validation_log);
assert!(result.is_err(), "missing claim_generator_info should fail");
assert!(
validation_log
.logged_items()
.iter()
.any(|item| item.validation_status.as_deref()
== Some(validation_status::CLAIM_MALFORMED)),
"should log CLAIM_MALFORMED"
);
}
fn signature_box_uri_missing(mut claim: Claim, signature: String) -> bool {
claim.signature = signature;
let mut validation_log = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
let result = Claim::verify_signature_box_uri(&claim, &mut validation_log);
let logged = validation_log.logged_items().iter().any(|item| {
item.validation_status.as_deref() == Some(validation_status::CLAIM_SIGNATURE_MISSING)
});
assert_eq!(result.is_err(), logged);
logged
}
#[test]
fn test_verify_signature_box_uri_absolute_to_own_manifest() {
let claim = create_test_claim().expect("create test claim");
let uri = to_signature_uri(claim.label());
assert!(!signature_box_uri_missing(claim, uri));
}
#[test]
fn test_verify_signature_box_uri_absolute_to_other_manifest() {
let claim = create_test_claim().expect("create test claim");
let uri = to_signature_uri("urn:c2pa:2e52408b-4fbe-4557-8e84-e7e4a21a5107");
assert!(signature_box_uri_missing(claim, uri));
}
#[test]
fn test_verify_signature_box_uri_absolute_to_other_box() {
let claim = create_test_claim().expect("create test claim");
let uri = format!("{}/c2pa.assertions", to_manifest_uri(claim.label()));
assert!(signature_box_uri_missing(claim, uri));
}
fn create_test_claim_with_cgi_spec_version(spec_version: &str) -> Claim {
let mut claim = create_test_claim().expect("create test claim");
let cgi = claim
.claim_generator_info
.as_mut()
.expect("test claim has claim_generator_info");
cgi[0].set_spec_version(spec_version);
claim
}
#[test]
fn test_verify_signature_box_uri_relative_v2_spec_2_3() {
for version in ["2.3", "2.3.0", "2.4.0", "2.10.0", "3"] {
let claim = create_test_claim_with_cgi_spec_version(version);
assert!(
signature_box_uri_missing(claim, "self#jumbf=c2pa.signature".to_string()),
"relative URI should be rejected for spec {version}"
);
}
}
#[test]
fn test_verify_signature_box_uri_relative_v2_claim_map_spec_2_3() {
let mut claim = create_test_claim().expect("create test claim");
claim.spec_version = Some("2.3.0".to_string());
assert!(signature_box_uri_missing(
claim,
"self#jumbf=c2pa.signature".to_string()
));
}
#[test]
fn test_verify_signature_box_uri_relative_v2_spec_version_precedence() {
for (cgi_version, claim_map_version, missing) in [
("2.2.0", "2.3.0", false),
("2.3.0", "2.2.0", true),
("2.4.0", "2.3.0", true),
("next", "2.2.0", true),
("2.2.0", "next", false),
] {
let mut claim = create_test_claim_with_cgi_spec_version(cgi_version);
claim.spec_version = Some(claim_map_version.to_string());
assert_eq!(
claim.spec_version().map(String::as_str),
Some(cgi_version),
"claim_generator_info should take precedence over the claim map"
);
assert_eq!(
signature_box_uri_missing(claim, "self#jumbf=c2pa.signature".to_string()),
missing,
"unexpected result for CGI spec {cgi_version}, claim-map spec {claim_map_version}"
);
}
}
#[test]
fn test_verify_signature_box_uri_relative_v2_claim_map_fallback() {
for (version, missing) in [("2.2.0", false), ("2.3.0", true), ("next", true)] {
let mut claim = create_test_claim().expect("create test claim");
claim.spec_version = Some(version.to_string());
assert_eq!(claim.spec_version(), None);
assert_eq!(
signature_box_uri_missing(claim, "self#jumbf=c2pa.signature".to_string()),
missing,
"claim-map spec {version} should be used when CGI has no specVersion"
);
}
}
#[test]
fn test_verify_signature_box_uri_relative_v2_signed_jpeg() -> Result<()> {
use crate::{
store::Store,
utils::{
test::{create_min_test_claim, create_test_streams, test_context},
test_signer::test_signer,
},
Reader, SigningAlg, ValidationState,
};
for version in [None, Some("2.2.0"), Some("2.3.0")] {
let mut context = test_context();
context.settings_mut().verify.verify_after_sign = false;
context.settings_mut().verify.verify_after_reading = true;
context.settings_mut().verify.verify_trust = false;
let mut claim = create_min_test_claim()?;
if let Some(version) = version {
claim
.claim_generator_info
.as_mut()
.expect("test claim has claim_generator_info")[0]
.set_spec_version(version);
}
let mut store = Store::from_context(&context);
store.commit_claim(claim)?;
store
.provenance_claim_mut()
.expect("store has a provenance claim")
.signature = "self#jumbf=c2pa.signature".to_string();
let (format, mut source, mut asset) = create_test_streams("earth_apollo17.jpg");
let signer = test_signer(SigningAlg::Ps256);
store.save_to_stream(format, &mut source, &mut asset, signer.as_ref(), &context)?;
let mut report = StatusTracker::default();
let restored = Store::from_stream(format, &mut asset, &mut report, &context)?;
let claim = restored
.provenance_claim()
.expect("signed JPEG has a provenance claim");
assert_eq!(claim.version(), 2);
assert_eq!(claim.spec_version().map(String::as_str), version);
assert_eq!(claim.signature, "self#jumbf=c2pa.signature");
assert!(!claim.signature_val().is_empty());
asset.set_position(0);
let reader = Reader::from_context(context).with_stream(format, &mut asset)?;
if version == Some("2.3.0") {
assert_eq!(reader.validation_state(), ValidationState::Invalid);
assert!(reader
.validation_status()
.expect("invalid JPEG has validation errors")
.iter()
.any(|status| status.code() == validation_status::CLAIM_SIGNATURE_MISSING));
} else {
assert_eq!(
reader.validation_state(),
ValidationState::Valid,
"signed pre-2.3 JPEG should validate for spec {version:?}"
);
assert!(
!report.has_any_error(),
"pre-2.3 JPEG has validation errors: {report:?}"
);
assert_eq!(reader.validation_status(), None);
}
}
Ok(())
}
#[test]
fn test_verify_signature_box_uri_relative_v2_unparseable_spec_version() {
let claim = create_test_claim_with_cgi_spec_version("next");
assert!(signature_box_uri_missing(
claim,
"self#jumbf=c2pa.signature".to_string()
));
}
#[test]
fn test_verify_signature_box_uri_relative_v2_before_spec_2_3() {
let undeclared = create_test_claim().expect("create test claim");
assert!(!signature_box_uri_missing(
undeclared,
"self#jumbf=c2pa.signature".to_string()
));
for version in ["2.2", "2.2.0", "2"] {
let claim = create_test_claim_with_cgi_spec_version(version);
assert!(
!signature_box_uri_missing(claim, "self#jumbf=c2pa.signature".to_string()),
"relative URI should be accepted for spec {version}"
);
}
}
#[test]
fn test_verify_signature_box_uri_relative_v2_to_other_box() {
let claim = create_test_claim().expect("create test claim");
assert!(signature_box_uri_missing(
claim,
"self#jumbf=c2pa.assertions".to_string()
));
}
#[test]
fn test_verify_signature_box_uri_relative_v1() {
let claim = create_test_claim_v1().expect("create test claim");
assert!(!signature_box_uri_missing(
claim,
"self#jumbf=c2pa.signature".to_string()
));
}
#[test]
fn test_verify_claim_generator_info_empty_name() {
let mut validation_log = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
let mut claim = Claim::new("test", Some("test"), 2);
claim.add_claim_generator_info(ClaimGeneratorInfo::new(""));
let result = Claim::verify_claim_generator_info(&claim, &mut validation_log);
assert!(result.is_err(), "empty name should fail");
assert!(
validation_log
.logged_items()
.iter()
.any(|item| item.validation_status.as_deref()
== Some(validation_status::CLAIM_MALFORMED)),
"should log CLAIM_MALFORMED"
);
}
#[test]
fn test_spec_version_from_claim_generator_info() {
let mut claim = Claim::new("test", Some("test"), 2);
let mut cgi = ClaimGeneratorInfo::new("test app");
cgi.set_spec_version("2.4.0");
claim.add_claim_generator_info(cgi);
assert_eq!(claim.spec_version().map(|s| s.as_str()), Some("2.4.0"));
}
#[test]
fn test_spec_version_in_claim_generator_info_round_trips() {
let mut claim = Claim::new("test", Some("test"), 2);
let mut cgi = ClaimGeneratorInfo::new("test app");
cgi.set_spec_version("2.4.0");
claim.add_claim_generator_info(cgi);
let data = claim.data().unwrap();
let decoded = Claim::from_data("test", &data).unwrap();
assert_eq!(decoded.spec_version().map(|s| s.as_str()), Some("2.4.0"));
let value: c2pa_cbor::Value = c2pa_cbor::from_slice(&data).unwrap();
assert!(matches!(
&value,
c2pa_cbor::Value::Map(map) if !map
.keys()
.any(|k| matches!(k, c2pa_cbor::Value::Text(t) if t == SPEC_VERSION_F))
));
}
#[test]
#[allow(deprecated)]
fn test_legacy_claim_level_spec_version_round_trips() {
let mut claim = Claim::new("test", Some("test"), 2);
claim.add_claim_generator_info(ClaimGeneratorInfo::new("test app"));
claim.set_spec_version(Some("2.3".to_owned()));
let data = claim.data().unwrap();
let decoded = Claim::from_data("test", &data).unwrap();
assert_eq!(decoded.spec_version.as_deref(), Some("2.3"));
let value: c2pa_cbor::Value = c2pa_cbor::from_slice(&data).unwrap();
assert!(matches!(
&value,
c2pa_cbor::Value::Map(map) if map
.keys()
.any(|k| matches!(k, c2pa_cbor::Value::Text(t) if t == SPEC_VERSION_F))
));
}
#[test]
fn test_spec_version_none_when_not_set_in_claim_generator_info() {
let mut claim = Claim::new("test", Some("test"), 2);
claim.add_claim_generator_info(ClaimGeneratorInfo::new("test app"));
assert_eq!(claim.spec_version(), None);
}
fn make_cloud_data(label: &str) -> assertions::CloudData {
assertions::CloudData::new(
label,
1024,
assertions::HashedExtUri::new(
"https://example.com/data",
"sha256",
vec![0xde, 0xad, 0xbe, 0xef],
),
)
}
fn make_external_reference(url: &str) -> assertions::ExternalReference {
assertions::ExternalReference::new_hashed(url, "sha256", vec![0xde, 0xad, 0xbe, 0xef])
}
#[test]
fn test_verify_external_reference_valid() {
let mut validation_log = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
let mut claim = create_test_claim().expect("create test claim");
claim
.add_assertion(&make_external_reference("https://example.com/ext.json"))
.expect("add external reference");
assert!(
Claim::verify_external_reference(&claim, &mut validation_log).is_ok(),
"valid external-reference assertion should pass"
);
assert!(validation_log.logged_items().is_empty());
}
#[test]
fn test_verify_external_reference_rejects_malformed() {
let mut validation_log = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
let mut claim = create_test_claim().expect("create test claim");
let bad = assertions::ExternalReference {
location: assertions::ExternalReferenceLocation::Hashed(assertions::HashedExtUri {
url: " ".to_owned(),
alg: String::new(),
hash: vec![],
data_types: None,
}),
label: None,
description: None,
metadata: None,
};
claim
.add_assertion(&bad)
.expect("add malformed external reference");
Claim::verify_external_reference(&claim, &mut validation_log).unwrap();
assert!(
validation_log
.logged_items()
.iter()
.any(|item| item.validation_status.as_deref()
== Some(validation_status::ASSERTION_EXTERNAL_REFERENCE_MALFORMED)),
"should log ASSERTION_EXTERNAL_REFERENCE_MALFORMED"
);
}
#[test]
fn test_verify_cloud_data_valid() {
let mut validation_log = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
let mut claim = create_test_claim().expect("create test claim");
claim
.add_assertion(&make_cloud_data("c2pa.metadata"))
.expect("add cloud data");
assert!(
Claim::verify_cloud_data(&claim, &mut validation_log).is_ok(),
"valid cloud-data assertion should pass"
);
assert!(validation_log.logged_items().is_empty());
}
#[test]
fn test_verify_cloud_data_zero_size_rejected() {
let mut validation_log = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
let mut claim = create_test_claim().expect("create test claim");
let cd = assertions::CloudData::new(
"c2pa.metadata",
0,
assertions::HashedExtUri::new(
"https://example.com/data",
"sha256",
vec![0xde, 0xad, 0xbe, 0xef],
),
);
claim.add_assertion(&cd).expect("add cloud data");
Claim::verify_cloud_data(&claim, &mut validation_log).unwrap();
assert!(
validation_log
.logged_items()
.iter()
.any(|item| item.validation_status.as_deref()
== Some(validation_status::ASSERTION_CLOUD_DATA_MALFORMED)),
"should log ASSERTION_CLOUD_DATA_MALFORMED"
);
}
fn verify_translated_action(translated: Action) -> StatusTracker {
use crate::assertions::DigitalSourceType;
let mut claim = Claim::new("test", Some("test"), 2);
let actions = Actions::new()
.add_action(
Action::new(c2pa_action::CREATED).set_source_type(DigitalSourceType::AlgorithmicMedia),
)
.add_action(translated);
claim.add_assertion(&actions).expect("add actions");
let svi = StoreValidationInfo::default();
let settings = Settings::default();
let mut validation_log =
StatusTracker::with_error_behavior(ErrorBehavior::ContinueWhenPossible);
Claim::verify_actions(&claim, &svi, &mut validation_log, &settings)
.expect("verify_actions should not throw");
validation_log
}
#[test]
fn test_verify_translated_action_missing_languages() {
let log = verify_translated_action(Action::new(c2pa_action::TRANSLATED));
assert!(
log.has_status(validation_status::ASSERTION_ACTION_MALFORMED),
"c2pa.translated without sourceLanguage/targetLanguage should be malformed"
);
}
#[test]
fn test_verify_translated_action_partial_languages() {
let translated = Action::new(c2pa_action::TRANSLATED)
.set_parameter("source_language", "en-US")
.expect("set source_language");
let log = verify_translated_action(translated);
assert!(
log.has_status(validation_status::ASSERTION_ACTION_MALFORMED),
"c2pa.translated with only sourceLanguage should be malformed"
);
}
#[test]
#[ignore = "validation disabled for C2PA 2.4 forward"]
fn test_verify_translated_action_with_languages_valid() {
let translated = Action::new(c2pa_action::TRANSLATED)
.set_parameter("source_language", "en-US")
.expect("set source_language")
.set_parameter("target_language", "fr-FR")
.expect("set target_language");
let log = verify_translated_action(translated);
assert!(
!log.has_status(validation_status::ASSERTION_ACTION_MALFORMED),
"c2pa.translated with both languages should not be malformed"
);
}
fn verify_related_assertions(claim: &mut Claim, related: Vec<HashedUri>) -> StatusTracker {
let edited = Action::new(c2pa_action::EDITED)
.set_parameter("relatedAssertions", related)
.unwrap();
let actions = Actions::new()
.add_action(
Action::new(c2pa_action::CREATED).set_source_type(DigitalSourceType::AlgorithmicMedia),
)
.add_action(edited);
claim.add_assertion(&actions).unwrap();
let svi = StoreValidationInfo::default();
let settings = Settings::default();
let mut validation_log =
StatusTracker::with_error_behavior(ErrorBehavior::ContinueWhenPossible);
Claim::verify_actions(claim, &svi, &mut validation_log, &settings).unwrap();
validation_log
}
#[test]
fn test_verify_related_assertions_empty_rejected() {
let mut claim = Claim::new("test", Some("test"), 2);
let log = verify_related_assertions(&mut claim, vec![]);
assert!(
log.has_status(validation_status::ASSERTION_ACTION_MALFORMED),
"empty relatedAssertions should be malformed"
);
}
#[test]
fn test_verify_related_assertions_unresolvable_rejected() {
let mut claim = Claim::new("test", Some("test"), 2);
let uri = HashedUri::new(
to_assertion_uri(claim.label(), "com.example.missing"),
Some("sha256".to_string()),
b"hash",
);
let log = verify_related_assertions(&mut claim, vec![uri]);
assert!(
log.has_status(validation_status::ASSERTION_ACTION_MALFORMED),
"unresolvable relatedAssertions reference should be malformed"
);
}
#[test]
fn test_verify_related_assertions_references_actions_rejected() {
let mut claim = Claim::new("test", Some("test"), 2);
let uri = HashedUri::new(
to_assertion_uri(claim.label(), Actions::LABEL_VERSIONED),
Some("sha256".to_string()),
b"hash",
);
let log = verify_related_assertions(&mut claim, vec![uri]);
assert!(
log.has_status(validation_status::ASSERTION_ACTION_MALFORMED),
"relatedAssertions referencing an actions assertion should be malformed"
);
}
#[test]
fn test_verify_related_assertions_references_ingredient_rejected() {
let mut claim = Claim::new("test", Some("test"), 2);
let ingredient_uri = claim
.add_assertion(&Ingredient::new_v3(Relationship::ComponentOf))
.unwrap();
let log = verify_related_assertions(&mut claim, vec![ingredient_uri]);
assert!(
log.has_status(validation_status::ASSERTION_ACTION_MALFORMED),
"relatedAssertions referencing an ingredient assertion should be malformed"
);
}
#[test]
fn test_verify_related_assertions_valid() {
use crate::assertions::UserCbor;
let mut claim = Claim::new("test", Some("test"), 2);
let uri = claim
.add_assertion(&UserCbor::new(
"com.example.related",
c2pa_cbor::to_vec(&"related payload").unwrap(),
))
.unwrap();
let log = verify_related_assertions(&mut claim, vec![uri]);
assert!(
!log.has_status(validation_status::ASSERTION_ACTION_MALFORMED),
"relatedAssertions referencing an allowed assertion should not be malformed"
);
}
#[test]
fn test_verify_actions_rejects_multiple_inception_actions_in_one_assertion() {
use crate::assertions::DigitalSourceType;
let mut claim = Claim::new("test", Some("test"), 2);
let actions = Actions::new()
.add_action(Action::new(c2pa_action::CREATED).set_source_type(DigitalSourceType::Empty))
.add_action(Action::new(c2pa_action::OPENED));
claim.add_assertion(&actions).expect("add actions");
let svi = StoreValidationInfo::default();
let settings = Settings::default();
let mut validation_log =
StatusTracker::with_error_behavior(ErrorBehavior::ContinueWhenPossible);
Claim::verify_actions(&claim, &svi, &mut validation_log, &settings)
.expect("verify_actions should not throw");
assert!(
validation_log.has_status(validation_status::ASSERTION_ACTION_MALFORMED),
"an actions assertion containing more than one c2pa.created/c2pa.opened action should be malformed"
);
}
#[test]
fn test_verify_actions_rejects_both_created_and_opened_actions() {
use crate::assertions::DigitalSourceType;
let mut claim = Claim::new("test", Some("test"), 2);
let created_actions = Actions::new()
.add_action(Action::new(c2pa_action::CREATED).set_source_type(DigitalSourceType::Empty));
let opened_actions = Actions::new().add_action(Action::new(c2pa_action::OPENED));
claim
.add_assertion(&created_actions)
.expect("add created actions");
claim
.add_assertion(&opened_actions)
.expect("add opened actions");
let svi = StoreValidationInfo::default();
let settings = Settings::default();
let mut validation_log =
StatusTracker::with_error_behavior(ErrorBehavior::ContinueWhenPossible);
Claim::verify_actions(&claim, &svi, &mut validation_log, &settings)
.expect("verify_actions should not throw");
assert!(
validation_log.has_status(validation_status::ASSERTION_ACTION_MALFORMED),
"a claim containing both c2pa.created and c2pa.opened actions should be malformed"
);
}
fn verify_watermark_action(action: Action, with_soft_binding: bool) -> StatusTracker {
use crate::assertions::DigitalSourceType;
let mut claim = Claim::new("test", Some("test"), 2);
let actions = Actions::new()
.add_action(
Action::new(c2pa_action::CREATED)
.set_source_type(DigitalSourceType::TrainedAlgorithmicMedia),
)
.add_action(action);
claim.add_assertion(&actions).expect("add actions");
if with_soft_binding {
claim
.add_assertion(&make_soft_binding(Some("com.example.wm")))
.expect("add soft binding");
}
let svi = StoreValidationInfo::default();
let settings = Settings::default();
let mut validation_log =
StatusTracker::with_error_behavior(ErrorBehavior::ContinueWhenPossible);
Claim::verify_actions(&claim, &svi, &mut validation_log, &settings)
.expect("verify_actions should not throw");
validation_log
}
#[test]
fn test_watermarked_action_without_soft_binding_logs_missing() {
let log = verify_watermark_action(Action::new(c2pa_action::WATERMARKED), false);
assert!(
log.has_status(validation_status::ACTION_ASSERTION_SOFTBINDING_MISSING),
"c2pa.watermarked without soft binding should log ACTION_ASSERTION_SOFTBINDING_MISSING"
);
}
#[test]
fn test_watermarked_bound_action_without_soft_binding_logs_missing() {
let log = verify_watermark_action(Action::new(c2pa_action::WATERMARKED_BOUND), false);
assert!(
log.has_status(validation_status::ACTION_ASSERTION_SOFTBINDING_MISSING),
"c2pa.watermarked.bound without soft binding should log ACTION_ASSERTION_SOFTBINDING_MISSING"
);
}
#[test]
fn test_watermarked_action_with_soft_binding_passes() {
let log = verify_watermark_action(Action::new(c2pa_action::WATERMARKED), true);
assert!(
!log.has_status(validation_status::ACTION_ASSERTION_SOFTBINDING_MISSING),
"c2pa.watermarked with soft binding present should not log ACTION_ASSERTION_SOFTBINDING_MISSING"
);
}
#[test]
fn test_verify_cloud_data_hard_binding_rejected() {
let mut validation_log = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
let mut claim = create_test_claim().expect("create test claim");
claim
.add_assertion(&make_cloud_data(assertions::labels::DATA_HASH))
.expect("add cloud data");
Claim::verify_cloud_data(&claim, &mut validation_log).unwrap();
assert!(
validation_log
.logged_items()
.iter()
.any(|item| item.validation_status.as_deref()
== Some(validation_status::ASSERTION_CLOUD_DATA_HARD_BINDING)),
"should log ASSERTION_CLOUD_DATA_HARD_BINDING"
);
}
#[test]
fn test_verify_cloud_data_actions_in_update_manifest_rejected() {
let mut validation_log = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
let mut claim = create_test_claim().expect("create test claim");
claim.set_update_manifest(true);
claim
.add_assertion(&make_cloud_data(assertions::labels::ACTIONS))
.expect("add cloud data");
Claim::verify_cloud_data(&claim, &mut validation_log).unwrap();
assert!(
validation_log
.logged_items()
.iter()
.any(|item| item.validation_status.as_deref()
== Some(validation_status::ASSERTION_CLOUD_DATA_ACTIONS)),
"should log ASSERTION_CLOUD_DATA_ACTIONS"
);
}
#[test]
fn test_update_manifest_with_claim_thumbnail_rejected() {
let mut claim = create_test_claim().expect("create test claim");
claim.set_update_manifest(true);
claim
.add_assertion(&assertions::EmbeddedData::new(
assertions::labels::JPEG_CLAIM_THUMBNAIL,
"image/jpeg",
vec![0xff, 0xd8, 0xff],
))
.expect("add claim thumbnail");
let mut validation_log =
StatusTracker::with_error_behavior(ErrorBehavior::ContinueWhenPossible);
Claim::verify_internal(
&claim,
&StoreValidationInfo::default(),
Err(Error::CoseSignature),
&mut validation_log,
&Context::new(),
)
.expect("verification should continue to report validation statuses");
assert!(
validation_log.has_status(validation_status::MANIFEST_UPDATE_INVALID),
"an update manifest containing one claim thumbnail should be invalid"
);
}
#[test]
fn test_claim_signature_not_validated_after_credential_rejected() {
use crate::{crypto::cose::CertificateInfo, jumbf::labels::to_signature_uri, log_item};
let claim = create_test_claim().expect("create test claim");
let signature_uri = to_signature_uri(claim.label());
let verify = |validation_log: &mut StatusTracker| {
let _ = Claim::verify_internal(
&claim,
&StoreValidationInfo::default(),
Ok(CertificateInfo {
validated: true,
..Default::default()
}),
validation_log,
&Context::new(),
);
};
for rejection in [
validation_status::SIGNING_CREDENTIAL_INVALID,
validation_status::SIGNING_CREDENTIAL_REVOKED,
validation_status::SIGNING_CREDENTIAL_EXPIRED,
validation_status::CLAIM_SIGNATURE_OUTSIDE_VALIDITY,
validation_status::ALGORITHM_UNSUPPORTED,
] {
let mut validation_log =
StatusTracker::with_error_behavior(ErrorBehavior::ContinueWhenPossible);
log_item!(signature_uri.clone(), "signing credential rejected", "test")
.validation_status(rejection)
.failure_no_throw(&mut validation_log, Error::CoseSignature);
verify(&mut validation_log);
assert!(
!validation_log.has_status(validation_status::CLAIM_SIGNATURE_VALIDATED),
"{rejection} should suppress claimSignature.validated"
);
let mut validation_log =
StatusTracker::with_error_behavior(ErrorBehavior::ContinueWhenPossible);
log_item!(
"self#jumbf=/c2pa/other/c2pa.signature",
"signing credential rejected",
"test"
)
.validation_status(rejection)
.failure_no_throw(&mut validation_log, Error::CoseSignature);
verify(&mut validation_log);
assert!(
validation_log.has_status(validation_status::CLAIM_SIGNATURE_VALIDATED),
"{rejection} for another claim should not suppress claimSignature.validated"
);
}
let mut validation_log =
StatusTracker::with_error_behavior(ErrorBehavior::ContinueWhenPossible);
log_item!(signature_uri, "signing certificate untrusted", "test")
.validation_status(validation_status::SIGNING_CREDENTIAL_UNTRUSTED)
.failure_no_throw(&mut validation_log, Error::CoseSignature);
verify(&mut validation_log);
assert!(validation_log.has_status(validation_status::CLAIM_SIGNATURE_VALIDATED));
}
#[test]
fn test_verify_cloud_data_actions_in_non_update_manifest_not_allowed() {
let mut validation_log = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
let mut claim = create_test_claim().expect("create test claim");
claim
.add_assertion(&make_cloud_data(assertions::labels::ACTIONS))
.expect("add cloud data");
Claim::verify_cloud_data(&claim, &mut validation_log).unwrap();
assert!(
validation_log.has_any_error(),
"actions in cloud-data of a non-update manifest should fail"
);
}
#[test]
fn test_inside_validity_not_reported_with_outside_validity() {
use crate::{crypto::cose::CertificateInfo, jumbf::labels::to_signature_uri, log_item};
let claim = create_test_claim().expect("create test claim");
let verify = |validation_log: &mut StatusTracker| {
let _ = Claim::verify_internal(
&claim,
&StoreValidationInfo::default(),
Ok(CertificateInfo {
validated: true,
..Default::default()
}),
validation_log,
&Context::new(),
);
};
for code in [
validation_status::CLAIM_SIGNATURE_OUTSIDE_VALIDITY,
validation_status::SIGNING_CREDENTIAL_EXPIRED,
] {
let mut validation_log =
StatusTracker::with_error_behavior(ErrorBehavior::ContinueWhenPossible);
log_item!(
to_signature_uri(claim.label()),
"certificate outside validity period",
"test"
)
.validation_status(code)
.failure_no_throw(&mut validation_log, Error::CoseSignature);
verify(&mut validation_log);
assert!(
!validation_log.has_status(validation_status::CLAIM_SIGNATURE_INSIDE_VALIDITY),
"{code}"
);
let mut validation_log =
StatusTracker::with_error_behavior(ErrorBehavior::ContinueWhenPossible);
log_item!(
"self#jumbf=/c2pa/other/c2pa.signature",
"certificate outside validity period",
"test"
)
.validation_status(code)
.failure_no_throw(&mut validation_log, Error::CoseSignature);
verify(&mut validation_log);
assert!(
validation_log.has_status(validation_status::CLAIM_SIGNATURE_INSIDE_VALIDITY),
"{code} for another claim should not suppress claimSignature.insideValidity"
);
}
let mut validation_log =
StatusTracker::with_error_behavior(ErrorBehavior::ContinueWhenPossible);
verify(&mut validation_log);
assert!(validation_log.has_status(validation_status::CLAIM_SIGNATURE_INSIDE_VALIDITY));
}