synta-certificate 0.1.2

X.509 certificate structures for synta ASN.1 library
Documentation

synta-certificate

Table of Contents generated with DocToc

X.509 certificate structures for the synta ASN.1 library.

Overview

This crate provides typed X.509 v3 certificate structures based on RFC 5280. The structures integrate directly with the synta ASN.1 encoder/decoder and are designed for high-throughput parsing: the issuer, subject, and extensions fields are stored as zero-copy RawDer<'a> spans — raw DER byte slices borrowed from the input buffer — so they are decoded lazily, on demand, without any allocation at parse time.

Algorithm identification helpers return &'static str or Option<&'static str> via direct OID component matching, with no string allocation or formatting.

Features

  • RFC 5280 compliant — complete X.509 v3 certificate and TBSCertificate structures
  • Zero-copy parsingissuer, subject, and extensions stored as RawDer<'a> byte spans; subjectPublicKey stored as BitStringRef<'a>
  • Lazy field decoding — Distinguished Names and extensions are decoded only when accessed, eliminating parse-time overhead
  • Auto-generated from ASN.1 schemas — all types generated from formal ASN.1 definitions via synta-codegen
  • Post-quantum ready — built-in support for ML-DSA (FIPS 204) and ML-KEM (FIPS 203) algorithms
  • Zero-alloc OID identificationidentify_signature_algorithm and identify_public_key_algorithm return static strings
  • RFC 4514-style DN formattingformat_dn walks the raw DER directly; format_dn_slash emits the openssl legacy /CN=.../O=... form used in AKI DirName output
  • Public key decodingdecode_public_key_info dispatches on the algorithm OID and returns a PublicKeyInfo enum with algorithm-specific fields (RSA modulus/exponent, EC curve metadata)
  • PEM encoder/decoder — dependency-free pem_to_der / der_to_pem (RFC 7468); no external crate required
  • X.509 CRL support — parse Certificate Revocation Lists (RFC 5280 §5) via the crl module
  • PKCS#10 CSR support — parse Certificate Signing Requests (RFC 2986) via the csr module
  • OCSP support — parse Online Certificate Status Protocol responses (RFC 6960) via the ocsp module
  • PKCS#7 / CMS certificate extraction — extract certificates from SignedData bundles (RFC 5652) via certs_from_pkcs7
  • PKCS#12 certificate extraction — extract certificates from PFX archives (RFC 7292) via certs_from_pkcs12; optional OpenSSL backend for encrypted bags
  • PKCS#12 private key extraction — extract OneAsymmetricKey (PKCS#8 / RFC 5958) blobs from PFX archives via keys_from_pkcs12; same OpenSSL backend for encrypted bags
  • Subject Alternative Name accessCertificate::subject_alt_names() returns parsed SAN entries as (tag_number, content) pairs; parse_general_names walks any raw SEQUENCE OF GeneralName DER span
  • DN attribute parsingparse_name_attrs extracts (dotted_oid, value_str) pairs from a raw Name DER span; decode_string_value decodes individual ASN.1 string values by tag, with Latin-1 normalisation for TeletexString
  • PKCS#9 OID constants — 9 attribute OIDs from RFC 2985, RFC 5652, RFC 2986, and RFC 7292 exposed in oids (PKCS9_EMAIL_ADDRESS, PKCS9_CONTENT_TYPE, PKCS9_MESSAGE_DIGEST, PKCS9_SIGNING_TIME, PKCS9_COUNTERSIGNATURE, PKCS9_CHALLENGE_PASSWORD, PKCS9_EXTENSION_REQUEST, PKCS9_FRIENDLY_NAME, PKCS9_LOCAL_KEY_ID)
  • PKCS#8 typesOneAsymmetricKey<'a> (RFC 5958) and PrivateKeyInfo<'a> alias generated from asn1/PKCS8.asn1 via pkcs8_types module
  • CMS EncryptedData encryptionOpensslEncryptor implements the Encryptor and CmsEncryptor traits, producing RFC 5652 §8 EncryptedData DER with AES-CBC and a random IV; OpensslDecryptor handles the symmetric decryption side
  • Python bindings — all pyo3 wrappers live in the synta-python crate; synta-certificate itself has no PyO3 dependency
  • no_std support — works in embedded and constrained environments with the alloc feature

