Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

x509-ocsp: implement builder #1259

Merged
merged 28 commits into from
Jan 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
4ea22bc
Fixed CrlReferences
bhesh Nov 5, 2023
36bbb6b
added a ton of x509-ocsp tests--fixed some things
bhesh Nov 7, 2023
7082fbe
fixed unused imports
bhesh Nov 7, 2023
4c7b7f1
changed to from()
bhesh Nov 7, 2023
1cdc67e
rudimentary builders
bhesh Nov 8, 2023
0993de0
make doc a bit more readable
bhesh Nov 8, 2023
d2ee92c
added SingleResponse builder
bhesh Nov 8, 2023
a5fb6cd
changed some things
bhesh Nov 12, 2023
98de978
moved time to its own module
bhesh Nov 12, 2023
25e2ead
fix doc tests for std feature
bhesh Nov 12, 2023
49cbc58
formatting
bhesh Nov 12, 2023
ea04fed
added nonce convenience
bhesh Nov 13, 2023
858c94b
clone/debug for builders
bhesh Nov 13, 2023
b13c3fc
documentation and remove reference time conversions
bhesh Nov 13, 2023
637f1cc
added copy where it could be
bhesh Nov 13, 2023
f0b90b3
refactored to keep builder methods in the expected files
bhesh Nov 13, 2023
f835a8d
added builder tests
bhesh Nov 13, 2023
36ffa2b
added revoked response builder test
bhesh Nov 13, 2023
90417a2
doc error
bhesh Nov 13, 2023
e9d9e42
test formatting
bhesh Nov 13, 2023
ae62ab6
finalizing version 1 of the builder
Nov 17, 2023
91aa84c
impl Display for builder::Error
bhesh Nov 17, 2023
37ab4a6
clippy errors in tests
bhesh Nov 17, 2023
0dc174c
merge master and update workflow to skip std on cross-compile
bhesh Nov 17, 2023
bd96ff3
nightly_cmd -> nightly-cmd
bhesh Nov 18, 2023
bef4142
fix rand and builder feature dependencies
bhesh Nov 18, 2023
6ef03c8
Restrict ArchiveCutoff OID
bhesh Nov 20, 2023
bbf727f
Merge branch 'master' into x509-ocsp/implement-builder
bhesh Jan 7, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/x509-ocsp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
toolchain: ${{ matrix.rust }}
targets: ${{ matrix.target }}
- uses: RustCrypto/actions/cargo-hack-install@master
- run: cargo hack build --target ${{ matrix.target }} --feature-powerset
- run: cargo hack build --target ${{ matrix.target }} --feature-powerset --exclude-features std

minimal-versions:
uses: RustCrypto/actions/.github/workflows/minimal-versions.yml@master
Expand Down
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 13 additions & 1 deletion x509-ocsp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,23 @@ der = { version = "0.7.8", features = ["alloc", "derive", "oid"] }
spki = { version = "0.7.2", features = ["alloc"] }
x509-cert = { version = "0.2.4", default-features = false }

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

[dev-dependencies]
hex-literal = "0.4.1"
lazy_static = "1.4.0"
rand = "0.8.5"
rsa = { version = "0.9.2", default-features = false, features = ["sha2"] }
sha1 = { version = "0.10.6", default-features = false, features = ["oid"] }
sha2 = { version = "0.10.8", default-features = false, features = ["oid"] }

[features]
rand = ["rand_core"]
builder = ["digest", "rand", "signature"]
std = ["der/std", "x509-cert/std"]

[package.metadata.docs.rs]
all-features = true
Expand Down
290 changes: 138 additions & 152 deletions x509-ocsp/src/basic.rs
Original file line number Diff line number Diff line change
@@ -1,37 +1,20 @@
//! Basic OCSP Response

use crate::{
ext::Nonce, AsResponseBytes, CertId, CertStatus, OcspGeneralizedTime, ResponderId, Version,
};
use alloc::vec::Vec;
use const_oid::AssociatedOid;
use const_oid::{
db::rfc6960::{ID_PKIX_OCSP_BASIC, ID_PKIX_OCSP_NONCE},
AssociatedOid,
};
use core::{default::Default, option::Option};
use der::{
asn1::{BitString, GeneralizedTime, Null, OctetString},
Choice, Decode, Enumerated, Sequence,
asn1::{BitString, ObjectIdentifier},
Decode, Sequence,
};
use spki::AlgorithmIdentifierOwned;
use x509_cert::{
certificate::Certificate,
crl::RevokedCert,
ext::{pkix::CrlReason, Extensions},
name::Name,
serial_number::SerialNumber,
time::Time,
};

/// OCSP `Version` as defined in [RFC 6960 Section 4.1.1].
///
/// ```text
/// Version ::= INTEGER { v1(0) }
/// ```
///
/// [RFC 6960 Section 4.1.1]: https://datatracker.ietf.org/doc/html/rfc6960#section-4.1.1
#[derive(Clone, Debug, Default, Copy, PartialEq, Eq, Enumerated)]
#[asn1(type = "INTEGER")]
#[repr(u8)]
pub enum Version {
/// Version 1 (default)
#[default]
V1 = 0,
}
use x509_cert::{certificate::Certificate, ext::Extensions};

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

