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 adc649040aed2ffd735a74de43f84b7c452a8a50
parent 65dedde78caa8eefc1ccc08a310e46359c06d732
Author: mauricefisher64 <92736594+mauricefisher64@users.noreply.github.com>
Date:   Wed,  7 Feb 2024 09:52:13 -0500

Ocsp support (#371)

* OCSP support

* update readme

* code cleanup

* update nom dependency

* Build fixes

* Attempt at timestamp fix.
Added OCSP stapling test.
Added Dylan simplification.

* restore accidentally removed test

* minor cleanup
merge from main
Diffstat:
MREADME.md | 3+++
Mmake_test_images/Cargo.toml | 2+-
Msdk/Cargo.toml | 5+++++
Msdk/src/cose_validator.rs | 215++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Msdk/src/lib.rs | 1-
Msdk/src/manifest_store_report.rs | 7++++++-
Msdk/src/ocsp_utils.rs | 543++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Msdk/src/openssl/mod.rs | 1+
Msdk/src/openssl/rsa_signer.rs | 29++++++++++++++++++-----------
Msdk/src/store.rs | 52+++++++++++++++++++++++++++++++++-------------------
Msdk/src/utils/hash_utils.rs | 8+++++++-
Msdk/src/validator.rs | 1+
Asdk/tests/fixtures/ocsp_good.data | 0
Asdk/tests/fixtures/ocsp_revoked.data | 0
14 files changed, 619 insertions(+), 248 deletions(-)

diff --git a/README.md b/README.md @@ -85,6 +85,9 @@ The Rust library crate provides: * `no_interleaved_io` forces fully-synchronous I/O; otherwise, the library uses threaded I/O for some operations to improve performance. * `fetch_remote_manifests` enables the verification step to retrieve externally referenced manifest stores. External manifests are only fetched if there is no embedded manifest store and no locally adjacent .c2pa manifest store file of the same name. * `json_schema` is used by `make schema` to produce a JSON schema document that represents the `ManifestStore` data structures. +* `fetch_ocsp_response` if the feature is enabled, during manifest validation if an OCSP response is not present in the manifest we will attempt a network call to fetch it. OCSP is used to check the revocation status of the manifest signing certificate. +* `psxxx_ocsp_stapling_experimental` this is an demonstration feature that will attempt to fetch the OCSP data from the OCSP responders listed in the manifest signing certificate. The response becomes part of the manifest and is used to prove the certificate was not revoked at the time of signing. This is only implemented for PS256, PS384 and PS512 signatures and is intended as a demonstration. + ## License diff --git a/make_test_images/Cargo.toml b/make_test_images/Cargo.toml @@ -18,7 +18,7 @@ env_logger = "0.10" log = "0.4.8" image = { version = "0.24.7", default-features = false, features = ["jpeg", "png"] } memchr = "2.7.1" -nom = "7.1.1" +nom = "7.1.3" regex = "1.5.6" serde = "1.0.137" serde_json = "1.0.81" diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml @@ -27,6 +27,8 @@ rustdoc-args = ["--cfg", "docsrs"] [features] default = [] add_thumbnails = ["image"] +fetch_ocsp_response = [] +psxxx_ocsp_stapling_experimental = [] file_io = ["openssl_sign"] serialize_thumbnails = [] xmp_write = ["xmp_toolkit"] @@ -87,6 +89,9 @@ multihash = "0.11.4" mp4 = "0.13.0" png_pong = "0.8.2" range-set = "0.0.9" +rasn-ocsp = "0.12.4" +rasn-pkix = "0.12.4" +rasn = "0.12.4" ring = "0.16.20" riff = "1.0.1" schemars = { version = "0.8.13", optional = true } diff --git a/sdk/src/cose_validator.rs b/sdk/src/cose_validator.rs @@ -32,6 +32,7 @@ use crate::wasm::webcrypto_validator::validate_async; use crate::{ asn1::rfc3161::TstInfo, error::{Error, Result}, + ocsp_utils::{check_ocsp_response, OcspData}, status_tracker::{log_item, StatusTracker}, time_stamp::gt_to_datetime, validation_status, @@ -104,7 +105,6 @@ fn has_oid(eku: &ExtendedKeyUsage, oid_val: &Oid) -> bool { } fn check_cert( - _alg: SigningAlg, ca_der_bytes: &[u8], validation_log: &mut impl StatusTracker, _tst_info_opt: Option<&TstInfo>, @@ -451,7 +451,7 @@ fn check_cert( } key_usage_good = true; } - if ku.key_cert_sign() { + if ku.key_cert_sign() || ku.non_repudiation() { key_usage_good = true; } // todo: warn if not marked critical @@ -642,6 +642,109 @@ fn get_sign_certs(sign1: &coset::CoseSign1) -> Result<Vec<Vec<u8>>> { get_unprotected_header_certs(sign1) } +// get OCSP der +fn get_ocsp_der(sign1: &coset::CoseSign1) -> Option<Vec<u8>> { + if let Some(der) = sign1 + .unprotected + .rest + .iter() + .find_map(|x: &(Label, Value)| { + if x.0 == Label::Text("rVals".to_string()) { + Some(x.1.clone()) + } else { + None + } + }) + { + match der { + Value::Map(rvals_map) => { + // find OCSP value if available + rvals_map.iter().find_map(|x: &(Value, Value)| { + if x.0 == Value::Text("ocspVals".to_string()) { + x.1.as_array() + .and_then(|ocsp_rsp_val| ocsp_rsp_val.first()) + .and_then(Value::as_bytes) + .cloned() + } else { + None + } + }) + } + _ => None, + } + } else { + None + } +} + +pub(crate) fn check_ocsp_status( + cose_bytes: &[u8], + data: &[u8], + validation_log: &mut impl StatusTracker, +) -> Result<OcspData> { + let sign1 = get_cose_sign1(cose_bytes, data, validation_log)?; + + let time_stamp_info = get_timestamp_info(&sign1, data); + + let mut result = Ok(OcspData::default()); + + if let Some(ocsp_response_der) = get_ocsp_der(&sign1) { + // check stapled OCSP response, must have timestamp + if let Ok(tst_info) = &time_stamp_info { + let signing_time = gt_to_datetime(tst_info.gen_time.clone()); + + // Check the OCSP response, only use if not malformed. Revocation errors are reported in the validation log + if let Ok(ocsp_data) = + check_ocsp_response(&ocsp_response_der, Some(signing_time), validation_log) + { + // if we get a valid response validate the certs + if ocsp_data.revoked_at.is_none() { + if let Some(ocsp_certs) = &ocsp_data.ocsp_certs { + check_cert(&ocsp_certs[0], validation_log, None)?; + } + } + result = Ok(ocsp_data); + } + } + } else { + // fetch OCSP response if feature "fetch_ocsp_response" + // only support fetching with the op + #[cfg(feature = "fetch_ocsp_response")] + { + // get the cert chain + let certs = get_sign_certs(&sign1)?; + + if let Some(ocsp_der) = crate::ocsp_utils::fetch_ocsp_response(&certs) { + // fetch_ocsp_response(&certs) { + let ocsp_response_der = ocsp_der; + + let signing_time = match &time_stamp_info { + Ok(tst_info) => { + let signing_time = gt_to_datetime(tst_info.gen_time.clone()); + Some(signing_time) + } + Err(_) => None, + }; + + // Check the OCSP response, only use if not malformed. Revocation errors are reported in the validation log + if let Ok(ocsp_data) = + check_ocsp_response(&ocsp_response_der, signing_time, validation_log) + { + // if we get a valid response validate the certs + if ocsp_data.revoked_at.is_none() { + if let Some(ocsp_certs) = &ocsp_data.ocsp_certs { + check_cert(&ocsp_certs[0], validation_log, None)?; + } + } + result = Ok(ocsp_data); + } + } + } + } + + result +} + // internal util function to dump the cert chain in PEM format #[allow(unused_variables)] fn dump_cert_chain(certs: &[Vec<u8>], output_path: Option<&std::path::Path>) -> Result<Vec<u8>> { @@ -775,11 +878,11 @@ pub async fn verify_cose_async( if !signature_only { // verify certs match get_timestamp_info(&sign1, &data) { - Ok(tst_info) => check_cert(alg, &der_bytes, validation_log, Some(&tst_info))?, + Ok(tst_info) => check_cert(&der_bytes, validation_log, Some(&tst_info))?, Err(e) => { // log timestamp errors match e { - Error::NotFound => check_cert(alg, &der_bytes, validation_log, None)?, + Error::NotFound => check_cert(&der_bytes, validation_log, None)?, Error::CoseTimeStampMismatch => { let log_item = log_item!( "Cose_Sign1", @@ -810,6 +913,9 @@ pub async fn verify_cose_async( } } + // check certificate revocation + check_ocsp_status(&cose_bytes, &data, validation_log)?; + // Check the signature, which needs to have the same `additional_data` provided, by // providing a closure that can do the verify operation. sign1.payload = Some(data.clone()); // restore payload @@ -845,7 +951,7 @@ pub async fn verify_cose_async( } #[allow(unused_variables)] -pub fn get_signing_info( +pub(crate) fn get_signing_info( cose_bytes: &[u8], data: &[u8], validation_log: &mut impl StatusTracker, @@ -876,12 +982,13 @@ pub fn get_signing_info( #[cfg(target_arch = "wasm32")] { ValidationInfo { - issuer_org, - date, alg, + date, + cert_serial_number, + issuer_org, validated: false, cert_chain: Vec::new(), - cert_serial_number, + revocation_status: None, } } #[cfg(not(target_arch = "wasm32"))] @@ -901,6 +1008,7 @@ pub fn get_signing_info( validated: false, cert_chain: certs, cert_serial_number, + revocation_status: None, } } } @@ -948,14 +1056,16 @@ pub fn verify_cose( // get the public key der let der_bytes = &certs[0]; + let time_stamp_info = get_timestamp_info(&sign1, data); + if !signature_only { // verify certs - match get_timestamp_info(&sign1, data) { - Ok(tst_info) => check_cert(alg, der_bytes, validation_log, Some(&tst_info))?, + match &time_stamp_info { + Ok(tst_info) => check_cert(der_bytes, validation_log, Some(tst_info))?, Err(e) => { // log timestamp errors match e { - Error::NotFound => check_cert(alg, der_bytes, validation_log, None)?, + Error::NotFound => check_cert(der_bytes, validation_log, None)?, Error::CoseTimeStampMismatch => { let log_item = log_item!( "Cose_Sign1", @@ -984,6 +1094,9 @@ pub fn verify_cose( } } + // check certificate revocation + check_ocsp_status(cose_bytes, data, validation_log)?; + // Check the signature, which needs to have the same `additional_data` provided, by // providing a closure that can do the verify operation. sign1.verify_signature(additional_data, |sig, verify_data| -> Result<()> { @@ -1002,6 +1115,8 @@ pub fn verify_cose( // return cert chain result.cert_chain = dump_cert_chain(&certs, None)?; + + result.revocation_status = Some(true); } // Note: not adding validation_log entry here since caller will supply claim specific info to log Ok(()) @@ -1099,7 +1214,9 @@ pub mod tests { use sha2::digest::generic_array::sequence::Shorten; use super::*; - use crate::{status_tracker::DetailedStatusTracker, SigningAlg}; + use crate::{ + signer::ConfigurableSigner, status_tracker::DetailedStatusTracker, Signer, SigningAlg, + }; #[test] #[cfg(feature = "file_io")] @@ -1113,7 +1230,7 @@ pub mod tests { if let Ok(signcert) = openssl::x509::X509::from_pem(&expired_cert) { let der_bytes = signcert.to_der().unwrap(); - assert!(check_cert(SigningAlg::Ps256, &der_bytes, &mut validation_log, None).is_err()); + assert!(check_cert(&der_bytes, &mut validation_log, None).is_err()); assert!(!validation_log.get_log().is_empty()); @@ -1203,22 +1320,22 @@ pub mod tests { if let Ok(signcert) = openssl::x509::X509::from_pem(&es256_cert) { let der_bytes = signcert.to_der().unwrap(); - assert!(check_cert(SigningAlg::Es256, &der_bytes, &mut validation_log, None).is_ok()); + assert!(check_cert(&der_bytes, &mut validation_log, None).is_ok()); } if let Ok(signcert) = openssl::x509::X509::from_pem(&es384_cert) { let der_bytes = signcert.to_der().unwrap(); - assert!(check_cert(SigningAlg::Es384, &der_bytes, &mut validation_log, None).is_ok()); + assert!(check_cert(&der_bytes, &mut validation_log, None).is_ok()); } if let Ok(signcert) = openssl::x509::X509::from_pem(&es512_cert) { let der_bytes = signcert.to_der().unwrap(); - assert!(check_cert(SigningAlg::Es512, &der_bytes, &mut validation_log, None).is_ok()); + assert!(check_cert(&der_bytes, &mut validation_log, None).is_ok()); } if let Ok(signcert) = openssl::x509::X509::from_pem(&rsa_pss256_cert) { let der_bytes = signcert.to_der().unwrap(); - assert!(check_cert(SigningAlg::Ps256, &der_bytes, &mut validation_log, None).is_ok()); + assert!(check_cert(&der_bytes, &mut validation_log, None).is_ok()); } } @@ -1244,4 +1361,68 @@ pub mod tests { assert_eq!(signing_time, None); } + #[test] + #[cfg(feature = "openssl_sign")] + fn test_stapled_ocsp() { + let mut validation_log = DetailedStatusTracker::new(); + + let mut claim = crate::claim::Claim::new("ocsp_sign_test", Some("contentauth")); + claim.build().unwrap(); + + let claim_bytes = claim.data().unwrap(); + + let sign_cert = include_bytes!("../tests/fixtures/certs/ps256.pub").to_vec(); + let pem_key = include_bytes!("../tests/fixtures/certs/ps256.pem").to_vec(); + let ocsp_rsp_data = include_bytes!("../tests/fixtures/ocsp_good.data"); + + let signer = crate::openssl::RsaSigner::from_signcert_and_pkey( + &sign_cert, + &pem_key, + SigningAlg::Ps256, + None, + ) + .unwrap(); + + // create a test signer that supports stapling + struct OcspSigner { + pub signer: Box<dyn crate::Signer>, + pub ocsp_rsp: Vec<u8>, + } + impl crate::Signer for OcspSigner { + fn sign(&self, data: &[u8]) -> Result<Vec<u8>> { + self.signer.sign(data) + } + + fn alg(&self) -> SigningAlg { + SigningAlg::Ps256 + } + + fn certs(&self) -> Result<Vec<Vec<u8>>> { + self.signer.certs() + } + + fn reserve_size(&self) -> usize { + self.signer.reserve_size() + } + + fn ocsp_val(&self) -> Option<Vec<u8>> { + Some(self.ocsp_rsp.clone()) + } + } + + let ocsp_signer = OcspSigner { + signer: Box::new(signer), + ocsp_rsp: ocsp_rsp_data.to_vec(), + }; + + // sign and staple + let cose_bytes = + crate::cose_sign::sign_claim(&claim_bytes, &ocsp_signer, ocsp_signer.reserve_size()) + .unwrap(); + + let cose_sign1 = get_cose_sign1(&cose_bytes, &claim_bytes, &mut validation_log).unwrap(); + let ocsp_stapled = get_ocsp_der(&cose_sign1).unwrap(); + + assert_eq!(ocsp_rsp_data, ocsp_stapled.as_slice()); + } } diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs @@ -114,7 +114,6 @@ mod signing_alg; #[cfg(feature = "file_io")] pub use ingredient::{DefaultOptions, IngredientOptions}; pub use signing_alg::{SigningAlg, UnknownAlgorithmError}; -#[cfg(feature = "openssl_sign")] pub(crate) mod ocsp_utils; #[cfg(feature = "openssl_sign")] mod openssl; diff --git a/sdk/src/manifest_store_report.rs b/sdk/src/manifest_store_report.rs @@ -102,7 +102,12 @@ impl ManifestStoreReport { let store = Store::load_from_asset(path.as_ref(), true, &mut validation_log)?; let cert_str = store.get_provenance_cert_chain()?; - println!("{cert_str}"); + println!("{cert_str}\n\n"); + + if let Some(ocsp_info) = store.get_ocsp_status() { + println!("{ocsp_info}"); + } + Ok(()) } diff --git a/sdk/src/ocsp_utils.rs b/sdk/src/ocsp_utils.rs @@ -11,27 +11,23 @@ // specific language governing permissions and limitations under // each license. -use std::io::Read; - use chrono::{DateTime, NaiveDateTime, Utc}; use conv::ConvUtil; -use openssl::ocsp::{self, OcspBasicResponse, OcspCertStatus, OcspRevokedStatus}; +use rasn_ocsp::{BasicOcspResponse, CertStatus, OcspResponse, OcspResponseStatus}; +use rasn_pkix::CrlReason; use crate::{ - error::{Error, Result}, - openssl::check_chain_order_der, - status_tracker::{log_item, StatusTracker}, - utils::base64, - validation_status, + status_tracker::{log_item, DetailedStatusTracker, StatusTracker}, + validation_status, Error, Result, }; -const DATE_FMT: &str = "%b %d %H:%M:%S %Y %Z"; - /// OcspData - struct to contain the OCSPResponse DER and the time /// for the next OCSP check -pub struct OcspData { +pub(crate) struct OcspData { pub ocsp_der: Vec<u8>, pub next_update: DateTime<Utc>, + pub revoked_at: Option<DateTime<Utc>>, + pub ocsp_certs: Option<Vec<Vec<u8>>>, } impl OcspData { @@ -39,6 +35,8 @@ impl OcspData { OcspData { ocsp_der: Vec::new(), next_update: Utc::now(), + revoked_at: None, + ocsp_certs: None, } } } @@ -48,54 +46,120 @@ impl Default for OcspData { Self { ocsp_der: Vec::new(), next_update: Utc::now(), + revoked_at: None, + ocsp_certs: None, } } } -fn get_ocsp_responders(cert_der: &[u8]) -> Option<Vec<String>> { - let cert = openssl::x509::X509::from_der(cert_der).ok()?; +#[cfg(feature = "fetch_ocsp_response")] +fn extract_aia_responders(cert: &x509_parser::certificate::X509Certificate) -> Option<Vec<String>> { + use x509_parser::der_parser::{oid, Oid}; + + const AD_OCSP_OID: Oid<'static> = oid!(1.3.6 .1 .5 .5 .7 .48 .1); + const AUTHORITY_INFO_ACCESS_OID: Oid<'static> = oid!(1.3.6 .1 .5 .5 .7 .1 .1); + + let em = cert.extensions_map().ok()?; - if let Ok(stack) = cert.ocsp_responders() { - let mut output: Vec<String> = Vec::new(); - for responder in stack { - output.push(responder.to_string()); + let aia_extension = em.get(&AUTHORITY_INFO_ACCESS_OID)?; + + match aia_extension.parsed_extension() { + x509_parser::extensions::ParsedExtension::AuthorityInfoAccess(aia) => { + let mut output = Vec::new(); + + for ad in &aia.accessdescs { + if let x509_parser::extensions::GeneralName::URI(uri) = ad.access_location { + if ad.access_method == AD_OCSP_OID { + output.push(uri.to_string()) + } + } + } + Some(output) } - Some(output) - } else { - None + _ => None, } } /// Check the supplied cert chain for an OCSP responder in the end-entity cert. If found it will attempt to -/// retrieve the OCSPResponse. -/// If successful returns OcspData containing the DER encoded OCSPResponse and the DateTime for when this cached response should -/// be refreshed. None otherwise. -pub fn get_ocsp_response(certs: &[Vec<u8>]) -> Option<OcspData> { - //} Option<DateTime<Utc>>) { - // must be in hierarchical order for this to work - if certs.len() < 2 || !check_chain_order_der(certs) { +/// retrieve the OCSPResponse. If successful returns OcspData containing the DER encoded OCSPResponse and +/// the DateTime for when this cached response should be refreshed, and the OCSP signer certificate chain. +/// None otherwise. +#[cfg(feature = "fetch_ocsp_response")] +pub(crate) fn fetch_ocsp_response(certs: &[Vec<u8>]) -> Option<Vec<u8>> { + use std::io::Read; + + use rasn::prelude::*; + use rasn_pkix::Certificate; + use x509_parser::prelude::*; + + // must have minimal chain in hierarchical order + if certs.len() < 2 { return None; } - if let Some(responders) = get_ocsp_responders(&certs[0]) { + let (_rem, cert) = X509Certificate::from_der(&certs[0]).ok()?; + + if let Some(responders) = extract_aia_responders(&cert) { + let sha1_oid = rasn::types::Oid::new(&[1, 3, 14, 3, 2, 26])?; // Sha1 Oid + let alg = rasn::types::ObjectIdentifier::from(sha1_oid); + + let sha1_ai = rasn_pkix::AlgorithmIdentifier { + algorithm: alg, + parameters: Some(Any::new(rasn::der::encode(&()).ok()?)), /* many OCSP responders expect this to be NULL not None */ + }; + for r in responders { let url = url::Url::parse(&r).ok()?; - let subject = openssl::x509::X509::from_der(&certs[0]).ok()?; - let issuer = openssl::x509::X509::from_der(&certs[1]).ok()?; + let subject: Certificate = rasn::der::decode(&certs[0]).ok()?; + let issuer: Certificate = rasn::der::decode(&certs[1]).ok()?; + + let issuer_name_raw = rasn::der::encode(&issuer.tbs_certificate.subject).ok()?; + let issuer_key_raw = &issuer + .tbs_certificate + .subject_public_key_info + .subject_public_key + .as_raw_slice(); + + let issuer_name_hash = + OctetString::from(crate::hash_utils::hash_sha1(&issuer_name_raw)); + let issuer_key_hash = OctetString::from(crate::hash_utils::hash_sha1(issuer_key_raw)); + let serial_number = subject.tbs_certificate.serial_number; + + // build request structures + + let req_cert = rasn_ocsp::CertId { + hash_algorithm: sha1_ai.clone(), + issuer_name_hash, + issuer_key_hash, + serial_number, + }; - let cert_id = openssl::ocsp::OcspCertId::from_cert( - openssl::hash::MessageDigest::sha1(), - &subject, - &issuer, - ) - .ok()?; + let ocsp_req = rasn_ocsp::Request { + req_cert, + single_request_extensions: None, + }; - let mut ocsp_req = ocsp::OcspRequest::new().ok()?; - ocsp_req.add_id(cert_id).ok()?; - let request_str = base64::encode(&ocsp_req.to_der().ok()?); + let request_list = vec![ocsp_req]; + + let tbs_request = rasn_ocsp::TbsRequest { + version: rasn_ocsp::Version::parse_bytes(b"0", 16)?, + requestor_name: None, + request_list, + request_extensions: None, + }; + + let ocsp_request = rasn_ocsp::OcspRequest { + tbs_request, + optional_signature: None, + }; + + // build query param + let request_der = rasn::der::encode(&ocsp_request).ok()?; + let request_str = crate::utils::base64::encode(&request_der); let req_url = url.join(&request_str).ok()?; + // fetch OCSP response let request = ureq::get(req_url.as_str()); let response = if let Some(host) = url.host() { request.set("Host", &host.to_string()).call().ok()? // for responders that don't support http 1.0 @@ -107,7 +171,7 @@ pub fn get_ocsp_response(certs: &[Vec<u8>]) -> Option<OcspData> { let len = response .header("Content-Length") .and_then(|s| s.parse::<usize>().ok()) - .unwrap_or(2000); + .unwrap_or(10000); let mut ocsp_rsp: Vec<u8> = Vec::with_capacity(len); @@ -117,182 +181,267 @@ pub fn get_ocsp_response(certs: &[Vec<u8>]) -> Option<OcspData> { .read_to_end(&mut ocsp_rsp) .ok()?; - // sanity check response - let ocsp_response = ocsp::OcspResponse::from_der(&ocsp_rsp).ok()?; - if ocsp_response.status() == ocsp::OcspResponseStatus::SUCCESSFUL { - if let Ok(basic_response) = ocsp_response.basic() { - if let Some(cert_status) = - get_end_entity_cert_status(certs, &basic_response) - { - if cert_status.status == OcspCertStatus::GOOD - || cert_status.status == OcspCertStatus::REVOKED - && cert_status.reason == OcspRevokedStatus::REMOVE_FROM_CRL - { - let next_update = NaiveDateTime::parse_from_str( - &cert_status.next_update.to_string(), + return Some(ocsp_rsp); + } + } + } + None +} +// check to OCSP response with optional signing time (if available) +// Returns - returns OcspData unless their is a structural error in the response. +pub(crate) fn check_ocsp_response( + ocsp_response_der: &[u8], + signing_time: Option<DateTime<Utc>>, + validation_log_out: &mut impl StatusTracker, +) -> Result<OcspData> { + const DATE_FMT: &str = "%Y-%m-%d %H:%M:%S %Z"; + + let mut validation_log = DetailedStatusTracker::default(); + + let mut output = OcspData::new(); + output.ocsp_der = ocsp_response_der.to_vec(); + let mut found_good = false; + + if let Ok(ocsp_response) = rasn::der::decode::<OcspResponse>(ocsp_response_der) { + if ocsp_response.status == OcspResponseStatus::Successful { + if let Some(response_bytes) = ocsp_response.bytes { + if let Ok(basic_response) = + rasn::der::decode::<BasicOcspResponse>(&response_bytes.response) + { + let response_data = &basic_response.tbs_response_data; + + // get OCSP cert chain if available + if let Some(ocsp_certs) = &basic_response.certs { + let mut cert_der_vec = Vec::new(); + + for ocsp_cert in ocsp_certs { + let cert_der = rasn::der::encode(ocsp_cert) + .map_err(|_e| Error::CoseInvalidCert)?; + cert_der_vec.push(cert_der); + } + + if output.ocsp_certs.is_none() { + output.ocsp_certs = Some(cert_der_vec); + } + } + + for single_response in &response_data.responses { + let cert_status = &single_response.cert_status; + + match cert_status { + CertStatus::Good => { + // check cert range against signing time + let this_update = NaiveDateTime::parse_from_str( + &single_response.this_update.to_string(), DATE_FMT, ) - .ok()?; - - let output = OcspData { - ocsp_der: ocsp_rsp, - next_update: DateTime::from_naive_utc_and_offset( - next_update, - chrono::Utc, - ), + .map_err(|_e| Error::CoseInvalidCert)? + .timestamp(); + + let next_update = if let Some(nu) = &single_response.next_update { + NaiveDateTime::parse_from_str(&nu.to_string(), DATE_FMT) + .map_err(|_e| Error::CoseInvalidCert)? + .timestamp() + } else { + this_update + }; + + // check to see if we are within range or current time within range + let in_range = if let Some(st) = signing_time { + st.timestamp() < this_update + || (st.timestamp() >= this_update + && st.timestamp() <= next_update) + } else { + // no timestamp so check against current time + // use instant to avoid wasm issues + let now_f64 = instant::now() / 1000.0; + let now: i64 = now_f64.approx_as().map_err(|_e| { + Error::BadParam("system time invalid".to_string()) + })?; + + now >= this_update && now <= next_update }; - return Some(output); + if let Some(nu) = &single_response.next_update { + let nu_utc = nu.naive_utc(); + output.next_update = + DateTime::from_naive_utc_and_offset(nu_utc, Utc); + } + + if !in_range { + let log_item = log_item!( + "OCSP_RESPONSE", + "certificate revoked", + "check_ocsp_response" + ) + .error(Error::CoseCertRevoked) + .validation_status( + validation_status::SIGNING_CREDENTIAL_REVOKED, + ); + validation_log.log_silent(log_item); + } else { + found_good = true; + break; // found good match so break + } } + CertStatus::Revoked(revoked_info) => { + if let Some(reason) = revoked_info.revocation_reason { + if reason == CrlReason::RemoveFromCRL { + // if it was revoked check if was revoked after signing time + let revocation_time = &revoked_info.revocation_time; + // check cert range against signing time + let revoked_at = NaiveDateTime::parse_from_str( + &revocation_time.to_string(), + DATE_FMT, + ) + .map_err(|_e| Error::CoseInvalidCert)? + .timestamp(); + + // check to see if we are within range or current time within range + let in_range = if let Some(st) = signing_time { + revoked_at > st.timestamp() + } else { + // no timestamp so check against current time + // use instant to avoid wasm issues + let now_f64 = instant::now() / 1000.0; + let now: i64 = now_f64.approx_as().map_err(|_e| { + Error::BadParam("system time invalid".to_string()) + })?; + + revoked_at > now + }; + + if !in_range { + let revoked_at_native = NaiveDateTime::parse_from_str( + &revocation_time.to_string(), + DATE_FMT, + ) + .map_err(|_e| Error::CoseInvalidCert)?; + + let utc_with_offset: DateTime<Utc> = + DateTime::from_naive_utc_and_offset( + revoked_at_native, + Utc, + ); + + let msg = format!( + "certificate revoked at: {}", + utc_with_offset + ); + let log_item = log_item!( + "OCSP_RESPONSE", + &msg, + "check_ocsp_response" + ) + .error(Error::CoseCertRevoked) + .validation_status( + validation_status::SIGNING_CREDENTIAL_REVOKED, + ); + validation_log.log_silent(log_item); + + output.revoked_at = + Some(DateTime::from_naive_utc_and_offset( + revoked_at_native, + Utc, + )); + } + } else { + let revoked_at_native = NaiveDateTime::parse_from_str( + &revoked_info.revocation_time.to_string(), + DATE_FMT, + ) + .map_err(|_e| Error::CoseInvalidCert)?; + + let utc_with_offset: DateTime<Utc> = + DateTime::from_naive_utc_and_offset( + revoked_at_native, + Utc, + ); + + let msg = + format!("certificate revoked at: {}", utc_with_offset); + let log_item = + log_item!("OCSP_RESPONSE", &msg, "check_ocsp_response") + .error(Error::CoseCertRevoked) + .validation_status( + validation_status::SIGNING_CREDENTIAL_REVOKED, + ); + validation_log.log_silent(log_item); + + output.revoked_at = + Some(DateTime::from_naive_utc_and_offset( + revoked_at_native, + Utc, + )); + } + } else { + let log_item = log_item!( + "OCSP_RESPONSE", + "certificate revoked", + "check_ocsp_response" + ) + .error(Error::CoseCertRevoked) + .validation_status( + validation_status::SIGNING_CREDENTIAL_REVOKED, + ); + validation_log.log_silent(log_item); + } + } + CertStatus::Unknown(_) => return Err(Error::UnsupportedType), /* noop for this case */ } } } } } } - None + // Per the spec if we cannot interpret the OCSP data treat it as if it did not exist + if !found_good { + validation_log_out + .get_log_mut() + .append(validation_log.get_log_mut()); + } + + Ok(output) } -// find the certificate to check -fn get_end_entity_cert_status<'a>( - certs: &[Vec<u8>], - basic_response: &'a OcspBasicResponse, -) -> Option<ocsp::OcspStatus<'a>> { - if certs.len() < 2 || !check_chain_order_der(certs) { - return None; - } +#[cfg(test)] +pub mod tests { + #![allow(clippy::panic)] + #![allow(clippy::unwrap_used)] - let subject = openssl::x509::X509::from_der(&certs[0]).ok()?; - let issuer = openssl::x509::X509::from_der(&certs[1]).ok()?; + use chrono::TimeZone; - let cert_id = openssl::ocsp::OcspCertId::from_cert( - openssl::hash::MessageDigest::sha1(), - &subject, - &issuer, - ) - .ok()?; + use super::*; + use crate::status_tracker::report_split_errors; + #[test] + fn test_good_response() { + let rsp_data = include_bytes!("../tests/fixtures/ocsp_good.data"); - basic_response.find_status(&cert_id) -} + let mut validation_log = DetailedStatusTracker::default(); -// check to OCSP response against the supplied certs and signing time (if available) -// Returns - empty result on success -pub(crate) fn _check_ocsp_response( - ocsp_response_der: &[u8], - certs: &[Vec<u8>], - signing_time: Option<chrono::DateTime<chrono::Utc>>, - validation_log: &mut impl StatusTracker, -) -> Result<()> { - if certs.len() < 2 || !check_chain_order_der(certs) { - return Err(Error::BadParam("certs vector not valid".to_string())); - } + let test_time = Utc.with_ymd_and_hms(2023, 2, 1, 8, 0, 0).unwrap(); - if let Ok(ocsp_response) = ocsp::OcspResponse::from_der(ocsp_response_der) { - if ocsp_response.status() == ocsp::OcspResponseStatus::SUCCESSFUL { - if let Ok(basic_response) = ocsp_response.basic() { - if let Some(cert_status) = get_end_entity_cert_status(certs, &basic_response) { - if cert_status.status == OcspCertStatus::GOOD - || cert_status.status == OcspCertStatus::REVOKED - && cert_status.reason == OcspRevokedStatus::REMOVE_FROM_CRL - { - // check cert range against signing time - let this_update = NaiveDateTime::parse_from_str( - &cert_status.this_update.to_string(), - DATE_FMT, - ) - .map_err(|_e| Error::CoseInvalidCert)? - .timestamp(); - let next_update = NaiveDateTime::parse_from_str( - &cert_status.next_update.to_string(), - DATE_FMT, - ) - .map_err(|_e| Error::CoseInvalidCert)? - .timestamp(); - - // check to see if we are within range or current time within range - let in_range = if let Some(st) = signing_time { - println!("{}, {}, {}", this_update, next_update, st.timestamp()); - st.timestamp() >= this_update && st.timestamp() <= next_update - } else { - // no timestamp so check against current time - // use instant to avoid wasm issues - let now_f64 = instant::now() / 1000.0; - let now: i64 = now_f64 - .approx_as::<i64>() - .map_err(|_e| Error::BadParam("system time invalid".to_string()))?; - - now >= this_update && now <= next_update - }; - - if !in_range { - let log_item = log_item!( - "OCSP_RESPONSE", - "certificate revoked", - "check_ocsp_response" - ) - .error(Error::CoseCertRevoked) - .validation_status(validation_status::SIGNING_CREDENTIAL_REVOKED); - validation_log.log_silent(log_item); - - return Err(Error::CoseCertRevoked); - } - } else if cert_status.status == OcspCertStatus::REVOKED - && cert_status.reason != OcspRevokedStatus::REMOVE_FROM_CRL - { - // if it was revoked check if was revoked after signing time - if let Some(revocation_time) = cert_status.revocation_time { - // check cert range against signing time - let revoked_at = NaiveDateTime::parse_from_str( - &revocation_time.to_string(), - DATE_FMT, - ) - .map_err(|_e| Error::CoseInvalidCert)? - .timestamp(); - - // check to see if we are within range or current time within range - let in_range = if let Some(st) = signing_time { - revoked_at > st.timestamp() - } else { - // no timestamp so check against current time - // use instant to avoid wasm issues - let now_f64 = instant::now() / 1000.0; - let now: i64 = now_f64.approx_as::<i64>().map_err(|_e| { - Error::BadParam("system time invalid".to_string()) - })?; - - revoked_at > now - }; - - if !in_range { - let log_item = log_item!( - "OCSP_RESPONSE", - "certificate revoked", - "check_ocsp_response" - ) - .error(Error::CoseCertRevoked) - .validation_status(validation_status::SIGNING_CREDENTIAL_REVOKED); - validation_log.log_silent(log_item); + let ocsp_data = + check_ocsp_response(rsp_data, Some(test_time), &mut validation_log).unwrap(); - return Err(Error::CoseCertRevoked); - } - } else { - let log_item = log_item!( - "OCSP_RESPONSE", - "certificate revoked", - "check_ocsp_response" - ) - .error(Error::CoseCertRevoked) - .validation_status(validation_status::SIGNING_CREDENTIAL_REVOKED); - validation_log.log_silent(log_item); - - return Err(Error::CoseCertRevoked); - } - } - } - }; - } + assert!(ocsp_data.revoked_at.is_none()); + assert!(ocsp_data.ocsp_certs.is_some()); } - // Per the spec if we cannot interpret the OCSP data treat it as if it did not exist - Ok(()) + #[test] + fn test_revoked_response() { + let rsp_data = include_bytes!("../tests/fixtures/ocsp_revoked.data"); + + let mut validation_log = DetailedStatusTracker::default(); + + let test_time = Utc.with_ymd_and_hms(2023, 2, 1, 8, 0, 0).unwrap(); + + let ocsp_data = + check_ocsp_response(rsp_data, Some(test_time), &mut validation_log).unwrap(); + + let errors = report_split_errors(validation_log.get_log_mut()); + + assert!(ocsp_data.revoked_at.is_some()); + assert!(!errors.is_empty()); + } } diff --git a/sdk/src/openssl/mod.rs b/sdk/src/openssl/mod.rs @@ -77,6 +77,7 @@ pub(crate) fn check_chain_order(certs: &[X509]) -> bool { } #[cfg(feature = "openssl_sign")] +#[allow(dead_code)] pub(crate) fn check_chain_order_der(cert_ders: &[Vec<u8>]) -> bool { let mut certs: Vec<X509> = Vec::new(); for cert_der in cert_ders { diff --git a/sdk/src/openssl/rsa_signer.rs b/sdk/src/openssl/rsa_signer.rs @@ -22,11 +22,7 @@ use openssl::{ }; use super::check_chain_order; -use crate::{ - ocsp_utils::{get_ocsp_response, OcspData}, - signer::ConfigurableSigner, - Error, Result, Signer, SigningAlg, -}; +use crate::{ocsp_utils::OcspData, signer::ConfigurableSigner, Error, Result, Signer, SigningAlg}; /// Implements `Signer` trait using OpenSSL's implementation of /// SHA256 + RSA encryption. @@ -44,7 +40,10 @@ pub struct RsaSigner { } impl RsaSigner { - pub fn update_ocsp(&self) { + // Sample of OCSP stapling while signing. This code is only for demo purposes and not for + // production use since there is no caching in the SDK and fetching is expensive. This is behind the + // feature flag 'psxxx_ocsp_stapling_experimental' + fn update_ocsp(&self) { // do we need an update let now = chrono::offset::Utc::now(); @@ -55,11 +54,19 @@ impl RsaSigner { if now < next_update { return; } - - if let Ok(certs) = self.certs() { - if let Some(ocsp_rsp) = get_ocsp_response(&certs) { - self.ocsp_size.set(ocsp_rsp.ocsp_der.len()); - self.ocsp_rsp.set(ocsp_rsp); + #[cfg(feature = "psxxx_ocsp_stapling_experimental")] + { + if let Ok(certs) = self.certs() { + if let Some(ocsp_rsp) = crate::ocsp_utils::fetch_ocsp_response(&certs) { + self.ocsp_size.set(ocsp_rsp.len()); + let mut validation_log = + crate::status_tracker::DetailedStatusTracker::default(); + if let Ok(ocsp_data) = + crate::ocsp_utils::check_ocsp_response(&ocsp_rsp, None, &mut validation_log) + { + self.ocsp_rsp.set(ocsp_data); + } + } } } } diff --git a/sdk/src/store.rs b/sdk/src/store.rs @@ -33,7 +33,7 @@ use crate::{ }, claim::{Claim, ClaimAssertion, ClaimAssetData}, cose_sign::cose_sign, - cose_validator::verify_cose, + cose_validator::{check_ocsp_status, verify_cose}, error::{Error, Result}, hash_utils::{hash_by_alg, vec_compare, verify_by_alg}, jumbf::{ @@ -378,7 +378,7 @@ impl Store { } /// Return certificate chain for the provenance claim - pub fn get_provenance_cert_chain(&self) -> Result<String> { + pub(crate) fn get_provenance_cert_chain(&self) -> Result<String> { let claim = self.provenance_claim().ok_or(Error::ProvenanceMissing)?; match claim.get_cert_chain() { @@ -387,6 +387,37 @@ impl Store { } } + /// Return OCSP info if available + // Currently only called from manifest_store behind a feature flag but this is allowable + // anywhere so allow dead code here for future uses to compile + #[allow(dead_code)] + pub(crate) fn get_ocsp_status(&self) -> Option<String> { + let claim = self + .provenance_claim() + .ok_or(Error::ProvenanceMissing) + .ok()?; + + let sig = claim.signature_val(); + let data = claim.data().ok()?; + let mut validation_log = OneShotStatusTracker::new(); + + if let Ok(info) = check_ocsp_status(sig, &data, &mut validation_log) { + if let Some(revoked_at) = &info.revoked_at { + Some(format!( + "Certificate Status: Revoked, revoked at: {}", + revoked_at + )) + } else { + Some(format!( + "Certificate Status: Good, next update: {}", + info.next_update + )) + } + } else { + None + } + } + /// Sign the claim and return signature. pub fn sign_claim( &self, @@ -4306,23 +4337,6 @@ pub mod tests { ); } - /* enable when we enable OCSP validation - #[test] - #[cfg(feature = "file_io")] - fn test_ocsp() { - let ap = fixture_path("ocsp_test.png"); - let mut report = DetailedStatusTracker::new(); - let _r = Store::load_from_asset(&ap, true, &mut report); - - println!( - "Error report for {}: {:?}", - ap.display(), - report.get_log() - ); - assert!(report.get_log().is_empty()); - } - */ - #[test] fn test_display() { let ap = fixture_path("CA.jpg"); diff --git a/sdk/src/utils/hash_utils.rs b/sdk/src/utils/hash_utils.rs @@ -22,7 +22,7 @@ use std::{ use log::warn; // multihash versions use multibase::{decode, encode}; -use multihash::{wrap, Code, Multihash, Sha2_256, Sha2_512, Sha3_256, Sha3_384, Sha3_512}; +use multihash::{wrap, Code, Multihash, Sha1, Sha2_256, Sha2_512, Sha3_256, Sha3_384, Sha3_512}; use range_set::RangeSet; use serde::{Deserialize, Serialize}; // direct sha functions @@ -466,6 +466,12 @@ pub fn hash256(data: &[u8]) -> String { encode(multibase::Base::Base64, wrapped.as_bytes()) } +pub fn hash_sha1(data: &[u8]) -> Vec<u8> { + let mh = Sha1::digest(data); + let digest = mh.digest(); + digest.to_vec() +} + /// Verify muiltihash against input data. True if match, /// false if no match or unsupported. The hash value should be /// be multibase encoded string. diff --git a/sdk/src/validator.rs b/sdk/src/validator.rs @@ -26,6 +26,7 @@ pub struct ValidationInfo { pub issuer_org: Option<String>, pub validated: bool, // claim signature is valid pub cert_chain: Vec<u8>, // certificate chain used to validate signature + pub revocation_status: Option<bool>, } /// Trait to support validating a signature against the provided data diff --git a/sdk/tests/fixtures/ocsp_good.data b/sdk/tests/fixtures/ocsp_good.data Binary files differ. diff --git a/sdk/tests/fixtures/ocsp_revoked.data b/sdk/tests/fixtures/ocsp_revoked.data Binary files differ.