Usage

Add to your Cargo.toml:

[dependencies]
synta-certificate = "0.1"
synta = "0.1"

Decoding a Certificate

use synta::{Decoder, Encoding};
use synta_certificate::Certificate;

let der_bytes: &[u8] = /* DER-encoded certificate */;

let mut decoder = Decoder::new(der_bytes, Encoding::Der);
let cert: Certificate = decoder.decode()?;

// serial_number is decoded eagerly
println!("Serial: {:?}", cert.tbs_certificate.serial_number);

// issuer and subject are stored as RawDer<'a> — raw DER bytes, zero-copy
let issuer_der: &[u8] = cert.tbs_certificate.issuer.as_bytes();

// format_dn walks the raw DER directly; no intermediate allocation
use synta_certificate::format_dn;
let subject_str = format_dn(cert.tbs_certificate.subject.as_bytes());
println!("Subject: {}", subject_str);

Encoding a Certificate

use synta::{Encoder, Encoding};
use synta_certificate::{Certificate, TBSCertificate, AlgorithmIdentifier, Validity};

let cert = Certificate {
    tbs_certificate: /* ... */,
    signature_algorithm: /* ... */,
    signature_value: /* ... */,
};

let mut encoder = Encoder::new(Encoding::Der);
encoder.encode(&cert)?;
let der_bytes = encoder.finish()?;

Algorithm Identification

identify_signature_algorithm and identify_public_key_algorithm use two-level integer dispatch: a length+prefix check selects the algorithm family, then a u32 match selects the variant. All discriminant values are derived from the generated oids constants via const-indexing. The functions never allocate and return &'static str.

use synta::ObjectIdentifier;
use synta_certificate::{oids, names, identify_signature_algorithm, identify_public_key_algorithm};

let oid = ObjectIdentifier::new(oids::SHA256_WITH_RSA).unwrap();
assert_eq!(identify_signature_algorithm(&oid), names::SHA256_WITH_RSA);

let oid = ObjectIdentifier::new(oids::ML_DSA_44).unwrap();
assert_eq!(identify_signature_algorithm(&oid), names::ML_DSA_44);

let oid = ObjectIdentifier::new(oids::ML_KEM_768).unwrap();
assert_eq!(identify_public_key_algorithm(&oid), Some(names::ML_KEM_768));

DN Formatting

format_dn formats a DER-encoded Name as a comma-space-separated string, matching the openssl x509 -text output style:

use synta_certificate::format_dn;

// Takes raw DER bytes of a Name (e.g. from RawDer::as_bytes())
let dn_string = format_dn(cert.tbs_certificate.subject.as_bytes());
// → "CN=example.com, O=Example Inc, C=US"

format_dn_slash produces the openssl legacy slash-prefix form used for DirName values inside Authority Key Identifier and Subject Alt Name:

use synta_certificate::format_dn_slash;

let dirn = format_dn_slash(name_der);
// → "/C=US/O=Example Inc/CN=example.com"

Public Key Decoding

decode_public_key_info inspects the algorithm OID and returns a PublicKeyInfo enum with algorithm-specific fields, so callers do not need to repeat OID dispatch for display or further processing:

use synta_certificate::{decode_public_key_info, PublicKeyInfo};

let spki = &cert.tbs_certificate.subject_public_key_info;
let info = decode_public_key_info(
    &spki.algorithm.algorithm,
    spki.algorithm.parameters.as_ref(),
    spki.subject_public_key.as_bytes(),
    spki.subject_public_key.bit_len(),
);

