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::Memory;
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(extension: &Extension<'_>, execution: &Memory) -> Result<()> {
23    const FIELD: &str = "extensions.ocspNoCheck";
24    let mut reader = extension.reader(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    key: WorkingKey<'a>,
52    revocation: ResponderRevocation,
53}
54
55impl<'a> DelegateFacts<'a> {
56    /// Actual directly issued responder certificate.
57    #[must_use]
58    pub fn responder(self) -> &'a Certificate<'a> {
59        self.responder
60    }
61    /// Caller-selected, already authenticated target issuer.
62    #[must_use]
63    pub fn issuer(self) -> &'a Certificate<'a> {
64        self.issuer
65    }
66    /// Responder effective key projected by the common path processor.
67    #[must_use]
68    pub fn key(self) -> WorkingKey<'a> {
69        self.key
70    }
71    /// Revocation exemption or unresolved checking requirement.
72    #[must_use]
73    pub fn revocation(self) -> ResponderRevocation {
74        self.revocation
75    }
76}
77
78/// Completed local delegation result; resource/provider failures stay Result.
79#[derive(Debug)]
80pub enum Delegation<'a> {
81    /// Direct local path, explicit OCSP purpose and keyUsage established.
82    Accepted(DelegateFacts<'a>),
83    /// Completed purpose/source rejection.
84    Rejected(DelegationFailure),
85    /// Common static path processor rejected this responder.
86    PathRejected(path::Rejection<'a>),
87    /// Local path bounds prevented a completed check.
88    Incomplete(path::Truncation),
89}
90
91/// Check direct delegation from the *actual*, already authenticated target
92/// issuing key. RFC6960 ยง4.2.2.2 requires the same issuing key, not merely the
93/// same CA name; the caller binds this issuer to its authenticated target path.
94/// <https://www.rfc-editor.org/rfc/rfc6960#section-4.2.2.2>
95///
96/// The one-link local path uses every common static check and the issuer's own
97/// name constraints. It does not resume original-root ancestor constraints;
98/// callers requiring those must validate the complete responder path as well.
99/// `context.interval` is the caller's response-acceptance interval, not an
100/// untrusted producedAt clock. A noCheck exemption never skips this validity.
101pub fn check_delegate<'a, V: Verify, W: Watch, R: Recognizer>(
102    responder: &'a Certificate<'a>,
103    issuer: &'a Certificate<'a>,
104    issuing_key: WorkingKey<'a>,
105    context: &ValidationContext<'a, '_, V, W, R>,
106) -> BuildResult<Delegation<'a>, V::Error, R::Error> {
107    let execution = context.execution;
108    let Some(anchor) =
109        EffectiveAnchor::bind(Anchor::from_certificate(issuer), issuing_key, execution)?
110    else {
111        return Ok(Delegation::Rejected(DelegationFailure::IssuerKeyBinding));
112    };
113    let extensions = responder.tbs().extensions();
114    let Some(purposes) = extensions.extended_key_usage(execution)? else {
115        return Ok(Delegation::Rejected(DelegationFailure::SigningPurpose));
116    };
117    if !purposes.has(oid::OCSP_SIGNING, execution)? {
118        return Ok(Delegation::Rejected(DelegationFailure::SigningPurpose));
119    }
120    if !super::authority::signing_usage(responder, execution)? {
121        return Ok(Delegation::Rejected(DelegationFailure::KeyUsage));
122    }
123    let revocation = match extensions.get(oid::OCSP_NO_CHECK, execution)? {
124        Some(extension) => {
125            read_no_check(extension, execution)?;
126            ResponderRevocation::NoCheck
127        }
128        None => ResponderRevocation::Unresolved,
129    };
130    Ok(
131        match path::validate_effective(&[responder], anchor, context)? {
132            Validation::Accepted(facts) => Delegation::Accepted(DelegateFacts {
133                responder,
134                issuer,
135                key: facts.key,
136                revocation,
137            }),
138            Validation::Rejected(reason) => Delegation::PathRejected(reason),
139            Validation::Incomplete(reason) => Delegation::Incomplete(reason),
140        },
141    )
142}
143
144#[cfg(test)]
145mod tests;