Skip to content

Commit eea9068

Browse files
feat: Move COSE OCSP support into c2pa-crypto (#793)
1 parent dbf64a6 commit eea9068

File tree

8 files changed

+481
-239
lines changed

8 files changed

+481
-239
lines changed

internal/crypto/src/cose/error.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,22 @@
1313

1414
use thiserror::Error;
1515

16-
use crate::time_stamp::TimeStampError;
16+
use crate::{cose::CertificateProfileError, time_stamp::TimeStampError};
1717

1818
/// Describes errors that can occur when processing or generating [COSE]
1919
/// signatures.
2020
///
2121
/// [COSE]: https://datatracker.ietf.org/doc/rfc9052/
2222
#[derive(Debug, Error)]
2323
pub enum CoseError {
24+
/// No signing certificate chain was found.
25+
#[error("missing signing certificate chain")]
26+
MissingSigningCertificateChain,
27+
28+
/// Signing certificates appeared in both protected and unprotected headers.
29+
#[error("multiple signing certificate chains detected")]
30+
MultipleSigningCertificateChains,
31+
2432
/// No time stamp token found.
2533
#[error("no time stamp token found in sigTst or sigTst2 header")]
2634
NoTimeStampToken,
@@ -32,4 +40,14 @@ pub enum CoseError {
3240
/// An error occurred while parsing a time stamp.
3341
#[error(transparent)]
3442
TimeStampError(#[from] TimeStampError),
43+
44+
/// The signing certificate(s) did not match the required certificate
45+
/// profile.
46+
#[error(transparent)]
47+
CertificateProfileError(#[from] CertificateProfileError),
48+
49+
/// An unexpected internal error occured while requesting the time stamp
50+
/// response.
51+
#[error("internal error ({0})")]
52+
InternalError(String),
3553
}

internal/crypto/src/cose/mod.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ pub use certificate_profile::{check_certificate_profile, CertificateProfileError
2626
mod error;
2727
pub use error::CoseError;
2828

29+
mod ocsp;
30+
pub use ocsp::{check_ocsp_status, check_ocsp_status_async, OcspFetchPolicy};
31+
2932
mod sigtst;
3033
pub use sigtst::{
31-
cose_countersign_data, parse_and_validate_sigtst, parse_and_validate_sigtst_async, TstToken,
34+
cose_countersign_data, parse_and_validate_sigtst, parse_and_validate_sigtst_async,
35+
validate_cose_tst_info, validate_cose_tst_info_async, TstToken,
3236
};

internal/crypto/src/cose/ocsp.rs

Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
// Copyright 2022 Adobe. All rights reserved.
2+
// This file is licensed to you under the Apache License,
3+
// Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0)
4+
// or the MIT license (http://opensource.org/licenses/MIT),
5+
// at your option.
6+
7+
// Unless required by applicable law or agreed to in writing,
8+
// this software is distributed on an "AS IS" BASIS, WITHOUT
9+
// WARRANTIES OR REPRESENTATIONS OF ANY KIND, either express or
10+
// implied. See the LICENSE-MIT and LICENSE-APACHE files for the
11+
// specific language governing permissions and limitations under
12+
// each license.
13+
14+
use async_generic::async_generic;
15+
use c2pa_status_tracker::StatusTracker;
16+
use chrono::{DateTime, Utc};
17+
use ciborium::value::Value;
18+
use coset::{CoseSign1, Label};
19+
20+
use crate::{
21+
cose::{
22+
check_certificate_profile, validate_cose_tst_info, validate_cose_tst_info_async,
23+
CertificateTrustPolicy, CoseError,
24+
},
25+
ocsp::OcspResponse,
26+
};
27+
28+
/// Given a COSE signature, extract the OCSP data and validate the status of
29+
/// that report.
30+
///
31+
/// TO DO: Determine if this needs to remain fully public after refactoring.
32+
#[async_generic]
33+
pub fn check_ocsp_status(
34+
sign1: &CoseSign1,
35+
data: &[u8],
36+
fetch_policy: OcspFetchPolicy,
37+
ctp: &CertificateTrustPolicy,
38+
validation_log: &mut impl StatusTracker,
39+
) -> Result<OcspResponse, CoseError> {
40+
match get_ocsp_der(sign1) {
41+
Some(ocsp_response_der) => {
42+
if _sync {
43+
check_stapled_ocsp_response(sign1, &ocsp_response_der, data, ctp, validation_log)
44+
} else {
45+
check_stapled_ocsp_response_async(
46+
sign1,
47+
&ocsp_response_der,
48+
data,
49+
ctp,
50+
validation_log,
51+
)
52+
.await
53+
}
54+
}
55+
56+
None => match fetch_policy {
57+
OcspFetchPolicy::FetchAllowed => {
58+
fetch_and_check_ocsp_response(sign1, data, ctp, validation_log)
59+
}
60+
OcspFetchPolicy::DoNotFetch => Ok(OcspResponse::default()),
61+
},
62+
}
63+
}
64+
65+
/// Policy for fetching OCSP responses.
66+
#[derive(Clone, Debug, Eq, PartialEq)]
67+
pub enum OcspFetchPolicy {
68+
/// Allow internet connection to fetch OCSP response.
69+
FetchAllowed,
70+
71+
/// Do not connect and ignore OCSP status if not available.
72+
DoNotFetch,
73+
}
74+
75+
#[async_generic]
76+
fn check_stapled_ocsp_response(
77+
sign1: &CoseSign1,
78+
ocsp_response_der: &[u8],
79+
data: &[u8],
80+
ctp: &CertificateTrustPolicy,
81+
validation_log: &mut impl StatusTracker,
82+
) -> Result<OcspResponse, CoseError> {
83+
let time_stamp_info = if _sync {
84+
validate_cose_tst_info(sign1, data)
85+
} else {
86+
validate_cose_tst_info_async(sign1, data).await
87+
};
88+
89+
// If the stapled OCSP response has a time stamp, we can validate it.
90+
let Ok(tst_info) = &time_stamp_info else {
91+
return Ok(OcspResponse::default());
92+
};
93+
94+
let signing_time: DateTime<Utc> = tst_info.gen_time.clone().into();
95+
96+
let Ok(ocsp_data) =
97+
OcspResponse::from_der_checked(ocsp_response_der, Some(signing_time), validation_log)
98+
else {
99+
return Ok(OcspResponse::default());
100+
};
101+
102+
// If we get a valid response, validate the certs.
103+
if ocsp_data.revoked_at.is_none() {
104+
if let Some(ocsp_certs) = &ocsp_data.ocsp_certs {
105+
check_certificate_profile(&ocsp_certs[0], ctp, validation_log, Some(tst_info))?;
106+
}
107+
}
108+
109+
Ok(ocsp_data)
110+
}
111+
112+
// TO DO: Add async version of this?
113+
fn fetch_and_check_ocsp_response(
114+
sign1: &CoseSign1,
115+
data: &[u8],
116+
ctp: &CertificateTrustPolicy,
117+
validation_log: &mut impl StatusTracker,
118+
) -> Result<OcspResponse, CoseError> {
119+
#[cfg(target_arch = "wasm32")]
120+
{
121+
let _ = (sign1, data, ctp, validation_log);
122+
Ok(OcspResponse::default())
123+
}
124+
125+
#[cfg(not(target_arch = "wasm32"))]
126+
{
127+
let certs = get_cert_chain(sign1)?;
128+
129+
let Some(ocsp_der) = crate::ocsp::fetch_ocsp_response(&certs) else {
130+
return Ok(OcspResponse::default());
131+
};
132+
133+
let ocsp_response_der = ocsp_der;
134+
135+
let signing_time: Option<DateTime<Utc>> = validate_cose_tst_info(sign1, data)
136+
.ok()
137+
.map(|tst_info| tst_info.gen_time.clone().into());
138+
139+
// Check the OCSP response, but only if it is well-formed.
140+
// Revocation errors are reported in the validation log.
141+
let Ok(ocsp_data) =
142+
OcspResponse::from_der_checked(&ocsp_response_der, signing_time, validation_log)
143+
else {
144+
// TO REVIEW: This is how the old code worked, but is it correct to ignore a
145+
// malformed OCSP response?
146+
return Ok(OcspResponse::default());
147+
};
148+
149+
// If we get a valid response validate the certs.
150+
if ocsp_data.revoked_at.is_none() {
151+
if let Some(ocsp_certs) = &ocsp_data.ocsp_certs {
152+
check_certificate_profile(&ocsp_certs[0], ctp, validation_log, None)?;
153+
}
154+
}
155+
156+
Ok(ocsp_data)
157+
}
158+
}
159+
160+
fn get_ocsp_der(sign1: &coset::CoseSign1) -> Option<Vec<u8>> {
161+
let der = sign1
162+
.unprotected
163+
.rest
164+
.iter()
165+
.find_map(|x: &(Label, Value)| {
166+
if x.0 == Label::Text("rVals".to_string()) {
167+
Some(x.1.clone())
168+
} else {
169+
None
170+
}
171+
})?;
172+
173+
let Value::Map(rvals_map) = der else {
174+
return None;
175+
};
176+
177+
// Find OCSP value if available.
178+
rvals_map.iter().find_map(|x: &(Value, Value)| {
179+
if x.0 == Value::Text("ocspVals".to_string()) {
180+
x.1.as_array()
181+
.and_then(|ocsp_rsp_val| ocsp_rsp_val.first())
182+
.and_then(Value::as_bytes)
183+
.cloned()
184+
} else {
185+
None
186+
}
187+
})
188+
}
189+
190+
// TO DO: See if this gets more widely used in crate.
191+
#[cfg(not(target_arch = "wasm32"))]
192+
fn get_cert_chain(sign1: &coset::CoseSign1) -> Result<Vec<Vec<u8>>, CoseError> {
193+
use coset::iana::{self, EnumI64};
194+
195+
// Check the protected header first.
196+
let Some(value) = sign1
197+
.protected
198+
.header
199+
.rest
200+
.iter()
201+
.find_map(|x: &(Label, Value)| {
202+
if x.0 == Label::Text("x5chain".to_string())
203+
|| x.0 == Label::Int(iana::HeaderParameter::X5Chain.to_i64())
204+
{
205+
Some(x.1.clone())
206+
} else {
207+
None
208+
}
209+
})
210+
else {
211+
// Not there: Also try unprotected header. (This was permitted in older versions
212+
// of C2PA.)
213+
return get_unprotected_header_certs(sign1);
214+
};
215+
216+
// Certs may be in protected or unprotected header, but not both.
217+
if get_unprotected_header_certs(sign1).is_ok() {
218+
return Err(CoseError::MultipleSigningCertificateChains);
219+
}
220+
221+
cert_chain_from_cbor_value(value)
222+
}
223+
224+
#[cfg(not(target_arch = "wasm32"))]
225+
fn get_unprotected_header_certs(sign1: &coset::CoseSign1) -> Result<Vec<Vec<u8>>, CoseError> {
226+
let Some(value) = sign1
227+
.unprotected
228+
.rest
229+
.iter()
230+
.find_map(|x: &(Label, Value)| {
231+
if x.0 == Label::Text("x5chain".to_string()) {
232+
Some(x.1.clone())
233+
} else {
234+
None
235+
}
236+
})
237+
else {
238+
return Err(CoseError::MissingSigningCertificateChain);
239+
};
240+
241+
cert_chain_from_cbor_value(value)
242+
}
243+
244+
#[cfg(not(target_arch = "wasm32"))]
245+
fn cert_chain_from_cbor_value(value: Value) -> Result<Vec<Vec<u8>>, CoseError> {
246+
match value {
247+
Value::Array(cert_chain) => {
248+
let certs: Vec<Vec<u8>> = cert_chain
249+
.iter()
250+
.filter_map(|c| {
251+
if let Value::Bytes(der_bytes) = c {
252+
Some(der_bytes.clone())
253+
} else {
254+
None
255+
}
256+
})
257+
.collect();
258+
259+
if certs.is_empty() {
260+
Err(CoseError::MissingSigningCertificateChain)
261+
} else {
262+
Ok(certs)
263+
}
264+
}
265+
266+
Value::Bytes(ref der_bytes) => Ok(vec![der_bytes.clone()]),
267+
268+
_ => Err(CoseError::MissingSigningCertificateChain),
269+
}
270+
}

internal/crypto/src/cose/sigtst.rs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
// each license.
1313

1414
use async_generic::async_generic;
15-
use coset::{sig_structure_data, ProtectedHeader, SignatureContext};
15+
use ciborium::value::Value;
16+
use coset::{sig_structure_data, Label, ProtectedHeader, SignatureContext};
1617
use serde::{Deserialize, Serialize};
1718

1819
use crate::{
@@ -21,6 +22,46 @@ use crate::{
2122
time_stamp::{verify_time_stamp, verify_time_stamp_async},
2223
};
2324

25+
/// Given a COSE signature, retrieve the `sigTst` header from it and validate
26+
/// the information within it.
27+
///
28+
/// Return a [`TstInfo`] struct if available and valid.
29+
#[async_generic]
30+
pub fn validate_cose_tst_info(sign1: &coset::CoseSign1, data: &[u8]) -> Result<TstInfo, CoseError> {
31+
let Some(sigtst) = &sign1
32+
.unprotected
33+
.rest
34+
.iter()
35+
.find_map(|x: &(Label, Value)| {
36+
if x.0 == Label::Text("sigTst".to_string()) {
37+
Some(x.1.clone())
38+
} else {
39+
None
40+
}
41+
})
42+
else {
43+
return Err(CoseError::NoTimeStampToken);
44+
};
45+
46+
let mut time_cbor: Vec<u8> = vec![];
47+
ciborium::into_writer(sigtst, &mut time_cbor)
48+
.map_err(|e| CoseError::InternalError(e.to_string()))?;
49+
50+
let tst_infos = if _sync {
51+
parse_and_validate_sigtst(&time_cbor, data, &sign1.protected)?
52+
} else {
53+
parse_and_validate_sigtst_async(&time_cbor, data, &sign1.protected).await?
54+
};
55+
56+
// For now, we only pay attention to the first time stamp header.
57+
// Technically, more are permitted, but we ignore them for now.
58+
let Some(tst_info) = tst_infos.into_iter().next() else {
59+
return Err(CoseError::NoTimeStampToken);
60+
};
61+
62+
Ok(tst_info)
63+
}
64+
2465
/// Parse the `sigTst` header from a COSE signature, which should contain one or
2566
/// more `TstInfo` structures ([RFC 3161] time stamps).
2667
///

0 commit comments

Comments
 (0)