Skip to main content

native_ossl/
x509.rs

1//! X.509 certificate — reading, inspecting, and building.
2//!
3//! # Types
4//!
5//! | Type              | Owned / Borrowed | Description                          |
6//! |-------------------|-----------------|--------------------------------------|
7//! | [`X509`]          | Owned (Arc-like) | Certificate, Clone via `up_ref`      |
8//! | [`X509Name`]      | Borrowed `'cert` | Subject or issuer distinguished name |
9//! | [`X509NameEntry`] | Borrowed `'name` | One RDN entry (e.g. CN, O, C)       |
10//! | [`X509Extension`] | Borrowed `'cert` | One certificate extension            |
11//! | [`X509NameOwned`] | Owned            | Mutable name for certificate builder |
12//! | [`X509Builder`]   | Owned builder    | Constructs a new X.509 certificate   |
13
14use crate::bio::{MemBio, MemBioBuf};
15use crate::error::ErrorStack;
16use crate::obj::Oid;
17use native_ossl_sys as sys;
18use std::ffi::CStr;
19use std::marker::PhantomData;
20use std::sync::Arc;
21
22// ── BrokenDownTime — structured calendar time ────────────────────────────────
23
24/// A calendar date and time in UTC, as returned by [`X509::not_before_tm`] and
25/// [`X509::not_after_tm`].
26///
27/// All fields are in natural units (year is the full Gregorian year; month is
28/// 1–12; all time fields are 0-based).
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct BrokenDownTime {
31    /// Full Gregorian year (e.g. 2026).
32    pub year: i32,
33    /// Month, 1–12.
34    pub month: u8,
35    /// Day of the month, 1–31.
36    pub day: u8,
37    /// Hour, 0–23.
38    pub hour: u8,
39    /// Minute, 0–59.
40    pub minute: u8,
41    /// Second, 0–60 (60 is possible for leap seconds).
42    pub second: u8,
43}
44
45// ── SignatureInfo — certificate signature algorithm metadata ──────────────────
46
47/// Decoded signature algorithm metadata from an [`X509`] certificate.
48///
49/// Returned by [`X509::signature_info`].
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct SignatureInfo {
52    /// NID of the digest algorithm used in the signature.
53    ///
54    /// `0` (`NID_undef`) for algorithms that do not use a separate
55    /// pre-hash step, such as Ed25519 and ML-DSA.
56    pub md_nid: i32,
57    /// NID of the public-key algorithm used in the signature.
58    pub pk_nid: i32,
59    /// Estimated security strength in bits (e.g. 128 for AES-128-equivalent).
60    pub security_bits: i32,
61}
62
63// ── KeyUsage ──────────────────────────────────────────────────────────────────
64
65/// Bitmask of key usage bits from an X.509 [`keyUsage`][rfc5280] extension.
66///
67/// Returned by [`X509::key_usage`].  Combine constants with `|` or test
68/// individual bits with [`Self::contains`].
69///
70/// [rfc5280]: https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.3
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub struct KeyUsage(pub u32);
73
74impl KeyUsage {
75    /// `digitalSignature` — signing data and authentication.
76    pub const DIGITAL_SIGNATURE: Self = Self(0x0080);
77    /// `nonRepudiation` (`contentCommitment` in RFC 5280).
78    pub const NON_REPUDIATION: Self = Self(0x0040);
79    /// `keyEncipherment` — encrypting symmetric keys.
80    pub const KEY_ENCIPHERMENT: Self = Self(0x0020);
81    /// `dataEncipherment` — directly encrypting data.
82    pub const DATA_ENCIPHERMENT: Self = Self(0x0010);
83    /// `keyAgreement` — Diffie-Hellman key exchange.
84    pub const KEY_AGREEMENT: Self = Self(0x0008);
85    /// `keyCertSign` — signing certificates.
86    pub const KEY_CERT_SIGN: Self = Self(0x0004);
87    /// `cRLSign` — signing CRLs.
88    pub const CRL_SIGN: Self = Self(0x0002);
89    /// `encipherOnly` — only encrypt during key agreement.
90    pub const ENCIPHER_ONLY: Self = Self(0x0001);
91    /// `decipherOnly` — only decrypt during key agreement.
92    pub const DECIPHER_ONLY: Self = Self(0x8000);
93
94    /// Return `true` if all bits in `flag` are set in `self`.
95    #[must_use]
96    pub fn contains(self, flag: Self) -> bool {
97        self.0 & flag.0 == flag.0
98    }
99
100    /// Return the raw bitmask value.
101    #[must_use]
102    pub fn bits(self) -> u32 {
103        self.0
104    }
105}
106
107impl std::ops::BitOr for KeyUsage {
108    type Output = Self;
109    fn bitor(self, rhs: Self) -> Self {
110        Self(self.0 | rhs.0)
111    }
112}
113
114impl std::ops::BitAnd for KeyUsage {
115    type Output = Self;
116    fn bitand(self, rhs: Self) -> Self {
117        Self(self.0 & rhs.0)
118    }
119}
120
121// ── GeneralName ───────────────────────────────────────────────────────────────
122
123/// A decoded entry from an X.509 Subject Alternative Name (`subjectAltName`)
124/// extension.
125///
126/// Returned by [`X509::subject_alt_names`].  Variants correspond to the
127/// `GeneralName` choice types defined in RFC 5280 § 4.2.1.6.
128#[derive(Debug, Clone, PartialEq, Eq)]
129pub enum GeneralName {
130    /// `dNSName` — a DNS hostname (e.g. `"example.com"`).
131    Dns(String),
132    /// `rfc822Name` — an email address.
133    Email(String),
134    /// `uniformResourceIdentifier` — a URI.
135    Uri(String),
136    /// `iPAddress` — 4 bytes for IPv4, 16 bytes for IPv6.
137    IpAddress(Vec<u8>),
138    /// `registeredID` — an OID (used in PKIX for specific identifier types).
139    RegisteredId(Oid),
140    /// `otherName` — application-specific name with a type OID and DER-encoded value.
141    ///
142    /// `value` contains the DER encoding of the `ASN1_TYPE` value as returned
143    /// by OpenSSL.  Callers that need to decode the value must parse it
144    /// according to the schema identified by `type_id`.
145    OtherName {
146        /// The type OID identifying the value schema.
147        type_id: Oid,
148        /// DER-encoded value bytes.
149        value: Vec<u8>,
150    },
151}
152
153// ── X509 — owned certificate ──────────────────────────────────────────────────
154
155/// An X.509 certificate (`X509*`).
156///
157/// Cloneable via `EVP_X509_up_ref`; wrapping in `Arc<X509>` is safe.
158pub struct X509 {
159    ptr: *mut sys::X509,
160}
161
162// SAFETY: `X509*` is reference-counted.
163unsafe impl Send for X509 {}
164unsafe impl Sync for X509 {}
165
166impl Clone for X509 {
167    fn clone(&self) -> Self {
168        unsafe { sys::X509_up_ref(self.ptr) };
169        X509 { ptr: self.ptr }
170    }
171}
172
173impl Drop for X509 {
174    fn drop(&mut self) {
175        unsafe { sys::X509_free(self.ptr) };
176    }
177}
178
179impl X509 {
180    /// Construct from a raw, owned `X509*`.
181    ///
182    /// # Safety
183    ///
184    /// `ptr` must be a valid, non-null `X509*` whose ownership is being transferred.
185    pub(crate) unsafe fn from_ptr(ptr: *mut sys::X509) -> Self {
186        X509 { ptr }
187    }
188
189    /// Load a certificate from PEM bytes.
190    ///
191    /// # Errors
192    pub fn from_pem(pem: &[u8]) -> Result<Self, ErrorStack> {
193        let bio = MemBioBuf::new(pem)?;
194        let ptr = unsafe {
195            sys::PEM_read_bio_X509(
196                bio.as_ptr(),
197                std::ptr::null_mut(),
198                None,
199                std::ptr::null_mut(),
200            )
201        };
202        if ptr.is_null() {
203            return Err(ErrorStack::drain());
204        }
205        Ok(unsafe { Self::from_ptr(ptr) })
206    }
207
208    /// Load a certificate from PEM bytes, accepting a library context for API
209    /// symmetry with other `from_pem_in` methods.
210    ///
211    /// OpenSSL 3.5 does not expose a libctx-aware `PEM_read_bio_X509_ex`
212    /// variant, so this calls the standard `PEM_read_bio_X509`.  The `ctx`
213    /// parameter is accepted but unused.  Certificate parsing itself does not
214    /// require provider dispatch; provider-bound operations use the context
215    /// stored in the key extracted from the certificate.
216    ///
217    /// # Errors
218    pub fn from_pem_in(_ctx: &Arc<crate::lib_ctx::LibCtx>, pem: &[u8]) -> Result<Self, ErrorStack> {
219        Self::from_pem(pem)
220    }
221
222    /// Create a new, empty `X509` object bound to the given library context.
223    ///
224    /// Use this instead of the implicit-context `X509Builder::new` when you need
225    /// the certificate to be associated with a FIPS-isolated or otherwise
226    /// explicit `LibCtx`.  The `propq` (property query) argument is `NULL`,
227    /// meaning default provider properties are used.
228    ///
229    /// # Errors
230    ///
231    /// Returns `Err` if OpenSSL cannot allocate the certificate structure.
232    pub fn new_in(ctx: &Arc<crate::lib_ctx::LibCtx>) -> Result<Self, ErrorStack> {
233        // SAFETY:
234        // - ctx.as_ptr() is non-null (LibCtx constructor invariant)
235        // - propq is null → use default algorithm properties
236        let ptr = unsafe { sys::X509_new_ex(ctx.as_ptr(), std::ptr::null()) };
237        if ptr.is_null() {
238            return Err(ErrorStack::drain());
239        }
240        Ok(unsafe { Self::from_ptr(ptr) })
241    }
242
243    /// Load a certificate from DER bytes.
244    ///
245    /// Zero-copy: parses from the caller's slice without an intermediate buffer.
246    ///
247    /// # Errors
248    ///
249    /// Returns `Err` if the DER is malformed, or if `der.len()` exceeds `i64::MAX`.
250    pub fn from_der(der: &[u8]) -> Result<Self, ErrorStack> {
251        let mut der_ptr = der.as_ptr();
252        let len = i64::try_from(der.len()).map_err(|_| ErrorStack::drain())?;
253        let ptr =
254            unsafe { sys::d2i_X509(std::ptr::null_mut(), std::ptr::addr_of_mut!(der_ptr), len) };
255        if ptr.is_null() {
256            return Err(ErrorStack::drain());
257        }
258        Ok(unsafe { Self::from_ptr(ptr) })
259    }
260
261    /// Serialise to PEM.
262    ///
263    /// # Errors
264    pub fn to_pem(&self) -> Result<Vec<u8>, ErrorStack> {
265        let mut bio = MemBio::new()?;
266        crate::ossl_call!(sys::PEM_write_bio_X509(bio.as_ptr(), self.ptr))?;
267        Ok(bio.into_vec())
268    }
269
270    /// Serialise to DER.
271    ///
272    /// Zero-copy: writes into a freshly allocated `Vec<u8>` without going
273    /// through an OpenSSL-owned buffer.
274    ///
275    /// # Errors
276    pub fn to_der(&self) -> Result<Vec<u8>, ErrorStack> {
277        let len = unsafe { sys::i2d_X509(self.ptr, std::ptr::null_mut()) };
278        if len < 0 {
279            return Err(ErrorStack::drain());
280        }
281        let mut buf = vec![0u8; usize::try_from(len).unwrap_or(0)];
282        let mut out_ptr = buf.as_mut_ptr();
283        let written = unsafe { sys::i2d_X509(self.ptr, std::ptr::addr_of_mut!(out_ptr)) };
284        if written < 0 {
285            return Err(ErrorStack::drain());
286        }
287        buf.truncate(usize::try_from(written).unwrap_or(0));
288        Ok(buf)
289    }
290
291    /// Subject distinguished name (borrowed).
292    #[must_use]
293    pub fn subject_name(&self) -> X509Name<'_> {
294        // get0: does not increment ref count; valid while self is alive.
295        // OpenSSL 4.x made this return `*const`; cast is safe — we never
296        // mutate through the borrowed pointer.
297        let ptr = unsafe { sys::X509_get_subject_name(self.ptr) }.cast();
298        X509Name {
299            ptr,
300            _owner: PhantomData,
301        }
302    }
303
304    /// Issuer distinguished name (borrowed).
305    #[must_use]
306    pub fn issuer_name(&self) -> X509Name<'_> {
307        let ptr = unsafe { sys::X509_get_issuer_name(self.ptr) }.cast();
308        X509Name {
309            ptr,
310            _owner: PhantomData,
311        }
312    }
313
314    /// Serial number as a signed 64-bit integer.
315    ///
316    /// Returns `None` if the serial number is too large to fit in `i64`.
317    #[must_use]
318    pub fn serial_number(&self) -> Option<i64> {
319        let ai = unsafe { sys::X509_get0_serialNumber(self.ptr) };
320        if ai.is_null() {
321            return None;
322        }
323        let mut n: i64 = 0;
324        let rc = unsafe { sys::ASN1_INTEGER_get_int64(std::ptr::addr_of_mut!(n), ai) };
325        if rc == 1 {
326            Some(n)
327        } else {
328            None
329        }
330    }
331
332    /// Serial number as a big-endian byte slice.
333    ///
334    /// Complementary to [`Self::serial_number`] for serials that exceed `i64::MAX`.
335    /// The bytes are the raw content octets of the DER `INTEGER`, not including
336    /// the tag or length.
337    ///
338    /// Returns `None` if the serial field is absent.
339    #[must_use]
340    pub fn serial_number_bytes(&self) -> Option<Vec<u8>> {
341        // SAFETY:
342        // - self.ptr is non-null (constructor invariant)
343        // - X509_get_serialNumber returns a pointer embedded in the X509 object;
344        //   it is valid for as long as self is alive
345        // - &self ensures the certificate is not mutated while we read the field
346        let ai = unsafe { sys::X509_get_serialNumber(self.ptr) };
347        if ai.is_null() {
348            return None;
349        }
350        // SAFETY: ai is non-null (checked above) and lives for the duration of
351        // &self; ASN1_INTEGER is typedef'd to ASN1_STRING in OpenSSL, so the
352        // cast to *const ASN1_STRING is safe.
353        Some(unsafe { asn1_string_data(ai.cast()) }.to_vec())
354    }
355
356    /// Validity `notBefore` as a human-readable UTC string.
357    ///
358    /// The format is `"Mmm DD HH:MM:SS YYYY GMT"` (OpenSSL default).
359    /// Returns `None` if the field is absent or cannot be printed.
360    #[must_use]
361    pub fn not_before_str(&self) -> Option<String> {
362        let t = unsafe { sys::X509_get0_notBefore(self.ptr) };
363        asn1_time_to_str(t)
364    }
365
366    /// Validity `notAfter` as a human-readable UTC string.
367    ///
368    /// Returns `None` if the field is absent or cannot be printed.
369    #[must_use]
370    pub fn not_after_str(&self) -> Option<String> {
371        let t = unsafe { sys::X509_get0_notAfter(self.ptr) };
372        asn1_time_to_str(t)
373    }
374
375    /// Validity `notBefore` as a structured [`BrokenDownTime`] in UTC.
376    ///
377    /// Returns `None` if the field is absent or cannot be parsed.
378    #[must_use]
379    pub fn not_before_tm(&self) -> Option<BrokenDownTime> {
380        // SAFETY:
381        // - self.ptr is non-null (constructor invariant)
382        // - X509_get0_notBefore returns a pointer embedded in the X509; valid while &self lives
383        // - &self ensures no concurrent mutation
384        let t = unsafe { sys::X509_get0_notBefore(self.ptr) };
385        asn1_time_to_broken_down(t)
386    }
387
388    /// Validity `notAfter` as a structured [`BrokenDownTime`] in UTC.
389    ///
390    /// Returns `None` if the field is absent or cannot be parsed.
391    #[must_use]
392    pub fn not_after_tm(&self) -> Option<BrokenDownTime> {
393        // SAFETY: same as not_before_tm
394        let t = unsafe { sys::X509_get0_notAfter(self.ptr) };
395        asn1_time_to_broken_down(t)
396    }
397
398    /// Returns `true` if the current time is within `[notBefore, notAfter]`.
399    #[must_use]
400    pub fn is_valid_now(&self) -> bool {
401        let nb = unsafe { sys::X509_get0_notBefore(self.ptr) };
402        let na = unsafe { sys::X509_get0_notAfter(self.ptr) };
403        // X509_cmp_time(t, NULL) < 0 → t < now; > 0 → t > now.
404        unsafe {
405            sys::X509_cmp_time(nb, std::ptr::null_mut()) <= 0
406                && sys::X509_cmp_time(na, std::ptr::null_mut()) >= 0
407        }
408    }
409
410    /// Extract the public key (owned `Pkey<Public>`).
411    ///
412    /// Calls `X509_get_pubkey` — the returned key is independently reference-counted.
413    ///
414    /// # Errors
415    pub fn public_key(&self) -> Result<crate::pkey::Pkey<crate::pkey::Public>, ErrorStack> {
416        let ptr = unsafe { sys::X509_get_pubkey(self.ptr) };
417        if ptr.is_null() {
418            return Err(ErrorStack::drain());
419        }
420        Ok(unsafe { crate::pkey::Pkey::from_ptr(ptr) })
421    }
422
423    /// Check whether the certificate's public key uses the named algorithm.
424    ///
425    /// Uses `X509_get0_pubkey` — no reference-count increment.  Call
426    /// [`Self::public_key`] if you need an owned [`crate::pkey::Pkey`] handle.
427    ///
428    /// Returns `false` if the certificate has no public key or if the algorithm
429    /// name does not match.
430    #[must_use]
431    pub fn public_key_is_a(&self, alg: &CStr) -> bool {
432        // SAFETY:
433        // - self.ptr is non-null (constructor invariant)
434        // - X509_get0_pubkey returns a borrowed pointer valid while &self holds;
435        //   we do not store it and do not outlive this call
436        // - &self ensures no concurrent mutation of the certificate
437        let pkey = unsafe { sys::X509_get0_pubkey(self.ptr) };
438        if pkey.is_null() {
439            return false;
440        }
441        // SAFETY: pkey is non-null (checked above); alg.as_ptr() is valid for
442        // the duration of this call (CStr invariant)
443        unsafe { sys::EVP_PKEY_is_a(pkey, alg.as_ptr()) == 1 }
444    }
445
446    /// Bit size of the certificate's public key.
447    ///
448    /// Uses `X509_get0_pubkey` — no reference-count increment.
449    ///
450    /// Returns `None` if the certificate has no public key.
451    #[must_use]
452    pub fn public_key_bits(&self) -> Option<u32> {
453        // SAFETY:
454        // - self.ptr is non-null (constructor invariant)
455        // - X509_get0_pubkey returns a borrowed pointer valid while &self holds
456        // - &self ensures no concurrent mutation of the certificate
457        let pkey = unsafe { sys::X509_get0_pubkey(self.ptr) };
458        if pkey.is_null() {
459            return None;
460        }
461        // SAFETY: pkey is non-null (checked above)
462        u32::try_from(unsafe { sys::EVP_PKEY_get_bits(pkey) }).ok()
463    }
464
465    /// Inspect the signature algorithm used in this certificate.
466    ///
467    /// Calls `X509_get_signature_info` to decode the signature algorithm fields
468    /// embedded in the certificate's `signatureAlgorithm` and `signature`
469    /// structures.
470    ///
471    /// `md_nid` is `0` (`NID_undef`) for algorithms that have no separate
472    /// pre-hash step, such as Ed25519 and ML-DSA (post-quantum lattice signatures
473    /// defined in FIPS 204).  Always check for `0` before using `md_nid` as a
474    /// digest identifier.
475    ///
476    /// # Errors
477    ///
478    /// Returns `Err` if OpenSSL cannot decode the signature algorithm (e.g. the
479    /// certificate's signature field is absent or uses an unrecognised OID).
480    pub fn signature_info(&self) -> Result<SignatureInfo, ErrorStack> {
481        let mut md_nid: std::ffi::c_int = 0;
482        let mut pk_nid: std::ffi::c_int = 0;
483        let mut security_bits: std::ffi::c_int = 0;
484        // SAFETY:
485        // - self.ptr is non-null (constructor invariant)
486        // - md_nid, pk_nid, security_bits are local stack variables; we pass
487        //   their addresses which are valid for the duration of this call
488        // - flags is null_mut(): we do not need the X509_SIG_INFO_TLS /
489        //   X509_SIG_INFO_VALID flags; OpenSSL accepts NULL for any out-param
490        // - &self ensures the certificate is not mutated concurrently
491        let rc = unsafe {
492            sys::X509_get_signature_info(
493                self.ptr,
494                &raw mut md_nid,
495                &raw mut pk_nid,
496                &raw mut security_bits,
497                std::ptr::null_mut(),
498            )
499        };
500        if rc != 1 {
501            return Err(ErrorStack::drain());
502        }
503        Ok(SignatureInfo {
504            md_nid,
505            pk_nid,
506            security_bits,
507        })
508    }
509
510    /// Verify this certificate was signed by `key`.
511    ///
512    /// Returns `Ok(true)` if the signature is valid, `Ok(false)` if not, or
513    /// `Err` on a fatal error.
514    ///
515    /// # Errors
516    pub fn verify(&self, key: &crate::pkey::Pkey<crate::pkey::Public>) -> Result<bool, ErrorStack> {
517        match unsafe { sys::X509_verify(self.ptr, key.as_ptr()) } {
518            1 => Ok(true),
519            0 => Ok(false),
520            _ => Err(ErrorStack::drain()),
521        }
522    }
523
524    /// Returns `true` if the certificate is self-signed.
525    #[must_use]
526    pub fn is_self_signed(&self) -> bool {
527        // verify_signature=0 → only check name match, not signature itself.
528        unsafe { sys::X509_self_signed(self.ptr, 0) == 1 }
529    }
530
531    /// Number of extensions in this certificate.
532    #[must_use]
533    pub fn extension_count(&self) -> usize {
534        let n = unsafe { sys::X509_get_ext_count(self.ptr) };
535        usize::try_from(n).unwrap_or(0)
536    }
537
538    /// Access extension by index (0-based).
539    ///
540    /// Returns `None` if `idx` is out of range.
541    #[must_use]
542    pub fn extension(&self, idx: usize) -> Option<X509Extension<'_>> {
543        let idx_i32 = i32::try_from(idx).ok()?;
544        // OpenSSL 4.x returns *const; cast is safe — borrowed, never mutated.
545        let ptr = unsafe { sys::X509_get_ext(self.ptr, idx_i32) }.cast::<sys::X509_EXTENSION>();
546        if ptr.is_null() {
547            None
548        } else {
549            Some(X509Extension {
550                ptr,
551                _owner: PhantomData,
552            })
553        }
554    }
555
556    /// Find the first extension with the given NID.
557    ///
558    /// Returns `None` if no such extension exists.
559    #[must_use]
560    pub fn extension_by_nid(&self, nid: i32) -> Option<X509Extension<'_>> {
561        let idx = unsafe { sys::X509_get_ext_by_NID(self.ptr, nid, -1) };
562        if idx < 0 {
563            return None;
564        }
565        let ptr = unsafe { sys::X509_get_ext(self.ptr, idx) }.cast::<sys::X509_EXTENSION>();
566        if ptr.is_null() {
567            None
568        } else {
569            Some(X509Extension {
570                ptr,
571                _owner: PhantomData,
572            })
573        }
574    }
575
576    /// Return the DER-encoded value of the first extension with the given NID.
577    ///
578    /// Returns `None` if the extension is not present.  The returned byte slice
579    /// is borrowed from the certificate's internal storage — zero-copy, no
580    /// allocation — and is valid for `'self`'s lifetime.
581    ///
582    /// To iterate all extensions or access criticality flags, use
583    /// [`Self::extension_by_nid`] or [`Self::extension`] instead.
584    #[must_use]
585    pub fn extension_der(&self, nid: i32) -> Option<&[u8]> {
586        // SAFETY: self.ptr is non-null (constructor invariant)
587        let idx = unsafe { sys::X509_get_ext_by_NID(self.ptr, nid, -1) };
588        if idx < 0 {
589            return None;
590        }
591        // SAFETY: idx is a valid extension index returned by X509_get_ext_by_NID
592        let ext = unsafe { sys::X509_get_ext(self.ptr, idx) };
593        if ext.is_null() {
594            return None;
595        }
596        // SAFETY: ext is non-null; X509_EXTENSION_get_data returns data borrowed
597        // from ext, which itself is borrowed from self
598        let data = unsafe { sys::X509_EXTENSION_get_data(ext) };
599        if data.is_null() {
600            return Some(&[]);
601        }
602        // SAFETY: data is a valid ASN1_OCTET_STRING; cast to ASN1_STRING is safe
603        // because ASN1_OCTET_STRING is typedef'd to ASN1_STRING in OpenSSL
604        Some(unsafe { asn1_string_data(data.cast()) })
605    }
606
607    /// Key usage bits from the certificate's `keyUsage` extension.
608    ///
609    /// Returns `None` if the extension is absent.  When present, returns a
610    /// [`KeyUsage`] bitmask; test individual bits with [`KeyUsage::contains`].
611    ///
612    /// OpenSSL internally caches the parsed extension; this call is cheap.
613    #[must_use]
614    pub fn key_usage(&self) -> Option<KeyUsage> {
615        // SAFETY: self.ptr is non-null (constructor invariant).
616        // X509_get_key_usage returns UINT32_MAX when the keyUsage extension
617        // is absent, and the decoded bitmask otherwise.
618        let bits = unsafe { sys::X509_get_key_usage(self.ptr) };
619        if bits == u32::MAX {
620            None
621        } else {
622            Some(KeyUsage(bits))
623        }
624    }
625
626    /// OIDs from the certificate's `extendedKeyUsage` extension.
627    ///
628    /// Returns an empty `Vec` if the extension is absent.  Each [`Oid`] in the
629    /// returned list identifies one extended key purpose (e.g. id-kp-serverAuth,
630    /// id-kp-clientAuth, or a vendor-specific OID not in the standard NID table).
631    ///
632    /// # Errors
633    ///
634    /// Returns an [`ErrorStack`] if the extension is present but malformed, or
635    /// if `OBJ_dup` fails to allocate a copy of an OID.
636    #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
637    pub fn extended_key_usage_oids(&self) -> Result<Vec<Oid>, ErrorStack> {
638        let nid = unsafe { sys::OBJ_sn2nid(c"extendedKeyUsage".as_ptr()) };
639        // SAFETY: X509_get_ext_d2i decodes and returns an owned STACK_OF(ASN1_OBJECT)*.
640        // NULL means the extension is absent.
641        let stack_ptr = unsafe {
642            sys::X509_get_ext_d2i(self.ptr, nid, std::ptr::null_mut(), std::ptr::null_mut())
643        };
644        if stack_ptr.is_null() {
645            return Ok(Vec::new());
646        }
647        let stack = stack_ptr.cast::<sys::OPENSSL_STACK>();
648        let n = usize::try_from(unsafe { sys::OPENSSL_sk_num(stack) }).unwrap_or(0);
649        let mut oids = Vec::with_capacity(n);
650        for i in 0..n {
651            let raw = unsafe { sys::OPENSSL_sk_value(stack, i as i32) };
652            if raw.is_null() {
653                continue;
654            }
655            // Duplicate so the Oid has its own allocation independent of the stack.
656            let dup = unsafe { sys::OBJ_dup(raw.cast::<sys::ASN1_OBJECT>()) };
657            if dup.is_null() {
658                // Free the stack (including originals) before propagating the error.
659                // SAFETY: stack was returned by X509_get_ext_d2i (owned); elements
660                // are ASN1_OBJECT* freed by ASN1_OBJECT_free.
661                unsafe {
662                    sys::OPENSSL_sk_pop_free(stack, Some(free_asn1_object));
663                }
664                return Err(ErrorStack::drain());
665            }
666            // SAFETY: dup is non-null and heap-allocated; Oid::from_ptr takes ownership.
667            oids.push(unsafe { Oid::from_ptr(dup) });
668        }
669        // Free the stack and its original elements (we hold dup'd copies).
670        unsafe { sys::OPENSSL_sk_pop_free(stack, Some(free_asn1_object)) };
671        Ok(oids)
672    }
673
674    /// Decoded Subject Alternative Name entries from the certificate.
675    ///
676    /// Returns an empty `Vec` if the `subjectAltName` extension is absent.
677    /// Variants [`GeneralName::Dns`], [`GeneralName::Email`],
678    /// [`GeneralName::Uri`], [`GeneralName::IpAddress`],
679    /// [`GeneralName::RegisteredId`], and [`GeneralName::OtherName`] are
680    /// decoded; `DirectoryName`, `X400Address`, and `EdiPartyName` entries are
681    /// silently skipped (uncommon in practice).
682    ///
683    /// # Errors
684    ///
685    /// Returns an [`ErrorStack`] if the extension is present but malformed.
686    #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
687    pub fn subject_alt_names(&self) -> Result<Vec<GeneralName>, ErrorStack> {
688        let nid = unsafe { sys::OBJ_sn2nid(c"subjectAltName".as_ptr()) };
689        // SAFETY: X509_get_ext_d2i decodes and returns an owned STACK_OF(GENERAL_NAME)*.
690        let stack_ptr = unsafe {
691            sys::X509_get_ext_d2i(self.ptr, nid, std::ptr::null_mut(), std::ptr::null_mut())
692        };
693        if stack_ptr.is_null() {
694            return Ok(Vec::new());
695        }
696        let stack = stack_ptr.cast::<sys::OPENSSL_STACK>();
697        let n = usize::try_from(unsafe { sys::OPENSSL_sk_num(stack) }).unwrap_or(0);
698        let mut names = Vec::with_capacity(n);
699        for i in 0..n {
700            let raw = unsafe { sys::OPENSSL_sk_value(stack, i as i32) };
701            if raw.is_null() {
702                continue;
703            }
704            let gn = raw.cast::<sys::GENERAL_NAME>();
705            // SAFETY: gn is a valid, non-null GENERAL_NAME* in the owned stack.
706            let gn_ref = unsafe { &*gn };
707            // GEN_* discriminants from <openssl/x509v3.h>
708            match gn_ref.type_ {
709                // GEN_EMAIL = 1
710                1 => {
711                    let s = unsafe { asn1_string_data(gn_ref.d.rfc822Name.cast()) };
712                    names.push(GeneralName::Email(String::from_utf8_lossy(s).into_owned()));
713                }
714                // GEN_DNS = 2
715                2 => {
716                    let s = unsafe { asn1_string_data(gn_ref.d.dNSName.cast()) };
717                    names.push(GeneralName::Dns(String::from_utf8_lossy(s).into_owned()));
718                }
719                // GEN_URI = 6
720                6 => {
721                    let s = unsafe { asn1_string_data(gn_ref.d.uniformResourceIdentifier.cast()) };
722                    names.push(GeneralName::Uri(String::from_utf8_lossy(s).into_owned()));
723                }
724                // GEN_IPADD = 7
725                7 => {
726                    let b = unsafe { asn1_string_data(gn_ref.d.iPAddress.cast()) };
727                    names.push(GeneralName::IpAddress(b.to_vec()));
728                }
729                // GEN_RID = 8
730                8 => {
731                    let rid = unsafe { gn_ref.d.registeredID };
732                    if !rid.is_null() {
733                        let dup = unsafe { sys::OBJ_dup(rid) };
734                        if !dup.is_null() {
735                            names.push(GeneralName::RegisteredId(unsafe { Oid::from_ptr(dup) }));
736                        }
737                    }
738                }
739                // GEN_OTHERNAME = 0
740                0 => {
741                    let other = unsafe { gn_ref.d.otherName };
742                    if !other.is_null() {
743                        let other_ref = unsafe { &*other };
744                        if !other_ref.type_id.is_null() {
745                            let oid_dup = unsafe { sys::OBJ_dup(other_ref.type_id) };
746                            if !oid_dup.is_null() {
747                                let value = unsafe { i2d_asn1_type_to_vec(other_ref.value) };
748                                names.push(GeneralName::OtherName {
749                                    type_id: unsafe { Oid::from_ptr(oid_dup) },
750                                    value,
751                                });
752                            }
753                        }
754                    }
755                }
756                // GEN_DIRNAME, GEN_X400, GEN_EDIPARTY — uncommon, skip
757                _ => {}
758            }
759        }
760        // Free the stack and all GENERAL_NAME elements.
761        unsafe { sys::OPENSSL_sk_pop_free(stack, Some(free_general_name)) };
762        Ok(names)
763    }
764
765    /// Raw `X509*` pointer valid for the lifetime of `self`.
766    #[must_use]
767    #[allow(dead_code)] // used by ssl module added in the next phase
768    pub(crate) fn as_ptr(&self) -> *mut sys::X509 {
769        self.ptr
770    }
771}
772
773// ── X509Name — borrowed distinguished name ────────────────────────────────────
774
775/// A borrowed distinguished name (`X509_NAME*`) tied to its owning `X509`.
776pub struct X509Name<'cert> {
777    ptr: *mut sys::X509_NAME,
778    _owner: PhantomData<&'cert X509>,
779}
780
781impl X509Name<'_> {
782    /// Number of entries (RDN components) in the name.
783    #[must_use]
784    pub fn entry_count(&self) -> usize {
785        usize::try_from(unsafe { sys::X509_NAME_entry_count(self.ptr) }).unwrap_or(0)
786    }
787
788    /// Access an entry by index (0-based).
789    #[must_use]
790    pub fn entry(&self, idx: usize) -> Option<X509NameEntry<'_>> {
791        let idx_i32 = i32::try_from(idx).ok()?;
792        // OpenSSL 4.x returns *const; cast is safe — borrowed, never mutated.
793        let ptr =
794            unsafe { sys::X509_NAME_get_entry(self.ptr, idx_i32) }.cast::<sys::X509_NAME_ENTRY>();
795        if ptr.is_null() {
796            None
797        } else {
798            Some(X509NameEntry {
799                ptr,
800                _owner: PhantomData,
801            })
802        }
803    }
804
805    /// Return a one-line string representation of this distinguished name.
806    ///
807    /// Produces the legacy `/CN=.../O=.../C=...` slash-separated format via
808    /// `X509_NAME_oneline`.  Useful for logging and debugging, but **not
809    /// recommended** for structured access — use [`Self::entry`] or
810    /// [`Self::to_string`] (which honours RFC 2253 / RFC 4514 flags) instead.
811    ///
812    /// Returns `None` if OpenSSL fails to allocate the string.
813    #[must_use]
814    pub fn to_oneline(&self) -> Option<String> {
815        // SAFETY:
816        // - self.ptr is non-null (constructor invariant)
817        // - passing NULL + 0 causes OpenSSL to heap-allocate the result; we
818        //   take ownership and must free it with OPENSSL_free
819        let ptr = unsafe { sys::X509_NAME_oneline(self.ptr, std::ptr::null_mut(), 0) };
820        if ptr.is_null() {
821            return None;
822        }
823        // SAFETY: ptr is a valid NUL-terminated C string allocated by OpenSSL
824        let s = unsafe { std::ffi::CStr::from_ptr(ptr) }
825            .to_string_lossy()
826            .into_owned();
827        // SAFETY: ptr was allocated by OpenSSL; CRYPTO_free is the underlying
828        // implementation of the OPENSSL_free macro (OPENSSL_free is a C macro
829        // and cannot be bound directly — CRYPTO_free is the actual function)
830        unsafe { sys::CRYPTO_free(ptr.cast(), c"x509.rs".as_ptr(), 0) };
831        Some(s)
832    }
833
834    /// Format the entire name as a single-line string.
835    ///
836    /// Uses `X509_NAME_print_ex` with `XN_FLAG_COMPAT` (traditional
837    /// `/CN=.../O=.../` format).  Returns `None` on error.
838    #[must_use]
839    pub fn to_string(&self) -> Option<String> {
840        let mut bio = MemBio::new().ok()?;
841        // flags = 0 → XN_FLAG_COMPAT (old /CN=…/O=… format).
842        let n = unsafe { sys::X509_NAME_print_ex(bio.as_ptr(), self.ptr, 0, 0) };
843        if n < 0 {
844            return None;
845        }
846        String::from_utf8(bio.into_vec()).ok()
847    }
848}
849
850// ── X509NameEntry — one RDN component ────────────────────────────────────────
851
852/// A borrowed entry within an [`X509Name`].
853pub struct X509NameEntry<'name> {
854    ptr: *mut sys::X509_NAME_ENTRY,
855    _owner: PhantomData<&'name ()>,
856}
857
858impl X509NameEntry<'_> {
859    /// NID of this field (e.g. `NID_commonName = 13`).
860    #[must_use]
861    pub fn nid(&self) -> i32 {
862        let obj = unsafe { sys::X509_NAME_ENTRY_get_object(self.ptr) };
863        unsafe { sys::OBJ_obj2nid(obj) }
864    }
865
866    /// Raw DER-encoded value bytes of this entry.
867    ///
868    /// The slice is valid as long as the owning certificate is alive.
869    #[must_use]
870    pub fn data(&self) -> &[u8] {
871        let asn1 = unsafe { sys::X509_NAME_ENTRY_get_data(self.ptr) };
872        if asn1.is_null() {
873            return &[];
874        }
875        // SAFETY: asn1 is valid for 'name (guaranteed by self._owner PhantomData).
876        unsafe { asn1_string_data(asn1) }
877    }
878}
879
880// ── X509Extension — borrowed extension ───────────────────────────────────────
881
882/// A borrowed extension within an [`X509`] certificate.
883pub struct X509Extension<'cert> {
884    ptr: *mut sys::X509_EXTENSION,
885    _owner: PhantomData<&'cert X509>,
886}
887
888impl X509Extension<'_> {
889    /// NID of this extension (e.g. `NID_subject_key_identifier`).
890    #[must_use]
891    pub fn nid(&self) -> i32 {
892        let obj = unsafe { sys::X509_EXTENSION_get_object(self.ptr) };
893        unsafe { sys::OBJ_obj2nid(obj) }
894    }
895
896    /// Returns `true` if this extension is marked critical.
897    #[must_use]
898    pub fn is_critical(&self) -> bool {
899        unsafe { sys::X509_EXTENSION_get_critical(self.ptr) == 1 }
900    }
901
902    /// Raw DER-encoded value bytes.
903    ///
904    /// The slice is valid as long as the owning certificate is alive.
905    #[must_use]
906    pub fn data(&self) -> &[u8] {
907        let asn1 = unsafe { sys::X509_EXTENSION_get_data(self.ptr) };
908        if asn1.is_null() {
909            return &[];
910        }
911        // SAFETY: asn1 is valid for 'cert (guaranteed by self._owner PhantomData).
912        // ASN1_OCTET_STRING is typedef'd to ASN1_STRING — cast is safe.
913        unsafe { asn1_string_data(asn1.cast()) }
914    }
915}
916
917// ── X509NameOwned — mutable name for the builder ─────────────────────────────
918
919/// An owned, mutable distinguished name (`X509_NAME*`).
920///
921/// Pass to [`X509Builder::set_subject_name`] / [`X509Builder::set_issuer_name`].
922pub struct X509NameOwned {
923    ptr: *mut sys::X509_NAME,
924}
925
926impl X509NameOwned {
927    /// Create an empty distinguished name.
928    ///
929    /// # Errors
930    pub fn new() -> Result<Self, ErrorStack> {
931        let ptr = unsafe { sys::X509_NAME_new() };
932        if ptr.is_null() {
933            return Err(ErrorStack::drain());
934        }
935        Ok(X509NameOwned { ptr })
936    }
937
938    /// Append a field entry by short name (e.g. `c"CN"`, `c"O"`, `c"C"`).
939    ///
940    /// `value` is the UTF-8 field value.
941    ///
942    /// # Panics
943    ///
944    /// # Errors
945    ///
946    /// Returns `Err` if the field cannot be added, or if `value.len()` exceeds `i32::MAX`.
947    pub fn add_entry_by_txt(&mut self, field: &CStr, value: &[u8]) -> Result<(), ErrorStack> {
948        let len = i32::try_from(value.len()).map_err(|_| ErrorStack::drain())?;
949        // MBSTRING_UTF8 = 0x1000 → encode value as UTF-8.
950        let rc = unsafe {
951            sys::X509_NAME_add_entry_by_txt(
952                self.ptr,
953                field.as_ptr(),
954                0x1000, // MBSTRING_UTF8
955                value.as_ptr(),
956                len,
957                -1, // append
958                0,
959            )
960        };
961        if rc != 1 {
962            return Err(ErrorStack::drain());
963        }
964        Ok(())
965    }
966}
967
968impl Drop for X509NameOwned {
969    fn drop(&mut self) {
970        unsafe { sys::X509_NAME_free(self.ptr) };
971    }
972}
973
974// ── X509Builder — certificate builder ────────────────────────────────────────
975
976/// Builder for a new X.509 certificate.
977///
978/// ```ignore
979/// let mut name = X509NameOwned::new()?;
980/// name.add_entry_by_txt(c"CN", b"example.com")?;
981///
982/// let cert = X509Builder::new()?
983///     .set_version(2)?                    // X.509v3
984///     .set_serial_number(1)?
985///     .set_not_before_offset(0)?          // valid from now
986///     .set_not_after_offset(365 * 86400)? // valid for 1 year
987///     .set_subject_name(&name)?
988///     .set_issuer_name(&name)?            // self-signed
989///     .set_public_key(&pub_key)?
990///     .sign(&priv_key, None)?             // None → no digest (Ed25519)
991///     .build();
992/// ```
993pub struct X509Builder {
994    ptr: *mut sys::X509,
995}
996
997impl X509Builder {
998    /// Allocate a new, empty `X509` structure.
999    ///
1000    /// # Errors
1001    pub fn new() -> Result<Self, ErrorStack> {
1002        let ptr = unsafe { sys::X509_new() };
1003        if ptr.is_null() {
1004            return Err(ErrorStack::drain());
1005        }
1006        Ok(X509Builder { ptr })
1007    }
1008
1009    /// Set the X.509 version (0 = v1, 1 = v2, 2 = v3).
1010    ///
1011    /// # Errors
1012    pub fn set_version(self, version: i64) -> Result<Self, ErrorStack> {
1013        crate::ossl_call!(sys::X509_set_version(self.ptr, version))?;
1014        Ok(self)
1015    }
1016
1017    /// Set the serial number.
1018    ///
1019    /// # Errors
1020    pub fn set_serial_number(self, n: i64) -> Result<Self, ErrorStack> {
1021        let ai = unsafe { sys::ASN1_INTEGER_new() };
1022        if ai.is_null() {
1023            return Err(ErrorStack::drain());
1024        }
1025        crate::ossl_call!(sys::ASN1_INTEGER_set_int64(ai, n)).map_err(|e| {
1026            unsafe { sys::ASN1_INTEGER_free(ai) };
1027            e
1028        })?;
1029        let rc = unsafe { sys::X509_set_serialNumber(self.ptr, ai) };
1030        unsafe { sys::ASN1_INTEGER_free(ai) };
1031        if rc != 1 {
1032            return Err(ErrorStack::drain());
1033        }
1034        Ok(self)
1035    }
1036
1037    /// Set `notBefore` to `now + offset_secs`.
1038    ///
1039    /// # Errors
1040    pub fn set_not_before_offset(self, offset_secs: i64) -> Result<Self, ErrorStack> {
1041        let t = unsafe { sys::X509_getm_notBefore(self.ptr) };
1042        if unsafe { sys::X509_gmtime_adj(t, offset_secs) }.is_null() {
1043            return Err(ErrorStack::drain());
1044        }
1045        Ok(self)
1046    }
1047
1048    /// Set `notAfter` to `now + offset_secs`.
1049    ///
1050    /// # Errors
1051    pub fn set_not_after_offset(self, offset_secs: i64) -> Result<Self, ErrorStack> {
1052        let t = unsafe { sys::X509_getm_notAfter(self.ptr) };
1053        if unsafe { sys::X509_gmtime_adj(t, offset_secs) }.is_null() {
1054            return Err(ErrorStack::drain());
1055        }
1056        Ok(self)
1057    }
1058
1059    /// Set the subject distinguished name.
1060    ///
1061    /// # Errors
1062    pub fn set_subject_name(self, name: &X509NameOwned) -> Result<Self, ErrorStack> {
1063        crate::ossl_call!(sys::X509_set_subject_name(self.ptr, name.ptr))?;
1064        Ok(self)
1065    }
1066
1067    /// Set the issuer distinguished name.
1068    ///
1069    /// # Errors
1070    pub fn set_issuer_name(self, name: &X509NameOwned) -> Result<Self, ErrorStack> {
1071        crate::ossl_call!(sys::X509_set_issuer_name(self.ptr, name.ptr))?;
1072        Ok(self)
1073    }
1074
1075    /// Set the public key.
1076    ///
1077    /// # Errors
1078    pub fn set_public_key<T: crate::pkey::HasPublic>(
1079        self,
1080        key: &crate::pkey::Pkey<T>,
1081    ) -> Result<Self, ErrorStack> {
1082        crate::ossl_call!(sys::X509_set_pubkey(self.ptr, key.as_ptr()))?;
1083        Ok(self)
1084    }
1085
1086    /// Sign the certificate.
1087    ///
1088    /// Pass `digest = None` for one-shot algorithms such as Ed25519.
1089    /// For ECDSA or RSA, pass the appropriate digest (e.g. SHA-256).
1090    ///
1091    /// # Errors
1092    pub fn sign(
1093        self,
1094        key: &crate::pkey::Pkey<crate::pkey::Private>,
1095        digest: Option<&crate::digest::DigestAlg>,
1096    ) -> Result<Self, ErrorStack> {
1097        let md_ptr = digest.map_or(std::ptr::null(), crate::digest::DigestAlg::as_ptr);
1098        // X509_sign returns the signature length (> 0) on success.
1099        let rc = unsafe { sys::X509_sign(self.ptr, key.as_ptr(), md_ptr) };
1100        if rc <= 0 {
1101            return Err(ErrorStack::drain());
1102        }
1103        Ok(self)
1104    }
1105
1106    /// Finalise and return the certificate.
1107    #[must_use]
1108    pub fn build(self) -> X509 {
1109        let ptr = self.ptr;
1110        std::mem::forget(self);
1111        X509 { ptr }
1112    }
1113}
1114
1115impl Drop for X509Builder {
1116    fn drop(&mut self) {
1117        unsafe { sys::X509_free(self.ptr) };
1118    }
1119}
1120
1121// ── X509Store — trust store ────────────────────────────────────────────────────
1122
1123/// An OpenSSL certificate trust store (`X509_STORE*`).
1124///
1125/// Cloneable via `X509_STORE_up_ref`; wrapping in `Arc<X509Store>` is safe.
1126pub struct X509Store {
1127    ptr: *mut sys::X509_STORE,
1128}
1129
1130unsafe impl Send for X509Store {}
1131unsafe impl Sync for X509Store {}
1132
1133impl Clone for X509Store {
1134    fn clone(&self) -> Self {
1135        unsafe { sys::X509_STORE_up_ref(self.ptr) };
1136        X509Store { ptr: self.ptr }
1137    }
1138}
1139
1140impl Drop for X509Store {
1141    fn drop(&mut self) {
1142        unsafe { sys::X509_STORE_free(self.ptr) };
1143    }
1144}
1145
1146impl X509Store {
1147    /// Create an empty trust store.
1148    ///
1149    /// # Errors
1150    pub fn new() -> Result<Self, ErrorStack> {
1151        let ptr = unsafe { sys::X509_STORE_new() };
1152        if ptr.is_null() {
1153            return Err(ErrorStack::drain());
1154        }
1155        Ok(X509Store { ptr })
1156    }
1157
1158    /// Add a trusted certificate to the store.
1159    ///
1160    /// The certificate's reference count is incremented internally.
1161    ///
1162    /// # Errors
1163    pub fn add_cert(&mut self, cert: &X509) -> Result<(), ErrorStack> {
1164        let rc = unsafe { sys::X509_STORE_add_cert(self.ptr, cert.ptr) };
1165        if rc != 1 {
1166            return Err(ErrorStack::drain());
1167        }
1168        Ok(())
1169    }
1170
1171    /// Add a CRL to the store.
1172    ///
1173    /// # Errors
1174    pub fn add_crl(&mut self, crl: &X509Crl) -> Result<(), ErrorStack> {
1175        let rc = unsafe { sys::X509_STORE_add_crl(self.ptr, crl.ptr) };
1176        if rc != 1 {
1177            return Err(ErrorStack::drain());
1178        }
1179        Ok(())
1180    }
1181
1182    /// Set verification flags (e.g. `X509_V_FLAG_CRL_CHECK`).
1183    ///
1184    /// # Errors
1185    pub fn set_flags(&mut self, flags: u64) -> Result<(), ErrorStack> {
1186        let rc = unsafe { sys::X509_STORE_set_flags(self.ptr, flags) };
1187        if rc != 1 {
1188            return Err(ErrorStack::drain());
1189        }
1190        Ok(())
1191    }
1192
1193    /// Return the raw `X509_STORE*` pointer.
1194    #[must_use]
1195    pub(crate) fn as_ptr(&self) -> *mut sys::X509_STORE {
1196        self.ptr
1197    }
1198}
1199
1200// ── X509StoreCtx — verification context ──────────────────────────────────────
1201
1202/// A chain-verification context (`X509_STORE_CTX*`).
1203///
1204/// Create with [`X509StoreCtx::new`], initialise with [`X509StoreCtx::init`],
1205/// then call [`X509StoreCtx::verify`].
1206pub struct X509StoreCtx {
1207    ptr: *mut sys::X509_STORE_CTX,
1208}
1209
1210impl Drop for X509StoreCtx {
1211    fn drop(&mut self) {
1212        unsafe { sys::X509_STORE_CTX_free(self.ptr) };
1213    }
1214}
1215
1216unsafe impl Send for X509StoreCtx {}
1217
1218impl X509StoreCtx {
1219    /// Allocate a new, uninitialised verification context.
1220    ///
1221    /// # Errors
1222    pub fn new() -> Result<Self, ErrorStack> {
1223        let ptr = unsafe { sys::X509_STORE_CTX_new() };
1224        if ptr.is_null() {
1225            return Err(ErrorStack::drain());
1226        }
1227        Ok(X509StoreCtx { ptr })
1228    }
1229
1230    /// Initialise the context for verifying `cert` against `store`.
1231    ///
1232    /// Call this before [`Self::verify`].
1233    ///
1234    /// # Errors
1235    pub fn init(&mut self, store: &X509Store, cert: &X509) -> Result<(), ErrorStack> {
1236        let rc = unsafe {
1237            sys::X509_STORE_CTX_init(self.ptr, store.ptr, cert.ptr, std::ptr::null_mut())
1238        };
1239        if rc != 1 {
1240            return Err(ErrorStack::drain());
1241        }
1242        Ok(())
1243    }
1244
1245    /// Verify the certificate chain.
1246    ///
1247    /// Returns `Ok(true)` if the chain is valid, `Ok(false)` if not (call
1248    /// [`Self::error`] to retrieve the error code), or `Err` on a fatal error.
1249    ///
1250    /// # Errors
1251    pub fn verify(&mut self) -> Result<bool, ErrorStack> {
1252        match unsafe { sys::X509_verify_cert(self.ptr) } {
1253            1 => Ok(true),
1254            0 => Ok(false),
1255            _ => Err(ErrorStack::drain()),
1256        }
1257    }
1258
1259    /// OpenSSL verification error code after a failed [`Self::verify`].
1260    ///
1261    /// Returns 0 (`X509_V_OK`) if no error occurred.  See `<openssl/x509_vfy.h>`
1262    /// for the full list of `X509_V_ERR_*` constants.
1263    #[must_use]
1264    pub fn error(&self) -> i32 {
1265        unsafe { sys::X509_STORE_CTX_get_error(self.ptr) }
1266    }
1267
1268    /// Collect the verified chain into a `Vec<X509>`.
1269    ///
1270    /// Only meaningful after a successful [`Self::verify`].  Returns an empty
1271    /// `Vec` if the chain is not available.
1272    #[must_use]
1273    // i < n where n came from OPENSSL_sk_num (i32), so the i as i32 cast is safe.
1274    #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
1275    pub fn chain(&self) -> Vec<X509> {
1276        let stack = unsafe { sys::X509_STORE_CTX_get0_chain(self.ptr) };
1277        if stack.is_null() {
1278            return Vec::new();
1279        }
1280        let n = unsafe { sys::OPENSSL_sk_num(stack.cast::<sys::OPENSSL_STACK>()) };
1281        let n = usize::try_from(n).unwrap_or(0);
1282        let mut out = Vec::with_capacity(n);
1283        for i in 0..n {
1284            let raw =
1285                unsafe { sys::OPENSSL_sk_value(stack.cast::<sys::OPENSSL_STACK>(), i as i32) };
1286            if raw.is_null() {
1287                continue;
1288            }
1289            // up_ref so each X509 in the Vec has its own reference.
1290            let cert_ptr = raw.cast::<sys::X509>();
1291            unsafe { sys::X509_up_ref(cert_ptr) };
1292            out.push(X509 { ptr: cert_ptr });
1293        }
1294        out
1295    }
1296}
1297
1298// ── X509Crl — certificate revocation list ─────────────────────────────────────
1299
1300/// An X.509 certificate revocation list (`X509_CRL*`).
1301///
1302/// Cloneable via `X509_CRL_up_ref`.
1303pub struct X509Crl {
1304    ptr: *mut sys::X509_CRL,
1305}
1306
1307unsafe impl Send for X509Crl {}
1308unsafe impl Sync for X509Crl {}
1309
1310impl Clone for X509Crl {
1311    fn clone(&self) -> Self {
1312        unsafe { sys::X509_CRL_up_ref(self.ptr) };
1313        X509Crl { ptr: self.ptr }
1314    }
1315}
1316
1317impl Drop for X509Crl {
1318    fn drop(&mut self) {
1319        unsafe { sys::X509_CRL_free(self.ptr) };
1320    }
1321}
1322
1323impl X509Crl {
1324    /// Construct from a raw, owned `X509_CRL*`.
1325    ///
1326    /// # Safety
1327    ///
1328    /// `ptr` must be a valid, non-null `X509_CRL*` whose ownership is transferred.
1329    pub(crate) unsafe fn from_ptr(ptr: *mut sys::X509_CRL) -> Self {
1330        X509Crl { ptr }
1331    }
1332
1333    /// Allocate a new, empty `X509_CRL` structure.
1334    ///
1335    /// The returned CRL has no fields set. Use this when constructing a CRL
1336    /// programmatically before populating its fields via the OpenSSL API.
1337    ///
1338    /// # Errors
1339    ///
1340    /// Returns `Err` if OpenSSL cannot allocate the structure.
1341    pub fn new() -> Result<Self, ErrorStack> {
1342        // SAFETY:
1343        // - X509_CRL_new() takes no arguments and returns a new heap allocation
1344        // - the returned pointer is non-null on success; null on allocation failure
1345        // - ownership is fully transferred to this X509Crl value
1346        let ptr = unsafe { sys::X509_CRL_new() };
1347        if ptr.is_null() {
1348            return Err(ErrorStack::drain());
1349        }
1350        Ok(unsafe { Self::from_ptr(ptr) })
1351    }
1352
1353    /// Allocate a new, empty `X509_CRL` structure bound to a library context.
1354    ///
1355    /// Equivalent to `X509_CRL_new_ex(ctx, NULL)`. The CRL will use providers
1356    /// from `ctx` for any cryptographic operations performed on it.
1357    ///
1358    /// # Errors
1359    ///
1360    /// Returns `Err` if OpenSSL cannot allocate the structure.
1361    pub fn new_in(ctx: &std::sync::Arc<crate::lib_ctx::LibCtx>) -> Result<Self, ErrorStack> {
1362        // SAFETY:
1363        // - ctx.as_ptr() is non-null (LibCtx invariant)
1364        // - propq is NULL, which is valid (no property query)
1365        // - the returned pointer is non-null on success; null on allocation failure
1366        // - ownership is fully transferred to this X509Crl value
1367        // - ctx borrow covers the duration of this call; no aliasing of the OSSL_LIB_CTX*
1368        let ptr = unsafe { sys::X509_CRL_new_ex(ctx.as_ptr(), std::ptr::null()) };
1369        if ptr.is_null() {
1370            return Err(ErrorStack::drain());
1371        }
1372        Ok(unsafe { Self::from_ptr(ptr) })
1373    }
1374
1375    /// Load a CRL from PEM bytes.
1376    ///
1377    /// # Errors
1378    pub fn from_pem(pem: &[u8]) -> Result<Self, ErrorStack> {
1379        let bio = MemBioBuf::new(pem)?;
1380        let ptr = unsafe {
1381            sys::PEM_read_bio_X509_CRL(
1382                bio.as_ptr(),
1383                std::ptr::null_mut(),
1384                None,
1385                std::ptr::null_mut(),
1386            )
1387        };
1388        if ptr.is_null() {
1389            return Err(ErrorStack::drain());
1390        }
1391        Ok(unsafe { Self::from_ptr(ptr) })
1392    }
1393
1394    /// Load a CRL from DER bytes.
1395    ///
1396    /// # Errors
1397    pub fn from_der(der: &[u8]) -> Result<Self, ErrorStack> {
1398        let bio = MemBioBuf::new(der)?;
1399        let ptr = unsafe { sys::d2i_X509_CRL_bio(bio.as_ptr(), std::ptr::null_mut()) };
1400        if ptr.is_null() {
1401            return Err(ErrorStack::drain());
1402        }
1403        Ok(unsafe { Self::from_ptr(ptr) })
1404    }
1405
1406    /// Serialise the CRL to PEM.
1407    ///
1408    /// # Errors
1409    pub fn to_pem(&self) -> Result<Vec<u8>, ErrorStack> {
1410        let mut bio = MemBio::new()?;
1411        let rc = unsafe { sys::PEM_write_bio_X509_CRL(bio.as_ptr(), self.ptr) };
1412        if rc != 1 {
1413            return Err(ErrorStack::drain());
1414        }
1415        Ok(bio.into_vec())
1416    }
1417
1418    /// Serialise the CRL to DER.
1419    ///
1420    /// # Errors
1421    pub fn to_der(&self) -> Result<Vec<u8>, ErrorStack> {
1422        let mut bio = MemBio::new()?;
1423        let rc = unsafe { sys::i2d_X509_CRL_bio(bio.as_ptr(), self.ptr) };
1424        if rc != 1 {
1425            return Err(ErrorStack::drain());
1426        }
1427        Ok(bio.into_vec())
1428    }
1429
1430    /// Issuer distinguished name (borrowed).
1431    #[must_use]
1432    pub fn issuer_name(&self) -> X509Name<'_> {
1433        let ptr = unsafe { sys::X509_CRL_get_issuer(self.ptr) };
1434        X509Name {
1435            ptr: ptr.cast(),
1436            _owner: PhantomData,
1437        }
1438    }
1439
1440    /// `thisUpdate` field as a human-readable string.
1441    #[must_use]
1442    pub fn last_update_str(&self) -> Option<String> {
1443        let t = unsafe { sys::X509_CRL_get0_lastUpdate(self.ptr) };
1444        asn1_time_to_str(t)
1445    }
1446
1447    /// `nextUpdate` field as a human-readable string.
1448    #[must_use]
1449    pub fn next_update_str(&self) -> Option<String> {
1450        let t = unsafe { sys::X509_CRL_get0_nextUpdate(self.ptr) };
1451        asn1_time_to_str(t)
1452    }
1453
1454    /// Verify this CRL was signed by `key`.
1455    ///
1456    /// Returns `Ok(true)` if valid, `Ok(false)` if not.
1457    ///
1458    /// # Errors
1459    pub fn verify(&self, key: &crate::pkey::Pkey<crate::pkey::Public>) -> Result<bool, ErrorStack> {
1460        match unsafe { sys::X509_CRL_verify(self.ptr, key.as_ptr()) } {
1461            1 => Ok(true),
1462            0 => Ok(false),
1463            _ => Err(ErrorStack::drain()),
1464        }
1465    }
1466
1467    /// Raw `X509_CRL*` pointer for use by internal APIs.
1468    #[must_use]
1469    #[allow(dead_code)]
1470    pub(crate) fn as_ptr(&self) -> *mut sys::X509_CRL {
1471        self.ptr
1472    }
1473}
1474
1475// ── NID ↔ name free functions ────────────────────────────────────────────────
1476
1477/// Look up a NID by its short name (e.g. `c"sha256"`, `c"rsaEncryption"`).
1478///
1479/// Returns `None` if the name is not in OpenSSL's object table.
1480#[must_use]
1481pub fn nid_from_short_name(sn: &CStr) -> Option<i32> {
1482    // SAFETY:
1483    // - sn.as_ptr() is valid, non-null, and NUL-terminated for the duration of
1484    //   this call (CStr invariant)
1485    // - OBJ_sn2nid performs a read-only table lookup; it does not retain the pointer
1486    // - no mutable global state is accessed
1487    let nid = unsafe { sys::OBJ_sn2nid(sn.as_ptr()) };
1488    if nid == 0 {
1489        None
1490    } else {
1491        Some(nid)
1492    }
1493}
1494
1495/// Look up a NID by OID text or short name.
1496///
1497/// Accepts dotted decimal (e.g. `c"2.16.840.1.101.3.4.2.1"`) or a short name
1498/// (e.g. `c"sha256"`).
1499///
1500/// Returns `None` if the string is not recognised by OpenSSL.
1501#[must_use]
1502pub fn nid_from_text(s: &CStr) -> Option<i32> {
1503    // SAFETY:
1504    // - s.as_ptr() is valid, non-null, and NUL-terminated for the duration of
1505    //   this call (CStr invariant)
1506    // - OBJ_txt2nid performs a read-only table lookup; it does not retain the pointer
1507    // - no mutable global state is accessed
1508    let nid = unsafe { sys::OBJ_txt2nid(s.as_ptr()) };
1509    if nid == 0 {
1510        None
1511    } else {
1512        Some(nid)
1513    }
1514}
1515
1516/// Look up the short name for a NID (e.g. `13` → `"CN"`, `672` → `"SHA256"`).
1517///
1518/// Returns `None` if the NID is not in OpenSSL's object table.
1519///
1520/// The returned string points to OpenSSL's static object table; its lifetime
1521/// is `'static` and no allocation is performed.
1522///
1523/// Use this together with [`crate::pkey::Pkey::is_a`] to perform provider-aware key-type
1524/// comparison: `pkey.is_a(nid_to_short_name(pknid)?)`.
1525#[must_use]
1526pub fn nid_to_short_name(nid: i32) -> Option<&'static CStr> {
1527    // SAFETY:
1528    // - nid is a plain integer; no pointer arguments
1529    // - OBJ_nid2sn returns a pointer into OpenSSL's static OBJ table, valid
1530    //   for the lifetime of the process ('static); it never allocates
1531    // - no mutable state is accessed; this is a read-only table lookup
1532    let ptr = unsafe { sys::OBJ_nid2sn(nid) };
1533    if ptr.is_null() {
1534        return None;
1535    }
1536    // SAFETY: ptr is non-null (checked above) and points to a NUL-terminated
1537    // string in OpenSSL's static object table
1538    Some(unsafe { CStr::from_ptr(ptr) })
1539}
1540
1541/// Look up the long name for a NID (e.g. `13` → `"commonName"`).
1542///
1543/// Returns `None` if the NID is not in OpenSSL's object table.
1544///
1545/// The returned string points to OpenSSL's static object table; its lifetime
1546/// is `'static` and no allocation is performed.
1547#[must_use]
1548pub fn nid_to_long_name(nid: i32) -> Option<&'static CStr> {
1549    // SAFETY: same as nid_to_short_name — plain integer argument, result
1550    // points to static storage in OpenSSL's OBJ table
1551    let ptr = unsafe { sys::OBJ_nid2ln(nid) };
1552    if ptr.is_null() {
1553        return None;
1554    }
1555    // SAFETY: ptr is non-null (checked above) and points to a NUL-terminated
1556    // string in OpenSSL's static object table
1557    Some(unsafe { CStr::from_ptr(ptr) })
1558}
1559
1560// ── Private helpers ───────────────────────────────────────────────────────────
1561
1562/// Convert an `ASN1_TIME*` to a [`BrokenDownTime`] via `ASN1_TIME_to_tm`.
1563fn asn1_time_to_broken_down(t: *const sys::ASN1_TIME) -> Option<BrokenDownTime> {
1564    if t.is_null() {
1565        return None;
1566    }
1567    // SAFETY:
1568    // - t is non-null (checked above) and valid for the duration of this call
1569    // - tm is zero-initialised; ASN1_TIME_to_tm fills all relevant fields
1570    // - no mutable aliasing: t comes from a &self borrow at call sites
1571    let mut tm = unsafe { std::mem::zeroed::<sys::tm>() };
1572    let rc = unsafe { sys::ASN1_TIME_to_tm(t, &raw mut tm) };
1573    if rc != 1 {
1574        return None;
1575    }
1576    Some(BrokenDownTime {
1577        year: tm.tm_year + 1900,
1578        month: u8::try_from(tm.tm_mon + 1).unwrap_or(0),
1579        day: u8::try_from(tm.tm_mday).unwrap_or(0),
1580        hour: u8::try_from(tm.tm_hour).unwrap_or(0),
1581        minute: u8::try_from(tm.tm_min).unwrap_or(0),
1582        second: u8::try_from(tm.tm_sec).unwrap_or(0),
1583    })
1584}
1585
1586/// Convert an `ASN1_TIME*` to a human-readable string via `ASN1_TIME_print`.
1587fn asn1_time_to_str(t: *const sys::ASN1_TIME) -> Option<String> {
1588    if t.is_null() {
1589        return None;
1590    }
1591    let mut bio = MemBio::new().ok()?;
1592    let rc = unsafe { sys::ASN1_TIME_print(bio.as_ptr(), t) };
1593    if rc != 1 {
1594        return None;
1595    }
1596    String::from_utf8(bio.into_vec()).ok()
1597}
1598
1599/// Extract the raw data bytes from an `ASN1_STRING*`.
1600///
1601/// # Safety
1602///
1603/// `asn1` must be a valid, non-null pointer for at least the duration of
1604/// lifetime `'a`.  The caller is responsible for ensuring the returned slice
1605/// does not outlive the owning ASN1 object.  Call sites bind the true lifetime
1606/// through their own `&self` borrow and `PhantomData` fields.
1607/// Free callback for `OPENSSL_sk_pop_free` when elements are `ASN1_OBJECT*`.
1608unsafe extern "C" fn free_asn1_object(p: *mut std::os::raw::c_void) {
1609    unsafe { sys::ASN1_OBJECT_free(p.cast()) }
1610}
1611
1612/// Free callback for `OPENSSL_sk_pop_free` when elements are `GENERAL_NAME*`.
1613unsafe extern "C" fn free_general_name(p: *mut std::os::raw::c_void) {
1614    unsafe { sys::GENERAL_NAME_free(p.cast()) }
1615}
1616
1617/// Serialize an `ASN1_TYPE*` to a DER byte vector.
1618/// Returns an empty `Vec` if `typ` is null or serialization fails.
1619unsafe fn i2d_asn1_type_to_vec(typ: *mut sys::ASN1_TYPE) -> Vec<u8> {
1620    if typ.is_null() {
1621        return Vec::new();
1622    }
1623    let len = unsafe { sys::i2d_ASN1_TYPE(typ, std::ptr::null_mut()) };
1624    let Ok(len_usize) = usize::try_from(len) else {
1625        return Vec::new();
1626    };
1627    let mut buf = vec![0u8; len_usize];
1628    let mut ptr = buf.as_mut_ptr();
1629    let written = unsafe { sys::i2d_ASN1_TYPE(typ, std::ptr::addr_of_mut!(ptr)) };
1630    let Ok(written_usize) = usize::try_from(written) else {
1631        return Vec::new();
1632    };
1633    buf.truncate(written_usize);
1634    buf
1635}
1636
1637unsafe fn asn1_string_data<'a>(asn1: *const sys::ASN1_STRING) -> &'a [u8] {
1638    let len = usize::try_from(sys::ASN1_STRING_length(asn1)).unwrap_or(0);
1639    let ptr = sys::ASN1_STRING_get0_data(asn1);
1640    if ptr.is_null() || len == 0 {
1641        return &[];
1642    }
1643    // SAFETY: ptr is valid for `len` bytes; lifetime 'a is upheld by the caller.
1644    std::slice::from_raw_parts(ptr, len)
1645}
1646
1647// ── Tests ─────────────────────────────────────────────────────────────────────
1648
1649#[cfg(test)]
1650mod tests {
1651    use super::*;
1652    use crate::pkey::{KeygenCtx, Pkey, Private, Public};
1653
1654    /// Build a self-signed Ed25519 certificate and run all read operations.
1655    fn make_self_signed() -> (X509, Pkey<Private>, Pkey<Public>) {
1656        let mut kgen = KeygenCtx::new(c"ED25519").unwrap();
1657        let priv_key = kgen.generate().unwrap();
1658        let pub_key = Pkey::<Public>::from(priv_key.clone());
1659
1660        let mut name = X509NameOwned::new().unwrap();
1661        name.add_entry_by_txt(c"CN", b"Test Cert").unwrap();
1662        name.add_entry_by_txt(c"O", b"Example Org").unwrap();
1663
1664        let cert = X509Builder::new()
1665            .unwrap()
1666            .set_version(2)
1667            .unwrap()
1668            .set_serial_number(1)
1669            .unwrap()
1670            .set_not_before_offset(0)
1671            .unwrap()
1672            .set_not_after_offset(365 * 86400)
1673            .unwrap()
1674            .set_subject_name(&name)
1675            .unwrap()
1676            .set_issuer_name(&name)
1677            .unwrap()
1678            .set_public_key(&pub_key)
1679            .unwrap()
1680            .sign(&priv_key, None)
1681            .unwrap()
1682            .build();
1683
1684        (cert, priv_key, pub_key)
1685    }
1686
1687    #[test]
1688    fn build_and_verify_self_signed() {
1689        let (cert, _, pub_key) = make_self_signed();
1690        assert!(cert.verify(&pub_key).unwrap());
1691        assert!(cert.is_self_signed());
1692    }
1693
1694    #[test]
1695    fn pem_round_trip() {
1696        let (cert, _, _) = make_self_signed();
1697        let pem = cert.to_pem().unwrap();
1698        assert!(pem.starts_with(b"-----BEGIN CERTIFICATE-----"));
1699
1700        let cert2 = X509::from_pem(&pem).unwrap();
1701        // Both should verify with the same key.
1702        assert_eq!(cert.to_der().unwrap(), cert2.to_der().unwrap());
1703    }
1704
1705    #[test]
1706    fn der_round_trip() {
1707        let (cert, _, _) = make_self_signed();
1708        let der = cert.to_der().unwrap();
1709        assert!(!der.is_empty());
1710
1711        let cert2 = X509::from_der(&der).unwrap();
1712        assert_eq!(cert2.to_der().unwrap(), der);
1713    }
1714
1715    #[test]
1716    fn subject_name_entries() {
1717        let (cert, _, _) = make_self_signed();
1718        let name = cert.subject_name();
1719
1720        assert_eq!(name.entry_count(), 2);
1721
1722        // First entry: CN (NID 13)
1723        let e0 = name.entry(0).unwrap();
1724        assert_eq!(e0.nid(), 13); // NID_commonName
1725        assert!(!e0.data().is_empty());
1726
1727        // to_string should include both components.
1728        let s = name.to_string().unwrap();
1729        assert!(s.contains("Test Cert") || s.contains("CN=Test Cert"));
1730    }
1731
1732    #[test]
1733    fn serial_number() {
1734        let (cert, _, _) = make_self_signed();
1735        assert_eq!(cert.serial_number(), Some(1));
1736    }
1737
1738    #[test]
1739    fn validity_strings_present() {
1740        let (cert, _, _) = make_self_signed();
1741        let nb = cert.not_before_str().unwrap();
1742        let na = cert.not_after_str().unwrap();
1743        // Both should contain "GMT" as OpenSSL uses UTC.
1744        assert!(nb.contains("GMT"), "not_before_str = {nb:?}");
1745        assert!(na.contains("GMT"), "not_after_str  = {na:?}");
1746    }
1747
1748    #[test]
1749    fn is_valid_now() {
1750        let (cert, _, _) = make_self_signed();
1751        assert!(cert.is_valid_now());
1752    }
1753
1754    #[test]
1755    fn public_key_extraction() {
1756        let (cert, _, pub_key) = make_self_signed();
1757        let extracted = cert.public_key().unwrap();
1758        // Both keys should verify the same signature.
1759        assert!(extracted.is_a(c"ED25519"));
1760        assert_eq!(pub_key.bits(), extracted.bits());
1761    }
1762
1763    #[test]
1764    fn clone_cert() {
1765        let (cert, _, pub_key) = make_self_signed();
1766        let cert2 = cert.clone();
1767        // Both references should share the same content.
1768        assert_eq!(cert.to_der().unwrap(), cert2.to_der().unwrap());
1769        assert!(cert2.verify(&pub_key).unwrap());
1770    }
1771
1772    #[test]
1773    fn verify_fails_with_wrong_key() {
1774        let (cert, _, _) = make_self_signed();
1775        // Generate a fresh, unrelated key pair.
1776        let mut kgen = KeygenCtx::new(c"ED25519").unwrap();
1777        let other_priv = kgen.generate().unwrap();
1778        let other_pub = Pkey::<Public>::from(other_priv);
1779
1780        // cert was not signed by other_pub → should return Ok(false).
1781        assert!(!cert.verify(&other_pub).unwrap());
1782    }
1783
1784    // ── X509Store / X509StoreCtx tests ──────────────────────────────────────
1785
1786    #[test]
1787    fn x509_store_add_cert_and_verify() {
1788        let (cert, _, _) = make_self_signed();
1789
1790        let mut store = X509Store::new().unwrap();
1791        store.add_cert(&cert).unwrap();
1792
1793        let mut ctx = X509StoreCtx::new().unwrap();
1794        ctx.init(&store, &cert).unwrap();
1795        // Self-signed cert trusted by its own store → should verify.
1796        assert!(ctx.verify().unwrap());
1797    }
1798
1799    #[test]
1800    fn x509_store_verify_untrusted_fails() {
1801        let (cert, _, _) = make_self_signed();
1802        // Empty store (nothing trusted).
1803        let store = X509Store::new().unwrap();
1804
1805        let mut ctx = X509StoreCtx::new().unwrap();
1806        ctx.init(&store, &cert).unwrap();
1807        assert!(!ctx.verify().unwrap());
1808        // Error code must be non-zero.
1809        assert_ne!(ctx.error(), 0);
1810    }
1811
1812    #[test]
1813    fn x509_store_ctx_chain_populated_after_verify() {
1814        let (cert, _, _) = make_self_signed();
1815        let mut store = X509Store::new().unwrap();
1816        store.add_cert(&cert).unwrap();
1817
1818        let mut ctx = X509StoreCtx::new().unwrap();
1819        ctx.init(&store, &cert).unwrap();
1820        assert!(ctx.verify().unwrap());
1821
1822        let chain = ctx.chain();
1823        assert!(
1824            !chain.is_empty(),
1825            "verified chain should contain at least the leaf"
1826        );
1827    }
1828
1829    // ── X509Crl tests ────────────────────────────────────────────────────────
1830
1831    #[test]
1832    fn x509crl_new_roundtrip() {
1833        // Allocate an empty CRL and verify it can be DER-serialised without panic.
1834        // (The DER of an empty/incomplete CRL may fail to encode a valid structure,
1835        // so we only assert that the call itself does not panic or crash.)
1836        let crl = X509Crl::new().expect("X509_CRL_new should succeed");
1837        // The CRL has no fields set; to_der may return Err (invalid ASN.1 state),
1838        // but must not panic or abort.
1839        let _result = crl.to_der();
1840        drop(crl);
1841    }
1842
1843    #[test]
1844    fn x509crl_new_in_roundtrip() {
1845        use std::sync::Arc;
1846        let ctx = Arc::new(crate::lib_ctx::LibCtx::new().expect("LibCtx::new should succeed"));
1847        let crl = X509Crl::new_in(&ctx).expect("X509_CRL_new_ex should succeed");
1848        let _result = crl.to_der();
1849        drop(crl);
1850    }
1851
1852    // A minimal CRL signed by an RSA/SHA-256 CA (generated via the openssl CLI).
1853    const TEST_CRL_PEM: &[u8] = b"\
1854-----BEGIN X509 CRL-----\n\
1855MIIBVjBAMA0GCSqGSIb3DQEBCwUAMBExDzANBgNVBAMMBlJTQSBDQRcNMjYwNDE1\n\
1856MTUwNDEzWhcNMjYwNTE1MTUwNDEzWjANBgkqhkiG9w0BAQsFAAOCAQEAi209u0hh\n\
1857Vz42YaqLplQwBoYCjtjETenl4xXRNcFOYU6Y+FmR66XNGkl9HbPClrz3hRMnbBYr\n\
1858OQJfWQOKS9lS0zpEI4qtlH/H1JBNGwiY32HMqf5HULn0w0ARvmoXR4NzsCecK22G\n\
1859gN61k5FCCpPY8HztsuoHMHAQ65W1WfBiTWu8ZH0nCCU0CA4MSaPZUiNt8/mJZzTG\n\
1860UwTGcZ/hcHQMpocBX40nE7ta5opcIpjG+q2uiCWhXwoqmYsLvdJ+Obw20bLirMHt\n\
1861UsmESTw5G+vcRCudoiSw89Z/jzsYq8yuFhRzF9kA/RtqCoQ+ylQSSH5hxzW2+bPd\n\
1862QPHivSGDiUhH6Q==\n\
1863-----END X509 CRL-----\n";
1864
1865    #[test]
1866    fn crl_pem_round_trip() {
1867        let crl = X509Crl::from_pem(TEST_CRL_PEM).unwrap();
1868        // issuer_name should be non-empty (RSA CA)
1869        let issuer = crl.issuer_name();
1870        assert!(issuer.entry_count() > 0);
1871        // last_update and next_update are present.
1872        assert!(crl.last_update_str().is_some());
1873        assert!(crl.next_update_str().is_some());
1874        // to_pem produces a valid CRL PEM.
1875        let pem = crl.to_pem().unwrap();
1876        assert!(pem.starts_with(b"-----BEGIN X509 CRL-----"));
1877    }
1878
1879    #[test]
1880    fn crl_der_round_trip() {
1881        let crl = X509Crl::from_pem(TEST_CRL_PEM).unwrap();
1882        let der = crl.to_der().unwrap();
1883        assert!(!der.is_empty());
1884        let crl2 = X509Crl::from_der(&der).unwrap();
1885        assert_eq!(crl2.to_der().unwrap(), der);
1886    }
1887
1888    #[test]
1889    fn crl_clone() {
1890        let crl = X509Crl::from_pem(TEST_CRL_PEM).unwrap();
1891        let crl2 = crl.clone();
1892        assert_eq!(crl.to_der().unwrap(), crl2.to_der().unwrap());
1893    }
1894
1895    // ── nid_from_short_name / nid_from_text tests ────────────────────────────
1896
1897    #[test]
1898    fn x509_nid_from_short_name_known() {
1899        // OBJ_sn2nid is case-sensitive; "SHA256" is the registered short name,
1900        // not "sha256" (lowercase).  CN has stable NID 13 across all versions.
1901        let nid = nid_from_short_name(c"SHA256");
1902        assert!(nid.is_some(), "SHA256 should be a known short name");
1903        assert_eq!(nid_from_short_name(c"CN"), Some(13));
1904    }
1905
1906    #[test]
1907    fn x509_nid_from_short_name_unknown() {
1908        assert_eq!(nid_from_short_name(c"not-a-real-algorithm-xyz"), None);
1909    }
1910
1911    #[test]
1912    fn x509_nid_from_text_dotted_oid() {
1913        // 2.5.4.3 is commonName — NID 13.
1914        assert_eq!(nid_from_text(c"2.5.4.3"), Some(13));
1915    }
1916
1917    #[test]
1918    fn x509_nid_from_text_short_name() {
1919        // OBJ_txt2nid accepts short names; use uppercase which OBJ_sn2nid also
1920        // recognises (OBJ_sn2nid is case-sensitive, lowercase "sha256" has no entry).
1921        let via_sn = nid_from_short_name(c"SHA256");
1922        let via_txt = nid_from_text(c"SHA256");
1923        assert_eq!(
1924            via_sn, via_txt,
1925            "short-name lookup must agree between OBJ_sn2nid and OBJ_txt2nid"
1926        );
1927    }
1928
1929    #[test]
1930    fn x509_nid_from_text_unknown() {
1931        assert_eq!(nid_from_text(c"9.9.9.9.9.9.9.9"), None);
1932    }
1933
1934    // ── serial_number_bytes tests ────────────────────────────────────────────
1935
1936    #[test]
1937    fn x509_serial_number_bytes_small_serial() {
1938        let (cert, _, _) = make_self_signed();
1939        // set_serial_number(1) → bytes should be [0x01].
1940        let bytes = cert.serial_number_bytes().unwrap();
1941        assert!(!bytes.is_empty());
1942        assert_eq!(*bytes.last().unwrap(), 1u8);
1943    }
1944
1945    #[test]
1946    fn x509_serial_number_bytes_consistent_with_serial_number() {
1947        let (cert, _, _) = make_self_signed();
1948        let bytes = cert.serial_number_bytes().unwrap();
1949        let n = cert.serial_number().unwrap();
1950        // For small positives: the big-endian bytes equal the i64 value.
1951        let be = n.to_be_bytes();
1952        // strip leading zeros to match OpenSSL's minimal encoding
1953        let start = be.iter().position(|&b| b != 0).unwrap_or(7);
1954        assert_eq!(bytes, &be[start..]);
1955    }
1956
1957    // ── not_before_tm / not_after_tm tests ──────────────────────────────────
1958
1959    #[test]
1960    fn x509_not_before_tm_is_some() {
1961        let (cert, _, _) = make_self_signed();
1962        let tm = cert.not_before_tm().expect("notBefore must be parseable");
1963        // The self-signed cert was built with set_not_before_offset(0) (≈ now).
1964        assert!(tm.year >= 2026, "year must be 2026 or later");
1965        assert!((1..=12).contains(&tm.month));
1966        assert!((1..=31).contains(&tm.day));
1967    }
1968
1969    #[test]
1970    fn x509_not_after_tm_one_year_after_not_before() {
1971        let (cert, _, _) = make_self_signed();
1972        let nb = cert.not_before_tm().unwrap();
1973        let na = cert.not_after_tm().unwrap();
1974        // The cert has a 365-day validity window.  After wrapping at year boundary
1975        // the notAfter year is either the same or one more than notBefore.
1976        let year_diff = na.year - nb.year;
1977        assert!(year_diff == 0 || year_diff == 1, "year diff must be 0 or 1");
1978    }
1979
1980    #[test]
1981    fn x509_not_before_tm_consistent_with_not_before_str() {
1982        let (cert, _, _) = make_self_signed();
1983        // Both methods must succeed on the same certificate.
1984        assert!(cert.not_before_tm().is_some());
1985        assert!(cert.not_before_str().is_some());
1986    }
1987
1988    // ── public_key_is_a / public_key_bits tests ──────────────────────────────
1989
1990    #[test]
1991    fn x509_public_key_is_a_ed25519() {
1992        let (cert, _, _) = make_self_signed();
1993        assert!(cert.public_key_is_a(c"ED25519"));
1994        assert!(!cert.public_key_is_a(c"RSA"));
1995    }
1996
1997    #[test]
1998    fn x509_public_key_bits_ed25519() {
1999        let (cert, _, _) = make_self_signed();
2000        // Ed25519 keys are 255 bits / 32 bytes; OpenSSL reports 253 or 256 bits
2001        // depending on version — just check it's non-zero.
2002        let bits = cert.public_key_bits().unwrap();
2003        assert!(bits > 0, "Ed25519 key must report non-zero bit size");
2004    }
2005
2006    #[test]
2007    fn x509_public_key_bits_agrees_with_public_key_method() {
2008        let (cert, _, _) = make_self_signed();
2009        let owned_bits = cert.public_key().unwrap().bits();
2010        let borrow_bits = cert.public_key_bits().unwrap();
2011        assert_eq!(owned_bits, borrow_bits);
2012    }
2013
2014    // ── nid_to_short_name / nid_to_long_name tests ───────────────────────────
2015
2016    #[test]
2017    fn nid_to_short_name_known_nid() {
2018        // NID 13 is "CN" (commonName) in all OpenSSL versions.
2019        let sn = nid_to_short_name(13).expect("NID 13 must be known");
2020        assert_eq!(sn.to_bytes(), b"CN");
2021    }
2022
2023    #[test]
2024    fn nid_to_long_name_known_nid() {
2025        let ln = nid_to_long_name(13).expect("NID 13 must be known");
2026        assert_eq!(ln.to_bytes(), b"commonName");
2027    }
2028
2029    #[test]
2030    fn nid_to_short_name_unknown_nid() {
2031        // A very large NID that is guaranteed not to be in the table.
2032        assert!(nid_to_short_name(i32::MAX).is_none());
2033    }
2034
2035    #[test]
2036    fn nid_to_long_name_unknown_nid() {
2037        assert!(nid_to_long_name(i32::MAX).is_none());
2038    }
2039
2040    #[test]
2041    fn nid_to_short_name_sha256() {
2042        // SHA-256 has NID 672 in OpenSSL; short name is "SHA256".
2043        let sn = nid_to_short_name(672).expect("NID 672 (SHA256) must be known");
2044        assert_eq!(sn.to_bytes(), b"SHA256");
2045    }
2046
2047    // ── X509Name::to_oneline tests ───────────────────────────────────────────
2048
2049    #[test]
2050    fn x509_name_oneline_returns_string() {
2051        let (cert, _, _) = make_self_signed();
2052        let name = cert.subject_name();
2053        let s = name
2054            .to_oneline()
2055            .expect("to_oneline must return Some for a non-empty name");
2056        // The legacy format includes "CN=" for the commonName entry.
2057        assert!(
2058            s.contains("CN="),
2059            "to_oneline output should contain CN=: {s:?}"
2060        );
2061    }
2062
2063    // ── X509::new_in tests ───────────────────────────────────────────────────
2064
2065    #[test]
2066    fn x509_new_in_lib_ctx() {
2067        use crate::lib_ctx::LibCtx;
2068        let ctx = Arc::new(LibCtx::new().expect("LibCtx::new must succeed"));
2069        let cert = X509::new_in(&ctx);
2070        assert!(
2071            cert.is_ok(),
2072            "X509::new_in must succeed with a valid LibCtx"
2073        );
2074    }
2075
2076    // ── X509::extension_der tests ────────────────────────────────────────────
2077
2078    #[test]
2079    fn x509_extension_der_absent_nid_returns_none() {
2080        let (cert, _, _) = make_self_signed();
2081        // NID 85 = subjectAltName; our simple self-signed cert has no SAN.
2082        let result = cert.extension_der(85);
2083        assert!(
2084            result.is_none(),
2085            "extension_der must return None for absent extension"
2086        );
2087    }
2088
2089    #[test]
2090    fn x509_extension_der_present_returns_some() {
2091        // Build a cert that has at least one extension: subjectKeyIdentifier (NID 82).
2092        // We rely on extension_count() to confirm at least one extension was found.
2093        let (cert, _, _) = make_self_signed();
2094        // The basic self-signed cert may or may not carry extensions depending on the
2095        // builder; use extension_count to find a valid NID and then call extension_der.
2096        let count = cert.extension_count();
2097        if count > 0 {
2098            let ext = cert
2099                .extension(0)
2100                .expect("extension(0) must be Some when count > 0");
2101            let nid = ext.nid();
2102            let der = cert
2103                .extension_der(nid)
2104                .expect("extension_der must return Some for a NID that exists in the cert");
2105            // The DER bytes for the value must match X509Extension::data().
2106            assert_eq!(
2107                der,
2108                ext.data(),
2109                "extension_der bytes must match X509Extension::data"
2110            );
2111        }
2112        // If count == 0, the test trivially passes: no extensions to check.
2113    }
2114}
2115
2116#[cfg(test)]
2117mod signature_info_tests {
2118    use super::*;
2119    use crate::pkey::{KeygenCtx, Pkey, Private, Public};
2120
2121    fn make_ed25519_cert() -> (X509, Pkey<Private>, Pkey<Public>) {
2122        let mut kgen = KeygenCtx::new(c"ED25519").unwrap();
2123        let priv_key = kgen.generate().unwrap();
2124        let pub_key = Pkey::<Public>::from(priv_key.clone());
2125
2126        let mut name = X509NameOwned::new().unwrap();
2127        name.add_entry_by_txt(c"CN", b"Ed25519 Sig Info Test")
2128            .unwrap();
2129
2130        let cert = X509Builder::new()
2131            .unwrap()
2132            .set_version(2)
2133            .unwrap()
2134            .set_serial_number(42)
2135            .unwrap()
2136            .set_not_before_offset(0)
2137            .unwrap()
2138            .set_not_after_offset(365 * 86400)
2139            .unwrap()
2140            .set_subject_name(&name)
2141            .unwrap()
2142            .set_issuer_name(&name)
2143            .unwrap()
2144            .set_public_key(&pub_key)
2145            .unwrap()
2146            .sign(&priv_key, None)
2147            .unwrap()
2148            .build();
2149
2150        (cert, priv_key, pub_key)
2151    }
2152
2153    /// Ed25519 is a "pre-hash-free" (one-shot) algorithm: the digest NID is
2154    /// `NID_undef` (0) because there is no separate hashing step.
2155    /// The public-key NID must correspond to "ED25519" in OpenSSL's OBJ table.
2156    #[test]
2157    fn x509_signature_info_ed25519() {
2158        let (cert, _, _) = make_ed25519_cert();
2159        let info = cert
2160            .signature_info()
2161            .expect("signature_info must succeed for a signed cert");
2162
2163        // Ed25519 uses no separate digest — md_nid must be NID_undef (0).
2164        assert_eq!(
2165            info.md_nid, 0,
2166            "Ed25519 md_nid must be 0 (NID_undef); got {}",
2167            info.md_nid
2168        );
2169
2170        // pk_nid must be non-zero and must map to the "ED25519" short name.
2171        assert_ne!(info.pk_nid, 0, "Ed25519 pk_nid must not be NID_undef");
2172        let sn = nid_to_short_name(info.pk_nid)
2173            .expect("Ed25519 pk_nid must have a short name in OpenSSL's OBJ table");
2174        assert_eq!(
2175            sn.to_bytes(),
2176            b"ED25519",
2177            "pk_nid short name must be ED25519; got {sn:?}"
2178        );
2179
2180        // Security bits for Ed25519 are 128.
2181        assert_eq!(
2182            info.security_bits, 128,
2183            "Ed25519 security bits must be 128; got {}",
2184            info.security_bits
2185        );
2186    }
2187}
2188
2189// ── key_usage / extended_key_usage / subject_alt_names tests ─────────────────
2190
2191#[cfg(test)]
2192mod x509_extensions_tests {
2193    use super::*;
2194    use crate::obj::Oid;
2195    use crate::pkey::{KeygenCtx, Pkey, Public};
2196
2197    /// A P-256 cert with KU, EKU, and SAN extensions set.
2198    fn make_ku_eku_san_cert() -> X509 {
2199        // Cert generated with:
2200        //   openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -noenc
2201        //     -days 3650 -subj "/CN=test"
2202        //     -addext "keyUsage=digitalSignature,keyEncipherment"
2203        //     -addext "extendedKeyUsage=serverAuth,clientAuth"
2204        //     -addext "subjectAltName=DNS:example.com,DNS:*.example.org,IP:127.0.0.1,email:user@example.com"
2205        let pem = b"-----BEGIN CERTIFICATE-----\n\
2206MIIB3zCCAYWgAwIBAgITO3vrfoe0FOHYovyQw92y4L6ttTAKBggqhkjOPQQDAjAP\n\
2207MQ0wCwYDVQQDDAR0ZXN0MB4XDTI2MDQyNTE1MjUzMFoXDTM2MDQyMjE1MjUzMFow\n\
2208DzENMAsGA1UEAwwEdGVzdDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABDE//RDS\n\
22095hyz9K7M62qDPsbwg/drLem4n5W0r8e6D6QjQ6WNBca1BABVxXmCKDvSIpKSRa+p\n\
2210Zhv5FbwiO/L2v+Ojgb8wgbwwHQYDVR0OBBYEFJxnt8JgIqZwtrg63T0lgeJBElCk\n\
2211MB8GA1UdIwQYMBaAFJxnt8JgIqZwtrg63T0lgeJBElCkMA8GA1UdEwEB/wQFMAMB\n\
2212Af8wCwYDVR0PBAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjA9\n\
2213BgNVHREENjA0ggtleGFtcGxlLmNvbYINKi5leGFtcGxlLm9yZ4cEfwAAAYEQdXNl\n\
2214ckBleGFtcGxlLmNvbTAKBggqhkjOPQQDAgNIADBFAiANTbn0J+j4dpkFxb0JKRoF\n\
22152Dh22JaVPgM+d/t+tt+GfQIhAJlQHlMSntnwVUsE0w1H/aMLoo94HStc1dGGi8/a\n\
22168Hbj\n\
2217-----END CERTIFICATE-----\n";
2218        X509::from_pem(pem).expect("test cert must parse")
2219    }
2220
2221    #[test]
2222    fn key_usage_present_and_correct() {
2223        let cert = make_ku_eku_san_cert();
2224        let ku = cert.key_usage().expect("keyUsage must be present");
2225        assert!(
2226            ku.contains(KeyUsage::DIGITAL_SIGNATURE),
2227            "digitalSignature must be set"
2228        );
2229        assert!(
2230            ku.contains(KeyUsage::KEY_ENCIPHERMENT),
2231            "keyEncipherment must be set"
2232        );
2233        assert!(!ku.contains(KeyUsage::CRL_SIGN), "cRLSign must not be set");
2234        assert!(
2235            !ku.contains(KeyUsage::KEY_CERT_SIGN),
2236            "keyCertSign must not be set"
2237        );
2238    }
2239
2240    fn make_plain_cert() -> X509 {
2241        let mut kgen = KeygenCtx::new(c"ED25519").unwrap();
2242        let priv_key = kgen.generate().unwrap();
2243        let pub_key = Pkey::<Public>::from(priv_key.clone());
2244        let mut name = X509NameOwned::new().unwrap();
2245        name.add_entry_by_txt(c"CN", b"Plain").unwrap();
2246        (|| -> Result<X509, ErrorStack> {
2247            Ok(X509Builder::new()?
2248                .set_version(2)?
2249                .set_serial_number(1)?
2250                .set_not_before_offset(0)?
2251                .set_not_after_offset(365 * 86400)?
2252                .set_subject_name(&name)?
2253                .set_issuer_name(&name)?
2254                .set_public_key(&pub_key)?
2255                .sign(&priv_key, None)?
2256                .build())
2257        })()
2258        .unwrap()
2259    }
2260
2261    #[test]
2262    fn key_usage_absent_returns_none() {
2263        let cert = make_plain_cert();
2264        assert!(cert.key_usage().is_none(), "plain cert has no keyUsage");
2265    }
2266
2267    #[test]
2268    fn extended_key_usage_oids_present() {
2269        let cert = make_ku_eku_san_cert();
2270        let oids = cert.extended_key_usage_oids().unwrap();
2271        assert_eq!(
2272            oids.len(),
2273            2,
2274            "expected 2 EKU OIDs: serverAuth + clientAuth"
2275        );
2276
2277        let server_auth = Oid::from_text("1.3.6.1.5.5.7.3.1").unwrap();
2278        let client_auth = Oid::from_text("1.3.6.1.5.5.7.3.2").unwrap();
2279        assert!(
2280            oids.contains(&server_auth),
2281            "serverAuth OID must be present"
2282        );
2283        assert!(
2284            oids.contains(&client_auth),
2285            "clientAuth OID must be present"
2286        );
2287    }
2288
2289    #[test]
2290    fn extended_key_usage_absent_returns_empty() {
2291        let cert = make_plain_cert();
2292        let oids = cert.extended_key_usage_oids().unwrap();
2293        assert!(oids.is_empty(), "plain cert has no EKU extension");
2294    }
2295
2296    #[test]
2297    fn subject_alt_names_dns_ip_email() {
2298        let cert = make_ku_eku_san_cert();
2299        let sans = cert.subject_alt_names().unwrap();
2300
2301        let dns_names: Vec<_> = sans
2302            .iter()
2303            .filter_map(|n| {
2304                if let GeneralName::Dns(s) = n {
2305                    Some(s.as_str())
2306                } else {
2307                    None
2308                }
2309            })
2310            .collect();
2311        assert!(
2312            dns_names.contains(&"example.com"),
2313            "example.com DNS SAN must be present"
2314        );
2315        assert!(
2316            dns_names.contains(&"*.example.org"),
2317            "*.example.org DNS SAN must be present"
2318        );
2319
2320        let ips: Vec<_> = sans
2321            .iter()
2322            .filter_map(|n| {
2323                if let GeneralName::IpAddress(b) = n {
2324                    Some(b.clone())
2325                } else {
2326                    None
2327                }
2328            })
2329            .collect();
2330        assert!(
2331            ips.contains(&vec![127, 0, 0, 1]),
2332            "127.0.0.1 IP SAN must be present"
2333        );
2334
2335        let emails: Vec<_> = sans
2336            .iter()
2337            .filter_map(|n| {
2338                if let GeneralName::Email(s) = n {
2339                    Some(s.as_str())
2340                } else {
2341                    None
2342                }
2343            })
2344            .collect();
2345        assert!(
2346            emails.contains(&"user@example.com"),
2347            "email SAN must be present"
2348        );
2349    }
2350
2351    #[test]
2352    fn subject_alt_names_absent_returns_empty() {
2353        let cert = make_plain_cert();
2354        let sans = cert.subject_alt_names().unwrap();
2355        assert!(sans.is_empty(), "plain cert has no subjectAltName");
2356    }
2357}