Skip to content

Commit c59f97e

Browse files
authored
x509-ocsp: implement builder (#1259)
1 parent b5d5dcb commit c59f97e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+2758
-199
lines changed

.github/workflows/x509-ocsp.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ jobs:
3838
toolchain: ${{ matrix.rust }}
3939
targets: ${{ matrix.target }}
4040
- uses: RustCrypto/actions/cargo-hack-install@master
41-
- run: cargo hack build --target ${{ matrix.target }} --feature-powerset
41+
- run: cargo hack build --target ${{ matrix.target }} --feature-powerset --exclude-features std
4242

4343
minimal-versions:
4444
uses: RustCrypto/actions/.github/workflows/minimal-versions.yml@master

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

x509-ocsp/Cargo.toml

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,23 @@ der = { version = "0.7.8", features = ["alloc", "derive", "oid"] }
2020
spki = { version = "0.7.2", features = ["alloc"] }
2121
x509-cert = { version = "0.2.4", default-features = false }
2222

23-
# optional dependencies
23+
# Optional
24+
digest = { version = "0.10.7", optional = true, default-features = false, features = ["oid"] }
2425
rand_core = { version = "0.6.4", optional = true, default-features = false }
26+
signature = { version = "2.1.0", optional = true, default-features = false, features = ["digest", "rand_core"] }
2527

2628
[dev-dependencies]
2729
hex-literal = "0.4.1"
30+
lazy_static = "1.4.0"
31+
rand = "0.8.5"
32+
rsa = { version = "0.9.2", default-features = false, features = ["sha2"] }
33+
sha1 = { version = "0.10.6", default-features = false, features = ["oid"] }
34+
sha2 = { version = "0.10.8", default-features = false, features = ["oid"] }
35+
36+
[features]
37+
rand = ["rand_core"]
38+
builder = ["digest", "rand", "signature"]
39+
std = ["der/std", "x509-cert/std"]
2840

2941
[package.metadata.docs.rs]
3042
all-features = true

x509-ocsp/src/basic.rs

Lines changed: 138 additions & 152 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,20 @@
11
//! Basic OCSP Response
22
3+
use crate::{
4+
ext::Nonce, AsResponseBytes, CertId, CertStatus, OcspGeneralizedTime, ResponderId, Version,
5+
};
36
use alloc::vec::Vec;
4-
use const_oid::AssociatedOid;
7+
use const_oid::{
8+
db::rfc6960::{ID_PKIX_OCSP_BASIC, ID_PKIX_OCSP_NONCE},
9+
AssociatedOid,
10+
};
511
use core::{default::Default, option::Option};
612
use der::{
7-
asn1::{BitString, GeneralizedTime, Null, OctetString},
8-
Choice, Decode, Enumerated, Sequence,
13+
asn1::{BitString, ObjectIdentifier},
14+
Decode, Sequence,
915
};
1016
use spki::AlgorithmIdentifierOwned;
11-
use x509_cert::{
12-
certificate::Certificate,
13-
crl::RevokedCert,
14-
ext::{pkix::CrlReason, Extensions},
15-
name::Name,
16-
serial_number::SerialNumber,
17-
time::Time,
18-
};
19-
20-
/// OCSP `Version` as defined in [RFC 6960 Section 4.1.1].
21-
///
22-
/// ```text
23-
/// Version ::= INTEGER { v1(0) }
24-
/// ```
25-
///
26-
/// [RFC 6960 Section 4.1.1]: https://datatracker.ietf.org/doc/html/rfc6960#section-4.1.1
27-
#[derive(Clone, Debug, Default, Copy, PartialEq, Eq, Enumerated)]
28-
#[asn1(type = "INTEGER")]
29-
#[repr(u8)]
30-
pub enum Version {
31-
/// Version 1 (default)
32-
#[default]
33-
V1 = 0,
34-
}
17+
use x509_cert::{certificate::Certificate, ext::Extensions};
3518

3619
/// BasicOcspResponse structure as defined in [RFC 6960 Section 4.2.1].
3720
///
@@ -55,6 +38,20 @@ pub struct BasicOcspResponse {
5538
pub certs: Option<Vec<Certificate>>,
5639
}
5740

41+
impl BasicOcspResponse {
42+
/// Returns the response's nonce value, if any. This method will return `None` if the response
43+
/// has no `Nonce` extension or decoding of the `Nonce` extension fails.
44+
pub fn nonce(&self) -> Option<Nonce> {
45+
self.tbs_response_data.nonce()
46+
}
47+
}
48+
49+
impl AssociatedOid for BasicOcspResponse {
50+
const OID: ObjectIdentifier = ID_PKIX_OCSP_BASIC;
51+
}
52+
53+
impl AsResponseBytes for BasicOcspResponse {}
54+
5855
/// ResponseData structure as defined in [RFC 6960 Section 4.2.1].
5956
///
6057
/// ```text
@@ -77,45 +74,30 @@ pub struct ResponseData {
7774
)]
7875
pub version: Version,
7976
pub responder_id: ResponderId,
80-
pub produced_at: GeneralizedTime,
77+
pub produced_at: OcspGeneralizedTime,
8178
pub responses: Vec<SingleResponse>,
8279

