From c59f97e8229e6b845bc98ec5a373196c96232147 Mon Sep 17 00:00:00 2001 From: bhesh Date: Sun, 7 Jan 2024 19:23:51 -0500 Subject: [PATCH] x509-ocsp: implement builder (#1259) --- .github/workflows/x509-ocsp.yml | 2 +- Cargo.lock | 2 + x509-ocsp/Cargo.toml | 14 +- x509-ocsp/src/basic.rs | 290 +++++---- x509-ocsp/src/builder.rs | 53 ++ x509-ocsp/src/builder/request.rs | 165 +++++ x509-ocsp/src/builder/response.rs | 184 ++++++ x509-ocsp/src/cert_id.rs | 96 +++ x509-ocsp/src/cert_status.rs | 116 ++++ x509-ocsp/src/ext.rs | 56 +- x509-ocsp/src/lib.rs | 39 +- x509-ocsp/src/request.rs | 93 ++- x509-ocsp/src/responder_id.rs | 48 ++ x509-ocsp/src/response.rs | 47 +- x509-ocsp/src/time.rs | 45 ++ x509-ocsp/tests/builder.rs | 436 ++++++++++++++ .../tests/examples/DODEMAILCA_63-resp.der | Bin 0 -> 3721 bytes x509-ocsp/tests/examples/ocsp-by-key-res.der | Bin 0 -> 511 bytes .../tests/examples/ocsp-dtm-no-chain-res.der | Bin 0 -> 526 bytes .../tests/examples/ocsp-internal-error.der | 2 + x509-ocsp/tests/examples/ocsp-malformed.der | 2 + .../examples/ocsp-multiple-exts-clean-req.der | Bin 0 -> 261 bytes .../tests/examples/ocsp-multiple-exts-res.der | Bin 0 -> 1453 bytes .../ocsp-multiple-requests-nonce-req.der | Bin 0 -> 777 bytes .../examples/ocsp-multiple-requests-req.der | Bin 0 -> 724 bytes .../examples/ocsp-multiple-responses-res.der | Bin 0 -> 2215 bytes .../tests/examples/ocsp-sig-required.der | 2 + x509-ocsp/tests/examples/ocsp-signed-req.der | Bin 0 -> 1055 bytes x509-ocsp/tests/examples/ocsp-try-later.der | 2 + .../tests/examples/ocsp-unauthorized.der | 2 + .../tests/examples/rsa-2048-sha256-ca-crl.der | Bin 0 -> 380 bytes .../tests/examples/rsa-2048-sha256-ca-key.der | Bin 0 -> 1217 bytes .../tests/examples/rsa-2048-sha256-ca.der | Bin 0 -> 815 bytes .../examples/rsa-2048-sha256-crt-key.der | Bin 0 -> 1218 bytes .../tests/examples/rsa-2048-sha256-crt.der | Bin 0 -> 691 bytes .../examples/rsa-2048-sha256-ocsp-crt-key.der | Bin 0 -> 1218 bytes .../examples/rsa-2048-sha256-ocsp-crt.der | Bin 0 -> 814 bytes .../rsa-2048-sha256-revoked-ocsp-res.der | Bin 0 -> 1310 bytes .../tests/examples/sha1-certid-ocsp-req.der | Bin 0 -> 70 bytes .../tests/examples/sha1-certid-ocsp-res.der | Bin 0 -> 1296 bytes .../tests/examples/sha224-certid-ocsp-req.der | Bin 0 -> 90 bytes .../tests/examples/sha256-certid-ocsp-req.der | Bin 0 -> 98 bytes .../tests/examples/sha256-certid-ocsp-res.der | Bin 0 -> 1326 bytes .../tests/examples/sha384-certid-ocsp-req.der | Bin 0 -> 131 bytes .../tests/examples/sha512-certid-ocsp-req.der | Bin 0 -> 167 bytes .../tests/examples/sha512-certid-ocsp-res.der | Bin 0 -> 1391 bytes x509-ocsp/tests/ext.rs | 220 +++++++ x509-ocsp/tests/ocsp.rs | 29 +- x509-ocsp/tests/requests.rs | 444 ++++++++++++++ x509-ocsp/tests/responses.rs | 568 ++++++++++++++++++ 50 files changed, 2758 insertions(+), 199 deletions(-) create mode 100644 x509-ocsp/src/builder.rs create mode 100644 x509-ocsp/src/builder/request.rs create mode 100644 x509-ocsp/src/builder/response.rs create mode 100644 x509-ocsp/src/cert_id.rs create mode 100644 x509-ocsp/src/cert_status.rs create mode 100644 x509-ocsp/src/responder_id.rs create mode 100644 x509-ocsp/src/time.rs create mode 100644 x509-ocsp/tests/builder.rs create mode 100644 x509-ocsp/tests/examples/DODEMAILCA_63-resp.der create mode 100644 x509-ocsp/tests/examples/ocsp-by-key-res.der create mode 100644 x509-ocsp/tests/examples/ocsp-dtm-no-chain-res.der create mode 100644 x509-ocsp/tests/examples/ocsp-internal-error.der create mode 100644 x509-ocsp/tests/examples/ocsp-malformed.der create mode 100644 x509-ocsp/tests/examples/ocsp-multiple-exts-clean-req.der create mode 100644 x509-ocsp/tests/examples/ocsp-multiple-exts-res.der create mode 100644 x509-ocsp/tests/examples/ocsp-multiple-requests-nonce-req.der create mode 100644 x509-ocsp/tests/examples/ocsp-multiple-requests-req.der create mode 100644 x509-ocsp/tests/examples/ocsp-multiple-responses-res.der create mode 100644 x509-ocsp/tests/examples/ocsp-sig-required.der create mode 100644 x509-ocsp/tests/examples/ocsp-signed-req.der create mode 100644 x509-ocsp/tests/examples/ocsp-try-later.der create mode 100644 x509-ocsp/tests/examples/ocsp-unauthorized.der create mode 100644 x509-ocsp/tests/examples/rsa-2048-sha256-ca-crl.der create mode 100644 x509-ocsp/tests/examples/rsa-2048-sha256-ca-key.der create mode 100644 x509-ocsp/tests/examples/rsa-2048-sha256-ca.der create mode 100644 x509-ocsp/tests/examples/rsa-2048-sha256-crt-key.der create mode 100644 x509-ocsp/tests/examples/rsa-2048-sha256-crt.der create mode 100644 x509-ocsp/tests/examples/rsa-2048-sha256-ocsp-crt-key.der create mode 100644 x509-ocsp/tests/examples/rsa-2048-sha256-ocsp-crt.der create mode 100644 x509-ocsp/tests/examples/rsa-2048-sha256-revoked-ocsp-res.der create mode 100644 x509-ocsp/tests/examples/sha1-certid-ocsp-req.der create mode 100644 x509-ocsp/tests/examples/sha1-certid-ocsp-res.der create mode 100644 x509-ocsp/tests/examples/sha224-certid-ocsp-req.der create mode 100644 x509-ocsp/tests/examples/sha256-certid-ocsp-req.der create mode 100644 x509-ocsp/tests/examples/sha256-certid-ocsp-res.der create mode 100644 x509-ocsp/tests/examples/sha384-certid-ocsp-req.der create mode 100644 x509-ocsp/tests/examples/sha512-certid-ocsp-req.der create mode 100644 x509-ocsp/tests/examples/sha512-certid-ocsp-res.der create mode 100644 x509-ocsp/tests/ext.rs create mode 100644 x509-ocsp/tests/requests.rs create mode 100644 x509-ocsp/tests/responses.rs diff --git a/.github/workflows/x509-ocsp.yml b/.github/workflows/x509-ocsp.yml index 893474599..779fccaf6 100644 --- a/.github/workflows/x509-ocsp.yml +++ b/.github/workflows/x509-ocsp.yml @@ -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 diff --git a/Cargo.lock b/Cargo.lock index 9dc998c11..d0a015eb0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1958,6 +1958,8 @@ dependencies = [ "const-oid 0.9.6", "der 0.7.8", "hex-literal 0.4.1", + "lazy_static", + "rand", "rand_core", "spki 0.7.3", "x509-cert", diff --git a/x509-ocsp/Cargo.toml b/x509-ocsp/Cargo.toml index 7930fc96e..41127d59e 100644 --- a/x509-ocsp/Cargo.toml +++ b/x509-ocsp/Cargo.toml @@ -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 diff --git a/x509-ocsp/src/basic.rs b/x509-ocsp/src/basic.rs index 1b51308fe..876e01563 100644 --- a/x509-ocsp/src/basic.rs +++ b/x509-ocsp/src/basic.rs @@ -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]. /// @@ -55,6 +38,20 @@ pub struct BasicOcspResponse { pub certs: Option>, } +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 { + 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 @@ -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, #[asn1(context_specific = "1", optional = "true", tag_mode = "EXPLICIT")] pub response_extensions: Option, } -/// 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 { + 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 @@ -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, + pub next_update: Option, #[asn1(context_specific = "1", optional = "true", tag_mode = "EXPLICIT")] pub single_extensions: Option, } -/// 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, -} + /// 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 { + 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( + issuer: &Certificate, + crl: &CertificateList, + serial_number: SerialNumber, + ) -> Result + 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::(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 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; diff --git a/x509-ocsp/src/builder.rs b/x509-ocsp/src/builder.rs new file mode 100644 index 000000000..d46389619 --- /dev/null +++ b/x509-ocsp/src/builder.rs @@ -0,0 +1,53 @@ +//! OCSP builder module + +use alloc::fmt; + +mod request; +mod response; + +pub use self::request::OcspRequestBuilder; +pub use self::response::OcspResponseBuilder; + +/// Error type +#[derive(Debug)] +pub enum Error { + /// ASN.1 DER-related errors + Asn1(der::Error), + + /// Public key errors + PublicKey(spki::Error), + + /// Signing errors + Signature(signature::Error), +} + +#[cfg(feature = "std")] +impl std::error::Error for Error {} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::Asn1(err) => write!(f, "ASN.1 error: {}", err), + Error::PublicKey(err) => write!(f, "public key error: {}", err), + Error::Signature(err) => write!(f, "signature error: {}", err), + } + } +} + +impl From for Error { + fn from(other: der::Error) -> Self { + Self::Asn1(other) + } +} + +impl From for Error { + fn from(other: spki::Error) -> Self { + Self::PublicKey(other) + } +} + +impl From for Error { + fn from(other: signature::Error) -> Self { + Self::Signature(other) + } +} diff --git a/x509-ocsp/src/builder/request.rs b/x509-ocsp/src/builder/request.rs new file mode 100644 index 000000000..eb7902d92 --- /dev/null +++ b/x509-ocsp/src/builder/request.rs @@ -0,0 +1,165 @@ +//! OCSP request builder + +use crate::{builder::Error, OcspRequest, Request, Signature, TbsRequest, Version}; +use alloc::vec::Vec; +use der::Encode; +use rand_core::CryptoRngCore; +use signature::{RandomizedSigner, Signer}; +use spki::{DynSignatureAlgorithmIdentifier, SignatureBitStringEncoding}; +use x509_cert::{ + ext::{pkix::name::GeneralName, AsExtension}, + name::Name, + Certificate, +}; + +/// X509 OCSP Request builder +/// +/// ``` +/// use der::Decode; +/// use sha1::Sha1; +/// use x509_cert::{serial_number::SerialNumber, Certificate}; +/// use x509_ocsp::builder::OcspRequestBuilder; +/// use x509_ocsp::{ext::Nonce, Request}; +/// +/// # const ISSUER_DER: &[u8] = include_bytes!("../../tests/examples/rsa-2048-sha256-ca.der"); +/// # const CERT_DER: &[u8] = include_bytes!("../../tests/examples/rsa-2048-sha256-crt.der"); +/// # const KEY_DER: &[u8] = include_bytes!("../../tests/examples/rsa-2048-sha256-crt-key.der"); +/// # use rsa::{pkcs1v15::SigningKey, pkcs8::DecodePrivateKey}; +/// # use sha2::Sha256; +/// # fn rsa_signer() -> SigningKey { +/// # let private_key = rsa::RsaPrivateKey::from_pkcs8_der(KEY_DER).unwrap(); +/// # let signing_key = SigningKey::::new(private_key); +/// # signing_key +/// # } +/// let issuer = Certificate::from_der(ISSUER_DER).unwrap(); +/// let cert = Certificate::from_der(CERT_DER).unwrap(); +/// +/// let req = OcspRequestBuilder::default() +/// .with_request(Request::from_cert::(&issuer, &cert).unwrap()) +/// .build(); +/// +/// let mut rng = rand::thread_rng(); +/// +/// let req = OcspRequestBuilder::default() +/// .with_request(Request::from_issuer::(&issuer, SerialNumber::from(2usize)).unwrap()) +/// .with_request(Request::from_issuer::(&issuer, SerialNumber::from(3usize)).unwrap()) +/// .with_request(Request::from_issuer::(&issuer, SerialNumber::from(4usize)).unwrap()) +/// .with_extension(Nonce::generate(&mut rng, 32).unwrap()) +/// .unwrap() +/// .build(); +/// +/// let mut signer = rsa_signer(); +/// let signer_cert_chain = vec![cert.clone()]; +/// let req = OcspRequestBuilder::default() +/// .with_request(Request::from_cert::(&issuer, &cert).unwrap()) +/// .with_extension(Nonce::generate(&mut rng, 32).unwrap()) +/// .unwrap() +/// .sign(&mut signer, Some(signer_cert_chain)) +/// .unwrap(); +/// ``` +#[derive(Clone, Debug, Default)] +pub struct OcspRequestBuilder { + tbs: TbsRequest, +} + +impl OcspRequestBuilder { + /// Returns an `OcspRequestBuilder` with the specified [`Version`] + pub fn new(version: Version) -> Self { + Self { + tbs: TbsRequest { + version, + requestor_name: None, + request_list: Vec::new(), + request_extensions: None, + }, + } + } + + /// Sets the requestor name as specified in [RFC 6960 Section 4.1.1] + /// + /// [RFC 6960 Section 4.1.1]: https://datatracker.ietf.org/doc/html/rfc6960#section-4.1.1 + pub fn with_requestor_name(mut self, requestor_name: GeneralName) -> Self { + self.tbs.requestor_name = Some(requestor_name); + self + } + + /// Adds a [`Request`] to the builder as defined in [RFC 6960 Section 4.1.1]. + /// + /// [RFC 6960 Section 4.1.1]: https://datatracker.ietf.org/doc/html/rfc6960#section-4.1.1 + pub fn with_request(mut self, request: Request) -> Self { + self.tbs.request_list.push(request); + self + } + + /// Adds a request 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 { + let ext = ext.to_extension(&Name::default(), &[])?; + match self.tbs.request_extensions { + Some(ref mut exts) => exts.push(ext), + None => self.tbs.request_extensions = Some(alloc::vec![ext]), + } + Ok(self) + } + + /// Consumes the builder and returns an [`OcspRequest`] + pub fn build(self) -> OcspRequest { + OcspRequest { + tbs_request: self.tbs, + optional_signature: None, + } + } + + /// Consumes the builder and returns a signed [`OcspRequest`]. Errors when the algorithm + /// identifier encoding, message encoding, or signature generation fails. + pub fn sign( + self, + signer: &mut S, + certificate_chain: Option>, + ) -> Result + where + S: Signer + DynSignatureAlgorithmIdentifier, + Sig: SignatureBitStringEncoding, + { + let signature_algorithm = signer.signature_algorithm_identifier()?; + let signature = signer.try_sign(&self.tbs.to_der()?)?.to_bitstring()?; + let optional_signature = Some(Signature { + signature_algorithm, + signature, + certs: certificate_chain, + }); + Ok(OcspRequest { + tbs_request: self.tbs, + optional_signature, + }) + } + + /// Consumes the builder and returns a signed [`OcspRequest`]. Errors when the algorithm + /// identifier encoding, message encoding, or signature generation fails. + pub fn sign_with_rng( + self, + signer: &mut S, + rng: &mut impl CryptoRngCore, + certificate_chain: Option>, + ) -> Result + where + S: RandomizedSigner + DynSignatureAlgorithmIdentifier, + Sig: SignatureBitStringEncoding, + { + let signature_algorithm = signer.signature_algorithm_identifier()?; + let signature = signer + .try_sign_with_rng(rng, &self.tbs.to_der()?)? + .to_bitstring()?; + let optional_signature = Some(Signature { + signature_algorithm, + signature, + certs: certificate_chain, + }); + Ok(OcspRequest { + tbs_request: self.tbs, + optional_signature, + }) + } +} diff --git a/x509-ocsp/src/builder/response.rs b/x509-ocsp/src/builder/response.rs new file mode 100644 index 000000000..d77349d8d --- /dev/null +++ b/x509-ocsp/src/builder/response.rs @@ -0,0 +1,184 @@ +//! OCSP response builder + +use crate::{ + builder::Error, BasicOcspResponse, OcspGeneralizedTime, OcspResponse, ResponderId, + ResponseData, SingleResponse, Version, +}; +use alloc::vec::Vec; +use der::Encode; +use rand_core::CryptoRngCore; +use signature::{RandomizedSigner, Signer}; +use spki::{DynSignatureAlgorithmIdentifier, SignatureBitStringEncoding}; +use x509_cert::{ + ext::{AsExtension, Extensions}, + name::Name, + Certificate, +}; + +/// X509 OCSP Response builder +/// +/// ``` +/// use der::{asn1::ObjectIdentifier, DateTime, Decode}; +/// use x509_cert::Certificate; +/// use x509_ocsp::builder::OcspResponseBuilder; +/// use x509_ocsp::{ext::Nonce, CertStatus, OcspGeneralizedTime, OcspRequest, OcspResponse, +/// SingleResponse, +/// }; +/// +/// # const OCSP_REQ_DER: &[u8] = include_bytes!( +/// # "../../tests/examples/ocsp-multiple-requests-nonce-req.der" +/// # ); +/// # const CA_DER: &[u8] = include_bytes!("../../tests/examples/rsa-2048-sha256-ca.der"); +/// # const CA_KEY_DER: &[u8] = include_bytes!("../../tests/examples/rsa-2048-sha256-ca-key.der"); +/// # use rsa::{pkcs1v15::SigningKey, pkcs8::DecodePrivateKey}; +/// # use sha2::Sha256; +/// # fn rsa_signer() -> SigningKey { +/// # let private_key = rsa::RsaPrivateKey::from_pkcs8_der(CA_KEY_DER).unwrap(); +/// # let signing_key = SigningKey::::new(private_key); +/// # signing_key +/// # } +/// let req = OcspRequest::from_der(OCSP_REQ_DER).unwrap(); +/// let ca = Certificate::from_der(CA_DER).unwrap(); +/// +/// let mut builder = OcspResponseBuilder::new(ca.tbs_certificate.subject.clone()) +/// .with_single_response( +/// SingleResponse::new( +/// req.tbs_request.request_list[0].req_cert.clone(), +/// CertStatus::good(), +/// OcspGeneralizedTime::from(DateTime::new(2023, 10, 31, 0, 0, 0).unwrap()), +/// ) +/// .with_next_update(OcspGeneralizedTime::from( +/// DateTime::new(2024, 1, 1, 0, 0, 0).unwrap() +/// )), +/// ); +/// +/// if let Some(nonce) = req.nonce() { +/// builder = builder.with_extension(nonce).unwrap(); +/// } +/// +/// #[cfg(feature = "std")] +/// let now = OcspGeneralizedTime::try_from(std::time::SystemTime::now()).unwrap(); +/// +/// #[cfg(not(feature = "std"))] +/// let now = OcspGeneralizedTime::from( +/// DateTime::new(2023, 11, 1, 0, 0, 0).unwrap() +/// ); +/// +/// let mut signer = rsa_signer(); +/// let signer_cert_chain = vec![ca.clone()]; +/// let resp = builder +/// .sign(&mut signer, Some(signer_cert_chain), now) +/// .unwrap(); +/// ``` +#[derive(Clone, Debug)] +pub struct OcspResponseBuilder { + responder_id: ResponderId, + responses: Vec, + response_extensions: Option, +} + +impl OcspResponseBuilder { + /// Returns a `OcspResponseBuilder` given the [`Version`], [`ResponderId`], and `Produced + /// At` values. + pub fn new(responder_id: impl Into) -> Self { + let responder_id = responder_id.into(); + Self { + responder_id, + responses: Vec::new(), + response_extensions: None, + } + } + + /// Adds a [`SingleResponse`] to the builder 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_single_response(mut self, single_response: SingleResponse) -> Self { + self.responses.push(single_response); + self + } + + /// Adds a 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 { + let ext = ext.to_extension(&Name::default(), &[])?; + match self.response_extensions { + Some(ref mut exts) => exts.push(ext), + None => self.response_extensions = Some(alloc::vec![ext]), + } + Ok(self) + } + + /// Consume the builder and returns [`ResponseData`] + fn into_response_data(self, produced_at: OcspGeneralizedTime) -> ResponseData { + ResponseData { + version: Version::default(), + responder_id: self.responder_id, + produced_at, + responses: self.responses, + response_extensions: self.response_extensions, + } + } + + /// Consumes the builder and returns a signed [`OcspResponse`]. Errors when the algorithm + /// identifier encoding, message encoding, or signature generation fails. + /// + /// Per [RFC 6960 Section 2.4], the `producedAt` value must be the time the request was + /// signed. + /// + /// [RFC 6960 Section 2.4]: https://datatracker.ietf.org/doc/html/rfc6960#section-2.4 + pub fn sign( + self, + signer: &mut S, + certificate_chain: Option>, + produced_at: OcspGeneralizedTime, + ) -> Result + where + S: Signer + DynSignatureAlgorithmIdentifier, + Sig: SignatureBitStringEncoding, + { + let tbs_response_data = self.into_response_data(produced_at); + let signature_algorithm = signer.signature_algorithm_identifier()?; + let signature = signer + .try_sign(&tbs_response_data.to_der()?)? + .to_bitstring()?; + Ok(OcspResponse::successful(BasicOcspResponse { + tbs_response_data, + signature_algorithm, + signature, + certs: certificate_chain, + })?) + } + + /// Consumes the builder and returns a signed [`OcspResponse`]. Errors when the algorithm + /// identifier encoding, message encoding, or signature generation fails. + /// + /// Per [RFC 6960 Section 2.4], the `producedAt` value must be the time the request was + /// signed. + /// + /// [RFC 6960 Section 2.4]: https://datatracker.ietf.org/doc/html/rfc6960#section-2.4 + pub fn sign_with_rng( + self, + signer: &mut S, + rng: &mut impl CryptoRngCore, + certificate_chain: Option>, + produced_at: OcspGeneralizedTime, + ) -> Result + where + S: RandomizedSigner + DynSignatureAlgorithmIdentifier, + Sig: SignatureBitStringEncoding, + { + let tbs_response_data = self.into_response_data(produced_at); + let signature_algorithm = signer.signature_algorithm_identifier()?; + let signature = signer + .try_sign_with_rng(rng, &tbs_response_data.to_der()?)? + .to_bitstring()?; + Ok(OcspResponse::successful(BasicOcspResponse { + tbs_response_data, + signature_algorithm, + signature, + certs: certificate_chain, + })?) + } +} diff --git a/x509-ocsp/src/cert_id.rs b/x509-ocsp/src/cert_id.rs new file mode 100644 index 000000000..c8681f206 --- /dev/null +++ b/x509-ocsp/src/cert_id.rs @@ -0,0 +1,96 @@ +//! X.509 OCSP CertID + +use der::{asn1::OctetString, Sequence}; +use spki::AlgorithmIdentifierOwned; +use x509_cert::serial_number::SerialNumber; + +/// 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, +} + +impl From<&CertId> for CertId { + /// Clones the referenced `CertID` + fn from(other: &CertId) -> Self { + other.clone() + } +} + +#[cfg(feature = "builder")] +mod builder { + use crate::{builder::Error, CertId}; + use const_oid::AssociatedOid; + use der::{ + asn1::{Null, OctetString}, + Encode, + }; + use digest::Digest; + use spki::AlgorithmIdentifierOwned; + use x509_cert::{serial_number::SerialNumber, Certificate}; + + impl CertId { + /// Generates a `CertID` by running the issuer's subject and key through the specified + /// [`Digest`]. + /// + /// [RFC 6960 Section 4.1.1] + /// + /// [RFC 6960 Section 4.1.1]: https://datatracker.ietf.org/doc/html/rfc6960#section-4.1.1 + pub fn from_issuer( + issuer: &Certificate, + serial_number: SerialNumber, + ) -> Result + where + D: Digest + AssociatedOid, + { + Ok(Self { + hash_algorithm: AlgorithmIdentifierOwned { + oid: D::OID, + parameters: Some(Null.into()), + }, + issuer_name_hash: OctetString::new( + D::digest(issuer.tbs_certificate.subject.to_der()?).to_vec(), + )?, + issuer_key_hash: OctetString::new( + D::digest( + issuer + .tbs_certificate + .subject_public_key_info + .subject_public_key + .raw_bytes(), + ) + .to_vec(), + )?, + serial_number, + }) + } + + /// Generates a `CertID` by running the issuer's subject and key through the specified + /// [`Digest`] and pulls the serial from `cert`. This does not ensure that `cert` is actually + /// issued by `issuer`. + /// + /// [RFC 6960 Section 4.1.1] + /// + /// [RFC 6960 Section 4.1.1]: https://datatracker.ietf.org/doc/html/rfc6960#section-4.1.1 + pub fn from_cert(issuer: &Certificate, cert: &Certificate) -> Result + where + D: Digest + AssociatedOid, + { + Self::from_issuer::(issuer, cert.tbs_certificate.serial_number.clone()) + } + } +} diff --git a/x509-ocsp/src/cert_status.rs b/x509-ocsp/src/cert_status.rs new file mode 100644 index 000000000..e66d03fb5 --- /dev/null +++ b/x509-ocsp/src/cert_status.rs @@ -0,0 +1,116 @@ +//! X.509 OCSP CertStatus + +use crate::OcspGeneralizedTime; +use const_oid::AssociatedOid; +use core::option::Option; +use der::{asn1::Null, Choice, Decode, Sequence}; +use x509_cert::{crl::RevokedCert, ext::pkix::CrlReason}; + +/// 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(Copy, 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), + + #[asn1(context_specific = "2", tag_mode = "IMPLICIT")] + Unknown(UnknownInfo), +} + +impl CertStatus { + /// Returns `CertStatus` set to `good` 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 good() -> Self { + Self::Good(Null) + } + + /// Returns `CertStatus` set to `revoked` 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 revoked(info: impl Into) -> Self { + Self::Revoked(info.into()) + } + + /// Returns `CertStatus` set to `unknown` 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 unknown() -> Self { + Self::Unknown(Null) + } +} + +/// 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(Copy, Clone, Debug, Eq, PartialEq, Sequence)] +#[allow(missing_docs)] +pub struct RevokedInfo { + pub revocation_time: OcspGeneralizedTime, + + #[asn1(context_specific = "0", optional = "true", tag_mode = "EXPLICIT")] + pub revocation_reason: Option, +} + +/// 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; + +impl From<&RevokedInfo> for RevokedInfo { + fn from(info: &RevokedInfo) -> Self { + *info + } +} + +impl From<&RevokedCert> for RevokedInfo { + /// Converts [`RevokedCert`] to [`RevokedInfo`]. + /// + /// Attempts to extract the [`CrlReason`]. If it fails, the `CrlReason` is set to `None`. + fn from(rc: &RevokedCert) -> Self { + Self { + revocation_time: rc.revocation_date.into(), + revocation_reason: match &rc.crl_entry_extensions { + Some(extns) => { + let mut filter = extns.iter().filter(|extn| extn.extn_id == CrlReason::OID); + match filter.next() { + Some(extn) => CrlReason::from_der(extn.extn_value.as_bytes()).ok(), + None => None, + } + } + None => None, + }, + } + } +} + +impl From for RevokedInfo { + /// Converts [`RevokedCert`] to [`RevokedInfo`]. + /// + /// Attempts to extract the [`CrlReason`]. If it fails, the `CrlReason` is set to `None`. + fn from(rc: RevokedCert) -> Self { + Self::from(&rc) + } +} diff --git a/x509-ocsp/src/ext.rs b/x509-ocsp/src/ext.rs index 7d8a97207..ea76258b0 100644 --- a/x509-ocsp/src/ext.rs +++ b/x509-ocsp/src/ext.rs @@ -1,5 +1,6 @@ //! OCSP Extensions +use crate::OcspGeneralizedTime; use alloc::vec::Vec; use const_oid::{ db::rfc6960::{ @@ -9,7 +10,7 @@ use const_oid::{ AssociatedOid, }; use der::{ - asn1::{GeneralizedTime, Ia5String, ObjectIdentifier, OctetString, Uint}, + asn1::{Ia5String, ObjectIdentifier, OctetString, Uint}, Sequence, ValueOrd, }; use spki::AlgorithmIdentifierOwned; @@ -19,7 +20,7 @@ use x509_cert::{ name::Name, }; -#[cfg(feature = "rand_core")] +#[cfg(feature = "rand")] use rand_core::CryptoRngCore; // x509-cert's is not exported @@ -60,8 +61,11 @@ impl Nonce { /// ```text /// Nonce ::= OCTET STRING(SIZE(1..32)) /// ``` - #[cfg(feature = "rand_core")] - pub fn generate(rng: &mut R, length: usize) -> Result { + #[cfg(feature = "rand")] + pub fn generate(rng: &mut R, length: usize) -> Result + where + R: CryptoRngCore, + { let mut bytes = alloc::vec![0; length]; rng.fill_bytes(&mut bytes); Self::new(bytes) @@ -69,23 +73,11 @@ impl Nonce { } /// CrlReferences extension as defined in [RFC 6960 Section 4.4.2] -pub struct CrlReferences(pub Vec); - -impl_newtype!(CrlReferences, Vec); - -// It may be desirable for the OCSP responder to indicate the CRL on -// which a revoked or onHold certificate is found. This can be useful -// where OCSP is used between repositories, and also as an auditing -// mechanism. The CRL may be specified by a URL (the URL at which the -// CRL is available), a number (CRL number), or a time (the time at -// which the relevant CRL was created). These extensions will be -// specified as singleExtensions. The identifier for this extension -// will be id-pkix-ocsp-crl, while the value will be CrlID. -impl_extension!(CrlReferences, critical = false); - -impl AssociatedOid for CrlReferences { - const OID: ObjectIdentifier = ID_PKIX_OCSP_CRL; -} +/// +/// This does not seem to be its own type and just another name for CrlID +/// +/// [RFC 6960 Section 4.4.2]: https://datatracker.ietf.org/doc/html/rfc6960#section-4.4.2 +pub type CrlReferences = CrlId; /// CrlID structure as defined in [RFC 6960 Section 4.4.2]. /// @@ -107,7 +99,21 @@ pub struct CrlId { pub crl_num: Option, #[asn1(context_specific = "2", optional = "true", tag_mode = "EXPLICIT")] - pub crl_time: Option, + pub crl_time: Option, +} + +// It may be desirable for the OCSP responder to indicate the CRL on +// which a revoked or onHold certificate is found. This can be useful +// where OCSP is used between repositories, and also as an auditing +// mechanism. The CRL may be specified by a URL (the URL at which the +// CRL is available), a number (CRL number), or a time (the time at +// which the relevant CRL was created). These extensions will be +// specified as singleExtensions. The identifier for this extension +// will be id-pkix-ocsp-crl, while the value will be CrlID. +impl_extension!(CrlId, critical = false); + +impl AssociatedOid for CrlId { + const OID: ObjectIdentifier = ID_PKIX_OCSP_CRL; } /// AcceptableResponses structure as defined in [RFC 6960 Section 4.4.3]. @@ -117,6 +123,7 @@ pub struct CrlId { /// ``` /// /// [RFC 6960 Section 4.4.3]: https://datatracker.ietf.org/doc/html/rfc6960#section-4.4.3 +#[derive(Clone, Debug, Eq, PartialEq)] pub struct AcceptableResponses(pub Vec); impl_newtype!(AcceptableResponses, Vec); @@ -138,9 +145,9 @@ impl AssociatedOid for AcceptableResponses { /// ``` /// /// [RFC 6960 Section 4.4.4]: https://datatracker.ietf.org/doc/html/rfc6960#section-4.4.4 -pub struct ArchiveCutoff(GeneralizedTime); +pub struct ArchiveCutoff(pub OcspGeneralizedTime); -impl_newtype!(ArchiveCutoff, GeneralizedTime); +impl_newtype!(ArchiveCutoff, OcspGeneralizedTime); impl_extension!(ArchiveCutoff, critical = false); impl AssociatedOid for ArchiveCutoff { @@ -176,6 +183,7 @@ impl_extension!(ServiceLocator, critical = false); /// ``` /// /// [RFC 6960 Section 4.4.7.1]: https://datatracker.ietf.org/doc/html/rfc6960#section-4.4.7.1 +#[derive(Clone, Debug, Eq, PartialEq)] pub struct PreferredSignatureAlgorithms(pub Vec); impl_newtype!( diff --git a/x509-ocsp/src/lib.rs b/x509-ocsp/src/lib.rs index bf0070f8f..69e5376b7 100644 --- a/x509-ocsp/src/lib.rs +++ b/x509-ocsp/src/lib.rs @@ -14,14 +14,43 @@ extern crate alloc; mod basic; +mod cert_id; +mod cert_status; mod request; +mod responder_id; mod response; +mod time; pub mod ext; -pub use basic::{ - BasicOcspResponse, CertId, CertStatus, KeyHash, ResponderId, ResponseData, RevokedInfo, - SingleResponse, UnknownInfo, Version, -}; +pub use basic::{BasicOcspResponse, ResponseData, SingleResponse}; +pub use cert_id::CertId; +pub use cert_status::{CertStatus, RevokedInfo, UnknownInfo}; pub use request::{OcspRequest, Request, Signature, TbsRequest}; -pub use response::{OcspNoCheck, OcspResponse, OcspResponseStatus, ResponseBytes}; +pub use responder_id::ResponderId; +pub use response::{AsResponseBytes, OcspNoCheck, OcspResponse, OcspResponseStatus, ResponseBytes}; +pub use time::OcspGeneralizedTime; + +#[cfg(feature = "std")] +extern crate std; + +#[cfg(feature = "builder")] +pub mod builder; + +use der::Enumerated; + +/// 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, +} diff --git a/x509-ocsp/src/request.rs b/x509-ocsp/src/request.rs index adb4f87c1..cdc608318 100644 --- a/x509-ocsp/src/request.rs +++ b/x509-ocsp/src/request.rs @@ -1,9 +1,10 @@ //! OCSP Request -use crate::{CertId, Version}; +use crate::{ext::Nonce, CertId, Version}; use alloc::vec::Vec; +use const_oid::db::rfc6960::ID_PKIX_OCSP_NONCE; use core::{default::Default, option::Option}; -use der::{asn1::BitString, Sequence}; +use der::{asn1::BitString, Decode, Sequence}; use spki::AlgorithmIdentifierOwned; use x509_cert::{ certificate::Certificate, @@ -28,6 +29,14 @@ pub struct OcspRequest { pub optional_signature: Option, } +impl OcspRequest { + /// Returns the request's nonce value, if any. This method will return `None` if the request + /// has no `Nonce` extension or decoding of the `Nonce` extension fails. + pub fn nonce(&self) -> Option { + self.tbs_request.nonce() + } +} + /// TBSRequest structure as defined in [RFC 6960 Section 4.1.1]. /// /// ```text @@ -39,7 +48,7 @@ pub struct OcspRequest { /// ``` /// /// [RFC 6960 Section 4.1.1]: https://datatracker.ietf.org/doc/html/rfc6960#section-4.1.1 -#[derive(Clone, Debug, Eq, PartialEq, Sequence)] +#[derive(Clone, Debug, Default, Eq, PartialEq, Sequence)] #[allow(missing_docs)] pub struct TbsRequest { #[asn1( @@ -58,6 +67,23 @@ pub struct TbsRequest { pub request_extensions: Option, } +impl TbsRequest { + /// Returns the request's nonce value, if any. This method will return `None` if the request + /// has no `Nonce` extension or decoding of the `Nonce` extension fails. + pub fn nonce(&self) -> Option { + match &self.request_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, + } + } +} + /// Signature structure as defined in [RFC 6960 Section 4.1.1]. /// /// ```text @@ -95,3 +121,64 @@ pub struct Request { #[asn1(context_specific = "0", optional = "true", tag_mode = "EXPLICIT")] pub single_request_extensions: Option, } + +#[cfg(feature = "builder")] +mod builder { + use crate::{builder::Error, CertId, Request}; + use const_oid::AssociatedOid; + use digest::Digest; + use x509_cert::{ext::AsExtension, name::Name, serial_number::SerialNumber, Certificate}; + + impl Request { + /// Returns a new `Request` with the specified `CertID` + pub fn new(req_cert: CertId) -> Self { + Self { + req_cert, + single_request_extensions: None, + } + } + + /// Generates a `CertID` by running the issuer's subject and key through the specified + /// [`Digest`]. + /// + /// [RFC 6960 Section 4.1.1] + /// + /// [RFC 6960 Section 4.1.1]: https://datatracker.ietf.org/doc/html/rfc6960#section-4.1.1 + pub fn from_issuer( + issuer: &Certificate, + serial_number: SerialNumber, + ) -> Result + where + D: Digest + AssociatedOid, + { + Ok(Self::new(CertId::from_issuer::(issuer, serial_number)?)) + } + + /// Generates a `CertID` by running the issuer's subject and key through the specified + /// [`Digest`] and pulls the serial from `cert`. This does not ensure that `cert` is actually + /// issued by `issuer`. + /// + /// [RFC 6960 Section 4.1.1] + /// + /// [RFC 6960 Section 4.1.1]: https://datatracker.ietf.org/doc/html/rfc6960#section-4.1.1 + pub fn from_cert(issuer: &Certificate, cert: &Certificate) -> Result + where + D: Digest + AssociatedOid, + { + Ok(Self::new(CertId::from_cert::(issuer, cert)?)) + } + + /// Adds a single request 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 { + let ext = ext.to_extension(&Name::default(), &[])?; + match self.single_request_extensions { + Some(ref mut exts) => exts.push(ext), + None => self.single_request_extensions = Some(alloc::vec![ext]), + } + Ok(self) + } + } +} diff --git a/x509-ocsp/src/responder_id.rs b/x509-ocsp/src/responder_id.rs new file mode 100644 index 000000000..097a39a33 --- /dev/null +++ b/x509-ocsp/src/responder_id.rs @@ -0,0 +1,48 @@ +//! X.509 OCSP ResponderID + +use der::{asn1::OctetString, Choice}; +use x509_cert::name::Name; + +/// 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 From for ResponderId { + fn from(other: Name) -> Self { + Self::ByName(other) + } +} + +impl From for ResponderId { + fn from(other: KeyHash) -> Self { + Self::ByKey(other) + } +} + +/// 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; diff --git a/x509-ocsp/src/response.rs b/x509-ocsp/src/response.rs index da365b881..75dd53499 100644 --- a/x509-ocsp/src/response.rs +++ b/x509-ocsp/src/response.rs @@ -1,11 +1,10 @@ //! OCSP Response -use crate::BasicOcspResponse; -use const_oid::db::rfc6960::ID_PKIX_OCSP_BASIC; +use const_oid::AssociatedOid; use core::option::Option; use der::{ asn1::{Null, ObjectIdentifier, OctetString}, - Encode, Enumerated, Sequence, + Enumerated, Sequence, }; /// OcspNoCheck as defined in [RFC 6960 Section 4.2.2.2.1]. @@ -39,17 +38,22 @@ pub struct OcspResponse { impl OcspResponse { /// Encodes an `OcspResponse` with the status set to `Successful` - pub fn successful(basic: BasicOcspResponse) -> Result { + /// + /// [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 successful(res: impl AsResponseBytes) -> Result { Ok(OcspResponse { response_status: OcspResponseStatus::Successful, - response_bytes: Some(ResponseBytes { - response_type: ID_PKIX_OCSP_BASIC, - response: OctetString::new(basic.to_der()?)?, - }), + response_bytes: Some(res.to_response_bytes()?), }) } /// Encodes an `OcspResponse` with the status set to `MalformedRequest` + /// + /// [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 malformed_request() -> Self { OcspResponse { response_status: OcspResponseStatus::MalformedRequest, @@ -58,6 +62,10 @@ impl OcspResponse { } /// Encodes an `OcspResponse` with the status set to `InternalError` + /// + /// [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 internal_error() -> Self { OcspResponse { response_status: OcspResponseStatus::InternalError, @@ -66,6 +74,10 @@ impl OcspResponse { } /// Encodes an `OcspResponse` with the status set to `TryLater` + /// + /// [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 try_later() -> Self { OcspResponse { response_status: OcspResponseStatus::TryLater, @@ -74,6 +86,10 @@ impl OcspResponse { } /// Encodes an `OcspResponse` with the status set to `SigRequired` + /// + /// [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 sig_required() -> Self { OcspResponse { response_status: OcspResponseStatus::SigRequired, @@ -82,6 +98,10 @@ impl OcspResponse { } /// Encodes an `OcspResponse` with the status set to `Unauthorized` + /// + /// [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 unauthorized() -> Self { OcspResponse { response_status: OcspResponseStatus::Unauthorized, @@ -132,3 +152,14 @@ pub struct ResponseBytes { pub response_type: ObjectIdentifier, pub response: OctetString, } + +/// Trait for encoding [`ResponseBytes`] +pub trait AsResponseBytes: AssociatedOid + der::Encode { + /// Encodes the response bytes of successful OCSP responses + fn to_response_bytes(&self) -> Result { + Ok(ResponseBytes { + response_type: ::OID, + response: OctetString::new(self.to_der()?)?, + }) + } +} diff --git a/x509-ocsp/src/time.rs b/x509-ocsp/src/time.rs new file mode 100644 index 000000000..b567b9491 --- /dev/null +++ b/x509-ocsp/src/time.rs @@ -0,0 +1,45 @@ +//! OCSP GeneralizedTime implementation + +use der::{ + asn1::{GeneralizedTime, UtcTime}, + DateTime, +}; +use x509_cert::{impl_newtype, time::Time}; + +/// [`GeneralizedTime`] wrapper for easy conversion from legacy `UTCTime` +/// +/// OCSP does not support `UTCTime` while many other X.509 structures do. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct OcspGeneralizedTime(pub GeneralizedTime); + +impl_newtype!(OcspGeneralizedTime, GeneralizedTime); + +#[cfg(feature = "std")] +impl TryFrom for OcspGeneralizedTime { + type Error = der::Error; + + fn try_from(other: std::time::SystemTime) -> Result { + Ok(Self(GeneralizedTime::from_system_time(other)?)) + } +} + +impl From for OcspGeneralizedTime { + fn from(other: DateTime) -> Self { + Self(GeneralizedTime::from_date_time(other)) + } +} + +impl From for OcspGeneralizedTime { + fn from(other: UtcTime) -> Self { + Self(GeneralizedTime::from_date_time(other.to_date_time())) + } +} + +impl From