Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

X.509 Certificates

Overview

X509 wraps OpenSSL’s X509* structure, which is reference-counted. It can be cloned cheaply (via X509_up_ref) and shared across threads.

This module also provides:

  • X509Store — a trust store for chain verification
  • X509StoreCtx — the verification context that consumes store + certificate
  • X509Crl — a certificate revocation list (CRL)

For PKCS#12 bundles (combining key, certificate, and CA chain), see the PKCS#12 guide. For OCSP response handling, see the OCSP guide.

X509 — Certificate Wrapper

#![allow(unused)]
fn main() {
pub struct X509 { /* X509* */ }

impl X509 {
    // ── Construction ──────────────────────────────────────────────────────────

    /// Create a new, empty X509 bound to an explicit library context.
    /// Use when FIPS-awareness is required. propq is always NULL (default properties).
    pub fn new_in(ctx: &Arc<LibCtx>) -> Result<Self, ErrorStack>;

    // ── Loading ───────────────────────────────────────────────────────────────

    /// Parse from PEM bytes. Zero-copy: uses MemBioBuf (BIO_new_mem_buf).
    pub fn from_pem(pem: &[u8]) -> Result<Self, ErrorStack>;

    /// Parse from PEM within a specific library context.
    /// Note: delegates to from_pem (PEM_read_bio_X509_ex does not exist in OpenSSL 3.5).
    pub fn from_pem_in(_ctx: &Arc<LibCtx>, pem: &[u8]) -> Result<Self, ErrorStack>;

    /// Parse from a DER byte slice. The full slice is consumed.
    pub fn from_der(der: &[u8]) -> Result<Self, ErrorStack>;

    // ── Serialization ─────────────────────────────────────────────────────────

    /// Encode as PEM into a freshly allocated Vec<u8>.
    pub fn to_pem(&self) -> Result<Vec<u8>, ErrorStack>;

    /// Encode as DER into a freshly allocated Vec<u8>.
    pub fn to_der(&self) -> Result<Vec<u8>, ErrorStack>;

    // ── Field Accessors ───────────────────────────────────────────────────────

    /// Subject distinguished name. Borrowed, valid while self is alive.
    pub fn subject_name(&self) -> X509Name<'_>;

    /// Issuer distinguished name. Borrowed.
    pub fn issuer_name(&self) -> X509Name<'_>;

    /// Serial number as i64. Returns None if the serial number exceeds i64::MAX.
    pub fn serial_number(&self) -> Option<i64>;

    /// Serial number as raw big-endian bytes (content octets only, no DER tag/length).
    /// Handles serials that exceed i64::MAX (up to 20 bytes per RFC 5280).
    /// Returns None if the serial field is absent.
    pub fn serial_number_bytes(&self) -> Option<Vec<u8>>;

    /// notBefore validity as a human-readable string ("Mmm DD HH:MM:SS YYYY GMT").
    pub fn not_before_str(&self) -> Option<String>;

    /// notAfter validity as a human-readable string.
    pub fn not_after_str(&self) -> Option<String>;

    /// notBefore as a structured BrokenDownTime (year/month/day/hour/minute/second in UTC).
    pub fn not_before_tm(&self) -> Option<BrokenDownTime>;

    /// notAfter as a structured BrokenDownTime.
    pub fn not_after_tm(&self) -> Option<BrokenDownTime>;

    /// true if the current time is within [notBefore, notAfter].
    pub fn is_valid_now(&self) -> bool;

    // ── Public Key ────────────────────────────────────────────────────────────

    /// Extract the public key (owned Pkey<Public>).
    /// Calls X509_get_pubkey; the returned key is independently reference-counted.
    pub fn public_key(&self) -> Result<Pkey<Public>, ErrorStack>;

    /// Check whether the certificate's public key uses the named algorithm.
    /// Uses X509_get0_pubkey — no refcount increment. Returns false if absent.
    pub fn public_key_is_a(&self, alg: &CStr) -> bool;

    /// Bit size of the certificate's public key. Uses X509_get0_pubkey — no refcount
    /// increment. Returns None if the certificate has no public key.
    pub fn public_key_bits(&self) -> Option<u32>;

    // ── Extensions ────────────────────────────────────────────────────────────

    /// Number of extensions.
    pub fn extension_count(&self) -> usize;

    /// Extension at zero-based index. Returns None if out of range.
    pub fn extension(&self, idx: usize) -> Option<X509Extension<'_>>;

    /// First extension with the given NID. Returns None if absent.
    pub fn extension_by_nid(&self, nid: i32) -> Option<X509Extension<'_>>;

    /// DER-encoded value bytes of the first extension with the given NID.
    /// Zero-copy: borrows from the certificate's internal storage.
    /// Returns None if the extension is absent.
    pub fn extension_der(&self, nid: i32) -> Option<&[u8]>;

    // ── Verification ──────────────────────────────────────────────────────────

    /// Verify that this certificate was signed by key.
    /// Returns Ok(true) if valid, Ok(false) if not, Err on fatal error.
    pub fn verify(&self, key: &Pkey<Public>) -> Result<bool, ErrorStack>;

    /// true if subject name == issuer name (name match only; no signature check).
    pub fn is_self_signed(&self) -> bool;

    /// Inspect the signature algorithm fields of this certificate.
    /// Returns md_nid (digest), pk_nid (public-key algorithm), and security_bits.
    /// md_nid is 0 (NID_undef) for algorithms with no separate pre-hash step
    /// (e.g. Ed25519, ML-DSA).
    pub fn signature_info(&self) -> Result<SignatureInfo, ErrorStack>;
}

impl Clone for X509 { /* X509_up_ref */ }
impl Drop  for X509 { /* X509_free  */ }
unsafe impl Send for X509 {}
unsafe impl Sync for X509 {}
}