match info {
    PublicKeyInfo::Rsa { modulus, exponent, bit_count } => {
        println!("RSA-{}, exponent={}", bit_count, exponent);
    }
    PublicKeyInfo::Ec { bit_count, curve_nist_name, .. } => {
        println!("EC-{} ({})", bit_count, curve_nist_name.unwrap_or("?"));
    }
    PublicKeyInfo::Unknown { alg_name, .. } => {
        println!("Unknown: {}", alg_name);
    }
}

PEM Encoding and Decoding

pem_to_der decodes every -----BEGIN ...----- block in a byte slice and returns the DER bytes for each. der_to_pem encodes DER bytes as a single PEM block with a caller-supplied label. Neither function depends on any external crate.

use synta_certificate::{pem_to_der, der_to_pem};

// Decode: may contain multiple blocks (e.g. a certificate chain)
let pem_bytes = std::fs::read("chain.pem").unwrap();
let ders: Vec<Vec<u8>> = pem_to_der(&pem_bytes);

// Encode: standard RFC 7468 format, 64-char lines
let pem_output = der_to_pem("CERTIFICATE", &der_bytes);
std::fs::write("cert.pem", &pem_output).unwrap();

Common labels: "CERTIFICATE", "CERTIFICATE REQUEST", "X509 CRL", "OCSP RESPONSE".

Parsing a CRL

The crl module exposes CertificateList (RFC 5280 §5):

use synta::{Decoder, Encoding};
use synta_certificate::crl::CertificateList;

let der = std::fs::read("revoked.crl").unwrap();
let mut dec = Decoder::new(&der, Encoding::Der);
let crl: CertificateList = dec.decode().unwrap();

let tbs = &crl.tbs_cert_list;
println!("Issuer: {:?}", tbs.issuer);
println!("This update: {:?}", tbs.this_update);
let revoked = tbs.revoked_certificates.as_ref().map_or(0, |v| v.len());
println!("Revoked entries: {}", revoked);

Parsing a CSR

