1#[cfg(feature = "fetch")]
30use std::time::Duration;
31use std::time::SystemTime;
32
33use der::{Decode, Encode};
34use sha1::Sha1;
35use x509_ocsp::builder::OcspRequestBuilder;
36use x509_ocsp::{BasicOcspResponse, OcspResponse, OcspResponseStatus, Request as OcspReq};
37
38const ID_AD_OCSP: &str = "1.3.6.1.5.5.7.48.1";
43
44#[derive(Debug, thiserror::Error)]
48pub enum OcspError {
49 #[error("certificate has no Authority Information Access extension")]
50 NoAia,
51 #[error("AIA extension has no OCSP responder URL")]
52 NoOcspUrl,
53 #[error(
54 "OCSP responder URL uses HTTPS, which is not supported by this crate \
55 (deliver pre-fetched OCSP responses through another channel): {0}"
56 )]
57 HttpsNotSupported(String),
58 #[error("invalid OCSP responder URL: {0}")]
59 InvalidUrl(String),
60 #[error("certificate parse failed: {0}")]
61 CertParse(String),
62 #[error("OCSP request build failed: {0}")]
63 RequestBuild(String),
64 #[error("OCSP responder returned HTTP {status}")]
65 HttpStatus { status: u16 },
66 #[error("OCSP responder unreachable: {0}")]
67 Transport(String),
68 #[error("OCSP response parse failed: {0}")]
69 ResponseParse(String),
70 #[error("OCSP responder returned non-successful status: {0}")]
71 ResponderError(String),
72 #[error("OCSP response body exceeds {cap} bytes")]
73 BodyTooLarge { cap: usize },
74}
75
76#[derive(Debug, Clone)]
83pub struct OcspStaple {
84 pub staple: Vec<u8>,
85 pub next_update: SystemTime,
86}
87
88const DEFAULT_NEXT_UPDATE_AHEAD: std::time::Duration = std::time::Duration::from_hours(168);
92
93#[cfg(feature = "fetch")]
98pub const FETCH_TIMEOUT: Duration = Duration::from_secs(10);
99
100pub fn extract_ocsp_url(cert_der: &[u8]) -> Result<String, OcspError> {
114 use x509_parser::extensions::{GeneralName, ParsedExtension};
115 use x509_parser::prelude::FromDer;
116
117 let (_, cert) = x509_parser::prelude::X509Certificate::from_der(cert_der)
118 .map_err(|e| OcspError::CertParse(format!("{e}")))?;
119
120 let mut saw_aia = false;
121 for ext in cert.tbs_certificate.extensions() {
122 if let ParsedExtension::AuthorityInfoAccess(aia) = ext.parsed_extension() {
123 saw_aia = true;
124 for desc in &aia.accessdescs {
125 if desc.access_method.to_id_string() == ID_AD_OCSP
126 && let GeneralName::URI(url) = &desc.access_location
127 {
128 return classify_url(url);
129 }
130 }
131 }
132 }
133 if saw_aia { Err(OcspError::NoOcspUrl) } else { Err(OcspError::NoAia) }
134}
135
136fn classify_url(url: &str) -> Result<String, OcspError> {
139 if url.starts_with("https://") {
140 Err(OcspError::HttpsNotSupported(url.to_owned()))
141 } else if url.starts_with("http://") {
142 Ok(url.to_owned())
143 } else {
144 Err(OcspError::InvalidUrl(format!("expected `http://` scheme, got: {url}")))
145 }
146}
147
148pub fn build_ocsp_request(cert_der: &[u8], issuer_der: &[u8]) -> Result<Vec<u8>, OcspError> {
158 use x509_cert::Certificate;
159 let cert = Certificate::from_der(cert_der).map_err(|e| OcspError::CertParse(format!("{e}")))?;
160 let issuer =
161 Certificate::from_der(issuer_der).map_err(|e| OcspError::CertParse(format!("{e}")))?;
162 let req = OcspRequestBuilder::default()
163 .with_request(
164 OcspReq::from_cert::<Sha1>(&issuer, &cert)
165 .map_err(|e| OcspError::RequestBuild(format!("{e}")))?,
166 )
167 .build();
168 req.to_der().map_err(|e| OcspError::RequestBuild(format!("DER encode: {e}")))
169}
170
171pub fn parse_ocsp_response(resp_der: &[u8]) -> Result<OcspStaple, OcspError> {
182 let resp = OcspResponse::from_der(resp_der)
183 .map_err(|e| OcspError::ResponseParse(format!("OcspResponse decode: {e}")))?;
184
185 if resp.response_status != OcspResponseStatus::Successful {
186 return Err(OcspError::ResponderError(format!("{:?}", resp.response_status)));
187 }
188
189 let response_bytes = resp
190 .response_bytes
191 .as_ref()
192 .ok_or_else(|| OcspError::ResponseParse("successful response has no responseBytes".into()))?;
193 let basic = BasicOcspResponse::from_der(response_bytes.response.as_bytes())
194 .map_err(|e| OcspError::ResponseParse(format!("BasicOcspResponse decode: {e}")))?;
195
196 let single = basic
197 .tbs_response_data
198 .responses
199 .first()
200 .ok_or_else(|| OcspError::ResponseParse("no SingleResponse entries".into()))?;
201
202 let next_update = match &single.next_update {
203 Some(t) => generalized_time_to_system(t),
204 None => {
205 generalized_time_to_system(&basic.tbs_response_data.produced_at) + DEFAULT_NEXT_UPDATE_AHEAD
210 }
211 };
212
213 Ok(OcspStaple { staple: resp_der.to_vec(), next_update })
214}
215
216fn generalized_time_to_system(t: &x509_ocsp::OcspGeneralizedTime) -> SystemTime {
217 SystemTime::UNIX_EPOCH + t.0.to_unix_duration()
218}
219
220#[cfg(feature = "fetch")]
221mod fetch {
222 use std::time::Duration;
223
224 use bytes::Bytes;
225 use http_body_util::{BodyExt, Full, Limited};
226 use hyper::Request;
227
228 use super::{
229 OcspError, OcspStaple, build_ocsp_request, classify_url, extract_ocsp_url, parse_ocsp_response,
230 };
231
232 const MAX_OCSP_BODY_BYTES: usize = 1024 * 1024;
238
239 pub async fn fetch_ocsp(
251 responder_url: &str,
252 request_der: Vec<u8>,
253 timeout: Duration,
254 ) -> Result<Vec<u8>, OcspError> {
255 classify_url(responder_url)?;
256 let parsed = url::Url::parse(responder_url)
257 .map_err(|e| OcspError::InvalidUrl(format!("parse {responder_url}: {e}")))?;
258 let host = parsed
259 .host_str()
260 .ok_or_else(|| OcspError::InvalidUrl(format!("no host in {responder_url}")))?
261 .to_owned();
262 let port = parsed.port().unwrap_or(80);
263 let path_and_query = if parsed.path().is_empty() {
264 "/".to_owned()
265 } else {
266 match parsed.query() {
267 Some(q) => format!("{}?{q}", parsed.path()),
268 None => parsed.path().to_owned(),
269 }
270 };
271
272 let fut = perform_fetch(host.clone(), port, path_and_query, request_der);
273 tokio::time::timeout(timeout, fut)
274 .await
275 .map_err(|_| OcspError::Transport(format!("timed out after {timeout:?}")))?
276 }
277
278 async fn perform_fetch(
279 host: String,
280 port: u16,
281 path_and_query: String,
282 body: Vec<u8>,
283 ) -> Result<Vec<u8>, OcspError> {
284 use hyper_util::rt::TokioIo;
285
286 let stream = tokio::net::TcpStream::connect((host.as_str(), port))
287 .await
288 .map_err(|e| OcspError::Transport(format!("connect {host}:{port}: {e}")))?;
289 let io = TokioIo::new(stream);
290 let (mut sender, conn) = hyper::client::conn::http1::handshake::<_, Full<Bytes>>(io)
291 .await
292 .map_err(|e| OcspError::Transport(format!("handshake: {e}")))?;
293 let conn_handle = tokio::spawn(async move {
294 let _ = conn.await;
297 });
298
299 let body_len = body.len();
300 let req = Request::builder()
301 .method("POST")
302 .uri(path_and_query)
303 .header(hyper::header::HOST, &host)
304 .header(hyper::header::CONTENT_TYPE, "application/ocsp-request")
305 .header(hyper::header::CONTENT_LENGTH, body_len.to_string())
306 .header(hyper::header::CONNECTION, "close")
307 .body(Full::new(Bytes::from(body)))
308 .map_err(|e| OcspError::Transport(format!("build request: {e}")))?;
309
310 let resp =
311 sender.send_request(req).await.map_err(|e| OcspError::Transport(format!("send: {e}")))?;
312 let status = resp.status();
313 if !status.is_success() {
314 conn_handle.abort();
315 return Err(OcspError::HttpStatus { status: status.as_u16() });
316 }
317 let limited = Limited::new(resp.into_body(), MAX_OCSP_BODY_BYTES);
318 let bytes = match limited.collect().await {
319 Ok(collected) => collected.to_bytes(),
320 Err(e) => {
321 conn_handle.abort();
322 if e.downcast_ref::<http_body_util::LengthLimitError>().is_some() {
323 return Err(OcspError::BodyTooLarge { cap: MAX_OCSP_BODY_BYTES });
324 }
325 return Err(OcspError::Transport(format!("read body: {e}")));
326 }
327 };
328 drop(sender);
329 let _ = conn_handle.await;
330 Ok(bytes.to_vec())
331 }
332
333 pub async fn fetch_ocsp_for_cert(
341 cert_der: &[u8],
342 issuer_der: &[u8],
343 timeout: Duration,
344 ) -> Result<OcspStaple, OcspError> {
345 let url = extract_ocsp_url(cert_der)?;
346 let req = build_ocsp_request(cert_der, issuer_der)?;
347 let resp_bytes = fetch_ocsp(&url, req, timeout).await?;
348 parse_ocsp_response(&resp_bytes)
349 }
350}
351
352#[cfg(feature = "fetch")]
353pub use fetch::{fetch_ocsp, fetch_ocsp_for_cert};
354
355#[cfg(test)]
356mod tests {
357 use rcgen::{
358 BasicConstraints, CertificateParams, IsCa, Issuer, KeyPair, KeyUsagePurpose,
359 PKCS_ECDSA_P256_SHA256,
360 };
361 use x509_cert::Certificate;
362
363 use super::*;
364
365 fn build_test_ca_and_leaf(aia_url: &str) -> (Vec<u8>, Vec<u8>) {
371 let ca_key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).expect("ca key");
373 let mut ca_params = CertificateParams::new(vec!["Test CA".to_owned()]).expect("ca params");
374 ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
375 ca_params.key_usages.push(KeyUsagePurpose::KeyCertSign);
376 ca_params.key_usages.push(KeyUsagePurpose::CrlSign);
377 let ca_cert = ca_params.clone().self_signed(&ca_key).expect("self_signed");
378 let ca_der = ca_cert.der().to_vec();
379
380 let leaf_key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).expect("leaf key");
382 let mut leaf_params =
383 CertificateParams::new(vec!["leaf.example".to_owned()]).expect("leaf params");
384 leaf_params.use_authority_key_identifier_extension = true;
385 leaf_params.custom_extensions.push(build_aia_custom_extension(aia_url));
386 let issuer = Issuer::from_params(&ca_params, &ca_key);
387 let leaf_cert = leaf_params.signed_by(&leaf_key, &issuer).expect("leaf signed_by");
388 let leaf_der = leaf_cert.der().to_vec();
389 (ca_der, leaf_der)
390 }
391
392 fn build_aia_custom_extension(aia_url: &str) -> rcgen::CustomExtension {
397 let oid_aia: &[u64] = &[1, 3, 6, 1, 5, 5, 7, 1, 1];
399 let ocsp_oid_der: Vec<u8> = vec![0x06, 0x08, 0x2b, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x01];
407 let url_bytes = aia_url.as_bytes();
408 let mut uri_tlv = vec![0x86];
409 uri_tlv.extend_from_slice(&der_length(url_bytes.len()));
410 uri_tlv.extend_from_slice(url_bytes);
411 let mut access_desc_inner = ocsp_oid_der;
412 access_desc_inner.extend_from_slice(&uri_tlv);
413 let mut access_desc_tlv = vec![0x30];
414 access_desc_tlv.extend_from_slice(&der_length(access_desc_inner.len()));
415 access_desc_tlv.extend_from_slice(&access_desc_inner);
416 let mut outer_tlv = vec![0x30];
417 outer_tlv.extend_from_slice(&der_length(access_desc_tlv.len()));
418 outer_tlv.extend_from_slice(&access_desc_tlv);
419 rcgen::CustomExtension::from_oid_content(oid_aia, outer_tlv)
420 }
421
422 fn der_length(n: usize) -> Vec<u8> {
423 if n < 0x80 {
426 vec![u8::try_from(n).unwrap()]
427 } else if n < 0x100 {
428 vec![0x81, u8::try_from(n).unwrap()]
429 } else {
430 vec![0x82, u8::try_from((n >> 8) & 0xff).unwrap(), u8::try_from(n & 0xff).unwrap()]
431 }
432 }
433
434 #[test]
435 fn extract_ocsp_url_returns_url_for_cert_with_aia() {
436 let (_, leaf_der) = build_test_ca_and_leaf("http://ocsp.example.test/");
437 let url = extract_ocsp_url(&leaf_der).expect("extract ok");
438 assert_eq!(url, "http://ocsp.example.test/");
439 }
440
441 #[test]
442 fn extract_ocsp_url_returns_no_aia_for_cert_without_extension() {
443 let key = KeyPair::generate_for(&PKCS_ECDSA_P256_SHA256).expect("key");
444 let params = CertificateParams::new(vec!["plain.example".to_owned()]).expect("params");
445 let cert = params.self_signed(&key).expect("self_signed");
446 let err = extract_ocsp_url(cert.der()).expect_err("no AIA → err");
447 assert!(matches!(err, OcspError::NoAia), "got {err:?}");
448 }
449
450 #[test]
451 fn extract_ocsp_url_returns_https_not_supported() {
452 let (_, leaf_der) = build_test_ca_and_leaf("https://ocsp.example.test/");
453 let err = extract_ocsp_url(&leaf_der).expect_err("HTTPS rejected");
454 match err {
455 OcspError::HttpsNotSupported(url) => {
456 assert_eq!(url, "https://ocsp.example.test/");
457 }
458 other => panic!("expected HttpsNotSupported, got {other:?}"),
459 }
460 }
461
462 #[test]
463 fn extract_ocsp_url_returns_invalid_url_for_non_http() {
464 let (_, leaf_der) = build_test_ca_and_leaf("ftp://ocsp.example.test/");
465 let err = extract_ocsp_url(&leaf_der).expect_err("ftp rejected");
466 assert!(matches!(err, OcspError::InvalidUrl(_)), "got {err:?}");
467 }
468
469 #[test]
470 fn build_ocsp_request_round_trips_through_x509_ocsp() {
471 let (issuer_der, leaf_der) = build_test_ca_and_leaf("http://ocsp.example.test/");
472 let bytes = build_ocsp_request(&leaf_der, &issuer_der).expect("build ok");
473 let req = x509_ocsp::OcspRequest::from_der(&bytes).expect("decode");
474 assert!(!req.tbs_request.request_list.is_empty());
475 let leaf = Certificate::from_der(&leaf_der).expect("leaf decode");
476 let want_serial = leaf.tbs_certificate.serial_number.clone();
477 let got_serial = req.tbs_request.request_list[0].req_cert.serial_number.clone();
478 assert_eq!(got_serial.as_bytes(), want_serial.as_bytes());
479 }
480
481 #[test]
482 fn parse_ocsp_response_returns_responder_error_on_try_later() {
483 let bytes = OcspResponse::try_later().to_der().expect("encode");
484 let err = parse_ocsp_response(&bytes).expect_err("try_later → err");
485 assert!(matches!(err, OcspError::ResponderError(_)), "got {err:?}");
486 }
487
488 #[test]
489 fn parse_ocsp_response_rejects_garbage_bytes() {
490 let err = parse_ocsp_response(&[0x30, 0x00]).expect_err("garbage rejected");
491 assert!(matches!(err, OcspError::ResponseParse(_)), "got {err:?}");
492 }
493
494 #[cfg(feature = "fetch")]
495 #[test]
496 fn fetch_ocsp_rejects_https_url_pre_connect() {
497 let rt = tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap();
500 let err = rt.block_on(async {
501 fetch_ocsp("https://ocsp.example.test/", vec![1, 2, 3], std::time::Duration::from_secs(1))
502 .await
503 .expect_err("https rejected")
504 });
505 assert!(matches!(err, OcspError::HttpsNotSupported(_)), "got {err:?}");
506 }
507}