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}