X509Name<'cert> — Distinguished Name

#![allow(unused)]
fn main() {
/// A borrowed distinguished name, lifetime-tied to the owning X509.
pub struct X509Name<'cert> { /* X509_NAME* */ }

impl X509Name<'_> {
    /// Number of RDN entries.
    pub fn entry_count(&self) -> usize;

    /// Entry at zero-based index. Returns None if out of range.
    pub fn entry(&self, idx: usize) -> Option<X509NameEntry<'_>>;

    /// Format as a string using X509_NAME_print_ex with XN_FLAG_COMPAT.
    /// Returns None if the BIO write fails.
    pub fn to_string(&self) -> Option<String>;

    /// Return the legacy one-line `/CN=.../O=.../C=...` representation
    /// produced by X509_NAME_oneline. Useful for logging and debugging.
    /// Prefer to_string() or entry() for structured access.
    /// Returns None if OpenSSL cannot allocate the string.
    pub fn to_oneline(&self) -> Option<String>;
}
}

X509NameEntry<'name> — One RDN Component

#![allow(unused)]
fn main() {
pub struct X509NameEntry<'name> { /* X509_NAME_ENTRY* */ }

impl X509NameEntry<'_> {
    /// NID of this field (e.g. NID_commonName = 13).
    pub fn nid(&self) -> i32;

    /// Raw DER-encoded value bytes.
    pub fn data(&self) -> &[u8];
}
}

X509Extension<'cert> — Certificate Extension

#![allow(unused)]
fn main() {
pub struct X509Extension<'cert> { /* X509_EXTENSION* */ }

impl X509Extension<'_> {
    /// NID of this extension.
    pub fn nid(&self) -> i32;

    /// true if this extension is marked critical.
    pub fn is_critical(&self) -> bool;

    /// Raw DER-encoded extension value bytes.
    pub fn data(&self) -> &[u8];
}
}

Building Certificates

X509NameOwned — Mutable Distinguished Name

#![allow(unused)]
fn main() {
/// An owned, mutable distinguished name for use with X509Builder.
pub struct X509NameOwned { /* X509_NAME* */ }

impl X509NameOwned {
    pub fn new() -> Result<Self, ErrorStack>;

    /// Append a field. `field` is a short name (c"CN", c"O", c"C").
    /// `value` is a UTF-8 byte slice.
    pub fn add_entry_by_txt(&mut self, field: &CStr, value: &[u8])
        -> Result<(), ErrorStack>;
}
}

X509Builder

