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 40fc8df617d99f7ffb76e61333856dcbafe1f882
parent f23822c4122c6f719d4cf665e4d2d0e9af9ca3ee
Author: mauricefisher64 <92736594+mauricefisher64@users.noreply.github.com>
Date:   Fri, 15 Jul 2022 14:05:38 -0400

Support for asynchronous signing of claims (#57)

* Fix bug in verify_from_buffer

* make_test fixes
adjust signature box reserved size to account for large timestamps

* change tsa to tsa_url

* Fix make images in makefile

* remove debug printlns

* Async claim signing support

* fix formatting

* allow async validation with OpenSSL validators

* simplify login in cose_pad

* clippy fixes

* cargo fmt fix

* Remove old comment to resolve PR request

* Restore `bmff` feature

Co-authored-by: Gavin Peacock <gpeacock@adobe.com>
Co-authored-by: Eric Scouten <scouten@adobe.com>
Diffstat:
Msdk/Cargo.toml | 5++++-
Msdk/src/cose_sign.rs | 223+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Msdk/src/cose_validator.rs | 23++++++++++++++++++-----
Msdk/src/openssl/mod.rs | 41++++++++++++++++-------------------------
Asdk/src/openssl/temp_signer_async.rs | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msdk/src/signer.rs | 21++++++++++++++++++++-
Msdk/src/store.rs | 166+++++++++++++++++++++++++++++++++++++++++++------------------------------------
7 files changed, 402 insertions(+), 179 deletions(-)

diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml @@ -17,7 +17,7 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [features] -async_signer = ["async-trait"] +async_signer = ["async-trait", "file_io"] bmff = [] # Work in progress support for BMFF-based containers file_io = ["openssl"] serialize_thumbnails = [] @@ -96,3 +96,6 @@ anyhow = "1.0.40" [target.'cfg(target_arch = "wasm32")'.dev-dependencies] wasm-bindgen-test = "0.3.31" + +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +actix = "0.11.0" diff --git a/sdk/src/cose_sign.rs b/sdk/src/cose_sign.rs @@ -16,7 +16,9 @@ #![deny(missing_docs)] use ciborium::value::Value; -use coset::{iana, CoseSign1, CoseSign1Builder, HeaderBuilder, Label, TaggedCborSerializable}; +use coset::{ + iana, CoseSign1, CoseSign1Builder, Header, HeaderBuilder, Label, TaggedCborSerializable, +}; use crate::{ claim::Claim, @@ -25,6 +27,77 @@ use crate::{ time_stamp::{cose_timestamp_countersign, make_cose_timestamp}, Error, Result, Signer, }; +fn build_unprotected_header( + data: &[u8], + alg: &str, + certs: Vec<Vec<u8>>, + ta_url: Option<String>, + ocsp_val: Option<Vec<u8>>, +) -> Result<(Header, Header)> { + let alg_id = match alg { + "ps256" => HeaderBuilder::new() + .algorithm(iana::Algorithm::PS256) + .build(), + "ps384" => HeaderBuilder::new() + .algorithm(iana::Algorithm::PS384) + .build(), + "ps512" => HeaderBuilder::new() + .algorithm(iana::Algorithm::PS512) + .build(), + "es256" => HeaderBuilder::new() + .algorithm(iana::Algorithm::ES256) + .build(), + "es384" => HeaderBuilder::new() + .algorithm(iana::Algorithm::ES384) + .build(), + "es512" => HeaderBuilder::new() + .algorithm(iana::Algorithm::ES512) + .build(), + "ed25519" => HeaderBuilder::new() + .algorithm(iana::Algorithm::EdDSA) + .build(), + _ => return Err(Error::UnsupportedType), + }; + + let sc_der_array_or_bytes = match certs.len() { + 1 => Value::Bytes(certs[0].clone()), // single cert + _ => { + let mut sc_der_array: Vec<Value> = Vec::new(); + for cert in certs { + sc_der_array.push(Value::Bytes(cert)); + } + Value::Array(sc_der_array) // provide vec of certs when required + } + }; + + let mut unprotected = if let Some(url) = ta_url { + let cts = cose_timestamp_countersign(data, alg, &url)?; + let sigtst_vec = serde_cbor::to_vec(&make_cose_timestamp(&cts))?; + let sigtst_cbor = serde_cbor::from_slice(&sigtst_vec)?; + + HeaderBuilder::new() + .text_value("x5chain".to_string(), sc_der_array_or_bytes) + .text_value("sigTst".to_string(), sigtst_cbor) + } else { + HeaderBuilder::new().text_value("x5chain".to_string(), sc_der_array_or_bytes) + }; + + // set the ocsp responder response if available + if let Some(ocsp) = ocsp_val { + let mut ocsp_vec: Vec<Value> = Vec::new(); + let mut r_vals: Vec<(Value, Value)> = vec![]; + + ocsp_vec.push(Value::Bytes(ocsp)); + r_vals.push((Value::Text("ocspVals".to_string()), Value::Array(ocsp_vec))); + + unprotected = unprotected.text_value("rVals".to_string(), Value::Map(r_vals)); + } + + // build complete header + let unprotected_header = unprotected.build(); + + Ok((alg_id, unprotected_header)) +} /// Generate a COSE signature for a block of bytes which must be a valid C2PA /// claim structure. @@ -87,85 +160,91 @@ pub(crate) fn cose_sign(signer: &dyn Signer, data: &[u8], box_size: usize) -> Re let alg = signer.alg().ok_or(Error::UnsupportedType)?; - let alg_id = match alg.as_ref() { - "ps256" => HeaderBuilder::new() - .algorithm(iana::Algorithm::PS256) - .build(), - "ps384" => HeaderBuilder::new() - .algorithm(iana::Algorithm::PS384) - .build(), - "ps512" => HeaderBuilder::new() - .algorithm(iana::Algorithm::PS512) - .build(), - "es256" => HeaderBuilder::new() - .algorithm(iana::Algorithm::ES256) - .build(), - "es384" => HeaderBuilder::new() - .algorithm(iana::Algorithm::ES384) - .build(), - "es512" => HeaderBuilder::new() - .algorithm(iana::Algorithm::ES512) - .build(), - "ed25519" => HeaderBuilder::new() - .algorithm(iana::Algorithm::EdDSA) - .build(), - _ => return Err(Error::UnsupportedType), - }; + // build complete header + let (alg_id, unprotected_header) = build_unprotected_header( + data, + &alg, + signer.certs()?, + signer.time_authority_url(), + signer.ocsp_val(), + )?; - // Get the public CAs for the Signer. - let certs = signer.certs()?; - let sc_der_array_or_bytes = match certs.len() { - 1 => Value::Bytes(certs[0].clone()), // single cert - _ => { - let mut sc_der_array: Vec<Value> = Vec::new(); - for cert in certs { - sc_der_array.push(Value::Bytes(cert)); - } - Value::Array(sc_der_array) // provide vec of certs when required - } - }; + let aad = b""; // no additional data required here - let mut unprotected = match signer.time_authority_url() { - Some(url) => { - let cts = cose_timestamp_countersign(data, &alg, &url)?; - let sigtst_vec = serde_cbor::to_vec(&make_cose_timestamp(&cts))?; - let sigtst_cbor = serde_cbor::from_slice(&sigtst_vec)?; + let sign1_builder = CoseSign1Builder::new() + .protected(alg_id) + .unprotected(unprotected_header) + .payload(data.to_vec()) + .try_create_signature(aad, |bytes| signer.sign(bytes))?; - HeaderBuilder::new() - .text_value("x5chain".to_string(), sc_der_array_or_bytes) - .text_value("sigTst".to_string(), sigtst_cbor) - } - None => { - let sign_time = chrono::Utc::now().to_rfc3339(); // todo: remove when switch to cose_timestamp - HeaderBuilder::new() - .text_value("x5chain".to_string(), sc_der_array_or_bytes) - .text_value("temp_signing_time".to_string(), Value::Text(sign_time)) - } - }; + let mut sign1 = sign1_builder.build(); + sign1.payload = None; // clear the payload since it is known - // set the ocsp responder response if available - if let Some(ocsp) = signer.ocsp_val() { - let mut ocsp_vec: Vec<Value> = Vec::new(); - let mut r_vals: Vec<(Value, Value)> = vec![]; + let c2pa_sig_data = pad_cose_sig(&mut sign1, box_size)?; - ocsp_vec.push(Value::Bytes(ocsp)); - r_vals.push((Value::Text("ocspVals".to_string()), Value::Array(ocsp_vec))); + // println!("sig: {}", Hexlify(&c2pa_sig_data)); - unprotected = unprotected.text_value("rVals".to_string(), Value::Map(r_vals)); - } + Ok(c2pa_sig_data) +} + +/// Returns signed Cose_Sign1 bytes for "data". The Cose_Sign1 will be signed with the algorithm from `Signer`. +#[cfg(feature = "async_signer")] +pub async fn cose_sign_async( + signer: &dyn crate::AsyncSigner, + data: &[u8], + box_size: usize, +) -> Result<Vec<u8>> { + // 13.2.1. X.509 Certificates + // + // X.509 Certificates are stored in a header named x5chain draft-ietf-cose-x509. + // The value is a CBOR array of byte strings, each of which contains the certificate + // encoded as ASN.1 distinguished encoding rules (DER). This array must contain at + // least one element. The first element of the array must be the certificate of + // the signer, and the subjectPublicKeyInfo element of the certificate will be the + // public key used to validate the signature. The Validity member of the TBSCertificate + // sequence provides the time validity period of the certificate. + + /* + This header parameter allows for a single X.509 certificate or a + chain of X.509 certificates to be carried in the message. + + * If a single certificate is conveyed, it is placed in a CBOR + byte string. + + * If multiple certificates are conveyed, a CBOR array of byte + strings is used, with each certificate being in its own byte + string. + */ + + let alg = signer.alg().ok_or(Error::UnsupportedType)?; // build complete header - let unprotected_header = unprotected.build(); + let (alg_id, unprotected_header) = build_unprotected_header( + data, + &alg, + signer.certs()?, + signer.time_authority_url(), + signer.ocsp_val(), + )?; let aad = b""; // no additional data required here let sign1_builder = CoseSign1Builder::new() .protected(alg_id) .unprotected(unprotected_header) - .payload(data.to_vec()) - .try_create_signature(aad, |bytes| signer.sign(bytes))?; + .payload(data.to_vec()); let mut sign1 = sign1_builder.build(); + + let tbs = coset::sig_structure_data( + coset::SignatureContext::CoseSign1, + sign1.protected.clone(), + None, + aad, + sign1.payload.as_ref().unwrap_or(&vec![]), + ); + sign1.signature = signer.sign(tbs).await?; + sign1.payload = None; // clear the payload since it is known let c2pa_sig_data = pad_cose_sig(&mut sign1, box_size)?; @@ -190,13 +269,13 @@ fn pad_cose_sig(sign1: &mut CoseSign1, end_size: usize) -> Result<Vec<u8>> { .map_err(|_e| Error::CoseSignature)?; let cur_size = cur_vec.len(); - // check for box too small - match cur_size > end_size { - true => { - return Err(Error::CoseSigboxTooSmall); - } - false if cur_size == end_size => return Ok(cur_vec), - false => (), + if cur_size == end_size { + return Ok(cur_vec); + } + + // check for box too small and matched size + if cur_size + PAD_OFFSET > end_size { + return Err(Error::CoseSigboxTooSmall); } let mut padding_found = false; @@ -235,7 +314,7 @@ fn pad_cose_sig(sign1: &mut CoseSign1, end_size: usize) -> Result<Vec<u8>> { match new_cbor.len() < end_size { true => target_guess += 1, false if new_cbor.len() == end_size => return Ok(new_cbor), - false => break, // we couuld not match end_size in a single pad so break and add a second + false => break, // we could not match end_size in a single pad so break and add a second } } diff --git a/sdk/src/cose_validator.rs b/sdk/src/cose_validator.rs @@ -954,12 +954,25 @@ async fn validate_with_cert_async( #[cfg(not(target_arch = "wasm32"))] async fn validate_with_cert_async( - _validator_str: &str, - _sig: &[u8], - _data: &[u8], - _der_bytes: &[u8], + validator_str: &str, + sig: &[u8], + data: &[u8], + der_bytes: &[u8], ) -> Result<String> { - Err(Error::CoseSignatureAlgorithmNotSupported) + // get the cert in der format + let (_rem, signcert) = + X509Certificate::from_der(der_bytes).map_err(|_err| Error::CoseInvalidCert)?; + let pk = signcert.public_key(); + let pk_der = pk.raw; + + let validator = + get_validator(validator_str).ok_or(Error::CoseSignatureAlgorithmNotSupported)?; + + if validator.validate(sig, data, pk_der)? { + Ok(extract_subject_from_cert(&signcert)?) + } else { + Err(Error::CoseSignature) + } } #[allow(unused_imports)] #[cfg(feature = "file_io")] diff --git a/sdk/src/openssl/mod.rs b/sdk/src/openssl/mod.rs @@ -32,6 +32,14 @@ pub(crate) use ed_validator::EdValidator; #[cfg(test)] pub(crate) mod temp_signer; +#[cfg(test)] +pub(crate) mod temp_signer_async; + +#[cfg(test)] +#[allow(unused_imports)] +#[cfg(feature = "async_signer")] +pub(crate) use temp_signer_async::AsyncSignerAdapter; + use openssl::x509::X509; pub(crate) fn check_chain_order(certs: &[X509]) -> bool { @@ -56,31 +64,14 @@ pub(crate) fn check_chain_order(certs: &[X509]) -> bool { } pub(crate) fn check_chain_order_der(cert_ders: &[Vec<u8>]) -> bool { - if cert_ders.len() > 1 { - let mut certs: Vec<X509> = Vec::new(); - for cert_der in cert_ders { - if let Ok(cert) = X509::from_der(cert_der) { - certs.push(cert); - } else { - return false; - } - } - - for (i, c) in certs.iter().enumerate() { - if let Some(next_c) = certs.get(i + 1) { - if let Ok(pkey) = next_c.public_key() { - if let Ok(verified) = c.verify(&pkey) { - if !verified { - return false; - } - } else { - return false; - } - } else { - return false; - } - } + let mut certs: Vec<X509> = Vec::new(); + for cert_der in cert_ders { + if let Ok(cert) = X509::from_der(cert_der) { + certs.push(cert); + } else { + return false; } } - true + + check_chain_order(&certs) } diff --git a/sdk/src/openssl/temp_signer_async.rs b/sdk/src/openssl/temp_signer_async.rs @@ -0,0 +1,102 @@ +// Copyright 2022 Adobe. All rights reserved. +// This file is licensed to you under the Apache License, +// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) +// or the MIT license (http://opensource.org/licenses/MIT), +// at your option. + +// Unless required by applicable law or agreed to in writing, +// this software is distributed on an "AS IS" BASIS, WITHOUT +// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or +// implied. See the LICENSE-MIT and LICENSE-APACHE files for the +// specific language governing permissions and limitations under +// each license. + +#![deny(missing_docs)] + +//! Temporary async signing instances for testing purposes. +//! +//! This is only a demonstration async Signer that is used to test +//! the asynchronous signing of claims. +//! This module should be used only for testing purposes. + +#[cfg(feature = "async_signer")] +fn get_local_signer(alg: &str) -> Box<dyn crate::Signer> { + let cert_dir = crate::utils::test::fixture_path("certs"); + + match alg { + "ps256" | "ps384" | "ps512" => { + let (s, _k) = super::temp_signer::get_rsa_signer(&cert_dir, alg, None); + Box::new(s) + } + "es256" | "es384" | "es512" => { + let (s, _k) = super::temp_signer::get_ec_signer(&cert_dir, alg, None); + Box::new(s) + } + "ed25519" => { + let (s, _k) = super::temp_signer::get_ed_signer(&cert_dir, alg, None); + Box::new(s) + } + _ => { + let (s, _k) = super::temp_signer::get_rsa_signer(&cert_dir, "ps256", None); + Box::new(s) + } + } +} + +#[cfg(feature = "async_signer")] +pub struct AsyncSignerAdapter { + alg: String, + certs: Vec<Vec<u8>>, + reserve_size: usize, + tsa_url: Option<String>, + ocsp_val: Option<Vec<u8>>, +} + +#[cfg(feature = "async_signer")] +impl AsyncSignerAdapter { + pub fn new(alg: String) -> Self { + let signer = get_local_signer(&alg); + + AsyncSignerAdapter { + alg, + certs: signer.certs().unwrap_or_default(), + reserve_size: signer.reserve_size(), + tsa_url: signer.time_authority_url(), + ocsp_val: signer.ocsp_val(), + } + } +} + +#[cfg(test)] +#[cfg(feature = "async_signer")] +#[async_trait::async_trait] +impl crate::AsyncSigner for AsyncSignerAdapter { + async fn sign(&self, data: Vec<u8>) -> crate::error::Result<Vec<u8>> { + let signer = get_local_signer(&self.alg); + signer.sign(&data) + } + + fn alg(&self) -> Option<String> { + Some(self.alg.clone()) + } + + fn certs(&self) -> crate::Result<Vec<Vec<u8>>> { + let mut output: Vec<Vec<u8>> = Vec::new(); + for v in &self.certs { + output.push(v.clone()); + } + Ok(output) + } + + fn reserve_size(&self) -> usize { + self.reserve_size + } + + fn time_authority_url(&self) -> Option<String> { + self.tsa_url.clone() + } + + fn ocsp_val(&self) -> Option<Vec<u8>> { + self.ocsp_val.clone() + } +} diff --git a/sdk/src/signer.rs b/sdk/src/signer.rs @@ -76,10 +76,29 @@ use async_trait::async_trait; #[async_trait] pub trait AsyncSigner: Sync { /// Returns a new byte array which is a signature over the original. - async fn sign(&self, data: &[u8]) -> Result<Vec<u8>>; + async fn sign(&self, data: Vec<u8>) -> Result<Vec<u8>>; + + /// Returns the algorithm of the Signer. + fn alg(&self) -> Option<String>; + + /// Returns the certificates as a Vec containing a Vec of DER bytes for each certificate. + fn certs(&self) -> Result<Vec<Vec<u8>>>; /// Returns the size in bytes of the largest possible expected signature. /// Signing will fail if the result of the `sign` function is larger /// than this value. fn reserve_size(&self) -> usize; + + /// URL for time authority to time stamp the signature + fn time_authority_url(&self) -> Option<String> { + None + } + + /// OCSP response for the signing cert if available + /// This is the only C2PA supported cert revocation method. + /// By pre-querying the value for a your signing cert the value can + /// be cached taking pressure off of the CA (recommended by C2PA spec) + fn ocsp_val(&self) -> Option<Vec<u8>> { + None + } } diff --git a/sdk/src/store.rs b/sdk/src/store.rs @@ -358,9 +358,38 @@ impl Store { &self, claim: &Claim, signer: &dyn AsyncSigner, + box_size: usize, ) -> Result<Vec<u8>> { + use crate::cose_sign::cose_sign_async; + use crate::cose_validator::verify_cose_async; + let claim_bytes = claim.data()?; - signer.sign(&claim_bytes).await + + match cose_sign_async(signer, &claim_bytes, box_size).await { + // Sanity check: Ensure that this signature is valid. + Ok(sig) => { + let mut cose_log = OneShotStatusTracker::new(); + match verify_cose_async( + sig.clone(), + claim_bytes, + b"".to_vec(), + false, + &mut cose_log, + ) + .await + { + Ok(_) => Ok(sig), + Err(err) => { + error!( + "Signature that was just generated does not validate: {:#?}", + err + ); + Err(err) + } + } + } + Err(e) => Err(e), + } } /// return the current provenance claim label if available @@ -1395,7 +1424,9 @@ impl Store { let jumbf_bytes = self.start_save(asset_path, output_path, signer.reserve_size())?; let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?; - let sig = self.sign_claim_async(pc, signer).await?; + let sig = self + .sign_claim_async(pc, signer, signer.reserve_size()) + .await?; let sig_placeholder = self.sign_claim_placeholder(pc, signer.reserve_size()); match self.finish_save(jumbf_bytes, output_path, sig, &sig_placeholder) { @@ -2142,79 +2173,64 @@ pub mod tests { assert!(find_bytes(&buf, &original_jumbf[0..1024]).is_none()); } - /* async signing not supported at the moment - NOTE: Add this to Cargo.toml if this test is restored. - - [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] - actix = "0.11.0" - - #[cfg(feature = "async_signer")] - #[actix::test] - async fn test_jumbf_generation_async() { - let signer = crate::AsyncPlaceholder {}; - - // test adding to actual image - let ap = fixture_path("earth_apollo17.jpg"); - let temp_dir = tempdir().expect("temp dir"); - let op = temp_dir_path(&temp_dir, "test-async.jpg"); - - // Create claims store. - let mut store = Store::new(); - - // Create a new claim. - let claim1 = create_test_claim().unwrap(); - - // Create a new claim. - let mut claim2 = Claim::new("Photoshop", Some("Adobe")); - create_editing_claim(&mut claim2).unwrap(); - - // Create a 3rd party claim - let mut claim_capture = Claim::new("capture", Some("claim_capture")); - create_capture_claim(&mut claim_capture).unwrap(); - - // Test generate JUMBF - // Get labels for label test - let claim1_label = claim1.label().to_string(); - let capture = claim_capture.label().to_string(); - let claim2_label = claim2.label().to_string(); - - /* - Move the claim to claims list. Note this is not real, the claims would have to be signed in between commits - */ - store.commit_claim(claim1).unwrap(); - store.save_to_asset_async(&ap, &signer, &op).await.unwrap(); - store.commit_claim(claim_capture).unwrap(); - store.save_to_asset_async(&ap, &signer, &op).await.unwrap(); - store.commit_claim(claim2).unwrap(); - store.save_to_asset_async(&ap, &signer, &op).await.unwrap(); - - // test finding claims by label - 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()); - - // Do we generate JUMBF - let jumbf_bytes = store.to_jumbf_async(&signer).unwrap(); - assert!(!jumbf_bytes.is_empty()); - - // write to new file - println!("Provenance: {}\n", store.provenance_path().unwrap()); - - // read from new file - let mut report: Vec<ValidationItem> = Vec::new(); - let new_store = Store::load_from_asset(&op, true, Some(&mut report)).unwrap(); - // Async placeholder signature won't verify. We need the load to complete, - // but we ignore the validation log which we know will have errors. - - let claim = new_store.provenance_claim().unwrap(); - let sig = claim.signature_val(); - - assert_eq!(&sig[0..19], b"invalid signature\0\0"); - } - */ + #[cfg(feature = "async_signer")] + #[actix::test] + async fn test_jumbf_generation_async() { + let signer = + crate::openssl::temp_signer_async::AsyncSignerAdapter::new("ps256".to_string()); + + // test adding to actual image + let ap = fixture_path("earth_apollo17.jpg"); + let temp_dir = tempdir().expect("temp dir"); + let op = temp_dir_path(&temp_dir, "test-async.jpg"); + + // Create claims store. + let mut store = Store::new(); + + // Create a new claim. + let claim1 = create_test_claim().unwrap(); + + // Create a new claim. + let mut claim2 = Claim::new("Photoshop", Some("Adobe")); + create_editing_claim(&mut claim2).unwrap(); + + // Create a 3rd party claim + let mut claim_capture = Claim::new("capture", Some("claim_capture")); + create_capture_claim(&mut claim_capture).unwrap(); + + // Test generate JUMBF + // Get labels for label test + 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).unwrap(); + store.save_to_asset_async(&ap, &signer, &op).await.unwrap(); + store.commit_claim(claim_capture).unwrap(); + store.save_to_asset_async(&ap, &signer, &op).await.unwrap(); + store.commit_claim(claim2).unwrap(); + store.save_to_asset_async(&ap, &signer, &op).await.unwrap(); + + // test finding claims by label + 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()); + + // Do we generate JUMBF + let jumbf_bytes = store.to_jumbf_async(&signer).unwrap(); + assert!(!jumbf_bytes.is_empty()); + + // write to new file + println!("Provenance: {}\n", store.provenance_path().unwrap()); + + // make sure we can read from new file + let mut report = DetailedStatusTracker::new(); + let _new_store = Store::load_from_asset(&op, true, &mut report).unwrap(); + } + #[test] #[cfg(feature = "file_io")] fn test_png_jumbf_generation() {