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:
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)]