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