use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::{json, Map, Value};
use std::collections::HashMap;
use std::fmt;
use std::path::Path;
use uuid::Uuid;
use crate::assertion::{
get_thumbnail_image_type, get_thumbnail_instance, get_thumbnail_type, Assertion, AssertionBase,
AssertionData,
};
use crate::assertions::{self, labels, BmffHash, DataHash};
use crate::cose_validator::{get_signing_info, verify_cose, verify_cose_async};
use crate::hashed_uri::HashedUri;
use crate::jumbf::{
self,
boxes::{CAICBORAssertionBox, CAIJSONAssertionBox, CAIUUIDAssertionBox, JumbfEmbeddedFileBox},
};
use crate::salt::{SaltGenerator, NO_SALT};
use crate::utils::hash_utils::{hash_by_alg, vec_compare, verify_by_alg};
use crate::error::{Error, Result};
use crate::status_tracker::{log_item, OneShotStatusTracker, StatusTracker};
use crate::validation_status;
use crate::validator::ValidationInfo;
const BUILD_HASH_ALG: &str = "sha256";
use HashedUri as C2PAAssertion;
const GH_FULL_VERSION_LIST: &str = "Sec-CH-UA-Full-Version-List";
const GH_UA: &str = "Sec-CH-UA";
pub enum ClaimAssetData<'a> {
PathData(&'a Path),
ByteData(&'a [u8]),
}
#[derive(PartialEq, Clone)]
pub struct ClaimAssertion {
assertion: Assertion,
instance: usize,
hash_val: Vec<u8>,
hash_alg: String,
salt: Option<Vec<u8>>,
}
impl ClaimAssertion {
pub fn new(
assertion: Assertion,
instance: usize,
hashval: &[u8],
alg: &str,
salt: Option<Vec<u8>>,
) -> ClaimAssertion {
ClaimAssertion {
assertion,
instance,
hash_val: hashval.to_vec(),
hash_alg: alg.to_string(),
salt,
}
}
pub fn update_assertion(&mut self, assertion: Assertion, hash: Vec<u8>) -> Result<()> {
self.hash_val = hash;
self.assertion = assertion;
Ok(())
}
pub fn label(&self) -> String {
let al_ref = self.assertion.label();
if self.instance > 0 {
if get_thumbnail_type(&al_ref) == labels::INGREDIENT_THUMBNAIL {
format!(
"{}__{}.{}",
get_thumbnail_type(&al_ref),
self.instance,
get_thumbnail_image_type(&al_ref)
)
} else {
format!("{}__{}", al_ref, self.instance)
}
} else {
self.assertion.label()
}
}
pub fn instance(&self) -> usize {
self.instance
}
pub fn instance_string(&self) -> String {
format!("{}", self.instance)
}
pub fn label_raw(&self) -> String {
self.assertion.label()
}
pub fn assertion(&self) -> &Assertion {
&self.assertion
}
pub fn hash(&self) -> &[u8] {
&self.hash_val
}
pub fn salt(&self) -> &Option<Vec<u8>> {
&self.salt
}
pub fn hash_alg(&self) -> &str {
&self.hash_alg
}
pub fn is_same_type(&self, input_assertion: &Assertion) -> bool {
Assertion::assertions_eq(&self.assertion, input_assertion)
}
}
impl fmt::Debug for ClaimAssertion {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:?}, instance: {}", self.assertion, self.instance)
}
}
#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
pub struct Claim {
#[serde(skip_deserializing, skip_serializing)]
update_manifest: bool,
#[serde(skip_serializing_if = "Option::is_none", rename = "dc:title")]
pub title: Option<String>,
#[serde(rename = "dc:format")]
pub format: String,
#[serde(rename = "instanceID")]
pub instance_id: String,
#[serde(skip_deserializing, skip_serializing)]
ingredients_store: HashMap<String, Vec<Claim>>,
#[serde(skip_deserializing, skip_serializing)]
box_prefix: String,
#[serde(skip_deserializing, skip_serializing)]
signature_val: Vec<u8>,
#[serde(skip_deserializing, skip_serializing)]
root: String,
#[serde(skip_deserializing, skip_serializing)]
label: String,
#[serde(skip_deserializing, skip_serializing)]
assertion_store: Vec<ClaimAssertion>,
#[serde(skip_deserializing, skip_serializing)]
vc_store: Vec<AssertionData>,
claim_generator: String,
signature: String, assertions: Vec<C2PAAssertion>,
#[serde(skip_deserializing, skip_serializing)]
original_bytes: Option<Vec<u8>>,
#[serde(skip_serializing_if = "Option::is_none")]
redacted_assertions: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
alg: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
alg_soft: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
claim_generator_hints: Option<HashMap<String, Value>>,
}
pub enum AssertionStoreJsonFormat {
None, KeyValue, KeyValueNoBinary, OrderedList, OrderedListNoBinary, }
#[derive(Serialize, Deserialize, Debug)]
pub struct JsonOrderedAssertionData {
label: String,
data: Value,
hash: String,
is_binary: bool,
mime_type: String,
}
impl Claim {
pub const LABEL: &'static str = assertions::labels::CLAIM;
pub fn new(claim_generator: &str, vendor: Option<&str>) -> Self {
let urn = Uuid::new_v4();
let l = match vendor {
Some(v) => format!(
"{}:{}",
v.to_lowercase(),
urn.to_urn().encode_lower(&mut Uuid::encode_buffer())
),
None => urn
.to_urn()
.encode_lower(&mut Uuid::encode_buffer())
.to_string(),
};
Claim {
box_prefix: "self#jumbf".to_string(),
root: jumbf::labels::MANIFEST_STORE.to_string(),
signature_val: Vec::new(),
ingredients_store: HashMap::new(),
label: l,
signature: "".to_string(),
claim_generator: claim_generator.to_string(),
assertion_store: Vec::new(),
vc_store: Vec::new(),
assertions: Vec::new(),
original_bytes: None,
redacted_assertions: None,
alg: Some(BUILD_HASH_ALG.to_string()),
alg_soft: None,
claim_generator_hints: None,
title: None,
format: "".to_string(),
instance_id: "".to_string(),
update_manifest: false,
}
}
pub fn build(&mut self) -> Result<()> {
if self.signature.is_empty() {
self.add_signature_box_link();
}
Ok(())
}
pub fn build_version() -> &'static str {
Self::LABEL
}
pub fn label(&self) -> &str {
&self.label
}
pub fn uri(&self) -> String {
jumbf::labels::to_manifest_uri(&self.label)
}
pub fn assertion_uri(&self, assertion_label: &str) -> String {
jumbf::labels::to_assertion_uri(&self.label, assertion_label)
}
pub fn signature_uri(&self) -> String {
jumbf::labels::to_signature_uri(&self.label)
}
fn add_signature_box_link(&mut self) {
self.signature = format!("{}={}", self.box_prefix, jumbf::labels::SIGNATURE);
}
pub(crate) fn set_signature_val(&mut self, signature: Vec<u8>) {
self.signature_val = signature;
}
pub fn signature_val(&self) -> &Vec<u8> {
&self.signature_val
}
pub fn claim_generator(&self) -> &str {
&self.claim_generator
}
pub fn format(&self) -> &str {
&self.format
}
pub fn instance_id(&self) -> &str {
&self.instance_id
}
pub fn set_title(&mut self, title: Option<String>) {
self.title = title;
}
pub fn title(&self) -> Option<&String> {
self.title.as_ref()
}
pub fn alg(&self) -> &str {
match self.alg.as_ref() {
Some(alg) => alg,
None => BUILD_HASH_ALG,
}
}
pub fn alg_soft(&self) -> Option<&String> {
self.alg_soft.as_ref()
}
pub fn update_manifest(&self) -> bool {
self.update_manifest
}
pub(crate) fn set_update_manifest(&mut self, is_update_manifest: bool) {
self.update_manifest = is_update_manifest;
}
pub fn add_claim_generator_hint(&mut self, hint_key: &str, hint_value: Value) {
if self.claim_generator_hints.is_none() {
self.claim_generator_hints = Some(HashMap::new());
}
if let Some(map) = &mut self.claim_generator_hints {
let curr_val = match hint_key {
GH_UA | GH_FULL_VERSION_LIST => {
if let Some(curr_ch_ua) = map.get(hint_key) {
curr_ch_ua.as_str().map(|curr_val| curr_val.to_owned())
} else {
None
}
}
_ => None,
};
if let Some(curr_val) = curr_val {
if let Some(append_val) = hint_value.as_str() {
map.insert(
hint_key.to_string(),
Value::String(format!("{}, {}", curr_val, append_val)),
);
}
return;
}
map.insert(hint_key.to_string(), hint_value);
}
}
pub fn get_claim_generator_hint_map(&self) -> Option<&HashMap<String, Value>> {
self.claim_generator_hints.as_ref()
}
pub fn calc_box_hash(
label: &str,
assertion: &Assertion,
salt: Option<Vec<u8>>,
alg: &str,
) -> Result<Vec<u8>> {
let d = assertion.decode_data();
let mut hash_bytes = Vec::with_capacity(2048);
match d {
AssertionData::Json(_) => {
let mut json_data = CAIJSONAssertionBox::new(label);
json_data.add_json(assertion.data().to_vec());
if let Some(salt) = salt {
json_data.set_salt(salt)?;
}
json_data.super_box().write_box_payload(&mut hash_bytes)?;
}
AssertionData::Binary(_) => {
let mut data = JumbfEmbeddedFileBox::new(label);
data.add_data(assertion.data().to_vec(), assertion.mime_type(), None);
if let Some(salt) = salt {
data.set_salt(salt)?;
}
data.super_box().write_box_payload(&mut hash_bytes)?;
}
AssertionData::Cbor(_) => {
let mut cbor_data = CAICBORAssertionBox::new(label);
cbor_data.add_cbor(assertion.data().to_vec());
if let Some(salt) = salt {
cbor_data.set_salt(salt)?;
}
cbor_data.super_box().write_box_payload(&mut hash_bytes)?;
}
AssertionData::Uuid(uuid_str, _) => {
let mut data = CAIUUIDAssertionBox::new(label);
data.add_uuid(uuid_str, assertion.data().to_vec())?;
if let Some(salt) = salt {
data.set_salt(salt)?;
}
data.super_box().write_box_payload(&mut hash_bytes)?;
}
}
Ok(hash_by_alg(alg, &hash_bytes, None))
}
pub fn add_assertion(
&mut self,
assertion_builder: &impl AssertionBase,
) -> Result<C2PAAssertion> {
self.add_assertion_with_salt(assertion_builder, NO_SALT)
}
pub fn add_assertion_with_salt(
&mut self,
assertion_builder: &impl AssertionBase,
salt_generator: &impl SaltGenerator,
) -> Result<C2PAAssertion> {
let assertion = assertion_builder.to_assertion()?;
let as_label = self.make_assertion_instance_label(assertion.label().as_ref());
let salt = salt_generator.generate_salt();
let hash = Claim::calc_box_hash(&as_label, &assertion, salt.clone(), self.alg())?;
let link = jumbf::labels::to_assertion_uri(self.label(), &as_label);
let link_relative = jumbf::labels::to_relative_uri(&link);
let c2pa_assertion = C2PAAssertion::new(link_relative, None, &hash);
let (_l, instance) = Claim::assertion_label_from_link(&as_label);
let ca = ClaimAssertion::new(assertion, instance, &hash, self.alg(), salt);
self.assertion_store.push(ca);
self.assertions.push(c2pa_assertion.clone());
Ok(c2pa_assertion)
}
pub(crate) fn vc_id(vc_json: &str) -> Result<String> {
let vc: Value =
serde_json::from_str(vc_json).map_err(|_err| Error::VerifiableCredentialInvalid)?;
let credential_subject = vc
.get("credentialSubject")
.ok_or(Error::VerifiableCredentialInvalid)?;
let id = credential_subject
.get("id")
.ok_or(Error::VerifiableCredentialInvalid)?
.as_str()
.ok_or(Error::VerifiableCredentialInvalid)?;
Ok(id.to_string())
}
pub fn add_verifiable_credential(&mut self, vc_json: &str) -> Result<HashedUri> {
let id = Claim::vc_id(vc_json)?;
let hash = hash_by_alg(self.alg(), vc_json.as_bytes(), None);
let link = jumbf::labels::to_verifiable_credential_uri(self.label(), &id);
let c2pa_assertion = C2PAAssertion::new(link, Some(self.alg().to_string()), &hash);
let credential = AssertionData::Json(vc_json.to_string());
self.vc_store.push(credential);
Ok(c2pa_assertion)
}
pub fn get_verifiable_credentials(&self) -> &Vec<AssertionData> {
&self.vc_store
}
pub(crate) fn put_assertion_store(&mut self, assertion: ClaimAssertion) {
self.assertion_store.push(assertion);
}
#[cfg(feature = "file_io")]
pub(crate) fn update_data_hash(&mut self, mut data_hash: DataHash) -> Result<()> {
let mut replacement_assertion = data_hash.to_assertion()?;
match self.assertion_store.iter_mut().find(|assertion| {
if !Assertion::assertions_eq(&replacement_assertion, assertion.assertion()) {
return false;
}
if let Ok(dh) = DataHash::from_assertion(assertion.assertion()) {
dh.name == data_hash.name
} else {
false
}
}) {
Some(ref mut dh_assertion) => {
let original_hash = dh_assertion.hash().to_vec();
let original_len = dh_assertion.assertion().data().len();
data_hash.pad_to_size(original_len)?;
replacement_assertion = data_hash.to_assertion()?;
let replacement_hash = Claim::calc_box_hash(
&dh_assertion.label(),
&replacement_assertion,
dh_assertion.salt().clone(),
dh_assertion.hash_alg(),
)?;
dh_assertion.update_assertion(replacement_assertion, replacement_hash)?;
match self.assertions.iter_mut().find_map(|f| {
if f.url().contains(&dh_assertion.label())
&& vec_compare(&f.hash(), &original_hash)
{
f.update_hash(dh_assertion.hash().to_vec());
Some(f)
} else {
None
}
}) {
Some(_) => Ok(()),
None => Err(Error::NotFound),
}
}
None => Err(Error::NotFound),
}
}
#[cfg(feature = "file_io")]
pub(crate) fn update_bmff_hash(&mut self, bmff_hash: BmffHash) -> Result<()> {
let replacement_assertion = bmff_hash.to_assertion()?;
match self.assertion_store.iter_mut().find(|assertion| {
Assertion::assertions_eq(&replacement_assertion, assertion.assertion())
}) {
Some(ref mut bmff_assertion) => {
let original_hash = bmff_assertion.hash().to_vec();
let replacement_hash = Claim::calc_box_hash(
&bmff_assertion.label(),
&replacement_assertion,
bmff_assertion.salt().clone(),
bmff_assertion.hash_alg(),
)?;
bmff_assertion.update_assertion(replacement_assertion, replacement_hash)?;
match self.assertions.iter_mut().find_map(|f| {
if f.url().contains(&bmff_assertion.label())
&& vec_compare(&f.hash(), &original_hash)
{
f.update_hash(bmff_assertion.hash().to_vec());
Some(f)
} else {
None
}
}) {
Some(_) => Ok(()),
None => Err(Error::NotFound),
}
}
None => Err(Error::NotFound),
}
}
fn redact_assertion(&mut self, assertion_uri: &str) -> Result<()> {
let (label, _instance) = Claim::assertion_label_from_link(assertion_uri);
if label == assertions::labels::ACTIONS {
return Err(Error::AssertionInvalidRedaction);
}
if let Some(index) = self
.assertion_store
.iter()
.position(|x| assertion_uri.contains(&x.label()))
{
self.assertion_store.remove(index);
Ok(())
} else {
Err(Error::AssertionInvalidRedaction)
}
}
pub fn hash(&self) -> Vec<u8> {
match self.data() {
Ok(claim_data) => hash_by_alg(self.alg(), &claim_data, None),
Err(_) => Vec::new(), }
}
pub fn signing_time(&self) -> Option<DateTime<Utc>> {
if let Some(validation_data) = self.signature_info() {
validation_data.date
} else {
None
}
}
pub fn signing_issuer(&self) -> Option<String> {
if let Some(validation_data) = self.signature_info() {
validation_data.issuer_org
} else {
None
}
}
pub fn signature_info(&self) -> Option<ValidationInfo> {
let sig = self.signature_val();
let data = self.data().ok()?;
let mut validation_log = OneShotStatusTracker::new();
Some(get_signing_info(sig, &data, &mut validation_log))
}
pub async fn verify_claim_async<'a>(
claim: &Claim,
asset_bytes: &'a [u8],
is_provenance: bool,
validation_log: &mut impl StatusTracker,
) -> Result<()> {
let sig = claim.signature_val().clone();
let additional_bytes: Vec<u8> = Vec::new();
let claim_data = claim.data()?;
let sig_box_err = match jumbf::labels::manifest_label_from_uri(&claim.signature) {
Some(signature_url) if signature_url != claim.label() => true,
_ => {
jumbf::labels::box_name_from_uri(&claim.signature).unwrap_or_else(|| "".to_string())
!= jumbf::labels::SIGNATURE
} };
if sig_box_err {
let log_item = log_item!(
claim.signature_uri(),
"signature missing",
"verify_claim_async"
)
.error(Error::ClaimMissingSignatureBox)
.validation_status(validation_status::CLAIM_SIGNATURE_MISSING);
validation_log.log(log_item, Some(Error::ClaimMissingSignatureBox))?;
}
let verified = verify_cose_async(
sig,
claim_data,
additional_bytes,
!is_provenance,
validation_log,
)
.await;
Claim::verify_internal(
claim,
&ClaimAssetData::ByteData(asset_bytes),
is_provenance,
verified,
validation_log,
)
}
pub fn verify_claim<'a>(
claim: &Claim,
asset_data: &ClaimAssetData<'a>,
is_provenance: bool,
validation_log: &mut impl StatusTracker,
) -> Result<()> {
let sig = claim.signature_val();
let additional_bytes: Vec<u8> = Vec::new();
let sig_box_err = match jumbf::labels::manifest_label_from_uri(&claim.signature) {
Some(signature_url) if signature_url != claim.label() => true,
_ => {
jumbf::labels::box_name_from_uri(&claim.signature).unwrap_or_else(|| "".to_string())
!= jumbf::labels::SIGNATURE
} };
if sig_box_err {
let log_item = log_item!(claim.signature_uri(), "signature missing", "verify_claim")
.error(Error::ClaimMissingSignatureBox)
.validation_status(validation_status::CLAIM_SIGNATURE_MISSING);
validation_log.log(log_item, Some(Error::ClaimMissingSignatureBox))?;
}
let data = if let Some(ref original_bytes) = claim.original_bytes {
original_bytes
} else {
return Err(Error::ClaimDecoding);
};
let verified = verify_cose(sig, data, &additional_bytes, !is_provenance, validation_log);
Claim::verify_internal(claim, asset_data, is_provenance, verified, validation_log)
}
fn verify_internal<'a>(
claim: &Claim,
asset_data: &ClaimAssetData<'a>,
is_provenance: bool,
verified: Result<ValidationInfo>,
validation_log: &mut impl StatusTracker,
) -> Result<()> {
const UNNAMED: &str = "unnamed";
let default_str = |s: &String| s.clone();
match verified {
Ok(vi) => {
if !vi.validated {
let log_item = log_item!(
claim.signature_uri(),
"claim signature is not valid",
"verify_internal"
)
.error(Error::CoseSignature)
.validation_status(validation_status::CLAIM_SIGNATURE_MISMATCH);
validation_log.log(log_item, Some(Error::CoseSignature))?;
} else {
let log_item = log_item!(
claim.signature_uri(),
"claim signature valid",
"verify_internal"
)
.validation_status(validation_status::CLAIM_SIGNATURE_VALIDATED);
validation_log.log_silent(log_item);
}
}
Err(parse_err) => {
let log_item = log_item!(
claim.signature_uri(),
"claim signature is not valid",
"verify_internal"
)
.error(parse_err)
.validation_status(validation_status::CLAIM_SIGNATURE_MISMATCH);
validation_log.log(log_item, Some(Error::CoseSignature))?;
}
};
if let Some(redactions) = claim.redactions() {
for r in redactions {
let r_manifest = jumbf::labels::manifest_label_from_uri(r)
.ok_or(Error::AssertionInvalidRedaction)?;
if claim.label().contains(&r_manifest) {
let log_item = log_item!(
claim.uri(),
"claim contains self redaction",
"verify_internal"
)
.error(Error::ClaimSelfRedact)
.validation_status(validation_status::ASSERTION_SELF_REDACTED);
validation_log.log(log_item, Some(Error::ClaimSelfRedact))?;
}
if r.contains(assertions::labels::ACTIONS) {
let log_item = log_item!(
claim.uri(),
"readaction of action assertions disallowed",
"verify_internal"
)
.error(Error::ClaimDisallowedRedaction)
.validation_status(validation_status::ACTION_ASSERTION_REDACTED);
validation_log.log(log_item, Some(Error::ClaimDisallowedRedaction))?;
}
}
}
if claim.update_manifest() && claim.label().contains(assertions::labels::ACTIONS) {
let log_item = log_item!(
claim.uri(),
"update manifests cannot contain actions",
"verify_internal"
)
.error(Error::UpdateManifestInvalid)
.validation_status(validation_status::MANIFEST_UPDATE_INVALID);
validation_log.log(log_item, Some(Error::UpdateManifestInvalid))?;
}
for assertion in claim.assertions() {
let (label, instance) = Claim::assertion_label_from_link(&assertion.url());
match claim.get_claim_assertion(&label, instance) {
Some(ca) => {
if !vec_compare(ca.hash(), &assertion.hash()) {
let log_item = log_item!(
assertion.url(),
format!("hash does not match assertion data: {}", assertion.url()),
"verify_internal"
)
.error(Error::HashMismatch(format!(
"Assertion hash failure: {}",
assertion.url()
)))
.validation_status(validation_status::ASSERTION_HASHEDURI_MISMATCH);
validation_log.log(
log_item,
Some(Error::HashMismatch(format!(
"Assertion hash failure: {}",
assertion.url()
))),
)?;
} else {
let log_item = log_item!(
assertion.url(),
format!("hashed uri matched: {}", assertion.url()),
"verify_internal"
)
.validation_status(validation_status::ASSERTION_HASHEDURI_MATCH);
validation_log.log_silent(log_item);
}
}
None => {
let log_item = log_item!(
assertion.url(),
format!("cannot find matching assertion: {}", assertion.url()),
"verify_internal"
)
.error(Error::AssertionMissing {
url: assertion.url(),
})
.validation_status(validation_status::ASSERTION_MISSING);
validation_log.log(
log_item,
Some(Error::AssertionMissing {
url: assertion.url(),
}),
)?;
}
}
}
if is_provenance {
if claim.data_hash_assertions().is_empty() && !claim.update_manifest() {
let log_item = log_item!(
&claim.uri(),
"claim missing data binding",
"verify_internal"
)
.error(Error::ClaimMissingHardBinding)
.validation_status(validation_status::HARD_BINDINGS_MISSING);
validation_log.log(log_item, Some(Error::ClaimMissingHardBinding))?;
}
if !claim.data_hash_assertions().is_empty() && claim.update_manifest() {
let log_item = log_item!(
&claim.uri(),
"update manifests cannot contain data hash assertions",
"verify_internal"
)
.error(Error::UpdateManifestInvalid)
.validation_status(validation_status::MANIFEST_UPDATE_INVALID);
validation_log.log(log_item, Some(Error::UpdateManifestInvalid))?;
}
for dh_assertion in claim.data_hash_assertions() {
if dh_assertion.label_root() == DataHash::LABEL {
let dh = DataHash::from_assertion(dh_assertion)?;
let name = dh.name.as_ref().map_or(UNNAMED.to_string(), default_str);
if !dh.is_remote_hash() {
let hash_result = match asset_data {
ClaimAssetData::PathData(asset_path) => {
dh.verify_hash(asset_path, Some(claim.alg().to_string()))
}
ClaimAssetData::ByteData(asset_bytes) => {
dh.verify_in_memory_hash(asset_bytes, Some(claim.alg().to_string()))
}
};
match hash_result {
Ok(_a) => {
let log_item = log_item!(
claim.assertion_uri(&dh_assertion.label()),
"data hash valid",
"verify_internal"
)
.validation_status(validation_status::ASSERTION_DATAHASH_MATCH);
validation_log.log_silent(log_item);
continue;
}
Err(e) => {
let log_item = log_item!(
claim.assertion_uri(&dh_assertion.label()),
format!("asset hash error, name: {}, error: {}", name, e),
"verify_internal"
)
.error(Error::HashMismatch(format!("Asset hash failure: {}", e)))
.validation_status(validation_status::ASSERTION_DATAHASH_MISMATCH);
validation_log.log(
log_item,
Some(Error::HashMismatch(format!("Asset hash failure: {}", e))),
)?;
}
}
}
} else {
let dh = BmffHash::from_assertion(dh_assertion)?;
let name = dh.name().map_or("unnamed".to_string(), default_str);
let hash_result = match asset_data {
ClaimAssetData::PathData(asset_path) => {
dh.verify_hash(asset_path, Some(claim.alg().to_string()))
}
ClaimAssetData::ByteData(asset_bytes) => {
dh.verify_in_memory_hash(asset_bytes, Some(claim.alg().to_string()))
}
};
match hash_result {
Ok(_a) => {
let log_item = log_item!(
claim.assertion_uri(&dh_assertion.label()),
"data hash valid",
"verify_internal"
)
.validation_status(validation_status::ASSERTION_DATAHASH_MATCH);
validation_log.log_silent(log_item);
continue;
}
Err(e) => {
let log_item = log_item!(
claim.assertion_uri(&dh_assertion.label()),
format!("asset hash error, name: {}, error: {}", name, e),
"verify_internal"
)
.error(Error::HashMismatch(format!("Asset hash failure: {}", e)))
.validation_status(validation_status::ASSERTION_DATAHASH_MISMATCH);
validation_log.log(
log_item,
Some(Error::HashMismatch(format!("Asset hash failure: {}", e))),
)?;
}
}
}
}
}
Ok(())
}
pub fn verify_hash(&self, hash: &[u8]) -> bool {
if let Some(ref original_bytes) = self.original_bytes {
verify_by_alg(self.alg(), hash, original_bytes, None)
} else if let Ok(claim_data) = self.data() {
verify_by_alg(self.alg(), hash, &claim_data, None)
} else {
false
}
}
pub fn data_hash_assertions(&self) -> Vec<&Assertion> {
let dummy_data = AssertionData::Cbor(Vec::new());
let dummy_hash = Assertion::new(DataHash::LABEL, None, dummy_data);
let mut data_hashes = self.assertions_by_type(&dummy_hash);
let dummy_bmff_data = AssertionData::Cbor(Vec::new());
let dummy_bmff_hash = Assertion::new(assertions::labels::BMFF_HASH, None, dummy_bmff_data);
data_hashes.append(&mut self.assertions_by_type(&dummy_bmff_hash));
data_hashes
}
pub fn bmff_hash_assertions(&self) -> Vec<&Assertion> {
let dummy_bmff_data = AssertionData::Cbor(Vec::new());
let dummy_bmff_hash = Assertion::new(assertions::labels::BMFF_HASH, None, dummy_bmff_data);
self.assertions_by_type(&dummy_bmff_hash)
}
pub fn ingredient_assertions(&self) -> Vec<&Assertion> {
let dummy_data = AssertionData::Cbor(Vec::new());
let dummy_ingredient = Assertion::new(labels::INGREDIENT, None, dummy_data);
self.assertions_by_type(&dummy_ingredient)
}
pub fn claim_assertion_store(&self) -> &Vec<ClaimAssertion> {
&self.assertion_store
}
pub fn claim_ingredient_store(&self) -> &HashMap<String, Vec<Claim>> {
&self.ingredients_store
}
pub fn claim_ingredient(&self, claim_guid: &str) -> Option<&Vec<Claim>> {
self.ingredients_store.get(claim_guid)
}
pub(crate) fn add_ingredient_data(
&mut self,
provenance_label: &str,
mut ingredient: Vec<Claim>,
redactions_opt: Option<Vec<String>>,
) -> Result<()> {
if let Some(redactions) = &redactions_opt {
for redaction in redactions {
if let Some(claim) = ingredient
.iter_mut()
.find(|x| redaction.contains(&x.label()))
{
claim.redact_assertion(redaction)?;
} else {
return Err(Error::AssertionRedactionNotFound);
}
}
}
self.redacted_assertions = redactions_opt;
self.ingredients_store
.insert(provenance_label.to_string(), ingredient);
Ok(())
}
pub fn redactions(&self) -> Option<&Vec<String>> {
self.redacted_assertions.as_ref()
}
pub fn assertion_store(&self) -> Vec<Assertion> {
self.assertion_store
.iter()
.map(|x| x.assertion.clone())
.collect()
}
pub fn assertions_by_type(&self, assertion_proto: &Assertion) -> Vec<&Assertion> {
self.assertion_store
.iter()
.filter_map(|x| {
if Assertion::assertions_eq(assertion_proto, x.assertion()) {
Some(&x.assertion)
} else {
None
}
})
.collect()
}
pub fn assertions(&self) -> &Vec<C2PAAssertion> {
&self.assertions
}
pub fn data(&self) -> Result<Vec<u8>> {
match self.original_bytes {
Some(ref ob) => Ok(ob.clone()),
None => Ok(serde_cbor::ser::to_vec(&self).map_err(|_err| Error::ClaimEncoding)?),
}
}
pub fn from_data(label: &str, data: &[u8]) -> Result<Claim> {
let mut claim: Claim = serde_cbor::from_slice(data).map_err(|_err| Error::ClaimDecoding)?;
claim.label = label.to_string();
claim.original_bytes = Some(data.to_owned());
Ok(claim)
}
pub fn to_json(
&self,
assertion_store_format: AssertionStoreJsonFormat,
pretty: bool,
) -> Result<String> {
let mut v = serde_json::to_value(self)?;
match assertion_store_format {
AssertionStoreJsonFormat::None => {}
AssertionStoreJsonFormat::KeyValue | AssertionStoreJsonFormat::KeyValueNoBinary => {
if let Value::Object(ref mut map) = v {
let mut json_map: Map<String, Value> = Map::new();
let iter = self.assertions.iter().zip(&self.assertion_store);
for (_key, claim_assertion) in iter {
let link = claim_assertion.label();
let (label, instance) = Self::assertion_label_from_link(&link);
let label = Self::label_with_instance(&label, instance);
match claim_assertion.assertion.decode_data() {
AssertionData::Json(x) => {
let decoded = serde_json::from_str(x)?;
json_map.insert(label, decoded);
}
AssertionData::Cbor(x) => {
let buf: Vec<u8> = Vec::new();
let mut from = serde_cbor::Deserializer::from_slice(x);
let mut to = serde_json::Serializer::new(buf);
serde_transcode::transcode(&mut from, &mut to)
.map_err(|_err| Error::AssertionEncoding)?;
let buf2 = to.into_inner();
let decoded: Value = serde_json::from_slice(&buf2)
.map_err(|_err| Error::AssertionEncoding)?;
json_map.insert(label, decoded);
}
AssertionData::Binary(x) => {
let d = match assertion_store_format {
AssertionStoreJsonFormat::KeyValue => {
Value::String(base64::encode(x))
}
AssertionStoreJsonFormat::KeyValueNoBinary => {
Value::String("omitted".to_owned())
}
_ => Value::String("".to_owned()),
};
json_map.insert(label, d);
continue;
}
AssertionData::Uuid(s, x) => {
let d = match assertion_store_format {
AssertionStoreJsonFormat::KeyValue => {
Value::String(base64::encode(x))
}
AssertionStoreJsonFormat::KeyValueNoBinary => {
Value::String("omitted".to_owned())
}
_ => Value::String("".to_owned()),
};
let m = json!({
"uuid": s,
"data": d,
});
json_map.insert(label, m);
continue;
}
}
}
let as_val = serde_json::to_value(json_map)?;
map.insert("assertion_store".to_string(), as_val);
map.insert(
"vc_store".to_string(),
serde_json::to_value(&self.vc_store)?,
);
map.insert("label".to_string(), Value::String(self.label.to_string()));
}
}
AssertionStoreJsonFormat::OrderedList
| AssertionStoreJsonFormat::OrderedListNoBinary => {
if let Value::Object(ref mut map) = v {
let mut json_vec: Vec<Value> = Vec::new();
for claim_assertion in self.claim_assertion_store() {
match claim_assertion.assertion.decode_data() {
AssertionData::Json(x) => {
let d: Value = serde_json::from_str(x)
.map_err(|_err| Error::AssertionEncoding)?;
let j = JsonOrderedAssertionData {
label: claim_assertion.label().to_owned(),
hash: base64::encode(claim_assertion.hash()),
data: d,
is_binary: false,
mime_type: claim_assertion.assertion.mime_type(),
};
let new_val = serde_json::to_value(j)?;
json_vec.push(new_val);
}
AssertionData::Cbor(x) => {
let buf: Vec<u8> = Vec::new();
let mut from = serde_cbor::Deserializer::from_slice(x);
let mut to = serde_json::Serializer::new(buf);
serde_transcode::transcode(&mut from, &mut to)
.map_err(|_err| Error::AssertionEncoding)?;
let buf2 = to.into_inner();
let d: Value = serde_json::from_slice(&buf2)
.map_err(|_err| Error::AssertionEncoding)?;
let j = JsonOrderedAssertionData {
label: claim_assertion.label().to_owned(),
hash: base64::encode(claim_assertion.hash()),
data: d,
is_binary: false,
mime_type: claim_assertion.assertion.mime_type(),
};
let new_val = serde_json::to_value(j)?;
json_vec.push(new_val);
}
AssertionData::Binary(x) => {
let d = match assertion_store_format {
AssertionStoreJsonFormat::OrderedList => {
Value::String(base64::encode(x))
}
AssertionStoreJsonFormat::OrderedListNoBinary => {
Value::String("omitted".to_owned())
}
_ => Value::String("".to_owned()),
};
let j = JsonOrderedAssertionData {
label: claim_assertion.label().to_owned(),
hash: base64::encode(claim_assertion.hash()),
data: d,
is_binary: true,
mime_type: claim_assertion.assertion.mime_type(),
};
let new_val = serde_json::to_value(j)?;
json_vec.push(new_val);
}
AssertionData::Uuid(s, x) => {
let d = match assertion_store_format {
AssertionStoreJsonFormat::OrderedList => {
Value::String(base64::encode(x))
}
AssertionStoreJsonFormat::OrderedListNoBinary => {
Value::String("omitted".to_owned())
}
_ => Value::String("".to_owned()),
};
let m = json!({
"uuid": s,
"data": d,
});
let j = JsonOrderedAssertionData {
label: claim_assertion.label().to_owned(),
hash: base64::encode(claim_assertion.hash()),
data: m,
is_binary: true,
mime_type: claim_assertion.assertion.mime_type(),
};
let new_val = serde_json::to_value(j)?;
json_vec.push(new_val);
}
}
}
let as_val = serde_json::to_value(json_vec)?;
map.insert("assertion_store".to_string(), as_val);
map.insert("label".to_string(), Value::String(self.label.to_string()));
}
}
}
if pretty {
serde_json::to_string_pretty(&v).map_err(|e| e.into())
} else {
serde_json::to_string(&v).map_err(|e| e.into())
}
}
pub fn assertion_label_from_link(assertion_link: &str) -> (String, usize) {
let v = jumbf::labels::to_normalized_uri(assertion_link);
let v2: Vec<&str> = v.split('/').collect();
if let Some(s) = v2.last() {
if get_thumbnail_type(s) == labels::INGREDIENT_THUMBNAIL {
let instance = get_thumbnail_instance(s).unwrap_or(0);
let label = match get_thumbnail_image_type(s).as_str() {
"none" => get_thumbnail_type(s),
image_type => format!("{}.{}", get_thumbnail_type(s), image_type),
};
(label, instance)
} else {
let label_parts: Vec<&str> = s.split("__").collect();
let mut instance: usize = 0;
if label_parts.len() == 2 {
match label_parts[1].parse::<usize>() {
Ok(i) => instance = i,
_ => instance = 0,
}
}
(label_parts[0].to_owned(), instance)
}
} else {
(v2[0].to_owned(), 0)
}
}
pub fn label_with_instance(label: &str, instance: usize) -> String {
if instance == 0 {
label.to_string()
} else if get_thumbnail_type(label) == labels::INGREDIENT_THUMBNAIL {
let tn_type = get_thumbnail_image_type(label);
format!("{}__{}.{}", get_thumbnail_type(label), instance, tn_type)
} else {
format!("{}__{}", label, instance)
}
}
pub fn assertion_hashed_uri_from_label(&self, assertion_label: &str) -> Option<&C2PAAssertion> {
self.assertions()
.iter()
.find(|hashed_uri| hashed_uri.url().contains(assertion_label))
}
fn make_assertion_instance_label(&self, assertion_label: &str) -> String {
let cnt = self.next_instance(assertion_label);
Claim::label_with_instance(assertion_label, cnt)
}
pub fn get_assertion(&self, assertion_label: &str, instance: usize) -> Option<&Assertion> {
let mut iter = self.claim_assertion_store().iter().filter_map(|ca| {
if ca.label_raw() == assertion_label && ca.instance() == instance {
Some(ca.assertion())
} else {
None
}
});
iter.next()
}
pub fn get_claim_assertion(
&self,
assertion_label: &str,
instance: usize,
) -> Option<&ClaimAssertion> {
self.claim_assertion_store()
.iter()
.find(|ca| ca.label_raw() == assertion_label && ca.instance() == instance)
}
pub fn get_claim_assertion_hash(&self, assertion_label: &str) -> Option<Vec<u8>> {
let (l, i) = Claim::assertion_label_from_link(assertion_label);
self.get_claim_assertion(&l, i).map(|a| a.hash().to_vec())
}
pub fn count_instances(&self, in_label: &str) -> usize {
let (l, i) = Claim::assertion_label_from_link(in_label);
let label = Claim::label_with_instance(&l, i);
self.assertions
.iter()
.filter(|assertion| assertion.url().contains(&label))
.count()
}
fn next_instance(&self, in_label: &str) -> usize {
let (label, _) = Claim::assertion_label_from_link(in_label);
match self
.assertion_store
.iter()
.filter(|&x| x.assertion.label().contains(&label))
.map(|x| {
let (_l, i) = Claim::assertion_label_from_link(&x.label());
i
})
.max()
{
Some(last_instance) => last_instance + 1,
None => 0,
}
}
pub fn has_assertion_type(&self, in_label: &str) -> bool {
let (label, _) = Claim::assertion_label_from_link(in_label);
let found = self
.assertion_store
.iter()
.find(|&x| x.assertion.label().starts_with(&label));
!matches!(found, None)
}
pub(crate) fn to_claim_uri(manifest_label: &str) -> String {
format!(
"{}/{}",
jumbf::labels::to_manifest_uri(manifest_label),
Self::LABEL
)
}
}
#[cfg(not(target_arch = "wasm32"))]
#[cfg(test)]
pub mod tests {
#![allow(clippy::expect_used)]
#![allow(clippy::unwrap_used)]
use super::*;
use crate::utils::test::create_test_claim;
#[test]
fn test_build_claim() {
let mut claim = create_test_claim().expect("create test claim");
claim.build().expect("bad claim");
let orig_binary = claim.data().expect("failure returning data");
let restored_claim =
Claim::from_data("as_adbe_1", &orig_binary).expect("could not restore from binary");
let restored_binary = restored_claim.data().expect("failure returning data");
assert_eq!(orig_binary, restored_binary);
println!("Restored Claim: {:?}", restored_claim);
assert_eq!(orig_binary, restored_claim.original_bytes.unwrap());
let json_str = claim
.to_json(AssertionStoreJsonFormat::OrderedList, true)
.expect("could not generate json");
println!("Claim: {}", json_str);
}
#[test]
fn test_build_claim_generator_hints() {
let mut claim = create_test_claim().expect("create test claim");
claim.add_claim_generator_hint(
GH_FULL_VERSION_LIST,
Value::String(r#""user app";v="2.3.4""#.to_string()),
);
claim.add_claim_generator_hint(
GH_FULL_VERSION_LIST,
Value::String(r#""some toolkit";v="1.0.0""#.to_string()),
);
let expected_value = r#""user app";v="2.3.4", "some toolkit";v="1.0.0""#;
let cg_map = claim.get_claim_generator_hint_map().unwrap();
let value = &cg_map[GH_FULL_VERSION_LIST];
assert_eq!(expected_value, value.as_str().unwrap());
}
}