Skip to main content

encypher_c2pa/c2pa-trust/
lib.rs

1// Copyright 2026 Encypher Corporation
2// SPDX-License-Identifier: Apache-2.0
3
4//! Certificate trust validation for C2PA claim-signing certificates.
5//!
6//! The defining feature over the upstream `c2pa-rs` validator is that
7//! [`validate_chain`] honors an explicit `validation_time`: certificate
8//! `notBefore`/`notAfter` windows are checked against the supplied instant
9//! rather than the system clock. This makes it possible to validate a
10//! signature *as of* the moment it was produced (e.g. anchored by a trusted
11//! timestamp token) even after the signing certificate has expired.
12//!
13//! # Components
14//! - [`TrustList`] — a set of [`TrustAnchor`] configurations (DER certificate,
15//!   trust window, and [`AnchorPurpose`]).
16//! - [`EkuPolicy`] — Extended Key Usage enforcement for leaf certificates.
17//! - [`validate_chain`] — build and validate a path from a leaf certificate to
18//!   a trust anchor configured for the requested purpose.
19//! - [`RevocationDenylist`] — internal serial/fingerprint revocation set.
20
21pub(crate) mod ocsp;
22mod profile;
23pub(crate) use ocsp::online::{
24    build_request as build_ocsp_request, evaluate as evaluate_ocsp_online,
25    responder_url as ocsp_responder_url, OnlinePolicy as OnlineOcspPolicy,
26    OnlineVerdict as OnlineOcspVerdict,
27};
28pub(crate) use ocsp::{
29    evaluate_verified as evaluate_ocsp_verified,
30    evaluate_verified_with as evaluate_ocsp_verified_with, OcspStatus, MAX_OCSP_RESPONSE_BYTES,
31};
32mod timestamp;
33/// Test-only RFC 3161 fixture minting. Compiled out of the published library.
34#[cfg(test)]
35pub(crate) mod timestamp_fixture;
36use std::{
37    collections::{HashMap, HashSet},
38    sync::Arc,
39};
40pub(crate) use timestamp::{
41    describe_timestamp_token, inspect_timestamp_token, token_from_timestamp_response,
42    verify_timestamp_token, verify_timestamp_token_with, TimestampResult, TokenDescription,
43};
44
45use const_oid::ObjectIdentifier;
46use der::{Decode, Encode};
47use ecdsa::signature::hazmat::PrehashVerifier;
48use rsa::pkcs1::DecodeRsaPublicKey;
49use rsa::signature::Verifier as _;
50use sha2::{Digest, Sha256, Sha384, Sha512};
51use thiserror::Error;
52use time::OffsetDateTime;
53use x509_cert::ext::pkix::{BasicConstraints, CertificatePolicies, ExtendedKeyUsage};
54use x509_cert::{certificate::Version, Certificate};
55
56// ---------------------------------------------------------------------------
57// OID constants
58// ---------------------------------------------------------------------------
59
60/// C2PA claim-signing EKU (`c2pa-kp-claimSigning`).
61pub const OID_C2PA_CLAIM_SIGNING: &str = "1.3.6.1.4.1.62558.2.1";
62/// `id-kp-emailProtection`.
63pub const OID_EMAIL_PROTECTION: &str = "1.3.6.1.5.5.7.3.4";
64/// Adobe `documentSigning` EKU.
65pub const OID_ADOBE_DOCUMENT_SIGNING: &str = "1.2.840.113583.1.1.5";
66/// IETF `id-kp-documentSigning` EKU.
67pub const OID_IETF_DOCUMENT_SIGNING: &str = "1.3.6.1.5.5.7.3.36";
68/// `id-kp-timeStamping` - required EKU for a TSA certificate, and a purpose a
69/// C2PA claim signer is never authorized for (see
70/// [`leaf_is_acceptable_claim_signer`]).
71pub const OID_KP_TIME_STAMPING: &str = "1.3.6.1.5.5.7.3.8";
72/// `id-kp-OCSPSigning` - delegated OCSP responder EKU, and a purpose a C2PA
73/// claim signer is never authorized for.
74pub const OID_KP_OCSP_SIGNING: &str = "1.3.6.1.5.5.7.3.9";
75/// Microsoft C2PA manifest-signing EKU.
76pub const OID_MICROSOFT_C2PA: &str = "1.3.6.1.4.1.311.76.59.1.9";
77
78const OID_EXT_EKU: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.29.37");
79const OID_EXT_BASIC_CONSTRAINTS: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.29.19");
80const OID_AT_COMMON_NAME: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.4.3");
81const OID_EXT_KEY_USAGE: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.5.29.15");
82/// `anyExtendedKeyUsage` — forbidden for a C2PA claim-signing leaf.
83const OID_ANY_EKU: &str = "2.5.29.37.0";
84
85const OID_EC_PUBLIC_KEY: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.2.1");
86const OID_RSA_ENCRYPTION: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.1");
87const OID_ED25519: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.101.112");
88
89const OID_CURVE_P256: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.3.1.7");
90const OID_CURVE_P384: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.132.0.34");
91const OID_CURVE_P521: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.132.0.35");
92
93const OID_ECDSA_SHA256: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.4.3.2");
94const OID_ECDSA_SHA384: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.4.3.3");
95const OID_ECDSA_SHA512: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10045.4.3.4");
96
97const OID_RSA_SHA256: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.11");
98const OID_RSA_SHA384: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.12");
99const OID_RSA_SHA512: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.13");
100
101/// Maximum certificates walked while building a chain, guarding against loops.
102const MAX_CHAIN_DEPTH: usize = 20;
103/// Maximum issuer candidates expanded across the whole path search, bounding
104/// the work an adversarial bag of cross-signed intermediates can force.
105const MAX_PATH_NODES: usize = 256;
106
107// ---------------------------------------------------------------------------
108// Certificate profile revision
109// ---------------------------------------------------------------------------
110
111/// Which C2PA revision's certificate and PKIX signature rules apply.
112///
113/// The unsuffixed entry points preserve the established C2PA 2.4 behavior.
114/// Their `_with` variants require an explicit profile, so an untrusted claim's
115/// declared revision can never select a more permissive algorithm set.
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub enum CertificateProfile {
118    /// The C2PA 2.2 trust behavior.
119    V2_2,
120    /// The established C2PA 2.4 trust behavior.
121    V2_4,
122    /// C2PA 2.5, including RSASSA-PSS and ML-DSA-44/65/87 certificate, OCSP,
123    /// and CMS signature verification. The 2.5 claim-credential algorithm
124    /// restrictions do not apply to TSA, OCSP responder, or CA certificates.
125    V2_5,
126}
127
128// ---------------------------------------------------------------------------
129// Errors
130// ---------------------------------------------------------------------------
131
132/// Errors produced while constructing trust material.
133#[derive(Debug, Error)]
134pub enum TrustError {
135    /// The PEM input contained no parseable certificates.
136    #[error("no certificates found in PEM input")]
137    NoCertificates,
138    /// A certificate could not be decoded.
139    #[error("failed to decode certificate: {0}")]
140    Decode(String),
141}
142
143// ---------------------------------------------------------------------------
144// Hash selection
145// ---------------------------------------------------------------------------
146
147#[derive(Clone, Copy)]
148enum SigHash {
149    Sha256,
150    Sha384,
151    Sha512,
152}
153
154fn digest_bytes(hash: SigHash, msg: &[u8]) -> Vec<u8> {
155    match hash {
156        SigHash::Sha256 => Sha256::digest(msg).to_vec(),
157        SigHash::Sha384 => Sha384::digest(msg).to_vec(),
158        SigHash::Sha512 => Sha512::digest(msg).to_vec(),
159    }
160}
161
162// ---------------------------------------------------------------------------
163// Trust anchors
164// ---------------------------------------------------------------------------
165
166/// The signing purpose a trust-anchor configuration authorizes.
167///
168/// The Trust Model keeps one list of trust-anchor configurations per accepted
169/// EKU, requires the time-stamping list to be "separate from the lists for
170/// C2PA signers", and requires a validator to "use only the trust anchors it
171/// associates with EKUs present in the certificate". Purposes are therefore
172/// not interchangeable: a claim signer never chains to a time-stamping anchor,
173/// and a CAWG named-actor credential never chains to a claim-signing anchor.
174#[derive(Debug, Clone, Copy, PartialEq, Eq)]
175pub enum AnchorPurpose {
176    /// C2PA claim signing.
177    ClaimSigning,
178    /// RFC 3161 time-stamping (`id-kp-timeStamping`).
179    TimeStamping,
180    /// CAWG named-actor (identity) credentials.
181    CawgIdentity,
182}
183
184/// Which entry of the CAWG trust configuration an anchor belongs to.
185///
186/// CAWG Identity 1.3 has a validator keep a *CAWG trust configuration*: a list
187/// of accepted EKUs, each with its own accepted certificate policies and trust
188/// anchors. Its "interim trust model additions" are not a property of the EKU
189/// but of two named sources - the Mozilla root store with the email trust bit
190/// and the IPTC Verified News Publishers lists - and they carry extra
191/// conditions, a trusted time stamp and the 31 March 2027 cutoff, that no
192/// other configured anchor is subject to. An anchor therefore has to say which
193/// entry configured it.
194///
195/// Only [`AnchorPurpose::CawgIdentity`] anchors are read through this field.
196#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197pub enum CawgTrustSource {
198    /// One of the two sources the interim S/MIME additions name. The interim
199    /// conditions bind, and this is the default for any anchor that does not
200    /// declare otherwise, so an unlabeled source is held to the stricter rule.
201    SmimeInterim,
202    /// The bundled Encypher Verified Organizations identity root.
203    EncypherVerifiedOrganizations,
204    /// An anchor or end-entity certificate the caller configured.
205    CallerSupplied,
206}
207
208impl CawgTrustSource {
209    /// The `trust_source` detail reported for a credential this entry accepted.
210    pub fn label(self) -> &'static str {
211        match self {
212            Self::SmimeInterim => "smime_interim",
213            Self::EncypherVerifiedOrganizations => "encypher_verified_organizations",
214            Self::CallerSupplied => "caller_supplied",
215        }
216    }
217
218    /// True when the interim S/MIME conditions apply to this entry.
219    pub fn interim(self) -> bool {
220        matches!(self, Self::SmimeInterim)
221    }
222}
223
224/// Provenance for the trust list that supplied an anchor configuration.
225///
226/// `source_type` uses the C2PA entity-specific namespace syntax, for example
227/// `c2pa.trustList` for claim-signing anchors or `c2pa.tsaTrustList` for TSA
228/// anchors. `identifier` names a specific list snapshot when one is known.
229/// For ETSI TS 119 602/612 lists it is the decimal `LoTESequenceNumber`.
230#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
231pub struct TrustSource {
232    /// URI identifying the trust list. It need not be network-retrievable.
233    pub uri: String,
234    /// Namespaced trust-list type.
235    #[serde(rename = "type")]
236    pub source_type: String,
237    /// Optional identifier for the exact trust-list instance.
238    #[serde(default, skip_serializing_if = "Option::is_none")]
239    pub identifier: Option<String>,
240}
241
242impl TrustSource {
243    /// Describe a trust list without a snapshot identifier.
244    pub fn new(uri: impl Into<String>, source_type: impl Into<String>) -> Self {
245        Self {
246            uri: uri.into(),
247            source_type: source_type.into(),
248            identifier: None,
249        }
250    }
251
252    /// Attach the identifier of the exact trust-list instance.
253    pub fn with_identifier(mut self, identifier: impl Into<String>) -> Self {
254        self.identifier = Some(identifier.into());
255        self
256    }
257}
258
259/// One trust-anchor configuration: the anchor certificate plus the window in
260/// which the configuration is trusted and the purpose it authorizes.
261///
262/// VAL-CRYP-0010/0011: a configuration carrying a `not_before` must not
263/// validate claim signatures whose signing time precedes it, and one carrying a
264/// `not_after` must not validate signatures after it. These bounds belong to
265/// the configuration and are independent of the anchor certificate's own
266/// `notBefore`/`notAfter`.
267#[derive(Debug, Clone)]
268pub struct TrustAnchor {
269    /// DER-encoded anchor certificate.
270    pub certificate: Vec<u8>,
271    /// The purpose this configuration authorizes.
272    pub purpose: AnchorPurpose,
273    /// Configured start of trust. `None` leaves the start unbounded.
274    pub not_before: Option<OffsetDateTime>,
275    /// Configured end of trust. `None` leaves the end unbounded.
276    pub not_after: Option<OffsetDateTime>,
277    /// The CAWG trust configuration entry that supplied this anchor. Read only
278    /// for [`AnchorPurpose::CawgIdentity`].
279    pub cawg_source: CawgTrustSource,
280    /// The trust list that supplied this configuration, when known. Lists
281    /// parsed from one source share this allocation across all their anchors.
282    pub source: Option<Arc<TrustSource>>,
283}
284
285impl TrustAnchor {
286    /// An unbounded configuration authorizing `purpose`.
287    pub fn new(certificate: Vec<u8>, purpose: AnchorPurpose) -> Self {
288        Self {
289            certificate,
290            purpose,
291            not_before: None,
292            not_after: None,
293            cawg_source: CawgTrustSource::SmimeInterim,
294            source: None,
295        }
296    }
297
298    /// Associate this anchor configuration with its supplying trust list.
299    pub fn with_source(mut self, source: TrustSource) -> Self {
300        self.source = Some(Arc::new(source));
301        self
302    }
303
304    /// Lowercase hex SHA-256 of the anchor certificate's DER.
305    pub fn fingerprint(&self) -> String {
306        fingerprint_hex(&self.certificate)
307    }
308
309    /// True when this configuration is in force at `at`.
310    pub fn active_at(&self, at: OffsetDateTime) -> bool {
311        self.not_before.is_none_or(|start| at >= start)
312            && self.not_after.is_none_or(|end| at <= end)
313    }
314}
315
316// ---------------------------------------------------------------------------
317// TrustList
318// ---------------------------------------------------------------------------
319
320/// A set of trust-anchor configurations.
321#[derive(Debug, Clone, Default)]
322pub struct TrustList {
323    /// The configured anchors.
324    pub anchors: Vec<TrustAnchor>,
325}
326
327impl TrustList {
328    /// Build a claim-signing trust list from a PEM bundle.
329    pub fn from_pem(pem: &str) -> Result<Self, TrustError> {
330        Self::from_pem_for(AnchorPurpose::ClaimSigning, pem)
331    }
332
333    /// Build a trust list for `purpose` from a PEM bundle containing one or
334    /// more certificates.
335    ///
336    /// Each `CERTIFICATE` block is parsed and re-encoded to canonical DER.
337    /// Returns [`TrustError::NoCertificates`] when the bundle yields no
338    /// certificates and [`TrustError::Decode`] when a block cannot be decoded.
339    pub fn from_pem_for(purpose: AnchorPurpose, pem: &str) -> Result<Self, TrustError> {
340        // Guard before x509-cert: `Certificate::load_pem_chain` PANICS
341        // (subtract with overflow) on input containing no PEM block at all —
342        // observed in the wild with the IPTC VNPL anchor list, which is served
343        // as a legitimate zero-byte file while no anchors are registered. A
344        // verifier must fail closed on such input, never crash.
345        if !pem.contains("-----BEGIN CERTIFICATE-----") {
346            return Err(TrustError::NoCertificates);
347        }
348        let certs = Certificate::load_pem_chain(pem.as_bytes())
349            .map_err(|e| TrustError::Decode(e.to_string()))?;
350        if certs.is_empty() {
351            return Err(TrustError::NoCertificates);
352        }
353        let mut anchors = Vec::with_capacity(certs.len());
354        for cert in &certs {
355            let der = cert
356                .to_der()
357                .map_err(|e| TrustError::Decode(e.to_string()))?;
358            anchors.push(TrustAnchor::new(der, purpose));
359        }
360        Ok(Self { anchors })
361    }
362
363    /// An unbounded trust list for `purpose` over already-decoded DER.
364    pub fn from_certificates(
365        purpose: AnchorPurpose,
366        certificates: impl IntoIterator<Item = Vec<u8>>,
367    ) -> Self {
368        Self {
369            anchors: certificates
370                .into_iter()
371                .map(|der| TrustAnchor::new(der, purpose))
372                .collect(),
373        }
374    }
375
376    /// Apply configured trust bounds to every anchor in this list.
377    pub fn with_bounds(
378        mut self,
379        not_before: Option<OffsetDateTime>,
380        not_after: Option<OffsetDateTime>,
381    ) -> Self {
382        for anchor in &mut self.anchors {
383            anchor.not_before = not_before;
384            anchor.not_after = not_after;
385        }
386        self
387    }
388
389    /// Declare which CAWG trust configuration entry supplied every anchor in
390    /// this list.
391    pub fn with_cawg_source(mut self, source: CawgTrustSource) -> Self {
392        for anchor in &mut self.anchors {
393            anchor.cawg_source = source;
394        }
395        self
396    }
397
398    /// Associate every anchor in this list with the same supplying trust list.
399    ///
400    /// This is intended for lists parsed from one fetched or bundled source.
401    /// Lists assembled from several sources should set each anchor's
402    /// [`TrustAnchor::source`] separately.
403    pub fn with_source(mut self, source: TrustSource) -> Self {
404        let source = Arc::new(source);
405        for anchor in &mut self.anchors {
406            anchor.source = Some(Arc::clone(&source));
407        }
408        self
409    }
410
411    /// The first configured certificate equal to `der`, whatever its purpose
412    /// or trust window.
413    pub fn find_certificate(&self, der: &[u8]) -> Option<&TrustAnchor> {
414        self.anchors.iter().find(|anchor| anchor.certificate == der)
415    }
416
417    /// True when `der` is one of the configured certificates.
418    pub fn contains_certificate(&self, der: &[u8]) -> bool {
419        self.find_certificate(der).is_some()
420    }
421
422    /// Every configured certificate, in configuration order.
423    pub fn certificates(&self) -> impl Iterator<Item = &[u8]> {
424        self.anchors
425            .iter()
426            .map(|anchor| anchor.certificate.as_slice())
427    }
428
429    /// Return the Common Name (`CN`) of each anchor certificate.
430    ///
431    /// Anchors without a `CN` attribute are skipped, mirroring the enterprise
432    /// `get_trust_anchor_subjects` behavior.
433    pub fn anchor_subjects(&self) -> Vec<String> {
434        self.certificates()
435            .filter_map(|der| {
436                let cert = Certificate::from_der(der).ok()?;
437                common_name(&cert)
438            })
439            .collect()
440    }
441
442    /// The anchors that authorize `purpose`, are in force at `at`, and pass
443    /// `admit`, each with its index in this list so a completed path can name
444    /// the anchor it terminated at.
445    fn active_anchors(
446        &self,
447        purpose: AnchorPurpose,
448        at: OffsetDateTime,
449        admit: impl Fn(&TrustAnchor) -> bool,
450    ) -> Vec<(usize, &TrustAnchor)> {
451        self.anchors
452            .iter()
453            .enumerate()
454            .filter(|(_, anchor)| {
455                anchor.purpose == purpose && anchor.active_at(at) && admit(anchor)
456            })
457            .collect()
458    }
459
460    /// Split this list by certificate type: trust anchors first, every other
461    /// certificate second. Each keeps its configuration. A CAWG interim list
462    /// "MAY include both CA and end-entity certificates", so a list is sorted
463    /// by what each certificate is rather than by the list it was published
464    /// in. An anchor is a CA certificate (BasicConstraints `cA = TRUE`) or a
465    /// self-issued X.509 v1 certificate, whose format predates the
466    /// BasicConstraints extension. A v3 certificate without BasicConstraints,
467    /// including a self-issued end entity, is never promoted to an issuer.
468    pub fn split_certificate_authorities(self) -> (Self, Self) {
469        let (authorities, end_entities) = self.anchors.into_iter().partition(|anchor| {
470            Certificate::from_der(&anchor.certificate).is_ok_and(|cert| {
471                is_ca_certificate(&cert)
472                    || (cert.tbs_certificate.version == Version::V1
473                        && cert.tbs_certificate.subject == cert.tbs_certificate.issuer
474                        && !has_extension(&cert, OID_EXT_BASIC_CONSTRAINTS))
475            })
476        });
477        (
478            Self {
479                anchors: authorities,
480            },
481            Self {
482                anchors: end_entities,
483            },
484        )
485    }
486}
487
488// ---------------------------------------------------------------------------
489// EkuPolicy
490// ---------------------------------------------------------------------------
491
492/// Extended Key Usage policy: a leaf certificate is acceptable when it carries
493/// at least one of the allowed EKU OIDs.
494#[derive(Debug, Clone)]
495pub struct EkuPolicy {
496    /// Dotted-decimal OIDs accepted as a valid claim-signing EKU.
497    pub allowed_oids: Vec<String>,
498}
499
500impl Default for EkuPolicy {
501    /// Default policy: the full C2PA claim-signing EKU set the reference
502    /// validator (c2pa-rs `check_certificate_profile`) accepts - the C2PA
503    /// claim-signing OID, both document-signing OIDs, `emailProtection`
504    /// (still the most widely deployed claim-signer EKU, e.g. the IPTC
505    /// newsroom guide's GlobalSign certificates), and the Microsoft C2PA OID.
506    /// `timeStamping` and `OCSPSigning` are never in this set: they authorize
507    /// a different purpose, and the profile forbids a certificate from being
508    /// valid for more than one (see [`leaf_acceptable_der`]).
509    fn default() -> Self {
510        Self {
511            allowed_oids: vec![
512                OID_C2PA_CLAIM_SIGNING.to_string(),
513                OID_ADOBE_DOCUMENT_SIGNING.to_string(),
514                OID_IETF_DOCUMENT_SIGNING.to_string(),
515                OID_EMAIL_PROTECTION.to_string(),
516                OID_MICROSOFT_C2PA.to_string(),
517            ],
518        }
519    }
520}
521
522impl EkuPolicy {
523    /// Return `true` when `cert_der` declares an Extended Key Usage extension
524    /// containing at least one of the policy's allowed OIDs.
525    ///
526    /// A certificate lacking an EKU extension is rejected (returns `false`),
527    /// unless the policy itself lists no allowed OIDs (in which case any
528    /// certificate is accepted, matching the upstream semantics).
529    pub fn cert_has_required_eku(&self, cert_der: &[u8]) -> bool {
530        if self.allowed_oids.is_empty() {
531            return true;
532        }
533        let Ok(cert) = Certificate::from_der(cert_der) else {
534            return false;
535        };
536        let Some(ekus) = certificate_eku_oids(&cert) else {
537            return false;
538        };
539        ekus.iter()
540            .any(|oid| self.allowed_oids.iter().any(|allowed| allowed == oid))
541    }
542}
543
544/// Collect the EKU OIDs (dotted strings) declared by a certificate, if any.
545fn certificate_eku_oids(cert: &Certificate) -> Option<Vec<String>> {
546    let exts = cert.tbs_certificate.extensions.as_ref()?;
547    let ext = exts.iter().find(|e| e.extn_id == OID_EXT_EKU)?;
548    let eku = ExtendedKeyUsage::from_der(ext.extn_value.as_bytes()).ok()?;
549    Some(eku.0.iter().map(|oid| oid.to_string()).collect())
550}
551
552/// Return the extended-key-usage OIDs declared by a DER certificate.
553pub fn certificate_eku_oids_der(cert_der: &[u8]) -> Option<Vec<String>> {
554    let cert = Certificate::from_der(cert_der).ok()?;
555    certificate_eku_oids(&cert)
556}
557
558/// Return the certificate-policy OIDs declared by a DER certificate.
559pub fn certificate_policy_oids_der(cert_der: &[u8]) -> Option<Vec<String>> {
560    let cert = Certificate::from_der(cert_der).ok()?;
561    let exts = cert.tbs_certificate.extensions.as_ref()?;
562    let oid = ObjectIdentifier::new_unwrap("2.5.29.32");
563    let ext = exts.iter().find(|extension| extension.extn_id == oid)?;
564    let policies = CertificatePolicies::from_der(ext.extn_value.as_bytes()).ok()?;
565    Some(
566        policies
567            .0
568            .iter()
569            .map(|policy| policy.policy_identifier.to_string())
570            .collect(),
571    )
572}
573
574/// True when the DER certificate's validity window contains `at`.
575pub fn certificate_valid_at(cert_der: &[u8], at: OffsetDateTime) -> bool {
576    Certificate::from_der(cert_der)
577        .map(|cert| valid_at(&cert, at))
578        .unwrap_or(false)
579}
580
581// ---------------------------------------------------------------------------
582// Chain validation
583// ---------------------------------------------------------------------------
584
585/// Result of a certificate chain validation.
586#[derive(Debug, Clone)]
587pub struct ChainResult {
588    /// `true` when the leaf chains to one of the supplied trust anchors and
589    /// every link verified (signatures + validity window at `validated_at`).
590    pub trusted: bool,
591    /// `true` when every certificate in the walked chain was inside its
592    /// validity window at `validated_at`. When `false`, the signature is
593    /// "outside validity" (distinct from merely untrusted): the leaf itself may
594    /// be valid but an issuer in the chain was expired/not-yet-valid.
595    pub chain_validity_ok: bool,
596    /// `true` when the leaf certificate is an acceptable C2PA claim signer:
597    /// it carries a permitted claim-signing EKU (and not `anyExtendedKeyUsage`),
598    /// is not a CA certificate, and does not assert the `keyCertSign` key usage.
599    /// When `false`, the credential is structurally invalid for claim signing.
600    pub leaf_acceptable: bool,
601    /// Human-readable explanation when `trusted` is `false`.
602    pub reason: Option<String>,
603    /// The instant the chain was evaluated against — the supplied
604    /// `validation_time` when provided, otherwise the current UTC time.
605    pub validated_at: OffsetDateTime,
606    /// Index, in the trust list that was searched, of the anchor the trusted
607    /// path terminated at. `None` unless `trusted`.
608    ///
609    /// A trust list can hold anchors configured under different rules - the
610    /// CAWG trust configuration is exactly that - so the caller has to be able
611    /// to tell which one accepted the credential.
612    pub anchor: Option<usize>,
613}
614
615impl ChainResult {
616    fn untrusted(reason: impl Into<String>, at: OffsetDateTime) -> Self {
617        Self {
618            trusted: false,
619            chain_validity_ok: true,
620            leaf_acceptable: true,
621            reason: Some(reason.into()),
622            validated_at: at,
623            anchor: None,
624        }
625    }
626
627    /// The anchor the trusted path terminated at, resolved against the trust
628    /// list that produced this result.
629    pub fn terminating_anchor<'a>(&self, trust: &'a TrustList) -> Option<&'a TrustAnchor> {
630        trust.anchors.get(self.anchor?)
631    }
632
633    /// Trust-list sources containing the anchor that terminated this path.
634    ///
635    /// Results are limited to configurations with the terminating anchor's
636    /// exact DER and purpose that were active at `validated_at`. This keeps
637    /// claim-signing and TSA provenance separate even when the same
638    /// certificate was configured for both purposes. Duplicate source metadata
639    /// is returned once, in trust-list configuration order.
640    ///
641    /// This helper describes results from [`validate_chain`] and
642    /// [`validate_chain_with`]. Callers using [`validate_chain_admitting`] must
643    /// apply the same admission predicate before reporting duplicate
644    /// configurations because an opaque predicate cannot be replayed here.
645    pub fn matching_anchor_sources<'a>(&self, trust: &'a TrustList) -> Vec<&'a TrustSource> {
646        let Some(terminating) = self.terminating_anchor(trust) else {
647            return Vec::new();
648        };
649        let mut sources = Vec::new();
650        for anchor in &trust.anchors {
651            if anchor.purpose != terminating.purpose
652                || anchor.certificate != terminating.certificate
653                || !anchor.active_at(self.validated_at)
654            {
655                continue;
656            }
657            if let Some(source) = anchor.source.as_deref() {
658                if !sources.contains(&source) {
659                    sources.push(source);
660                }
661            }
662        }
663        sources
664    }
665}
666
667/// How a candidate certification path ended.
668///
669/// Ordered worst to best so the path builder can keep the most favorable
670/// outcome it found across every candidate path it explored.
671#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
672enum PathOutcome {
673    /// No candidate path reached an anchor in force for the purpose.
674    NoPath,
675    /// A path reached an anchor but failed the profile or a path constraint.
676    Rejected(&'static str),
677    /// A path reached an anchor, but a certificate on it was outside its
678    /// validity window at the validation instant.
679    OutsideValidity,
680    /// A path reached an anchor and satisfied every check.
681    Trusted,
682}
683
684/// Validate `leaf_der` against the anchors in `trust` that authorize `purpose`,
685/// optionally using `intermediates_der` to bridge the chain.
686///
687/// When `validation_time` is `Some`, all `notBefore`/`notAfter` checks use that
688/// instant instead of the system clock — this is the key capability that lets a
689/// signature remain verifiable after its certificate expires, provided the
690/// validation time falls within the certificate's original validity window.
691/// The same instant selects which trust-anchor configurations are in force
692/// (VAL-CRYP-0010/0011).
693///
694/// Path building searches the caller-supplied intermediates and the in-force
695/// anchors as an unordered bag, backtracking when a candidate issuer leads
696/// nowhere, and the completed path is checked against the C2PA certificate
697/// profile and the RFC 5280 section 6 constraints ([`profile::path_violation`]).
698/// The end-entity certificate's own profile compliance is reported separately
699/// in `leaf_acceptable`, because a TSA or CAWG leaf is validated against a
700/// different purpose's EKU set by its own caller.
701pub fn validate_chain(
702    leaf_der: &[u8],
703    intermediates_der: &[Vec<u8>],
704    trust: &TrustList,
705    purpose: AnchorPurpose,
706    validation_time: Option<OffsetDateTime>,
707) -> ChainResult {
708    validate_chain_with(
709        leaf_der,
710        intermediates_der,
711        trust,
712        purpose,
713        validation_time,
714        CertificateProfile::V2_4,
715    )
716}
717
718/// [`validate_chain`] under an explicit certificate profile.
719pub fn validate_chain_with(
720    leaf_der: &[u8],
721    intermediates_der: &[Vec<u8>],
722    trust: &TrustList,
723    purpose: AnchorPurpose,
724    validation_time: Option<OffsetDateTime>,
725    profile: CertificateProfile,
726) -> ChainResult {
727    validate_chain_admitting_with(
728        leaf_der,
729        intermediates_der,
730        trust,
731        purpose,
732        validation_time,
733        profile,
734        |_| true,
735    )
736}
737
738/// [`validate_chain`] restricted to the anchors `admit` accepts.
739///
740/// The CAWG trust configuration keeps anchors per accepted EKU, so one trust
741/// list can hold anchors that may terminate a path for one EKU and not for
742/// another.
743pub fn validate_chain_admitting(
744    leaf_der: &[u8],
745    intermediates_der: &[Vec<u8>],
746    trust: &TrustList,
747    purpose: AnchorPurpose,
748    validation_time: Option<OffsetDateTime>,
749    admit: impl Fn(&TrustAnchor) -> bool,
750) -> ChainResult {
751    validate_chain_admitting_with(
752        leaf_der,
753        intermediates_der,
754        trust,
755        purpose,
756        validation_time,
757        CertificateProfile::V2_4,
758        admit,
759    )
760}
761
762/// [`validate_chain_admitting`] under an explicit certificate profile.
763pub fn validate_chain_admitting_with(
764    leaf_der: &[u8],
765    intermediates_der: &[Vec<u8>],
766    trust: &TrustList,
767    purpose: AnchorPurpose,
768    validation_time: Option<OffsetDateTime>,
769    profile: CertificateProfile,
770    admit: impl Fn(&TrustAnchor) -> bool,
771) -> ChainResult {
772    let at = validation_time.unwrap_or_else(OffsetDateTime::now_utc);
773
774    let leaf = match Certificate::from_der(leaf_der) {
775        Ok(c) => c,
776        Err(e) => return ChainResult::untrusted(format!("invalid leaf certificate: {e}"), at),
777    };
778
779    // Leaf acceptability for claim signing: the C2PA certificate profile plus
780    // a permitted claim-signing EKU. Independent of trust-anchor chaining — an
781    // otherwise-trusted chain with an unacceptable leaf is still not a valid
782    // claim signer.
783    let leaf_acceptable = leaf_is_acceptable_claim_signer_with(&leaf, profile);
784
785    // Every in-force certificate configured for this purpose can bridge a
786    // path. Only an anchor accepted by `admit` may terminate it. Keeping those
787    // roles separate lets, for example, a CA listed under one CAWG profile
788    // bridge to an eligible root under another profile without widening the
789    // set of trusted termination points. The same admitted certificate
790    // configured twice keeps its first entry.
791    let mut anchor_indices: HashMap<String, usize> = HashMap::new();
792    let mut candidates: Vec<Certificate> = Vec::new();
793    for der in intermediates_der {
794        if let Ok(c) = Certificate::from_der(der) {
795            candidates.push(c);
796        }
797    }
798    for (index, anchor) in trust.active_anchors(purpose, at, |_| true) {
799        if let Ok(c) = Certificate::from_der(&anchor.certificate) {
800            if admit(anchor) {
801                anchor_indices
802                    .entry(fingerprint_hex(&anchor.certificate))
803                    .or_insert(index);
804            }
805            candidates.push(c);
806        }
807    }
808
809    let mut path = vec![leaf];
810    let mut seen: HashSet<String> = HashSet::from([fingerprint_hex(leaf_der)]);
811    let mut budget = MAX_PATH_NODES;
812    let mut anchor = None;
813    let search = PathSearchContext {
814        candidates: &candidates,
815        anchor_indices: &anchor_indices,
816        at,
817        profile,
818        rules: profile::ProfileRules::for_purpose(profile, purpose),
819    };
820    let outcome = extend_path(&mut path, &search, &mut seen, &mut budget, &mut anchor);
821
822    let (trusted, chain_validity_ok, reason) = match outcome {
823        PathOutcome::Trusted => (true, true, None),
824        PathOutcome::OutsideValidity => (
825            false,
826            false,
827            Some("a certificate in the chain was outside its validity window".to_string()),
828        ),
829        PathOutcome::Rejected(reason) => (false, true, Some(reason.to_string())),
830        PathOutcome::NoPath => (
831            false,
832            valid_at(path.first().expect("path holds the leaf"), at),
833            Some("certificate does not chain to a trusted anchor".to_string()),
834        ),
835    };
836    ChainResult {
837        trusted,
838        chain_validity_ok,
839        leaf_acceptable,
840        reason,
841        validated_at: at,
842        anchor: trusted.then_some(anchor).flatten(),
843    }
844}
845
846/// Immutable inputs shared by every recursive step of one path search.
847///
848/// Keeping these borrowed inputs together makes the mutable DFS state
849/// (`path`, `seen`, `budget`, and `anchor`) explicit without copying the
850/// candidate bag or changing its iteration order.
851struct PathSearchContext<'a> {
852    candidates: &'a [Certificate],
853    anchor_indices: &'a HashMap<String, usize>,
854    at: OffsetDateTime,
855    profile: CertificateProfile,
856    rules: profile::ProfileRules,
857}
858
859/// Depth-first path building from `path.last()` towards an in-force anchor.
860///
861/// Subject names are not unique and trust stores routinely hold cross-signed
862/// CAs with identical names, so a single greedy step up the chain can dead-end
863/// on a certificate that never reaches an anchor. Every issuer that actually
864/// signed the current certificate is therefore tried, and the best outcome
865/// across the explored paths is returned. `seen` breaks loops; `budget` bounds
866/// the search against an adversarial bag of intermediates.
867///
868/// `anchor` receives the index of the anchor a trusted path terminated at. The
869/// search stops at the first trusted path, so the value cannot be overwritten
870/// by a later, worse one.
871fn extend_path(
872    path: &mut Vec<Certificate>,
873    search: &PathSearchContext<'_>,
874    seen: &mut HashSet<String>,
875    budget: &mut usize,
876    anchor: &mut Option<usize>,
877) -> PathOutcome {
878    let current = path.last().expect("path is never empty").clone();
879    let Ok(current_der) = current.to_der() else {
880        return PathOutcome::NoPath;
881    };
882    if let Some(&index) = search.anchor_indices.get(&fingerprint_hex(&current_der)) {
883        let outcome = evaluate_path(path, search.at, search.rules, search.profile);
884        if outcome == PathOutcome::Trusted {
885            *anchor = Some(index);
886        }
887        return outcome;
888    }
889    if path.len() >= MAX_CHAIN_DEPTH
890        || (search.profile != CertificateProfile::V2_5
891            && current.tbs_certificate.subject == current.tbs_certificate.issuer)
892    {
893        // Exact 2.5 may continue through a self-issued rollover certificate
894        // when a distinct previous-key issuer actually verifies it. Older
895        // profiles retain their established self-issued termination behavior.
896        return PathOutcome::NoPath;
897    }
898
899    let mut best = PathOutcome::NoPath;
900    for candidate in search.candidates {
901        if *budget == 0 {
902            break;
903        }
904        if candidate.tbs_certificate.subject != current.tbs_certificate.issuer
905            || !verify_signature_with(&current, candidate, search.profile)
906        {
907            continue;
908        }
909        let Ok(der) = candidate.to_der() else {
910            continue;
911        };
912        let fingerprint = fingerprint_hex(&der);
913        if !seen.insert(fingerprint.clone()) {
914            continue;
915        }
916        *budget -= 1;
917        path.push(candidate.clone());
918        let outcome = extend_path(path, search, seen, budget, anchor);
919        path.pop();
920        seen.remove(&fingerprint);
921        best = best.max(outcome);
922        if best == PathOutcome::Trusted {
923            break;
924        }
925    }
926    best
927}
928
929/// Check a complete path, end entity first and selected anchor last.
930fn evaluate_path(
931    path: &[Certificate],
932    at: OffsetDateTime,
933    rules: profile::ProfileRules,
934    certificate_profile: CertificateProfile,
935) -> PathOutcome {
936    if let Some(reason) = profile::path_violation(path, rules) {
937        return PathOutcome::Rejected(reason);
938    }
939    let validity_path = if certificate_profile == CertificateProfile::V2_5 {
940        &path[..path.len().saturating_sub(1)]
941    } else {
942        path
943    };
944    if validity_path.iter().any(|cert| !valid_at(cert, at)) {
945        return PathOutcome::OutsideValidity;
946    }
947    PathOutcome::Trusted
948}
949
950/// Find the certificate that issued `leaf_der`, searching `candidates` (e.g. the
951/// x5chain intermediates followed by the trust anchors).
952///
953/// A candidate is accepted as the issuer when its subject DN equals `leaf`'s
954/// issuer DN **and** it actually signed `leaf`. Returns a borrowed issuer DER
955/// slice so callers do not clone a chain merely to evaluate revocation.
956///
957/// This resolves the authority an OCSP responder must be authorized by: per
958/// RFC 6960 §4.2.2.2 the responder is either the issuer of the certificate in
959/// question or a responder certificate issued by that same issuer. The issuer
960/// is frequently a trust anchor that is *not* carried in the COSE x5chain, so it
961/// must be located across both the chain and the trust list.
962pub fn resolve_issuer<'a>(
963    leaf_der: &[u8],
964    candidates: impl IntoIterator<Item = &'a [u8]>,
965) -> Option<&'a [u8]> {
966    resolve_issuer_with(leaf_der, candidates, CertificateProfile::V2_4)
967}
968
969/// [`resolve_issuer`] under an explicit certificate profile.
970pub fn resolve_issuer_with<'a>(
971    leaf_der: &[u8],
972    candidates: impl IntoIterator<Item = &'a [u8]>,
973    profile: CertificateProfile,
974) -> Option<&'a [u8]> {
975    let leaf = Certificate::from_der(leaf_der).ok()?;
976    let leaf_issuer_dn = leaf.tbs_certificate.issuer.to_der().ok()?;
977    for cand_der in candidates {
978        let Ok(cand) = Certificate::from_der(cand_der) else {
979            continue;
980        };
981        let Ok(cand_subject_dn) = cand.tbs_certificate.subject.to_der() else {
982            continue;
983        };
984        if cand_subject_dn == leaf_issuer_dn && verify_signature_with(&leaf, &cand, profile) {
985            return Some(cand_der);
986        }
987    }
988    None
989}
990// ---------------------------------------------------------------------------
991// RevocationDenylist
992// ---------------------------------------------------------------------------
993
994/// Internal revocation denylist matching leaf certificates by serial number or
995/// SHA-256 fingerprint (both lowercase hex, no separators).
996#[derive(Debug, Clone, Default)]
997pub struct RevocationDenylist {
998    /// Revoked certificate serial numbers (lowercase hex, minimal form).
999    pub serials: HashSet<String>,
1000    /// Revoked certificate SHA-256 fingerprints (lowercase hex).
1001    pub fingerprints: HashSet<String>,
1002}
1003
1004impl RevocationDenylist {
1005    /// Build a denylist, normalizing every token to trimmed lowercase.
1006    pub fn new(
1007        serials: impl IntoIterator<Item = String>,
1008        fingerprints: impl IntoIterator<Item = String>,
1009    ) -> Self {
1010        Self {
1011            serials: normalize_tokens(serials),
1012            fingerprints: normalize_tokens(fingerprints),
1013        }
1014    }
1015
1016    /// Return `true` when `cert_der` is revoked by serial or fingerprint.
1017    ///
1018    /// The fingerprint check works even when the certificate cannot be parsed,
1019    /// since it is computed directly over the supplied DER. The serial check
1020    /// requires a parseable certificate.
1021    pub fn is_revoked(&self, cert_der: &[u8]) -> bool {
1022        if !self.fingerprints.is_empty() && self.fingerprints.contains(&fingerprint_hex(cert_der)) {
1023            return true;
1024        }
1025        if !self.serials.is_empty() {
1026            if let Ok(cert) = Certificate::from_der(cert_der) {
1027                let serial = serial_hex(cert.tbs_certificate.serial_number.as_bytes());
1028                if self.serials.contains(&serial) {
1029                    return true;
1030                }
1031            }
1032        }
1033        false
1034    }
1035}
1036
1037fn normalize_tokens(values: impl IntoIterator<Item = String>) -> HashSet<String> {
1038    values
1039        .into_iter()
1040        .filter_map(|v| {
1041            let t = v.trim().to_ascii_lowercase();
1042            if t.is_empty() {
1043                None
1044            } else {
1045                Some(t)
1046            }
1047        })
1048        .collect()
1049}
1050
1051// ---------------------------------------------------------------------------
1052// Certificate helpers
1053// ---------------------------------------------------------------------------
1054
1055/// Lowercase hex SHA-256 fingerprint of a DER certificate.
1056fn fingerprint_hex(der: &[u8]) -> String {
1057    hex::encode(Sha256::digest(der))
1058}
1059
1060/// Render a DER serial-number integer as minimal lowercase hex.
1061///
1062/// Matches Python's `format(cert.serial_number, "x")`: leading zero bytes
1063/// (including the sign-guard `0x00`) are dropped, and a zero serial renders as
1064/// `"0"`.
1065fn serial_hex(bytes: &[u8]) -> String {
1066    let mut s = String::with_capacity(bytes.len() * 2);
1067    for b in bytes {
1068        s.push_str(&format!("{b:02x}"));
1069    }
1070    let trimmed = s.trim_start_matches('0');
1071    if trimmed.is_empty() {
1072        "0".to_string()
1073    } else {
1074        trimmed.to_string()
1075    }
1076}
1077
1078/// Extract the Common Name attribute value from a certificate's subject.
1079fn common_name(cert: &Certificate) -> Option<String> {
1080    for rdn in cert.tbs_certificate.subject.0.iter() {
1081        for atav in rdn.0.iter() {
1082            if atav.oid == OID_AT_COMMON_NAME {
1083                // CN string types (PrintableString/UTF8String/IA5String) all carry
1084                // UTF-8-compatible content in their value bytes.
1085                let raw = atav.value.value();
1086                return Some(String::from_utf8_lossy(raw).into_owned());
1087            }
1088        }
1089    }
1090    None
1091}
1092
1093/// True when the certificate's validity window contains `t`.
1094fn valid_at(cert: &Certificate, t: OffsetDateTime) -> bool {
1095    let nb = cert
1096        .tbs_certificate
1097        .validity
1098        .not_before
1099        .to_unix_duration()
1100        .as_secs() as i64;
1101    let na = cert
1102        .tbs_certificate
1103        .validity
1104        .not_after
1105        .to_unix_duration()
1106        .as_secs() as i64;
1107    let now = t.unix_timestamp();
1108    nb <= now && now <= na
1109}
1110
1111/// True when the certificate carries BasicConstraints with `cA = TRUE`.
1112fn is_ca_certificate(cert: &Certificate) -> bool {
1113    let Some(exts) = cert.tbs_certificate.extensions.as_ref() else {
1114        return false;
1115    };
1116    let Some(ext) = exts.iter().find(|e| e.extn_id == OID_EXT_BASIC_CONSTRAINTS) else {
1117        return false;
1118    };
1119    BasicConstraints::from_der(ext.extn_value.as_bytes())
1120        .map(|bc| bc.ca)
1121        .unwrap_or(false)
1122}
1123
1124fn has_extension(cert: &Certificate, oid: ObjectIdentifier) -> bool {
1125    cert.tbs_certificate
1126        .extensions
1127        .as_ref()
1128        .is_some_and(|exts| exts.iter().any(|e| e.extn_id == oid))
1129}
1130
1131/// The BasicConstraints `pathLenConstraint` of a CA certificate, if present.
1132/// `None` means unconstrained (or not a CA / no extension).
1133fn path_len_constraint(cert: &Certificate) -> Option<usize> {
1134    let exts = cert.tbs_certificate.extensions.as_ref()?;
1135    let ext = exts
1136        .iter()
1137        .find(|e| e.extn_id == OID_EXT_BASIC_CONSTRAINTS)?;
1138    let bc = BasicConstraints::from_der(ext.extn_value.as_bytes()).ok()?;
1139    bc.path_len_constraint.map(|n| n as usize)
1140}
1141
1142/// True when the certificate asserts the `keyCertSign` key usage bit.
1143fn has_key_cert_sign(cert: &Certificate) -> bool {
1144    let Some(exts) = cert.tbs_certificate.extensions.as_ref() else {
1145        return false;
1146    };
1147    let Some(ext) = exts.iter().find(|e| e.extn_id == OID_EXT_KEY_USAGE) else {
1148        return false;
1149    };
1150    use x509_cert::ext::pkix::KeyUsage;
1151    KeyUsage::from_der(ext.extn_value.as_bytes())
1152        .map(|ku| ku.key_cert_sign())
1153        .unwrap_or(false)
1154}
1155
1156/// True when the certificate carries a keyUsage extension that asserts the
1157/// `digitalSignature` bit. A claim-signing leaf MUST declare keyUsage with
1158/// digitalSignature: a leaf with no keyUsage extension is rejected.
1159fn allows_digital_signature(cert: &Certificate) -> bool {
1160    let Some(exts) = cert.tbs_certificate.extensions.as_ref() else {
1161        return false;
1162    };
1163    let Some(ext) = exts.iter().find(|e| e.extn_id == OID_EXT_KEY_USAGE) else {
1164        return false;
1165    };
1166    use x509_cert::ext::pkix::KeyUsage;
1167    KeyUsage::from_der(ext.extn_value.as_bytes())
1168        .map(|ku| ku.digital_signature())
1169        .unwrap_or(false)
1170}
1171
1172/// True when a DER certificate satisfies the C2PA end-entity certificate
1173/// profile, independent of the application-specific permitted EKU set.
1174///
1175/// CAWG imports the C2PA credential profile but defines its own accepted EKUs
1176/// (IETF document signing and interim S/MIME email protection), so it uses
1177/// this predicate before applying its own EKU policy.
1178pub fn leaf_profile_acceptable_der(leaf_der: &[u8]) -> bool {
1179    leaf_profile_acceptable_der_with(leaf_der, CertificateProfile::V2_4)
1180}
1181
1182/// [`leaf_profile_acceptable_der`] under an explicit certificate profile.
1183pub fn leaf_profile_acceptable_der_with(leaf_der: &[u8], profile: CertificateProfile) -> bool {
1184    Certificate::from_der(leaf_der).is_ok_and(|leaf| {
1185        profile::profile_violation_with(
1186            &leaf,
1187            profile::CertRole::EndEntity,
1188            profile::ProfileRules::for_purpose(profile, AnchorPurpose::ClaimSigning),
1189        )
1190        .is_none()
1191    })
1192}
1193
1194/// True when `leaf` is an acceptable C2PA claim-signing certificate.
1195///
1196/// Per the C2PA trust model the leaf MUST:
1197/// - satisfy the end-entity certificate profile ([`profile::profile_violation`]):
1198///   v3, no unique IDs, an Authority Key Identifier, a Key Usage extension
1199///   asserting `digitalSignature` and not `keyCertSign`, no `cA` basic
1200///   constraint, a non-empty EKU without `anyExtendedKeyUsage`, an allowed
1201///   signature algorithm and public key, and no unrecognized critical extension;
1202/// - NOT be valid for `timeStamping` or `OCSPSigning`, which are separate
1203///   purposes a claim signer is never authorized for;
1204/// - carry at least one permitted claim-signing EKU ([`EkuPolicy::default`]).
1205fn leaf_is_acceptable_claim_signer(leaf: &Certificate) -> bool {
1206    leaf_is_acceptable_claim_signer_with(leaf, CertificateProfile::V2_4)
1207}
1208
1209fn leaf_is_acceptable_claim_signer_with(leaf: &Certificate, profile: CertificateProfile) -> bool {
1210    let rules = profile::ProfileRules::for_purpose(profile, AnchorPurpose::ClaimSigning);
1211    if profile::profile_violation_with(leaf, profile::CertRole::EndEntity, rules).is_some() {
1212        return false;
1213    }
1214    let Some(ekus) = certificate_eku_oids(leaf) else {
1215        return false;
1216    };
1217    // Purpose isolation (Trust Model, Certificate Trust Chain): a certificate
1218    // is authorized for at most one of C2PA signing, time-stamp signing, and
1219    // OCSP response signing, and "a validator shall ensure a signing
1220    // certificate is authorized for the purpose for which it is being used".
1221    // A certificate valid for timeStamping or OCSPSigning is therefore never a
1222    // valid claim signer - whether or not that is its only EKU.
1223    if ekus
1224        .iter()
1225        .any(|oid| oid == OID_KP_TIME_STAMPING || oid == OID_KP_OCSP_SIGNING)
1226    {
1227        return false;
1228    }
1229    let policy = EkuPolicy::default();
1230    ekus.iter()
1231        .any(|oid| policy.allowed_oids.iter().any(|allowed| allowed == oid))
1232}
1233
1234/// [`leaf_is_acceptable_claim_signer`] over a DER-encoded certificate, for
1235/// callers that trust the certificate directly (allowed list) and therefore
1236/// never run a chain evaluation. Unparseable certificates are unacceptable.
1237pub fn leaf_acceptable_der(leaf_der: &[u8]) -> bool {
1238    leaf_acceptable_der_with(leaf_der, CertificateProfile::V2_4)
1239}
1240
1241/// [`leaf_acceptable_der`] under an explicit certificate profile.
1242pub fn leaf_acceptable_der_with(leaf_der: &[u8], profile: CertificateProfile) -> bool {
1243    Certificate::from_der(leaf_der)
1244        .map(|certificate| leaf_is_acceptable_claim_signer_with(&certificate, profile))
1245        .unwrap_or(false)
1246}
1247
1248/// Verify that `subject` was signed by `issuer`'s public key.
1249///
1250/// Supports ECDSA over NIST P-256/P-384/P-521, RSA PKCS#1 v1.5, and Ed25519.
1251/// Unsupported algorithms return `false` rather than erroring.
1252fn verify_signature(subject: &Certificate, issuer: &Certificate) -> bool {
1253    let Ok(tbs) = subject.tbs_certificate.to_der() else {
1254        return false;
1255    };
1256    let Some(sig) = subject.signature.as_bytes() else {
1257        return false;
1258    };
1259    let spki = &issuer.tbs_certificate.subject_public_key_info;
1260    let Some(pubkey) = spki.subject_public_key.as_bytes() else {
1261        return false;
1262    };
1263    let sig_alg = subject.signature_algorithm.oid;
1264    let key_alg = spki.algorithm.oid;
1265
1266    if key_alg == OID_EC_PUBLIC_KEY {
1267        let hash = match sig_alg {
1268            OID_ECDSA_SHA256 => SigHash::Sha256,
1269            OID_ECDSA_SHA384 => SigHash::Sha384,
1270            OID_ECDSA_SHA512 => SigHash::Sha512,
1271            _ => return false,
1272        };
1273        let curve = match spki.algorithm.parameters.as_ref() {
1274            Some(p) => match p.decode_as::<ObjectIdentifier>() {
1275                Ok(oid) => oid,
1276                Err(_) => return false,
1277            },
1278            None => return false,
1279        };
1280        verify_ecdsa(curve, hash, pubkey, sig, &tbs)
1281    } else if key_alg == OID_RSA_ENCRYPTION {
1282        let hash = match sig_alg {
1283            OID_RSA_SHA256 => SigHash::Sha256,
1284            OID_RSA_SHA384 => SigHash::Sha384,
1285            OID_RSA_SHA512 => SigHash::Sha512,
1286            _ => return false,
1287        };
1288        verify_rsa(hash, pubkey, sig, &tbs)
1289    } else if key_alg == OID_ED25519 {
1290        verify_ed25519(pubkey, sig, &tbs)
1291    } else {
1292        false
1293    }
1294}
1295/// [`verify_signature`] under an explicit certificate profile.
1296fn verify_signature_with(
1297    subject: &Certificate,
1298    issuer: &Certificate,
1299    profile: CertificateProfile,
1300) -> bool {
1301    if matches!(profile, CertificateProfile::V2_2 | CertificateProfile::V2_4) {
1302        return verify_signature(subject, issuer);
1303    }
1304    if subject.signature_algorithm != subject.tbs_certificate.signature {
1305        return false;
1306    }
1307    let (Ok(spki), Ok(algorithm), Ok(tbs), Some(signature)) = (
1308        issuer.tbs_certificate.subject_public_key_info.to_der(),
1309        subject.signature_algorithm.to_der(),
1310        subject.tbs_certificate.to_der(),
1311        subject.signature.as_bytes(),
1312    ) else {
1313        return false;
1314    };
1315    crate::c2pa_crypto::verify_x509_signature(&spki, &algorithm, &tbs, signature).is_ok()
1316}
1317
1318fn verify_ecdsa(
1319    curve: ObjectIdentifier,
1320    hash: SigHash,
1321    pubkey: &[u8],
1322    sig_der: &[u8],
1323    tbs: &[u8],
1324) -> bool {
1325    let prehash = digest_bytes(hash, tbs);
1326    if curve == OID_CURVE_P256 {
1327        let (Ok(vk), Ok(sig)) = (
1328            p256::ecdsa::VerifyingKey::from_sec1_bytes(pubkey),
1329            p256::ecdsa::Signature::from_der(sig_der),
1330        ) else {
1331            return false;
1332        };
1333        vk.verify_prehash(&prehash, &sig).is_ok()
1334    } else if curve == OID_CURVE_P384 {
1335        let (Ok(vk), Ok(sig)) = (
1336            p384::ecdsa::VerifyingKey::from_sec1_bytes(pubkey),
1337            p384::ecdsa::Signature::from_der(sig_der),
1338        ) else {
1339            return false;
1340        };
1341        vk.verify_prehash(&prehash, &sig).is_ok()
1342    } else if curve == OID_CURVE_P521 {
1343        let (Ok(vk), Ok(sig)) = (
1344            p521::ecdsa::VerifyingKey::from_sec1_bytes(pubkey),
1345            p521::ecdsa::Signature::from_der(sig_der),
1346        ) else {
1347            return false;
1348        };
1349        vk.verify_prehash(&prehash, &sig).is_ok()
1350    } else {
1351        false
1352    }
1353}
1354
1355fn verify_rsa(hash: SigHash, pubkey_der: &[u8], sig: &[u8], tbs: &[u8]) -> bool {
1356    let Ok(pubkey) = rsa::RsaPublicKey::from_pkcs1_der(pubkey_der) else {
1357        return false;
1358    };
1359    let Ok(signature) = rsa::pkcs1v15::Signature::try_from(sig) else {
1360        return false;
1361    };
1362    match hash {
1363        SigHash::Sha256 => rsa::pkcs1v15::VerifyingKey::<Sha256>::new(pubkey)
1364            .verify(tbs, &signature)
1365            .is_ok(),
1366        SigHash::Sha384 => rsa::pkcs1v15::VerifyingKey::<Sha384>::new(pubkey)
1367            .verify(tbs, &signature)
1368            .is_ok(),
1369        SigHash::Sha512 => rsa::pkcs1v15::VerifyingKey::<Sha512>::new(pubkey)
1370            .verify(tbs, &signature)
1371            .is_ok(),
1372    }
1373}
1374
1375fn verify_ed25519(pubkey: &[u8], sig: &[u8], tbs: &[u8]) -> bool {
1376    let Ok(key_bytes): Result<[u8; 32], _> = pubkey.try_into() else {
1377        return false;
1378    };
1379    let Ok(vk) = ed25519_dalek::VerifyingKey::from_bytes(&key_bytes) else {
1380        return false;
1381    };
1382    let Ok(signature) = ed25519_dalek::Signature::from_slice(sig) else {
1383        return false;
1384    };
1385    vk.verify_strict(tbs, &signature).is_ok()
1386}
1387
1388#[cfg(test)]
1389mod tests;