Skip to main content

variant_ssl/
ocsp.rs

1use bitflags::bitflags;
2use foreign_types::{ForeignType, ForeignTypeRef};
3use libc::{c_int, c_long, c_ulong};
4use std::mem;
5use std::ptr;
6use std::sync::OnceLock;
7
8use crate::asn1::{Asn1GeneralizedTime, Asn1GeneralizedTimeRef};
9use crate::error::ErrorStack;
10use crate::hash::MessageDigest;
11use crate::stack::StackRef;
12use crate::util::ForeignTypeRefExt;
13use crate::x509::store::X509StoreRef;
14use crate::x509::{X509Ref, X509};
15use crate::{cvt, cvt_p};
16use openssl_macros::corresponds;
17
18// Sentinel value used when next_update is not present in OCSP response
19// This represents the maximum possible time (9999-12-31 23:59:59 UTC)
20static SENTINEL_MAX_TIME: OnceLock<Asn1GeneralizedTime> = OnceLock::new();
21
22fn get_sentinel_max_time() -> &'static Asn1GeneralizedTimeRef {
23    SENTINEL_MAX_TIME
24        .get_or_init(|| {
25            Asn1GeneralizedTime::from_str("99991231235959Z")
26                .expect("Failed to create sentinel time")
27        })
28        .as_ref()
29}
30
31bitflags! {
32    #[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
33    #[repr(transparent)]
34    pub struct OcspFlag: c_ulong {
35        const NO_CERTS = ffi::OCSP_NOCERTS as c_ulong;
36        const NO_INTERN = ffi::OCSP_NOINTERN as c_ulong;
37        const NO_CHAIN = ffi::OCSP_NOCHAIN as c_ulong;
38        const NO_VERIFY = ffi::OCSP_NOVERIFY as c_ulong;
39        const NO_EXPLICIT = ffi::OCSP_NOEXPLICIT as c_ulong;
40        #[cfg(not(awslc_fips))]
41        const NO_CA_SIGN = ffi::OCSP_NOCASIGN as c_ulong;
42        #[cfg(not(awslc_fips))]
43        const NO_DELEGATED = ffi::OCSP_NODELEGATED as c_ulong;
44        #[cfg(not(awslc_fips))]
45        const NO_CHECKS = ffi::OCSP_NOCHECKS as c_ulong;
46        const TRUST_OTHER = ffi::OCSP_TRUSTOTHER as c_ulong;
47        const RESPID_KEY = ffi::OCSP_RESPID_KEY as c_ulong;
48        const NO_TIME = ffi::OCSP_NOTIME as c_ulong;
49    }
50}
51
52#[derive(Copy, Clone, Debug, PartialEq, Eq)]
53pub struct OcspResponseStatus(c_int);
54
55impl OcspResponseStatus {
56    pub const SUCCESSFUL: OcspResponseStatus =
57        OcspResponseStatus(ffi::OCSP_RESPONSE_STATUS_SUCCESSFUL);
58    pub const MALFORMED_REQUEST: OcspResponseStatus =
59        OcspResponseStatus(ffi::OCSP_RESPONSE_STATUS_MALFORMEDREQUEST);
60    pub const INTERNAL_ERROR: OcspResponseStatus =
61        OcspResponseStatus(ffi::OCSP_RESPONSE_STATUS_INTERNALERROR);
62    pub const TRY_LATER: OcspResponseStatus =
63        OcspResponseStatus(ffi::OCSP_RESPONSE_STATUS_TRYLATER);
64    pub const SIG_REQUIRED: OcspResponseStatus =
65        OcspResponseStatus(ffi::OCSP_RESPONSE_STATUS_SIGREQUIRED);
66    pub const UNAUTHORIZED: OcspResponseStatus =
67        OcspResponseStatus(ffi::OCSP_RESPONSE_STATUS_UNAUTHORIZED);
68
69    pub fn from_raw(raw: c_int) -> OcspResponseStatus {
70        OcspResponseStatus(raw)
71    }
72
73    #[allow(clippy::trivially_copy_pass_by_ref)]
74    pub fn as_raw(&self) -> c_int {
75        self.0
76    }
77}
78
79#[derive(Copy, Clone, Debug, PartialEq, Eq)]
80pub struct OcspCertStatus(c_int);
81
82impl OcspCertStatus {
83    pub const GOOD: OcspCertStatus = OcspCertStatus(ffi::V_OCSP_CERTSTATUS_GOOD);
84    pub const REVOKED: OcspCertStatus = OcspCertStatus(ffi::V_OCSP_CERTSTATUS_REVOKED);
85    pub const UNKNOWN: OcspCertStatus = OcspCertStatus(ffi::V_OCSP_CERTSTATUS_UNKNOWN);
86
87    pub fn from_raw(raw: c_int) -> OcspCertStatus {
88        OcspCertStatus(raw)
89    }
90
91    #[allow(clippy::trivially_copy_pass_by_ref)]
92    pub fn as_raw(&self) -> c_int {
93        self.0
94    }
95}
96
97#[derive(Copy, Clone, Debug, PartialEq, Eq)]
98pub struct OcspRevokedStatus(c_int);
99
100impl OcspRevokedStatus {
101    #[cfg(not(awslc_fips))]
102    pub const NO_STATUS: OcspRevokedStatus = OcspRevokedStatus(ffi::OCSP_REVOKED_STATUS_NOSTATUS);
103    #[cfg(awslc_fips)]
104    pub const NO_STATUS: OcspRevokedStatus = OcspRevokedStatus(-1);
105    pub const UNSPECIFIED: OcspRevokedStatus =
106        OcspRevokedStatus(ffi::OCSP_REVOKED_STATUS_UNSPECIFIED);
107    pub const KEY_COMPROMISE: OcspRevokedStatus =
108        OcspRevokedStatus(ffi::OCSP_REVOKED_STATUS_KEYCOMPROMISE);
109    pub const CA_COMPROMISE: OcspRevokedStatus =
110        OcspRevokedStatus(ffi::OCSP_REVOKED_STATUS_CACOMPROMISE);
111    pub const AFFILIATION_CHANGED: OcspRevokedStatus =
112        OcspRevokedStatus(ffi::OCSP_REVOKED_STATUS_AFFILIATIONCHANGED);
113    pub const STATUS_SUPERSEDED: OcspRevokedStatus =
114        OcspRevokedStatus(ffi::OCSP_REVOKED_STATUS_SUPERSEDED);
115    pub const STATUS_CESSATION_OF_OPERATION: OcspRevokedStatus =
116        OcspRevokedStatus(ffi::OCSP_REVOKED_STATUS_CESSATIONOFOPERATION);
117    pub const STATUS_CERTIFICATE_HOLD: OcspRevokedStatus =
118        OcspRevokedStatus(ffi::OCSP_REVOKED_STATUS_CERTIFICATEHOLD);
119    pub const REMOVE_FROM_CRL: OcspRevokedStatus =
120        OcspRevokedStatus(ffi::OCSP_REVOKED_STATUS_REMOVEFROMCRL);
121
122    pub fn from_raw(raw: c_int) -> OcspRevokedStatus {
123        OcspRevokedStatus(raw)
124    }
125
126    #[allow(clippy::trivially_copy_pass_by_ref)]
127    pub fn as_raw(&self) -> c_int {
128        self.0
129    }
130}
131
132pub struct OcspStatus<'a> {
133    /// The overall status of the response.
134    pub status: OcspCertStatus,
135    /// If `status` is `CERT_STATUS_REVOKED`, the reason for the revocation.
136    pub reason: OcspRevokedStatus,
137    /// If `status` is `CERT_STATUS_REVOKED`, the time at which the certificate was revoked.
138    pub revocation_time: Option<&'a Asn1GeneralizedTimeRef>,
139    /// The time that this revocation check was performed.
140    pub this_update: &'a Asn1GeneralizedTimeRef,
141    /// The time at which this revocation check expires.
142    ///
143    /// # Deprecated
144    /// Contains a sentinel maximum time (99991231235959Z) when the field is
145    /// not present in the response.
146    /// Use [`next_update()`](Self::next_update) instead.
147    #[deprecated(since = "0.10.75", note = "Use the next_update() method instead")]
148    pub next_update: &'a Asn1GeneralizedTimeRef,
149    // The actual optional next_update value from the OCSP response.
150    next_update_opt: Option<&'a Asn1GeneralizedTimeRef>,
151}
152
153impl OcspStatus<'_> {
154    /// Returns the time at which this revocation check expires.
155    ///
156    /// Returns `None` if the OCSP response does not include a `next_update`
157    /// field.
158    pub fn next_update(&self) -> Option<&Asn1GeneralizedTimeRef> {
159        self.next_update_opt
160    }
161
162    /// Checks validity of the `this_update` and `next_update` fields.
163    ///
164    /// The `nsec` parameter specifies an amount of slack time that will be used when comparing
165    /// those times with the current time to account for delays and clock skew.
166    ///
167    /// The `maxsec` parameter limits the maximum age of the `this_update` parameter to prohibit
168    /// very old responses.
169    #[corresponds(OCSP_check_validity)]
170    pub fn check_validity(&self, nsec: u32, maxsec: Option<u32>) -> Result<(), ErrorStack> {
171        let next_update_ptr = self
172            .next_update_opt
173            .map(|t| t.as_ptr())
174            .unwrap_or(ptr::null_mut());
175        unsafe {
176            cvt(ffi::OCSP_check_validity(
177                self.this_update.as_ptr(),
178                next_update_ptr,
179                nsec as c_long,
180                maxsec.map(|n| n as c_long).unwrap_or(-1),
181            ))
182            .map(|_| ())
183        }
184    }
185}
186
187foreign_type_and_impl_send_sync! {
188    type CType = ffi::OCSP_BASICRESP;
189    fn drop = ffi::OCSP_BASICRESP_free;
190
191    pub struct OcspBasicResponse;
192    pub struct OcspBasicResponseRef;
193}
194
195impl OcspBasicResponseRef {
196    /// Verifies the validity of the response.
197    ///
198    /// The `certs` parameter contains a set of certificates that will be searched when locating the
199    /// OCSP response signing certificate. Some responders do not include this in the response.
200    #[corresponds(OCSP_basic_verify)]
201    pub fn verify(
202        &self,
203        certs: &StackRef<X509>,
204        store: &X509StoreRef,
205        flags: OcspFlag,
206    ) -> Result<(), ErrorStack> {
207        unsafe {
208            cvt(ffi::OCSP_basic_verify(
209                self.as_ptr(),
210                certs.as_ptr(),
211                store.as_ptr(),
212                flags.bits(),
213            ))
214            .map(|_| ())
215        }
216    }
217
218    /// Looks up the status for the specified certificate ID.
219    #[corresponds(OCSP_resp_find_status)]
220    pub fn find_status<'a>(&'a self, id: &OcspCertIdRef) -> Option<OcspStatus<'a>> {
221        unsafe {
222            let mut status = ffi::V_OCSP_CERTSTATUS_UNKNOWN;
223            let mut reason = ffi::OCSP_REVOKED_STATUS_NOSTATUS;
224            let mut revocation_time = ptr::null_mut();
225            let mut this_update = ptr::null_mut();
226            let mut next_update = ptr::null_mut();
227
228            let r = ffi::OCSP_resp_find_status(
229                self.as_ptr(),
230                id.as_ptr(),
231                &mut status,
232                &mut reason,
233                &mut revocation_time,
234                &mut this_update,
235                &mut next_update,
236            );
237            if r == 1 {
238                let revocation_time = Asn1GeneralizedTimeRef::from_const_ptr_opt(revocation_time);
239                let next_update_opt = Asn1GeneralizedTimeRef::from_const_ptr_opt(next_update);
240                // For backwards compatibility, use sentinel max time if next_update is not present
241                let next_update_compat = next_update_opt.unwrap_or_else(|| get_sentinel_max_time());
242
243                #[allow(deprecated)]
244                Some(OcspStatus {
245                    status: OcspCertStatus(status),
246                    reason: OcspRevokedStatus(reason),
247                    revocation_time,
248                    this_update: Asn1GeneralizedTimeRef::from_ptr(this_update),
249                    next_update: next_update_compat,
250                    next_update_opt,
251                })
252            } else {
253                None
254            }
255        }
256    }
257}
258
259foreign_type_and_impl_send_sync! {
260    type CType = ffi::OCSP_CERTID;
261    fn drop = ffi::OCSP_CERTID_free;
262
263    pub struct OcspCertId;
264    pub struct OcspCertIdRef;
265}
266
267impl OcspCertId {
268    /// Constructs a certificate ID for certificate `subject`.
269    #[corresponds(OCSP_cert_to_id)]
270    pub fn from_cert(
271        digest: MessageDigest,
272        subject: &X509Ref,
273        issuer: &X509Ref,
274    ) -> Result<OcspCertId, ErrorStack> {
275        unsafe {
276            cvt_p(ffi::OCSP_cert_to_id(
277                digest.as_ptr(),
278                subject.as_ptr(),
279                issuer.as_ptr(),
280            ))
281            .map(OcspCertId)
282        }
283    }
284}
285
286foreign_type_and_impl_send_sync! {
287    type CType = ffi::OCSP_RESPONSE;
288    fn drop = ffi::OCSP_RESPONSE_free;
289
290    pub struct OcspResponse;
291    pub struct OcspResponseRef;
292}
293
294impl OcspResponse {
295    /// Creates an OCSP response from the status and optional body.
296    ///
297    /// A body should only be provided if `status` is `RESPONSE_STATUS_SUCCESSFUL`.
298    #[corresponds(OCSP_response_create)]
299    pub fn create(
300        status: OcspResponseStatus,
301        body: Option<&OcspBasicResponseRef>,
302    ) -> Result<OcspResponse, ErrorStack> {
303        unsafe {
304            ffi::init();
305
306            cvt_p(ffi::OCSP_response_create(
307                status.as_raw(),
308                body.map(|r| r.as_ptr()).unwrap_or(ptr::null_mut()),
309            ))
310            .map(OcspResponse)
311        }
312    }
313
314    from_der! {
315        /// Deserializes a DER-encoded OCSP response.
316        #[corresponds(d2i_OCSP_RESPONSE)]
317        from_der,
318        OcspResponse,
319        ffi::d2i_OCSP_RESPONSE
320    }
321}
322
323impl OcspResponseRef {
324    to_der! {
325        /// Serializes the response to its standard DER encoding.
326        #[corresponds(i2d_OCSP_RESPONSE)]
327        to_der,
328        ffi::i2d_OCSP_RESPONSE
329    }
330
331    /// Returns the status of the response.
332    #[corresponds(OCSP_response_status)]
333    pub fn status(&self) -> OcspResponseStatus {
334        unsafe { OcspResponseStatus(ffi::OCSP_response_status(self.as_ptr())) }
335    }
336
337    /// Returns the basic response.
338    ///
339    /// This will only succeed if `status()` returns `RESPONSE_STATUS_SUCCESSFUL`.
340    #[corresponds(OCSP_response_get1_basic)]
341    pub fn basic(&self) -> Result<OcspBasicResponse, ErrorStack> {
342        unsafe { cvt_p(ffi::OCSP_response_get1_basic(self.as_ptr())).map(OcspBasicResponse) }
343    }
344}
345
346foreign_type_and_impl_send_sync! {
347    type CType = ffi::OCSP_REQUEST;
348    fn drop = ffi::OCSP_REQUEST_free;
349
350    pub struct OcspRequest;
351    pub struct OcspRequestRef;
352}
353
354impl OcspRequest {
355    #[corresponds(OCSP_REQUEST_new)]
356    pub fn new() -> Result<OcspRequest, ErrorStack> {
357        unsafe {
358            ffi::init();
359
360            cvt_p(ffi::OCSP_REQUEST_new()).map(OcspRequest)
361        }
362    }
363
364    from_der! {
365        /// Deserializes a DER-encoded OCSP request.
366        #[corresponds(d2i_OCSP_REQUEST)]
367        from_der,
368        OcspRequest,
369        ffi::d2i_OCSP_REQUEST
370    }
371}
372
373impl OcspRequestRef {
374    to_der! {
375        /// Serializes the request to its standard DER encoding.
376        #[corresponds(i2d_OCSP_REQUEST)]
377        to_der,
378        ffi::i2d_OCSP_REQUEST
379    }
380
381    #[corresponds(OCSP_request_add0_id)]
382    pub fn add_id(&mut self, id: OcspCertId) -> Result<&mut OcspOneReqRef, ErrorStack> {
383        unsafe {
384            let ptr = cvt_p(ffi::OCSP_request_add0_id(self.as_ptr(), id.as_ptr()))?;
385            mem::forget(id);
386            Ok(OcspOneReqRef::from_ptr_mut(ptr))
387        }
388    }
389}
390
391foreign_type_and_impl_send_sync! {
392    type CType = ffi::OCSP_ONEREQ;
393    fn drop = ffi::OCSP_ONEREQ_free;
394
395    pub struct OcspOneReq;
396    pub struct OcspOneReqRef;
397}
398
399#[cfg(test)]
400mod tests {
401    use super::{
402        get_sentinel_max_time, OcspCertId, OcspCertStatus, OcspResponse, OcspResponseStatus,
403        OcspRevokedStatus,
404    };
405    use crate::hash::MessageDigest;
406    use crate::x509::X509;
407
408    // Test vectors: OCSP response with next_update=NULL and associated certificates
409    const OCSP_RESPONSE_NO_NEXTUPDATE: &[u8] =
410        include_bytes!("../test/ocsp_resp_no_nextupdate.der");
411    const OCSP_CA_CERT: &[u8] = include_bytes!("../test/ocsp_ca_cert.der");
412    const OCSP_SUBJECT_CERT: &[u8] = include_bytes!("../test/ocsp_subject_cert.der");
413    const OCSP_RESPONSE_REVOKED: &[u8] = include_bytes!("../test/ocsp_resp_revoked.der");
414
415    #[test]
416    fn test_ocsp_no_next_update() {
417        // Verify find_status correctly handles OCSP responses with next_update=NULL
418        let response = OcspResponse::from_der(OCSP_RESPONSE_NO_NEXTUPDATE).unwrap();
419        assert_eq!(response.status(), OcspResponseStatus::SUCCESSFUL);
420
421        let ca_cert = X509::from_der(OCSP_CA_CERT).unwrap();
422        let subject_cert = X509::from_der(OCSP_SUBJECT_CERT).unwrap();
423        let basic = response.basic().unwrap();
424
425        let cert_id =
426            OcspCertId::from_cert(MessageDigest::sha256(), &subject_cert, &ca_cert).unwrap();
427
428        let status = basic
429            .find_status(&cert_id)
430            .expect("find_status should find the status");
431
432        assert!(status.next_update().is_none());
433
434        #[allow(deprecated)]
435        let deprecated_next = status.next_update;
436        let sentinel = get_sentinel_max_time();
437        assert_eq!(format!("{}", deprecated_next), format!("{}", sentinel));
438
439        assert_eq!(status.status, OcspCertStatus::GOOD);
440    }
441
442    #[test]
443    fn test_ocsp_revoked() {
444        let response = OcspResponse::from_der(OCSP_RESPONSE_REVOKED).unwrap();
445        assert_eq!(response.status(), OcspResponseStatus::SUCCESSFUL);
446
447        let ca_cert = X509::from_der(OCSP_CA_CERT).unwrap();
448        let subject_cert = X509::from_der(OCSP_SUBJECT_CERT).unwrap();
449        let basic = response.basic().unwrap();
450
451        let cert_id =
452            OcspCertId::from_cert(MessageDigest::sha256(), &subject_cert, &ca_cert).unwrap();
453
454        let status = basic
455            .find_status(&cert_id)
456            .expect("find_status should find the status");
457
458        assert_eq!(status.status, OcspCertStatus::REVOKED);
459        assert_eq!(status.reason, OcspRevokedStatus::STATUS_SUPERSEDED);
460    }
461}