diff --git a/internal/crypto/Cargo.toml b/internal/crypto/Cargo.toml index ee9af33b9..06b3712f6 100644 --- a/internal/crypto/Cargo.toml +++ b/internal/crypto/Cargo.toml @@ -36,6 +36,8 @@ base64 = "0.22.1" bcder = "0.7.3" bytes = "1.7.2" c2pa-status-tracker = { path = "../status-tracker", version = "0.1.0" } +ciborium = "0.2.2" +coset = "0.3.1" getrandom = { version = "0.2.7", features = ["js"] } hex = "0.4.3" rand = "0.8.5" @@ -44,6 +46,7 @@ rasn-ocsp = "0.18.0" rasn-pkix = "0.18.0" schemars = { version = "0.8.21", optional = true } serde = { version = "1.0.197", features = ["derive"] } +serde_bytes = "0.11.5" sha1 = "0.10.6" sha2 = "0.10.6" thiserror = "1.0.61" diff --git a/internal/crypto/src/cose/error.rs b/internal/crypto/src/cose/error.rs new file mode 100644 index 000000000..526f5ad6b --- /dev/null +++ b/internal/crypto/src/cose/error.rs @@ -0,0 +1,35 @@ +// Copyright 2024 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. + +use thiserror::Error; + +use crate::time_stamp::TimeStampError; + +/// Describes errors that can occur when processing or generating [COSE] +/// signatures. +/// +/// [COSE]: https://datatracker.ietf.org/doc/rfc9052/ +#[derive(Debug, Error)] +pub enum CoseError { + /// No time stamp token found. + #[error("no time stamp token found in sigTst or sigTst2 header")] + NoTimeStampToken, + + /// An error occurred while parsing CBOR. + #[error("error while parsing CBOR ({0})")] + CborParsingError(String), + + /// An error occurred while parsing a time stamp. + #[error(transparent)] + TimeStampError(#[from] TimeStampError), +} diff --git a/internal/crypto/src/cose/mod.rs b/internal/crypto/src/cose/mod.rs new file mode 100644 index 000000000..992f52ea9 --- /dev/null +++ b/internal/crypto/src/cose/mod.rs @@ -0,0 +1,24 @@ +// Copyright 2024 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. + +//! This module provides functions for working with [COSE] signatures. +//! +//! [COSE]: https://datatracker.ietf.org/doc/rfc9052/ + +mod error; +pub use error::CoseError; + +mod sigtst; +pub use sigtst::{ + cose_countersign_data, parse_and_validate_sigtst, parse_and_validate_sigtst_async, TstToken, +}; diff --git a/internal/crypto/src/cose/sigtst.rs b/internal/crypto/src/cose/sigtst.rs new file mode 100644 index 000000000..5408e9885 --- /dev/null +++ b/internal/crypto/src/cose/sigtst.rs @@ -0,0 +1,87 @@ +// 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. + +use async_generic::async_generic; +use coset::{sig_structure_data, ProtectedHeader, SignatureContext}; +use serde::{Deserialize, Serialize}; + +use crate::{ + asn1::rfc3161::TstInfo, + cose::CoseError, + time_stamp::{verify_time_stamp, verify_time_stamp_async}, +}; + +/// Parse the `sigTst` header from a COSE signature, which should contain one or +/// more `TstInfo` structures ([RFC 3161] time stamps). +/// +/// Validate each time stamp and return them if valid. +/// +/// [RFC 3161]: https://datatracker.ietf.org/doc/html/rfc3161 +#[async_generic] +pub fn parse_and_validate_sigtst( + sigtst_cbor: &[u8], + data: &[u8], + p_header: &ProtectedHeader, +) -> Result, CoseError> { + let tst_container: TstContainer = ciborium::from_reader(sigtst_cbor) + .map_err(|err| CoseError::CborParsingError(err.to_string()))?; + + let mut tstinfos: Vec = vec![]; + + for token in &tst_container.tst_tokens { + let tbs = cose_countersign_data(data, p_header); + let tst_info = if _sync { + verify_time_stamp(&token.val, &tbs)? + } else { + verify_time_stamp_async(&token.val, &tbs).await? + }; + + tstinfos.push(tst_info); + } + + if tstinfos.is_empty() { + Err(CoseError::NoTimeStampToken) + } else { + Ok(tstinfos) + } +} + +/// Given an arbitrary message and a COSE protected header, generate the binary +/// blob to be signed as part of the COSE signature. +pub fn cose_countersign_data(data: &[u8], p_header: &ProtectedHeader) -> Vec { + let aad: Vec = vec![]; + + sig_structure_data( + SignatureContext::CounterSignature, + p_header.clone(), + None, + &aad, + data, + ) +} + +/// Raw contents of an [RFC 3161] time stamp. +/// +/// [RFC 3161]: https://datatracker.ietf.org/doc/html/rfc3161 +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct TstToken { + #[allow(missing_docs)] + #[serde(with = "serde_bytes")] + pub val: Vec, +} + +#[derive(Debug, Deserialize, Eq, PartialEq, Serialize)] +struct TstContainer { + #[serde(rename = "tstTokens")] + tst_tokens: Vec, +} diff --git a/internal/crypto/src/lib.rs b/internal/crypto/src/lib.rs index f977b1fa6..c41173434 100644 --- a/internal/crypto/src/lib.rs +++ b/internal/crypto/src/lib.rs @@ -21,6 +21,7 @@ pub mod asn1; pub mod base64; +pub mod cose; pub mod hash; pub(crate) mod internal; pub mod ocsp; diff --git a/internal/crypto/src/time_stamp/error.rs b/internal/crypto/src/time_stamp/error.rs index f75915224..f9ca9fd80 100644 --- a/internal/crypto/src/time_stamp/error.rs +++ b/internal/crypto/src/time_stamp/error.rs @@ -60,7 +60,7 @@ pub enum TimeStampError { #[error("unable to complete HTTP request ({0})")] HttpConnectionError(String), - /// An unexpected internal error occured whiel requesting the time stamp + /// An unexpected internal error occured while requesting the time stamp /// response. #[error("internal error ({0})")] InternalError(String), diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml index ebbc25c2c..a5f2f490a 100644 --- a/sdk/Cargo.toml +++ b/sdk/Cargo.toml @@ -79,7 +79,7 @@ chrono = { version = "0.4.38", default-features = false, features = [ "serde", "wasmbind", ] } -ciborium = "0.2.0" +ciborium = "0.2.2" config = { version = "0.14.0", default-features = false, features = [ "json", "json5", diff --git a/sdk/src/cose_validator.rs b/sdk/src/cose_validator.rs index e00904d82..b746d7827 100644 --- a/sdk/src/cose_validator.rs +++ b/sdk/src/cose_validator.rs @@ -17,6 +17,7 @@ use asn1_rs::{Any, Class, Header, Tag}; use async_generic::async_generic; use c2pa_crypto::{ asn1::rfc3161::TstInfo, + cose::{parse_and_validate_sigtst, parse_and_validate_sigtst_async}, ocsp::OcspResponse, p1363::parse_ec_der_sig, raw_signature::{validator_for_signing_alg, RawSignatureValidator}, @@ -811,10 +812,9 @@ fn get_timestamp_info(sign1: &coset::CoseSign1, data: &[u8]) -> Result { let time_cbor = serde_cbor::to_vec(t)?; let tst_infos = if _sync { - crate::time_stamp::cose_sigtst_to_tstinfos(&time_cbor, data, &sign1.protected)? + parse_and_validate_sigtst(&time_cbor, data, &sign1.protected)? } else { - crate::time_stamp::cose_sigtst_to_tstinfos_async(&time_cbor, data, &sign1.protected) - .await? + parse_and_validate_sigtst_async(&time_cbor, data, &sign1.protected).await? }; // there should only be one but consider handling more in the future since it is technically ok diff --git a/sdk/src/error.rs b/sdk/src/error.rs index 6be0da5f3..d4ee87532 100644 --- a/sdk/src/error.rs +++ b/sdk/src/error.rs @@ -327,3 +327,13 @@ impl From for Error { } } } + +impl From for Error { + fn from(err: c2pa_crypto::cose::CoseError) -> Self { + match err { + c2pa_crypto::cose::CoseError::NoTimeStampToken => Self::NotFound, + c2pa_crypto::cose::CoseError::CborParsingError(_) => Self::CoseTimeStampGeneration, + c2pa_crypto::cose::CoseError::TimeStampError(e) => e.into(), + } + } +} diff --git a/sdk/src/time_stamp.rs b/sdk/src/time_stamp.rs index 72fde5113..8bb47db34 100644 --- a/sdk/src/time_stamp.rs +++ b/sdk/src/time_stamp.rs @@ -12,34 +12,11 @@ // each license. use async_generic::async_generic; -use c2pa_crypto::{ - asn1::rfc3161::TstInfo, - time_stamp::{verify_time_stamp, verify_time_stamp_async}, -}; -use coset::{sig_structure_data, ProtectedHeader}; +use c2pa_crypto::cose::{cose_countersign_data, TstToken}; +use coset::ProtectedHeader; use serde::{Deserialize, Serialize}; -use crate::{ - error::{Error, Result}, - AsyncSigner, Signer, -}; - -// Generate TimeStamp signature according to https://datatracker.ietf.org/doc/html/rfc3161 -// using the specified Time Authority - -#[allow(dead_code)] -pub(crate) fn cose_countersign_data(data: &[u8], p_header: &ProtectedHeader) -> Vec { - let aad: Vec = Vec::new(); - - // create sig_structure_data to be signed - sig_structure_data( - coset::SignatureContext::CounterSignature, - p_header.clone(), - None, - &aad, - data, - ) -} +use crate::{error::Result, AsyncSigner, Signer}; #[async_generic( async_signature( @@ -68,38 +45,9 @@ pub(crate) fn cose_timestamp_countersign( } } -#[async_generic] -pub(crate) fn cose_sigtst_to_tstinfos( - sigtst_cbor: &[u8], - data: &[u8], - p_header: &ProtectedHeader, -) -> Result> { - let tst_container: TstContainer = - serde_cbor::from_slice(sigtst_cbor).map_err(|_err| Error::CoseTimeStampGeneration)?; - - let mut tstinfos: Vec = Vec::new(); - - for token in &tst_container.tst_tokens { - let tbs = cose_countersign_data(data, p_header); - let tst_info = if _sync { - verify_time_stamp(&token.val, &tbs)? - } else { - verify_time_stamp_async(&token.val, &tbs).await? - }; - - tstinfos.push(tst_info); - } - - if tstinfos.is_empty() { - Err(Error::NotFound) - } else { - Ok(tstinfos) - } -} - // 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>> { +fn timestamp_data(signer: &dyn Signer, data: &[u8]) -> Option>> { if _sync { signer .send_time_stamp_request(data) @@ -114,44 +62,26 @@ pub fn timestamp_data(signer: &dyn Signer, data: &[u8]) -> Option } } -#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)] -pub struct TstToken { - #[serde(with = "serde_bytes")] - pub val: Vec, -} - -#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)] -pub struct TstContainer { - #[serde(rename = "tstTokens")] - pub tst_tokens: Vec, -} - -impl TstContainer { - pub fn new() -> Self { - TstContainer { - tst_tokens: Vec::new(), - } - } - - pub fn add_token(&mut self, token: TstToken) { - self.tst_tokens.push(token); - } -} - -impl Default for TstContainer { - fn default() -> Self { - Self::new() - } -} - // Wrap rfc3161 TimeStampRsp in COSE sigTst object pub(crate) fn make_cose_timestamp(ts_data: &[u8]) -> TstContainer { let token = TstToken { val: ts_data.to_vec(), }; - let mut container = TstContainer::new(); + let mut container = TstContainer::default(); container.add_token(token); container } + +#[derive(Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +pub(crate) struct TstContainer { + #[serde(rename = "tstTokens")] + pub(crate) tst_tokens: Vec, +} + +impl TstContainer { + pub fn add_token(&mut self, token: TstToken) { + self.tst_tokens.push(token); + } +}