The csr module exposes CertificationRequest (RFC 2986 / PKCS#10):

use synta::{Decoder, Encoding};
use synta_certificate::csr::CertificationRequest;

let der = std::fs::read("request.csr").unwrap();
let mut dec = Decoder::new(&der, Encoding::Der);
let csr: CertificationRequest = dec.decode().unwrap();

let info = &csr.certification_request_info;
println!("Subject: {:?}", info.subject);
println!("Version: {:?}", info.version);

Parsing an OCSP Response

The ocsp module exposes OCSPResponse (RFC 6960):

use synta::{Decoder, Encoding};
use synta_certificate::ocsp::OCSPResponse;

let der = std::fs::read("response.der").unwrap();
let mut dec = Decoder::new(&der, Encoding::Der);
let resp: OCSPResponse = dec.decode().unwrap();

println!("Status: {:?}", resp.response_status);
if let Some(rb) = &resp.response_bytes {
    println!("Response type OID: {:?}", rb.response_type);
}

Extracting Certificates from PKCS#7 / CMS

certs_from_pkcs7 extracts DER-encoded certificates from a PKCS#7 SignedData bundle (RFC 5652). Both DER and BER input are accepted:

use synta_certificate::certs_from_pkcs7;

let p7b = std::fs::read("bundle.p7b").unwrap();
let certs: Vec<Vec<u8>> = certs_from_pkcs7(&p7b).unwrap();
println!("Found {} certificate(s)", certs.len());

Extracting Certificates from PKCS#12

certs_from_pkcs12 extracts DER-encoded certificates from a PKCS#12 PFX archive (RFC 7292). Pass NoCrypto to reject encrypted bags:

use synta_certificate::{certs_from_pkcs12, NoCrypto};

let pfx = std::fs::read("keystore.p12").unwrap();
let certs: Vec<Vec<u8>> = certs_from_pkcs12(&pfx, b"password", &NoCrypto).unwrap();

Enable the openssl feature to decrypt bags encrypted with PBES2/PBKDF2 + AES or legacy RC2/3DES algorithms (with deprecated-pkcs12-algorithms):

[dependencies]
synta-certificate = { version = "0.1", features = ["openssl"] }
use synta_certificate::{certs_from_pkcs12, OpensslDecryptor};

let pfx = std::fs::read("encrypted.p12").unwrap();
let certs = certs_from_pkcs12(&pfx, b"secret", &OpensslDecryptor).unwrap();

Extracting Private Keys from PKCS#12

keys_from_pkcs12 extracts OneAsymmetricKey (PKCS#8, RFC 5958) blobs from a PKCS#12 PFX archive. It follows the same decryptor pattern as certs_from_pkcs12:

use synta_certificate::{keys_from_pkcs12, NoCrypto};

let pfx = std::fs::read("keystore.p12").unwrap();
let keys: Vec<Vec<u8>> = keys_from_pkcs12(&pfx, b"password", &NoCrypto).unwrap();
println!("Found {} private key(s)", keys.len());

Enable the openssl feature for bags encrypted with PBES2/PBKDF2 + AES:

use synta_certificate::{keys_from_pkcs12, OpensslDecryptor};

let pfx = std::fs::read("encrypted.p12").unwrap();
let keys = keys_from_pkcs12(&pfx, b"secret", &OpensslDecryptor).unwrap();

Subject Alternative Names

Certificate::subject_alt_names() locates the SAN extension (OID 2.5.29.17) and returns parsed GeneralName entries as (tag_number, content) pairs. Returns an empty Vec when no SAN extension is present.

Tag numbers follow the GeneralName CHOICE (RFC 5280 §4.2.1.6):

Tag Alternative Content
0 otherName OtherName value bytes
1 rfc822Name IA5String bytes (email)
2 dNSName IA5String bytes (DNS name)
4 directoryName Complete Name SEQUENCE TLV
6 uniformResourceIdentifier IA5String bytes (URI)
7 iPAddress 4 bytes (IPv4) or 16 bytes (IPv6)
8 registeredID OID value bytes
use synta::{Decoder, Encoding};
use synta_certificate::Certificate;

let der = std::fs::read("cert.der").unwrap();
let mut dec = Decoder::new(&der, Encoding::Der);
let cert: Certificate = dec.decode().unwrap();

for (tag, content) in cert.subject_alt_names() {
    match tag {
        2 => println!("DNS: {}", String::from_utf8_lossy(&content)),
        7 if content.len() == 4 => {
            let octets: [u8; 4] = content.try_into().unwrap();
            println!("IP: {}", std::net::Ipv4Addr::from(octets));
        }
        _ => {}
    }
}

parse_general_names does the same walk on any raw SEQUENCE OF GeneralName DER span, e.g. an extension value retrieved by OID:

use synta_certificate::parse_general_names;

// san_der is the DER bytes of a SEQUENCE OF GeneralName
let san_der: Vec<u8> = std::fs::read("san.der").unwrap();
for (tag, content) in parse_general_names(&san_der) {
    println!("tag={} len={}", tag, content.len());
}

Parsing Distinguished Name Attributes

parse_name_attrs walks a raw Name DER span and returns one (dotted_oid, value_str) pair per AttributeTypeAndValue. String values are decoded per-tag (TeletexString → Latin-1, BMP/Universal → UTF-16 BE → UTF-8); unrecognised tags produce a #hexstring.

use synta::{Decoder, Encoding};
use synta_certificate::{Certificate, parse_name_attrs};

let der = std::fs::read("cert.der").unwrap();
let mut dec = Decoder::new(&der, Encoding::Der);
let cert: Certificate = dec.decode().unwrap();

// issuer and subject are RawDer<'a>; as_bytes() returns the Name TLV
let attrs = parse_name_attrs(cert.tbs_certificate.subject.as_bytes());
for (oid, value) in &attrs {
    println!("{} = {}", oid, value);
}
// e.g.: "2.5.4.3 = example.com", "2.5.4.10 = Example Inc", "2.5.4.6 = US"

decode_string_value decodes a single value field given its tag number and raw value bytes (tag + length already stripped):

use synta_certificate::decode_string_value;

// tag 12 = UTF8String, 19 = PrintableString, 20 = TeletexString, 22 = IA5String
let s = decode_string_value(12, b"hello");
assert_eq!(s, "hello");

CMS EncryptedData Encryption and Decryption

OpensslEncryptor implements both Encryptor (raw AES-CBC) and CmsEncryptor (full RFC 5652 §8 EncryptedData DER assembly). A fresh random IV is generated for every call via openssl::rand::rand_bytes.

use synta_certificate::{
    CmsDecryptor as _, CmsEncryptor as _,
    OpensslDecryptor, OpensslEncryptor,
    cms_rfc5652_types::EncryptedData,
    pkcs7_types, pkcs12_types,
};
use synta::{Decoder, Encoding};
use synta::traits::Encode as _;

let key: [u8; 16] = *b"0123456789abcdef";
let plaintext = b"secret payload";

// Encrypt → DER-encoded EncryptedData SEQUENCE
let der = OpensslEncryptor
    .create_encrypted_data(
        pkcs7_types::ID_DATA,         // content type (id-data)
        pkcs12_types::ID_AES128_CBC,  // cipher (AES-128-CBC, 16-byte key)
        plaintext,
        &key,
    )
    .expect("encryption failed");

// Decrypt: decode the EncryptedData, re-encode AlgorithmIdentifier to
// DER bytes, then call OpensslDecryptor.decrypt.
let mut decoder = Decoder::new(&der, Encoding::Der);
let ed: EncryptedData<'_> = decoder.decode().unwrap();

let mut alg_enc = synta::Encoder::new(Encoding::Der);
ed.encrypted_content_info
    .content_encryption_algorithm
    .encode(&mut alg_enc)
    .unwrap();
let alg_der = alg_enc.finish().unwrap();

let ciphertext = ed.encrypted_content_info.encrypted_content
    .as_ref()
    .unwrap()
    .as_bytes();

let recovered = OpensslDecryptor
    .decrypt(&alg_der, ciphertext, &key)
    .expect("decryption failed");
assert_eq!(&recovered, plaintext as &[u8]);

The Encryptor trait can also be used directly, returning (alg_id_der, ciphertext) without the EncryptedData wrapper — useful when building custom CMS structures:

use synta_certificate::{Encryptor as _, OpensslEncryptor, pkcs12_types};

let key = [0u8; 32];
let (alg_id_der, ciphertext) = OpensslEncryptor
    .encrypt(pkcs12_types::ID_AES256_CBC, b"data", &key)
    .expect("encryption failed");
// alg_id_der: DER AlgorithmIdentifier with embedded random IV
// ciphertext: raw AES-256-CBC ciphertext bytes
let _ = (alg_id_der, ciphertext);

Certificate Structure

Core Types

Type Description
Certificate<'a> Top-level X.509 certificate (lifetime bound to input buffer)
TBSCertificate<'a> To-be-signed certificate data
AlgorithmIdentifier<'a> Algorithm OID with optional parameters
SubjectPublicKeyInfo<'a> Public key algorithm and key bits
PublicKeyInfo Decoded public key — Rsa { modulus, exponent, bit_count }, Ec { ... }, or Unknown
Validity notBefore / notAfter validity period
Time CHOICE of UTCTime or GeneralizedTime

Key Field Types

Field Rust type Notes
tbs_certificate.issuer RawDer<'a> Zero-copy; raw DER bytes of the Name SEQUENCE
tbs_certificate.subject RawDer<'a> Zero-copy; raw DER bytes of the Name SEQUENCE
tbs_certificate.extensions Option<RawDer<'a>> Zero-copy; decoded lazily
subject_public_key_info.subject_public_key BitStringRef<'a> Zero-copy; borrowed key bits
tbs_certificate.serial_number Integer Eagerly decoded
tbs_certificate.version Option<Integer> Eagerly decoded
signature_algorithm.algorithm ObjectIdentifier Eagerly decoded

Type Aliases

Alias Underlying type RFC 5280 name
Version Integer Version
CertificateSerialNumber Integer CertificateSerialNumber
UniqueIdentifier BitString UniqueIdentifier

Supported Algorithms

Signature Algorithms

Algorithm OID prefix Standard
RSA (PKCS #1) 1.2.840.113549.1.1.* RFC 3279
ECDSA 1.2.840.10045.4.* RFC 5480
EdDSA (Ed25519) 1.3.101.112 RFC 8410
EdDSA (Ed448) 1.3.101.113 RFC 8410
ML-DSA-44/65/87 2.16.840.1.101.3.4.3.17–19 FIPS 204
DSA 1.2.840.10040.4.* FIPS 186

Public Key Algorithms

Algorithm Standard
RSA RFC 3279
ECDSA RFC 5480
EdDSA (Ed25519, Ed448) RFC 8410
ML-DSA (44, 65, 87) FIPS 204
ML-KEM (512, 768, 1024) FIPS 203
DSA FIPS 186

Cargo Features

Feature Default Description
std yes Standard library support
alloc no Allocation support for no_std environments
derive yes Enable derive macros for Encode/Decode traits
serde no Derive Serialize/Deserialize on generated types
openssl no OpenSSL-backed PKCS#12 decryption and CMS encryption/decryption; enables OpensslDecryptor and OpensslEncryptor
deprecated-pkcs12-algorithms no Support legacy 3DES and RC2 PKCS#12 encryption; requires openssl

no_std Usage

[dependencies]
synta-certificate = { version = "0.1", default-features = false, features = ["alloc"] }

Code Generation

All ASN.1 types are auto-generated during build time from formal schemas in the workspace asn1/ directory using synta-codegen. To regenerate:

cargo build -p synta-certificate
Schema file Generated module Standard
asn1/X509-Certificate.asn1 crate root (Certificate, TBSCertificate, …) RFC 5280 §4
asn1/X509-CRL.asn1 crl (CertificateList, TBSCertList, …) RFC 5280 §5
asn1/PKCS10-CSR.asn1 csr (CertificationRequest, …) RFC 2986
asn1/OCSP.asn1 ocsp (OCSPResponse, BasicOCSPResponse, …) RFC 6960
asn1/PKCS7-CMS.asn1 pkcs7_types RFC 5652
asn1/PKCS12.asn1 pkcs12_types RFC 7292
asn1/PKCS8.asn1 pkcs8_types (OneAsymmetricKey, PrivateKeyInfo) RFC 5958
asn1/PKCS9.asn1 pkcs9_types (OID constants for PKCS#9 attributes) RFC 2985, RFC 5652 §11

The build.rs post-processor applies additional patches (e.g. StringTypeMode::Borrowed for zero-copy types, extensions pattern fixup) after synta-codegen runs.

License

Licensed under either of:

at your option.

References

  • RFC 5280 — Internet X.509 PKI Certificate and CRL Profile
  • RFC 2986 — PKCS#10: Certification Request Syntax Specification
  • RFC 6960 — Online Certificate Status Protocol (OCSP)
  • RFC 5652 — Cryptographic Message Syntax (CMS / PKCS#7)
  • RFC 7292 — PKCS#12: Personal Information Exchange Syntax
  • RFC 7468 — Textual Encodings of PKIX Structures (PEM)
  • RFC 8410 — Algorithm Identifiers for Ed25519 and Ed448
  • FIPS 204 — ML-DSA (Module-Lattice Digital Signature Standard)
  • FIPS 203 — ML-KEM (Module-Lattice Key-Encapsulation Mechanism Standard)