impl BasicOcspResponse {
/// Returns the response's nonce value, if any. This method will return `None` if the response
/// has no `Nonce` extension or decoding of the `Nonce` extension fails.
pub fn nonce(&self) -> Option<Nonce> {
self.tbs_response_data.nonce()
}
}

impl AssociatedOid for BasicOcspResponse {
const OID: ObjectIdentifier = ID_PKIX_OCSP_BASIC;
}

impl AsResponseBytes for BasicOcspResponse {}

/// ResponseData structure as defined in [RFC 6960 Section 4.2.1].
///
/// ```text
Expand All @@ -77,45 +74,30 @@ pub struct ResponseData {
)]
pub version: Version,
pub responder_id: ResponderId,
pub produced_at: GeneralizedTime,
pub produced_at: OcspGeneralizedTime,
pub responses: Vec<SingleResponse>,

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

/// ResponderID structure as defined in [RFC 6960 Section 4.2.1].
///
/// ```text
/// ResponderID ::= CHOICE {
/// byName [1] Name,
/// byKey [2] KeyHash }
/// ```
///
/// [RFC 6960 Section 4.2.1]: https://datatracker.ietf.org/doc/html/rfc6960#section-4.2.1
#[derive(Clone, Debug, Eq, PartialEq, Choice)]
#[allow(missing_docs)]
pub enum ResponderId {
#[asn1(context_specific = "1", tag_mode = "EXPLICIT", constructed = "true")]
ByName(Name),

#[asn1(context_specific = "2", tag_mode = "EXPLICIT", constructed = "true")]
ByKey(KeyHash),
impl ResponseData {
/// Returns the response's nonce value, if any. This method will return `None` if the response
/// has no `Nonce` extension or decoding of the `Nonce` extension fails.
pub fn nonce(&self) -> Option<Nonce> {
match &self.response_extensions {
Some(extns) => {
let mut filter = extns.iter().filter(|e| e.extn_id == ID_PKIX_OCSP_NONCE);
match filter.next() {
Some(extn) => Nonce::from_der(extn.extn_value.as_bytes()).ok(),
None => None,
}
}
None => None,
}
}
}

/// KeyHash structure as defined in [RFC 6960 Section 4.2.1].
///
/// ```text
/// KeyHash ::= OCTET STRING -- SHA-1 hash of responder's public key
/// -- (i.e., the SHA-1 hash of the value of the
/// -- BIT STRING subjectPublicKey [excluding
/// -- the tag, length, and number of unused
/// -- bits] in the responder's certificate)
/// ```
///
/// [RFC 6960 Section 4.2.1]: https://datatracker.ietf.org/doc/html/rfc6960#section-4.2.1
pub type KeyHash = OctetString;

/// SingleResponse structure as defined in [RFC 6960 Section 4.2.1].
///
/// ```text
Expand All @@ -133,112 +115,116 @@ pub type KeyHash = OctetString;
pub struct SingleResponse {
pub cert_id: CertId,
pub cert_status: CertStatus,
pub this_update: GeneralizedTime,
pub this_update: OcspGeneralizedTime,

#[asn1(context_specific = "0", optional = "true", tag_mode = "EXPLICIT")]
pub next_update: Option<GeneralizedTime>,
pub next_update: Option<OcspGeneralizedTime>,

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

/// CertID structure as defined in [RFC 6960 Section 4.1.1].
///
/// ```text
/// CertID ::= SEQUENCE {
/// hashAlgorithm AlgorithmIdentifier,
/// issuerNameHash OCTET STRING, -- Hash of issuer's DN
/// issuerKeyHash OCTET STRING, -- Hash of issuer's public key
/// serialNumber CertificateSerialNumber }
/// ```
///
/// [RFC 6960 Section 4.1.1]: https://datatracker.ietf.org/doc/html/rfc6960#section-4.1.1
#[derive(Clone, Debug, Eq, PartialEq, Sequence)]
#[allow(missing_docs)]
pub struct CertId {
pub hash_algorithm: AlgorithmIdentifierOwned,
pub issuer_name_hash: OctetString,
pub issuer_key_hash: OctetString,
pub serial_number: SerialNumber,
}

/// CertStatus structure as defined in [RFC 6960 Section 4.2.1].
///
/// ```text
/// CertStatus ::= CHOICE {
/// good [0] IMPLICIT NULL,
/// revoked [1] IMPLICIT RevokedInfo,
/// unknown [2] IMPLICIT UnknownInfo }
/// ```
///
/// [RFC 6960 Section 4.2.1]: https://datatracker.ietf.org/doc/html/rfc6960#section-4.2.1
#[derive(Clone, Debug, Eq, PartialEq, Choice)]
#[allow(missing_docs)]
pub enum CertStatus {
#[asn1(context_specific = "0", tag_mode = "IMPLICIT")]
Good(Null),

#[asn1(context_specific = "1", tag_mode = "IMPLICIT", constructed = "true")]
Revoked(RevokedInfo),
#[cfg(feature = "builder")]
mod builder {
use crate::{builder::Error, CertId, CertStatus, OcspGeneralizedTime, SingleResponse};
use const_oid::AssociatedOid;
use digest::Digest;
use x509_cert::{
crl::CertificateList, ext::AsExtension, name::Name, serial_number::SerialNumber,
Certificate,
};

impl SingleResponse {
/// Returns a `SingleResponse` given the `CertID`, `CertStatus`, and `This Update`. `Next
/// Update` is set to `None`.
pub fn new(
cert_id: CertId,
cert_status: CertStatus,
this_update: OcspGeneralizedTime,
) -> Self {
Self {
cert_id,
cert_status,
this_update,
next_update: None,
single_extensions: None,
}
}

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

/// RevokedInfo structure as defined in [RFC 6960 Section 4.2.1].
///
/// ```text
/// RevokedInfo ::= SEQUENCE {
/// revocationTime GeneralizedTime,
/// revocationReason [0] EXPLICIT CRLReason OPTIONAL }
/// ```
///
/// [RFC 6960 Section 4.2.1]: https://datatracker.ietf.org/doc/html/rfc6960#section-4.2.1
#[derive(Clone, Debug, Eq, PartialEq, Sequence)]
#[allow(missing_docs)]
pub struct RevokedInfo {
pub revocation_time: GeneralizedTime,
/// Sets `nextUpdate` in the `singleResponse` as defined in [RFC 6960 Section 4.2.1].
///
/// [RFC 6960 Section 4.2.1]: https://datatracker.ietf.org/doc/html/rfc6960#section-4.2.1
pub fn with_next_update(mut self, next_update: OcspGeneralizedTime) -> Self {
self.next_update = Some(next_update);
self
}

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

impl From<&RevokedCert> for RevokedInfo {
fn from(rc: &RevokedCert) -> Self {
Self {
revocation_time: match rc.revocation_date {
Time::UtcTime(t) => GeneralizedTime::from_date_time(t.to_date_time()),
Time::GeneralTime(t) => t,
},
revocation_reason: if let Some(extensions) = &rc.crl_entry_extensions {
let mut filter = extensions
.iter()
.filter(|ext| ext.extn_id == CrlReason::OID);
match filter.next() {
None => None,
Some(ext) => match CrlReason::from_der(ext.extn_value.as_bytes()) {
Ok(reason) => Some(reason),
Err(_) => None,
},
/// Returns a `SingleResponse` by searching through the CRL to see if `serial` is revoked. If
/// not, the `CertStatus` is set to good. The `CertID` is built from the issuer and serial
/// number. This method does not ensure the CRL is issued by the issuer and only asserts the
/// serial is not revoked in the provided CRL.
///
/// `thisUpdate` and `nextUpdate` will be pulled from the CRL.
///
/// NOTE: this method complies with [RFC 2560 Section 2.2] and not [RFC 6960 Section 2.2].
/// [RFC 6960] limits the `good` status to only issued certificates. [RFC 2560] only asserts
/// the serial was not revoked and makes no assertion the serial was ever issued.
///
/// [RFC 2560]: https://datatracker.ietf.org/doc/html/rfc2560
/// [RFC 2560 Section 2.2]: https://datatracker.ietf.org/doc/html/rfc2560#section-2.2
/// [RFC 6960]: https://datatracker.ietf.org/doc/html/rfc6960
/// [RFC 6960 Section 2.2]: https://datatracker.ietf.org/doc/html/rfc6960#section-2.2
pub fn from_crl<D>(
issuer: &Certificate,
crl: &CertificateList,
serial_number: SerialNumber,
) -> Result<Self, Error>
where
D: Digest + AssociatedOid,
{
let cert_status = match &crl.tbs_cert_list.revoked_certificates {
Some(revoked_certs) => {
let mut filter = revoked_certs
.iter()
.filter(|rc| rc.serial_number == serial_number);
match filter.next() {
None => CertStatus::good(),
Some(rc) => CertStatus::revoked(rc),
}
}
} else {
None
},
None => CertStatus::good(),
};
let cert_id = CertId::from_issuer::<D>(issuer, serial_number)?;
let this_update = crl.tbs_cert_list.this_update.into();
let next_update = crl.tbs_cert_list.next_update.map(|t| t.into());
Ok(Self {
cert_id,
cert_status,
this_update,
next_update,
single_extensions: None,
})
}
}
}

impl From<RevokedCert> for RevokedInfo {
fn from(rc: RevokedCert) -> Self {
Self::from(&rc)
}
}

/// RevokedInfo structure as defined in [RFC 6960 Section 4.2.1].
///
/// ```text
/// UnknownInfo ::= NULL
/// ```
///
/// [RFC 6960 Section 4.2.1]: https://datatracker.ietf.org/doc/html/rfc6960#section-4.2.1
pub type UnknownInfo = Null;
Loading