-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Loading status checks…
Improve metrics, use GET, certificate validity check
1 parent
8e73b94
commit 5e911d5
Showing
7 changed files
with
301 additions
and
167 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
# ocsp-stapler | ||
|
||
The `ocsp-stapler` crate provides two structs: `Client` and `Stapler`. | ||
|
||
## `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) | ||
|
||
- Currently only SHA-1 digest for OCSP request is supported since it's the only one that LetsEncrypt uses | ||
- 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. | ||
|
||
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. | ||
|
||
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 | ||
- 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 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. | ||
|
||
Background worker duties: | ||
- Receieves the certificates from `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` | ||
|
||
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()`. | ||
|
||
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 | ||
|
||
### 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. | ||
|
||
# Example | ||
|
||
```rust,ignore | ||
// 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(); | ||
let stapler = Arc::new(ocsp_stapler::Stapler::new(inner)); | ||
// Then you can build & use server_config 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 | ||
stapler.stop().await; | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,74 +1,52 @@ | ||
#![warn(clippy::all)] | ||
#![warn(clippy::nursery)] | ||
|
||
//! # ocsp-stapler | ||
//! | ||
//! The `ocsp-stapler` crate provides two structs: `Client` and `Stapler`. | ||
//! | ||
//! ## `Client` | ||
//! `Client` is an OCSP client that you can use to do OCSP requests to OCSP responders of the Certificate Authorities. | ||
//! | ||
//! ## `Stapler` | ||
//! `Stapler` uses `Client` internally and provides a Rustls-compatible API to attach (staple) OCSP responses to the certificates. | ||
//! It wraps whatever that implements Rustls' `ResolvesServerCert` trait and also implements the same trait. | ||
//! | ||
//! The workflow is the following: | ||
//! - `Stapler` receives a `ClientHello` from Rustls and forwards it to ther wrapped trait object to get 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 - it sends the certificate to the background worker for eventual processing & stapling. | ||
//! Meanwhilte it returns to Rustls the original unstapled certificate | ||
//! - If found - it respondes with a stapled version of the certificate | ||
//! | ||
//! Background worker duties: | ||
//! - Receieves the certificates from `Stapler`, processes them and inserts into the local storage | ||
//! - Wakes up every 60s (or when a new certificate is added) to do the following: | ||
//! - 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` | ||
//! | ||
//! 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()`. | ||
//! | ||
//! # Example | ||
//! | ||
//! ```rust,ignore | ||
//! // 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(); | ||
//! | ||
//! let stapler = Arc::new(ocsp_stapler::Stapler::new(inner)); | ||
//! | ||
//! let server_config = rustls::server::ServerConfig::builder() | ||
//! .with_no_client_auth() | ||
//! .with_cert_resolver(stapler.clone()); | ||
//! | ||
//! // Then you can use server_config wherever applicable | ||
//! | ||
//! // Stop the background worker to clean up | ||
//! stapler.stop().await; | ||
//! ``` | ||
pub mod client; | ||
pub mod stapler; | ||
|
||
pub use client::Client; | ||
pub use stapler::Stapler; | ||
|
||
use chrono::{DateTime, FixedOffset}; | ||
use anyhow::{anyhow, Error}; | ||
use chrono::{DateTime, FixedOffset, TimeDelta}; | ||
use x509_parser::certificate; | ||
|
||
/// Allow some time inconsistencies | ||
pub(crate) const LEEWAY: TimeDelta = TimeDelta::minutes(5); | ||
|
||
/// OCSP response validity interval | ||
#[derive(Clone)] | ||
pub struct OcspValidity { | ||
pub this_update: DateTime<FixedOffset>, | ||
pub next_update: DateTime<FixedOffset>, | ||
#[derive(Clone, Debug)] | ||
pub struct Validity { | ||
pub not_before: DateTime<FixedOffset>, | ||
pub not_after: DateTime<FixedOffset>, | ||
} | ||
|
||
impl OcspValidity { | ||
// Check if we're already past the half of this validity duration | ||
impl TryFrom<&certificate::Validity> for Validity { | ||
type Error = Error; | ||
fn try_from(v: &certificate::Validity) -> Result<Self, Self::Error> { | ||
let not_before = DateTime::from_timestamp(v.not_before.timestamp(), 0) | ||
.ok_or_else(|| anyhow!("unable to parse not_before"))? | ||
.into(); | ||
|
||
let not_after = DateTime::from_timestamp(v.not_after.timestamp(), 0) | ||
.ok_or_else(|| anyhow!("unable to parse not_after"))? | ||
.into(); | ||
|
||
Ok(Self { | ||
not_before, | ||
not_after, | ||
}) | ||
} | ||
} | ||
|
||
impl Validity { | ||
/// Check if we're already past the half of this validity duration | ||
pub fn time_to_update(&self, now: DateTime<FixedOffset>) -> bool { | ||
now >= self.this_update + ((self.next_update - self.this_update) / 2) | ||
now >= self.not_before + ((self.not_after - self.not_before) / 2) | ||
} | ||
|
||
/// Check if it's valid | ||
pub fn valid(&self, now: DateTime<FixedOffset>) -> bool { | ||
now >= (self.not_before - LEEWAY) && now <= (self.not_after + LEEWAY) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters