use std::path::Path;
use tracing::warn;
use super::TlsConfigError;
const MAX_OCSP_PARSE_BYTES: usize = 64 * 1024;
#[must_use]
pub fn staple_from_env(prefix: &str) -> Option<Vec<u8>> {
if !crate::surfaces::env_enabled("NAUTALID_TLS_OCSP") {
return None;
}
let path = std::env::var(format!("{prefix}_OCSP_RESPONSE_PATH"))
.ok()
.filter(|p| !p.is_empty())
.or_else(|| {
std::env::var("NAUTALID_TLS_OCSP_RESPONSE_PATH")
.ok()
.filter(|p| !p.is_empty())
})?;
read_staple_file(&path).ok()
}
pub fn resolve_staple(cert_pem: &str, prefix: &str) -> Option<Vec<u8>> {
if let Some(staple) = staple_from_env(prefix) {
return Some(staple);
}
if fetch_enabled() {
match fetch_live_staple(cert_pem) {
Ok(staple) => return Some(staple),
Err(err) => warn!(%err, "live OCSP staple fetch failed"),
}
}
None
}
fn fetch_enabled() -> bool {
crate::surfaces::env_enabled("NAUTALID_TLS_OCSP_FETCH")
}
fn read_staple_file(path: &str) -> Result<Vec<u8>, TlsConfigError> {
let bytes = std::fs::read(path).map_err(|source| TlsConfigError::Io {
path: path.to_owned(),
source,
})?;
if bytes.is_empty() {
return Err(TlsConfigError::Rustls(format!(
"empty OCSP staple at {}",
Path::new(path).display()
)));
}
validate_ocsp_bytes(&bytes)?;
Ok(bytes)
}
fn validate_ocsp_bytes(bytes: &[u8]) -> Result<(), TlsConfigError> {
if bytes.len() > MAX_OCSP_PARSE_BYTES {
return Err(TlsConfigError::Rustls(format!(
"OCSP response exceeds {} byte parse cap",
MAX_OCSP_PARSE_BYTES
)));
}
Ok(())
}
fn fetch_live_staple(cert_pem: &str) -> Result<Vec<u8>, TlsConfigError> {
let (leaf_der, issuer_der) = pem_chain_ders(cert_pem)?;
if let Some(url) = ocsp_url_from_env() {
let request = ocsp_staple::build_ocsp_request(&leaf_der, &issuer_der)
.map_err(map_ocsp_err)?;
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| TlsConfigError::Rustls(e.to_string()))?;
let bytes = rt
.block_on(ocsp_staple::fetch_ocsp(
&url,
request,
ocsp_staple::FETCH_TIMEOUT,
))
.map_err(map_ocsp_err)?;
validate_ocsp_bytes(&bytes)?;
let staple = ocsp_staple::parse_ocsp_response(&bytes).map_err(map_ocsp_err)?;
return Ok(staple.staple);
}
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| TlsConfigError::Rustls(e.to_string()))?;
let staple = rt
.block_on(ocsp_staple::fetch_ocsp_for_cert(
&leaf_der,
&issuer_der,
ocsp_staple::FETCH_TIMEOUT,
))
.map_err(map_ocsp_err)?;
validate_ocsp_bytes(&staple.staple)?;
Ok(staple.staple)
}
fn map_ocsp_err(err: ocsp_staple::OcspError) -> TlsConfigError {
TlsConfigError::Rustls(err.to_string())
}
fn ocsp_url_from_env() -> Option<String> {
std::env::var("NAUTALID_TLS_OCSP_URL")
.ok()
.filter(|u| !u.trim().is_empty())
.map(|u| u.trim().to_string())
}
fn pem_chain_ders(cert_pem: &str) -> Result<(Vec<u8>, Vec<u8>), TlsConfigError> {
let mut reader = std::io::Cursor::new(cert_pem.as_bytes());
let certs: Vec<Vec<u8>> = rustls_pemfile::certs(&mut reader)
.collect::<Result<Vec<_>, _>>()
.map_err(|e| TlsConfigError::BadCert(e.to_string()))?
.into_iter()
.map(|c| c.to_vec())
.collect();
let leaf = certs
.first()
.cloned()
.ok_or_else(|| TlsConfigError::BadCert("no certificate in PEM".into()))?;
let issuer = certs.get(1).cloned().unwrap_or_else(|| leaf.clone());
Ok((leaf, issuer))
}
pub fn apply_staple(
mut key: rustls::sign::CertifiedKey,
staple: Option<Vec<u8>>,
) -> rustls::sign::CertifiedKey {
if let Some(ocsp) = staple {
key.ocsp = Some(ocsp);
}
key
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn disabled_without_env() {
unsafe {
std::env::remove_var("NAUTALID_TLS_OCSP");
}
assert!(staple_from_env("NAUTALID_TLS").is_none());
}
#[test]
fn rejects_oversized_ocsp_bytes() {
let oversized = vec![0u8; MAX_OCSP_PARSE_BYTES + 1];
assert!(validate_ocsp_bytes(&oversized).is_err());
assert!(validate_ocsp_bytes(&[1, 2, 3]).is_ok());
}
}