From d90b2e1a441adf35d18d0b24102ff97f0b8ad290 Mon Sep 17 00:00:00 2001 From: Igor Novgorodov Date: Sun, 26 May 2024 14:58:35 +0200 Subject: [PATCH] add preload/status API, update docs, bump version --- Cargo.lock | 2 +- Cargo.toml | 4 ++-- README.md | 20 ++++++++++++----- README_DOCS.md | 60 +++++++++++++++++++++++++++++--------------------- src/lib.rs | 4 ++-- src/stapler.rs | 29 ++++++++++++++++++++++-- 6 files changed, 81 insertions(+), 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 53c4384..1769f3b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1029,7 +1029,7 @@ dependencies = [ [[package]] name = "ocsp-stapler" -version = "0.3.1" +version = "0.4.0" dependencies = [ "anyhow", "arc-swap", diff --git a/Cargo.toml b/Cargo.toml index 92caa4a..10bef62 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ocsp-stapler" -version = "0.3.1" +version = "0.4.0" edition = "2021" license = "MPL-2.0" description = "OCSP stapler & client with support for Rustls" @@ -15,7 +15,7 @@ categories = [ "web-programming::http-server", ] -readme = "README_DOCS.md" +readme = "README.md" [dependencies] anyhow = "1.0" diff --git a/README.md b/README.md index 0c7c099..79b8573 100644 --- a/README.md +++ b/README.md @@ -2,32 +2,40 @@ [![crates.io](https://img.shields.io/crates/v/ocsp-stapler.svg)](https://crates.io/crates/ocsp-stapler) [![Documentation](https://docs.rs/ocsp-stapler/badge.svg)](https://docs.rs/ocsp-stapler) -[![MIT/Apache-2 licensed](https://img.shields.io/crates/l/ocsp-stapler.svg)](./LICENSE) +[![MPL-2 Licensed](https://img.shields.io/crates/l/ocsp-stapler.svg)](./LICENSE) OCSP stapler for Rustls. - OCSP `Client` that can be used separately - `Stapler` wraps `Arc` trait object and automatically staples all certificates provided by it -`Stapler::new()` spawns background worker using `tokio::spawn` so it must be executed in the Tokio context. - Please see the [docs](https://docs.rs/ocsp-stapler) for more details. ## Example ```rust,ignore +// Read the chain & private key and combine them into CertifiedKey +let certs = std::fs::read("chain.pem").unwrap(); +let certs = rustls_pemfile::certs(&mut certs.as_ref()).collect::, _>>().unwrap(); + +let key = std::fs::read("private.pem").unwrap(); +let key = rustls_pemfile::private_key(&mut key.as_ref()).unwrap(); +let key = aws_lc_rs::sign::any_supported_type(&key).unwrap(); + +let ckey = CertifiedKey::new(certs, key); + // Inner service that provides certificates to Rustls, can be anything -let ckey: CertifiedKey = ...; let mut inner = rustls::server::ResolvesServerCertUsingSni::new(); inner.add("crates.io", ckey).unwrap(); +// Create a Stapler wrapping inner resolver let stapler = Arc::new(ocsp_stapler::Stapler::new(inner)); -// Then you can build & use server_config wherever applicable +// Then you can build & use ServerConfig wherever applicable let server_config = rustls::server::ServerConfig::builder() .with_no_client_auth() .with_cert_resolver(stapler.clone()); -// Stop the background worker to clean up +// Stop the background worker to clean up resources stapler.stop().await; ``` diff --git a/README_DOCS.md b/README_DOCS.md index 3115540..9a6b21b 100644 --- a/README_DOCS.md +++ b/README_DOCS.md @@ -1,63 +1,73 @@ # ocsp-stapler -The `ocsp-stapler` crate provides two structs: `Client` and `Stapler`. +The `ocsp-stapler` crate provides the following main structs: -## `Client` -[`client::Client`](client) is an OCSP client that can be used to query the OCSP responders of the Certificate Authorities. It tries to mostly conform to the [lightweight OCSP profile](https://datatracker.ietf.org/doc/html/rfc5019) +## [`Client`](client::Client) +[`Client`](client::Client) is an OCSP client that can be used to query the OCSP responders of the Certificate Authorities. It tries to mostly conform to the [lightweight OCSP profile](https://datatracker.ietf.org/doc/html/rfc5019) that LetsEncrypt uses. -- Currently only SHA-1 digest for OCSP request is supported since it's the only one that LetsEncrypt uses +- Currently only SHA-1 digest for OCSP request is supported - Requests <= 255 bytes will be sent using GET and Base64, otherwise POST -## `Stapler` -[`stapler::Stapler`](stapler) uses [`client::Client`](client) internally and provides a Rustls-compatible API to attach (staple) OCSP responses to the certificates. +## [`Stapler`](stapler::Stapler) +[`Stapler`](stapler::Stapler) uses [`Client`](client::Client) internally and provides a Rustls-compatible API to attach (staple) OCSP responses to the certificates. -It wraps whatever that implements Rustls' [`rustls::server::ResolvesServerCert`](https://docs.rs/rustls/latest/rustls/server/trait.ResolvesServerCert.html) trait and also implements the same trait itself. +It wraps anything that implements [`ResolvesServerCert`](rustls::server::ResolvesServerCert) and also implements the same trait itself. The workflow is the following: -- [`stapler::Stapler`](stapler) receives a `ClientHello` from Rustls and forwards it to the wrapped resolver to retrieve the certificate chain +- [`Stapler`](stapler::Stapler) receives a [`ClientHello`](rustls::server::ClientHello) from Rustls and forwards it to the wrapped resolver to retrieve the certificate chain - It calculates the SHA-1 fingerprint over the whole end-entity certificate and uses that to check if it has the same certificate in the local storage: - - If not, then it sends the certificate to the background worker for eventual processing & stapling. -Meanwhilte it returns to Rustls the original unstapled certificate + - If not, then it sends the certificate to the background worker for eventual processing & stapling. Meanwhilte it returns to Rustls the original unstapled certificate - If found, it responds with a stapled version of the certificate -Since the certificates are only stapled eventually then the `Must-Staple` marked certificates will not work out of the box - first request for them will always be failed by the client. Maybe later an API to pre-staple them will be added. +Since the certificates are only stapled eventually then the `Must-Staple` marked certificates will not work out of the box - first request for them will always be failed by the client. In this case you can use [`Stapler::preload`](stapler::Stapler::preload) to pre-staple the certificate and [`Stapler::status`](stapler::Stapler::status) to check if the stapling was done. Background worker duties: -- Receieves the certificates from `Stapler`, processes them and inserts into the local storage +- Receieves the certificates from [`Stapler`](stapler::Stapler), processes them and inserts into the local storage - Wakes up every minute (or when a new certificate is added) to do the following: -- Obtain OCSP responses for newly added certificates -- Renew the OCSP responses that are already past 50% of their validity interval -- Check for expired certificates & purge them -- Check for expired OCSP responses and clear them -- Post an updated version of storage that is shared with `Stapler` + - Obtain OCSP responses for the newly added certificates if any + - Renew the OCSP responses that are already past 50% of their validity interval + - Check for expired certificates & purge them + - Check for expired OCSP responses and clear them. This is needed to make sure we don't serve expired OCSP responses for whatever reason (e.g. OCSP responder might return us stale results etc) + - Post an updated version of storage that is shared with [`Stapler`](stapler::Stapler) -Background worker is spawned by `Stapler::new()` using `tokio::spawn` so it must be executed in Tokio context. -It runs indefinitely unless stopped with `Stapler::stop()`. +Background worker is spawned by [`Stapler::new`](stapler::Stapler::new) using `tokio::spawn` so it must be executed in Tokio context. +It runs indefinitely unless stopped with [`Stapler::stop`](stapler::Stapler::stop). Other notes: -- Stapler does not check the certificate validity (i.e. does not traverse the chain up to the root) -- Certificates without the issuer's certificate are passed through as-is since we can't query the OCSP without access to the issuer's public key +- Stapler does not check the certificate chain (i.e. does not traverse the chain up to the root), it only checks that current time fits in its validity period. If it's not valid - we don't try staple it and pass through as-is. + +- Certificates without the issuers (i.e. when [`CertifiedKey`](rustls::sign::CertifiedKey) contains only single end-entity certificate) are passed through as-is since we can't query the OCSP without access to the issuer's public key. ### Metrics -Stapler supports a few Prometheus metrics - create it using one of `new_..._with_registry()` constructors and provide a Prometheus `Registry` reference to register the metrics in. +Stapler supports a few Prometheus metrics - create it using one of `new_..._with_registry()` constructors and provide a Prometheus [`Registry`](prometheus::Registry) reference to register the metrics in. ### Example ```rust,ignore +// Read the chain & private key and combine them into CertifiedKey +let certs = std::fs::read("chain.pem").unwrap(); +let certs = rustls_pemfile::certs(&mut certs.as_ref()).collect::, _>>().unwrap(); + +let key = std::fs::read("private.pem").unwrap(); +let key = rustls_pemfile::private_key(&mut key.as_ref()).unwrap(); +let key = aws_lc_rs::sign::any_supported_type(&key).unwrap(); + +let ckey = CertifiedKey::new(certs, key); + // Inner service that provides certificates to Rustls, can be anything -let ckey: CertifiedKey = ...; let mut inner = rustls::server::ResolvesServerCertUsingSni::new(); inner.add("crates.io", ckey).unwrap(); +// Create a Stapler wrapping inner resolver let stapler = Arc::new(ocsp_stapler::Stapler::new(inner)); -// Then you can build & use server_config wherever applicable +// Then you can build & use ServerConfig wherever applicable let server_config = rustls::server::ServerConfig::builder() .with_no_client_auth() .with_cert_resolver(stapler.clone()); -// Stop the background worker to clean up +// Stop the background worker to clean up resources stapler.stop().await; ``` diff --git a/src/lib.rs b/src/lib.rs index b0917bb..8b510eb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -17,7 +17,7 @@ use x509_parser::certificate; /// Allow some time inconsistencies pub(crate) const LEEWAY: TimeDelta = TimeDelta::minutes(5); -/// OCSP response validity interval +/// Generic validity interval for certificates and OCSP responses #[derive(Clone, Debug)] pub struct Validity { pub not_before: DateTime, @@ -50,7 +50,7 @@ impl TryFrom<&certificate::Validity> for Validity { impl Validity { /// Check if we're already past the half of this validity duration - pub fn time_to_update(&self, now: DateTime) -> bool { + pub fn past_half_validity(&self, now: DateTime) -> bool { now >= self.not_before + ((self.not_after - self.not_before) / 2) } diff --git a/src/stapler.rs b/src/stapler.rs index cc94c9e..bad12dd 100644 --- a/src/stapler.rs +++ b/src/stapler.rs @@ -13,7 +13,7 @@ use prometheus::{ register_histogram_vec_with_registry, register_int_counter_vec_with_registry, register_int_gauge_vec_with_registry, HistogramVec, IntCounterVec, IntGaugeVec, Registry, }; -use rasn_ocsp::CertStatus; +use rasn_ocsp::{CertStatus, OcspResponseStatus}; use rustls::{ pki_types::CertificateDer, server::{ClientHello, ResolvesServerCert}, @@ -164,6 +164,31 @@ impl Stapler { } } + /// Preloads the certificate into the Stapler before the request to resolve() comes. + /// This allows e.g. to load certificates with `Must-Staple` extension in a way that + /// when the first request comes they're already stapled. + /// Has no effect if the same certificate was already preloaded. Silently discards the certificate + /// if it's not correct (doens't have the issuer, out of validity window etc) + pub fn preload(&self, ckey: Arc) { + if ckey.cert.len() < 2 { + return; + } + + let fp = Fingerprint::from(&ckey.cert[0]); + let _ = self.tx.try_send((fp, ckey)); + } + + /// Returns the certificate revocation status of the provided CertifiedKey. + /// It will be None if no successful OCSP request was made. + pub fn status(&self, ckey: Arc) -> Option { + if ckey.cert.len() < 2 { + return None; + } + + let fp = Fingerprint::from(&ckey.cert[0]); + Some(self.storage.load_full()?.get(&fp)?.status.clone()) + } + /// Tells the background worker to stop and waits until it does pub async fn stop(&self) { self.token.cancel(); @@ -239,7 +264,7 @@ async fn refresh_certificate( ) -> Result { // Check if this OCSP response is still valid if let Some(x) = &cert.ocsp_validity { - if !x.time_to_update(now) { + if !x.past_half_validity(now) { return Ok(RefreshResult::StillValid); } }