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