#![allow(unused)]
fn main() {
pub struct X509Builder { /* X509* under construction */ }

impl X509Builder {
    pub fn new() -> Result<Self, ErrorStack>;

    /// Set version: 0=v1, 1=v2, 2=v3.
    pub fn set_version(self, version: i64) -> Result<Self, ErrorStack>;

    pub fn set_serial_number(self, n: i64) -> Result<Self, ErrorStack>;

    /// Set notBefore to now + offset_secs.
    pub fn set_not_before_offset(self, offset_secs: i64) -> Result<Self, ErrorStack>;

    /// Set notAfter to now + offset_secs.
    pub fn set_not_after_offset(self, offset_secs: i64) -> Result<Self, ErrorStack>;

    pub fn set_subject_name(self, name: &X509NameOwned) -> Result<Self, ErrorStack>;
    pub fn set_issuer_name(self, name: &X509NameOwned)  -> Result<Self, ErrorStack>;

    pub fn set_public_key<T: HasPublic>(self, key: &Pkey<T>) -> Result<Self, ErrorStack>;

    /// Sign the certificate.
    /// Pass digest = None for EdDSA (Ed25519, Ed448).
    pub fn sign(self, key: &Pkey<Private>, digest: Option<&DigestAlg>)
        -> Result<Self, ErrorStack>;

    /// Finalise and return the X509.
    pub fn build(self) -> X509;
}
}

X509Store — Trust Store

An X509Store holds trusted CA certificates and CRLs. Pass it to X509StoreCtx::init to verify a certificate chain, or to OcspResponse::basic_verify to verify an OCSP response signature.

#![allow(unused)]
fn main() {
pub struct X509Store { /* X509_STORE* */ }

impl X509Store {
    /// Create an empty trust store.
    pub fn new() -> Result<Self, ErrorStack>;

    /// Add a trusted certificate.
    /// The certificate's reference count is incremented internally.
    pub fn add_cert(&mut self, cert: &X509) -> Result<(), ErrorStack>;

    /// Add a CRL to the store.
    pub fn add_crl(&mut self, crl: &X509Crl) -> Result<(), ErrorStack>;

    /// Set verification flags (e.g. X509_V_FLAG_CRL_CHECK).
    pub fn set_flags(&mut self, flags: u64) -> Result<(), ErrorStack>;
}

impl Clone for X509Store { /* X509_STORE_up_ref */ }
}

X509Store is Clone (via X509_STORE_up_ref) and Send + Sync.

X509StoreCtx — Chain Verification Context

X509StoreCtx drives X509_verify_cert. It is single-use: create, initialise, verify, inspect, then drop.

#![allow(unused)]
fn main() {
pub struct X509StoreCtx { /* X509_STORE_CTX* */ }

impl X509StoreCtx {
    /// Allocate a new, uninitialised verification context.
    pub fn new() -> Result<Self, ErrorStack>;

    /// Initialise for verifying `cert` against `store`.
    /// Call before `verify`.
    pub fn init(&mut self, store: &X509Store, cert: &X509) -> Result<(), ErrorStack>;

    /// Verify the certificate chain.
    ///
    /// Returns Ok(true) if valid, Ok(false) if not (call `error()` for the
    /// error code), or Err on a fatal OpenSSL error.
    pub fn verify(&mut self) -> Result<bool, ErrorStack>;

    /// OpenSSL verification error code after a failed `verify`.
    /// Returns 0 (X509_V_OK) if no error occurred.
    /// See <openssl/x509_vfy.h> for X509_V_ERR_* constants.
    pub fn error(&self) -> i32;

    /// Collect the verified chain as owned X509 values.
    /// Only meaningful after a successful `verify`.
    pub fn chain(&self) -> Vec<X509>;
}
}

X509Crl — Certificate Revocation List

