ocsp_stapler/
client.rs

1use std::time::Duration;
2
3use anyhow::{anyhow, Context, Error};
4use base64::prelude::*;
5use http::{header::CONTENT_TYPE, StatusCode};
6use num_bigint::BigInt;
7use rasn::types::{OctetString, Oid};
8use rasn_ocsp::{
9    BasicOcspResponse, CertId, CertStatus, OcspRequest, OcspResponse, OcspResponseStatus, Request,
10    TbsRequest, Version,
11};
12use rasn_pkix::AlgorithmIdentifier;
13use sha1::{Digest, Sha1};
14use url::Url;
15use x509_parser::{oid_registry::OID_PKIX_ACCESS_DESCRIPTOR_OCSP, prelude::*};
16
17use super::Validity;
18
19/// OCSP response
20pub struct Response {
21    /// Raw OCSP response body.
22    /// Useful e.g. for stapling
23    pub raw: Vec<u8>,
24    /// OCSP response validity interval
25    pub ocsp_validity: Validity,
26    /// Certificate revocation status
27    pub cert_status: CertStatus,
28}
29
30/// Extracts OCSP responder URL from the given certificate
31fn extract_ocsp_url(cert: &X509Certificate) -> Option<String> {
32    cert.extensions()
33        .iter()
34        .find_map(|x| {
35            if let ParsedExtension::AuthorityInfoAccess(v) = x.parsed_extension() {
36                Some(v)
37            } else {
38                None
39            }
40        })?
41        .accessdescs
42        .iter()
43        .filter(|x| x.access_method == OID_PKIX_ACCESS_DESCRIPTOR_OCSP)
44        .find_map(|x| {
45            if let GeneralName::URI(v) = x.access_location {
46                Some(v.to_string())
47            } else {
48                None
49            }
50        })
51}
52
53/// Prepares the OCSP request for given cert/issuer pair
54fn prepare_ocsp_request(cert: &[u8], issuer: &[u8]) -> Result<(OcspRequest, Url), Error> {
55    // Parse the DER-encoded cert & issuer
56    let cert = X509Certificate::from_der(cert)
57        .context("unable to parse cert")?
58        .1;
59    let issuer = X509Certificate::from_der(issuer)
60        .context("unable to parse issuer")?
61        .1;
62
63    // Extract OCSP responder URL
64    let url =
65        Url::parse(&extract_ocsp_url(&cert).ok_or_else(|| anyhow!("unable to extract OCSP URL"))?)
66            .context("unable to parse OCSP URL")?;
67
68    // LetsEncrypt supports only lightweight OCSP profile with SHA1 exclusively.
69    // Since its purpose here is non-cryptographic - it's not a security issue.
70    //
71    // See:
72    // - https://github.com/letsencrypt/boulder/issues/5523#issuecomment-877301162
73    // - https://datatracker.ietf.org/doc/html/rfc5019
74    let hash_algorithm = AlgorithmIdentifier {
75        algorithm: Oid::ISO_IDENTIFIED_ORGANISATION_OIW_SECSIG_ALGORITHM_SHA1.to_owned(),
76        parameters: None,
77    };
78
79    // Calculate the hashes required for OCSP request
80    let issuer_name_hash = OctetString::from_slice(Sha1::digest(cert.issuer.as_raw()).as_slice());
81    let issuer_key_hash =
82        OctetString::from_slice(Sha1::digest(&issuer.public_key().subject_public_key).as_slice());
83
84    // Prepare the request
85    let serial_number: BigInt = cert.serial.clone().into();
86    let req_cert = CertId {
87        hash_algorithm,
88        serial_number: serial_number.into(),
89        issuer_name_hash,
90        issuer_key_hash,
91    };
92
93    let request = Request {
94        req_cert,
95        single_request_extensions: None,
96    };
97
98    let tbs_request = TbsRequest {
99        version: Version::ZERO,
100        requestor_name: None,
101        request_list: vec![request],
102        request_extensions: None,
103    };
104
105    Ok((
106        OcspRequest {
107            tbs_request,
108            optional_signature: None,
109        },
110        url,
111    ))
112}
113
114/// OCSP client
115pub struct Client {
116    http_client: reqwest::Client,
117}
118
119impl Default for Client {
120    fn default() -> Self {
121        Self::new()
122    }
123}
124
125impl Client {
126    /// Creates a new OCSP client with a default Reqwest client
127    pub fn new() -> Self {
128        Self {
129            http_client: reqwest::Client::builder()
130                .connect_timeout(Duration::from_millis(3000))
131                .timeout(Duration::from_millis(6000))
132                .build()
133                .unwrap(),
134        }
135    }
136
137    /// Creates a new OCSP client using provided Reqwest client
138    pub const fn new_with_client(http_client: reqwest::Client) -> Self {
139        Self { http_client }
140    }
141
142    async fn execute(&self, url: Url, ocsp_request: OcspRequest) -> Result<OcspResponse, Error> {
143        // DER-encode it
144        let ocsp_request = rasn::der::encode(&ocsp_request)
145            .map_err(|e| anyhow!("unable to serialize OCSP request: {e}"))?;
146
147        // Execute HTTP request
148        // Send using GET if it's <= 255 bytes as required by
149        // https://datatracker.ietf.org/doc/html/rfc5019
150        let request = if ocsp_request.len() <= 255 {
151            // Encode the request as Base64 and append it to the URL
152            let ocsp_request = BASE64_STANDARD.encode(ocsp_request);
153            let url = url
154                .join(&ocsp_request)
155                .context("unable to append base64 request")?;
156
157            self.http_client.get(url)
158        } else {
159            self.http_client.post(url).body(ocsp_request)
160        };
161
162        let response = request
163            .header(CONTENT_TYPE, "application/ocsp-request")
164            .send()
165            .await
166            .context("HTTP request failed")?;
167
168        if response.status() != StatusCode::OK {
169            return Err(anyhow!("Incorrect HTTP code: {}", response.status()));
170        }
171
172        let body = response
173            .bytes()
174            .await
175            .context("unable to read OCSP response body")?;
176
177        // Parse the response
178        let ocsp_response: OcspResponse = rasn::der::decode(&body)
179            .map_err(|e| anyhow!("unable to decode OcspResponse: {e:#}"))?;
180
181        Ok(ocsp_response)
182    }
183
184    /// Fetches the raw OCSP response for the given certificate chain.
185    /// Certificates must be DER-encoded.
186    pub async fn query_raw(&self, cert: &[u8], issuer: &[u8]) -> Result<OcspResponse, Error> {
187        // Prepare OCSP request & URL
188        let (ocsp_request, url) =
189            prepare_ocsp_request(cert, issuer).context("unable to prepare OCSP request")?;
190
191        self.execute(url, ocsp_request).await
192    }
193
194    /// Fetches the raw OCSP response and returns its validity & status.
195    /// Certificates must be DER-encoded.
196    pub async fn query(&self, cert: &[u8], issuer: &[u8]) -> Result<Response, Error> {
197        let ocsp_response = self
198            .query_raw(cert, issuer)
199            .await
200            .context("Unable to perform OCSP query")?;
201
202        if ocsp_response.status != OcspResponseStatus::Successful {
203            return Err(anyhow!(
204                "Incorrect OCSP response status: {:?}",
205                ocsp_response.status
206            ));
207        }
208
209        // DER-encode it
210        let raw = rasn::der::encode(&ocsp_response)
211            .map_err(|e| anyhow!("unable to serialize OCSP response: {e}"))?;
212
213        let ocsp_basic: BasicOcspResponse = rasn::der::decode(
214            &ocsp_response
215                .bytes
216                .ok_or_else(|| anyhow!("empty OCSP response"))?
217                .response,
218        )
219        .map_err(|e| anyhow!("unable to decode BasicOcspResponse: {e}"))?;
220
221        if ocsp_basic.tbs_response_data.responses.len() != 1 {
222            return Err(anyhow!(
223                "OCSP response should contain exactly one certificate"
224            ));
225        }
226
227        let resp = ocsp_basic.tbs_response_data.responses[0].clone();
228
229        Ok(Response {
230            raw,
231            cert_status: resp.cert_status,
232            ocsp_validity: Validity {
233                not_before: resp.this_update,
234                not_after: resp
235                    .next_update
236                    .ok_or_else(|| anyhow!("No next-update field in the response"))?,
237            },
238        })
239    }
240}
241
242#[cfg(test)]
243pub(crate) mod test {
244    use std::str::FromStr;
245
246    use super::*;
247
248    use hex_literal::hex;
249    use httptest::{matchers::*, responders::*, Expectation, Server};
250    use rasn::types::{Integer, OctetString};
251    use rustls::{crypto::ring, sign::CertifiedKey};
252
253    const OCSP_REQUEST: &[u8] = include_bytes!("../test/ocsp_request.bin");
254    const OCSP_RESPONSE: &[u8] = include_bytes!("../test/ocsp_response.bin");
255    const CHAIN: &[u8] = include_bytes!("../test/chain.pem");
256    const KEY: &[u8] = include_bytes!("../test/key.pem");
257
258    fn test_request() -> OcspRequest {
259        OcspRequest {
260            tbs_request: TbsRequest {
261                version: Version::ZERO,
262                requestor_name: None,
263                request_list: vec![Request {
264                    req_cert: CertId {
265                        hash_algorithm: AlgorithmIdentifier {
266                            algorithm: Oid::ISO_IDENTIFIED_ORGANISATION_OIW_SECSIG_ALGORITHM_SHA1
267                                .to_owned(),
268                            parameters: None,
269                        },
270                        issuer_name_hash: OctetString::from(
271                            &hex!("36175FAA02C887BDD95CA13549512D1E97FADFA9")[..],
272                        ),
273                        issuer_key_hash: OctetString::from(
274                            &hex!("6691287B8D8654BAF6203197AEC491E9AFB70BCB")[..],
275                        ),
276                        serial_number: Integer::from(
277                            num_bigint::BigInt::from_str(
278                                "3819096869935823013274658159093914787918510",
279                            )
280                            .unwrap(),
281                        ),
282                    },
283                    single_request_extensions: None,
284                }],
285                request_extensions: None,
286            },
287            optional_signature: None,
288        }
289    }
290
291    pub(crate) fn test_ckey() -> CertifiedKey {
292        let certs = CHAIN.to_vec();
293        let certs = rustls_pemfile::certs(&mut certs.as_ref())
294            .collect::<Result<Vec<_>, _>>()
295            .unwrap();
296
297        let key = KEY.to_vec();
298        let key = rustls_pemfile::private_key(&mut key.as_ref())
299            .unwrap()
300            .unwrap();
301
302        let key = ring::sign::any_supported_type(&key).unwrap();
303        CertifiedKey::new(certs, key)
304    }
305
306    #[test]
307    fn test_extract_url() {
308        let certs = CHAIN.to_vec();
309        let certs = rustls_pemfile::certs(&mut certs.as_ref())
310            .collect::<Result<Vec<_>, _>>()
311            .unwrap();
312
313        let cert = X509Certificate::from_der(&certs[0]).unwrap().1;
314
315        assert_eq!(
316            extract_ocsp_url(&cert),
317            Some("http://stg-e5.o.lencr.org".to_string())
318        )
319    }
320
321    #[test]
322    fn test_prepare_request() {
323        let certs = CHAIN.to_vec();
324        let certs = rustls_pemfile::certs(&mut certs.as_ref())
325            .collect::<Result<Vec<_>, _>>()
326            .unwrap();
327
328        let (ocsp_request, url) = prepare_ocsp_request(&certs[0], &certs[1]).unwrap();
329
330        assert_eq!(url.to_string(), "http://stg-e5.o.lencr.org/");
331        assert_eq!(ocsp_request, test_request());
332        assert_eq!(rasn::der::encode(&ocsp_request).unwrap(), OCSP_REQUEST);
333    }
334
335    #[tokio::test]
336    async fn test_execute() {
337        // Install a cryptography provider, otherwise the OCSP stapler would panic
338        ring::default_provider()
339            .install_default()
340            .unwrap_or_default();
341
342        let server = Server::run();
343
344        server.expect(
345            Expectation::matching(request::method("GET"))
346                .respond_with(status_code(200).body(OCSP_RESPONSE)),
347        );
348
349        let client = Client::new();
350        let ocsp_response = client
351            .execute(Url::parse(&server.url_str("/")).unwrap(), test_request())
352            .await
353            .unwrap();
354
355        assert_eq!(OCSP_RESPONSE, rasn::der::encode(&ocsp_response).unwrap());
356    }
357}