Skip to content
ecdsa.com

Decode a certificate · Rust

How to decode an X.509 certificate in Rust

To decode an X.509 certificate in Rust, use x509-parser, the zero-copy parser from the Rusticata project: Pem::iter_from_buffer walks every certificate in a PEM file and parse_x509 exposes subject, validity, extensions and the public key with typed accessors. This recipe prints the fields of an ECDSA certificate chain, names OIDs and computes the SHA-256 fingerprint.

Tested with x509-parser 0.18.1 (Rust 1.99.1, sha2 0.11.1) and OpenSSL 3.5.7 (test root certificate).

Cargo.toml — x509-parser, plus sha2 and hex for the fingerprint
[dependencies]
x509-parser = "=0.18.1"
sha2 = "=0.11.1"
hex = "=0.4.3"
src/main.rs — every certificate in a PEM file: fields, SANs, curve, fingerprint
use sha2::{Digest, Sha256};
use x509_parser::der_parser::oid::Oid;
use x509_parser::extensions::GeneralName;
use x509_parser::objects::{oid_registry, oid2sn};
use x509_parser::pem::Pem;
/// Short name of an OID ("ecdsa-with-SHA256"), or its dotted form when the registry does not know it.
fn oid_name(oid: &Oid) -> String {
oid2sn(oid, oid_registry()).map(str::to_owned).unwrap_or_else(|_| oid.to_id_string())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let data = std::fs::read("chain.pem")?;
// every "BEGIN CERTIFICATE" block in the file, leaf first
for pem in Pem::iter_from_buffer(&data) {
let pem = pem?;
let cert = pem.parse_x509()?; // borrows pem.contents, the DER bytes
println!("subject: {}", cert.subject());
println!("issuer: {}", cert.issuer());
println!("serial: {}", cert.raw_serial_as_string());
println!("valid: {} -> {}", cert.validity().not_before, cert.validity().not_after);
println!("signed with: {}", oid_name(&cert.signature_algorithm.algorithm));
if let Some(san) = cert.subject_alternative_name()? {
for name in &san.value.general_names {
if let GeneralName::DNSName(dns) = name {
println!("DNS name: {dns}");
}
}
}
let curve = cert.public_key().algorithm.parameters.as_ref().map(|p| p.as_oid()).transpose()?;
if let Some(curve) = curve {
println!("key: EC on {}", oid_name(&curve));
}
println!("SHA-256 fingerprint: {}", hex::encode(Sha256::digest(&pem.contents)));
println!("CA: {}, currently valid: {}", cert.is_ca(), cert.validity().is_valid());
}
Ok(())
}

How it works

  1. Pem::iter_from_buffer yields one Pem per block, so bundle files need no splitting; pem.contents holds the DER bytes and parse_x509 borrows from it without copying.
  2. subject() and issuer() display the attributes in the order the certificate stores them (CN=…, O=…, C=…), as OpenSSL does — not in the reversed RFC 4514 string order (C=…,O=…,CN=…) that Java and .NET print.
  3. Extension getters return Result<Option<…>>: an error for a malformed extension, None when it is absent. subject_alternative_name() lists GeneralName values such as DNSName.
  4. oid2sn with the bundled oid_registry() turns OIDs into OpenSSL-style short names (ecdsa-with-SHA256, prime256v1); oid_name falls back to the dotted form for OIDs the registry does not know.
  5. The fingerprint is SHA-256 over pem.contents, the DER of the whole certificate; is_ca() and validity().is_valid() answer the two most common questions about a certificate in a chain.

Gotchas

  • oid2sn returns Result<&str, NidError>, and NidError does not implement std::error::Error — ? into Box<dyn Error> does not compile. Map the error, as oid_name does.
  • validity().is_valid() compares with the current time: the example certificate stops being valid after 15 January 2027, so code that tests it must not hard-code true.
  • Parsing checks structure only. Signature verification needs the verify feature (verify_signature with the issuer's public key), and chain trust needs a validator such as rustls-webpki.
  • parse_x509_pem fails with MissingHeader on DER input; for binary certificates call x509_parser::parse_x509_certificate(&der) directly.

Related recipes