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 c33c9c7df03c4df3e2b847f1124fc73243595252
parent f7d0d145748c6b6ab45691ebe0464a0618a6f4c4
Author: mauricefisher64 <92736594+mauricefisher64@users.noreply.github.com>
Date:   Fri, 26 May 2023 09:17:42 -0400

Generate and validate 1.3 Cose signatures (#256)

* Generate and validate 1.3 Cose signatures

* Fix broken unit test

* clippy fix

* Update serde version for export_schema

* Fix typo

---------

Co-authored-by: Gavin  Peacock <gpeacock@adobe.com>
Diffstat:
Mexport_schema/Cargo.toml | 2+-
Msdk/src/cose_sign.rs | 64++++++++++++++++++++++++++++------------------------------------
Msdk/src/cose_validator.rs | 91++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
3 files changed, 112 insertions(+), 45 deletions(-)

diff --git a/export_schema/Cargo.toml b/export_schema/Cargo.toml @@ -10,4 +10,4 @@ rust-version = "1.65.0" anyhow = "1.0.40" c2pa = { path = "../sdk", features = ["file_io", "json_schema"] } schemars = "0.8.12" -serde_json = "1.0" +serde_json = "1.0.81" diff --git a/sdk/src/cose_sign.rs b/sdk/src/cose_sign.rs @@ -17,7 +17,8 @@ use ciborium::value::Value; use coset::{ - iana, CoseSign1, CoseSign1Builder, Header, HeaderBuilder, Label, TaggedCborSerializable, + iana::{self, EnumI64}, + CoseSign1, CoseSign1Builder, Header, HeaderBuilder, Label, TaggedCborSerializable, }; use crate::{ @@ -96,7 +97,7 @@ pub(crate) fn cose_sign(signer: &dyn Signer, data: &[u8], box_size: usize) -> Re let alg = signer.alg(); // build complete header - let (alg_id, unprotected_header) = build_unprotected_header( + let (protected_header, unprotected_header) = build_headers( data, alg, signer.certs()?, @@ -107,7 +108,7 @@ pub(crate) fn cose_sign(signer: &dyn Signer, data: &[u8], box_size: usize) -> Re let aad = b""; // no additional data required here let sign1_builder = CoseSign1Builder::new() - .protected(alg_id) + .protected(protected_header) .unprotected(unprotected_header) .payload(data.to_vec()) .try_create_signature(aad, |bytes| signer.sign(bytes))?; @@ -153,7 +154,7 @@ pub async fn cose_sign_async( let alg = signer.alg(); // build complete header - let (alg_id, unprotected_header) = build_unprotected_header( + let (protected_header, unprotected_header) = build_headers( data, alg, signer.certs()?, @@ -164,7 +165,7 @@ pub async fn cose_sign_async( let aad = b""; // no additional data required here let sign1_builder = CoseSign1Builder::new() - .protected(alg_id) + .protected(protected_header) .unprotected(unprotected_header) .payload(data.to_vec()); @@ -188,35 +189,21 @@ pub async fn cose_sign_async( Ok(c2pa_sig_data) } -fn build_unprotected_header( +fn build_headers( data: &[u8], alg: SigningAlg, certs: Vec<Vec<u8>>, ta_url: Option<String>, ocsp_val: Option<Vec<u8>>, ) -> Result<(Header, Header)> { - 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 mut protected_h = match alg { + SigningAlg::Ps256 => HeaderBuilder::new().algorithm(iana::Algorithm::PS256), + SigningAlg::Ps384 => HeaderBuilder::new().algorithm(iana::Algorithm::PS384), + SigningAlg::Ps512 => HeaderBuilder::new().algorithm(iana::Algorithm::PS512), + SigningAlg::Es256 => HeaderBuilder::new().algorithm(iana::Algorithm::ES256), + SigningAlg::Es384 => HeaderBuilder::new().algorithm(iana::Algorithm::ES384), + SigningAlg::Es512 => HeaderBuilder::new().algorithm(iana::Algorithm::ES512), + SigningAlg::Ed25519 => HeaderBuilder::new().algorithm(iana::Algorithm::EdDSA), }; let sc_der_array_or_bytes = match certs.len() { @@ -230,16 +217,20 @@ fn build_unprotected_header( } }; - let mut unprotected = if let Some(url) = ta_url { + // add certs to protected header (spec 1.3 now requires integer 33(X5Chain) in favor of string "x5chain" going forward) + protected_h = protected_h.value( + iana::HeaderParameter::X5Chain.to_i64(), + sc_der_array_or_bytes, + ); + + let mut unprotected_h = 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) + HeaderBuilder::new().text_value("sigTst".to_string(), sigtst_cbor) } else { - HeaderBuilder::new().text_value("x5chain".to_string(), sc_der_array_or_bytes) + HeaderBuilder::new() }; // set the ocsp responder response if available @@ -250,13 +241,14 @@ fn build_unprotected_header( 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)); + unprotected_h = unprotected_h.text_value("rVals".to_string(), Value::Map(r_vals)); } // build complete header - let unprotected_header = unprotected.build(); + let protected_header = protected_h.build(); + let unprotected_header = unprotected_h.build(); - Ok((alg_id, unprotected_header)) + Ok((protected_header, unprotected_header)) } const PAD: &str = "pad"; diff --git a/sdk/src/cose_validator.rs b/sdk/src/cose_validator.rs @@ -14,7 +14,10 @@ use asn1_rs::{Any, Class, Header, Tag}; use ciborium::value::Value; use conv::*; -use coset::{sig_structure_data, Label, TaggedCborSerializable}; +use coset::{ + iana::{self, EnumI64}, + sig_structure_data, Label, TaggedCborSerializable, +}; use x509_parser::{ der_parser::{ber::parse_ber_sequence, oid}, oid_registry::Oid, @@ -52,6 +55,8 @@ const SECP521R1_OID: Oid<'static> = oid!(1.3.132 .0 .35); const SECP384R1_OID: Oid<'static> = oid!(1.3.132 .0 .34); const PRIME256V1_OID: Oid<'static> = oid!(1.2.840 .10045 .3 .1 .7); +const DOCUMENT_SIGNING_OID: Oid<'static> = oid!(1.3.6 .1 .5 .5 .7 .3 .36); + /********************** Supported Valiators *************************************** RS256 RSASSA-PKCS1-v1_5 using SHA-256 - not recommended RS384 RSASSA-PKCS1-v1_5 using SHA-384 - not recommended @@ -91,6 +96,11 @@ fn get_cose_sign1( } } } + +fn has_oid(eku: &ExtendedKeyUsage, oid_val: &Oid) -> bool { + eku.other.iter().any(|v| v == oid_val) +} + fn check_cert( _alg: SigningAlg, ca_der_bytes: &[u8], @@ -370,7 +380,11 @@ fn check_cert( return Err(Error::CoseInvalidCert); } - if !(eku.email_protection || eku.ocsp_signing || eku.time_stamping) { + if !(eku.email_protection + || eku.ocsp_signing + || eku.time_stamping + || has_oid(eku, &DOCUMENT_SIGNING_OID)) + { let log_item = log_item!( "Cose_Sign1", "certificate missing required EKU", @@ -389,7 +403,8 @@ fn check_cert( && (eku.client_auth | eku.code_signing | eku.email_protection - | eku.server_auth)) + | eku.server_auth + | has_oid(eku, &DOCUMENT_SIGNING_OID))) { let log_item = log_item!( "Cose_Sign1", @@ -525,11 +540,8 @@ fn get_sign_cert(sign1: &coset::CoseSign1) -> Result<Vec<u8>> { let certs = get_sign_certs(sign1)?; Ok(certs[0].clone()) } -// get the public key der -fn get_sign_certs(sign1: &coset::CoseSign1) -> Result<Vec<Vec<u8>>> { - let mut certs: Vec<Vec<u8>> = Vec::new(); - // get the public key der +fn get_unprotected_header_certs(sign1: &coset::CoseSign1) -> Result<Vec<Vec<u8>>> { if let Some(der) = sign1 .unprotected .rest @@ -542,6 +554,8 @@ fn get_sign_certs(sign1: &coset::CoseSign1) -> Result<Vec<Vec<u8>>> { } }) { + let mut certs: Vec<Vec<u8>> = Vec::new(); + match der { Value::Array(cert_chain) => { // handle array of certs @@ -550,7 +564,12 @@ fn get_sign_certs(sign1: &coset::CoseSign1) -> Result<Vec<Vec<u8>>> { certs.push(der_bytes.clone()); } } - Ok(certs) + + if certs.is_empty() { + Err(Error::CoseMissingKey) + } else { + Ok(certs) + } } Value::Bytes(ref der_bytes) => { // handle single cert case @@ -563,6 +582,62 @@ fn get_sign_certs(sign1: &coset::CoseSign1) -> Result<Vec<Vec<u8>>> { Err(Error::CoseX5ChainMissing) } } +// get the public key der +fn get_sign_certs(sign1: &coset::CoseSign1) -> Result<Vec<Vec<u8>>> { + // check for protected header int, then protected header x5chain, + // then the legacy unprotected x5chain to get the public key der + + // check the protected header + if let Some(der) = sign1 + .protected + .header + .rest + .iter() + .find_map(|x: &(Label, Value)| { + if x.0 == Label::Text("x5chain".to_string()) + || x.0 == Label::Int(iana::HeaderParameter::X5Chain.to_i64()) + { + Some(x.1.clone()) + } else { + None + } + }) + { + // make sure there are no certs in the legacy unprotected header, certs + // are only allowing in protect OR unprotected header + if get_unprotected_header_certs(sign1).is_ok() { + return Err(Error::CoseVerifier); + } + + let mut certs: Vec<Vec<u8>> = Vec::new(); + + match der { + Value::Array(cert_chain) => { + // handle array of certs + for c in cert_chain { + if let Value::Bytes(der_bytes) = c { + certs.push(der_bytes.clone()); + } + } + + if certs.is_empty() { + return Err(Error::CoseX5ChainMissing); + } else { + return Ok(certs); + } + } + Value::Bytes(ref der_bytes) => { + // handle single cert case + certs.push(der_bytes.clone()); + return Ok(certs); + } + _ => return Err(Error::CoseX5ChainMissing), + } + } + + // check the unprotected header if necessary + get_unprotected_header_certs(sign1) +} // internal util function to dump the cert chain in PEM format #[allow(unused_variables)]