c2pa-rs

A fork of https://github.com/contentauth/c2pa-rs/
git clone git://archive.git.mtrnord.blog/mtrnords-photography-manager/c2pa-rs.git
Log | Files | Refs | README

commit d51f33cd96f2c7ab4e5b9d9c1804cb2213cfaee1
parent acb7a9d6c7554fc8f4cb0c0884c471473b095a6e
Author: mauricefisher64 <92736594+mauricefisher64@users.noreply.github.com>
Date:   Mon, 12 Jun 2023 10:42:03 -0400

Update Timestamp message imprint to include entire protected header (#264)

* Initial box hash support

* Full box hash implementation for JPG

* Box hash fixes

* More unit tests and safety checks

* Formatting fix

* Another format fix

* More error checking

* Add API to get embeddable manifest

* PR feedback

* Add missing test file

* Change timestamp message imprint to include entire protected header

* Format fixes

* Fix for mixing cbor and json in a single object

* cleanup

* Simplify fix to use existing code
Diffstat:
Msdk/src/assertion.rs | 1+
Msdk/src/assertions/actions.rs | 20+++++++++++++++-----
Msdk/src/assertions/box_hash.rs | 36+++++++++++++++++++++++++++++++-----
Msdk/src/cose_sign.rs | 14+++++++++++---
Msdk/src/cose_validator.rs | 4++--
Msdk/src/manifest.rs | 21++++++---------------
Msdk/src/time_stamp.rs | 43+++++++------------------------------------
7 files changed, 73 insertions(+), 66 deletions(-)

diff --git a/sdk/src/assertion.rs b/sdk/src/assertion.rs @@ -323,6 +323,7 @@ impl Assertion { /// Return assertion as serde_json Object /// this may have loss of cbor structure if unsupported in conversion to json + /// It should always do the correct thing when using the correct tagged CBOR types pub(crate) fn as_json_object(&self) -> AssertionDecodeResult<Value> { match self.decode_data() { AssertionData::Json(x) => serde_json::from_str(x) diff --git a/sdk/src/assertions/actions.rs b/sdk/src/assertions/actions.rs @@ -14,7 +14,7 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; -use serde_json::Value; +use serde_cbor::Value; use crate::{ assertion::{Assertion, AssertionBase, AssertionCbor}, @@ -22,7 +22,7 @@ use crate::{ error::Result, resource_store::UriOrResource, utils::cbor_types::DateT, - ClaimGeneratorInfo, Error, + ClaimGeneratorInfo, }; const ASSERTION_CREATION_VERSION: usize = 2; @@ -263,7 +263,9 @@ impl Action { key: S, value: T, ) -> Result<Self> { - let value = serde_json::to_value(value).map_err(|_| Error::AssertionEncoding)?; + let value_bytes = serde_cbor::ser::to_vec(&value)?; + let value = serde_cbor::from_slice(&value_bytes)?; + self.parameters = Some(match self.parameters { Some(mut parameters) => { parameters.insert(key.into(), value); @@ -422,9 +424,17 @@ impl Actions { self } - /// Creates an [`Actions`] assertion from a compatible JSON value. + /// Creates a CBOR [`Actions`] assertion from a compatible JSON value. pub fn from_json_value(json: &serde_json::Value) -> Result<Self> { - let actions: Actions = serde_json::from_value(json.clone())?; + let buf: Vec<u8> = Vec::new(); + let json_str = json.to_string(); + let mut from = serde_json::Deserializer::from_str(&json_str); + let mut to = serde_cbor::Serializer::new(buf); + + serde_transcode::transcode(&mut from, &mut to)?; + let buf2 = to.into_inner(); + + let actions: Actions = serde_cbor::from_slice(&buf2)?; Ok(actions) } } diff --git a/sdk/src/assertions/box_hash.rs b/sdk/src/assertions/box_hash.rs @@ -17,12 +17,11 @@ use serde::{Deserialize, Serialize}; use serde_bytes::ByteBuf; use crate::{ - assertion::{Assertion, AssertionBase, AssertionCbor}, + assertion::{Assertion, AssertionBase, AssertionCbor, AssertionJson}, assertions::labels, asset_io::{AssetBoxHash, CAIRead}, error::{Error, Result}, utils::hash_utils::{hash_stream_by_alg, verify_stream_by_alg, HashRange}, - AssertionJson, }; const ASSERTION_CREATION_VERSION: usize = 1; @@ -291,8 +290,6 @@ impl AssertionBase for BoxHash { const LABEL: &'static str = Self::LABEL; const VERSION: Option<usize> = Some(ASSERTION_CREATION_VERSION); - // todo: this mechanism needs to change since a struct could support different versions - fn to_assertion(&self) -> crate::error::Result<Assertion> { Self::to_cbor_assertion(self) } @@ -416,7 +413,7 @@ mod tests { bh.generate_box_hash_from_stream(&mut input, "sha256", bhp, true) .unwrap(); - // save and reload JSOH + // save and reload JSON let bh_json_assertion = bh.to_json_assertion().unwrap(); println!("Box hash json: {:?}", bh_json_assertion.decode_data()); @@ -427,4 +424,33 @@ mod tests { .verify_stream_hash(&mut input, Some("sha256"), bhp) .unwrap(); } + + #[test] + fn test_cbor_round_trop() { + let ap = fixture_path("CA.jpg"); + + let bhp = get_assetio_handler_from_path(&ap) + .unwrap() + .asset_box_hash_ref() + .unwrap(); + + let mut input = File::open(&ap).unwrap(); + + let mut bh = BoxHash { boxes: Vec::new() }; + + // generate box hashes + bh.generate_box_hash_from_stream(&mut input, "sha256", bhp, true) + .unwrap(); + + // save and reload CBOR + let bh_cbor_assertion = bh.to_cbor_assertion().unwrap(); + println!("Box hash cbor: {:?}", bh_cbor_assertion.decode_data()); + + let reloaded_bh = BoxHash::from_cbor_assertion(&bh_cbor_assertion).unwrap(); + + // see if they match reading + reloaded_bh + .verify_stream_hash(&mut input, Some("sha256"), bhp) + .unwrap(); + } } diff --git a/sdk/src/cose_sign.rs b/sdk/src/cose_sign.rs @@ -18,7 +18,8 @@ use ciborium::value::Value; use coset::{ iana::{self, EnumI64}, - CoseSign1, CoseSign1Builder, Header, HeaderBuilder, Label, TaggedCborSerializable, + CoseSign1, CoseSign1Builder, Header, HeaderBuilder, Label, ProtectedHeader, + TaggedCborSerializable, }; use crate::{ @@ -222,9 +223,17 @@ fn build_headers( iana::HeaderParameter::X5Chain.to_i64(), sc_der_array_or_bytes, ); + let protected_header = protected_h.build(); let mut unprotected_h = if let Some(url) = ta_url { - let cts = cose_timestamp_countersign(data, alg, &url)?; + let cts = cose_timestamp_countersign( + data, + &ProtectedHeader { + original_data: None, + header: protected_header.clone(), + }, + &url, + )?; let sigtst_vec = serde_cbor::to_vec(&make_cose_timestamp(&cts))?; let sigtst_cbor = serde_cbor::from_slice(&sigtst_vec)?; @@ -245,7 +254,6 @@ fn build_headers( } // build complete header - let protected_header = protected_h.build(); let unprotected_header = unprotected_h.build(); Ok((protected_header, unprotected_header)) diff --git a/sdk/src/cose_validator.rs b/sdk/src/cose_validator.rs @@ -696,9 +696,9 @@ fn get_timestamp_info(sign1: &coset::CoseSign1, data: &[u8]) -> Result<TstInfo> } }) { - let alg = get_signing_alg(sign1)?; let time_cbor = serde_cbor::to_vec(t)?; - let tst_infos = crate::time_stamp::cose_sigtst_to_tstinfos(&time_cbor, data, alg)?; + let tst_infos = + crate::time_stamp::cose_sigtst_to_tstinfos(&time_cbor, data, &sign1.protected)?; // there should only be one but consider handling more in the future since it is technically ok if !tst_infos.is_empty() { diff --git a/sdk/src/manifest.rs b/sdk/src/manifest.rs @@ -25,7 +25,7 @@ use uuid::Uuid; #[cfg(feature = "file_io")] use crate::AsyncSigner; use crate::{ - assertion::{AssertionBase, AssertionData, AssertionDecodeError}, + assertion::{AssertionBase, AssertionData}, assertions::{labels, Actions, CreativeWork, Exif, SoftwareAgent, Thumbnail, User, UserCbor}, asset_io::CAIRead, claim::{Claim, RemoteManifest}, @@ -638,7 +638,7 @@ impl Manifest { )?; manifest.add_ingredient(ingredient); } - labels::DATA_HASH | labels::BMFF_HASH => { + labels::DATA_HASH | labels::BMFF_HASH | labels::BOX_HASH => { // do not include data hash when reading manifests } label if label.starts_with(labels::CLAIM_THUMBNAIL) => { @@ -648,24 +648,15 @@ impl Manifest { _ => { // inject assertions for all other assertions match assertion.decode_data() { - AssertionData::Json(x) => { - let value = serde_json::from_str(x).map_err(|e| { - AssertionDecodeError::from_assertion_and_json_err(assertion, e) - })?; + AssertionData::Json(_) | AssertionData::Cbor(_) => { + let value = assertion.as_json_object()?; let ma = ManifestAssertion::new(base_label, value) .set_instance(claim_assertion.instance()) .set_kind(ManifestAssertionKind::Json); + manifest.assertions.push(ma); } - AssertionData::Cbor(x) => { - let value: Value = - serde_cbor::from_slice(x.as_slice()).map_err(|e| { - AssertionDecodeError::from_assertion_and_cbor_err(assertion, e) - })?; - let ma = ManifestAssertion::new(base_label, value) - .set_instance(claim_assertion.instance()); - manifest.assertions.push(ma); - } + // todo: support binary forms AssertionData::Binary(_x) => {} AssertionData::Uuid(_, _) => {} diff --git a/sdk/src/time_stamp.rs b/sdk/src/time_stamp.rs @@ -14,7 +14,7 @@ use std::convert::TryFrom; use bcder::decode::Constructed; -use coset::{iana, sig_structure_data, HeaderBuilder, ProtectedHeader}; +use coset::{sig_structure_data, ProtectedHeader}; use serde::{Deserialize, Serialize}; use x509_certificate::DigestAlgorithm::{self}; @@ -27,45 +27,16 @@ use crate::{ rfc5652::{CertificateChoices::Certificate, SignedData, OID_ID_SIGNED_DATA}, }, hash_utils::vec_compare, - SigningAlg, }; #[allow(dead_code)] -pub(crate) fn cose_countersign_data(data: &[u8], alg: SigningAlg) -> Vec<u8> { - let alg_id = match alg { - SigningAlg::Ps256 => HeaderBuilder::new() - .algorithm(iana::Algorithm::PS256) - .build(), - SigningAlg::Ps384 => HeaderBuilder::new() - .algorithm(iana::Algorithm::PS384) - .build(), - SigningAlg::Ps512 => HeaderBuilder::new() - .algorithm(iana::Algorithm::PS512) - .build(), - SigningAlg::Es256 => HeaderBuilder::new() - .algorithm(iana::Algorithm::ES256) - .build(), - SigningAlg::Es384 => HeaderBuilder::new() - .algorithm(iana::Algorithm::ES384) - .build(), - SigningAlg::Es512 => HeaderBuilder::new() - .algorithm(iana::Algorithm::ES512) - .build(), - SigningAlg::Ed25519 => HeaderBuilder::new() - .algorithm(iana::Algorithm::EdDSA) - .build(), - }; - - let p_header = ProtectedHeader { - original_data: None, - header: alg_id, - }; +pub(crate) fn cose_countersign_data(data: &[u8], p_header: &ProtectedHeader) -> Vec<u8> { let aad: Vec<u8> = Vec::new(); // create sig_structure_data to be signed sig_structure_data( coset::SignatureContext::CounterSignature, - p_header, + p_header.clone(), None, &aad, data, @@ -75,7 +46,7 @@ pub(crate) fn cose_countersign_data(data: &[u8], alg: SigningAlg) -> Vec<u8> { #[allow(dead_code)] pub(crate) fn cose_timestamp_countersign( data: &[u8], - alg: SigningAlg, + p_header: &ProtectedHeader, tsa_url: &str, ) -> Result<Vec<u8>> { // create countersignature with TimeStampReq parameters @@ -85,7 +56,7 @@ pub(crate) fn cose_timestamp_countersign( // algorithm sha256 // create sig data structure to be time stamped - let sd = cose_countersign_data(data, alg); + let sd = cose_countersign_data(data, p_header); timestamp_data(tsa_url, &sd) } @@ -94,7 +65,7 @@ pub(crate) fn cose_timestamp_countersign( pub(crate) fn cose_sigtst_to_tstinfos( sigtst_cbor: &[u8], data: &[u8], - alg: SigningAlg, + p_header: &ProtectedHeader, ) -> Result<Vec<TstInfo>> { let tst_container: TstContainer = serde_cbor::from_slice(sigtst_cbor).map_err(|_err| Error::CoseTimeStampGeneration)?; @@ -102,7 +73,7 @@ pub(crate) fn cose_sigtst_to_tstinfos( let mut tstinfos: Vec<TstInfo> = Vec::new(); for token in &tst_container.tst_tokens { - let tbs = cose_countersign_data(data, alg); + let tbs = cose_countersign_data(data, p_header); let tst_info = verify_timestamp(&token.val, &tbs)?; tstinfos.push(tst_info); }