#![allow(unused)]
fn main() {
pub struct X509Crl { /* X509_CRL* */ }

impl X509Crl {
    /// Allocate a new, empty CRL structure (X509_CRL_new).
    pub fn new() -> Result<Self, ErrorStack>;

    /// Allocate a new, empty CRL bound to a library context (X509_CRL_new_ex).
    pub fn new_in(ctx: &Arc<LibCtx>) -> Result<Self, ErrorStack>;

    /// Parse from PEM bytes.
    pub fn from_pem(pem: &[u8]) -> Result<Self, ErrorStack>;

    /// Parse from DER bytes.
    pub fn from_der(der: &[u8]) -> Result<Self, ErrorStack>;

    /// Serialise to PEM.
    pub fn to_pem(&self) -> Result<Vec<u8>, ErrorStack>;

    /// Serialise to DER.
    pub fn to_der(&self) -> Result<Vec<u8>, ErrorStack>;

    /// Issuer distinguished name (borrowed).
    pub fn issuer_name(&self) -> X509Name<'_>;

    /// `thisUpdate` field as a human-readable string.
    pub fn last_update_str(&self) -> Option<String>;

    /// `nextUpdate` field as a human-readable string.
    pub fn next_update_str(&self) -> Option<String>;

    /// Verify this CRL was signed by `key`.
    /// Returns Ok(true) if valid, Ok(false) if not.
    pub fn verify(&self, key: &Pkey<Public>) -> Result<bool, ErrorStack>;
}

impl Clone for X509Crl { /* X509_CRL_up_ref */ }
}

X509Crl is Clone (via X509_CRL_up_ref) and Send + Sync.

NID Lookup Free Functions

OpenSSL uses numeric identifiers (NIDs) to represent OIDs internally. Two free functions in native_ossl::x509 convert human-readable names to NIDs:

#![allow(unused)]
fn main() {
/// Look up a NID by short name (e.g. c"sha256", c"CN", c"rsaEncryption").
/// Returns None if the name is not in OpenSSL's object table.
pub fn nid_from_short_name(sn: &CStr) -> Option<i32>;

/// Look up a NID by OID text or short name.
/// Accepts dotted decimal ("2.5.4.3") or short name ("CN").
/// Returns None if the string is not recognised.
pub fn nid_from_text(s: &CStr) -> Option<i32>;
}

These wrap OBJ_sn2nid and OBJ_txt2nid respectively. Use them when you have a NID-based API (e.g. X509::extension_by_nid) but only know the algorithm by name:

#![allow(unused)]
fn main() {
use native_ossl::x509::nid_from_short_name;

// Find Subject Key Identifier extension by NID.
let nid = nid_from_short_name(c"subjectKeyIdentifier").unwrap();
let ext = cert.extension_by_nid(nid);
}

Examples

Create an X509 Bound to a Library Context

#![allow(unused)]
fn main() {
use std::sync::Arc;
use native_ossl::lib_ctx::LibCtx;
use native_ossl::x509::X509;

let ctx = Arc::new(LibCtx::new()?);
let _prov = ctx.load_provider(c"default")?;
let cert = X509::new_in(&ctx)?;
// cert is now associated with ctx for any provider-dispatched operations.
}

Load and Inspect a Certificate

#![allow(unused)]
fn main() {
use native_ossl::x509::X509;

let pem = std::fs::read("cert.pem")?;
let cert = X509::from_pem(&pem)?;

// Print subject
if let Some(subject) = cert.subject_name().to_string() {
    println!("Subject: {subject}");
}

// Print validity
println!("Not before: {:?}", cert.not_before_str());
println!("Not after:  {:?}", cert.not_after_str());
println!("Valid now: {}", cert.is_valid_now());

// Serial number
if let Some(serial) = cert.serial_number() {
    println!("Serial: {serial}");
}
}

Inspect Name Entries Manually

#![allow(unused)]
fn main() {
let name = cert.subject_name();
for i in 0..name.entry_count() {
    if let Some(entry) = name.entry(i) {
        println!("  NID {}: {:?}", entry.nid(), entry.data());
    }
}
}

Build a Self-Signed Certificate (Ed25519)

#![allow(unused)]
fn main() {
use native_ossl::pkey::KeygenCtx;
use native_ossl::x509::{X509Builder, X509NameOwned};

let key = KeygenCtx::new(c"ED25519")?.generate()?;

let mut name = X509NameOwned::new()?;
name.add_entry_by_txt(c"CN", b"example.com")?;
name.add_entry_by_txt(c"O",  b"Example Corp")?;

let cert = X509Builder::new()?
    .set_version(2)?                    // X.509v3
    .set_serial_number(1)?
    .set_not_before_offset(0)?          // valid from now
    .set_not_after_offset(365 * 86400)? // valid for one year
    .set_subject_name(&name)?
    .set_issuer_name(&name)?            // self-signed
    .set_public_key(&key)?
    .sign(&key, None)?                  // None = no separate digest (EdDSA)
    .build();

let pem = cert.to_pem()?;
}