8380
#[asn1(context_specific = "1", optional = "true", tag_mode = "EXPLICIT")]
8481
pub response_extensions: Option<Extensions>,
8582
}
8683

87-
/// ResponderID structure as defined in [RFC 6960 Section 4.2.1].
88-
///
89-
/// ```text
90-
/// ResponderID ::= CHOICE {
91-
/// byName [1] Name,
92-
/// byKey [2] KeyHash }
93-
/// ```
94-
///
95-
/// [RFC 6960 Section 4.2.1]: https://datatracker.ietf.org/doc/html/rfc6960#section-4.2.1
96-
#[derive(Clone, Debug, Eq, PartialEq, Choice)]
97-
#[allow(missing_docs)]
98-
pub enum ResponderId {
99-
#[asn1(context_specific = "1", tag_mode = "EXPLICIT", constructed = "true")]
100-
ByName(Name),
101-
102-
#[asn1(context_specific = "2", tag_mode = "EXPLICIT", constructed = "true")]
103-
ByKey(KeyHash),
84+
impl ResponseData {
85+
/// Returns the response's nonce value, if any. This method will return `None` if the response
86+
/// has no `Nonce` extension or decoding of the `Nonce` extension fails.
87+
pub fn nonce(&self) -> Option<Nonce> {
88+
match &self.response_extensions {
89+
Some(extns) => {
90+
let mut filter = extns.iter().filter(|e| e.extn_id == ID_PKIX_OCSP_NONCE);
91+
match filter.next() {
92+
Some(extn) => Nonce::from_der(extn.extn_value.as_bytes()).ok(),
93+
None => None,
94+
}
95+
}
96+
None => None,
97+
}
98+
}
10499
}
105100

106-
/// KeyHash structure as defined in [RFC 6960 Section 4.2.1].
107-
///
108-
/// ```text
109-
/// KeyHash ::= OCTET STRING -- SHA-1 hash of responder's public key
110-
/// -- (i.e., the SHA-1 hash of the value of the
111-
/// -- BIT STRING subjectPublicKey [excluding
112-
/// -- the tag, length, and number of unused
113-
/// -- bits] in the responder's certificate)
114-
/// ```
115-
///
116-
/// [RFC 6960 Section 4.2.1]: https://datatracker.ietf.org/doc/html/rfc6960#section-4.2.1
117-
pub type KeyHash = OctetString;
118-
119101
/// SingleResponse structure as defined in [RFC 6960 Section 4.2.1].
120102
///
121103
/// ```text
@@ -133,112 +115,116 @@ pub type KeyHash = OctetString;
133115
pub struct SingleResponse {
134116
pub cert_id: CertId,
135117
pub cert_status: CertStatus,
136-
pub this_update: GeneralizedTime,
118+
pub this_update: OcspGeneralizedTime,
137119

138120
#[asn1(context_specific = "0", optional = "true", tag_mode = "EXPLICIT")]
139-
pub next_update: Option<GeneralizedTime>,
121+
pub next_update: Option<OcspGeneralizedTime>,
140122

141123
#[asn1(context_specific = "1", optional = "true", tag_mode = "EXPLICIT")]
142124
pub single_extensions: Option<Extensions>,
143125
}
144126

145-
/// CertID structure as defined in [RFC 6960 Section 4.1.1].
146-
///
147-
/// ```text
148-
/// CertID ::= SEQUENCE {
149-
/// hashAlgorithm AlgorithmIdentifier,
150-
/// issuerNameHash OCTET STRING, -- Hash of issuer's DN
151-
/// issuerKeyHash OCTET STRING, -- Hash of issuer's public key
152-
/// serialNumber CertificateSerialNumber }
153-
/// ```
154-
///
155-
/// [RFC 6960 Section 4.1.1]: https://datatracker.ietf.org/doc/html/rfc6960#section-4.1.1
156-
#[derive(Clone, Debug, Eq, PartialEq, Sequence)]
157-
#[allow(missing_docs)]
158-
pub struct CertId {
159-
pub hash_algorithm: AlgorithmIdentifierOwned,
160-
pub issuer_name_hash: OctetString,
161-
pub issuer_key_hash: OctetString,
162-
pub serial_number: SerialNumber,
163-
}
164-
165-
/// CertStatus structure as defined in [RFC 6960 Section 4.2.1].
166-
///
167-
/// ```text
168-
/// CertStatus ::= CHOICE {
169-
/// good [0] IMPLICIT NULL,
170-
/// revoked [1] IMPLICIT RevokedInfo,
171-
/// unknown [2] IMPLICIT UnknownInfo }
172-
/// ```
173-
///
174-
/// [RFC 6960 Section 4.2.1]: https://datatracker.ietf.org/doc/html/rfc6960#section-4.2.1
175-
#[derive(Clone, Debug, Eq, PartialEq, Choice)]
176-
#[allow(missing_docs)]
177-
pub enum CertStatus {
178-
#[asn1(context_specific = "0", tag_mode = "IMPLICIT")]
179-
Good(Null),
180-
181-
#[asn1(context_specific = "1", tag_mode = "IMPLICIT", constructed = "true")]
182-
Revoked(RevokedInfo),
127+
#[cfg(feature = "builder")]
128+
mod builder {
129+
use crate::{builder::Error, CertId, CertStatus, OcspGeneralizedTime, SingleResponse};
130+
use const_oid::AssociatedOid;
131+
use digest::Digest;
132+
use x509_cert::{
133+
crl::CertificateList, ext::AsExtension, name::Name, serial_number::SerialNumber,
134+
Certificate,
135+
};
136+
137+
impl SingleResponse {
138+
/// Returns a `SingleResponse` given the `CertID`, `CertStatus`, and `This Update`. `Next
139+
/// Update` is set to `None`.
140+
pub fn new(
141+
cert_id: CertId,
142+
cert_status: CertStatus,
143+
this_update: OcspGeneralizedTime,
144+
) -> Self {
145+
Self {
146+
cert_id,
147+
cert_status,
148+
this_update,
149+
next_update: None,
150+
single_extensions: None,
151+
}
152+
}
183153

184-
#[asn1(context_specific = "2", tag_mode = "IMPLICIT")]
185-
Unknown(UnknownInfo),
186-
}
154+
/// Sets `thisUpdate` in the `singleResponse` as defined in [RFC 6960 Section 4.2.1].
155+
///
156+
/// [RFC 6960 Section 4.2.1]: https://datatracker.ietf.org/doc/html/rfc6960#section-4.2.1
157+
pub fn with_this_update(mut self, this_update: OcspGeneralizedTime) -> Self {
158+
self.this_update = this_update;
159+
self
160+
}
187161

188-
/// RevokedInfo structure as defined in [RFC 6960 Section 4.2.1].
189-
///
190-
/// ```text
191-
/// RevokedInfo ::= SEQUENCE {
192-
/// revocationTime GeneralizedTime,
193-
/// revocationReason [0] EXPLICIT CRLReason OPTIONAL }
194-
/// ```
195-
///
196-
/// [RFC 6960 Section 4.2.1]: https://datatracker.ietf.org/doc/html/rfc6960#section-4.2.1
197-
#[derive(Clone, Debug, Eq, PartialEq, Sequence)]
198-
#[allow(missing_docs)]
199-
pub struct RevokedInfo {
200-
pub revocation_time: GeneralizedTime,
162+
/// Sets `nextUpdate` in the `singleResponse` as defined in [RFC 6960 Section 4.2.1].
163+
///
164+
/// [RFC 6960 Section 4.2.1]: https://datatracker.ietf.org/doc/html/rfc6960#section-4.2.1
165+
pub fn with_next_update(mut self, next_update: OcspGeneralizedTime) -> Self {
166+
self.next_update = Some(next_update);
167+
self
168+
}
201169

202-
#[asn1(context_specific = "0", optional = "true", tag_mode = "EXPLICIT")]
203-
pub revocation_reason: Option<CrlReason>,
204-
}
170+
/// Adds a single response extension as specified in [RFC 6960 Section 4.4]. Errors when the
171+
/// extension encoding fails.
172+
///
173+
/// [RFC 6960 Section 4.4]: https://datatracker.ietf.org/doc/html/rfc6960#section-4.4
174+
pub fn with_extension(mut self, ext: impl AsExtension) -> Result<Self, Error> {
175+
let ext = ext.to_extension(&Name::default(), &[])?;
176+
match self.single_extensions {
177+
Some(ref mut exts) => exts.push(ext),
178+
None => self.single_extensions = Some(alloc::vec![ext]),
179+
}
180+
Ok(self)
181+
}
205182

206-
impl From<&RevokedCert> for RevokedInfo {
207-
fn from(rc: &RevokedCert) -> Self {
208-
Self {
209-
revocation_time: match rc.revocation_date {
210-
Time::UtcTime(t) => GeneralizedTime::from_date_time(t.to_date_time()),
211-
Time::GeneralTime(t) => t,
212-
},
213-
revocation_reason: if let Some(extensions) = &rc.crl_entry_extensions {
214-
let mut filter = extensions
215-
.iter()
216-
.filter(|ext| ext.extn_id == CrlReason::OID);
217-
match filter.next() {
218-
None => None,
219-
Some(ext) => match CrlReason::from_der(ext.extn_value.as_bytes()) {
220-
Ok(reason) => Some(reason),
221-
Err(_) => None,
222-
},
183+
/// Returns a `SingleResponse` by searching through the CRL to see if `serial` is revoked. If
184+
/// not, the `CertStatus` is set to good. The `CertID` is built from the issuer and serial
185+
/// number. This method does not ensure the CRL is issued by the issuer and only asserts the
186+
/// serial is not revoked in the provided CRL.
187+
///
188+
/// `thisUpdate` and `nextUpdate` will be pulled from the CRL.
189+
///
190+
/// NOTE: this method complies with [RFC 2560 Section 2.2] and not [RFC 6960 Section 2.2].
191+
/// [RFC 6960] limits the `good` status to only issued certificates. [RFC 2560] only asserts
192+
/// the serial was not revoked and makes no assertion the serial was ever issued.
193+
///
194+
/// [RFC 2560]: https://datatracker.ietf.org/doc/html/rfc2560
195+
/// [RFC 2560 Section 2.2]: https://datatracker.ietf.org/doc/html/rfc2560#section-2.2
196+
/// [RFC 6960]: https://datatracker.ietf.org/doc/html/rfc6960
197+
/// [RFC 6960 Section 2.2]: https://datatracker.ietf.org/doc/html/rfc6960#section-2.2
198+
pub fn from_crl<D>(
199+
issuer: &Certificate,
200+
crl: &CertificateList,
201+
serial_number: SerialNumber,
202+
) -> Result<Self, Error>
203+
where
204+
D: Digest + AssociatedOid,
205+
{
206+
let cert_status = match &crl.tbs_cert_list.revoked_certificates {
207+
Some(revoked_certs) => {
208+
let mut filter = revoked_certs
209+
.iter()
210+
.filter(|rc| rc.serial_number == serial_number);
211+
match filter.next() {
212+
None => CertStatus::good(),
213+
Some(rc) => CertStatus::revoked(rc),
214+
}
223215
}
224-
} else {
225-
None
226-
},
216+
None => CertStatus::good(),
217+
};
218+
let cert_id = CertId::from_issuer::<D>(issuer, serial_number)?;
219+
let this_update = crl.tbs_cert_list.this_update.into();
220+
let next_update = crl.tbs_cert_list.next_update.map(|t| t.into());
221+
Ok(Self {
222+
cert_id,
223+
cert_status,
224+
this_update,
225+
next_update,
226+
single_extensions: None,
227+
})
227228
}
228229
}
229230
}
230-
231-
impl From<RevokedCert> for RevokedInfo {
232-
fn from(rc: RevokedCert) -> Self {
233-
Self::from(&rc)
234-
}
235-
}
236-
237-
/// RevokedInfo structure as defined in [RFC 6960 Section 4.2.1].
238-
///
239-
/// ```text
240-
/// UnknownInfo ::= NULL
241-
/// ```
242-
///
243-
/// [RFC 6960 Section 4.2.1]: https://datatracker.ietf.org/doc/html/rfc6960#section-4.2.1
244-
pub type UnknownInfo = Null;

0 commit comments

Comments
 (0)