#![allow(clippy::expect_used)]
#![allow(clippy::panic)]
#![allow(clippy::unwrap_used)]
#[cfg(feature = "file_io")]
use std::fs;
#[cfg(feature = "file_io")]
use std::path::PathBuf;
use std::{
collections::HashSet,
io::{Cursor, Read, Seek, SeekFrom, Write},
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
};
use c2pa_macros::c2pa_test_async;
#[cfg(feature = "file_io")]
use memchr::memmem;
use serde::Serialize;
#[cfg(feature = "file_io")]
use sha2::Sha256;
#[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))]
use wasm_bindgen_test::wasm_bindgen_test;
use super::store_core::MAX_INGREDIENT_DEPTH;
#[cfg(feature = "file_io")]
use crate::{
assertion::AssertionJson,
assertions::{labels::BOX_HASH, BoxHash, DataHash},
asset_io::ObjectType,
hashed_uri::HashedUri,
jumbf_io::get_assetio_handler_from_path,
utils::{
hash_utils::{HashRange, Hasher},
io_utils::tempdirectory,
test::{temp_dir_path, write_jpeg_placeholder_file, TEST_USER_ASSERTION},
test_signer::test_cawg_signer,
},
};
use crate::{
assertion::{Assertion, AssertionData},
assertions::{labels, Action, Actions, CollectionHash, Ingredient, Relationship, Uuid},
asset_io::ReadWriteSeek,
claim::{AssertionStoreJsonFormat, Claim, ClaimAssertion, ClaimAssetData},
context::Context,
dynamic_assertion::{
AsyncDynamicAssertion, DynamicAssertion, DynamicAssertionContent, PartialClaim,
},
error::{Error, Result},
jumbf::{boxes::C2PA_REDACTION_UUID, labels::to_assertion_uri},
jumbf_io::load_jumbf_from_stream,
settings::{Settings, SettingsValidate},
status_tracker::{ErrorBehavior, LogItem, StatusTracker},
store::{InvalidClaimError, Store, StoreValidationInfo},
utils::{
patch::patch_bytes,
test::{create_test_claim, create_test_streams, fixture_path},
test_signer::{async_test_signer, test_signer},
},
validation_status,
validation_status::ALGORITHM_UNSUPPORTED,
AsyncSigner, ClaimGeneratorInfo, DigitalSourceType, Signer, SigningAlg,
};
fn create_editing_claim(claim: &mut Claim) -> Result<&mut Claim> {
let uuid_str = "deadbeefdeadbeefdeadbeefdeadbeef";
let some_binary_data: Vec<u8> = vec![
0x0d, 0x0e, 0x0a, 0x0d, 0x0b, 0x0e, 0x0e, 0x0f, 0x0a, 0x0d, 0x0b, 0x0e, 0x0a, 0x0d, 0x0b,
0x0e,
];
let actions = Actions::new().add_action(Action::new("c2pa.created"));
claim.add_assertion(&actions)?;
let uuid_assertion = Uuid::new("test uuid", uuid_str.to_string(), some_binary_data);
claim.add_assertion(&uuid_assertion)?;
Ok(claim)
}
fn create_capture_claim(claim: &mut Claim) -> Result<&mut Claim> {
let actions = Actions::new()
.add_action(Action::new("c2pa.created").set_source_type(DigitalSourceType::Empty));
claim.add_assertion(&actions)?;
Ok(claim)
}
#[test]
fn test_claim_signature_inspection() -> Result<()> {
let store = Store::new();
let settings = Settings::default();
let signer = test_signer(SigningAlg::Es256);
let mut claim = create_test_claim()?;
assert!(claim.cose_sign1().is_err());
assert!(!claim.has_ocsp_vals());
let signature = store.sign_claim(&claim, signer.as_ref(), signer.reserve_size(), &settings)?;
claim.set_signature_val(signature);
assert!(claim.cose_sign1().is_ok());
let info = claim.signature_info().expect("signature info");
let cert_chain = claim.get_cert_chain(&settings)?;
assert!(!cert_chain.is_empty());
assert_eq!(cert_chain, info.cert_chain);
assert!(!claim.has_ocsp_vals());
Ok(())
}
#[c2pa_test_async]
async fn test_claim_signature_inspection_requires_serializable_claim() {
let claim = Claim::new("test", Some("test"), 2);
assert!(claim.data().is_err());
assert!(claim.cose_sign1().is_err());
assert!(claim.signature_info().is_none());
assert!(claim.signature_info_async().await.is_none());
assert!(claim.get_cert_chain(&Settings::default()).is_err());
assert!(!claim.has_ocsp_vals());
}
#[c2pa_test_async]
async fn test_claim_signature_inspection_async() -> Result<()> {
let store = Store::new();
let settings = Settings::default();
let signer = async_test_signer(SigningAlg::Es256);
let mut claim = create_test_claim()?;
let signature = store
.sign_claim_async(&claim, signer.as_ref(), signer.reserve_size(), &settings)
.await?;
claim.set_signature_val(signature);
let info = claim.signature_info_async().await.expect("signature info");
assert_eq!(claim.get_cert_chain(&settings)?, info.cert_chain);
assert!(!info.cert_chain.is_empty());
Ok(())
}
#[test]
fn test_ocsp_manifest_selection() -> Result<()> {
use crate::settings::builder::OcspFetchScope;
let mut store = Store::new();
let mut first = create_test_claim()?;
first.set_signature_val(vec![1]);
let first_label = store.commit_claim(first)?;
let mut second = create_test_claim()?;
second.set_signature_val(vec![1]);
let active_label = store.commit_claim(second)?;
let mut settings = Settings::default();
for scope in [
None,
Some(OcspFetchScope::Active),
Some(OcspFetchScope::All),
] {
for should_override in [None, Some(false), Some(true)] {
settings.builder.certificate_status_fetch = scope;
settings.builder.certificate_status_should_override = should_override;
let expected = match (scope, should_override) {
(Some(OcspFetchScope::Active), Some(_)) => vec![active_label.clone()],
(Some(OcspFetchScope::All), Some(_)) => {
vec![first_label.clone(), active_label.clone()]
}
_ => Vec::new(),
};
assert_eq!(store.get_manifest_labels_for_ocsp(&settings), expected);
}
}
settings.builder.certificate_status_fetch = Some(OcspFetchScope::Active);
settings.builder.certificate_status_should_override = Some(true);
assert!(Store::new()
.get_manifest_labels_for_ocsp(&settings)
.is_empty());
Ok(())
}
#[c2pa_test_async]
async fn test_ocsp_missing_and_invalid_claims() -> Result<()> {
let context = Context::new();
let mut store = Store::new();
assert!(store.get_ocsp_status(&context).is_none());
assert!(store.get_ocsp_status_async(&context).await.is_none());
let mut log = StatusTracker::default();
assert!(store
.get_ocsp_response_ders(vec!["missing".to_string()], &mut log, &context)?
.is_empty());
assert!(store
.get_ocsp_response_ders_async(vec!["missing".to_string()], &mut log, &context)
.await?
.is_empty());
let mut claim = create_test_claim()?;
claim.set_signature_val(vec![1]);
let label = store.commit_claim(claim)?;
assert!(store.get_ocsp_status(&context).is_none());
assert!(store.get_ocsp_status_async(&context).await.is_none());
assert!(store
.get_ocsp_response_ders(vec![label.clone()], &mut log, &context)
.is_err());
assert!(store
.get_ocsp_response_ders_async(vec![label], &mut log, &context)
.await
.is_err());
Ok(())
}
#[test]
fn test_jumbf_generation() {
let context = Context::new();
let (format, mut input_stream, mut output_stream) = create_test_streams("earth_apollo17.jpg");
let mut store = Store::from_context(&context);
let cgi = ClaimGeneratorInfo::new("claim_v1_unit_test");
let mut claim_capture = Claim::new("capture", Some("claim_capture"), 1);
create_capture_claim(&mut claim_capture).unwrap();
claim_capture.add_claim_generator_info(cgi.clone());
let signer = test_signer(SigningAlg::Ps256);
store.commit_claim(claim_capture).unwrap();
store
.save_to_stream(
format,
&mut input_stream,
&mut output_stream,
signer.as_ref(),
&context,
)
.unwrap();
let mut report = StatusTracker::default();
output_stream.rewind().unwrap();
let new_store = Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();
assert!(!report.has_any_error());
for claim in new_store.claims() {
let _restored_json = claim
.to_json(AssertionStoreJsonFormat::OrderedList, false)
.unwrap();
let _orig_json = store
.get_claim(claim.label())
.unwrap()
.to_json(AssertionStoreJsonFormat::OrderedList, false)
.unwrap();
println!(
"Claim: {} \n{}",
claim.label(),
claim
.to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true)
.expect("could not restore from json")
);
for hashed_uri in claim.assertions() {
let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url());
claim.get_claim_assertion(&label, instance).unwrap();
}
}
}
#[test]
fn test_claim_v2_generation() {
let context = Context::new();
let (format, mut input_stream, mut output_stream) = create_test_streams("earth_apollo17.jpg");
let mut store = Store::from_context(&context);
let cgi = ClaimGeneratorInfo::new("claim_v2_unit_test");
let mut claim_capture = Claim::new("capture", Some("claim_capture"), 2);
create_capture_claim(&mut claim_capture).unwrap();
claim_capture.add_claim_generator_info(cgi.clone());
let signer = test_signer(SigningAlg::Ps256);
store.commit_claim(claim_capture).unwrap();
store
.save_to_stream(
format,
&mut input_stream,
&mut output_stream,
signer.as_ref(),
&context,
)
.unwrap();
let mut report = StatusTracker::default();
output_stream.rewind().unwrap();
let new_store = Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();
assert!(!report.has_any_error());
for claim in new_store.claims() {
let _restored_json = claim
.to_json(AssertionStoreJsonFormat::OrderedList, false)
.unwrap();
let _orig_json = store
.get_claim(claim.label())
.unwrap()
.to_json(AssertionStoreJsonFormat::OrderedList, false)
.unwrap();
println!(
"Claim: {} \n{}",
claim.label(),
claim
.to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true)
.expect("could not restore from json")
);
for hashed_uri in claim.assertions() {
let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url());
claim.get_claim_assertion(&label, instance).unwrap();
}
}
}
#[test]
fn test_bad_claim_v2_generation() {
let mut context = Context::new();
context.settings_mut().verify.verify_after_sign = false;
let (format, mut input_stream, mut output_stream) = create_test_streams("earth_apollo17.jpg");
let mut store = Store::from_context(&context);
let cgi = ClaimGeneratorInfo::new("claim_v2_unit_test");
let mut claim_capture = Claim::new("capture", Some("claim_capture"), 2);
create_capture_claim(&mut claim_capture).unwrap();
claim_capture.add_claim_generator_info(cgi.clone());
let action = Actions::new().add_action(Action::new("c2pa.opened"));
claim_capture.add_assertion(&action).unwrap();
let signer = test_signer(SigningAlg::Ps256);
store.commit_claim(claim_capture).unwrap();
store
.save_to_stream(
format,
&mut input_stream,
&mut output_stream,
signer.as_ref(),
&context,
)
.unwrap();
let mut report = StatusTracker::default();
output_stream.rewind().unwrap();
let _new_store = Store::from_stream(format, &mut output_stream, &mut report, &context);
assert!(report.has_any_error());
assert!(report.has_error(Error::ValidationRule(
"only first action can be created or opened".to_string()
)));
}
#[test]
#[cfg(feature = "file_io")]
#[ignore = "we need to make this work again"]
fn test_unknown_asset_type_generation() {
let context = crate::context::Context::new();
let (_format, mut input_stream, mut output_stream) =
create_test_streams("unsupported_type.txt");
let format = "text/plain";
let mut store = Store::from_context(&context);
let claim1 = create_test_claim().unwrap();
let mut claim2 = Claim::new("Photoshop", Some("Adobe"), 1);
create_editing_claim(&mut claim2).unwrap();
let mut claim_capture = Claim::new("capture", Some("claim_capture"), 1);
create_capture_claim(&mut claim_capture).unwrap();
let signer = test_signer(SigningAlg::Ps256);
store.commit_claim(claim1).unwrap();
store
.save_to_stream(
format,
&mut input_stream,
&mut output_stream,
signer.as_ref(),
&context,
)
.unwrap();
output_stream.rewind().unwrap();
let new_store = Store::from_stream(
format,
&mut output_stream,
&mut StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError),
&context,
)
.unwrap();
for claim in new_store.claims() {
let _restored_json = claim
.to_json(AssertionStoreJsonFormat::OrderedList, false)
.unwrap();
let _orig_json = store
.get_claim(claim.label())
.unwrap()
.to_json(AssertionStoreJsonFormat::OrderedList, false)
.unwrap();
println!(
"Claim: {} \n{}",
claim.label(),
claim
.to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true)
.expect("could not restore from json")
);
for hashed_uri in claim.assertions() {
let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url());
claim.get_claim_assertion(&label, instance).unwrap();
}
}
}
#[test]
#[cfg(feature = "file_io")]
fn test_detects_unverifiable_signature() {
let context = crate::context::Context::new();
struct BadSigner {}
impl Signer for BadSigner {
fn sign(&self, _data: &[u8]) -> Result<Vec<u8>> {
Ok(b"not a valid signature".to_vec())
}
fn alg(&self) -> SigningAlg {
SigningAlg::Ps256
}
fn certs(&self) -> Result<Vec<Vec<u8>>> {
Ok(Vec::new())
}
fn reserve_size(&self) -> usize {
42
}
}
let (format, mut input_stream, mut output_stream) = create_test_streams("earth_apollo17.jpg");
let mut store = Store::from_context(&context);
let claim = create_test_claim().unwrap();
let signer = BadSigner {};
store.commit_claim(claim).unwrap();
store
.save_to_stream(
format,
&mut input_stream,
&mut output_stream,
&signer,
&context,
)
.unwrap_err();
}
#[test]
#[cfg(feature = "file_io")]
fn test_sign_with_expired_cert() {
use crate::{create_signer, SigningAlg};
let context = Context::new();
let (format, mut input_stream, mut output_stream) = create_test_streams("earth_apollo17.jpg");
let mut store = Store::from_context(&context);
let claim = create_test_claim().unwrap();
let signcert_path = fixture_path("rsa-pss256_key-expired.pub");
let pkey_path = fixture_path("rsa-pss256-expired.pem");
let signer =
create_signer::from_files(signcert_path, pkey_path, SigningAlg::Ps256, None).unwrap();
store.commit_claim(claim).unwrap();
let r = store.save_to_stream(
format,
&mut input_stream,
&mut output_stream,
&signer,
&context,
);
assert!(r.is_err());
assert_eq!(
r.err().unwrap().to_string(),
"the certificate was not valid at time of signing"
);
}
#[test]
#[cfg(feature = "file_io")]
fn test_jumbf_replacement_generation() {
let context = Context::new();
let mut store = Store::from_context(&context);
let claim1 = create_test_claim().unwrap();
store.commit_claim(claim1).unwrap();
let jumbf_bytes = store.to_jumbf_internal(512).unwrap();
assert!(!jumbf_bytes.is_empty());
let ap = fixture_path("prerelease.jpg");
let temp_dir = tempdirectory().expect("temp dir");
let op = temp_dir_path(&temp_dir, "replacement_test.jpg");
let original_jumbf = context.io().read_c2pa_from_file(&ap).unwrap();
context
.io()
.write_c2pa_to_file(&jumbf_bytes, &ap, Some(&op))
.unwrap();
let saved_jumbf = context.io().read_c2pa_from_file(&op).unwrap();
assert_eq!(&jumbf_bytes, &saved_jumbf);
let buf = fs::read(&op).unwrap();
assert_eq!(memmem::find(&buf, &original_jumbf[0..1024]), None);
}
#[c2pa_test_async]
async fn test_jumbf_generation_async() -> Result<()> {
let context = crate::context::Context::new();
let signer = async_test_signer(SigningAlg::Ps256);
let (format, mut input_stream, mut output_stream) = create_test_streams("earth_apollo17.jpg");
let mut store = Store::from_context(&context);
let claim1 = crate::utils::test::create_test_claim()?;
let mut claim2 = Claim::new("Photoshop", Some("Adobe"), 1);
create_editing_claim(&mut claim2)?;
let mut claim_capture = Claim::new("capture", Some("claim_capture"), 1);
create_capture_claim(&mut claim_capture)?;
let claim1_label = claim1.label().to_string();
let capture = claim_capture.label().to_string();
let claim2_label = claim2.label().to_string();
store.commit_claim(claim1)?;
store
.save_to_stream_async(
format,
&mut input_stream,
&mut output_stream,
&signer,
&context,
)
.await?;
store.commit_claim(claim_capture)?;
let mut temp_stream = Cursor::new(Vec::new());
output_stream.rewind()?;
store
.save_to_stream_async(
format,
&mut output_stream,
&mut temp_stream,
&signer,
&context,
)
.await?;
store.commit_claim(claim2)?;
temp_stream.rewind()?;
output_stream.rewind()?;
store
.save_to_stream_async(
format,
&mut temp_stream,
&mut output_stream,
&signer,
&context,
)
.await
.unwrap();
let c1 = store.get_claim(&claim1_label);
let c2 = store.get_claim(&capture);
let c3 = store.get_claim(&claim2_label);
assert_eq!(&claim1_label, c1.unwrap().label());
assert_eq!(&capture, c2.unwrap().label());
assert_eq!(claim2_label, c3.unwrap().label());
let jumbf_bytes = store.to_jumbf_internal(signer.reserve_size())?;
assert!(!jumbf_bytes.is_empty());
println!("Provenance: {}\n", store.provenance_path().unwrap());
let mut report = StatusTracker::default();
output_stream.rewind()?;
let _new_store =
Store::from_stream_async(format, &mut output_stream, &mut report, &context).await?;
assert!(!report.has_any_error());
Ok(())
}
#[test]
fn test_png_jumbf_generation() {
let mut context = Context::new();
context.settings_mut().verify.verify_after_sign = false;
let (format, mut input_stream, mut output_stream) = create_test_streams("libpng-test.png");
let mut store = Store::from_context(&context);
let claim1 = create_test_claim().unwrap();
let mut claim2 = Claim::new("Photoshop", Some("Adobe"), 1);
create_editing_claim(&mut claim2).unwrap();
let mut claim_capture = Claim::new("capture", Some("claim_capture"), 1);
create_capture_claim(&mut claim_capture).unwrap();
let signer = test_signer(SigningAlg::Ps256);
store.commit_claim(claim1).unwrap();
store
.save_to_stream(
format,
&mut input_stream,
&mut output_stream,
&signer,
&context,
)
.unwrap();
store.commit_claim(claim_capture).unwrap();
output_stream.rewind().unwrap();
let mut temp_stream = Cursor::new(Vec::new());
store
.save_to_stream(
format,
&mut output_stream,
&mut temp_stream,
&signer,
&context,
)
.unwrap();
store.commit_claim(claim2).unwrap();
temp_stream.rewind().unwrap();
output_stream.rewind().unwrap();
store
.save_to_stream(
format,
&mut temp_stream,
&mut output_stream,
signer.as_ref(),
&context,
)
.unwrap();
println!("Provenance: {}\n", store.provenance_path().unwrap());
let mut report = StatusTracker::default();
output_stream.rewind().unwrap();
let new_store = Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();
let _some_binary_data: Vec<u8> = vec![
0x0d, 0x0e, 0x0a, 0x0d, 0x0b, 0x0e, 0x0e, 0x0f, 0x0a, 0x0d, 0x0b, 0x0e, 0x0a, 0x0d, 0x0b,
0x0e,
];
for claim in new_store.claims() {
let _restored_json = claim
.to_json(AssertionStoreJsonFormat::OrderedList, false)
.unwrap();
let _orig_json = store
.get_claim(claim.label())
.unwrap()
.to_json(AssertionStoreJsonFormat::OrderedList, false)
.unwrap();
for hashed_uri in claim.assertions() {
let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url());
claim
.get_claim_assertion(&label, instance)
.expect("Should find assertion");
}
}
}
#[test]
fn test_png_compressed_jumbf_generation() {
let mut context = Context::new();
context.settings_mut().verify.verify_after_sign = false;
let (format, mut input_stream, mut output_stream) = create_test_streams("libpng-test.png");
let mut store = Store::from_context(&context);
let claim1 = create_test_claim().unwrap();
let mut claim2 = Claim::new("Photoshop", Some("Adobe"), 1);
create_editing_claim(&mut claim2).unwrap();
claim2.set_compressed_manifest(true);
let mut claim_capture = Claim::new("capture", Some("claim_capture"), 1);
create_capture_claim(&mut claim_capture).unwrap();
claim_capture.set_compressed_manifest(true);
let signer = test_signer(SigningAlg::Ps256);
store.commit_claim(claim1).unwrap();
store
.save_to_stream(
format,
&mut input_stream,
&mut output_stream,
&signer,
&context,
)
.unwrap();
store.commit_claim(claim_capture).unwrap();
output_stream.rewind().unwrap();
let mut temp_stream = Cursor::new(Vec::new());
store
.save_to_stream(
format,
&mut output_stream,
&mut temp_stream,
&signer,
&context,
)
.unwrap();
store.commit_claim(claim2).unwrap();
temp_stream.rewind().unwrap();
output_stream.rewind().unwrap();
store
.save_to_stream(
format,
&mut temp_stream,
&mut output_stream,
signer.as_ref(),
&context,
)
.unwrap();
println!("Provenance: {}\n", store.provenance_path().unwrap());
let mut report = StatusTracker::default();
output_stream.rewind().unwrap();
let new_store = Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();
let _some_binary_data: Vec<u8> = vec![
0x0d, 0x0e, 0x0a, 0x0d, 0x0b, 0x0e, 0x0e, 0x0f, 0x0a, 0x0d, 0x0b, 0x0e, 0x0a, 0x0d, 0x0b,
0x0e,
];
for claim in new_store.claims() {
let _restored_json = claim
.to_json(AssertionStoreJsonFormat::OrderedList, false)
.unwrap();
let _orig_json = store
.get_claim(claim.label())
.unwrap()
.to_json(AssertionStoreJsonFormat::OrderedList, false)
.unwrap();
for hashed_uri in claim.assertions() {
let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url());
claim
.get_claim_assertion(&label, instance)
.expect("Should find assertion");
}
}
}
#[test]
#[cfg(feature = "file_io")]
fn test_get_data_boxes() {
use crate::jumbf::labels::to_relative_uri;
let claim1 = create_test_claim().unwrap();
for (uri, db) in claim1.databoxes() {
assert!(claim1.get_databox(uri).is_some());
let rel_path = to_relative_uri(&uri.url());
let rel_hr = HashedUri::new(rel_path, uri.alg(), &uri.hash());
assert!(claim1.get_databox(&rel_hr).is_some());
assert_eq!(db, claim1.get_databox(uri).unwrap());
}
}
#[test]
fn test_wav_jumbf_generation() {
let context = crate::context::Context::new();
let (format, mut input_stream, mut output_stream) = create_test_streams("sample1.wav");
let mut store = Store::from_context(&context);
let claim1 = create_test_claim().unwrap();
let mut claim2 = Claim::new("Photoshop", Some("Adobe"), 1);
create_editing_claim(&mut claim2).unwrap();
let mut claim_capture = Claim::new("capture", Some("claim_capture"), 1);
create_capture_claim(&mut claim_capture).unwrap();
let signer = test_signer(SigningAlg::Ps256);
store.commit_claim(claim1).unwrap();
store
.save_to_stream(
format,
&mut input_stream,
&mut output_stream,
signer.as_ref(),
&context,
)
.unwrap();
store.commit_claim(claim_capture).unwrap();
output_stream.rewind().unwrap();
let mut temp_stream = Cursor::new(Vec::new());
store
.save_to_stream(
format,
&mut output_stream,
&mut temp_stream,
signer.as_ref(),
&context,
)
.unwrap();
store.commit_claim(claim2).unwrap();
output_stream.rewind().unwrap();
store
.save_to_stream(
format,
&mut temp_stream,
&mut output_stream,
signer.as_ref(),
&context,
)
.unwrap();
println!("Provenance: {}\n", store.provenance_path().unwrap());
let mut report = StatusTracker::default();
let _some_binary_data: Vec<u8> = vec![
0x0d, 0x0e, 0x0a, 0x0d, 0x0b, 0x0e, 0x0e, 0x0f, 0x0a, 0x0d, 0x0b, 0x0e, 0x0a, 0x0d, 0x0b,
0x0e,
];
output_stream.rewind().unwrap();
let new_store = Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();
for claim in new_store.claims() {
let _restored_json = claim
.to_json(AssertionStoreJsonFormat::OrderedList, false)
.unwrap();
let _orig_json = store
.get_claim(claim.label())
.unwrap()
.to_json(AssertionStoreJsonFormat::OrderedList, false)
.unwrap();
println!(
"Claim: {} \n{}",
claim.label(),
claim
.to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true)
.expect("could not restore from json")
);
for hashed_uri in claim.assertions() {
let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url());
claim
.get_claim_assertion(&label, instance)
.expect("Should find assertion");
}
}
}
#[test]
fn test_avi_jumbf_generation() {
let context = crate::context::Context::new();
let (format, mut input_stream, mut output_stream) = create_test_streams("test.avi");
let mut store = Store::from_context(&context);
let claim1 = create_test_claim().unwrap();
let mut claim2 = Claim::new("Photoshop", Some("Adobe"), 1);
create_editing_claim(&mut claim2).unwrap();
let mut claim_capture = Claim::new("capture", Some("claim_capture"), 1);
create_capture_claim(&mut claim_capture).unwrap();
let signer = test_signer(SigningAlg::Ps256);
store.commit_claim(claim1).unwrap();
store
.save_to_stream(
format,
&mut input_stream,
&mut output_stream,
&signer,
&context,
)
.unwrap();
store.commit_claim(claim_capture).unwrap();
output_stream.rewind().unwrap();
let mut temp_stream = Cursor::new(Vec::new());
store
.save_to_stream(
format,
&mut output_stream,
&mut temp_stream,
&signer,
&context,
)
.unwrap();
store.commit_claim(claim2).unwrap();
temp_stream.rewind().unwrap();
output_stream.rewind().unwrap();
store
.save_to_stream(
format,
&mut temp_stream,
&mut output_stream,
signer.as_ref(),
&context,
)
.unwrap();
println!("Provenance: {}\n", store.provenance_path().unwrap());
let mut report = StatusTracker::default();
let _some_binary_data: Vec<u8> = vec![
0x0d, 0x0e, 0x0a, 0x0d, 0x0b, 0x0e, 0x0e, 0x0f, 0x0a, 0x0d, 0x0b, 0x0e, 0x0a, 0x0d, 0x0b,
0x0e,
];
output_stream.rewind().unwrap();
let new_store = Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();
for claim in new_store.claims() {
let _restored_json = claim
.to_json(AssertionStoreJsonFormat::OrderedList, false)
.unwrap();
let _orig_json = store
.get_claim(claim.label())
.unwrap()
.to_json(AssertionStoreJsonFormat::OrderedList, false)
.unwrap();
println!(
"Claim: {} \n{}",
claim.label(),
claim
.to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true)
.expect("could not restore from json")
);
for hashed_uri in claim.assertions() {
let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url());
claim
.get_claim_assertion(&label, instance)
.expect("Should find assertion");
}
}
}
#[test]
fn test_webp_jumbf_generation() {
let context = crate::context::Context::new();
let (format, mut input_stream, mut output_stream) = create_test_streams("sample1.webp");
let mut store = Store::from_context(&context);
let claim1 = create_test_claim().unwrap();
let mut claim2 = Claim::new("Photoshop", Some("Adobe"), 1);
create_editing_claim(&mut claim2).unwrap();
let mut claim_capture = Claim::new("capture", Some("claim_capture"), 1);
create_capture_claim(&mut claim_capture).unwrap();
let signer = test_signer(SigningAlg::Ps256);
store.commit_claim(claim1).unwrap();
store
.save_to_stream(
format,
&mut input_stream,
&mut output_stream,
&signer,
&context,
)
.unwrap();
store.commit_claim(claim_capture).unwrap();
output_stream.rewind().unwrap();
let mut temp_stream = Cursor::new(Vec::new());
store
.save_to_stream(
format,
&mut output_stream,
&mut temp_stream,
&signer,
&context,
)
.unwrap();
store.commit_claim(claim2).unwrap();
temp_stream.rewind().unwrap();
output_stream.rewind().unwrap();
store
.save_to_stream(
format,
&mut temp_stream,
&mut output_stream,
signer.as_ref(),
&context,
)
.unwrap();
println!("Provenance: {}\n", store.provenance_path().unwrap());
let mut report = StatusTracker::default();
let _some_binary_data: Vec<u8> = vec![
0x0d, 0x0e, 0x0a, 0x0d, 0x0b, 0x0e, 0x0e, 0x0f, 0x0a, 0x0d, 0x0b, 0x0e, 0x0a, 0x0d, 0x0b,
0x0e,
];
output_stream.rewind().unwrap();
let new_store = Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();
for claim in new_store.claims() {
let _restored_json = claim
.to_json(AssertionStoreJsonFormat::OrderedList, false)
.unwrap();
let _orig_json = store
.get_claim(claim.label())
.unwrap()
.to_json(AssertionStoreJsonFormat::OrderedList, false)
.unwrap();
println!(
"Claim: {} \n{}",
claim.label(),
claim
.to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true)
.expect("could not restore from json")
);
for hashed_uri in claim.assertions() {
let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url());
claim
.get_claim_assertion(&label, instance)
.expect("Should find assertion");
}
}
}
#[test]
fn test_heic() {
let context = crate::context::Context::new();
let (format, mut input_stream, mut output_stream) = create_test_streams("sample1.heic");
let mut store = Store::from_context(&context);
let claim1 = create_test_claim().unwrap();
let signer = test_signer(SigningAlg::Ps256);
store.commit_claim(claim1).unwrap();
store
.save_to_stream(
format,
&mut input_stream,
&mut output_stream,
signer.as_ref(),
&context,
)
.unwrap();
let mut report = StatusTracker::default();
output_stream.rewind().unwrap();
let new_store = Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();
for claim in new_store.claims() {
println!(
"Claim: {} \n{}",
claim.label(),
claim
.to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true)
.expect("could not restore from json")
);
for hashed_uri in claim.assertions() {
let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url());
claim
.get_claim_assertion(&label, instance)
.expect("Should find assertion");
}
}
}
#[test]
fn test_avif() {
let context = crate::context::Context::new();
let (format, mut input_stream, mut output_stream) = create_test_streams("sample1.avif");
let mut store = Store::from_context(&context);
let claim1 = create_test_claim().unwrap();
let signer = test_signer(SigningAlg::Ps256);
store.commit_claim(claim1).unwrap();
store
.save_to_stream(
format,
&mut input_stream,
&mut output_stream,
signer.as_ref(),
&context,
)
.unwrap();
let mut report = StatusTracker::default();
output_stream.rewind().unwrap();
let new_store = Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();
for claim in new_store.claims() {
println!(
"Claim: {} \n{}",
claim.label(),
claim
.to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true)
.expect("could not restore from json")
);
for hashed_uri in claim.assertions() {
let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url());
claim
.get_claim_assertion(&label, instance)
.expect("Should find assertion");
}
}
}
#[test]
fn test_heif() {
let context = crate::context::Context::new();
let (format, mut input_stream, mut output_stream) = create_test_streams("sample1.heif");
let mut store = Store::from_context(&context);
let claim1 = create_test_claim().unwrap();
let signer = test_signer(SigningAlg::Ps256);
store.commit_claim(claim1).unwrap();
store
.save_to_stream(
format,
&mut input_stream,
&mut output_stream,
signer.as_ref(),
&context,
)
.unwrap();
let mut report = StatusTracker::default();
output_stream.rewind().unwrap();
let new_store = Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();
for claim in new_store.claims() {
println!(
"Claim: {} \n{}",
claim.label(),
claim
.to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true)
.expect("could not restore from json")
);
for hashed_uri in claim.assertions() {
let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url());
claim
.get_claim_assertion(&label, instance)
.expect("Should find assertion");
}
}
}
#[test]
#[ignore = "This is not generating the expected error. Needs investigation."]
fn test_manifest_bad_sig() {
let context = Context::new();
let (format, mut input_stream, _output_stream) = create_test_streams("CIE-sig-CA.jpg");
let tracker = &mut StatusTracker::default();
let result = Store::from_stream(format, &mut input_stream, tracker, &context);
assert!(result.is_ok());
println!("Error report: {tracker:?}");
assert!(tracker.has_error(Error::AssertionInvalidRedaction));
}
#[test]
fn test_unsupported_type_without_external_manifest() {
let context = Context::new();
let (format, mut input_stream, _output_stream) = create_test_streams("unsupported_type.txt");
let mut report = StatusTracker::default();
let result = Store::from_stream(format, &mut input_stream, &mut report, &context);
if cfg!(feature = "unstable_plain_text") {
assert!(matches!(result, Err(Error::JumbfNotFound)));
assert!(report.has_error(Error::JumbfNotFound));
} else {
assert!(matches!(result, Err(Error::UnsupportedType)));
assert!(report.has_error(Error::UnsupportedType));
}
println!("Error report: {report:?}");
assert!(!report.logged_items().is_empty());
}
fn make_brotli_bomb_jumbf(decompressed_size: usize) -> Vec<u8> {
use brotli::enc::BrotliEncoderParams;
use crate::jumbf::boxes::{
BMFFBox, Cai, JUMBFBrotliContentBox, JUMBFSuperBox, CAI_MANIFEST_UUID,
};
let payload = vec![0u8; decompressed_size];
let mut compressed = Vec::new();
brotli::BrotliCompress(
&mut Cursor::new(payload.as_slice()),
&mut compressed,
&BrotliEncoderParams::default(),
)
.expect("brotli compress");
let brotli_box = JUMBFBrotliContentBox::new(compressed);
let mut manifest_sbox = JUMBFSuperBox::new("test.manifest", Some(CAI_MANIFEST_UUID));
manifest_sbox.add_data_box(Box::new(brotli_box));
let mut cai = Cai::new();
cai.add_box(Box::new(manifest_sbox));
let mut bytes = Vec::new();
cai.super_box()
.write_box(&mut bytes)
.expect("serialize cai block");
bytes
}
#[test]
fn test_from_jumbf_rejects_brotli_bomb_via_thread_local() {
crate::settings::set_settings_value("core.max_decompressed_manifest_size_in_mb", 1usize)
.unwrap();
let result = Store::from_jumbf(
&make_brotli_bomb_jumbf(2 * 1024 * 1024),
&mut StatusTracker::default(),
);
crate::settings::reset_default_settings().unwrap();
assert!(result.is_err());
}
#[test]
fn test_from_jumbf_with_context_rejects_brotli_bomb() {
let settings = Settings::default()
.with_value("core.max_decompressed_manifest_size_in_mb", 1usize)
.unwrap();
let context = Context::new().with_settings(settings).unwrap();
let result = Store::from_jumbf_with_context(
&make_brotli_bomb_jumbf(2 * 1024 * 1024),
&mut StatusTracker::default(),
&context,
);
assert!(result.is_err());
}
#[test]
fn test_from_jumbf_reads_c2md_standard_manifest() {
use crate::{
jumbf::boxes::{CAI_MANIFEST_C2MD_UUID, CAI_MANIFEST_UUID},
utils::test::create_test_store,
};
let store = create_test_store().unwrap();
let jumbf = store.to_jumbf_internal(0).unwrap();
let c2ma = hex::decode(CAI_MANIFEST_UUID).unwrap();
let c2md = hex::decode(CAI_MANIFEST_C2MD_UUID).unwrap();
let pos = jumbf
.windows(c2ma.len())
.position(|w| w == c2ma.as_slice())
.expect("manifest c2ma UUID present");
let mut patched = jumbf.clone();
patched[pos..pos + c2md.len()].copy_from_slice(&c2md);
let mut log = StatusTracker::default();
let restored = Store::from_jumbf(&patched, &mut log).expect("c2md manifest should be read");
assert_eq!(
restored.claims().len(),
store.claims().len(),
"c2md manifest should be read as a standard manifest"
);
}
#[test]
fn test_c2md_manifest_preserved_on_round_trip() {
use crate::{
jumbf::boxes::{CAI_MANIFEST_C2MD_UUID, CAI_MANIFEST_UUID},
utils::test::create_test_store,
};
let store = create_test_store().unwrap();
let jumbf = store.to_jumbf_internal(0).unwrap();
let c2ma = hex::decode(CAI_MANIFEST_UUID).unwrap();
let c2md = hex::decode(CAI_MANIFEST_C2MD_UUID).unwrap();
let pos = jumbf
.windows(c2ma.len())
.position(|w| w == c2ma.as_slice())
.expect("manifest c2ma UUID present");
let mut patched = jumbf.clone();
patched[pos..pos + c2md.len()].copy_from_slice(&c2md);
let mut log = StatusTracker::default();
let restored = Store::from_jumbf(&patched, &mut log).expect("c2md manifest should be read");
let round_tripped = restored.to_jumbf_internal(0).unwrap();
let c2md_count = round_tripped
.windows(c2md.len())
.filter(|w| *w == c2md.as_slice())
.count();
let c2ma_count = round_tripped
.windows(c2ma.len())
.filter(|w| *w == c2ma.as_slice())
.count();
assert_eq!(
c2md_count, 1,
"c2md tag should survive re-serialization of the store"
);
assert_eq!(
c2ma_count, 0,
"a c2md manifest must not be rewritten as c2ma on round-trip"
);
}
#[test]
fn test_manifest_box_hash_uses_box_uuid_not_label() {
use crate::utils::test::create_test_store;
let store = create_test_store().unwrap();
let jumbf = store.to_jumbf_internal(0).unwrap();
let mut log = StatusTracker::default();
let baseline = Store::from_jumbf(&jumbf, &mut log).unwrap();
let baseline_pc = baseline.provenance_claim().unwrap();
let baseline_hash = baseline
.get_manifest_box_hashes(baseline_pc)
.manifest_box_hash;
assert!(!baseline_hash.is_empty());
let old_label = b"c2pa.signature\0";
let new_label = b"c2pa.databoxes";
let pos = jumbf
.windows(old_label.len())
.rposition(|w| w == old_label)
.expect("signature box label present");
let mut patched = jumbf.clone();
patched[pos..pos + new_label.len()].copy_from_slice(new_label);
let mut log = StatusTracker::default();
let tampered = Store::from_jumbf(&patched, &mut log).unwrap();
let tampered_pc = tampered.provenance_claim().unwrap();
let tampered_hash = tampered
.get_manifest_box_hashes(tampered_pc)
.manifest_box_hash;
assert_eq!(
baseline_hash, tampered_hash,
"manifest box hash must be derived from box UUIDs, not labels"
);
}
#[test]
fn test_duplicate_assertion_store_box_is_rejected() {
use crate::{
jumbf::boxes::{CAI_ASSERTION_STORE_UUID, CAI_MANIFEST_UUID},
utils::test::create_test_store,
};
fn superbox_range(jumbf: &[u8], uuid_hex: &str) -> (usize, usize) {
let mut needle = b"jumd".to_vec();
needle.extend_from_slice(&hex::decode(uuid_hex).unwrap());
let jumd_pos = jumbf
.windows(needle.len())
.position(|w| w == needle.as_slice())
.expect("superbox present");
let start = jumd_pos - 12;
let size = u32::from_be_bytes(jumbf[start..start + 4].try_into().unwrap()) as usize;
(start, size)
}
fn add_u32(buf: &mut [u8], at: usize, delta: usize) {
let v = u32::from_be_bytes(buf[at..at + 4].try_into().unwrap());
buf[at..at + 4].copy_from_slice(&(v + delta as u32).to_be_bytes());
}
let store = create_test_store().unwrap();
let jumbf = store.to_jumbf_internal(0).unwrap();
let mut log = StatusTracker::default();
Store::from_jumbf(&jumbf, &mut log).expect("baseline manifest should read");
let (as_start, as_len) = superbox_range(&jumbf, CAI_ASSERTION_STORE_UUID);
let as_end = as_start + as_len;
let (manifest_start, _) = superbox_range(&jumbf, CAI_MANIFEST_UUID);
let mut patched = Vec::with_capacity(jumbf.len() + as_len);
patched.extend_from_slice(&jumbf[..as_end]);
patched.extend_from_slice(&jumbf[as_start..as_end]);
patched.extend_from_slice(&jumbf[as_end..]);
add_u32(&mut patched, manifest_start, as_len);
add_u32(&mut patched, 0, as_len);
let mut log = StatusTracker::default();
let result = Store::from_jumbf(&patched, &mut log);
assert!(
matches!(
result,
Err(Error::InvalidClaim(
InvalidClaimError::C2paMultipleManifestBoxes
))
),
"duplicate assertion store must be rejected, got {result:?}"
);
}
#[test]
fn test_bad_jumbf() {
let (format, mut input_stream, _output_stream) = create_test_streams("prerelease.jpg");
let mut report = StatusTracker::default();
let _r = Store::from_stream(format, &mut input_stream, &mut report, &Context::new());
println!("Error report: {report:?}");
assert!(!report.logged_items().is_empty());
assert!(report.has_error(Error::PrereleaseError));
}
#[test]
fn test_detect_byte_change() {
let (format, mut input_stream, _output_stream) = create_test_streams("XCA.jpg");
let mut report = StatusTracker::default();
Store::from_stream(format, &mut input_stream, &mut report, &Context::new()).unwrap();
println!("Error report: {report:?}");
assert!(!report.logged_items().is_empty());
assert!(report.has_status(validation_status::ASSERTION_DATAHASH_MISMATCH));
}
#[test]
fn test_old_manifest() {
let (format, mut input_stream, _output_stream) = create_test_streams("prerelease.jpg");
let mut report = StatusTracker::default();
let _r = Store::from_stream(format, &mut input_stream, &mut report, &Context::new());
println!("Error report: {report:?}");
assert!(!report.logged_items().is_empty());
let errors: Vec<&LogItem> = report.filter_errors().collect();
assert!(errors[0]
.err_val
.as_ref()
.unwrap()
.starts_with("Prerelease"));
}
#[test]
fn test_verifiable_credentials() {
use crate::utils::test::create_test_store_v1;
let context = crate::context::Context::new();
let (format, mut input_stream, mut output_stream) = create_test_streams("earth_apollo17.jpg");
let signer = test_signer(SigningAlg::Ps256);
let mut store = create_test_store_v1().unwrap();
store
.save_to_stream(
format,
&mut input_stream,
&mut output_stream,
signer.as_ref(),
&context,
)
.unwrap();
output_stream.rewind().unwrap();
let restored_store = Store::from_stream(
format,
&mut output_stream,
&mut StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError),
&context,
)
.unwrap();
let pc = restored_store.provenance_claim().unwrap();
let vc = pc.get_verifiable_credentials();
assert!(!vc.is_empty());
match &vc[0] {
AssertionData::Json(s) => {
assert!(s.contains("did:nppa:eb1bb9934d9896a374c384521410c7f14"))
}
_ => panic!("expected JSON assertion data"),
}
}
#[test]
fn test_data_box_creation() {
use crate::utils::test::create_test_store_v1;
let context = crate::context::Context::new();
let (format, mut input_stream, mut output_stream) = create_test_streams("earth_apollo17.jpg");
let signer = test_signer(SigningAlg::Ps256);
let mut store = create_test_store_v1().unwrap();
store
.save_to_stream(
format,
&mut input_stream,
&mut output_stream,
signer.as_ref(),
&context,
)
.unwrap();
output_stream.rewind().unwrap();
let restored_store = Store::from_stream(
format,
&mut output_stream,
&mut StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError),
&context,
)
.unwrap();
let pc = restored_store.provenance_claim().unwrap();
let databoxes = pc.databoxes();
assert!(!databoxes.is_empty());
for (uri, db) in databoxes {
println!(
"URI: {}, data: {}",
uri.url(),
String::from_utf8_lossy(&db.data)
);
}
}
fn patch_and_report(
fixture_name: &str,
search_bytes: &[u8],
replace_bytes: &[u8],
) -> StatusTracker {
let (format, input_stream, _output_stream) = create_test_streams(fixture_name);
let mut data = input_stream.into_inner();
patch_bytes(&mut data, search_bytes, replace_bytes).expect("patch_bytes");
let mut patched_stream = std::io::Cursor::new(data);
let mut report = StatusTracker::default();
let _r = Store::from_stream(format, &mut patched_stream, &mut report, &Context::new()); println!("report: {report:?}");
report
}
#[test]
fn test_update_manifest_v1() {
use crate::{hashed_uri::HashedUri, utils::test::create_test_store_v1};
let context = crate::context::Context::new();
let (format, mut input_stream, mut output_stream) = create_test_streams("earth_apollo17.jpg");
let signer = test_signer(SigningAlg::Ps256);
let mut store = create_test_store_v1().unwrap();
store
.save_to_stream(
format,
&mut input_stream,
&mut output_stream,
signer.as_ref(),
&context,
)
.unwrap();
let mut report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
output_stream.rewind().unwrap();
let restored_store =
Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();
let pc = restored_store.provenance_claim().unwrap();
assert!(!pc.update_manifest());
let mut claim = Claim::new("adobe unit test", Some("update_manifest"), 1);
output_stream.rewind().unwrap();
let mut new_store = Store::load_ingredient_to_claim(
&mut claim,
&load_jumbf_from_stream(format, &mut output_stream).unwrap(),
None,
&context,
)
.unwrap();
let ingredient_hashes = new_store.get_manifest_box_hashes(pc);
let parent_hashed_uri = HashedUri::new(
restored_store.provenance_path().unwrap(),
Some(pc.alg().to_string()),
&ingredient_hashes.manifest_box_hash,
);
let ingredient = Ingredient::new_v2("update_manifest.jpg", "image/jpeg")
.set_parent()
.set_c2pa_manifest_from_hashed_uri(Some(parent_hashed_uri));
claim.add_assertion(&ingredient).unwrap();
new_store.commit_update_manifest(claim).unwrap();
output_stream.rewind().unwrap();
let mut output_stream2 = std::io::Cursor::new(Vec::new());
new_store
.save_to_stream(
format,
&mut output_stream,
&mut output_stream2,
signer.as_ref(),
&context,
)
.unwrap();
output_stream2.rewind().unwrap();
let um_store = Store::from_stream(format, &mut output_stream2, &mut report, &context).unwrap();
let um = um_store.provenance_claim().unwrap();
assert!(um.update_manifest());
assert!(!report.has_any_error());
}
#[test]
fn test_bad_update_manifest_v1() {
use crate::{hashed_uri::HashedUri, utils::test::create_test_store_v1};
let context = crate::context::Context::new();
let (format, mut input_stream, mut output_stream) = create_test_streams("earth_apollo17.jpg");
let signer = test_signer(SigningAlg::Ps256);
let mut store = create_test_store_v1().unwrap();
store
.save_to_stream(
format,
&mut input_stream,
&mut output_stream,
signer.as_ref(),
&context,
)
.unwrap();
let mut report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
output_stream.rewind().unwrap();
let restored_store =
Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();
let pc = restored_store.provenance_claim().unwrap();
assert!(!pc.update_manifest());
let mut claim = Claim::new("adobe unit test", Some("update_manifest"), 1);
output_stream.rewind().unwrap();
let mut new_store = Store::load_ingredient_to_claim(
&mut claim,
&load_jumbf_from_stream(format, &mut output_stream).unwrap(),
None,
&context,
)
.unwrap();
let ingredient_hashes = new_store.get_manifest_box_hashes(pc);
let parent_hashed_uri = HashedUri::new(
restored_store.provenance_path().unwrap(),
Some(pc.alg().to_string()),
&ingredient_hashes.manifest_box_hash,
);
let ingredient = Ingredient::new_v3(Relationship::ParentOf)
.set_parent()
.set_c2pa_manifest_from_hashed_uri(Some(parent_hashed_uri));
claim.add_assertion(&ingredient).unwrap();
assert!(new_store.commit_update_manifest(claim).is_err());
}
#[test]
fn test_update_manifest_v2() {
use crate::{
hashed_uri::HashedUri, jumbf::labels::to_signature_uri, utils::test::create_test_store_v1,
ClaimGeneratorInfo, ValidationResults,
};
let context = crate::context::Context::new();
let signer = test_signer(SigningAlg::Ps256);
let (format, mut input_stream, mut output_stream) = create_test_streams("earth_apollo17.jpg");
let mut store = create_test_store_v1().unwrap();
store
.save_to_stream(
format,
&mut input_stream,
&mut output_stream,
signer.as_ref(),
&context,
)
.unwrap();
let mut report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
output_stream.rewind().unwrap();
let ingredient_vec = output_stream.get_ref().clone();
let mut ingredient_stream = Cursor::new(ingredient_vec);
let restored_store =
Store::from_stream(format, &mut ingredient_stream, &mut report, &context).unwrap();
let pc = restored_store.provenance_claim().unwrap();
assert!(!pc.update_manifest());
let mut claim = Claim::new("adobe unit test", Some("update_manifest_vendor"), 2);
let cgi = ClaimGeneratorInfo::new("claim_v2_unit_test");
claim.add_claim_generator_info(cgi);
ingredient_stream.rewind().unwrap();
let (manifest_bytes, _) =
Store::load_jumbf_from_stream(format, &mut ingredient_stream, &context).unwrap();
let mut new_store =
Store::load_ingredient_to_claim(&mut claim, &manifest_bytes, None, &context).unwrap();
let ingredient_hashes = new_store.get_manifest_box_hashes(pc);
let parent_hashed_uri = HashedUri::new(
restored_store.provenance_path().unwrap(),
Some(pc.alg().to_string()),
&ingredient_hashes.manifest_box_hash,
);
let signature_hashed_uri = HashedUri::new(
to_signature_uri(pc.label()),
Some(pc.alg().to_string()),
&ingredient_hashes.signature_box_hash,
);
let validation_results = ValidationResults::from_store(&restored_store, &report);
let ingredient = Ingredient::new_v3(Relationship::ParentOf)
.set_active_manifests_and_signature_from_hashed_uri(
Some(parent_hashed_uri),
Some(signature_hashed_uri),
) .set_validation_results(Some(validation_results));
claim.add_assertion(&ingredient).unwrap();
let ingredient = claim.ingredient_assertions()[0];
let ingregient_uri = to_assertion_uri(claim.label(), &ingredient.label());
let ingredient_hashed_uri = HashedUri::new(
ingregient_uri,
Some(claim.alg().to_owned()),
ingredient.hash(),
);
let opened = Action::new("c2pa.opened")
.set_parameter("ingredients", vec![ingredient_hashed_uri])
.unwrap();
let em = Action::new("c2pa.edited.metadata");
let actions = Actions::new().add_action(opened).add_action(em);
claim.add_assertion(&actions).unwrap();
new_store.commit_update_manifest(claim).unwrap();
output_stream.rewind().unwrap();
let mut output_stream2 = std::io::Cursor::new(Vec::new());
new_store
.save_to_stream(
format,
&mut output_stream,
&mut output_stream2,
signer.as_ref(),
&context,
)
.unwrap();
let mut um_report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
output_stream2.rewind().unwrap();
let um_store =
Store::from_stream(format, &mut output_stream2, &mut um_report, &context).unwrap();
let um = um_store.provenance_claim().unwrap();
assert!(um.update_manifest());
assert!(!um_report.has_any_error());
}
#[test]
#[cfg(feature = "file_io")]
fn test_update_manifest_v2_bmff() {
use crate::{
hashed_uri::HashedUri, jumbf::labels::to_signature_uri, ClaimGeneratorInfo,
ValidationResults,
};
let context = crate::context::Context::new();
let signer = test_signer(SigningAlg::Ps256);
let (format, mut input_stream, mut output_stream) = create_test_streams("video1.mp4");
let mut report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
let mut store = Store::from_stream(format, &mut input_stream, &mut report, &context).unwrap();
let pc = store.provenance_claim().unwrap();
let mut claim = Claim::new("adobe unit test", Some("update_manifest_vendor"), 2);
let cgi = ClaimGeneratorInfo::new("claim_v2_unit_test");
claim.add_claim_generator_info(cgi);
let ingredient_hashes = store.get_manifest_box_hashes(pc);
let parent_hashed_uri = HashedUri::new(
store.provenance_path().unwrap(),
Some(pc.alg().to_string()),
&ingredient_hashes.manifest_box_hash,
);
let signature_hashed_uri = HashedUri::new(
to_signature_uri(pc.label()),
Some(pc.alg().to_string()),
&ingredient_hashes.signature_box_hash,
);
let validation_results = ValidationResults::from_store(&store, &report);
let ingredient = Ingredient::new_v3(Relationship::ParentOf)
.set_active_manifests_and_signature_from_hashed_uri(
Some(parent_hashed_uri),
Some(signature_hashed_uri),
) .set_validation_results(Some(validation_results));
claim.add_assertion(&ingredient).unwrap();
let ingredient = claim.ingredient_assertions()[0];
let ingregient_uri = to_assertion_uri(claim.label(), &ingredient.label());
let ingredient_hashed_uri = HashedUri::new(
ingregient_uri,
Some(claim.alg().to_owned()),
ingredient.hash(),
);
let opened = Action::new("c2pa.opened")
.set_parameter("ingredients", vec![ingredient_hashed_uri])
.unwrap();
let em = Action::new("c2pa.edited.metadata");
let actions = Actions::new().add_action(opened).add_action(em);
claim.add_assertion(&actions).unwrap();
store.commit_update_manifest(claim).unwrap();
store
.save_to_stream(
format,
&mut input_stream,
&mut output_stream,
signer.as_ref(),
&context,
)
.unwrap();
let mut um_report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
output_stream.rewind().unwrap();
let mut um_store =
Store::from_stream(format, &mut output_stream, &mut um_report, &context).unwrap();
let um = um_store.provenance_claim().unwrap();
assert!(um.update_manifest());
assert!(um_report.has_status(validation_status::ASSERTION_BMFFHASH_MATCH));
let mut claim2 = Claim::new("adobe unit test", Some("ordinary_manifest_vendor"), 2);
let cgi = ClaimGeneratorInfo::new("claim_v2_unit_test");
claim2.add_claim_generator_info(cgi);
let update_pc = um_store.provenance_claim().unwrap();
let ingredient_hashes = um_store.get_manifest_box_hashes(update_pc);
let parent_hashed_uri = HashedUri::new(
um_store.provenance_path().unwrap(),
Some(update_pc.alg().to_string()),
&ingredient_hashes.manifest_box_hash,
);
let signature_hashed_uri = HashedUri::new(
to_signature_uri(update_pc.label()),
Some(update_pc.alg().to_string()),
&ingredient_hashes.signature_box_hash,
);
let validation_results = ValidationResults::from_store(&um_store, &report);
let ingredient = Ingredient::new_v3(Relationship::ParentOf)
.set_active_manifests_and_signature_from_hashed_uri(
Some(parent_hashed_uri),
Some(signature_hashed_uri),
) .set_validation_results(Some(validation_results));
claim2.add_assertion(&ingredient).unwrap();
let ingredient = claim2.ingredient_assertions()[0];
let ingregient_uri = to_assertion_uri(claim2.label(), &ingredient.label());
let ingredient_hashed_uri = HashedUri::new(
ingregient_uri,
Some(claim2.alg().to_owned()),
ingredient.hash(),
);
let opened = Action::new("c2pa.opened")
.set_parameter("ingredients", vec![ingredient_hashed_uri])
.unwrap();
let editted = Action::new("c2pa.edited");
let actions = Actions::new().add_action(opened).add_action(editted);
claim2.add_assertion(&actions).unwrap();
um_store.commit_claim(claim2).unwrap();
output_stream.rewind().unwrap();
let mut output_stream2 = Cursor::new(Vec::new());
um_store
.save_to_stream(
format,
&mut output_stream,
&mut output_stream2,
signer.as_ref(),
&context,
)
.unwrap();
let mut collapsed_report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
output_stream2.rewind().unwrap();
let collapsed_store =
Store::from_stream(format, &mut output_stream2, &mut collapsed_report, &context).unwrap();
let cm = collapsed_store.provenance_claim().unwrap();
assert!(!cm.update_manifest());
assert!(collapsed_report.has_status(validation_status::ASSERTION_BMFFHASH_MATCH));
}
#[test]
#[cfg(feature = "file_io")]
fn test_update_manifest_with_timestamp_assertion() {
let (format, mut input_stream, _output_stream) = create_test_streams("update_manifest.jpg");
let mut report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
let restored_store =
Store::from_stream(format, &mut input_stream, &mut report, &Context::new()).unwrap();
let pc = restored_store.provenance_claim().unwrap();
assert!(pc.update_manifest());
}
#[test]
#[cfg(all(feature = "openssl", not(target_arch = "wasm32")))]
fn test_v1_timestamp_rejects_sha1_message_imprint() {
use openssl::hash::MessageDigest;
use crate::{
crypto::{
cose::CertificateTrustPolicy,
time_stamp::verify::message_imprint_regression::{build_token, OID_SHA1, OID_SHA256},
},
utils::test::create_test_store_v1,
};
let context = Context::new();
let (format, mut input_stream, mut output_stream) = create_test_streams("earth_apollo17.jpg");
let signer = test_signer(SigningAlg::Ps256);
let mut store = create_test_store_v1().unwrap();
store
.save_to_stream(
format,
&mut input_stream,
&mut output_stream,
signer.as_ref(),
&context,
)
.unwrap();
let mut report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
output_stream.rewind().unwrap();
let restored_store =
Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();
let claim = restored_store.provenance_claim().unwrap();
assert_eq!(claim.version(), 1);
let signature = claim.cose_sign1().unwrap().signature;
let ctp = CertificateTrustPolicy::default();
let sha256_token = build_token(OID_SHA256, MessageDigest::sha256(), &signature);
assert!(Store::verified_timestamp_for_claim(claim, &sha256_token, &ctp).is_some());
let sha1_token = build_token(OID_SHA1, MessageDigest::sha1(), &signature);
assert!(Store::verified_timestamp_for_claim(claim, &sha1_token, &ctp).is_none());
}
fn add_ingredient_assertion_to_claim(
store: &Store,
claim: &mut Claim,
action: &str,
relationship: Relationship,
instance: Option<usize>,
) -> Result<()> {
use crate::{hashed_uri::HashedUri, jumbf::labels::to_signature_uri, ValidationResults};
let report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
let pc = store.provenance_claim().unwrap();
let ingredient_hashes = store.get_manifest_box_hashes(pc);
let parent_hashed_uri = HashedUri::new(
store.provenance_path().unwrap(),
Some(pc.alg().to_string()),
&ingredient_hashes.manifest_box_hash,
);
let signature_hashed_uri = HashedUri::new(
to_signature_uri(pc.label()),
Some(pc.alg().to_string()),
&ingredient_hashes.signature_box_hash,
);
let validation_results = ValidationResults::from_store(store, &report);
let ingredient = Ingredient::new_v3(relationship)
.set_active_manifests_and_signature_from_hashed_uri(
Some(parent_hashed_uri),
Some(signature_hashed_uri),
) .set_validation_results(Some(validation_results));
claim.add_assertion(&ingredient).unwrap();
let ingredient = claim.ingredient_assertions()[0];
let mut ingredient_uri = to_assertion_uri(claim.label(), &ingredient.label());
if let Some(instance) = instance {
ingredient_uri.push_str(&format!("__{instance}"));
}
let ingredient_hashed_uri = HashedUri::new(
ingredient_uri,
Some(claim.alg().to_owned()),
ingredient.hash(),
);
let a = Action::new(action)
.set_parameter("ingredients", vec![ingredient_hashed_uri])
.unwrap();
let actions = Actions::new().add_action(a);
claim.add_assertion(&actions).unwrap();
Ok(())
}
#[test]
fn test_ingredient_conflict_with_current_manifest() {
use crate::{
hashed_uri::HashedUri, jumbf::labels::to_signature_uri, utils::test::create_test_store_v1,
ClaimGeneratorInfo, ValidationResults,
};
let context = crate::context::Context::new();
let signer = test_signer(SigningAlg::Ps256);
let (format, mut input_stream, mut output_stream) = create_test_streams("earth_apollo17.jpg");
let mut store = create_test_store_v1().unwrap();
store
.save_to_stream(
format,
&mut input_stream,
&mut output_stream,
signer.as_ref(),
&context,
)
.unwrap();
let mut report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
output_stream.rewind().unwrap();
let ingredient_vec = output_stream.get_ref().clone();
let mut ingredient_stream = Cursor::new(ingredient_vec.clone());
let restored_store =
Store::from_stream(format, &mut ingredient_stream, &mut report, &context).unwrap();
let pc = restored_store.provenance_claim().unwrap();
assert!(!pc.update_manifest());
let mut claim = Claim::new("adobe unit test", Some("update_manifest_1"), 2);
let cgi = ClaimGeneratorInfo::new("claim_v2_unit_test");
claim.add_claim_generator_info(cgi);
let redacted_uri = to_assertion_uri(pc.label(), labels::SCHEMA_ORG_INTERNAL);
let (manifest_bytes, _) =
Store::load_jumbf_from_stream(format, &mut Cursor::new(ingredient_vec.clone()), &context)
.unwrap();
let mut redacted_store = Store::load_ingredient_to_claim(
&mut claim,
&manifest_bytes,
Some(vec![redacted_uri.clone()]),
&context,
)
.unwrap();
let ingredient_hashes = restored_store.get_manifest_box_hashes(pc);
let parent_hashed_uri = HashedUri::new(
restored_store.provenance_path().unwrap(),
Some(pc.alg().to_string()),
&ingredient_hashes.manifest_box_hash,
);
let signature_hashed_uri = HashedUri::new(
to_signature_uri(pc.label()),
Some(pc.alg().to_string()),
&ingredient_hashes.signature_box_hash,
);
let validation_results = ValidationResults::from_store(&restored_store, &report);
let ingredient = Ingredient::new_v3(Relationship::ParentOf)
.set_active_manifests_and_signature_from_hashed_uri(
Some(parent_hashed_uri),
Some(signature_hashed_uri),
) .set_validation_results(Some(validation_results));
claim.add_assertion(&ingredient).unwrap();
let ingredient = claim.ingredient_assertions()[0];
let ingregient_uri = to_assertion_uri(claim.label(), &ingredient.label());
let ingredient_hashed_uri = HashedUri::new(
ingregient_uri,
Some(claim.alg().to_owned()),
ingredient.hash(),
);
let opened = Action::new("c2pa.opened")
.set_parameter("ingredients", vec![ingredient_hashed_uri])
.unwrap();
let em = Action::new("c2pa.edited.metadata");
let actions = Actions::new().add_action(opened).add_action(em);
claim.add_assertion(&actions).unwrap();
redacted_store.commit_update_manifest(claim).unwrap();
output_stream.rewind().unwrap();
let mut output_stream2 = std::io::Cursor::new(Vec::new());
redacted_store
.save_to_stream(
format,
&mut output_stream,
&mut output_stream2,
signer.as_ref(),
&context,
)
.unwrap();
let mut um_report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
output_stream2.rewind().unwrap();
let um_store =
Store::from_stream(format, &mut output_stream2, &mut um_report, &context).unwrap();
let um = um_store.provenance_claim().unwrap();
assert!(um.update_manifest());
assert!(!um_report.has_any_error());
let mut new_claim = Claim::new("adobe unit test", Some("merged_manifests"), 2);
let cgi = ClaimGeneratorInfo::new("claim_v2_unit_test");
new_claim.add_claim_generator_info(cgi);
let mut original_log = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
let (original_manifest_bytes, _) =
Store::load_jumbf_from_stream(format, &mut Cursor::new(ingredient_vec), &context).unwrap();
let original_store = Store::from_jumbf(&original_manifest_bytes, &mut original_log).unwrap();
Store::load_ingredient_to_claim(&mut new_claim, &original_manifest_bytes, None, &context)
.unwrap();
add_ingredient_assertion_to_claim(
&original_store,
&mut new_claim,
"c2pa.opened",
Relationship::ParentOf,
None,
)
.unwrap();
output_stream2.rewind().unwrap();
let mut redacted_log = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
let (redacted_manifest_bytes, _) =
Store::load_jumbf_from_stream(format, &mut output_stream2, &context).unwrap();
let redacted_store = Store::from_jumbf(&redacted_manifest_bytes, &mut redacted_log).unwrap();
Store::load_ingredient_to_claim(
&mut new_claim,
&redacted_manifest_bytes,
Some(vec![redacted_uri.clone()]),
&context,
)
.unwrap();
add_ingredient_assertion_to_claim(
&redacted_store,
&mut new_claim,
"c2pa.placed",
Relationship::ComponentOf,
Some(1),
)
.unwrap();
output_stream2.rewind().unwrap();
let mut output_stream3 = std::io::Cursor::new(Vec::new());
let mut final_store = Store::from_context(&Context::new());
final_store.commit_claim(new_claim).unwrap();
final_store
.save_to_stream(
format,
&mut output_stream2,
&mut output_stream3,
signer.as_ref(),
&context,
)
.unwrap();
output_stream3.rewind().unwrap();
let mut resolved_store_report =
StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
let resolved_store = Store::from_stream(
format,
&mut output_stream3,
&mut resolved_store_report,
&context,
)
.unwrap();
let redacted_claim = resolved_store.get_claim(pc.label()).unwrap();
assert!(redacted_claim
.get_assertion(labels::SCHEMA_ORG_INTERNAL, 0)
.is_none());
}
#[test]
fn test_ingredient_labels_versioned_on_conflict() {
let context = Context::new();
let signer = test_signer(SigningAlg::Ps256);
let shared_label = "urn:c2pa:11111111-2222-4333-8444-555555555555";
let signed_ingredient = |variant: &str| -> Vec<u8> {
let mut store = Store::from_context(&Context::new());
let mut claim =
Claim::new_with_user_guid("ingredient label conflict test", shared_label, 2).unwrap();
claim.add_claim_generator_info(ClaimGeneratorInfo::new("conflict_test"));
claim
.add_databox("text/plain", variant.as_bytes().to_vec(), None)
.unwrap();
let actions = Actions::new()
.add_action(Action::new("c2pa.created").set_source_type(DigitalSourceType::Empty));
claim.add_assertion(&actions).unwrap();
store.commit_claim(claim).unwrap();
let (format, mut input, mut output) = create_test_streams("earth_apollo17.jpg");
store
.save_to_stream(format, &mut input, &mut output, signer.as_ref(), &context)
.unwrap();
output.rewind().unwrap();
let (bytes, _) = Store::load_jumbf_from_stream(format, &mut output, &context).unwrap();
bytes
};
let bytes_a = signed_ingredient("variant-a");
let bytes_b = signed_ingredient("variant-b");
let bytes_c = signed_ingredient("variant-c");
let mut claim = Claim::new(
"ingredient label conflict test top level",
Some("c2pa-rs-sdk-test"),
2,
);
claim.add_claim_generator_info(ClaimGeneratorInfo::new("outer"));
Store::load_ingredient_to_claim(&mut claim, &bytes_a, None, &context).unwrap();
Store::load_ingredient_to_claim(&mut claim, &bytes_b, None, &context).unwrap();
Store::load_ingredient_to_claim(&mut claim, &bytes_c, None, &context).unwrap();
let keys: Vec<&String> = claim.claim_ingredient_store().keys().collect();
let has = |label: &str| keys.iter().any(|k| k.as_str() == label);
assert!(has(shared_label));
assert!(
has(&format!("{shared_label}::1_1")),
"on first conflict, relabel should have version 1, got: {keys:?}"
);
assert!(
has(&format!("{shared_label}::2_1")),
"second conflict should relabel to version 2, got: {keys:?}"
);
}
#[test]
fn test_ingredient_conflict_with_incoming_manifest() {
use crate::{
hashed_uri::HashedUri, jumbf::labels::to_signature_uri, utils::test::create_test_store_v1,
ClaimGeneratorInfo, ValidationResults,
};
let context = Context::new();
let signer = test_signer(SigningAlg::Ps256);
let (format, mut input_stream, mut output_stream) = create_test_streams("earth_apollo17.jpg");
let mut store = create_test_store_v1().unwrap();
store
.save_to_stream(
format,
&mut input_stream,
&mut output_stream,
signer.as_ref(),
&context,
)
.unwrap();
let mut report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
output_stream.rewind().unwrap();
let ingredient_vec = output_stream.get_ref().clone();
let mut ingredient_stream = Cursor::new(ingredient_vec.clone());
let restored_store =
Store::from_stream(format, &mut ingredient_stream, &mut report, &context).unwrap();
let pc = restored_store.provenance_claim().unwrap();
assert!(!pc.update_manifest());
let mut claim = Claim::new("adobe unit test", Some("update_manifest_1"), 2);
let cgi = ClaimGeneratorInfo::new("claim_v2_unit_test");
claim.add_claim_generator_info(cgi);
let redacted_uri = to_assertion_uri(pc.label(), labels::SCHEMA_ORG_INTERNAL);
let (manifest_bytes, _) =
Store::load_jumbf_from_stream(format, &mut Cursor::new(ingredient_vec.clone()), &context)
.unwrap();
let mut redacted_store = Store::load_ingredient_to_claim(
&mut claim,
&manifest_bytes,
Some(vec![redacted_uri]),
&context,
)
.unwrap();
let ingredient_hashes = restored_store.get_manifest_box_hashes(pc);
let parent_hashed_uri = HashedUri::new(
restored_store.provenance_path().unwrap(),
Some(pc.alg().to_string()),
&ingredient_hashes.manifest_box_hash,
);
let signature_hashed_uri = HashedUri::new(
to_signature_uri(pc.label()),
Some(pc.alg().to_string()),
&ingredient_hashes.signature_box_hash,
);
let validation_results = ValidationResults::from_store(&restored_store, &report);
let ingredient = Ingredient::new_v3(Relationship::ParentOf)
.set_active_manifests_and_signature_from_hashed_uri(
Some(parent_hashed_uri),
Some(signature_hashed_uri),
) .set_validation_results(Some(validation_results));
claim.add_assertion(&ingredient).unwrap();
let ingredient = claim.ingredient_assertions()[0];
let ingregient_uri = to_assertion_uri(claim.label(), &ingredient.label());
let ingredient_hashed_uri = HashedUri::new(
ingregient_uri,
Some(claim.alg().to_owned()),
ingredient.hash(),
);
let opened = Action::new("c2pa.opened")
.set_parameter("ingredients", vec![ingredient_hashed_uri])
.unwrap();
let em = Action::new("c2pa.edited.metadata");
let actions = Actions::new().add_action(opened).add_action(em);
claim.add_assertion(&actions).unwrap();
redacted_store.commit_update_manifest(claim).unwrap();
output_stream.rewind().unwrap();
let mut output_stream2 = std::io::Cursor::new(Vec::new());
redacted_store
.save_to_stream(
format,
&mut output_stream,
&mut output_stream2,
signer.as_ref(),
&context,
)
.unwrap();
let mut um_report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
output_stream2.rewind().unwrap();
let um_store =
Store::from_stream(format, &mut output_stream2, &mut um_report, &context).unwrap();
let um = um_store.provenance_claim().unwrap();
assert!(um.update_manifest());
assert!(!um_report.has_any_error());
let mut new_claim = Claim::new("adobe unit test", Some("update_manifest_2"), 2);
let cgi = ClaimGeneratorInfo::new("claim_v2_unit_test");
new_claim.add_claim_generator_info(cgi);
let (original_manifest_bytes, _) =
Store::load_jumbf_from_stream(format, &mut Cursor::new(ingredient_vec), &context).unwrap();
Store::load_ingredient_to_claim(&mut new_claim, &original_manifest_bytes, None, &context)
.unwrap();
let not_redacted_claim = new_claim.claim_ingredient(pc.label()).unwrap();
assert!(not_redacted_claim
.get_assertion(labels::SCHEMA_ORG_INTERNAL, 0)
.is_some());
output_stream2.rewind().unwrap();
let (redacted_manifest_bytes, _) =
Store::load_jumbf_from_stream(format, &mut output_stream2, &context).unwrap();
Store::load_ingredient_to_claim(&mut new_claim, &redacted_manifest_bytes, None, &context)
.unwrap();
let redacted_claim = new_claim.claim_ingredient(pc.label()).unwrap();
assert!(redacted_claim
.get_assertion(labels::SCHEMA_ORG_INTERNAL, 0)
.is_none());
}
#[test]
#[cfg(feature = "file_io")]
fn test_ingredient_conflicting_redactions_to_same_manifest() {
use crate::{
hashed_uri::HashedUri, jumbf::labels::to_signature_uri, utils::test::create_test_store_v1,
ClaimGeneratorInfo, ValidationResults,
};
let context = Context::new();
let signer = test_signer(SigningAlg::Ps256);
let (format, mut input_stream, mut output_stream) = create_test_streams("earth_apollo17.jpg");
let mut store = create_test_store_v1().unwrap();
store
.save_to_stream(
format,
&mut input_stream,
&mut output_stream,
signer.as_ref(),
&context,
)
.unwrap();
let mut report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
output_stream.rewind().unwrap();
let restored_store =
Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();
let pc = restored_store.provenance_claim().unwrap();
assert!(!pc.update_manifest());
let mut claim = Claim::new("adobe unit test", Some("update_manifest_1"), 2);
let cgi = ClaimGeneratorInfo::new("claim_v2_unit_test");
claim.add_claim_generator_info(cgi.clone());
let redacted_uri = to_assertion_uri(pc.label(), labels::SCHEMA_ORG_INTERNAL);
output_stream.rewind().unwrap();
let ingredient_vec = load_jumbf_from_stream(format, &mut output_stream).unwrap();
let mut redacted_store = Store::load_ingredient_to_claim(
&mut claim,
&ingredient_vec,
Some(vec![redacted_uri]),
&context,
)
.unwrap();
let ingredient_hashes = restored_store.get_manifest_box_hashes(pc);
let parent_hashed_uri = HashedUri::new(
restored_store.provenance_path().unwrap(),
Some(pc.alg().to_string()),
&ingredient_hashes.manifest_box_hash,
);
let signature_hashed_uri = HashedUri::new(
to_signature_uri(pc.label()),
Some(pc.alg().to_string()),
&ingredient_hashes.signature_box_hash,
);
let validation_results = ValidationResults::from_store(&restored_store, &report);
let ingredient = Ingredient::new_v3(Relationship::ParentOf)
.set_active_manifests_and_signature_from_hashed_uri(
Some(parent_hashed_uri),
Some(signature_hashed_uri),
) .set_validation_results(Some(validation_results));
claim.add_assertion(&ingredient).unwrap();
let ingredient = claim.ingredient_assertions()[0];
let ingregient_uri = to_assertion_uri(claim.label(), &ingredient.label());
let ingredient_hashed_uri = HashedUri::new(
ingregient_uri,
Some(claim.alg().to_owned()),
ingredient.hash(),
);
let opened = Action::new("c2pa.opened")
.set_parameter("ingredients", vec![ingredient_hashed_uri])
.unwrap();
let em = Action::new("c2pa.edited.metadata");
let actions = Actions::new().add_action(opened).add_action(em);
claim.add_assertion(&actions).unwrap();
redacted_store.commit_update_manifest(claim).unwrap();
output_stream.rewind().unwrap();
let mut op_output = std::io::Cursor::new(Vec::new());
redacted_store
.save_to_stream(
format,
&mut output_stream,
&mut op_output,
signer.as_ref(),
&context,
)
.unwrap();
let mut um_report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
op_output.rewind().unwrap();
let um_store = Store::from_stream(format, &mut op_output, &mut um_report, &context).unwrap();
let um = um_store.provenance_claim().unwrap();
assert!(um.update_manifest());
assert!(!um_report.has_any_error());
let mut claim2 = Claim::new("adobe unit test", Some("update_manifest_1"), 2);
claim2.add_claim_generator_info(cgi);
let redacted_uri2 = to_assertion_uri(pc.label(), TEST_USER_ASSERTION);
let mut redacted_store2 = Store::load_ingredient_to_claim(
&mut claim2,
&ingredient_vec,
Some(vec![redacted_uri2]),
&context,
)
.unwrap();
let ingredient_hashes2 = restored_store.get_manifest_box_hashes(pc);
let parent_hashed_uri2 = HashedUri::new(
restored_store.provenance_path().unwrap(),
Some(pc.alg().to_string()),
&ingredient_hashes2.manifest_box_hash,
);
let signature_hashed_uri2 = HashedUri::new(
to_signature_uri(pc.label()),
Some(pc.alg().to_string()),
&ingredient_hashes2.signature_box_hash,
);
let validation_results2 = ValidationResults::from_store(&restored_store, &report);
let ingredient2 = Ingredient::new_v3(Relationship::ParentOf)
.set_active_manifests_and_signature_from_hashed_uri(
Some(parent_hashed_uri2),
Some(signature_hashed_uri2),
) .set_validation_results(Some(validation_results2));
claim2.add_assertion(&ingredient2).unwrap();
let ingredient2 = claim2.ingredient_assertions()[0];
let ingregient_uri2 = to_assertion_uri(claim2.label(), &ingredient2.label());
let ingredient_hashed_uri2 = HashedUri::new(
ingregient_uri2,
Some(claim2.alg().to_owned()),
ingredient2.hash(),
);
let opened2 = Action::new("c2pa.opened")
.set_parameter("ingredients", vec![ingredient_hashed_uri2])
.unwrap();
let em2 = Action::new("c2pa.edited.metadata");
let actions2 = Actions::new().add_action(opened2).add_action(em2);
claim2.add_assertion(&actions2).unwrap();
redacted_store2.commit_update_manifest(claim2).unwrap();
output_stream.rewind().unwrap();
let mut op2_output = std::io::Cursor::new(Vec::new());
redacted_store2
.save_to_stream(
format,
&mut output_stream,
&mut op2_output,
signer.as_ref(),
&context,
)
.unwrap();
let mut new_claim = Claim::new("adobe unit test", Some("update_manifest_2"), 2);
let cgi = ClaimGeneratorInfo::new("claim_v2_unit_test");
new_claim.add_claim_generator_info(cgi);
op_output.rewind().unwrap();
Store::load_ingredient_to_claim(
&mut new_claim,
&load_jumbf_from_stream(format, &mut op_output).unwrap(),
None,
&context,
)
.unwrap();
op2_output.rewind().unwrap();
Store::load_ingredient_to_claim(
&mut new_claim,
&load_jumbf_from_stream(format, &mut op2_output).unwrap(),
None,
&context,
)
.unwrap();
let redacted_claim = new_claim.claim_ingredient(pc.label()).unwrap();
assert!(redacted_claim
.get_assertion(labels::SCHEMA_ORG_INTERNAL, 0)
.is_none());
assert!(redacted_claim
.get_assertion(TEST_USER_ASSERTION, 0)
.is_none());
}
#[test]
fn test_forged_content_in_redacted_slot_is_rejected() {
use crate::{
assertions::C2paReason, claim::ClaimAssertionType, Builder, BuilderIntent, Reader,
};
const TEST_IMAGE: &[u8] = include_bytes!("../../tests/fixtures/CA.jpg");
const ASSERTION_LABEL: &str = "stds.schema-org.CreativeWork";
let mut input = Cursor::new(TEST_IMAGE);
let parent = Reader::default()
.with_stream("image/jpeg", &mut input)
.expect("read parent");
let parent_manifest_label = parent.active_label().unwrap().to_owned();
let redacted_uri = to_assertion_uri(&parent_manifest_label, ASSERTION_LABEL);
let mut builder = Builder::default();
builder.set_intent(BuilderIntent::Edit);
builder.definition.redactions = Some(vec![redacted_uri.clone()]);
let redacted_action = Action::new("c2pa.redacted")
.set_reason(C2paReason::PiiPresent)
.set_parameter("redacted".to_owned(), redacted_uri.clone())
.unwrap();
builder.add_action(redacted_action).unwrap();
let signer = test_signer(SigningAlg::Ps256);
let mut output = Cursor::new(Vec::new());
builder
.sign(signer.as_ref(), "image/jpeg", &mut input, &mut output)
.expect("builder sign");
output.set_position(0);
let mut load_report = StatusTracker::default();
let mut store =
Store::from_stream("image/jpeg", &mut output, &mut load_report, &Context::new())
.expect("load store");
let mut baseline_report = StatusTracker::default();
let _ = Store::verify_store(&store, None, &mut baseline_report, &Context::new());
assert!(
!baseline_report.has_status(validation_status::ASSERTION_NOT_REDACTED),
"a properly redacted (removed) assertion must not raise assertion.notRedacted"
);
let alg = store
.get_claim(&parent_manifest_label)
.expect("parent claim present")
.alg()
.to_owned();
let forged = Assertion::from_data_cbor(ASSERTION_LABEL, &[0xa1, 0x61, 0x78, 0x01]);
let forged_hash = Claim::calc_assertion_box_hash(ASSERTION_LABEL, &forged, None, &alg).unwrap();
let forged_ca = ClaimAssertion::new(
forged,
0,
&forged_hash,
&alg,
None,
ClaimAssertionType::Created,
);
store
.get_claim_mut(&parent_manifest_label)
.expect("parent claim present")
.put_assertion_store(forged_ca);
let mut attack_report = StatusTracker::default();
let _ = Store::verify_store(&store, None, &mut attack_report, &Context::new());
assert!(
attack_report.has_status(validation_status::ASSERTION_NOT_REDACTED),
"forged non-zero content at a redacted URI must be rejected with assertion.notRedacted"
);
}
#[test]
fn test_redacted_slot_content_through_reader() {
use crate::{
assertions::C2paReason, claim::ClaimAssertionType, Builder, BuilderIntent, Reader,
};
const TEST_IMAGE: &[u8] = include_bytes!("../../tests/fixtures/CA.jpg");
const ASSERTION_LABEL: &str = "stds.schema-org.CreativeWork";
fn build_redacted_store(image: &[u8]) -> (Store, String) {
let mut input = Cursor::new(image);
let parent = Reader::default()
.with_stream("image/jpeg", &mut input)
.expect("read parent");
let parent_manifest_label = parent.active_label().unwrap().to_owned();
let redacted_uri = to_assertion_uri(&parent_manifest_label, ASSERTION_LABEL);
let mut builder = Builder::default();
builder.set_intent(BuilderIntent::Edit);
builder.definition.redactions = Some(vec![redacted_uri.clone()]);
let redacted_action = Action::new("c2pa.redacted")
.set_reason(C2paReason::PiiPresent)
.set_parameter("redacted".to_owned(), redacted_uri)
.unwrap();
builder.add_action(redacted_action).unwrap();
let signer = test_signer(SigningAlg::Ps256);
let mut output = Cursor::new(Vec::new());
builder
.sign(signer.as_ref(), "image/jpeg", &mut input, &mut output)
.expect("builder sign");
output.set_position(0);
let mut load_report = StatusTracker::default();
let store =
Store::from_stream("image/jpeg", &mut output, &mut load_report, &Context::new())
.expect("load store");
(store, parent_manifest_label)
}
fn plant_assertion(store: &mut Store, parent_manifest_label: &str, assertion: Assertion) {
let alg = store
.get_claim(parent_manifest_label)
.expect("parent claim present")
.alg()
.to_owned();
let hash = Claim::calc_assertion_box_hash(ASSERTION_LABEL, &assertion, None, &alg).unwrap();
let claim_assertion =
ClaimAssertion::new(assertion, 0, &hash, &alg, None, ClaimAssertionType::Created);
store
.get_claim_mut(parent_manifest_label)
.expect("parent claim present")
.put_assertion_store(claim_assertion);
}
fn count_status(log: &StatusTracker, code: &str) -> usize {
log.logged_items()
.iter()
.filter(|item| item.validation_status.as_deref() == Some(code))
.count()
}
{
let (mut store, parent_manifest_label) = build_redacted_store(TEST_IMAGE);
let zeroed = Assertion::from_data_uuid(ASSERTION_LABEL, C2PA_REDACTION_UUID, &[0u8; 4]);
plant_assertion(&mut store, &parent_manifest_label, zeroed);
let mut validation_log = StatusTracker::default();
let _ = Store::verify_store(&store, None, &mut validation_log, &Context::new());
let mut reader = Reader::default();
let _ = reader.with_store(store, &mut validation_log);
assert_eq!(
count_status(&validation_log, validation_status::ASSERTION_NOT_REDACTED),
0,
"a present, zeroed redacted box must not raise assertion.notRedacted"
);
assert_eq!(
count_status(&validation_log, validation_status::ASSERTION_MISSING),
0,
"a present, zeroed redacted box must not be counted as missing"
);
}
{
let (mut store, parent_manifest_label) = build_redacted_store(TEST_IMAGE);
let forged = Assertion::from_data_cbor(ASSERTION_LABEL, &[0xa1, 0x61, 0x78, 0x01]);
plant_assertion(&mut store, &parent_manifest_label, forged);
let mut validation_log = StatusTracker::default();
let _ = Store::verify_store(&store, None, &mut validation_log, &Context::new());
let mut reader = Reader::default();
let _ = reader.with_store(store, &mut validation_log);
assert_eq!(
count_status(&validation_log, validation_status::ASSERTION_NOT_REDACTED),
1,
"forged non-zero content at a redacted URI must be rejected exactly once"
);
}
}
#[test]
fn test_claim_decoding() {
let report = patch_and_report("C.jpg", b"claim_generator", b"claim_generatur");
assert!(!report.logged_items().is_empty());
assert!(report.logged_items()[0]
.err_val
.as_ref()
.unwrap()
.starts_with("ClaimDecoding"))
}
#[test]
fn test_claim_modified() {
let report = patch_and_report("C.jpg", b"C.jpg", b"X.jpg");
assert!(!report.logged_items().is_empty());
assert!(report.has_status(validation_status::TIMESTAMP_MISMATCH));
}
#[test]
fn test_assertion_hash_mismatch() {
let report = patch_and_report("CA.jpg", b"brightnesscontrast", b"brightnesscontraxx");
let first_error = report.filter_errors().next().cloned().unwrap();
assert_eq!(
first_error.validation_status.as_deref(),
Some(validation_status::ASSERTION_HASHEDURI_MISMATCH)
);
}
#[test]
fn test_claim_missing() {
const SEARCH_BYTES: &[u8] =
b"c2pa_manifest\xA3\x63url\x78\x4aself#jumbf=/c2pa/contentauth:urn:uuid:";
const REPLACE_BYTES: &[u8] =
b"c2pa_manifest\xA3\x63url\x78\x4aself#jumbf=/c2pa/contentauth:urn:uuix:";
let report = patch_and_report("CIE-sig-CA.jpg", SEARCH_BYTES, REPLACE_BYTES);
assert!(report.has_status(validation_status::ASSERTION_HASHEDURI_MISMATCH));
assert!(report.has_status(validation_status::INGREDIENT_MANIFEST_MISSING));
}
#[test]
fn test_display() {
let context = Context::new();
let (format, mut input_stream, _output_stream) = create_test_streams("CA.jpg");
let mut report = StatusTracker::default();
let store =
Store::from_stream(format, &mut input_stream, &mut report, &context).expect("from_stream");
assert!(!report.has_any_error());
println!("store = {store}");
}
#[test]
fn test_no_alg() {
let (format, mut input_stream, _output_stream) = create_test_streams("no_alg.jpg");
let mut report = StatusTracker::default();
let _store = Store::from_stream(format, &mut input_stream, &mut report, &Context::new());
assert!(report.has_status(ALGORITHM_UNSUPPORTED));
}
#[test]
fn test_legacy_ingredient_hash() {
let context = Context::new();
let (format, input_stream, _output_stream) = create_test_streams("legacy_ingredient_hash.jpg");
let mut report = StatusTracker::default();
let store =
Store::from_stream(format, input_stream, &mut report, &context).expect("from_stream");
println!("store = {store}");
}
#[test]
#[ignore = "this test is not generating the expected errors - the test image cert has expired"]
fn test_bmff_legacy() {
let mut context = Context::new();
context.settings_mut().verify.verify_trust = false;
let (format, mut input_stream, _output_stream) = create_test_streams("legacy.mp4");
let mut report = StatusTracker::default();
let store = Store::from_stream(format, &mut input_stream, &mut report, &context);
println!("store = {report:#?}");
assert!(store.is_err());
assert!(report.has_error(Error::ValidationRule(
"opened, placed and removed items must have ingredient(s) parameters".into()
)));
assert!(report.filter_errors().count() == 2);
}
#[test]
fn test_bmff_fragments() {
let context = Context::new();
let init_stream_path = fixture_path("dashinit.mp4");
let segment_stream_path = fixture_path("dash1.m4s");
let init_stream = std::fs::File::open(init_stream_path).unwrap();
let init_stream = std::io::BufReader::new(init_stream);
let segment_stream = std::fs::File::open(segment_stream_path).unwrap();
let segment_stream = std::io::BufReader::new(segment_stream);
let mut report = StatusTracker::default();
let store =
Store::load_fragment_from_stream("mp4", init_stream, segment_stream, &mut report, &context)
.expect("load_from_asset");
println!("store = {store}");
}
#[test]
fn test_bmff_jumbf_generation() {
let context = crate::context::Context::new();
let (format, mut input_stream, mut output_stream) = create_test_streams("video1.mp4");
let mut store = Store::from_context(&context);
let claim1 = create_test_claim().unwrap();
let signer = test_signer(SigningAlg::Ps256);
store.commit_claim(claim1).unwrap();
store
.save_to_stream(
format,
&mut input_stream,
&mut output_stream,
signer.as_ref(),
&context,
)
.unwrap();
let mut report = StatusTracker::default();
output_stream.set_position(0);
let new_store = Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();
assert!(!report.has_any_error());
println!("store = {new_store}");
}
#[test]
fn test_bmff_jumbf_generation_qt() {
let context = crate::context::Context::new();
let (format, mut input_stream, mut output_stream) = create_test_streams("c.mov");
let mut store = Store::from_context(&context);
let claim1 = create_test_claim().unwrap();
let signer = test_signer(SigningAlg::Ps256);
store.commit_claim(claim1).unwrap();
store
.save_to_stream(
format,
&mut input_stream,
&mut output_stream,
signer.as_ref(),
&context,
)
.unwrap();
let mut report = StatusTracker::default();
output_stream.set_position(0);
let new_store = Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();
assert!(!report.has_any_error());
println!("store = {new_store}");
}
#[test]
fn test_bmff_jumbf_generation_claim_v1() {
let context = crate::context::Context::new();
let (format, mut input_stream, mut output_stream) = create_test_streams("video1.mp4");
let mut store = Store::from_context(&context);
let claim1 = crate::utils::test::create_test_claim_v1().unwrap();
let signer = test_signer(SigningAlg::Ps256);
store.commit_claim(claim1).unwrap();
store
.save_to_stream(
format,
&mut input_stream,
&mut output_stream,
signer.as_ref(),
&context,
)
.unwrap();
let mut report = StatusTracker::default();
output_stream.set_position(0);
let new_store = Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();
assert!(!report.has_any_error());
println!("store = {new_store}");
}
#[cfg(feature = "file_io")]
#[test]
fn test_jumbf_generation_with_bmffv3_fixed_block_size() {
let context = crate::context::Context::new();
crate::settings::set_settings_value("core.merkle_tree_chunk_size_in_kb", 1).unwrap();
let (format, mut input_stream, mut output_stream) =
create_test_streams("BigBuckBunny_320x180.mp4");
let mut store = Store::from_context(&context);
let claim1 = create_test_claim().unwrap();
let signer = test_signer(SigningAlg::Ps256);
store.commit_claim(claim1).unwrap();
store
.save_to_stream(
format,
&mut input_stream,
&mut output_stream,
signer.as_ref(),
&context,
)
.unwrap();
let mut report = StatusTracker::default();
output_stream.set_position(0);
let new_store = Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();
assert!(!report.has_any_error());
println!("store = {new_store}");
}
#[test]
fn test_jumbf_generation_with_bmffv3_fixed_block_size_no_proof() {
let context = crate::context::Context::new();
let (format, mut input_stream, mut output_stream) = create_test_streams("video1.mp4");
crate::settings::set_settings_value("core.merkle_tree_chunk_size_in_kb", 1).unwrap();
crate::settings::set_settings_value("core.merkle_tree_max_proofs", 0).unwrap();
let mut store = Store::from_context(&context);
let claim1 = create_test_claim().unwrap();
let signer = test_signer(SigningAlg::Ps256);
store.commit_claim(claim1).unwrap();
store
.save_to_stream(
format,
&mut input_stream,
&mut output_stream,
signer.as_ref(),
&context,
)
.unwrap();
let mut report = StatusTracker::default();
output_stream.set_position(0);
let new_store = Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();
assert!(!report.has_any_error());
println!("store = {new_store}");
}
#[test]
fn test_jumbf_generation_with_bmffv3_fixed_block_size_stream() {
let context = crate::context::Context::new();
let (format, mut input_stream, mut output_stream) = create_test_streams("video1.mp4");
crate::settings::set_settings_value("core.merkle_tree_chunk_size_in_kb", 1).unwrap();
let mut store = Store::from_context(&context);
let claim1 = create_test_claim().unwrap();
let signer = test_signer(SigningAlg::Ps256);
store.commit_claim(claim1).unwrap();
store
.save_to_stream(
format,
&mut input_stream,
&mut output_stream,
signer.as_ref(),
&context,
)
.unwrap();
let mut report = StatusTracker::default();
output_stream.set_position(0);
let new_store = Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();
assert!(!report.has_any_error());
println!("store = {new_store}");
}
#[test]
fn test_bmff_jumbf_stream_generation() {
let mut context = Context::new();
context.settings_mut().verify.verify_after_reading = false;
let (format, mut input_stream, mut output_stream) = create_test_streams("video1.mp4");
let mut store = Store::from_context(&context);
let claim1 = create_test_claim().unwrap();
let signer = test_signer(SigningAlg::Ps256);
store.commit_claim(claim1).unwrap();
store
.save_to_stream(
format,
&mut input_stream,
&mut output_stream,
signer.as_ref(),
&context,
)
.unwrap();
let mut report = StatusTracker::default();
output_stream.set_position(0);
let (manifest_bytes, _) =
Store::load_jumbf_from_stream(format, &mut input_stream, &context).unwrap();
let _new_store = {
Store::from_manifest_data_and_stream(
&manifest_bytes,
format,
&mut output_stream,
&mut report,
&context,
)
.unwrap()
};
println!("report = {report:#?}");
assert!(!report.has_any_error());
}
#[test]
fn test_removed_jumbf() {
let context = Context::new();
let (format, mut input_stream, _output_stream) = create_test_streams("no_manifest.jpg");
let mut report = StatusTracker::default();
let result = Store::from_stream(format, &mut input_stream, &mut report, &context);
assert!(result.is_err());
assert!(matches!(result, Err(Error::JumbfNotFound)));
assert!(report.has_error(Error::JumbfNotFound));
}
fn external_manifest_test(file_name: &str) {
use crate::utils::test::{run_file_test, TestFileSetup};
run_file_test(file_name, |setup: &TestFileSetup| {
let context = crate::context::Context::new();
let mut store = Store::from_context(&context);
let mut claim = create_test_claim().unwrap();
let signer = test_signer(SigningAlg::Ps256);
claim.set_remote_manifest(setup.sidecar_url()).unwrap();
store.commit_claim(claim).unwrap();
let (format, mut input_stream, mut output_stream) = setup.create_streams();
println!("format = {format}");
let saved_manifest = store
.save_to_stream(
format,
&mut input_stream,
&mut output_stream,
signer.as_ref(),
&context,
)
.unwrap();
let mut asset_reader = std::fs::File::open(&setup.output_path).unwrap();
let ext_ref = crate::utils::xmp_inmemory_utils::XmpInfo::from_source(
&mut asset_reader,
setup.extension(),
)
.provenance
.unwrap();
assert_eq!(ext_ref.to_lowercase(), setup.sidecar_url().to_lowercase());
let mut validation_log =
StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
let mut validation_stream = std::fs::File::open(&setup.output_path).unwrap();
Store::from_manifest_data_and_stream(
&saved_manifest,
format,
&mut validation_stream,
&mut validation_log,
&context,
)
.unwrap();
});
}
#[test]
fn test_external_manifest_embedded_png() {
external_manifest_test("libpng-test.png");
}
#[test]
fn test_external_manifest_embedded_tiff() {
external_manifest_test("TUSCANY.TIF");
}
#[test]
fn test_external_manifest_embedded_webp() {
external_manifest_test("sample1.webp");
}
#[test]
fn test_user_guid_external_manifest_embedded() {
let context = crate::context::Context::new();
let (format, mut input_stream, mut output_stream) = create_test_streams("libpng-test.png");
let mut store = Store::from_context(&context);
let mut claim = create_test_claim().unwrap();
let signer = test_signer(SigningAlg::Ps256);
let fp = format!("file:/{}", "temp_sidecar.c2pa");
let url = url::Url::parse(&fp).unwrap();
let url_string: String = url.into();
claim.set_embed_remote_manifest(url_string.clone()).unwrap();
store.commit_claim(claim).unwrap();
let saved_manifest = store
.save_to_stream(
format,
&mut input_stream,
&mut output_stream,
signer.as_ref(),
&context,
)
.unwrap();
output_stream.rewind().unwrap();
let output_data = output_stream.get_ref().clone();
let mut output_reader = Cursor::new(output_data);
let ext_ref =
crate::utils::xmp_inmemory_utils::XmpInfo::from_source(&mut output_reader, format)
.provenance
.unwrap();
assert_eq!(ext_ref, url_string);
let mut validation_log = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
output_stream.rewind().unwrap();
Store::from_manifest_data_and_stream(
&saved_manifest,
format,
&mut output_stream,
&mut validation_log,
&context,
)
.unwrap();
}
#[c2pa_test_async]
async fn test_jumbf_generation_stream() {
let context = crate::context::Context::new();
let file_buffer = include_bytes!("../../tests/fixtures/earth_apollo17.jpg").to_vec();
let mut buf_io = Cursor::new(file_buffer);
let mut store = Store::from_context(&context);
let claim1 = create_test_claim().unwrap();
let signer = async_test_signer(SigningAlg::Ps256);
store.commit_claim(claim1).unwrap();
let result: Vec<u8> = Vec::new();
let mut result_stream = Cursor::new(result);
store
.save_to_stream_async(
"image/jpeg",
&mut buf_io,
&mut result_stream,
signer.as_ref(),
&context,
)
.await
.unwrap();
result_stream.rewind().unwrap();
let mut report = StatusTracker::default();
let _new_store =
Store::from_stream_async("image/jpeg", &mut result_stream, &mut report, &context)
.await
.unwrap();
assert!(!report.has_any_error());
}
#[test]
fn test_tiff_jumbf_generation() {
let context = crate::context::Context::new();
let (format, mut input_stream, mut output_stream) = create_test_streams("TUSCANY.TIF");
let mut store = Store::from_context(&context);
let claim1 = create_test_claim().unwrap();
let signer = test_signer(SigningAlg::Ps256);
store.commit_claim(claim1).unwrap();
store
.save_to_stream(
format,
&mut input_stream,
&mut output_stream,
signer.as_ref(),
&context,
)
.unwrap();
println!("Provenance: {}\n", store.provenance_path().unwrap());
let mut report = StatusTracker::default();
output_stream.rewind().unwrap();
let new_store = Store::from_stream(format, &mut output_stream, &mut report, &context).unwrap();
assert!(!report.has_any_error());
for claim in new_store.claims() {
let _restored_json = claim
.to_json(AssertionStoreJsonFormat::OrderedList, false)
.unwrap();
let _orig_json = store
.get_claim(claim.label())
.unwrap()
.to_json(AssertionStoreJsonFormat::OrderedList, false)
.unwrap();
println!(
"Claim: {} \n{}",
claim.label(),
claim
.to_json(AssertionStoreJsonFormat::OrderedListNoBinary, true)
.expect("could not restore from json")
);
for hashed_uri in claim.assertions() {
let (label, instance) = Claim::assertion_label_from_link(&hashed_uri.url());
claim
.get_claim_assertion(&label, instance)
.expect("Should find assertion");
}
}
}
#[c2pa_test_async]
#[cfg(feature = "file_io")]
async fn test_boxhash_embeddable_manifest_async() {
let context = crate::context::Context::new();
let ap = fixture_path("boxhash.jpg");
let box_hash_path = fixture_path("boxhash.json");
let mut store = Store::from_context(&context);
let mut claim = create_test_claim().unwrap();
let box_hash_data = std::fs::read(box_hash_path).unwrap();
let assertion = Assertion::from_data_json(BOX_HASH, &box_hash_data).unwrap();
let box_hash = BoxHash::from_json_assertion(&assertion).unwrap();
claim.add_assertion(&box_hash).unwrap();
store.commit_claim(claim).unwrap();
let signer = async_test_signer(SigningAlg::Ps256);
let em = store
.get_box_hashed_embeddable_manifest_async(&signer, &context)
.await
.unwrap();
let cm = Store::get_composed_manifest(&em, "image/jpeg", &context).unwrap();
let jpeg_io = get_assetio_handler_from_path(&ap).unwrap();
let mut input_file = std::fs::File::open(&ap).unwrap();
let ol = jpeg_io
.get_writer("jpg")
.unwrap()
.get_object_locations(&mut input_file)
.unwrap();
let cai_loc = ol.iter().find(|o| o.htype == ObjectType::C2pa).unwrap();
jpeg_io.get_reader().read_c2pa(&mut input_file).unwrap();
let outbuf = Vec::new();
let mut out_stream = Cursor::new(outbuf);
input_file.rewind().unwrap();
let mut before = vec![0u8; usize::try_from(cai_loc.offset).unwrap()];
input_file.read_exact(before.as_mut_slice()).unwrap();
out_stream.write_all(&before).unwrap();
out_stream.write_all(&cm).unwrap();
let mut after_buf = Vec::new();
input_file.read_to_end(&mut after_buf).unwrap();
out_stream.write_all(&after_buf).unwrap();
out_stream.rewind().unwrap();
let mut report = StatusTracker::default();
let _new_store = Store::from_stream_async("image/jpeg", &mut out_stream, &mut report, &context)
.await
.unwrap();
assert!(!report.has_any_error());
}
#[test]
#[cfg(feature = "file_io")]
fn test_boxhash_embeddable_manifest() {
let context = crate::context::Context::new();
let ap = fixture_path("boxhash.jpg");
let box_hash_path = fixture_path("boxhash.json");
let mut store = Store::from_context(&context);
let mut claim = create_test_claim().unwrap();
let box_hash_data = std::fs::read(box_hash_path).unwrap();
let assertion = Assertion::from_data_json(BOX_HASH, &box_hash_data).unwrap();
let box_hash = BoxHash::from_json_assertion(&assertion).unwrap();
claim.add_assertion(&box_hash).unwrap();
store.commit_claim(claim).unwrap();
let signer = test_signer(SigningAlg::Ps256);
let em = store
.get_box_hashed_embeddable_manifest(signer.as_ref(), &context)
.unwrap();
let cm = Store::get_composed_manifest(&em, "jpg", &context).unwrap();
let jpeg_io = get_assetio_handler_from_path(&ap).unwrap();
let mut input_file = std::fs::File::open(&ap).unwrap();
let ol = jpeg_io
.get_writer("jpg")
.unwrap()
.get_object_locations(&mut input_file)
.unwrap();
let cai_loc = ol.iter().find(|o| o.htype == ObjectType::C2pa).unwrap();
jpeg_io.get_reader().read_c2pa(&mut input_file).unwrap();
let outbuf = Vec::new();
let mut out_stream = Cursor::new(outbuf);
input_file.rewind().unwrap();
let mut before = vec![0u8; usize::try_from(cai_loc.offset).unwrap()];
input_file.read_exact(before.as_mut_slice()).unwrap();
out_stream.write_all(&before).unwrap();
out_stream.write_all(&cm).unwrap();
let mut after_buf = Vec::new();
input_file.read_to_end(&mut after_buf).unwrap();
out_stream.write_all(&after_buf).unwrap();
out_stream.rewind().unwrap();
let mut report = StatusTracker::default();
let _new_store =
Store::from_stream("image/jpeg", &mut out_stream, &mut report, &context).unwrap();
assert!(!report.has_any_error());
}
#[c2pa_test_async]
#[cfg(feature = "file_io")]
async fn test_datahash_embeddable_manifest_async() {
let context = crate::context::Context::new();
use std::io::SeekFrom;
let ap = fixture_path("cloud.jpg");
let signer = async_test_signer(SigningAlg::Ps256);
let mut store = Store::from_context(&context);
let claim = create_test_claim().unwrap();
store.commit_claim(claim).unwrap();
let placeholder = store
.get_data_hashed_manifest_placeholder(signer.reserve_size(), "jpeg", &context)
.unwrap();
let temp_dir = tempdirectory().unwrap();
let output = temp_dir_path(&temp_dir, "boxhash-out.jpg");
let mut output_file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(&output)
.unwrap();
let offset = write_jpeg_placeholder_file(&placeholder, &ap, &mut output_file, None).unwrap();
let exclusion = HashRange::new(offset as u64, placeholder.len() as u64);
let exclusions = vec![exclusion];
let mut dh = DataHash::new("source_hash", "sha256");
dh.exclusions = Some(exclusions);
output_file.rewind().unwrap();
let cm = store
.get_data_hashed_embeddable_manifest_async(
&dh,
&signer,
"jpeg",
Some(&mut output_file),
&context,
)
.await
.unwrap();
output_file.seek(SeekFrom::Start(offset as u64)).unwrap();
output_file.write_all(&cm).unwrap();
output_file.rewind().unwrap();
let mut report = StatusTracker::default();
let _new_store =
Store::from_stream_async("image/jpeg", &mut output_file, &mut report, &context)
.await
.unwrap();
assert!(!report.has_any_error());
}
#[test]
#[cfg(feature = "file_io")]
fn test_datahash_embeddable_manifest() {
let context = crate::context::Context::new();
use std::io::SeekFrom;
let ap = fixture_path("cloud.jpg");
let signer = test_signer(SigningAlg::Ps256);
let mut store = Store::from_context(&context);
let claim = create_test_claim().unwrap();
store.commit_claim(claim).unwrap();
let placeholder = store
.get_data_hashed_manifest_placeholder(Signer::reserve_size(&signer), "jpeg", &context)
.unwrap();
let temp_dir = tempdirectory().unwrap();
let output = temp_dir_path(&temp_dir, "boxhash-out.jpg");
let mut output_file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(&output)
.unwrap();
let offset = write_jpeg_placeholder_file(&placeholder, &ap, &mut output_file, None).unwrap();
let exclusion = HashRange::new(offset as u64, placeholder.len() as u64);
let exclusions = vec![exclusion];
let mut dh = DataHash::new("source_hash", "sha256");
dh.exclusions = Some(exclusions);
output_file.rewind().unwrap();
let cm = store
.get_data_hashed_embeddable_manifest(
&dh,
signer.as_ref(),
"jpeg",
Some(&mut output_file),
&context,
)
.unwrap();
output_file.seek(SeekFrom::Start(offset as u64)).unwrap();
output_file.write_all(&cm).unwrap();
output_file.rewind().unwrap();
let mut report = StatusTracker::default();
let _new_store =
Store::from_stream("image/jpeg", &mut output_file, &mut report, &context).unwrap();
assert!(!report.has_any_error());
}
#[test]
fn test_sign_manifest_errors_when_dynamic_placeholders_missing() {
#[derive(Debug)]
struct TestDynamicAssertion {}
impl DynamicAssertion for TestDynamicAssertion {
fn label(&self) -> String {
"com.mycompany.myassertion".to_string()
}
fn reserve_size(&self) -> Result<usize> {
Ok(64)
}
fn content(
&self,
_label: &str,
_size: Option<usize>,
_claim: &PartialClaim,
) -> Result<DynamicAssertionContent> {
Ok(DynamicAssertionContent::Cbor(Vec::new()))
}
}
struct DynamicSigner(Box<dyn Signer>);
impl crate::Signer for DynamicSigner {
fn sign(&self, data: &[u8]) -> Result<Vec<u8>> {
self.0.sign(data)
}
fn alg(&self) -> SigningAlg {
self.0.alg()
}
fn certs(&self) -> Result<Vec<Vec<u8>>> {
self.0.certs()
}
fn reserve_size(&self) -> usize {
self.0.reserve_size()
}
fn dynamic_assertions(&self) -> Vec<Box<dyn DynamicAssertion>> {
vec![Box::new(TestDynamicAssertion {})]
}
}
let context = crate::context::Context::new();
let signer = DynamicSigner(test_signer(SigningAlg::Ps256));
let mut store = Store::from_context(&context);
store.commit_claim(create_test_claim().unwrap()).unwrap();
store
.get_data_hashed_manifest_placeholder(Signer::reserve_size(&signer), "jpeg", &context)
.unwrap();
let err = store.sign_manifest(&signer, &context).unwrap_err();
match err {
Error::BadParam(msg) => assert!(
msg.contains("com.mycompany.myassertion"),
"error should name the assertion missing a placeholder slot: {msg}"
),
other => panic!("expected Error::BadParam, got {other:?}"),
}
}
#[test]
#[cfg(feature = "file_io")]
fn test_datahash_embeddable_manifest_user_hashed() {
let context = crate::context::Context::new();
use std::io::SeekFrom;
use sha2::Digest;
let ap = fixture_path("cloud.jpg");
let mut hasher = Hasher::SHA256(Sha256::new());
let signer = test_signer(SigningAlg::Ps256);
let mut store = Store::from_context(&context);
let claim = create_test_claim().unwrap();
store.commit_claim(claim).unwrap();
let placeholder = store
.get_data_hashed_manifest_placeholder(Signer::reserve_size(&signer), "jpeg", &context)
.unwrap();
let temp_dir = tempdirectory().unwrap();
let output = temp_dir_path(&temp_dir, "boxhash-out.jpg");
let mut output_file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(&output)
.unwrap();
let offset =
write_jpeg_placeholder_file(&placeholder, &ap, &mut output_file, Some(&mut hasher))
.unwrap();
let exclusion = HashRange::new(offset as u64, placeholder.len() as u64);
let exclusions = vec![exclusion];
let mut dh = DataHash::new("source_hash", "sha256");
dh.hash = Hasher::finalize(hasher);
dh.exclusions = Some(exclusions);
let cm = store
.get_data_hashed_embeddable_manifest(&dh, signer.as_ref(), "jpeg", None, &context)
.unwrap();
output_file.seek(SeekFrom::Start(offset as u64)).unwrap();
output_file.write_all(&cm).unwrap();
output_file.rewind().unwrap();
let mut report = StatusTracker::default();
let _new_store =
Store::from_stream("image/jpeg", &mut output_file, &mut report, &context).unwrap();
assert!(!report.has_any_error());
}
#[test]
fn test_dynamic_assertions() {
let context = crate::context::Context::new();
#[derive(Serialize)]
struct TestAssertion {
my_tag: String,
}
#[derive(Debug)]
struct TestDynamicAssertion {}
impl DynamicAssertion for TestDynamicAssertion {
fn label(&self) -> String {
"com.mycompany.myassertion".to_string()
}
fn reserve_size(&self) -> Result<usize> {
let assertion = TestAssertion {
my_tag: "some value I will replace".to_string(),
};
Ok(c2pa_cbor::to_vec(&assertion)?.len())
}
fn content(
&self,
_label: &str,
_size: Option<usize>,
claim: &PartialClaim,
) -> Result<DynamicAssertionContent> {
assert!(claim
.assertions()
.inspect(|a| {
dbg!(a);
})
.any(|a| a.url().contains("c2pa.hash")));
let assertion = TestAssertion {
my_tag: "some value I will replace".to_string(),
};
Ok(DynamicAssertionContent::Cbor(
c2pa_cbor::to_vec(&assertion).unwrap(),
))
}
}
struct DynamicSigner(Box<dyn Signer>);
impl DynamicSigner {
fn new() -> Self {
Self(test_signer(SigningAlg::Ps256))
}
}
impl crate::Signer for DynamicSigner {
fn sign(&self, data: &[u8]) -> crate::error::Result<Vec<u8>> {
self.0.sign(data)
}
fn alg(&self) -> SigningAlg {
self.0.alg()
}
fn certs(&self) -> crate::Result<Vec<Vec<u8>>> {
self.0.certs()
}
fn reserve_size(&self) -> usize {
self.0.reserve_size()
}
fn time_authority_url(&self) -> Option<String> {
self.0.time_authority_url()
}
fn ocsp_val(&self) -> Option<Vec<u8>> {
self.0.ocsp_val()
}
fn dynamic_assertions(&self) -> Vec<Box<dyn crate::dynamic_assertion::DynamicAssertion>> {
vec![Box::new(TestDynamicAssertion {})]
}
}
let file_buffer = include_bytes!("../../tests/fixtures/earth_apollo17.jpg").to_vec();
let mut buf_io = Cursor::new(file_buffer);
let mut store = Store::from_context(&context);
let claim1 = create_test_claim().unwrap();
let signer = DynamicSigner::new();
store.commit_claim(claim1).unwrap();
let result: Vec<u8> = Vec::new();
let mut result_stream = Cursor::new(result);
store
.save_to_stream("jpeg", &mut buf_io, &mut result_stream, &signer, &context)
.unwrap();
result_stream.rewind().unwrap();
let mut report = StatusTracker::default();
let new_store =
Store::from_stream("image/jpeg", &mut result_stream, &mut report, &context).unwrap();
println!("new_store: {new_store}");
assert!(!report.has_any_error());
}
#[test]
fn test_dynamic_assertions_same_label_twice() {
let context = crate::context::Context::new();
#[derive(Serialize)]
struct TestAssertion {
my_tag: String,
}
#[derive(Debug)]
struct TestDynamicAssertion {
tag: String,
}
impl DynamicAssertion for TestDynamicAssertion {
fn label(&self) -> String {
"com.mycompany.myassertion".to_string()
}
fn reserve_size(&self) -> Result<usize> {
let assertion = TestAssertion {
my_tag: self.tag.clone(),
};
Ok(c2pa_cbor::to_vec(&assertion)?.len())
}
fn content(
&self,
label: &str,
_size: Option<usize>,
_claim: &PartialClaim,
) -> Result<DynamicAssertionContent> {
let expected = if self.tag.len() > 8 {
"com.mycompany.myassertion__1"
} else {
"com.mycompany.myassertion"
};
assert_eq!(label, expected);
let assertion = TestAssertion {
my_tag: self.tag.clone(),
};
Ok(DynamicAssertionContent::Cbor(
c2pa_cbor::to_vec(&assertion).unwrap(),
))
}
}
struct DynamicSigner(Box<dyn Signer>);
impl crate::Signer for DynamicSigner {
fn sign(&self, data: &[u8]) -> crate::error::Result<Vec<u8>> {
self.0.sign(data)
}
fn alg(&self) -> SigningAlg {
self.0.alg()
}
fn certs(&self) -> crate::Result<Vec<Vec<u8>>> {
self.0.certs()
}
fn reserve_size(&self) -> usize {
self.0.reserve_size()
}
fn time_authority_url(&self) -> Option<String> {
self.0.time_authority_url()
}
fn ocsp_val(&self) -> Option<Vec<u8>> {
self.0.ocsp_val()
}
fn dynamic_assertions(&self) -> Vec<Box<dyn crate::dynamic_assertion::DynamicAssertion>> {
vec![
Box::new(TestDynamicAssertion {
tag: "first".to_string(),
}),
Box::new(TestDynamicAssertion {
tag: "second, and longer".to_string(),
}),
]
}
}
let file_buffer = include_bytes!("../../tests/fixtures/earth_apollo17.jpg").to_vec();
let mut buf_io = Cursor::new(file_buffer);
let mut store = Store::from_context(&context);
let claim1 = create_test_claim().unwrap();
let signer = DynamicSigner(test_signer(SigningAlg::Ps256));
store.commit_claim(claim1).unwrap();
let mut result_stream = Cursor::new(Vec::new());
store
.save_to_stream("jpeg", &mut buf_io, &mut result_stream, &signer, &context)
.unwrap();
result_stream.rewind().unwrap();
let mut report = StatusTracker::default();
let new_store =
Store::from_stream("image/jpeg", &mut result_stream, &mut report, &context).unwrap();
assert!(!report.has_any_error(), "{report:?}");
let claim = new_store.provenance_claim().unwrap();
let mut tags = Vec::new();
for label in ["com.mycompany.myassertion", "com.mycompany.myassertion__1"] {
let ca = claim
.claim_assertion_store()
.iter()
.find(|ca| ca.label() == label)
.unwrap_or_else(|| panic!("missing {label}"));
let value: serde_json::Value = c2pa_cbor::from_slice(ca.assertion().data()).unwrap();
tags.push(value["my_tag"].as_str().unwrap().to_string());
}
assert_eq!(tags, vec!["first", "second, and longer"]);
}
#[c2pa_test_async]
async fn test_async_dynamic_assertions() {
use async_trait::async_trait;
let context = crate::context::Context::new();
#[derive(Serialize)]
struct TestAssertion {
my_tag: String,
}
#[derive(Debug)]
struct TestDynamicAssertion {}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl AsyncDynamicAssertion for TestDynamicAssertion {
fn label(&self) -> String {
"com.mycompany.myassertion".to_string()
}
fn reserve_size(&self) -> Result<usize> {
let assertion = TestAssertion {
my_tag: "some value I will replace".to_string(),
};
Ok(c2pa_cbor::to_vec(&assertion)?.len())
}
async fn content(
&self,
_label: &str,
_size: Option<usize>,
claim: &PartialClaim,
) -> Result<DynamicAssertionContent> {
assert!(claim
.assertions()
.inspect(|a| {
dbg!(a);
})
.any(|a| a.url().contains("c2pa.hash")));
let assertion = TestAssertion {
my_tag: "some value I will replace".to_string(),
};
Ok(DynamicAssertionContent::Cbor(
c2pa_cbor::to_vec(&assertion).unwrap(),
))
}
}
struct DynamicSigner(Box<dyn AsyncSigner>);
impl DynamicSigner {
fn new() -> Self {
Self(async_test_signer(SigningAlg::Ps256))
}
}
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
impl crate::AsyncSigner for DynamicSigner {
async fn sign(&self, data: Vec<u8>) -> crate::error::Result<Vec<u8>> {
self.0.sign(data).await
}
fn alg(&self) -> SigningAlg {
self.0.alg()
}
fn certs(&self) -> crate::Result<Vec<Vec<u8>>> {
self.0.certs()
}
fn reserve_size(&self) -> usize {
self.0.reserve_size()
}
fn time_authority_url(&self) -> Option<String> {
self.0.time_authority_url()
}
async fn ocsp_val(&self) -> Option<Vec<u8>> {
self.0.ocsp_val().await
}
fn dynamic_assertions(
&self,
) -> Vec<Box<dyn crate::dynamic_assertion::AsyncDynamicAssertion>> {
vec![Box::new(TestDynamicAssertion {})]
}
}
let file_buffer = include_bytes!("../../tests/fixtures/earth_apollo17.jpg").to_vec();
let mut buf_io = Cursor::new(file_buffer);
let mut store = Store::from_context(&context);
let claim1 = create_test_claim().unwrap();
let signer = DynamicSigner::new();
store.commit_claim(claim1).unwrap();
let result: Vec<u8> = Vec::new();
let mut result_stream = Cursor::new(result);
store
.save_to_stream_async("jpeg", &mut buf_io, &mut result_stream, &signer, &context)
.await
.unwrap();
result_stream.rewind().unwrap();
let mut report = StatusTracker::default();
let new_store = Store::from_stream_async("jpeg", &mut result_stream, &mut report, &context)
.await
.unwrap();
println!("new_store: {new_store}");
let result = result_stream.into_inner();
Store::verify_store_async(
&new_store,
Some(&mut ClaimAssetData::Bytes(&result, "jpg")),
&mut report,
&context,
)
.await
.unwrap();
assert!(!report.has_any_error());
}
#[test]
#[cfg(feature = "file_io")]
fn test_fragmented_jumbf_generation() {
let mut context = crate::context::Context::new();
context.settings_mut().verify.verify_after_reading = true;
let tempdir = tempdirectory().expect("temp dir");
let output_path = tempdir.path();
let mut inits = Vec::new();
for item in glob::glob(&fixture_path("bunny/**/BigBuckBunny_2s_init.mp4").to_string_lossy())
.unwrap()
.flatten()
{
inits.push(item);
}
let mut store = Store::from_context(&context);
let claim = create_test_claim().unwrap();
store.commit_claim(claim).unwrap();
let signer = test_cawg_signer(SigningAlg::Ps256, &[labels::SCHEMA_ORG_INTERNAL]).unwrap();
store
.save_to_bmff_fragmented(
&inits,
&PathBuf::from("BigBuckBunny_2s*.m4s"),
&output_path.to_path_buf(),
signer.as_ref(),
&context,
)
.unwrap();
for init_path in &inits {
let init_name = PathBuf::from(init_path.file_name().unwrap_or_default());
let init_dir = PathBuf::from(init_path.parent().unwrap().file_name().unwrap());
let mut output_file = output_path.to_path_buf();
output_file = output_file.join(&init_dir).join(&init_name);
let mut init_stream = std::fs::File::open(&output_file).unwrap();
let mut fragments = Vec::new();
let frag_glob = output_path.join(&init_dir).join("BigBuckBunny_2s*.m4s");
let frag_glob_str = frag_glob.to_str().unwrap();
for entry in glob::glob(frag_glob_str).unwrap() {
fragments.push(entry.unwrap());
}
for entry in &fragments {
let mut validation_log = StatusTracker::default();
let mut fragment_stream = std::fs::File::open(entry).unwrap();
let _manifest = Store::load_fragment_from_stream(
"mp4",
&mut init_stream,
&mut fragment_stream,
&mut validation_log,
&context,
)
.unwrap();
init_stream.seek(std::io::SeekFrom::Start(0)).unwrap();
assert!(!validation_log.has_any_error());
}
let mut validation_log = StatusTracker::default();
init_stream.rewind().unwrap();
let _manifest = Store::load_from_file_and_fragments(
"mp4",
&mut init_stream,
&fragments,
&mut validation_log,
&context,
)
.unwrap();
assert!(!validation_log.has_any_error());
}
}
#[test]
#[cfg(feature = "file_io")]
fn test_bogus_cert() {
use crate::builder::{Builder, BuilderIntent};
crate::settings::set_settings_value("verify.verify_after_sign", false).unwrap();
crate::settings::set_settings_value("verify.verify_trust", false).unwrap();
let png = include_bytes!("../../tests/fixtures/libpng-test.png"); let ed25519 = include_bytes!("../../tests/fixtures/certs/ed25519.pem");
let certs = include_bytes!("../../tests/fixtures/certs/es256.pub");
let mut context = Context::new();
context.settings_mut().verify.verify_after_sign = false;
context.settings_mut().verify.verify_trust = false;
let mut builder = Builder::from_context(context);
builder.set_intent(BuilderIntent::Create(DigitalSourceType::Empty));
let signer =
crate::create_signer::from_keys(certs, ed25519, SigningAlg::Ed25519, None).unwrap();
let mut dst = Cursor::new(Vec::new());
builder
.sign(&signer, "image/png", &mut Cursor::new(png), &mut dst)
.unwrap();
let reader = crate::Reader::default()
.with_stream("image/png", &mut dst)
.unwrap();
assert_eq!(reader.validation_state(), crate::ValidationState::Invalid);
}
#[test]
fn test_from_and_to_jumbf() {
let context = crate::context::Context::new();
let ap = fixture_path("C.jpg");
let mut stream = std::fs::File::open(&ap).unwrap();
let format = "image/jpeg";
let (manifest_bytes, _remote_url) =
Store::load_jumbf_from_stream(format, &mut stream, &context).unwrap();
let store =
Store::from_jumbf_with_context(&manifest_bytes, &mut StatusTracker::default(), &context)
.unwrap();
let jumbf = store
.to_jumbf_internal(0)
.expect("Failed to convert store to JUMBF");
assert_eq!(jumbf, manifest_bytes);
}
#[c2pa_test_async]
async fn test_store_load_fragment_from_stream_async() {
let context = crate::context::Context::new();
let init_segment = include_bytes!("../../tests/fixtures/dashinit.mp4");
let fragment = include_bytes!("../../tests/fixtures/dash1.m4s");
let mut init_stream = Cursor::new(init_segment);
let mut fragment_stream = Cursor::new(fragment);
let format = "mp4";
let mut validation_log = StatusTracker::default();
let result = Store::load_fragment_from_stream_async(
format,
&mut init_stream,
&mut fragment_stream,
&mut validation_log,
&context,
)
.await;
match result {
Ok(_manifest) => {
if validation_log.has_any_error() {
let errors: Vec<_> = validation_log.filter_errors().collect();
let has_unexpected_errors = errors.iter().any(|item| {
if let Some(err_val) = &item.err_val {
if err_val.contains("CertificateTrustError") {
return false; }
}
true
});
if has_unexpected_errors {
panic!("Validation log contains unexpected errors: {validation_log:?}",);
}
}
}
Err(e) => {
panic!("Failed to load fragment from stream: {e:?}");
}
}
}
#[test]
fn test_stream_context_handling_for_c_ffi_layer() {
struct FlushTrackingStream<T: ReadWriteSeek> {
inner: T,
buffer: Vec<u8>,
flush_called: Arc<AtomicBool>,
dropped: Arc<AtomicBool>,
}
impl<T: ReadWriteSeek> FlushTrackingStream<T> {
fn new(inner: T, flush_called: Arc<AtomicBool>, dropped: Arc<AtomicBool>) -> Self {
Self {
inner,
buffer: Vec::new(),
flush_called,
dropped,
}
}
}
impl<T: ReadWriteSeek> Read for FlushTrackingStream<T> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
self.inner.read(buf)
}
}
impl<T: ReadWriteSeek> Write for FlushTrackingStream<T> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
if !self.buffer.is_empty() {
self.inner.write_all(&self.buffer)?;
}
self.buffer = buf.to_vec();
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
self.flush_called.store(true, Ordering::SeqCst);
if !self.buffer.is_empty() {
self.inner.write_all(&self.buffer)?;
self.buffer.clear();
}
self.inner.flush()
}
}
impl<T: ReadWriteSeek> Seek for FlushTrackingStream<T> {
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
if !self.buffer.is_empty() && !self.flush_called.load(Ordering::SeqCst) {
return Err(std::io::Error::other(
"Simulated use-after-free: attempting to seek with unflushed buffer (similar to accessing freed FFI context)",
));
}
self.inner.seek(pos)
}
fn rewind(&mut self) -> std::io::Result<()> {
if !self.buffer.is_empty() && !self.flush_called.load(Ordering::SeqCst) {
return Err(std::io::Error::other(
"Simulated use-after-free: attempting to rewind with unflushed buffer (similar to accessing freed FFI context)",
));
}
self.inner.rewind()
}
fn stream_position(&mut self) -> std::io::Result<u64> {
self.inner.stream_position()
}
}
impl<T: ReadWriteSeek> Drop for FlushTrackingStream<T> {
fn drop(&mut self) {
self.dropped.store(true, Ordering::SeqCst);
}
}
let context = Context::new();
let (format, mut input_stream, output_stream) = create_test_streams("earth_apollo17.jpg");
let flush_called = Arc::new(AtomicBool::new(false));
let dropped = Arc::new(AtomicBool::new(false));
let mut tracking_stream =
FlushTrackingStream::new(output_stream, flush_called.clone(), dropped.clone());
let mut store = Store::from_context(&context);
let cgi = ClaimGeneratorInfo::new("flush_test");
let mut claim = Claim::new("test", Some("flush_test"), 1);
create_capture_claim(&mut claim).unwrap();
claim.add_claim_generator_info(cgi);
let signer = test_signer(SigningAlg::Ps256);
store.commit_claim(claim).unwrap();
store
.save_to_stream(
format,
&mut input_stream,
&mut tracking_stream,
signer.as_ref(),
&context,
)
.expect("save_to_stream should succeed and properly flush the stream");
assert!(
flush_called.load(Ordering::SeqCst),
"flush() must be called during save_to_stream to prevent use-after-free in FFI scenarios"
);
assert!(
tracking_stream.buffer.is_empty(),
"buffer should be empty after flush"
);
}
#[test]
fn test_stream_context_handling_for_c_ffi_layer_no_use_after_free() {
use std::io::Cursor;
#[repr(C)]
struct StreamContext {
data: Cursor<Vec<u8>>,
magic: u64,
}
impl StreamContext {
fn new() -> Self {
Self {
data: Cursor::new(Vec::new()),
magic: 0xdeadbeefcafebabe,
}
}
fn mark_freed(&mut self) {
self.magic = 0xcccccccccccccccc;
}
fn check_valid(&self) -> std::io::Result<()> {
if self.magic != 0xdeadbeefcafebabe {
return Err(std::io::Error::other(
format!(
"Use-after-free detected: StreamContext magic is 0x{:X} (expected 0xDEADBEEFCAFEBABE)",
self.magic
),
));
}
Ok(())
}
}
struct UnsafeStream {
context: *mut StreamContext,
flush_called: Arc<AtomicBool>,
}
impl UnsafeStream {
fn new(flush_called: Arc<AtomicBool>) -> Self {
let context = Box::into_raw(Box::new(StreamContext::new()));
Self {
context,
flush_called,
}
}
unsafe fn context_ref(&self) -> &StreamContext {
&*self.context
}
unsafe fn context_mut(&mut self) -> &mut StreamContext {
&mut *self.context
}
}
impl Read for UnsafeStream {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
unsafe {
self.context_ref().check_valid()?;
self.context_mut().data.read(buf)
}
}
}
impl Write for UnsafeStream {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
unsafe {
self.context_ref().check_valid()?;
self.context_mut().data.write(buf)
}
}
fn flush(&mut self) -> std::io::Result<()> {
unsafe {
self.context_ref().check_valid()?;
self.flush_called.store(true, Ordering::SeqCst);
self.context_mut().data.flush()
}
}
}
impl Seek for UnsafeStream {
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
unsafe {
self.context_ref().check_valid()?;
self.context_mut().data.seek(pos)
}
}
fn rewind(&mut self) -> std::io::Result<()> {
unsafe {
self.context_ref().check_valid()?;
self.context_mut().data.rewind()
}
}
fn stream_position(&mut self) -> std::io::Result<u64> {
unsafe {
self.context_ref().check_valid()?;
self.context_mut().data.stream_position()
}
}
}
impl Drop for UnsafeStream {
fn drop(&mut self) {
unsafe {
if !self.context.is_null() {
if !self.flush_called.load(Ordering::SeqCst) {
(*self.context).mark_freed();
}
drop(Box::from_raw(self.context));
}
}
}
}
unsafe impl Send for UnsafeStream {}
unsafe impl Sync for UnsafeStream {}
let context = Context::new();
let (format, mut input_stream, _) = create_test_streams("earth_apollo17.jpg");
let flush_called = Arc::new(AtomicBool::new(false));
let mut unsafe_stream = UnsafeStream::new(flush_called.clone());
let mut store = Store::from_context(&context);
let cgi = ClaimGeneratorInfo::new("unsafe_flush_test");
let mut claim = Claim::new("test", Some("unsafe_flush_test"), 1);
create_capture_claim(&mut claim).unwrap();
claim.add_claim_generator_info(cgi);
let signer = test_signer(SigningAlg::Ps256);
store.commit_claim(claim).unwrap();
let result = store.save_to_stream(
format,
&mut input_stream,
&mut unsafe_stream,
signer.as_ref(),
&context,
);
assert!(
result.is_ok(),
"save_to_stream should succeed. Error: {:?}",
result.err()
);
assert!(flush_called.load(Ordering::SeqCst));
}
#[test]
fn test_ingredient_depth_limit_get_claim_referenced_manifests() {
let store = Store::new();
let claim = Claim::new("depth_test", Some("contentauth"), 2);
let mut svi = StoreValidationInfo::default();
let mut validation_log = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
let labels: Vec<String> = (0..MAX_INGREDIENT_DEPTH)
.map(|i| format!("claim_{i}"))
.collect();
let mut claim_label_path: Vec<&str> = labels.iter().map(String::as_str).collect();
let result = Store::get_claim_referenced_manifests_impl(
&claim,
&store,
&mut svi,
true,
&mut validation_log,
&mut claim_label_path,
);
assert!(
matches!(result, Err(Error::InvalidAsset(_))),
"expected Err(InvalidAsset) for depth >= MAX_INGREDIENT_DEPTH, got: {result:?}"
);
}
#[test]
fn test_ingredient_depth_limit_ingredient_checks() {
use crate::context::Context;
let store = Store::new();
let claim = Claim::new("depth_test", Some("contentauth"), 2);
let svi = StoreValidationInfo::default();
let mut validation_log = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
let context = Context::new();
let result = Store::ingredient_checks(
&store,
&claim,
&svi,
&mut validation_log,
MAX_INGREDIENT_DEPTH,
&context,
&mut HashSet::new(),
);
assert!(
matches!(result, Err(Error::InvalidAsset(_))),
"expected Err(InvalidAsset) for depth >= MAX_INGREDIENT_DEPTH, got: {result:?}"
);
}
#[test]
fn test_redacted_ingredient_assertion_removes_claim() {
use crate::{
hashed_uri::HashedUri, jumbf::labels::to_signature_uri, utils::test::create_test_store_v1,
ClaimGeneratorInfo, ValidationResults,
};
let context = Context::new();
let signer = test_signer(SigningAlg::Ps256);
let (format, mut input_stream, mut m1_output) = create_test_streams("earth_apollo17.jpg");
let mut m1_store = create_test_store_v1().unwrap();
m1_store
.save_to_stream(
format,
&mut input_stream,
&mut m1_output,
signer.as_ref(),
&context,
)
.unwrap();
m1_output.rewind().unwrap();
let m1_vec = m1_output.get_ref().clone();
let mut m1_report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
let m1_store_loaded = Store::from_stream(
format,
Cursor::new(m1_vec.clone()),
&mut m1_report,
&context,
)
.unwrap();
let m1_pc = m1_store_loaded.provenance_claim().unwrap();
let m1_label = m1_pc.label().to_owned();
let mut m2_claim = Claim::new("test", Some("m2_ingredient_redact"), 2);
m2_claim.add_claim_generator_info(ClaimGeneratorInfo::new("test"));
let (m1_jumbf, _) =
Store::load_jumbf_from_stream(format, &mut Cursor::new(m1_vec.clone()), &context).unwrap();
let mut m2_store =
Store::load_ingredient_to_claim(&mut m2_claim, &m1_jumbf, None, &context).unwrap();
let m1_hashes = m1_store_loaded.get_manifest_box_hashes(m1_pc);
let parent_uri = HashedUri::new(
m1_store_loaded.provenance_path().unwrap(),
Some(m1_pc.alg().to_string()),
&m1_hashes.manifest_box_hash,
);
let sig_uri = HashedUri::new(
to_signature_uri(m1_pc.label()),
Some(m1_pc.alg().to_string()),
&m1_hashes.signature_box_hash,
);
let m1_validation = ValidationResults::from_store(&m1_store_loaded, &m1_report);
let parent_ingredient = Ingredient::new_v3(Relationship::ParentOf)
.set_active_manifests_and_signature_from_hashed_uri(Some(parent_uri), Some(sig_uri))
.set_validation_results(Some(m1_validation));
m2_claim.add_assertion(&parent_ingredient).unwrap();
let ia = m2_claim.ingredient_assertions()[0];
let ia_hashed_uri = HashedUri::new(
to_assertion_uri(m2_claim.label(), &ia.label()),
Some(m2_claim.alg().to_owned()),
ia.hash(),
);
let actions = Actions::new().add_action(
Action::new("c2pa.opened")
.set_parameter("ingredients", vec![ia_hashed_uri])
.unwrap(),
);
m2_claim.add_assertion(&actions).unwrap();
m2_store.commit_update_manifest(m2_claim).unwrap();
m1_output.rewind().unwrap();
let mut m2_output = Cursor::new(Vec::new());
m2_store
.save_to_stream(
format,
&mut m1_output,
&mut m2_output,
signer.as_ref(),
&context,
)
.unwrap();
m2_output.rewind().unwrap();
let m2_vec = m2_output.get_ref().clone();
let mut m2_report = StatusTracker::with_error_behavior(ErrorBehavior::StopOnFirstError);
let m2_store_loaded = Store::from_stream(
format,
Cursor::new(m2_vec.clone()),
&mut m2_report,
&context,
)
.unwrap();
let m2_pc = m2_store_loaded.provenance_claim().unwrap();
let m2_label = m2_pc.label().to_owned();
let m2_ingredient_assertions = m2_pc.ingredient_assertions();
assert!(
!m2_ingredient_assertions.is_empty(),
"M2 should have an ingredient assertion"
);
let ingredient_redaction_uri =
to_assertion_uri(m2_pc.label(), &m2_ingredient_assertions[0].label());
let mut m3_claim = Claim::new("test", Some("m3_ingredient_redact"), 2);
m3_claim.add_claim_generator_info(ClaimGeneratorInfo::new("test"));
let (m2_jumbf, _) =
Store::load_jumbf_from_stream(format, &mut Cursor::new(m2_vec), &context).unwrap();
Store::load_ingredient_to_claim(
&mut m3_claim,
&m2_jumbf,
Some(vec![ingredient_redaction_uri]),
&context,
)
.unwrap();
assert!(
m3_claim.claim_ingredient(&m2_label).is_some(),
"M2 should remain in M3's ingredients"
);
assert!(
m3_claim.claim_ingredient(&m1_label).is_none(),
"M1 should be removed because its only referencing assertion was redacted"
);
}
const COLLECTION_ZIP: &[u8] = include_bytes!("../../tests/fixtures/sample1.zip");
#[test]
fn test_store_verify_zip_collection_hash() {
let mut claim = create_test_claim().unwrap();
let mut collection = CollectionHash::new(claim.alg().to_owned());
collection
.gen_hash_from_zip_stream(&mut Cursor::new(COLLECTION_ZIP))
.unwrap();
claim.add_created_assertion(&collection).unwrap();
let mut store = Store::from_context(&Context::new());
store.commit_claim(claim).unwrap();
let claim = store.provenance_claim().unwrap();
let svi = StoreValidationInfo {
binding_claim: claim.label().to_string(),
..Default::default()
};
let context = Context::new();
let mut report = StatusTracker::default();
let mut asset_data = ClaimAssetData::Bytes(COLLECTION_ZIP, "zip");
Claim::verify_hash_binding(claim, &mut asset_data, &svi, &mut report, &context).unwrap();
assert!(report.has_status(validation_status::ASSERTION_COLLECTIONHASH_MATCH));
}
#[test]
fn test_sign_zip_generates_valid_collection_hash() {
let mut context = Context::new();
context.settings_mut().verify.verify_after_sign = false;
let mut store = Store::from_context(&context);
store.commit_claim(create_test_claim().unwrap()).unwrap();
let signer = test_signer(SigningAlg::Ps256);
let mut input = Cursor::new(COLLECTION_ZIP.to_vec());
let mut output = Cursor::new(Vec::new());
store
.save_to_stream("zip", &mut input, &mut output, signer.as_ref(), &context)
.unwrap();
let mut report = StatusTracker::default();
output.rewind().unwrap();
Store::from_stream("zip", &mut output, &mut report, &context).unwrap();
assert!(
report.has_status(validation_status::ASSERTION_COLLECTIONHASH_MATCH),
"the generated collection hash should verify against the signed ZIP"
);
}
#[test]
fn test_sign_zip_update_manifest_is_unsupported() {
let mut context = Context::new();
context.settings_mut().verify.verify_after_sign = false;
let mut claim = create_test_claim().unwrap();
claim.set_update_manifest(true);
let mut store = Store::from_context(&context);
store.commit_claim(claim).unwrap();
let signer = test_signer(SigningAlg::Ps256);
let mut input = Cursor::new(COLLECTION_ZIP.to_vec());
let mut output = Cursor::new(Vec::new());
let result = store.save_to_stream("zip", &mut input, &mut output, signer.as_ref(), &context);
assert!(
matches!(&result, Err(Error::BadParam(msg)) if msg.contains("update manifests are not supported")),
"expected update manifests to be rejected for ZIP, got {result:?}"
);
}
#[cfg(feature = "fetch_remote_manifests")]
mod remote_manifest_response_tests {
use std::sync::atomic::AtomicUsize;
use super::*;
use crate::{
http::{AsyncHttpResolver, HttpResolverError, SyncHttpResolver},
settings::DEFAULT_MAX_MANIFEST_RESPONSE_SIZE,
};
const TEST_RESPONSE_SIZE: u64 = 16;
struct Resolver {
content_length: Option<&'static str>,
body_length: u64,
error_after: Option<u64>,
probe_interruptions: usize,
bytes_read: Arc<AtomicUsize>,
}
struct Body {
remaining: u64,
error_after: Option<u64>,
probe_interruptions: usize,
eof_returned: bool,
bytes_read: Arc<AtomicUsize>,
}
impl Read for Body {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
if self.bytes_read.load(Ordering::SeqCst) as u64 == TEST_RESPONSE_SIZE
&& self.probe_interruptions > 0
{
self.probe_interruptions -= 1;
return Err(std::io::ErrorKind::Interrupted.into());
}
if self
.error_after
.is_some_and(|limit| self.bytes_read.load(Ordering::SeqCst) as u64 >= limit)
|| self.eof_returned
{
return Err(std::io::Error::other("test stream failure"));
}
let until_error = self.error_after.map_or(u64::MAX, |limit| {
limit - self.bytes_read.load(Ordering::SeqCst) as u64
});
let len = self.remaining.min(buf.len() as u64).min(until_error) as usize;
buf[..len].fill(b'x');
self.remaining -= len as u64;
if !buf.is_empty() && len == 0 {
self.eof_returned = true;
}
self.bytes_read.fetch_add(len, Ordering::SeqCst);
Ok(len)
}
}
impl SyncHttpResolver for Resolver {
fn http_resolve(
&self,
_request: http::Request<Vec<u8>>,
) -> std::result::Result<http::Response<Box<dyn Read>>, HttpResolverError> {
let mut response = http::Response::builder().status(200);
if let Some(len) = self.content_length {
response = response.header(http::header::CONTENT_LENGTH, len);
}
Ok(response
.body(Box::new(Body {
remaining: self.body_length,
error_after: self.error_after,
probe_interruptions: self.probe_interruptions,
eof_returned: false,
bytes_read: self.bytes_read.clone(),
}) as Box<dyn Read>)
.unwrap())
}
}
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
impl AsyncHttpResolver for Resolver {
async fn http_resolve_async(
&self,
request: http::Request<Vec<u8>>,
) -> std::result::Result<http::Response<Box<dyn Read>>, HttpResolverError> {
self.http_resolve(request)
}
}
async fn fetch(
content_length: Option<&'static str>,
body_length: u64,
error_after: Option<u64>,
use_async: bool,
) -> (Result<Vec<u8>>, usize) {
let context = Context::new()
.with_settings_value("verify.max_manifest_response_size", TEST_RESPONSE_SIZE)
.unwrap();
fetch_with_context(context, content_length, body_length, error_after, use_async).await
}
async fn fetch_with_context(
context: Context,
content_length: Option<&'static str>,
body_length: u64,
error_after: Option<u64>,
use_async: bool,
) -> (Result<Vec<u8>>, usize) {
let bytes_read = Arc::new(AtomicUsize::new(0));
let resolver = Arc::new(Resolver {
content_length,
body_length,
error_after,
probe_interruptions: 0,
bytes_read: bytes_read.clone(),
});
let context = context
.with_resolver(resolver.clone())
.with_resolver_async(resolver);
let result = if use_async {
Store::fetch_remote_manifest_async("https://example.com/manifest.c2pa", &context).await
} else {
Store::fetch_remote_manifest("https://example.com/manifest.c2pa", &context)
};
(result, bytes_read.load(Ordering::SeqCst))
}
fn assert_size_error(result: Result<Vec<u8>>) {
assert!(
matches!(result, Err(Error::RemoteManifestFetch(ref msg)) if msg == "remote manifest exceeds the 16 byte response size limit"),
"expected response size error, got {result:?}"
);
}
#[c2pa_test_async]
async fn test_remote_manifest_overflow_probe_retries_interrupted() {
for use_async in [false, true] {
for body_length in [TEST_RESPONSE_SIZE, TEST_RESPONSE_SIZE + 1] {
let bytes_read = Arc::new(AtomicUsize::new(0));
let resolver = Arc::new(Resolver {
content_length: None,
body_length,
error_after: None,
probe_interruptions: 2,
bytes_read: bytes_read.clone(),
});
let context = Context::new()
.with_settings_value("verify.max_manifest_response_size", TEST_RESPONSE_SIZE)
.unwrap()
.with_resolver(resolver.clone())
.with_resolver_async(resolver);
let result = if use_async {
Store::fetch_remote_manifest_async(
"https://example.com/manifest.c2pa",
&context,
)
.await
} else {
Store::fetch_remote_manifest("https://example.com/manifest.c2pa", &context)
};
if body_length == TEST_RESPONSE_SIZE {
assert_eq!(result.unwrap(), vec![b'x'; TEST_RESPONSE_SIZE as usize]);
} else {
assert_size_error(result);
}
assert_eq!(bytes_read.load(Ordering::SeqCst) as u64, body_length);
}
}
}
#[c2pa_test_async]
async fn test_remote_manifest_oversized_header_rejected_before_reading() {
for use_async in [false, true] {
for header in ["18446744073709551615", "4294967295", "17"] {
let (result, bytes_read) = fetch(Some(header), 1, None, use_async).await;
assert_size_error(result);
assert_eq!(bytes_read, 0);
}
}
}
#[c2pa_test_async]
async fn test_remote_manifest_body_limit_ignores_untrusted_header() {
for use_async in [false, true] {
for header in [
None,
Some("1"),
Some("invalid"),
Some("18446744073709551616"),
] {
let (result, bytes_read) =
fetch(header, TEST_RESPONSE_SIZE + 100, None, use_async).await;
assert_size_error(result);
assert_eq!(bytes_read as u64, TEST_RESPONSE_SIZE + 1);
}
}
}
#[c2pa_test_async]
async fn test_remote_manifest_exact_limit_accepted() {
for use_async in [false, true] {
for header in [None, Some("16")] {
let (result, bytes_read) = fetch(header, TEST_RESPONSE_SIZE, None, use_async).await;
let body = result.unwrap();
assert_eq!(body.len() as u64, TEST_RESPONSE_SIZE);
assert!(body.iter().all(|byte| *byte == b'x'));
assert_eq!(bytes_read as u64, TEST_RESPONSE_SIZE);
}
}
}
#[c2pa_test_async]
async fn test_remote_manifest_small_body_not_truncated_by_header() {
for use_async in [false, true] {
for header in [
None,
Some("0"),
Some("1"),
Some("3"),
Some("10"),
Some("invalid"),
] {
let (result, bytes_read) = fetch(header, 3, None, use_async).await;
assert_eq!(result.unwrap(), b"xxx");
assert_eq!(bytes_read, 3);
}
let (result, bytes_read) = fetch(Some("0"), 0, None, use_async).await;
assert!(result.unwrap().is_empty());
assert_eq!(bytes_read, 0);
}
}
#[c2pa_test_async]
async fn test_remote_manifest_body_read_error_propagated() {
for use_async in [false, true] {
let (result, _) = fetch(None, 3, Some(0), use_async).await;
assert!(
matches!(result, Err(Error::RemoteManifestFetch(ref msg)) if msg.contains("error reading content stream")),
"expected stream read error, got {result:?}"
);
}
}
#[c2pa_test_async]
async fn test_remote_manifest_overflow_probe_read_error_propagated() {
for use_async in [false, true] {
let (result, bytes_read) = fetch(
Some("16"),
TEST_RESPONSE_SIZE,
Some(TEST_RESPONSE_SIZE),
use_async,
)
.await;
assert!(
matches!(result, Err(Error::RemoteManifestFetch(ref msg)) if msg == "error reading content stream"),
"expected overflow probe read error, got {result:?}"
);
assert_eq!(bytes_read as u64, TEST_RESPONSE_SIZE);
}
}
#[c2pa_test_async]
async fn test_remote_manifest_default_and_raised_limits_are_independent() {
for use_async in [false, true] {
let raised = Context::new()
.with_settings_value(
"verify.max_manifest_response_size",
DEFAULT_MAX_MANIFEST_RESPONSE_SIZE + 1,
)
.unwrap();
let (result, bytes_read) = fetch_with_context(
raised,
Some("10485761"),
DEFAULT_MAX_MANIFEST_RESPONSE_SIZE + 1,
None,
use_async,
)
.await;
let body = result.unwrap();
assert_eq!(body.len() as u64, DEFAULT_MAX_MANIFEST_RESPONSE_SIZE + 1);
assert_eq!(bytes_read, body.len());
let default = Context::new();
assert_eq!(
default.settings().verify.max_manifest_response_size,
DEFAULT_MAX_MANIFEST_RESPONSE_SIZE
);
let (result, bytes_read) =
fetch_with_context(default, Some("10485761"), 1, None, use_async).await;
assert!(
matches!(result, Err(Error::RemoteManifestFetch(ref msg)) if msg == "remote manifest exceeds the 10485760 byte response size limit"),
"expected the default limit to remain unchanged, got {result:?}"
);
assert_eq!(bytes_read, 0);
let (result, bytes_read) = fetch_with_context(
Context::new(),
Some("10485760"),
DEFAULT_MAX_MANIFEST_RESPONSE_SIZE,
None,
use_async,
)
.await;
assert_eq!(
result.unwrap().len() as u64,
DEFAULT_MAX_MANIFEST_RESPONSE_SIZE
);
assert_eq!(bytes_read as u64, DEFAULT_MAX_MANIFEST_RESPONSE_SIZE);
}
}
#[c2pa_test_async]
async fn test_remote_manifest_directly_mutated_invalid_limit_rejected() {
for use_async in [false, true] {
for limit in [0, u64::MAX] {
let mut context = Context::new();
context.settings_mut().verify.max_manifest_response_size = limit;
let (result, bytes_read) =
fetch_with_context(context, None, 1, None, use_async).await;
assert!(
matches!(result, Err(Error::BadParam(ref msg)) if msg.contains("max_manifest_response_size")),
"expected invalid settings error, got {result:?}"
);
assert_eq!(bytes_read, 0);
}
}
}
}
#[cfg(all(not(target_arch = "wasm32"), feature = "fetch_remote_manifests"))]
#[test]
fn test_remote_manifest_redirect_to_internal_blocked() {
use httpmock::prelude::*;
let server = MockServer::start();
let redirect = server.mock(|when, then| {
when.method(GET).path("/redirect-to-internal");
then.status(302)
.header("Location", "http://169.254.169.254/latest/meta-data/");
});
let context = Context::new();
let result = Store::fetch_remote_manifest(&server.url("/redirect-to-internal"), &context);
let err = result.expect_err("the redirect to an internal address must be rejected");
let msg = err.to_string();
assert!(
msg.contains("redirect") && msg.contains("internal"),
"error should explain the blocked internal redirect, got: {msg}"
);
redirect.assert_calls(1);
}
#[cfg(all(not(target_arch = "wasm32"), feature = "fetch_remote_manifests"))]
#[test]
fn test_remote_manifest_redirects_disabled() {
use httpmock::prelude::*;
let server = MockServer::start();
let redirect = server.mock(|when, then| {
when.method(GET).path("/redirect");
then.status(302).header("Location", server.url("/target"));
});
let target = server.mock(|when, then| {
when.method(GET).path("/target");
then.status(200).body("SSRF_CONFIRMED");
});
let context = Context::new()
.with_settings("[core]\nallow_redirects = false\n")
.unwrap();
let result = Store::fetch_remote_manifest(&server.url("/redirect"), &context);
assert!(
result.is_err(),
"allow_redirects = false must not follow the redirect"
);
redirect.assert_calls(1);
target.assert_calls(0);
}
fn build_asset_with_input_to_ingredient(corrupt_hash: bool) -> (&'static str, Vec<u8>) {
use crate::{
hashed_uri::HashedUri, jumbf::labels::to_signature_uri, utils::test::create_test_store,
ClaimGeneratorInfo, ValidationResults,
};
let mut context = Context::new();
context.settings_mut().verify.verify_after_sign = false;
let signer = test_signer(SigningAlg::Ps256);
let (format, mut m1_input, mut m1_output) = create_test_streams("earth_apollo17.jpg");
let mut m1_store = create_test_store().unwrap();
m1_store
.save_to_stream(
format,
&mut m1_input,
&mut m1_output,
signer.as_ref(),
&context,
)
.unwrap();
m1_output.rewind().unwrap();
let m1_vec = m1_output.get_ref().clone();
let mut m1_report = StatusTracker::default();
let m1_store_loaded = Store::from_stream(
format,
Cursor::new(m1_vec.clone()),
&mut m1_report,
&context,
)
.unwrap();
let m1_pc = m1_store_loaded.provenance_claim().unwrap();
let mut m2_claim = Claim::new("input_to_test", Some("m2_input_to"), 2);
m2_claim.add_claim_generator_info(ClaimGeneratorInfo::new("test"));
let (m1_jumbf, _) =
Store::load_jumbf_from_stream(format, &mut Cursor::new(m1_vec.clone()), &context).unwrap();
let mut m2_store =
Store::load_ingredient_to_claim(&mut m2_claim, &m1_jumbf, None, &context).unwrap();
let m1_hashes = m1_store_loaded.get_manifest_box_hashes(m1_pc);
let mut manifest_box_hash = m1_hashes.manifest_box_hash.clone();
if corrupt_hash {
manifest_box_hash[0] ^= 0xff;
}
let parent_uri = HashedUri::new(
m1_store_loaded.provenance_path().unwrap(),
Some(m1_pc.alg().to_string()),
&manifest_box_hash,
);
let sig_uri = HashedUri::new(
to_signature_uri(m1_pc.label()),
Some(m1_pc.alg().to_string()),
&m1_hashes.signature_box_hash,
);
let m1_validation = ValidationResults::from_store(&m1_store_loaded, &m1_report);
let input_to_ingredient = Ingredient::new_v3(Relationship::InputTo)
.set_active_manifests_and_signature_from_hashed_uri(Some(parent_uri), Some(sig_uri))
.set_validation_results(Some(m1_validation));
m2_claim.add_assertion(&input_to_ingredient).unwrap();
let ia = m2_claim.ingredient_assertions()[0];
let ia_hashed_uri = HashedUri::new(
to_assertion_uri(m2_claim.label(), &ia.label()),
Some(m2_claim.alg().to_owned()),
ia.hash(),
);
let actions = Actions::new()
.add_action(Action::new("c2pa.created").set_source_type(DigitalSourceType::Empty))
.add_action(
Action::new("c2pa.edited")
.set_parameter("ingredients", vec![ia_hashed_uri])
.unwrap(),
);
m2_claim.add_assertion(&actions).unwrap();
m2_store.commit_claim(m2_claim).unwrap();
let (_f, mut m2_input, mut m2_output) = create_test_streams("earth_apollo17.jpg");
m2_store
.save_to_stream(
format,
&mut m2_input,
&mut m2_output,
signer.as_ref(),
&context,
)
.unwrap();
m2_output.rewind().unwrap();
(format, m2_output.get_ref().clone())
}
#[test]
fn test_input_to_ingredient_is_validated() {
let context = Context::new();
let (format, m2_vec) = build_asset_with_input_to_ingredient(false);
let mut report = StatusTracker::default();
Store::from_stream(format, Cursor::new(m2_vec), &mut report, &context).unwrap();
assert!(
!report.has_any_error(),
"valid inputTo ingredient should not produce errors: {:?}",
report.logged_items()
);
assert!(
report.has_status(validation_status::INGREDIENT_MANIFEST_VALIDATED),
"inputTo ingredient manifest must be hash-validated, got: {:?}",
report.logged_items()
);
}
#[test]
fn test_input_to_ingredient_hash_mismatch_detected() {
let context = Context::new();
let (format, m2_vec) = build_asset_with_input_to_ingredient(true);
let mut report = StatusTracker::default();
let _ = Store::from_stream(format, Cursor::new(m2_vec), &mut report, &context);
assert!(
report.has_status(validation_status::INGREDIENT_MANIFEST_MISMATCH),
"tampered inputTo ingredient must be detected as a manifest mismatch, got: {:?}",
report.logged_items()
);
assert!(
report.has_any_error(),
"tampered inputTo ingredient must fail validation"
);
}
#[test]
fn test_hash_binding_manifest_parent_cycle_terminates() {
use crate::{hashed_uri::HashedUri, jumbf::labels::to_manifest_uri, ClaimGeneratorInfo};
let mut store = Store::new();
let mut claim_a = Claim::new("cycle_a", Some("contentauth"), 2);
let mut claim_b = Claim::new("cycle_b", Some("contentauth"), 2);
claim_a.add_claim_generator_info(ClaimGeneratorInfo::new("test"));
claim_b.add_claim_generator_info(ClaimGeneratorInfo::new("test"));
claim_a.set_update_manifest(true);
claim_b.set_update_manifest(true);
let label_a = claim_a.label().to_owned();
let label_b = claim_b.label().to_owned();
for (claim, parent_label) in [(&mut claim_a, &label_b), (&mut claim_b, &label_a)] {
let parent_uri = HashedUri::new(
to_manifest_uri(parent_label),
Some(claim.alg().to_owned()),
&[0u8; 32],
);
let ingredient = Ingredient::new_v2("parent", "image/jpeg")
.set_parent()
.set_c2pa_manifest_from_hashed_uri(Some(parent_uri));
claim.add_assertion(&ingredient).unwrap();
}
store.insert_restored_claim(label_a.clone(), claim_a);
store.insert_restored_claim(label_b, claim_b);
let binding = store.get_hash_binding_manifest(store.get_claim(&label_a).unwrap());
assert!(
binding.is_none(),
"a parentOf cycle has no binding manifest and must terminate, got {binding:?}"
);
}
#[test]
fn test_shared_ingredient_inputto_graph_verified_once() {
use crate::{
hashed_uri::HashedUri, jumbf::labels::to_signature_uri, utils::test::create_test_store,
ClaimGeneratorInfo, ValidationResults,
};
const DEPTH: usize = 13;
let mut build_context = Context::new();
build_context.settings_mut().verify.verify_after_sign = false;
build_context.settings_mut().verify.verify_after_reading = false;
let signer = test_signer(SigningAlg::Ps256);
let (format, mut leaf_input, mut leaf_output) = create_test_streams("earth_apollo17.jpg");
create_test_store()
.unwrap()
.save_to_stream(
format,
&mut leaf_input,
&mut leaf_output,
signer.as_ref(),
&build_context,
)
.unwrap();
leaf_output.rewind().unwrap();
let mut prev_vec = leaf_output.get_ref().clone();
for level in 1..DEPTH {
let mut prev_report = StatusTracker::default();
let prev_store = Store::from_stream(
format,
Cursor::new(prev_vec.clone()),
&mut prev_report,
&build_context,
)
.unwrap();
let prev_pc = prev_store.provenance_claim().unwrap();
let prev_hashes = prev_store.get_manifest_box_hashes(prev_pc);
let mut claim = Claim::new("deep_shared", Some(&format!("m{level}")), 2);
claim.add_claim_generator_info(ClaimGeneratorInfo::new("test"));
let (prev_jumbf, _) =
Store::load_jumbf_from_stream(format, &mut Cursor::new(prev_vec), &build_context)
.unwrap();
let mut store =
Store::load_ingredient_to_claim(&mut claim, &prev_jumbf, None, &build_context).unwrap();
for _ in 0..2 {
let parent_uri = HashedUri::new(
prev_store.provenance_path().unwrap(),
Some(prev_pc.alg().to_string()),
&prev_hashes.manifest_box_hash,
);
let sig_uri = HashedUri::new(
to_signature_uri(prev_pc.label()),
Some(prev_pc.alg().to_string()),
&prev_hashes.signature_box_hash,
);
let validation = ValidationResults::from_store(&prev_store, &prev_report);
let ingredient = Ingredient::new_v3(Relationship::InputTo)
.set_active_manifests_and_signature_from_hashed_uri(Some(parent_uri), Some(sig_uri))
.set_validation_results(Some(validation));
claim.add_assertion(&ingredient).unwrap();
}
let mut actions = Actions::new()
.add_action(Action::new("c2pa.created").set_source_type(DigitalSourceType::Empty));
for ia in claim.ingredient_assertions() {
let ia_hashed_uri = HashedUri::new(
to_assertion_uri(claim.label(), &ia.label()),
Some(claim.alg().to_owned()),
ia.hash(),
);
actions = actions.add_action(
Action::new("c2pa.edited")
.set_parameter("ingredients", vec![ia_hashed_uri])
.unwrap(),
);
}
claim.add_assertion(&actions).unwrap();
store.commit_claim(claim).unwrap();
let (_f, mut input, mut output) = create_test_streams("earth_apollo17.jpg");
store
.save_to_stream(
format,
&mut input,
&mut output,
signer.as_ref(),
&build_context,
)
.unwrap();
output.rewind().unwrap();
prev_vec = output.get_ref().clone();
}
let mut report = StatusTracker::default();
Store::from_stream(format, Cursor::new(prev_vec), &mut report, &Context::new()).unwrap();
let validated = report
.logged_items()
.iter()
.filter(|i| {
i.validation_status.as_deref() == Some(validation_status::INGREDIENT_MANIFEST_VALIDATED)
})
.count();
assert!(
validated <= 4 * DEPTH,
"shared ingredient subtrees must be verified only once"
);
}
#[test]
fn test_custom_anchor_ekus() {
let mut context = crate::context::Context::new();
let ap = fixture_path("C.jpg");
let mut stream = std::fs::File::open(&ap).unwrap();
let format = "image/jpeg";
let (manifest_bytes, _remote_url) =
Store::load_jumbf_from_stream(format, &mut stream, &context).unwrap();
let mut log = StatusTracker::default();
let _store = Store::from_jumbf_with_context(&manifest_bytes, &mut log, &context).unwrap();
assert!(!log.has_any_error());
let mut settings = Settings::default();
let ekus = settings.trust.trust_config.take();
let anchors = settings.trust.anchors.as_mut().unwrap();
assert!(anchors.len() == 1); anchors[0].trust_config = ekus;
settings.validate().unwrap();
context.set_settings(settings).unwrap();
stream.rewind().unwrap();
log = StatusTracker::default();
let _store = Store::from_stream(format, &mut stream, &mut log, &context).unwrap();
assert!(!log.has_any_error());
}