pub(crate) mod ocsp;
pub(crate) use ocsp::{
evaluate_verified as evaluate_ocsp_verified, OcspStatus, MAX_OCSP_RESPONSE_BYTES,
};
mod timestamp;
use std::collections::HashSet;
pub(crate) use timestamp::{
inspect_timestamp_token, token_from_timestamp_response, verify_timestamp_token, TimestampResult,
};
use const_oid::ObjectIdentifier;
use der::{Decode, Encode};
use ecdsa::signature::hazmat::PrehashVerifier;
use rsa::pkcs1::DecodeRsaPublicKey;
use rsa::signature::Verifier as _;
use sha2::{Digest, Sha256, Sha384, Sha512};
use thiserror::Error;
use time::OffsetDateTime;
use x509_cert::ext::pkix::{BasicConstraints, CertificatePolicies, ExtendedKeyUsage};
use x509_cert::Certificate;
pub const OID_C2PA_CLAIM_SIGNING: &str = "1.3.6.1.4.1.62558.2.1";
pub const OID_EMAIL_PROTECTION: &str = "1.3.6.1.5.5.7.3.4";
pub const OID_ADOBE_DOCUMENT_SIGNING: &str = "1.2.840.113583.1.1.5";
pub const OID_IETF_DOCUMENT_SIGNING: &str = "1.3.6.1.5.5.7.3.36";
pub const OID_KP_TIME_STAMPING: &str = "1.3.6.1.5.5.7.3.8";
pub const OID_KP_OCSP_SIGNING: &str = "1.3.6.1.5.5.7.3.9";
pub const OID_MICROSOFT_C2PA: &str = "1.3.6.1.4.1.311.76.59.1.9";
const OID_EXT_EKU: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.29.37");
const OID_EXT_BASIC_CONSTRAINTS: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.29.19");
const OID_AT_COMMON_NAME: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.4.3");
const OID_EXT_KEY_USAGE: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.29.15");
const OID_ANY_EKU: &str = "2.5.29.37.0";
const OID_EC_PUBLIC_KEY: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.2.1");
const OID_RSA_ENCRYPTION: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.1");
const OID_ED25519: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.101.112");
const OID_CURVE_P256: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.3.1.7");
const OID_CURVE_P384: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.132.0.34");
const OID_CURVE_P521: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.132.0.35");
const OID_ECDSA_SHA256: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.4.3.2");
const OID_ECDSA_SHA384: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.4.3.3");
const OID_ECDSA_SHA512: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.4.3.4");
const OID_RSA_SHA256: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.11");
const OID_RSA_SHA384: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.12");
const OID_RSA_SHA512: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.13");
const MAX_CHAIN_DEPTH: usize = 20;
#[derive(Debug, Error)]
pub enum TrustError {
#[error("no certificates found in PEM input")]
NoCertificates,
#[error("failed to decode certificate: {0}")]
Decode(String),
}
#[derive(Clone, Copy)]
enum SigHash {
Sha256,
Sha384,
Sha512,
}
fn digest_bytes(hash: SigHash, msg: &[u8]) -> Vec<u8> {
match hash {
SigHash::Sha256 => Sha256::digest(msg).to_vec(),
SigHash::Sha384 => Sha384::digest(msg).to_vec(),
SigHash::Sha512 => Sha512::digest(msg).to_vec(),
}
}
#[derive(Debug, Clone, Default)]
pub struct TrustList {
pub anchors: Vec<Vec<u8>>,
}
impl TrustList {
pub fn from_pem(pem: &str) -> Result<Self, TrustError> {
if !pem.contains("-----BEGIN CERTIFICATE-----") {
return Err(TrustError::NoCertificates);
}
let certs = Certificate::load_pem_chain(pem.as_bytes())
.map_err(|e| TrustError::Decode(e.to_string()))?;
if certs.is_empty() {
return Err(TrustError::NoCertificates);
}
let mut anchors = Vec::with_capacity(certs.len());
for cert in &certs {
let der = cert
.to_der()
.map_err(|e| TrustError::Decode(e.to_string()))?;
anchors.push(der);
}
Ok(Self { anchors })
}
pub fn anchor_subjects(&self) -> Vec<String> {
self.anchors
.iter()
.filter_map(|der| {
let cert = Certificate::from_der(der).ok()?;
common_name(&cert)
})
.collect()
}
fn anchor_fingerprints(&self) -> HashSet<String> {
self.anchors
.iter()
.map(|der| fingerprint_hex(der))
.collect()
}
}
#[derive(Debug, Clone)]
pub struct EkuPolicy {
pub allowed_oids: Vec<String>,
}
impl Default for EkuPolicy {
fn default() -> Self {
Self {
allowed_oids: vec![
OID_C2PA_CLAIM_SIGNING.to_string(),
OID_ADOBE_DOCUMENT_SIGNING.to_string(),
OID_IETF_DOCUMENT_SIGNING.to_string(),
OID_EMAIL_PROTECTION.to_string(),
OID_MICROSOFT_C2PA.to_string(),
],
}
}
}
impl EkuPolicy {
pub fn cert_has_required_eku(&self, cert_der: &[u8]) -> bool {
if self.allowed_oids.is_empty() {
return true;
}
let Ok(cert) = Certificate::from_der(cert_der) else {
return false;
};
let Some(ekus) = certificate_eku_oids(&cert) else {
return false;
};
ekus.iter()
.any(|oid| self.allowed_oids.iter().any(|allowed| allowed == oid))
}
}
fn certificate_eku_oids(cert: &Certificate) -> Option<Vec<String>> {
let exts = cert.tbs_certificate.extensions.as_ref()?;
let ext = exts.iter().find(|e| e.extn_id == OID_EXT_EKU)?;
let eku = ExtendedKeyUsage::from_der(ext.extn_value.as_bytes()).ok()?;
Some(eku.0.iter().map(|oid| oid.to_string()).collect())
}
pub fn certificate_eku_oids_der(cert_der: &[u8]) -> Option<Vec<String>> {
let cert = Certificate::from_der(cert_der).ok()?;
certificate_eku_oids(&cert)
}
pub fn certificate_policy_oids_der(cert_der: &[u8]) -> Option<Vec<String>> {
let cert = Certificate::from_der(cert_der).ok()?;
let exts = cert.tbs_certificate.extensions.as_ref()?;
let oid = ObjectIdentifier::new_unwrap("2.5.29.32");
let ext = exts.iter().find(|extension| extension.extn_id == oid)?;
let policies = CertificatePolicies::from_der(ext.extn_value.as_bytes()).ok()?;
Some(
policies
.0
.iter()
.map(|policy| policy.policy_identifier.to_string())
.collect(),
)
}
pub fn certificate_valid_at(cert_der: &[u8], at: OffsetDateTime) -> bool {
Certificate::from_der(cert_der)
.map(|cert| valid_at(&cert, at))
.unwrap_or(false)
}
#[derive(Debug, Clone)]
pub struct ChainResult {
pub trusted: bool,
pub chain_validity_ok: bool,
pub leaf_acceptable: bool,
pub reason: Option<String>,
pub validated_at: OffsetDateTime,
}
impl ChainResult {
fn untrusted(reason: impl Into<String>, at: OffsetDateTime) -> Self {
Self {
trusted: false,
chain_validity_ok: true,
leaf_acceptable: true,
reason: Some(reason.into()),
validated_at: at,
}
}
}
#[allow(clippy::explicit_counter_loop)]
pub fn validate_chain(
leaf_der: &[u8],
intermediates_der: &[Vec<u8>],
trust: &TrustList,
validation_time: Option<OffsetDateTime>,
) -> ChainResult {
let at = validation_time.unwrap_or_else(OffsetDateTime::now_utc);
let leaf = match Certificate::from_der(leaf_der) {
Ok(c) => c,
Err(e) => return ChainResult::untrusted(format!("invalid leaf certificate: {e}"), at),
};
let leaf_acceptable = leaf_is_acceptable_claim_signer(&leaf);
let mut chain_validity_ok = valid_at(&leaf, at);
let mut candidates: Vec<Certificate> = Vec::new();
for der in intermediates_der {
if let Ok(c) = Certificate::from_der(der) {
candidates.push(c);
}
}
for der in &trust.anchors {
if let Ok(c) = Certificate::from_der(der) {
candidates.push(c);
}
}
let anchor_fps = trust.anchor_fingerprints();
let mut chain_fps: HashSet<String> = HashSet::new();
let mut seen: HashSet<String> = HashSet::new();
let mut current = leaf;
let mut current_der: Vec<u8> = leaf_der.to_vec();
let mut intermediates_below: usize = 0;
let untrusted_with = |reason: &str, validity_ok: bool| ChainResult {
trusted: false,
chain_validity_ok: validity_ok,
leaf_acceptable,
reason: Some(reason.to_string()),
validated_at: at,
};
for _ in 0..MAX_CHAIN_DEPTH {
let fp = fingerprint_hex(¤t_der);
chain_fps.insert(fp.clone());
if seen.contains(&fp) {
break;
}
seen.insert(fp);
if current.tbs_certificate.subject == current.tbs_certificate.issuer {
if !verify_signature(¤t, ¤t) {
return untrusted_with("chain root signature invalid", chain_validity_ok);
}
break;
}
let issuer = candidates
.iter()
.find(|candidate| {
candidate.tbs_certificate.subject == current.tbs_certificate.issuer
&& verify_signature(¤t, candidate)
})
.cloned();
let Some(issuer) = issuer else {
break; };
if !is_ca_certificate(&issuer) {
return untrusted_with("issuer certificate is not a CA", chain_validity_ok);
}
if let Some(max) = path_len_constraint(&issuer) {
if intermediates_below > max {
return untrusted_with("issuer pathLenConstraint violated", chain_validity_ok);
}
}
if !valid_at(&issuer, at) {
chain_validity_ok = false;
}
intermediates_below += 1;
current_der = match issuer.to_der() {
Ok(d) => d,
Err(e) => {
return untrusted_with(&format!("failed to encode issuer: {e}"), chain_validity_ok)
}
};
current = issuer;
}
let chains_to_anchor = chain_fps.intersection(&anchor_fps).next().is_some();
let trusted = chains_to_anchor && chain_validity_ok;
ChainResult {
trusted,
chain_validity_ok,
leaf_acceptable,
reason: if trusted {
None
} else if !chains_to_anchor {
Some("certificate does not chain to a trusted anchor".into())
} else {
Some("a certificate in the chain was outside its validity window".into())
},
validated_at: at,
}
}
pub fn resolve_issuer<'a>(
leaf_der: &[u8],
candidates: impl IntoIterator<Item = &'a [u8]>,
) -> Option<&'a [u8]> {
let leaf = Certificate::from_der(leaf_der).ok()?;
let leaf_issuer_dn = leaf.tbs_certificate.issuer.to_der().ok()?;
for cand_der in candidates {
let Ok(cand) = Certificate::from_der(cand_der) else {
continue;
};
let Ok(cand_subject_dn) = cand.tbs_certificate.subject.to_der() else {
continue;
};
if cand_subject_dn == leaf_issuer_dn && verify_signature(&leaf, &cand) {
return Some(cand_der);
}
}
None
}
#[derive(Debug, Clone, Default)]
pub struct RevocationDenylist {
pub serials: HashSet<String>,
pub fingerprints: HashSet<String>,
}
impl RevocationDenylist {
pub fn new(
serials: impl IntoIterator<Item = String>,
fingerprints: impl IntoIterator<Item = String>,
) -> Self {
Self {
serials: normalize_tokens(serials),
fingerprints: normalize_tokens(fingerprints),
}
}
pub fn is_revoked(&self, cert_der: &[u8]) -> bool {
if !self.fingerprints.is_empty() && self.fingerprints.contains(&fingerprint_hex(cert_der)) {
return true;
}
if !self.serials.is_empty() {
if let Ok(cert) = Certificate::from_der(cert_der) {
let serial = serial_hex(cert.tbs_certificate.serial_number.as_bytes());
if self.serials.contains(&serial) {
return true;
}
}
}
false
}
}
fn normalize_tokens(values: impl IntoIterator<Item = String>) -> HashSet<String> {
values
.into_iter()
.filter_map(|v| {
let t = v.trim().to_ascii_lowercase();
if t.is_empty() {
None
} else {
Some(t)
}
})
.collect()
}
fn fingerprint_hex(der: &[u8]) -> String {
hex::encode(Sha256::digest(der))
}
fn serial_hex(bytes: &[u8]) -> String {
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes {
s.push_str(&format!("{b:02x}"));
}
let trimmed = s.trim_start_matches('0');
if trimmed.is_empty() {
"0".to_string()
} else {
trimmed.to_string()
}
}
fn common_name(cert: &Certificate) -> Option<String> {
for rdn in cert.tbs_certificate.subject.0.iter() {
for atav in rdn.0.iter() {
if atav.oid == OID_AT_COMMON_NAME {
let raw = atav.value.value();
return Some(String::from_utf8_lossy(raw).into_owned());
}
}
}
None
}
fn valid_at(cert: &Certificate, t: OffsetDateTime) -> bool {
let nb = cert
.tbs_certificate
.validity
.not_before
.to_unix_duration()
.as_secs() as i64;
let na = cert
.tbs_certificate
.validity
.not_after
.to_unix_duration()
.as_secs() as i64;
let now = t.unix_timestamp();
nb <= now && now <= na
}
fn is_ca_certificate(cert: &Certificate) -> bool {
let Some(exts) = cert.tbs_certificate.extensions.as_ref() else {
return false;
};
let Some(ext) = exts.iter().find(|e| e.extn_id == OID_EXT_BASIC_CONSTRAINTS) else {
return false;
};
BasicConstraints::from_der(ext.extn_value.as_bytes())
.map(|bc| bc.ca)
.unwrap_or(false)
}
fn path_len_constraint(cert: &Certificate) -> Option<usize> {
let exts = cert.tbs_certificate.extensions.as_ref()?;
let ext = exts
.iter()
.find(|e| e.extn_id == OID_EXT_BASIC_CONSTRAINTS)?;
let bc = BasicConstraints::from_der(ext.extn_value.as_bytes()).ok()?;
bc.path_len_constraint.map(|n| n as usize)
}
fn has_key_cert_sign(cert: &Certificate) -> bool {
let Some(exts) = cert.tbs_certificate.extensions.as_ref() else {
return false;
};
let Some(ext) = exts.iter().find(|e| e.extn_id == OID_EXT_KEY_USAGE) else {
return false;
};
use x509_cert::ext::pkix::KeyUsage;
KeyUsage::from_der(ext.extn_value.as_bytes())
.map(|ku| ku.key_cert_sign())
.unwrap_or(false)
}
fn allows_digital_signature(cert: &Certificate) -> bool {
let Some(exts) = cert.tbs_certificate.extensions.as_ref() else {
return false;
};
let Some(ext) = exts.iter().find(|e| e.extn_id == OID_EXT_KEY_USAGE) else {
return false;
};
use x509_cert::ext::pkix::KeyUsage;
KeyUsage::from_der(ext.extn_value.as_bytes())
.map(|ku| ku.digital_signature())
.unwrap_or(false)
}
pub fn leaf_profile_acceptable_der(leaf_der: &[u8]) -> bool {
let Ok(leaf) = Certificate::from_der(leaf_der) else {
return false;
};
if is_ca_certificate(&leaf) || has_key_cert_sign(&leaf) || !allows_digital_signature(&leaf) {
return false;
}
certificate_eku_oids(&leaf).is_some_and(|ekus| !ekus.iter().any(|oid| oid == OID_ANY_EKU))
}
fn leaf_is_acceptable_claim_signer(leaf: &Certificate) -> bool {
if is_ca_certificate(leaf) || has_key_cert_sign(leaf) {
return false;
}
if !allows_digital_signature(leaf) {
return false;
}
let Some(ekus) = certificate_eku_oids(leaf) else {
return false;
};
if ekus.iter().any(|oid| oid == OID_ANY_EKU) {
return false;
}
let special = ekus
.iter()
.any(|oid| oid == OID_KP_TIME_STAMPING || oid == OID_KP_OCSP_SIGNING);
if special {
return ekus.len() == 1;
}
let policy = EkuPolicy::default();
ekus.iter()
.any(|oid| policy.allowed_oids.iter().any(|allowed| allowed == oid))
}
pub fn leaf_acceptable_der(leaf_der: &[u8]) -> bool {
Certificate::from_der(leaf_der)
.map(|c| leaf_is_acceptable_claim_signer(&c))
.unwrap_or(false)
}
fn verify_signature(subject: &Certificate, issuer: &Certificate) -> bool {
let Ok(tbs) = subject.tbs_certificate.to_der() else {
return false;
};
let Some(sig) = subject.signature.as_bytes() else {
return false;
};
let spki = &issuer.tbs_certificate.subject_public_key_info;
let Some(pubkey) = spki.subject_public_key.as_bytes() else {
return false;
};
let sig_alg = subject.signature_algorithm.oid;
let key_alg = spki.algorithm.oid;
if key_alg == OID_EC_PUBLIC_KEY {
let hash = match sig_alg {
OID_ECDSA_SHA256 => SigHash::Sha256,
OID_ECDSA_SHA384 => SigHash::Sha384,
OID_ECDSA_SHA512 => SigHash::Sha512,
_ => return false,
};
let curve = match spki.algorithm.parameters.as_ref() {
Some(p) => match p.decode_as::<ObjectIdentifier>() {
Ok(oid) => oid,
Err(_) => return false,
},
None => return false,
};
verify_ecdsa(curve, hash, pubkey, sig, &tbs)
} else if key_alg == OID_RSA_ENCRYPTION {
let hash = match sig_alg {
OID_RSA_SHA256 => SigHash::Sha256,
OID_RSA_SHA384 => SigHash::Sha384,
OID_RSA_SHA512 => SigHash::Sha512,
_ => return false,
};
verify_rsa(hash, pubkey, sig, &tbs)
} else if key_alg == OID_ED25519 {
verify_ed25519(pubkey, sig, &tbs)
} else {
false
}
}
fn verify_ecdsa(
curve: ObjectIdentifier,
hash: SigHash,
pubkey: &[u8],
sig_der: &[u8],
tbs: &[u8],
) -> bool {
let prehash = digest_bytes(hash, tbs);
if curve == OID_CURVE_P256 {
let (Ok(vk), Ok(sig)) = (
p256::ecdsa::VerifyingKey::from_sec1_bytes(pubkey),
p256::ecdsa::Signature::from_der(sig_der),
) else {
return false;
};
vk.verify_prehash(&prehash, &sig).is_ok()
} else if curve == OID_CURVE_P384 {
let (Ok(vk), Ok(sig)) = (
p384::ecdsa::VerifyingKey::from_sec1_bytes(pubkey),
p384::ecdsa::Signature::from_der(sig_der),
) else {
return false;
};
vk.verify_prehash(&prehash, &sig).is_ok()
} else if curve == OID_CURVE_P521 {
let (Ok(vk), Ok(sig)) = (
p521::ecdsa::VerifyingKey::from_sec1_bytes(pubkey),
p521::ecdsa::Signature::from_der(sig_der),
) else {
return false;
};
vk.verify_prehash(&prehash, &sig).is_ok()
} else {
false
}
}
fn verify_rsa(hash: SigHash, pubkey_der: &[u8], sig: &[u8], tbs: &[u8]) -> bool {
let Ok(pubkey) = rsa::RsaPublicKey::from_pkcs1_der(pubkey_der) else {
return false;
};
let Ok(signature) = rsa::pkcs1v15::Signature::try_from(sig) else {
return false;
};
match hash {
SigHash::Sha256 => rsa::pkcs1v15::VerifyingKey::<Sha256>::new(pubkey)
.verify(tbs, &signature)
.is_ok(),
SigHash::Sha384 => rsa::pkcs1v15::VerifyingKey::<Sha384>::new(pubkey)
.verify(tbs, &signature)
.is_ok(),
SigHash::Sha512 => rsa::pkcs1v15::VerifyingKey::<Sha512>::new(pubkey)
.verify(tbs, &signature)
.is_ok(),
}
}
fn verify_ed25519(pubkey: &[u8], sig: &[u8], tbs: &[u8]) -> bool {
let Ok(key_bytes): Result<[u8; 32], _> = pubkey.try_into() else {
return false;
};
let Ok(vk) = ed25519_dalek::VerifyingKey::from_bytes(&key_bytes) else {
return false;
};
let Ok(signature) = ed25519_dalek::Signature::from_slice(sig) else {
return false;
};
vk.verify_strict(tbs, &signature).is_ok()
}
#[cfg(test)]
mod tests;