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            #[cfg(not(awslc_fips))]
224            let mut reason = ffi::OCSP_REVOKED_STATUS_NOSTATUS;
225            #[cfg(awslc_fips)]
226            let mut reason = -1;
227            let mut revocation_time = ptr::null_mut();
228            let mut this_update = ptr::null_mut();
229            let mut next_update = ptr::null_mut();
230
231            let r = ffi::OCSP_resp_find_status(
232                self.as_ptr(),
233                id.as_ptr(),
234                &mut status,
235                &mut reason,
236                &mut revocation_time,
237                &mut this_update,
238                &mut next_update,
239            );
240            if r == 1 {
241                let revocation_time = Asn1GeneralizedTimeRef::from_const_ptr_opt(revocation_time);
242                let next_update_opt = Asn1GeneralizedTimeRef::from_const_ptr_opt(next_update);
243                // For backwards compatibility, use sentinel max time if next_update is not present
244                let next_update_compat = next_update_opt.unwrap_or_else(|| get_sentinel_max_time());
245
246                #[allow(deprecated)]
247                Some(OcspStatus {
248                    status: OcspCertStatus(status),
249                    reason: OcspRevokedStatus(reason),
250                    revocation_time,
251                    this_update: Asn1GeneralizedTimeRef::from_ptr(this_update),
252                    next_update: next_update_compat,
253                    next_update_opt,
254                })
255            } else {
256                None
257            }
258        }
259    }
260}
261
262foreign_type_and_impl_send_sync! {
263    type CType = ffi::OCSP_CERTID;
264    fn drop = ffi::OCSP_CERTID_free;
265
266    pub struct OcspCertId;
267    pub struct OcspCertIdRef;
268}
269
270impl OcspCertId {
271    /// Constructs a certificate ID for certificate `subject`.
272    #[corresponds(OCSP_cert_to_id)]
273    pub fn from_cert(
274        digest: MessageDigest,
275        subject: &X509Ref,
276        issuer: &X509Ref,
277    ) -> Result<OcspCertId, ErrorStack> {
278        unsafe {
279            cvt_p(ffi::OCSP_cert_to_id(
280                digest.as_ptr(),
281                subject.as_ptr(),
282                issuer.as_ptr(),
283            ))
284            .map(OcspCertId)
285        }
286    }
287}
288
289foreign_type_and_impl_send_sync! {
290    type CType = ffi::OCSP_RESPONSE;
291    fn drop = ffi::OCSP_RESPONSE_free;
292
293    pub struct OcspResponse;
294    pub struct OcspResponseRef;
295}
296
297impl OcspResponse {
298    /// Creates an OCSP response from the status and optional body.
299    ///
300    /// A body should only be provided if `status` is `RESPONSE_STATUS_SUCCESSFUL`.
301    #[corresponds(OCSP_response_create)]
302    pub fn create(
303        status: OcspResponseStatus,
304        body: Option<&OcspBasicResponseRef>,
305    ) -> Result<OcspResponse, ErrorStack> {
306        unsafe {
307            ffi::init();
308
309            cvt_p(ffi::OCSP_response_create(
310                status.as_raw(),
311                body.map(|r| r.as_ptr()).unwrap_or(ptr::null_mut()),
312            ))
313            .map(OcspResponse)
314        }
315    }
316
317    from_der! {
318        /// Deserializes a DER-encoded OCSP response.
319        #[corresponds(d2i_OCSP_RESPONSE)]
320        from_der,
321        OcspResponse,
322        ffi::d2i_OCSP_RESPONSE
323    }
324}
325
326impl OcspResponseRef {
327    to_der! {
328        /// Serializes the response to its standard DER encoding.
329        #[corresponds(i2d_OCSP_RESPONSE)]
330        to_der,
331        ffi::i2d_OCSP_RESPONSE
332    }
333
334    /// Returns the status of the response.
335    #[corresponds(OCSP_response_status)]
336    pub fn status(&self) -> OcspResponseStatus {
337        unsafe { OcspResponseStatus(ffi::OCSP_response_status(self.as_ptr())) }
338    }
339
340    /// Returns the basic response.
341    ///
342    /// This will only succeed if `status()` returns `RESPONSE_STATUS_SUCCESSFUL`.
343    #[corresponds(OCSP_response_get1_basic)]
344    pub fn basic(&self) -> Result<OcspBasicResponse, ErrorStack> {
345        unsafe { cvt_p(ffi::OCSP_response_get1_basic(self.as_ptr())).map(OcspBasicResponse) }
346    }
347}
348
349foreign_type_and_impl_send_sync! {
350    type CType = ffi::OCSP_REQUEST;
351    fn drop = ffi::OCSP_REQUEST_free;
352
353    pub struct OcspRequest;
354    pub struct OcspRequestRef;
355}
356
357impl OcspRequest {
358    #[corresponds(OCSP_REQUEST_new)]
359    pub fn new() -> Result<OcspRequest, ErrorStack> {
360        unsafe {
361            ffi::init();
362
363            cvt_p(ffi::OCSP_REQUEST_new()).map(OcspRequest)
364        }
365    }
366
367    from_der! {
368        /// Deserializes a DER-encoded OCSP request.
369        #[corresponds(d2i_OCSP_REQUEST)]
370        from_der,
371        OcspRequest,
372        ffi::d2i_OCSP_REQUEST
373    }
374}
375
376impl OcspRequestRef {
377    to_der! {
378        /// Serializes the request to its standard DER encoding.
379        #[corresponds(i2d_OCSP_REQUEST)]
380        to_der,
381        ffi::i2d_OCSP_REQUEST
382    }
383
384    #[corresponds(OCSP_request_add0_id)]
385    pub fn add_id(&mut self, id: OcspCertId) -> Result<&mut OcspOneReqRef, ErrorStack> {
386        unsafe {
387            let ptr = cvt_p(ffi::OCSP_request_add0_id(self.as_ptr(), id.as_ptr()))?;
388            mem::forget(id);
389            Ok(OcspOneReqRef::from_ptr_mut(ptr))
390        }
391    }
392}
393
394foreign_type_and_impl_send_sync! {
395    type CType = ffi::OCSP_ONEREQ;
396    fn drop = ffi::OCSP_ONEREQ_free;
397
398    pub struct OcspOneReq;
399    pub struct OcspOneReqRef;
400}
401
402#[cfg(test)]
403mod tests {
404    use super::{
405        get_sentinel_max_time, OcspCertId, OcspCertStatus, OcspResponse, OcspResponseStatus,
406        OcspRevokedStatus,
407    };
408    use crate::hash::MessageDigest;
409    use crate::x509::X509;
410
411    // Test vectors: OCSP response with next_update=NULL and associated certificates
412    const OCSP_RESPONSE_NO_NEXTUPDATE: &[u8] =
413        include_bytes!("../test/ocsp_resp_no_nextupdate.der");
414    const OCSP_CA_CERT: &[u8] = include_bytes!("../test/ocsp_ca_cert.der");
415    const OCSP_SUBJECT_CERT: &[u8] = include_bytes!("../test/ocsp_subject_cert.der");
416    const OCSP_RESPONSE_REVOKED: &[u8] = include_bytes!("../test/ocsp_resp_revoked.der");
417
418    #[test]
419    fn test_ocsp_no_next_update() {
420        // Verify find_status correctly handles OCSP responses with next_update=NULL
421        let response = OcspResponse::from_der(OCSP_RESPONSE_NO_NEXTUPDATE).unwrap();
422        assert_eq!(response.status(), OcspResponseStatus::SUCCESSFUL);
423
424        let ca_cert = X509::from_der(OCSP_CA_CERT).unwrap();
425        let subject_cert = X509::from_der(OCSP_SUBJECT_CERT).unwrap();
426        let basic = response.basic().unwrap();
427
428        let cert_id =
429            OcspCertId::from_cert(MessageDigest::sha256(), &subject_cert, &ca_cert).unwrap();
430
431        let status = basic
432            .find_status(&cert_id)
433            .expect("find_status should find the status");
434
435        assert!(status.next_update().is_none());
436
437        #[allow(deprecated)]
438        let deprecated_next = status.next_update;
439        let sentinel = get_sentinel_max_time();
440        assert_eq!(format!("{}", deprecated_next), format!("{}", sentinel));
441
442        assert_eq!(status.status, OcspCertStatus::GOOD);
443    }
444
445    #[test]
446    fn test_ocsp_revoked() {
447        let response = OcspResponse::from_der(OCSP_RESPONSE_REVOKED).unwrap();
448        assert_eq!(response.status(), OcspResponseStatus::SUCCESSFUL);
449
450        let ca_cert = X509::from_der(OCSP_CA_CERT).unwrap();
451        let subject_cert = X509::from_der(OCSP_SUBJECT_CERT).unwrap();
452        let basic = response.basic().unwrap();
453
454        let cert_id =
455            OcspCertId::from_cert(MessageDigest::sha256(), &subject_cert, &ca_cert).unwrap();
456
457        let status = basic
458            .find_status(&cert_id)
459            .expect("find_status should find the status");
460
461        assert_eq!(status.status, OcspCertStatus::REVOKED);
462        assert_eq!(status.reason, OcspRevokedStatus::STATUS_SUPERSEDED);
463    }
464}