Skip to main content

structured_pkix/ocsp/
delegation.rs

1//! Direct responder delegation uses the common supplied-path processor.
2
3use crate::error::{Located, Result};
4use crate::extension::{Extension, Recognizer};
5use crate::path::{
6    self, Anchor, BuildResult, EffectiveAnchor, Validation, ValidationContext, Verify, Watch,
7    WorkingKey,
8};
9use crate::{Certificate, oid};
10use structured_asn1::memory::Work;
11use structured_asn1::{Tag, universal};
12
13/// The responder certificate's revocation-checking fact, not a trust decision.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum ResponderRevocation {
16    /// A valid NULL-valued id-pkix-ocsp-nocheck exemption covers this certificate.
17    NoCheck,
18    /// No exemption; checking/acceptance remains the caller's explicit policy.
19    Unresolved,
20}
21
22pub(crate) fn read_no_check<W: Work>(extension: &Extension<'_>, execution: &W) -> Result<()> {
23    const FIELD: &str = "extensions.ocspNoCheck";
24    let mut reader = extension.reader_scoped(execution).field(FIELD)?;
25    let element = reader
26        .expect(Tag::primitive(universal::NULL))
27        .field(FIELD)?;
28    reader.null(&element).field(FIELD)?;
29    // DER primitive NULL must have empty contents; the producing Reader
30    // enforces that value rule. NoCheck is not any arbitrary one-value schema.
31    super::end(&reader, FIELD)
32}
33
34/// Completed OCSP-specific purpose/source refusal, separate from path rejection.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum DelegationFailure {
37    /// Effective issuing key does not belong to the supplied issuer certificate.
38    IssuerKeyBinding,
39    /// The delegate lacks explicit id-kp-OCSPSigning; anyExtendedKeyUsage is not it.
40    SigningPurpose,
41    /// Present keyUsage allows neither applicable object-signing use.
42    KeyUsage,
43}
44
45/// Accepted local delegation facts. Neither the issuer's original-root trust
46/// nor the responder's unresolved revocation is silently established here.
47#[derive(Debug, Clone, Copy)]
48pub struct DelegateFacts<'a> {
49    responder: &'a Certificate<'a>,
50    issuer: &'a Certificate<'a>,
51    issuing_key: WorkingKey<'a>,
52    key: WorkingKey<'a>,
53    revocation: ResponderRevocation,
54}
55
56impl<'a> DelegateFacts<'a> {
57    /// Actual directly issued responder certificate.
58    #[must_use]
59    pub fn responder(self) -> &'a Certificate<'a> {
60        self.responder
61    }
62    /// Caller-selected, already authenticated target issuer.
63    #[must_use]
64    pub fn issuer(self) -> &'a Certificate<'a> {
65        self.issuer
66    }
67    /// Actual effective issuing key used for the delegate certificate check.
68    /// Inherited parameters keep their original source; rebuilding a standing
69    /// key from `issuer()` would lose this validated-path association.
70    #[must_use]
71    pub fn issuing_key(self) -> WorkingKey<'a> {
72        self.issuing_key
73    }
74    /// Responder effective key projected by the common path processor.
75    #[must_use]
76    pub fn key(self) -> WorkingKey<'a> {
77        self.key
78    }
79    /// Revocation exemption or unresolved checking requirement.
80    #[must_use]
81    pub fn revocation(self) -> ResponderRevocation {
82        self.revocation
83    }
84}
85
86/// Completed local delegation result; resource/provider failures stay Result.
87#[derive(Debug)]
88#[expect(
89    clippy::large_enum_variant,
90    reason = "fixed-size local facts retain both original issuer and responder parameter sources; avoid boxing solely for enum layout"
91)]
92pub enum Delegation<'a> {
93    /// Direct local path, explicit OCSP purpose and keyUsage established.
94    Accepted(DelegateFacts<'a>),
95    /// Completed purpose/source rejection.
96    Rejected(DelegationFailure),
97    /// Common static path processor rejected this responder.
98    PathRejected(path::Rejection<'a>),
99    /// Local path bounds prevented a completed check.
100    Incomplete(path::Truncation),
101}
102
103/// Check direct delegation from the *actual*, already authenticated target
104/// issuing key. RFC6960 ยง4.2.2.2 requires the same issuing key, not merely the
105/// same CA name; the caller binds this issuer to its authenticated target path.
106/// <https://www.rfc-editor.org/rfc/rfc6960#section-4.2.2.2>
107///
108/// The one-link local path uses every common static check and the issuer's own
109/// name constraints. It does not resume original-root ancestor constraints;
110/// callers requiring those must validate the complete responder path as well.
111/// `context.interval` is the caller's response-acceptance interval, not an
112/// untrusted producedAt clock. A noCheck exemption never skips this validity.
113pub fn check_delegate<'a, V: Verify<E>, W: Watch, R: Recognizer<E>, E: Work>(
114    responder: &'a Certificate<'a>,
115    issuer: &'a Certificate<'a>,
116    issuing_key: WorkingKey<'a>,
117    context: &ValidationContext<'a, '_, V, W, R, E>,
118) -> BuildResult<Delegation<'a>, V::Error, R::Error> {
119    let execution = context.execution;
120    let Some(anchor) =
121        EffectiveAnchor::bind(Anchor::from_certificate(issuer), issuing_key, execution)?
122    else {
123        return Ok(Delegation::Rejected(DelegationFailure::IssuerKeyBinding));
124    };
125    let extensions = responder.tbs().extensions();
126    let Some(purposes) = extensions.extended_key_usage(execution)? else {
127        return Ok(Delegation::Rejected(DelegationFailure::SigningPurpose));
128    };
129    if !purposes.has(oid::OCSP_SIGNING, execution)? {
130        return Ok(Delegation::Rejected(DelegationFailure::SigningPurpose));
131    }
132    if !super::authority::signing_usage(responder, execution)? {
133        return Ok(Delegation::Rejected(DelegationFailure::KeyUsage));
134    }
135    let revocation = match extensions.get(oid::OCSP_NO_CHECK, execution)? {
136        Some(extension) => {
137            read_no_check(extension, execution)?;
138            ResponderRevocation::NoCheck
139        }
140        None => ResponderRevocation::Unresolved,
141    };
142    Ok(
143        match path::validate_effective(&[responder], anchor, context)? {
144            Validation::Accepted(facts) => Delegation::Accepted(DelegateFacts {
145                responder,
146                issuer,
147                issuing_key,
148                key: facts.key,
149                revocation,
150            }),
151            Validation::Rejected(reason) => Delegation::PathRejected(reason),
152            Validation::Incomplete(reason) => Delegation::Incomplete(reason),
153        },
154    )
155}
156
157#[cfg(test)]
158mod tests;