Build a Self-Signed Certificate (RSA)

#![allow(unused)]
fn main() {
use native_ossl::pkey::KeygenCtx;
use native_ossl::digest::DigestAlg;
use native_ossl::x509::{X509Builder, X509NameOwned};

let key = KeygenCtx::new(c"RSA")?.generate()?;
let sha256 = DigestAlg::fetch(c"SHA2-256", None)?;

let mut name = X509NameOwned::new()?;
name.add_entry_by_txt(c"CN", b"example.com")?;

let cert = X509Builder::new()?
    .set_version(2)?
    .set_serial_number(1)?
    .set_not_before_offset(0)?
    .set_not_after_offset(365 * 86400)?
    .set_subject_name(&name)?
    .set_issuer_name(&name)?
    .set_public_key(&key)?
    .sign(&key, Some(&sha256))?         // RSA requires an explicit digest
    .build();
}

Verify a Certificate Signature

#![allow(unused)]
fn main() {
let cert = X509::from_pem(&pem)?;
let ca_cert = X509::from_pem(&ca_pem)?;
let ca_key = ca_cert.public_key()?;

match cert.verify(&ca_key)? {
    true  => println!("Signature valid"),
    false => println!("Signature INVALID"),
}
}

Chain Verification with X509Store

use native_ossl::x509::{X509, X509Store, X509StoreCtx};

// Build a trust store from PEM-encoded CA certificates.
let mut store = X509Store::new()?;
for ca_pem in &ca_pem_list {
    let ca = X509::from_pem(ca_pem)?;
    store.add_cert(&ca)?;
}

// Verify an end-entity certificate against the store.
let cert = X509::from_pem(&ee_pem)?;
let mut ctx = X509StoreCtx::new()?;
ctx.init(&store, &cert)?;

match ctx.verify()? {
    true => {
        println!("Chain valid");
        // Inspect the verified chain (leaf first).
        for c in ctx.chain() {
            println!("  {}", c.subject_name().to_string().unwrap_or_default());
        }
    }
    false => {
        println!("Chain INVALID: error code {}", ctx.error());
    }
}

Load and Verify a CRL

use native_ossl::x509::{X509, X509Crl};

let crl_pem = std::fs::read("crl.pem")?;
let crl = X509Crl::from_pem(&crl_pem)?;

println!("Issuer:      {}", crl.issuer_name().to_string().unwrap_or_default());
println!("This update: {:?}", crl.last_update_str());
println!("Next update: {:?}", crl.next_update_str());

// Verify the CRL signature with the issuer's public key.
let ca_cert = X509::from_pem(&ca_pem)?;
let ca_key = ca_cert.public_key()?;

match crl.verify(&ca_key)? {
    true  => println!("CRL signature valid"),
    false => println!("CRL signature INVALID"),
}

CRL-Checked Chain Verification

To enable CRL checking in X509StoreCtx, add the CRL to the store and set the appropriate flag:

use native_ossl::x509::{X509, X509Crl, X509Store, X509StoreCtx};

const X509_V_FLAG_CRL_CHECK: u64 = 0x4;

let mut store = X509Store::new()?;
store.add_cert(&ca_cert)?;
store.add_crl(&crl)?;
store.set_flags(X509_V_FLAG_CRL_CHECK)?;

let mut ctx = X509StoreCtx::new()?;
ctx.init(&store, &end_entity_cert)?;
let ok = ctx.verify()?;

NID Lookup Free Functions

OpenSSL uses numeric identifiers (NIDs) to identify algorithms and object types. These free functions convert between NIDs and human-readable names.

#![allow(unused)]
fn main() {
/// Short name for a NID (e.g. 13 → "CN", 672 → "SHA256"). Returns None if unknown.
/// Result points to OpenSSL's static table; lifetime is 'static, no allocation.
pub fn nid_to_short_name(nid: i32) -> Option<&'static CStr>;

/// Long name for a NID (e.g. 13 → "commonName"). Returns None if unknown.
pub fn nid_to_long_name(nid: i32) -> Option<&'static CStr>;
}

Provider-aware key-type comparison

