#[cfg(feature = "fetch")]
use std::time::Duration;
use std::time::SystemTime;
use der::{Decode, Encode};
use sha1::Sha1;
use x509_ocsp::builder::OcspRequestBuilder;
use x509_ocsp::{BasicOcspResponse, OcspResponse, OcspResponseStatus, Request as OcspReq};
const ID_AD_OCSP: &str = "1.3.6.1.5.5.7.48.1";
#[derive(Debug, thiserror::Error)]
pub enum OcspError {
#[error("certificate has no Authority Information Access extension")]
NoAia,
#[error("AIA extension has no OCSP responder URL")]
NoOcspUrl,
#[error(
"OCSP responder URL uses HTTPS, which is not supported by this crate \
(deliver pre-fetched OCSP responses through another channel): {0}"
)]
HttpsNotSupported(String),
#[error("invalid OCSP responder URL: {0}")]
InvalidUrl(String),
#[error("certificate parse failed: {0}")]
CertParse(String),
#[error("OCSP request build failed: {0}")]
RequestBuild(String),
#[error("OCSP responder returned HTTP {status}")]
HttpStatus { status: u16 },
#[error("OCSP responder unreachable: {0}")]
Transport(String),
#[error("OCSP response parse failed: {0}")]
ResponseParse(String),
#[error("OCSP responder returned non-successful status: {0}")]
ResponderError(String),
#[error("OCSP response body exceeds {cap} bytes")]
BodyTooLarge { cap: usize },
}
#[derive(Debug, Clone)]
pub struct OcspStaple {
pub staple: Vec<u8>,
pub next_update: SystemTime,
}
const DEFAULT_NEXT_UPDATE_AHEAD: std::time::Duration = std::time::Duration::from_hours(168);
#[cfg(feature = "fetch")]
pub const FETCH_TIMEOUT: Duration = Duration::from_secs(10);
pub fn extract_ocsp_url(cert_der: &[u8]) -> Result<String, OcspError> {
use x509_parser::extensions::{GeneralName, ParsedExtension};
use x509_parser::prelude::FromDer;
let (_, cert) = x509_parser::prelude::X509Certificate::from_der(cert_der)
.map_err(|e| OcspError::CertParse(format!("{e}")))?;
let mut saw_aia = false;
for ext in cert.tbs_certificate.extensions() {
if let ParsedExtension::AuthorityInfoAccess(aia) = ext.parsed_extension() {
saw_aia = true;
for desc in &aia.accessdescs {
if desc.access_method.to_id_string() == ID_AD_OCSP
&& let GeneralName::URI(url) = &desc.access_location
{
return classify_url(url);
}
}
}
}
if saw_aia { Err(OcspError::NoOcspUrl) } else { Err(OcspError::NoAia) }
}
fn classify_url(url: &str) -> Result<String, OcspError> {
if url.starts_with("https://") {
Err(OcspError::HttpsNotSupported(url.to_owned()))
} else if url.starts_with("http://") {
Ok(url.to_owned())
} else {
Err(OcspError::InvalidUrl(format!("expected `http://` scheme, got: {url}")))
}
}
pub fn build_ocsp_request(cert_der: &[u8], issuer_der: &[u8]) -> Result<Vec<u8>, OcspError> {
use x509_cert::Certificate;
let cert = Certificate::from_der(cert_der).map_err(|e| OcspError::CertParse(format!("{e}")))?;
let issuer =
Certificate::from_der(issuer_der).map_err(|e| OcspError::CertParse(format!("{e}")))?;
let req = OcspRequestBuilder::default()
.with_request(
OcspReq::from_cert::<Sha1>(&issuer, &cert)
.map_err(|e| OcspError::RequestBuild(format!("{e}")))?,
)
.build();
req.to_der().map_err(|e| OcspError::RequestBuild(format!("DER encode: {e}")))
}
pub fn parse_ocsp_response(resp_der: &[u8]) -> Result<OcspStaple, OcspError> {
let resp = OcspResponse::from_der(resp_der)
.map_err(|e| OcspError::ResponseParse(format!("OcspResponse decode: {e}")))?;
if resp.response_status != OcspResponseStatus::Successful {
return Err(OcspError::ResponderError(format!("{:?}", resp.response_status)));
}
let response_bytes = resp
.response_bytes
.as_ref()
.ok_or_else(|| OcspError::ResponseParse("successful response has no responseBytes".into()))?;
let basic = BasicOcspResponse::from_der(response_bytes.response.as_bytes())
.map_err(|e| OcspError::ResponseParse(format!("BasicOcspResponse decode: {e}")))?;
let single = basic
.tbs_response_data
.responses
.first()
.ok_or_else(|| OcspError::ResponseParse("no SingleResponse entries".into()))?;
let next_update = match &single.next_update {
Some(t) => generalized_time_to_system(t),
None => {
generalized_time_to_system(&basic.tbs_response_data.produced_at) + DEFAULT_NEXT_UPDATE_AHEAD
}
};
Ok(OcspStaple { staple: resp_der.to_vec(), next_update })
}
fn generalized_time_to_system(t: &x509_ocsp::OcspGeneralizedTime) -> SystemTime {
SystemTime::UNIX_EPOCH + t.0.to_unix_duration()
}
#[cfg(feature = "fetch")]
mod fetch {
use std::time::Duration;
use bytes::Bytes;
use http_body_util::{BodyExt, Full, Limited};
use hyper::Request;
use super::{
OcspError, OcspStaple, build_ocsp_request, classify_url, extract_ocsp_url, parse_ocsp_response,
};
const MAX_OCSP_BODY_BYTES: usize = 1024 * 1024;
pub async fn fetch_ocsp(
responder_url: &str,
request_der: Vec<u8>,
timeout: Duration,
) -> Result<Vec<u8>, OcspError> {
classify_url(responder_url)?;
let parsed = url::Url::parse(responder_url)
.map_err(|e| OcspError::InvalidUrl(format!("parse {responder_url}: {e}")))?;
let host = parsed
.host_str()
.ok_or_else(|| OcspError::InvalidUrl(format!("no host in {responder_url}")))?
.to_owned();
let port = parsed.port().unwrap_or(80);
let path_and_query = if parsed.path().is_empty() {
"/".to_owned()
} else {
match parsed.query() {
Some(q) => format!("{}?{q}", parsed.path()),
None => parsed.path().to_owned(),
}
};
let fut = perform_fetch(host.clone(), port, path_and_query, request_der);
tokio::time::timeout(timeout, fut)
.await
.map_err(|_| OcspError::Transport(format!("timed out after {timeout:?}")))?
}
async fn perform_fetch(
host: String,
port: u16,
path_and_query: String,
body: Vec<u8>,
) -> Result<Vec<u8>, OcspError> {
use hyper_util::rt::TokioIo;
let stream = tokio::net::TcpStream::connect((host.as_str(), port))
.await
.map_err(|e| OcspError::Transport(format!("connect {host}:{port}: {e}")))?;
let io = TokioIo::new(stream);
let (mut sender, conn) = hyper::client::conn::http1::handshake::<_, Full<Bytes>>(io)
.await
.map_err(|e| OcspError::Transport(format!("handshake: {e}")))?;
let conn_handle = tokio::spawn(async move {
let _ = conn.await;
});
let body_len = body.len();
let req = Request::builder()
.method("POST")
.uri(path_and_query)
.header(hyper::header::HOST, &host)
.header(hyper::header::CONTENT_TYPE, "application/ocsp-request")
.header(hyper::header::CONTENT_LENGTH, body_len.to_string())
.header(hyper::header::CONNECTION, "close")
.body(Full::new(Bytes::from(body)))
.map_err(|e| OcspError::Transport(format!("build request: {e}")))?;
let resp =
sender.send_request(req).await.map_err(|e| OcspError::Transport(format!("send: {e}")))?;
let status = resp.status();
if !status.is_success() {
conn_handle.abort();
return Err(OcspError::HttpStatus { status: status.as_u16() });
}
let limited = Limited::new(resp.into_body(), MAX_OCSP_BODY_BYTES);
let bytes = match limited.collect().await {
Ok(collected) => collected.to_bytes(),
Err(e) => {
conn_handle.abort();
if e.downcast_ref::<http_body_util::LengthLimitError>().is_some() {
return Err(OcspError::BodyTooLarge { cap: MAX_OCSP_BODY_BYTES });
}
return Err(OcspError::Transport(format!("read body: {e}")));
}
};
drop(sender);
let _ = conn_handle.await;
Ok(bytes.to_vec())
}
pub async fn fetch_ocsp_for_cert(
cert_der: &[u8],
issuer_der: &[u8],
timeout: Duration,
) -> Result<OcspStaple, OcspError> {
let url = extract_ocsp_url(cert_der)?;
let req = build_ocsp_request(cert_der, issuer_der)?;
let resp_bytes = fetch_ocsp(&url, req, timeout).await?;
parse_ocsp_response(&resp_bytes)
}
}
#[cfg(feature = "fetch")]
pub use fetch::{fetch_ocsp, fetch_ocsp_for_cert};
#[cfg(test)]
mod tests {
use rcgen::{
BasicConstraints, CertificateParams, IsCa, Issuer, KeyPair, KeyUsagePurpose,
PKCS_ECDSA_P256_SHA256,
};
use x509_cert::Certificate;
use super::*;
fn build_test_ca_and_leaf(aia_url: &str) -> (Vec<u8>, Vec<u8>) {
let ca_key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).expect("ca key");
let mut ca_params = CertificateParams::new(vec!["Test CA".to_owned()]).expect("ca params");
ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
ca_params.key_usages.push(KeyUsagePurpose::KeyCertSign);
ca_params.key_usages.push(KeyUsagePurpose::CrlSign);
let ca_cert = ca_params.clone().self_signed(&ca_key).expect("self_signed");
let ca_der = ca_cert.der().to_vec();
let leaf_key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).expect("leaf key");
let mut leaf_params =
CertificateParams::new(vec!["leaf.example".to_owned()]).expect("leaf params");
leaf_params.use_authority_key_identifier_extension = true;
leaf_params.custom_extensions.push(build_aia_custom_extension(aia_url));
let issuer = Issuer::from_params(&ca_params, &ca_key);
let leaf_cert = leaf_params.signed_by(&leaf_key, &issuer).expect("leaf signed_by");
let leaf_der = leaf_cert.der().to_vec();
(ca_der, leaf_der)
}
fn build_aia_custom_extension(aia_url: &str) -> rcgen::CustomExtension {
let oid_aia: &[u64] = &[1, 3, 6, 1, 5, 5, 7, 1, 1];
let ocsp_oid_der: Vec<u8> = vec![0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x01];
let url_bytes = aia_url.as_bytes();
let mut uri_tlv = vec![0x86];
uri_tlv.extend_from_slice(&der_length(url_bytes.len()));
uri_tlv.extend_from_slice(url_bytes);
let mut access_desc_inner = ocsp_oid_der;
access_desc_inner.extend_from_slice(&uri_tlv);
let mut access_desc_tlv = vec![0x30];
access_desc_tlv.extend_from_slice(&der_length(access_desc_inner.len()));
access_desc_tlv.extend_from_slice(&access_desc_inner);
let mut outer_tlv = vec![0x30];
outer_tlv.extend_from_slice(&der_length(access_desc_tlv.len()));
outer_tlv.extend_from_slice(&access_desc_tlv);
rcgen::CustomExtension::from_oid_content(oid_aia, outer_tlv)
}
fn der_length(n: usize) -> Vec<u8> {
if n < 0x80 {
vec![u8::try_from(n).unwrap()]
} else if n < 0x100 {
vec![0x81, u8::try_from(n).unwrap()]
} else {
vec![0x82, u8::try_from((n >> 8) & 0xff).unwrap(), u8::try_from(n & 0xff).unwrap()]
}
}
#[test]
fn extract_ocsp_url_returns_url_for_cert_with_aia() {
let (_, leaf_der) = build_test_ca_and_leaf("http://ocsp.example.test/");
let url = extract_ocsp_url(&leaf_der).expect("extract ok");
assert_eq!(url, "http://ocsp.example.test/");
}
#[test]
fn extract_ocsp_url_returns_no_aia_for_cert_without_extension() {
let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).expect("key");
let params = CertificateParams::new(vec!["plain.example".to_owned()]).expect("params");
let cert = params.self_signed(&key).expect("self_signed");
let err = extract_ocsp_url(cert.der()).expect_err("no AIA → err");
assert!(matches!(err, OcspError::NoAia), "got {err:?}");
}
#[test]
fn extract_ocsp_url_returns_https_not_supported() {
let (_, leaf_der) = build_test_ca_and_leaf("https://ocsp.example.test/");
let err = extract_ocsp_url(&leaf_der).expect_err("HTTPS rejected");
match err {
OcspError::HttpsNotSupported(url) => {
assert_eq!(url, "https://ocsp.example.test/");
}
other => panic!("expected HttpsNotSupported, got {other:?}"),
}
}
#[test]
fn extract_ocsp_url_returns_invalid_url_for_non_http() {
let (_, leaf_der) = build_test_ca_and_leaf("ftp://ocsp.example.test/");
let err = extract_ocsp_url(&leaf_der).expect_err("ftp rejected");
assert!(matches!(err, OcspError::InvalidUrl(_)), "got {err:?}");
}
#[test]
fn build_ocsp_request_round_trips_through_x509_ocsp() {
let (issuer_der, leaf_der) = build_test_ca_and_leaf("http://ocsp.example.test/");
let bytes = build_ocsp_request(&leaf_der, &issuer_der).expect("build ok");
let req = x509_ocsp::OcspRequest::from_der(&bytes).expect("decode");
assert!(!req.tbs_request.request_list.is_empty());
let leaf = Certificate::from_der(&leaf_der).expect("leaf decode");
let want_serial = leaf.tbs_certificate.serial_number.clone();
let got_serial = req.tbs_request.request_list[0].req_cert.serial_number.clone();
assert_eq!(got_serial.as_bytes(), want_serial.as_bytes());
}
#[test]
fn parse_ocsp_response_returns_responder_error_on_try_later() {
let bytes = OcspResponse::try_later().to_der().expect("encode");
let err = parse_ocsp_response(&bytes).expect_err("try_later → err");
assert!(matches!(err, OcspError::ResponderError(_)), "got {err:?}");
}
#[test]
fn parse_ocsp_response_rejects_garbage_bytes() {
let err = parse_ocsp_response(&[0x30, 0x00]).expect_err("garbage rejected");
assert!(matches!(err, OcspError::ResponseParse(_)), "got {err:?}");
}
#[cfg(feature = "fetch")]
#[test]
fn fetch_ocsp_rejects_https_url_pre_connect() {
let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
let err = rt.block_on(async {
fetch_ocsp("https://ocsp.example.test/", vec![1, 2, 3], std::time::Duration::from_secs(1))
.await
.expect_err("https rejected")
});
assert!(matches!(err, OcspError::HttpsNotSupported(_)), "got {err:?}");
}
}