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 verificationX509StoreCtx— the verification context that consumes store + certificateX509Crl— 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_dertakes&[u8]— the full slice is consumed. A local pointer is passed tod2i_X509; the external slice reference is not advanced. Use for parsing a single, complete DER buffer.- Two serial number accessors —
serial_number()returnsOption<i64>(convenient for small serials);serial_number_bytes()returns the raw big-endian bytes viaX509_get_serialNumber(handles serials up to 20 bytes per RFC 5280). Useserial_number_bytes()when interoperating with CAs that issue large random serials. - Two public key paths —
public_key()returns an ownedPkey<Public>with a refcount increment (X509_get_pubkey).public_key_is_a()/public_key_bits()useX509_get0_pubkey(no refcount increment) for cheap introspection. Prefer theget0path when you only need to check the key type or size. public_key()is always owned —X509_get_pubkeyperformsEVP_PKEY_up_refinternally. There is no borrowed&Pkey<Public>getter.- No
Asn1Timetype — validity dates are accessed as formatted strings vianot_before_str()/not_after_str(), which returnOption<String>usingASN1_TIME_print. No structured timestamp type is exposed. X509NameOwnedvsX509Name— 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_signedchecks name equality only — it does not verify the signature. To check the signature usecert.verify(&cert.public_key()?).- Extensions — raw DER bytes are accessible via
X509Extension::data()or the convenience accessorX509::extension_der(nid). No structured extension parsing is provided; use the NID to identify the extension and interpret the bytes yourself or vianative-ossl-sysdirectly. X509StoreisClone—X509_STORE_up_refallows sharing the same trust store across threads or verification contexts.X509StoreCtxis single-use — callinginitagain afterverifyis not supported. Allocate a freshX509StoreCtxfor each certificate to verify.X509StoreCtx::chain— eachX509in the returnedVecis independently reference-counted viaX509_up_ref. The vector may outlive theX509StoreCtx.X509CrlisClone—X509_CRL_up_refallows sharing CRL objects across multiple stores without copying.not_before_tm/not_after_tmreturnBrokenDownTime— a simple struct withyear(full Gregorian year),month(1–12),day,hour,minute,second. Backed byASN1_TIME_to_tm; suitable for timestamp arithmetic without a datetime crate dependency. For comparisons against the current time, preferis_valid_now()which uses OpenSSL’s own time comparison.nid_to_short_name/nid_to_long_namereturn&'static CStr— the pointers come from OpenSSL’s static internal OBJ table (not heap-allocated per call). The'staticlifetime is correct and no deallocation is needed. These are pure table lookups backed byOBJ_nid2sn/OBJ_nid2ln.X509Name::to_oneline— produces the legacy/CN=.../O=.../C=...format viaX509_NAME_oneline. Allocates a C string that is immediately freed after conversion to a RustString(usingCRYPTO_free, the function underlying theOPENSSL_freemacro). Preferto_string()orentry()for structured access; useto_oneline()only for quick log output.X509::new_in— creates an emptyX509bound to an explicitLibCtx, useful for FIPS-isolated certificate construction. WrapsX509_new_exwith aNULLproperty query string (default provider properties).signature_infomd_nidis 0 for pre-hash-free algorithms — Ed25519 and ML-DSA (FIPS 204) sign the message directly without a separate digest step.X509_get_signature_inforeturnsNID_undef(0) formdnidin these cases. Always check formd_nid == 0before using it as a digest identifier. For traditional algorithms (RSA, ECDSA),md_nidis a valid non-zero NID such as the one for SHA-256 or SHA-384.