Skip to main content

structured_ltv/pki/
status.rs

1//! Request/issuer-key association with an actually checked path member.
2
3use super::{CheckedPath, exact};
4use structured_asn1::memory::{MemoryError, Work};
5use structured_pkix::{
6    ocsp::{AuthoritySource, BoundEvidence},
7    path::WorkingKey,
8};
9
10/// Completed source mismatch, not malformed XML or an unsuccessful OCSP lookup.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum OcspBindingFailure {
13    /// Common evaluation processed another original target certificate.
14    Target,
15    /// Its selected issuer has a different original SPKI from this path issuer.
16    Issuer,
17    /// Its authenticated issuing key used a different effective parameter state.
18    EffectiveKey,
19}
20
21/// Processing/resource refusal is separate from a completed source mismatch.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum OcspBindingError {
24    /// Caller selected no such original path member; not a document fault.
25    MemberIndex {
26        /// Invalid issuer-to-subject ordinal.
27        index: usize,
28    },
29    /// Current execution refused before source/parameter comparison.
30    Resource(MemoryError),
31    /// Internal authenticated-source provenance is incomplete. This cannot be
32    /// treated as a malformed document, mismatch or configured trust.
33    IssuerKeyUnstated,
34}
35
36/// Actual target and issuing-key association. This does not bless the status,
37/// nonce, update window or configured responder policy. Delegation still needs
38/// original-root path constraints and any unresolved responder revocation.
39pub struct OcspTarget<'view, 'certificate, 'document> {
40    path: &'view CheckedPath<'document>,
41    member: usize,
42    evidence: &'view BoundEvidence<'certificate, 'document, 'certificate, 'document>,
43}
44
45impl<'view, 'certificate, 'document> OcspTarget<'view, 'certificate, 'document> {
46    /// Same actual common-validator path, without a substituted issuer key.
47    #[must_use]
48    pub fn path(&self) -> &'view CheckedPath<'document> {
49        self.path
50    }
51    /// Exact original target's issuer-to-subject path ordinal.
52    #[must_use]
53    pub fn member(&self) -> usize {
54        self.member
55    }
56    /// Same original target/request/status source and temporal facts.
57    #[must_use]
58    pub fn evidence(
59        &self,
60    ) -> &'view BoundEvidence<'certificate, 'document, 'certificate, 'document> {
61        self.evidence
62    }
63    /// Original authorization branch. Configured trust remains explicit and
64    /// does not fabricate an issuer-signature/delegation-key proof.
65    #[must_use]
66    pub fn authority(&self) -> AuthoritySource {
67        self.evidence.evidence().source().source()
68    }
69}
70
71/// Completed association or source mismatch, never a partial Good assertion.
72pub enum OcspBinding<'view, 'certificate, 'document> {
73    /// Target and original issuing key are bound to this path member.
74    Bound(OcspTarget<'view, 'certificate, 'document>),
75    /// A complete original-source comparison found a mismatch.
76    Rejected(OcspBindingFailure),
77}
78
79impl<'document> CheckedPath<'document> {
80    /// Attach a common OCSP target result to the actual path member and issuing
81    /// key. A successful `CertID` match alone cannot establish that the caller
82    /// supplied the issuer that actually certified this target.
83    ///
84    /// RFC6960 ยง4.2.2.2 requires the same issuing key for direct delegation.
85    /// Match that original SPKI and authenticated inherited parameter state,
86    /// rather than reconstructing a standing key from a certificate or accepting
87    /// a matching issuer DN. Both the common `CertID` evaluation and this common
88    /// path already checked Name equality against the SAME original target.
89    /// Explicit configured-responder trust used no issuing-key authentication;
90    /// its separate branch is retained, with the same issuer SPKI check.
91    /// <https://www.rfc-editor.org/rfc/rfc6960#section-4.2.2.2>
92    ///
93    /// # Panics
94    /// Only if the private vector/index invariant is violated: a validated
95    /// nonzero member index always has its immediately preceding issuer.
96    pub fn bind_ocsp<'view, 'certificate, W: Work>(
97        &'view self,
98        member: usize,
99        evidence: &'view BoundEvidence<'certificate, 'document, 'certificate, 'document>,
100        execution: &W,
101    ) -> Result<OcspBinding<'view, 'certificate, 'document>, OcspBindingError> {
102        let resource = OcspBindingError::Resource;
103        execution.charge_work(1).map_err(resource)?;
104        let Some(target) = self.members.get(member) else {
105            return Err(OcspBindingError::MemberIndex { index: member });
106        };
107        if !exact(
108            target.certificate().as_bytes(),
109            evidence.target().as_bytes(),
110            execution,
111        )
112        .map_err(resource)?
113        {
114            return Ok(OcspBinding::Rejected(OcspBindingFailure::Target));
115        }
116        execution.charge_work(1).map_err(resource)?;
117        let expected = if member == 0 {
118            WorkingKey::standing(self.anchor.key())
119        } else {
120            self.members[member - 1].key()
121        };
122        let source = evidence.evidence().source();
123        if !exact(
124            expected.key().as_bytes(),
125            source.issuer().tbs().public_key().as_bytes(),
126            execution,
127        )
128        .map_err(resource)?
129        {
130            return Ok(OcspBinding::Rejected(OcspBindingFailure::Issuer));
131        }
132        if let Some(actual) = source.issuing_key() {
133            let same = match (expected.parameters(), actual.parameters()) {
134                (None, None) => true,
135                (Some(left), Some(right)) => exact(left, right, execution).map_err(resource)?,
136                _ => false,
137            };
138            if !same {
139                return Ok(OcspBinding::Rejected(OcspBindingFailure::EffectiveKey));
140            }
141        } else if !matches!(source.source(), AuthoritySource::Configured) {
142            return Err(OcspBindingError::IssuerKeyUnstated);
143        }
144        Ok(OcspBinding::Bound(OcspTarget {
145            path: self,
146            member,
147            evidence,
148        }))
149    }
150}