diff --git a/sdk/examples/v2show.rs b/sdk/examples/v2show.rs index 62c2fb909..ef96221c3 100644 --- a/sdk/examples/v2show.rs +++ b/sdk/examples/v2show.rs @@ -57,6 +57,7 @@ fn main() -> Result<()> { Err(e) => Err(e), }?; println!("{reader}"); + //println!("\n\n{:#?}", reader); } else { println!("Prints a manifest report (requires a file path argument)") } diff --git a/sdk/src/claim.rs b/sdk/src/claim.rs index bb5e0f0eb..c2a28bb09 100644 --- a/sdk/src/claim.rs +++ b/sdk/src/claim.rs @@ -18,7 +18,6 @@ use std::{collections::HashMap, fmt}; use async_generic::async_generic; use c2pa_crypto::base64; use c2pa_status_tracker::{log_item, OneShotStatusTracker, StatusTracker}; -use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use serde_json::{json, Map, Value}; use uuid::Uuid; @@ -34,10 +33,7 @@ use crate::{ AssetType, BmffHash, BoxHash, DataBox, DataHash, Metadata, }, asset_io::CAIRead, - cose_validator::{ - check_ocsp_status, check_ocsp_status_async, get_signing_info, get_signing_info_async, - verify_cose, verify_cose_async, - }, + cose_validator::{get_signing_info, get_signing_info_async, verify_cose, verify_cose_async}, error::{Error, Result}, hashed_uri::HashedUri, jumbf::{ @@ -996,42 +992,17 @@ impl Claim { } } - /// Return the signing date and time for this claim, if there is one. - pub fn signing_time(&self) -> Option> { - if let Some(validation_data) = self.signature_info() { - validation_data.date - } else { - None - } - } - - /// Return the signing issuer for this claim, if there is one. - pub fn signing_issuer(&self) -> Option { - if let Some(validation_data) = self.signature_info() { - validation_data.issuer_org - } else { - None - } - } - - /// Return the cert's serial number, if there is one. - pub fn signing_cert_serial(&self) -> Option { - self.signature_info() - .and_then(|validation_info| validation_info.cert_serial_number) - .map(|serial| serial.to_string()) - } - /// Return information about the signature #[async_generic] - pub fn signature_info(&self) -> Option { + pub(crate) fn signature_info(&self, th: &dyn TrustHandlerConfig) -> Option { let sig = self.signature_val(); let data = self.data().ok()?; let mut validation_log = OneShotStatusTracker::default(); if _sync { - Some(get_signing_info(sig, &data, &mut validation_log)) + Some(get_signing_info(sig, &data, th, &mut validation_log)) } else { - Some(get_signing_info_async(sig, &data, &mut validation_log).await) + Some(get_signing_info_async(sig, &data, th, &mut validation_log).await) } } @@ -1070,9 +1041,6 @@ impl Claim { .failure(validation_log, Error::ClaimMissingSignatureBox)?; } - // check certificate revocation - check_ocsp_status_async(&sig, &data, th, validation_log).await?; - let verified = verify_cose_async(sig, data, additional_bytes, cert_check, th, validation_log).await; @@ -1115,21 +1083,19 @@ impl Claim { return Err(Error::ClaimDecoding); }; - // check certificate revocation - check_ocsp_status(sig, data, th, validation_log)?; - let verified = verify_cose(sig, data, &additional_bytes, cert_check, th, validation_log); Claim::verify_internal(claim, asset_data, is_provenance, verified, validation_log) } /// Get the signing certificate chain as PEM bytes - pub fn get_cert_chain(&self) -> Result> { + #[cfg(feature = "v1_api")] + pub(crate) fn get_cert_chain(&self, th: &dyn TrustHandlerConfig) -> Result> { let sig = self.signature_val(); let data = self.data()?; let mut validation_log = OneShotStatusTracker::default(); - let vi = get_signing_info(sig, &data, &mut validation_log); + let vi = get_signing_info(sig, &data, th, &mut validation_log); Ok(vi.cert_chain) } diff --git a/sdk/src/cose_validator.rs b/sdk/src/cose_validator.rs index 0192b40ae..a83ca0bd5 100644 --- a/sdk/src/cose_validator.rs +++ b/sdk/src/cose_validator.rs @@ -966,6 +966,8 @@ pub(crate) async fn verify_cose_async( th: &dyn TrustHandlerConfig, validation_log: &mut impl StatusTracker, ) -> Result { + let oscp_info = check_ocsp_status_async(&cose_bytes, &data, th, validation_log).await?; + let mut sign1 = get_cose_sign1(&cose_bytes, &data, validation_log)?; let alg = match get_signing_alg(&sign1) { @@ -1094,6 +1096,11 @@ pub(crate) async fn verify_cose_async( // return cert chain result.cert_chain = dump_cert_chain(&get_sign_certs(&sign1)?)?; + + // return revocation status and update info + result.revocation_date = oscp_info.revoked_at; + + result.ocsp_next_update = Some(oscp_info.next_update); } Ok(result) @@ -1104,6 +1111,7 @@ pub(crate) async fn verify_cose_async( pub(crate) fn get_signing_info( cose_bytes: &[u8], data: &[u8], + th: &dyn TrustHandlerConfig, validation_log: &mut impl StatusTracker, ) -> ValidationInfo { let mut date = None; @@ -1111,6 +1119,12 @@ pub(crate) fn get_signing_info( let mut alg: Option = None; let mut cert_serial_number = None; + let (revocation_date, ocsp_next_update) = + match check_ocsp_status(cose_bytes, data, th, validation_log) { + Ok(oscp_info) => (oscp_info.revoked_at, Some(oscp_info.next_update)), + Err(_) => (None, None), + }; + let sign1 = match get_cose_sign1(cose_bytes, data, validation_log) { Ok(sign1) => { // get the public key der @@ -1152,7 +1166,8 @@ pub(crate) fn get_signing_info( validated: false, cert_chain: certs, cert_serial_number, - revocation_status: None, + revocation_date, + ocsp_next_update, } } @@ -1169,6 +1184,9 @@ pub(crate) fn verify_cose( th: &dyn TrustHandlerConfig, validation_log: &mut impl StatusTracker, ) -> Result { + // check certificate revocation + let oscp_info = check_ocsp_status(cose_bytes, data, th, validation_log)?; + let sign1 = get_cose_sign1(cose_bytes, data, validation_log)?; let alg = match get_signing_alg(&sign1) { @@ -1283,7 +1301,9 @@ pub(crate) fn verify_cose( // return cert chain result.cert_chain = dump_cert_chain(&certs)?; - result.revocation_status = Some(true); + result.revocation_date = oscp_info.revoked_at; + + result.ocsp_next_update = Some(oscp_info.next_update); } // Note: not adding validation_log entry here since caller will supply claim specific info to log Ok(()) diff --git a/sdk/src/manifest.rs b/sdk/src/manifest.rs index 774798e6f..294055c12 100644 --- a/sdk/src/manifest.rs +++ b/sdk/src/manifest.rs @@ -702,9 +702,9 @@ impl Manifest { // get verified signing info let si = if _sync { - claim.signature_info() + claim.signature_info(store.trust_handler()) } else { - claim.signature_info_async().await + claim.signature_info_async(store.trust_handler()).await }; manifest.signature_info = match si { @@ -715,7 +715,8 @@ impl Manifest { cert_serial_number: signature_info.cert_serial_number.map(|s| s.to_string()), cert_chain: String::from_utf8(signature_info.cert_chain) .map_err(|_e| Error::CoseInvalidCert)?, - revocation_status: signature_info.revocation_status, + revocation_date: signature_info.revocation_date.map(|d| d.to_rfc3339()), + ocsp_next_update: signature_info.ocsp_next_update.map(|d| d.to_rfc3339()), }), None => None, }; @@ -1481,17 +1482,21 @@ pub struct SignatureInfo { #[serde(skip_serializing_if = "Option::is_none")] pub time: Option, - /// Revocation status of the certificate. + // The date the certificate was revoked. #[serde(skip_serializing_if = "Option::is_none")] - pub revocation_status: Option, + pub revocation_date: Option, + + // The date the OCSP response should be updated. + #[serde(skip_serializing_if = "Option::is_none")] + pub ocsp_next_update: Option, /// The cert chain for this claim. #[serde(skip)] // don't serialize this, let someone ask for it - cert_chain: String, + pub cert_chain: String, } impl SignatureInfo { - // returns the cert chain for this signature + /// Returns the cert chain for this signature. pub fn cert_chain(&self) -> &str { &self.cert_chain } diff --git a/sdk/src/manifest_store_report.rs b/sdk/src/manifest_store_report.rs index 100990404..bc0e67bed 100644 --- a/sdk/src/manifest_store_report.rs +++ b/sdk/src/manifest_store_report.rs @@ -45,7 +45,10 @@ impl ManifestStoreReport { pub(crate) fn from_store(store: &Store) -> Result { let mut manifests = HashMap::::new(); for claim in store.claims() { - manifests.insert(claim.label().to_owned(), ManifestReport::from_claim(claim)?); + manifests.insert( + claim.label().to_owned(), + ManifestReport::from_claim(claim, store)?, + ); } Ok(ManifestStoreReport { @@ -105,8 +108,18 @@ impl ManifestStoreReport { let cert_str = store.get_provenance_cert_chain()?; println!("{cert_str}\n\n"); - if let Some(ocsp_info) = store.get_ocsp_status() { - println!("{ocsp_info}"); + let claim_label = store + .provenance_path() + .ok_or(crate::Error::ProvenanceMissing)?; + if let Some(ocsp_info) = store.get_ocsp_status(&claim_label) { + if let Some(revoked_at) = &ocsp_info.revoked_at { + println!("Certificate Status: Revoked, revoked at: {}", revoked_at); + } else { + println!( + "Certificate Status: Good, next update: {}", + ocsp_info.next_update + ); + } } Ok(()) @@ -298,7 +311,7 @@ struct ManifestReport { } impl ManifestReport { - fn from_claim(claim: &Claim) -> Result { + fn from_claim(claim: &Claim, store: &Store) -> Result { let mut assertion_store = HashMap::::new(); let claim_assertions = claim.claim_assertion_store(); for claim_assertion in claim_assertions.iter() { @@ -329,11 +342,18 @@ impl ManifestReport { }) .collect(); - let signature = match claim.signature_info() { + let signature = match claim.signature_info(store.trust_handler()) { Some(info) => SignatureReport { alg: info.alg.map_or_else(String::new, |a| a.to_string()), issuer: info.issuer_org, time: info.date.map(|d| d.to_rfc3339()), + cert_serial_number: info.cert_serial_number.map(|s| s.to_string()), + cert_chain: Some( + String::from_utf8(info.cert_chain) + .map_err(|_e| crate::Error::CoseInvalidCert)?, + ), + revocation_date: info.revocation_date.map(|d| d.to_rfc3339()), + ocsp_next_update: info.ocsp_next_update.map(|d| d.to_rfc3339()), }, None => SignatureReport::default(), }; @@ -373,9 +393,26 @@ struct SignatureReport { // human readable issuing authority for this signature #[serde(skip_serializing_if = "Option::is_none")] issuer: Option, - // the time the signature was created + + /// The serial number of the certificate. + #[serde(skip_serializing_if = "Option::is_none")] + cert_serial_number: Option, + + /// The time the signature was created. #[serde(skip_serializing_if = "Option::is_none")] time: Option, + + // The date the certificate was revoked. + #[serde(skip_serializing_if = "Option::is_none")] + revocation_date: Option, + + // The date the OCSP response should be updated. + #[serde(skip_serializing_if = "Option::is_none")] + ocsp_next_update: Option, + + /// The cert chain for this claim. + #[serde(skip_serializing_if = "Option::is_none")] + cert_chain: Option, } // replace the value of any field in the json string with a given key with the string diff --git a/sdk/src/reader.rs b/sdk/src/reader.rs index 98f09029c..daa3e1486 100644 --- a/sdk/src/reader.rs +++ b/sdk/src/reader.rs @@ -252,6 +252,14 @@ impl Reader { self.manifest_store.to_string() } + /// Get the manifest store as a Detailed JSON string + pub fn detailed_json(&self) -> Result { + Ok(format!( + "{}", + ManifestStoreReport::from_store(self.manifest_store.store())? + )) + } + /// Get the [`ValidationStatus`] array of the manifest store if it exists. /// Call this method to check for validation errors. /// @@ -465,6 +473,18 @@ pub mod tests { // Ok(()) // } + #[test] + #[cfg(feature = "file_io")] + #[allow(clippy::unwrap_used)] + /// Test that the reader can validate a file with nested assertion errors + fn test_reader_to_folder() -> Result<()> { + let reader = Reader::from_file("tests/fixtures/CACAE-uri-CA.jpg")?; + //println!("{reader}"); + assert_eq!(reader.validation_status(), None); + reader.to_folder("../target/reader_folder")?; + Ok(()) + } + #[test] /// Test that the reader can validate a file with nested assertion errors fn test_reader_from_file_nested_errors() -> Result<()> { @@ -495,12 +515,13 @@ pub mod tests { #[test] #[cfg(feature = "file_io")] - /// Test that the reader can validate a file with nested assertion errors - fn test_reader_to_folder() -> Result<()> { - let reader = Reader::from_file("tests/fixtures/CACAE-uri-CA.jpg")?; - assert_eq!(reader.validation_status(), None); - reader.to_folder("../target/reader_folder")?; - assert!(std::path::Path::new("../target/reader_folder/manifest.json").exists()); + #[allow(clippy::unwrap_used)] + fn test_reader_detailed_json() -> Result<()> { + let reader = Reader::from_file("tests/fixtures/XCA.jpg")?; + assert!(reader.validation_status().is_some()); + let detailed_json = reader.detailed_json().unwrap(); + //println!("{}", detailed_json); + assert!(detailed_json.contains("assertion_store")); Ok(()) } } diff --git a/sdk/src/store.rs b/sdk/src/store.rs index 1d6260d8b..616ef3273 100644 --- a/sdk/src/store.rs +++ b/sdk/src/store.rs @@ -23,7 +23,7 @@ use std::{ use async_generic::async_generic; use async_recursion::async_recursion; -use c2pa_crypto::hash::sha256; +use c2pa_crypto::{hash::sha256, ocsp::OcspResponse}; use c2pa_status_tracker::{log_item, DetailedStatusTracker, OneShotStatusTracker, StatusTracker}; use log::error; @@ -200,7 +200,7 @@ impl Store { self.trust_handler.clear(); } - fn trust_handler(&self) -> &dyn TrustHandlerConfig { + pub(crate) fn trust_handler(&self) -> &dyn TrustHandlerConfig { self.trust_handler.as_ref() } @@ -448,7 +448,7 @@ impl Store { pub(crate) fn get_provenance_cert_chain(&self) -> Result { let claim = self.provenance_claim().ok_or(Error::ProvenanceMissing)?; - match claim.get_cert_chain() { + match claim.get_cert_chain(self.trust_handler()) { Ok(chain) => String::from_utf8(chain).map_err(|_e| Error::CoseInvalidCert), Err(e) => Err(e), } @@ -458,31 +458,19 @@ impl Store { // 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 { + pub(crate) fn get_ocsp_status(&self, manifest_label: &str) -> Option { let claim = self - .provenance_claim() - .ok_or(Error::ProvenanceMissing) + .get_claim(manifest_label) + .ok_or(Error::ClaimMissing { + label: manifest_label.to_string(), + }) .ok()?; let sig = claim.signature_val(); let data = claim.data().ok()?; let mut validation_log = OneShotStatusTracker::default(); - if let Ok(info) = check_ocsp_status(sig, &data, self.trust_handler(), &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 - } + check_ocsp_status(sig, &data, self.trust_handler(), &mut validation_log).ok() } /// Sign the claim and return signature. diff --git a/sdk/src/validator.rs b/sdk/src/validator.rs index dc06bc551..b84faed7c 100644 --- a/sdk/src/validator.rs +++ b/sdk/src/validator.rs @@ -23,5 +23,6 @@ pub struct ValidationInfo { pub issuer_org: Option, pub validated: bool, // claim signature is valid pub cert_chain: Vec, // certificate chain used to validate signature - pub revocation_status: Option, + pub revocation_date: Option>, + pub ocsp_next_update: Option>, } diff --git a/sdk/tests/common/compare_readers.rs b/sdk/tests/common/compare_readers.rs index e68b91ba6..461b36f17 100644 --- a/sdk/tests/common/compare_readers.rs +++ b/sdk/tests/common/compare_readers.rs @@ -167,6 +167,7 @@ fn compare_json_values( || path.ends_with(".instanceId") || path.ends_with(".identifier") || path.ends_with(".time") + || path.contains(".ocsp_next_update") || path.contains(".hash") || path.contains("claim_generator") // changes with every version (todo: get more specific) || val1.is_string() && val2.is_string() && val1.to_string().contains("urn:uuid:")) diff --git a/sdk/tests/known_good/C.json b/sdk/tests/known_good/C.json index 5f41e62cb..e1b4308a1 100644 --- a/sdk/tests/known_good/C.json +++ b/sdk/tests/known_good/C.json @@ -56,7 +56,8 @@ "alg": "Ps256", "issuer": "C2PA Test Signing Cert", "cert_serial_number": "720724073027128164015125666832722375746636448153", - "time": "2024-08-06T21:53:37+00:00" + "time": "2024-08-06T21:53:37+00:00", + "ocsp_next_update": "2024-11-20T00:15:54.189494+00:00" }, "label": "contentauth:urn:uuid:b2b1f7fa-b119-4de1-9c0d-c97fbea3f2c3" } diff --git a/sdk/tests/known_good/CA.json b/sdk/tests/known_good/CA.json index a746ee626..a5fed58fc 100644 --- a/sdk/tests/known_good/CA.json +++ b/sdk/tests/known_good/CA.json @@ -76,7 +76,8 @@ "alg": "Ps256", "issuer": "C2PA Test Signing Cert", "cert_serial_number": "720724073027128164015125666832722375746636448153", - "time": "2024-08-06T21:53:37+00:00" + "time": "2024-08-06T21:53:37+00:00", + "ocsp_next_update": "2024-11-20T00:15:54.189494+00:00" }, "label": "contentauth:urn:uuid:c2677d4b-0a93-4444-876f-ed2f2d40b8cf" } diff --git a/sdk/tests/known_good/CA_test.json b/sdk/tests/known_good/CA_test.json index 4c0f7f2fa..a064d860c 100644 --- a/sdk/tests/known_good/CA_test.json +++ b/sdk/tests/known_good/CA_test.json @@ -41,7 +41,8 @@ "signature_info": { "alg": "Ed25519", "issuer": "C2PA Test Signing Cert", - "cert_serial_number": "638838410810235485828984295321338730070538954823" + "cert_serial_number": "638838410810235485828984295321338730070538954823", + "ocsp_next_update": "2024-11-20T00:15:54.189494+00:00" }, "label": "urn:uuid:b6eb5aed-cc20-469f-b23a-88eb3b43775b" } diff --git a/sdk/tests/known_good/XCA.json b/sdk/tests/known_good/XCA.json index 0960e5faf..59801c6d8 100644 --- a/sdk/tests/known_good/XCA.json +++ b/sdk/tests/known_good/XCA.json @@ -76,7 +76,8 @@ "alg": "Ps256", "issuer": "C2PA Test Signing Cert", "cert_serial_number": "720724073027128164015125666832722375746636448153", - "time": "2024-08-06T21:53:37+00:00" + "time": "2024-08-06T21:53:37+00:00", + "ocsp_next_update": "2024-11-20T00:15:54.189494+00:00" }, "label": "contentauth:urn:uuid:c2677d4b-0a93-4444-876f-ed2f2d40b8cf" }