Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Report OCSP revocation and update status in SignatureInfo #691

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions sdk/examples/v2show.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)")
}
Expand Down
48 changes: 7 additions & 41 deletions sdk/src/claim.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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::{
Expand Down Expand Up @@ -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<DateTime<Utc>> {
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<String> {
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<String> {
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<ValidationInfo> {
pub(crate) fn signature_info(&self, th: &dyn TrustHandlerConfig) -> Option<ValidationInfo> {
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)
}
}

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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<Vec<u8>> {
#[cfg(feature = "v1_api")]
pub(crate) fn get_cert_chain(&self, th: &dyn TrustHandlerConfig) -> Result<Vec<u8>> {
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)
}
Expand Down
24 changes: 22 additions & 2 deletions sdk/src/cose_validator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -966,6 +966,8 @@ pub(crate) async fn verify_cose_async(
th: &dyn TrustHandlerConfig,
validation_log: &mut impl StatusTracker,
) -> Result<ValidationInfo> {
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) {
Expand Down Expand Up @@ -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)
Expand All @@ -1104,13 +1111,20 @@ 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;
let mut issuer_org = None;
let mut alg: Option<SigningAlg> = 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
Expand Down Expand Up @@ -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,
}
}

Expand All @@ -1169,6 +1184,9 @@ pub(crate) fn verify_cose(
th: &dyn TrustHandlerConfig,
validation_log: &mut impl StatusTracker,
) -> Result<ValidationInfo> {
// 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) {
Expand Down Expand Up @@ -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(())
Expand Down
19 changes: 12 additions & 7 deletions sdk/src/manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
};
Expand Down Expand Up @@ -1481,17 +1482,21 @@ pub struct SignatureInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub time: Option<String>,

/// Revocation status of the certificate.
// The date the certificate was revoked.
#[serde(skip_serializing_if = "Option::is_none")]
pub revocation_status: Option<bool>,
pub revocation_date: Option<String>,

// The date the OCSP response should be updated.
#[serde(skip_serializing_if = "Option::is_none")]
pub ocsp_next_update: Option<String>,

/// 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
}
Expand Down
49 changes: 43 additions & 6 deletions sdk/src/manifest_store_report.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,10 @@
pub(crate) fn from_store(store: &Store) -> Result<Self> {
let mut manifests = HashMap::<String, ManifestReport>::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 {
Expand Down Expand Up @@ -105,8 +108,18 @@
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
);
}

Check warning on line 122 in sdk/src/manifest_store_report.rs

View check run for this annotation

Codecov / codecov/patch

sdk/src/manifest_store_report.rs#L115-L122

Added lines #L115 - L122 were not covered by tests
}

Ok(())
Expand Down Expand Up @@ -298,7 +311,7 @@
}

impl ManifestReport {
fn from_claim(claim: &Claim) -> Result<Self> {
fn from_claim(claim: &Claim, store: &Store) -> Result<Self> {
let mut assertion_store = HashMap::<String, Value>::new();
let claim_assertions = claim.claim_assertion_store();
for claim_assertion in claim_assertions.iter() {
Expand Down Expand Up @@ -329,11 +342,18 @@
})
.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(),
};
Expand Down Expand Up @@ -373,9 +393,26 @@
// human readable issuing authority for this signature
#[serde(skip_serializing_if = "Option::is_none")]
issuer: Option<String>,
// the time the signature was created

/// The serial number of the certificate.
#[serde(skip_serializing_if = "Option::is_none")]
cert_serial_number: Option<String>,

/// The time the signature was created.
#[serde(skip_serializing_if = "Option::is_none")]
time: Option<String>,

// The date the certificate was revoked.
#[serde(skip_serializing_if = "Option::is_none")]
revocation_date: Option<String>,

// The date the OCSP response should be updated.
#[serde(skip_serializing_if = "Option::is_none")]
ocsp_next_update: Option<String>,

/// The cert chain for this claim.
#[serde(skip_serializing_if = "Option::is_none")]
cert_chain: Option<String>,
}

// replace the value of any field in the json string with a given key with the string <omitted>
Expand Down
33 changes: 27 additions & 6 deletions sdk/src/reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
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.
///
Expand Down Expand Up @@ -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<()> {
Expand Down Expand Up @@ -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(())
}
}
Loading
Loading