When inspecting certificate signature algorithms with X509_get_signature_info, you get a pknid (signer’s key NID). Comparing it against a public key using EVP_PKEY_get_base_id is broken for provider-based keys (ML-DSA, ML-KEM, SLH-DSA) — that function returns NID_undef (0) for all of them. The correct pattern uses EVP_PKEY_is_a with the NID’s short name:

#![allow(unused)]
fn main() {
// WRONG for provider-based keys:
// pkey.get_base_id() == pknid   ← returns 0 for all provider keys

// Correct — works for both legacy and provider-based keys:
use native_ossl::x509::nid_to_short_name;

if let Some(sn) = nid_to_short_name(pknid) {
    if pkey.is_a(sn) {
        // subject key matches the signing algorithm
    }
}
}

Design Notes

  • from_der takes &[u8] — the full slice is consumed. A local pointer is passed to d2i_X509; the external slice reference is not advanced. Use for parsing a single, complete DER buffer.
  • Two serial number accessorsserial_number() returns Option<i64> (convenient for small serials); serial_number_bytes() returns the raw big-endian bytes via X509_get_serialNumber (handles serials up to 20 bytes per RFC 5280). Use serial_number_bytes() when interoperating with CAs that issue large random serials.
  • Two public key pathspublic_key() returns an owned Pkey<Public> with a refcount increment (X509_get_pubkey). public_key_is_a() / public_key_bits() use X509_get0_pubkey (no refcount increment) for cheap introspection. Prefer the get0 path when you only need to check the key type or size.
  • public_key() is always ownedX509_get_pubkey performs EVP_PKEY_up_ref internally. There is no borrowed &Pkey<Public> getter.
  • No Asn1Time type — validity dates are accessed as formatted strings via not_before_str() / not_after_str(), which return Option<String> using ASN1_TIME_print. No structured timestamp type is exposed.
  • X509NameOwned vs X509Name — the owned type is for the builder (mutable). The borrowed type (X509Name<'cert>) is for reading from a parsed certificate. They are not interchangeable.
  • is_self_signed checks name equality only — it does not verify the signature. To check the signature use cert.verify(&cert.public_key()?).
  • Extensions — raw DER bytes are accessible via X509Extension::data() or the convenience accessor X509::extension_der(nid). No structured extension parsing is provided; use the NID to identify the extension and interpret the bytes yourself or via native-ossl-sys directly.
  • X509Store is CloneX509_STORE_up_ref allows sharing the same trust store across threads or verification contexts.
  • X509StoreCtx is single-use — calling init again after verify is not supported. Allocate a fresh X509StoreCtx for each certificate to verify.
  • X509StoreCtx::chain — each X509 in the returned Vec is independently reference-counted via X509_up_ref. The vector may outlive the X509StoreCtx.
  • X509Crl is CloneX509_CRL_up_ref allows sharing CRL objects across multiple stores without copying.
  • not_before_tm / not_after_tm return BrokenDownTime — a simple struct with year (full Gregorian year), month (1–12), day, hour, minute, second. Backed by ASN1_TIME_to_tm; suitable for timestamp arithmetic without a datetime crate dependency. For comparisons against the current time, prefer is_valid_now() which uses OpenSSL’s own time comparison.
  • nid_to_short_name / nid_to_long_name return &'static CStr — the pointers come from OpenSSL’s static internal OBJ table (not heap-allocated per call). The 'static lifetime is correct and no deallocation is needed. These are pure table lookups backed by OBJ_nid2sn / OBJ_nid2ln.
  • X509Name::to_oneline — produces the legacy /CN=.../O=.../C=... format via X509_NAME_oneline. Allocates a C string that is immediately freed after conversion to a Rust String (using CRYPTO_free, the function underlying the OPENSSL_free macro). Prefer to_string() or entry() for structured access; use to_oneline() only for quick log output.
  • X509::new_in — creates an empty X509 bound to an explicit LibCtx, useful for FIPS-isolated certificate construction. Wraps X509_new_ex with a NULL property query string (default provider properties).
  • signature_info md_nid is 0 for pre-hash-free algorithms — Ed25519 and ML-DSA (FIPS 204) sign the message directly without a separate digest step. X509_get_signature_info returns NID_undef (0) for mdnid in these cases. Always check for md_nid == 0 before using it as a digest identifier. For traditional algorithms (RSA, ECDSA), md_nid is a valid non-zero NID such as the one for SHA-256 or SHA-384.