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 f7c044f3566bfbf90c9bc091501cef696488910c
parent d2946dc71fcb9006222b056c1261db85e15939cc
Author: Eric Scouten <scouten@adobe.com>
Date:   Thu, 28 Sep 2023 11:09:56 -0700

(MINOR) Signer can call timestamp authority directly (#311)


Diffstat:
Mexport_schema/Cargo.toml | 2+-
Mmake_test_images/Cargo.toml | 2+-
Msdk/Cargo.toml | 3++-
Msdk/src/cose_sign.rs | 123+++++++++++++++++++++++++------------------------------------------------------
Msdk/src/signer.rs | 26++++++++++++++++++++++++++
Msdk/src/time_stamp.rs | 47++++++++++++++++++++++++++++++-----------------
6 files changed, 99 insertions(+), 104 deletions(-)

diff --git a/export_schema/Cargo.toml b/export_schema/Cargo.toml @@ -4,7 +4,7 @@ version = "0.26.0" authors = ["Dave Kozma <dkozma@adobe.com>"] license = "MIT OR Apache-2.0" edition = "2018" -rust-version = "1.65.0" +rust-version = "1.70.0" [dependencies] anyhow = "1.0.40" diff --git a/make_test_images/Cargo.toml b/make_test_images/Cargo.toml @@ -4,7 +4,7 @@ version = "0.26.0" authors = ["Gavin Peacock <gpeacock@adobe.com>"] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.65.0" +rust-version = "1.70.0" [dependencies] anyhow = "1.0.40" diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml @@ -17,7 +17,7 @@ readme = "../README.md" keywords = ["xmp", "metadata"] categories = ["api-bindings"] edition = "2021" -rust-version = "1.65.0" +rust-version = "1.70.0" exclude = ["tests/fixtures"] [package.metadata.docs.rs] @@ -57,6 +57,7 @@ crate-type = ["lib"] [dependencies] asn1-rs = "0.5.2" +async-generic = "0.1.2" async-trait = { version = "0.1.48" } atree = "0.5.2" base64 = "0.21.2" diff --git a/sdk/src/cose_sign.rs b/sdk/src/cose_sign.rs @@ -15,6 +15,7 @@ #![deny(missing_docs)] +use async_generic::async_generic; use ciborium::value::Value; use coset::{ iana::{self}, @@ -26,8 +27,10 @@ use crate::{ claim::Claim, cose_validator::verify_cose, status_tracker::OneShotStatusTracker, - time_stamp::{cose_timestamp_countersign, make_cose_timestamp}, - Error, Result, Signer, SigningAlg, + time_stamp::{ + cose_timestamp_countersign, cose_timestamp_countersign_async, make_cose_timestamp, + }, + AsyncSigner, Error, Result, Signer, SigningAlg, }; /// Generate a COSE signature for a block of bytes which must be a valid C2PA @@ -72,64 +75,12 @@ pub fn sign_claim(claim_bytes: &[u8], signer: &dyn Signer, box_size: usize) -> R /// Returns signed Cose_Sign1 bytes for `data`. /// The Cose_Sign1 will be signed with the algorithm from [`Signer`]. -pub(crate) fn cose_sign(signer: &dyn Signer, 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(); - - // build complete header - let (protected_header, unprotected_header) = build_headers( - 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(protected_header) - .unprotected(unprotected_header) - .payload(data.to_vec()) - .try_create_signature(aad, |bytes| signer.sign(bytes))?; - - let mut sign1 = sign1_builder.build(); - sign1.payload = None; // clear the payload since it is known - - let c2pa_sig_data = pad_cose_sig(&mut sign1, box_size)?; - - // println!("sig: {}", Hexlify(&c2pa_sig_data)); - - Ok(c2pa_sig_data) -} - -/// Returns signed Cose_Sign1 bytes for "data". The Cose_Sign1 will be signed with the algorithm from `Signer`. -pub async fn cose_sign_async( - signer: &dyn crate::AsyncSigner, +#[async_generic(async_signature( + signer: &dyn AsyncSigner, data: &[u8], - box_size: usize, -) -> Result<Vec<u8>> { + box_size: usize +))] +pub(crate) fn cose_sign(signer: &dyn Signer, 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. @@ -155,15 +106,13 @@ pub async fn cose_sign_async( let alg = signer.alg(); // build complete header - let (protected_header, unprotected_header) = build_headers( - data, - alg, - signer.certs()?, - signer.time_authority_url(), - signer.ocsp_val(), - )?; + let (protected_header, unprotected_header) = if _sync { + build_headers(signer, data, alg)? + } else { + build_headers_async(signer, data, alg).await? + }; - let aad = b""; // no additional data required here + let aad: &[u8; 0] = b""; // no additional data required here let sign1_builder = CoseSign1Builder::new() .protected(protected_header) @@ -179,7 +128,12 @@ pub async fn cose_sign_async( aad, sign1.payload.as_ref().unwrap_or(&vec![]), ); - sign1.signature = signer.sign(tbs).await?; + + if _sync { + sign1.signature = signer.sign(&tbs)?; + } else { + sign1.signature = signer.sign(tbs).await?; + } sign1.payload = None; // clear the payload since it is known @@ -190,13 +144,8 @@ pub async fn cose_sign_async( Ok(c2pa_sig_data) } -fn build_headers( - data: &[u8], - alg: SigningAlg, - certs: Vec<Vec<u8>>, - ta_url: Option<String>, - ocsp_val: Option<Vec<u8>>, -) -> Result<(Header, Header)> { +#[async_generic(async_signature(signer: &dyn AsyncSigner, data: &[u8], alg: SigningAlg))] +fn build_headers(signer: &dyn Signer, data: &[u8], alg: SigningAlg) -> Result<(Header, Header)> { let protected_h = match alg { SigningAlg::Ps256 => HeaderBuilder::new().algorithm(iana::Algorithm::PS256), SigningAlg::Ps384 => HeaderBuilder::new().algorithm(iana::Algorithm::PS384), @@ -207,6 +156,9 @@ fn build_headers( SigningAlg::Ed25519 => HeaderBuilder::new().algorithm(iana::Algorithm::EdDSA), }; + let certs = signer.certs()?; + let ocsp_val = signer.ocsp_val(); + let sc_der_array_or_bytes = match certs.len() { 1 => Value::Bytes(certs[0].clone()), // single cert _ => { @@ -228,16 +180,19 @@ fn build_headers( */ let protected_header = protected_h.build(); + let ph2 = ProtectedHeader { + original_data: None, + header: protected_header.clone(), + }; + + let maybe_cts = if _sync { + cose_timestamp_countersign(signer, data, &ph2) + } else { + cose_timestamp_countersign_async(signer, data, &ph2).await + }; - let mut unprotected_h = if let Some(url) = ta_url { - let cts = cose_timestamp_countersign( - data, - &ProtectedHeader { - original_data: None, - header: protected_header.clone(), - }, - &url, - )?; + let mut unprotected_h = if let Some(cts) = maybe_cts { + let cts = cts?; let sigtst_vec = serde_cbor::to_vec(&make_cose_timestamp(&cts))?; let sigtst_cbor = serde_cbor::from_slice(&sigtst_vec)?; diff --git a/sdk/src/signer.rs b/sdk/src/signer.rs @@ -34,6 +34,18 @@ pub trait Signer { None } + /// Request RFC 3161 timestamp to be included in the manifest data + /// structure. + /// + /// `message` is a preliminary hash of the claim + /// + /// The default implementation will send the request to the URL + /// provided by [`Self::time_authority_url()`], if any. + fn send_timestamp_request(&self, message: &[u8]) -> Option<Result<Vec<u8>>> { + self.time_authority_url() + .map(|url| crate::time_stamp::default_rfc3161_request(&url, message)) + } + /// 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 @@ -99,6 +111,20 @@ pub trait AsyncSigner: Sync { None } + /// Request RFC 3161 timestamp to be included in the manifest data + /// structure. + /// + /// `message` is a preliminary hash of the claim + /// + /// The default implementation will send the request to the URL + /// provided by [`Self::time_authority_url()`], if any. + async fn send_timestamp_request(&self, message: &[u8]) -> Option<Result<Vec<u8>>> { + // NOTE: This is currently synchronous, but may become + // async in the future. + self.time_authority_url() + .map(|url| crate::time_stamp::default_rfc3161_request(&url, message)) + } + /// 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 diff --git a/sdk/src/time_stamp.rs b/sdk/src/time_stamp.rs @@ -13,6 +13,7 @@ use std::convert::TryFrom; +use async_generic::async_generic; use bcder::decode::Constructed; use coset::{sig_structure_data, ProtectedHeader}; use serde::{Deserialize, Serialize}; @@ -27,6 +28,7 @@ use crate::{ rfc5652::{CertificateChoices::Certificate, SignedData, OID_ID_SIGNED_DATA}, }, hash_utils::vec_compare, + AsyncSigner, Signer, }; #[allow(dead_code)] @@ -43,12 +45,17 @@ pub(crate) fn cose_countersign_data(data: &[u8], p_header: &ProtectedHeader) -> ) } -#[allow(dead_code)] +#[async_generic( + async_signature( + signer: &dyn AsyncSigner, + data: &[u8], + p_header: &ProtectedHeader, + ))] pub(crate) fn cose_timestamp_countersign( + signer: &dyn Signer, data: &[u8], p_header: &ProtectedHeader, - tsa_url: &str, -) -> Result<Vec<u8>> { +) -> Option<Result<Vec<u8>>> { // create countersignature with TimeStampReq parameters // payload: data // context "CounterSigner" @@ -58,7 +65,11 @@ pub(crate) fn cose_timestamp_countersign( // create sig data structure to be time stamped let sd = cose_countersign_data(data, p_header); - timestamp_data(tsa_url, &sd) + if _sync { + timestamp_data(signer, &sd) + } else { + timestamp_data_async(signer, &sd).await + } } #[allow(dead_code)] @@ -85,17 +96,6 @@ pub(crate) fn cose_sigtst_to_tstinfos( } } -/// Get URL to Time Authority to use -#[allow(dead_code)] // in case we make use of this later -pub fn get_ta_url() -> Option<String> { - //const TA_URL: &str = "http://timestamp.digicert.com"; - - match std::env::var("CAI_TA_URL") { - Ok(url) => Some(url), - Err(_) => None, - } -} - /// internal only function to work around bug in serialization of TimeStampResponse /// so we just return the data directly #[cfg(feature = "openssl_sign")] @@ -167,7 +167,7 @@ fn time_stamp_request_http( /// ASN.1 request object with reasonable defaults. #[cfg(feature = "openssl_sign")] -fn time_stamp_message_http( +pub(crate) fn time_stamp_message_http( url: &str, message: &[u8], digest_algorithm: DigestAlgorithm, @@ -258,9 +258,21 @@ impl TimeStampResponse { } } } + /// Generate TimeStamp based on rfc3161 using "data" as MessageImprint and return raw TimeStampRsp bytes +#[async_generic(async_signature(signer: &dyn AsyncSigner, data: &[u8]))] +pub fn timestamp_data(signer: &dyn Signer, data: &[u8]) -> Option<Result<Vec<u8>>> { + if _sync { + signer.send_timestamp_request(data) + } else { + signer.send_timestamp_request(data).await + // TO DO: Fix bug in async_generic. This .await + // should be automatically removed. + } +} + #[allow(unused_variables)] -pub fn timestamp_data(url: &str, data: &[u8]) -> Result<Vec<u8>> { +pub fn default_rfc3161_request(url: &str, data: &[u8]) -> Result<Vec<u8>> { #[cfg(feature = "openssl_sign")] { let ts = time_stamp_message_http(url, data, x509_certificate::DigestAlgorithm::Sha256)?; @@ -275,6 +287,7 @@ pub fn timestamp_data(url: &str, data: &[u8]) -> Result<Vec<u8>> { Err(Error::WasmNoCrypto) } } + pub fn gt_to_datetime( gt: x509_certificate::asn1time::GeneralizedTime, ) -> chrono::DateTime<chrono::Utc> {