#[cfg(test)]
use crate::c2pa_cbor::encode;
use crate::c2pa_cbor::{map_from_pairs, Profile, Value};
#[cfg(test)]
use sha2::{Digest, Sha256};
pub const HASH_ALG_SHA256: &str = "sha256";
#[derive(Debug, Clone)]
pub struct ClaimGeneratorInfo {
pub name: String,
pub version: String,
pub spec_version: Option<String>,
pub c2pa_rs: Option<String>,
}
impl ClaimGeneratorInfo {
pub fn to_value(&self) -> Value {
let mut pairs: Vec<(String, Value)> = vec![
("name".into(), Value::Text(self.name.clone())),
("version".into(), Value::Text(self.version.clone())),
];
if let Some(rs) = &self.c2pa_rs {
pairs.push(("org.contentauth.c2pa_rs".into(), Value::Text(rs.clone())));
}
if let Some(sv) = &self.spec_version {
pairs.push(("specVersion".into(), Value::Text(sv.clone())));
}
map_from_pairs(pairs)
}
}
pub struct AssertionRef<'a> {
pub label: &'a str,
pub jumbf_content: &'a [u8],
}
#[cfg(test)]
fn hash_assertion(content: &[u8]) -> Vec<u8> {
let mut h = Sha256::new();
h.update(content);
h.finalize().to_vec()
}
#[cfg(test)]
fn assertion_ref_value(label: &str, content: &[u8]) -> Value {
map_from_pairs([
(
"url".into(),
Value::Text(format!("self#jumbf=c2pa.assertions/{label}")),
),
("hash".into(), Value::Bytes(hash_assertion(content))),
("alg".into(), Value::Text(HASH_ALG_SHA256.into())),
])
}
pub struct ClaimOptions<'a> {
pub manifest_label: &'a str,
pub instance_id: &'a str,
pub generator: &'a ClaimGeneratorInfo,
pub title: Option<&'a str>,
pub alg: &'a str,
pub profile: Profile,
}
#[cfg(test)]
pub fn build_claim_value(opts: &ClaimOptions, assertions: &[AssertionRef]) -> Value {
let refs: Vec<Value> = assertions
.iter()
.map(|a| assertion_ref_value(a.label, a.jumbf_content))
.collect();
let mut pairs: Vec<(String, Value)> = vec![
(
"instanceID".into(),
Value::Text(opts.instance_id.to_string()),
),
("claim_generator_info".into(), opts.generator.to_value()),
(
"signature".into(),
Value::Text(format!(
"self#jumbf=/c2pa/{}/c2pa.signature",
opts.manifest_label
)),
),
("created_assertions".into(), Value::Array(refs)),
("alg".into(), Value::Text(opts.alg.to_string())),
];
if let Some(t) = opts.title {
pairs.push(("dc:title".into(), Value::Text(t.to_string())));
}
map_from_pairs(pairs)
}
#[cfg(test)]
pub fn build_claim_cbor(
opts: &ClaimOptions,
assertions: &[AssertionRef],
) -> Result<Vec<u8>, crate::c2pa_cbor::EncodeError> {
let claim = build_claim_value(opts, assertions);
encode(&claim, opts.profile)
}
#[cfg(test)]
mod tests {
use super::*;
fn gen_info() -> ClaimGeneratorInfo {
ClaimGeneratorInfo {
name: "Encypher Enterprise API/1.0".into(),
version: "1.0".into(),
spec_version: Some("2.2".into()),
c2pa_rs: Some("0.78.4".into()),
}
}
#[test]
fn claim_has_required_v2_fields_only() {
let gen = gen_info();
let opts = ClaimOptions {
manifest_label: "urn:c2pa:abc",
instance_id: "urn:uuid:def",
generator: &gen,
title: Some("Test"),
alg: HASH_ALG_SHA256,
profile: Profile::LegacyPipelineBDefinite,
};
let assertions = [AssertionRef {
label: "c2pa.actions.v2",
jumbf_content: b"dummy",
}];
let claim = build_claim_value(&opts, &assertions);
assert!(claim.get("instanceID").is_some());
assert!(claim.get("claim_generator_info").is_some());
assert!(claim.get("signature").is_some());
assert!(claim.get("created_assertions").is_some());
assert!(claim.get("alg").is_some());
assert!(claim.get("dc:title").is_some());
assert!(claim.get("claim_generator").is_none());
assert!(claim.get("dc:format").is_none());
assert!(claim.get("assertions").is_none());
}
#[test]
fn signature_uri_references_manifest() {
let gen = gen_info();
let opts = ClaimOptions {
manifest_label: "urn:c2pa:xyz",
instance_id: "urn:uuid:1",
generator: &gen,
title: None,
alg: HASH_ALG_SHA256,
profile: Profile::LegacyPipelineBDefinite,
};
let claim = build_claim_value(&opts, &[]);
assert_eq!(
claim.get("signature").and_then(|v| v.as_text()),
Some("self#jumbf=/c2pa/urn:c2pa:xyz/c2pa.signature")
);
}
#[test]
fn assertion_hash_is_sha256_of_content() {
let content = b"hello assertion";
let mut h = Sha256::new();
h.update(content);
let expected = h.finalize().to_vec();
let v = assertion_ref_value("c2pa.actions.v2", content);
assert_eq!(
v.get("hash").and_then(|x| x.as_bytes()),
Some(&expected[..])
);
}
#[test]
fn generator_info_preserves_c2pa_rs_compat_field() {
let gen = gen_info();
let v = gen.to_value();
assert_eq!(
v.get("org.contentauth.c2pa_rs").and_then(|x| x.as_text()),
Some("0.78.4")
);
assert_eq!(v.get("specVersion").and_then(|x| x.as_text()), Some("2.2"));
}
#[test]
fn profile_controls_claim_encoding() {
let gen = gen_info();
let opts_def = ClaimOptions {
manifest_label: "urn:c2pa:a",
instance_id: "urn:uuid:b",
generator: &gen,
title: None,
alg: HASH_ALG_SHA256,
profile: Profile::LegacyPipelineBDefinite,
};
let def = build_claim_cbor(&opts_def, &[]).unwrap();
assert!((0xa0..=0xb7).contains(&def[0]));
let opts_indef = ClaimOptions {
profile: Profile::LegacyC2paRs0784Indefinite,
..opts_def
};
let indef = build_claim_cbor(&opts_indef, &[]).unwrap();
assert_eq!(indef[0], 0xbf); }
}