structured_pkix/ocsp/retention.rs
1//! Status retention and the extended revoked definition (RFC6960 §§2.2, 4.4.4, 4.4.8).
2
3use super::{BasicResponse, RecordedStatus, SingleResponse, end};
4use crate::crl::Reason;
5use crate::error::{Error, ErrorCode, Located, Result};
6use crate::extension::Extension;
7use crate::oid;
8use crate::temporal::{Instant, Reading};
9use structured_asn1::memory::Work;
10use structured_asn1::{Tag, universal};
11
12const CUTOFF: &str = "ocspResponse.singleResponse.archiveCutoff";
13const EXTENDED: &str = "ocspResponse.extendedRevoke";
14
15// RFC6960 §4.4.4: `ArchiveCutoff ::= GeneralizedTime`, with no criticality
16// requirement, so either marking is accepted.
17// https://www.rfc-editor.org/rfc/rfc6960#section-4.4.4
18pub(super) fn archive_cutoff<W: Work>(extension: &Extension<'_>, execution: &W) -> Result<Reading> {
19 let mut reader = extension.reader_scoped(execution).field(CUTOFF)?;
20 let value = reader
21 .expect(Tag::primitive(universal::GENERALIZED_TIME))
22 .field(CUTOFF)?;
23 end(&reader, CUTOFF)?;
24 crate::time::read_generalized_in(&reader, &value, CUTOFF)
25}
26
27// RFC6960 §4.4.8: "The value of the extension SHALL be NULL. This extension
28// MUST NOT be marked critical."
29// https://www.rfc-editor.org/rfc/rfc6960#section-4.4.8
30pub(super) fn extended_revoke<W: Work>(extension: &Extension<'_>, execution: &W) -> Result<()> {
31 if extension.is_critical() {
32 return Err(Error::new(
33 ErrorCode::MalformedValue,
34 EXTENDED,
35 extension.span().start,
36 ));
37 }
38 let mut reader = extension.reader_scoped(execution).field(EXTENDED)?;
39 let value = reader
40 .expect(Tag::primitive(universal::NULL))
41 .field(EXTENDED)?;
42 reader.null(&value).field(EXTENDED)?;
43 end(&reader, EXTENDED)
44}
45
46// RFC6960 §4.4.8: the extension "MUST be placed in responseExtensions, and it
47// MUST NOT appear in singleExtensions".
48pub(super) fn misplaced(extension: &Extension<'_>) -> Error {
49 Error::new(
50 ErrorCode::MalformedValue,
51 "ocspResponse.singleResponse.extendedRevoke",
52 extension.span().start,
53 )
54}
55
56// RFC6960 §2.2: a "revoked" answer for a non-issued certificate specifies
57// certificateHold and revocationTime January 1, 1970, and includes neither CRL
58// references (§4.4.2) nor CRL entry extensions (§4.4.5). Only that exact
59// assertion, under a response carrying the extended revoked definition, is
60// read as non-issued; any other hold stays a hold.
61// https://www.rfc-editor.org/rfc/rfc6960#section-2.2
62pub(super) fn not_issued<W: Work>(assertion: &SingleResponse<'_>, execution: &W) -> Result<bool> {
63 let RecordedStatus::Revoked {
64 time,
65 reason: Some(Reason::CertificateHold),
66 ..
67 } = assertion.status
68 else {
69 return Ok(false);
70 };
71 if time.instant() != Instant::whole(0) {
72 return Ok(false);
73 }
74 for kind in [
75 oid::OCSP_CRL,
76 oid::CRL_REASON,
77 oid::INVALIDITY_DATE,
78 oid::CERTIFICATE_ISSUER,
79 ] {
80 if assertion.extensions.get(kind, execution)?.is_some() {
81 return Ok(false);
82 }
83 }
84 Ok(true)
85}
86
87impl BasicResponse<'_> {
88 /// Whether the responder applies the extended revoked definition, so that a
89 /// matching "revoked" assertion can name a certificate that was never issued.
90 pub fn extended_revoke<W: Work>(&self, execution: &W) -> Result<bool> {
91 match self.extensions.get(oid::OCSP_EXTENDED_REVOKE, execution)? {
92 Some(extension) => extended_revoke(extension, execution).map(|()| true),
93 None => Ok(false),
94 }
95 }
96
97 /// Status of `responses()[index]` under this response's extended revoked
98 /// definition: [`super::Status::NotIssued`] for the exact RFC6960 §2.2
99 /// non-issued answer, otherwise the same reading as
100 /// [`SingleResponse::status`]. `None` when there is no such assertion.
101 ///
102 /// The assertion is named by position so that it always belongs to this
103 /// response: §4.4.8 scopes the definition to the response that carries it.
104 ///
105 /// # Examples
106 ///
107 /// ```
108 /// use std::sync::Arc;
109 /// use structured_asn1::Parser;
110 /// use structured_asn1::memory::{Memory, MemoryAccount};
111 /// use structured_pkix::ocsp::{Response, Status};
112 ///
113 /// fn tlv(tag: u8, contents: &[u8]) -> Vec<u8> {
114 /// let length = contents.len() as u8;
115 /// let header: &[u8] = if length < 128 { &[tag, length] } else { &[tag, 0x81, length] };
116 /// [header, contents].concat()
117 /// }
118 /// let algorithm = tlv(0x30, &[6, 3, 0x2a, 3, 4]);
119 /// let id = tlv(0x30, &[algorithm.clone(), tlv(4, &[1; 20]), tlv(4, &[2; 20]), vec![2, 1, 1]].concat());
120 /// let date = tlv(24, b"20261012120000Z");
121 /// // revoked, certificateHold, 1970-01-01T00:00:00Z
122 /// let hold = tlv(0xa1, &[tlv(24, b"19700101000000Z"), tlv(0xa0, &[10, 1, 6])].concat());
123 /// let single = tlv(0x30, &[id, hold, date.clone()].concat());
124 /// let extended = tlv(0x30, &[tlv(6, &[0x2b, 6, 1, 5, 5, 7, 0x30, 1, 9]), tlv(4, &[5, 0])].concat());
125 /// let data = tlv(0x30, &[
126 /// tlv(0xa2, &tlv(4, &[3; 20])), date, tlv(0x30, &single), tlv(0xa1, &tlv(0x30, &extended)),
127 /// ].concat());
128 /// let basic = tlv(0x30, &[data, algorithm, vec![3, 2, 0, 0xab]].concat());
129 /// let bytes = tlv(0x30, &[tlv(6, &[0x2b, 6, 1, 5, 5, 7, 0x30, 1, 1]), tlv(4, &basic)].concat());
130 /// let envelope = tlv(0x30, &[&[10, 1, 0][..], &tlv(0xa0, &bytes)].concat());
131 ///
132 /// let execution = Memory::new(Arc::new(MemoryAccount::new(65536, 65536)));
133 /// let parser = Parser::der(execution.clone());
134 /// let Response::Successful(payload) = Response::read(&parser, &envelope, 0).unwrap() else {
135 /// unreachable!()
136 /// };
137 /// let response = payload.into_basic(&execution).unwrap().unwrap();
138 /// assert!(matches!(response.responses()[0].status(), Status::Revoked { .. }));
139 /// assert_eq!(response.status_of(0, &execution).unwrap(), Some(Status::NotIssued));
140 /// assert_eq!(response.status_of(1, &execution).unwrap(), None);
141 /// ```
142 ///
143 /// # Errors
144 ///
145 /// A malformed response-wide extended revoked definition, a misplaced
146 /// marker on the selected assertion, or a spent work ceiling. Other
147 /// extension syntax and critical demands still require `check_extensions`.
148 pub fn status_of<W: Work>(&self, index: usize, execution: &W) -> Result<Option<super::Status>> {
149 let Some(assertion) = self.responses.get(index) else {
150 return Ok(None);
151 };
152 // RFC6960 §4.4.8 forbids this marker in singleExtensions. Check its
153 // placement even when another critical demand is unsupported; this
154 // status-only reading must not bless a malformed not-issued answer.
155 // https://www.rfc-editor.org/rfc/rfc6960#section-4.4.8
156 if let Some(extension) = assertion
157 .extensions
158 .get(oid::OCSP_EXTENDED_REVOKE, execution)?
159 {
160 return Err(misplaced(extension));
161 }
162 let extended = self.extended_revoke(execution)?;
163 status(assertion, extended, execution).map(Some)
164 }
165}
166
167pub(super) fn status<W: Work>(
168 assertion: &SingleResponse<'_>,
169 extended: bool,
170 execution: &W,
171) -> Result<super::Status> {
172 if extended && not_issued(assertion, execution)? {
173 return Ok(super::Status::NotIssued);
174 }
175 Ok(assertion.status())
176}
177
178impl SingleResponse<'_> {
179 /// The responder's archive cutoff for this assertion: status is retained for
180 /// certificates whose validity ended at or after it, so a certificate with
181 /// `notAfter >= cutoff` stays covered after its expiry.
182 /// Decimal fractions retain their subnanosecond remainder; comparing an
183 /// expiry to `cutoff.start()` alone must not discard that remainder.
184 pub fn archive_cutoff<W: Work>(&self, execution: &W) -> Result<Option<Reading>> {
185 match self.extensions.get(oid::OCSP_ARCHIVE_CUTOFF, execution)? {
186 Some(extension) => archive_cutoff(extension, execution).map(Some),
187 None => Ok(None),
188 }
189 }
190}