Security

X.509 certificates

Read X.509 certificates: subject, issuer, the names they cover, validity dates, days left and SHA-256 fingerprint, for expiry alerts.

Version 1.0.0 · 3 functions · 2 crates

#certificates#tls#expiry#monitoring

Functions

What use x509.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use x509_read, x509_read_all from x509 (Quillo 0.6).

x509_read(pem: str) → anyor an error

The first certificate in pem: { subject, common_name, issuer, issuer_common_name, names, not_before, not_after, days_left, valid_now, self_signed, is_ca, serial, sha256_fingerprint, key_type }. names are the subject alternative names (DNS names, IPs, email: and uri: ones); times are UTC ISO 8601; days_left is negative once it has expired. Nothing checks who signed it.

x509_read_all(pem: str) → listor an error

Every certificate in pem (a chain, leaf first, or a CA bundle), each as x509_read gives it; at most 500.

x509_covers(pem: str, host: str) → boolor an error

Whether the first certificate names host among its DNS names or IPs, a *. wildcard covering one label (*.example.com covers www.example.com, not example.com or a.b.example.com). Case does not matter.

Get it

$ quillo install x509               # x509.qwer into this folder$ quillo install x509 --example     # and its example, x509.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/x509.qwer$ curl -O https://quillolang.org/plugins/files/x509.qwe$ curl -O https://quillolang.org/plugins/files/x509_sample.pem

Then, in your program, all of it or only what you call:

main.qwe
use x509.qweruse x509_read, x509_read_all from x509      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]
x509-parser = "0.16"sha2 = "0.10"

Example

x509.qwe
// A certificate report: what a partner's certificate covers, and how long it has left.////   quillo run x509.qwe      then open http://localhost:8080/certificateuse "x509.qwer"let CHAIN = read_file("x509_sample.pem")?route GET /certificate {    let c = x509_read(CHAIN)?    { for: c.common_name, from: c.issuer_common_name, names: c.names, until: c.not_after, days_left: c.days_left, fingerprint: c.sha256_fingerprint }}route GET /chain = x509_read_all(CHAIN)? | map((c) => { name: c.common_name, ca: c.is_ca, self_signed: c.self_signed, key: c.key_type })route GET /covers ? { host: str = "www.shop.example.com" } = { host, covered: x509_covers(CHAIN, host)? }// An alert for certificates that end within 30 days.route GET /expiring = x509_read_all(CHAIN)? | filter((c) => c.days_left < 30) | map((c) => c.common_name)
$ quillo run x509.qwe$ curl 'localhost:8080/certificate'$ curl 'localhost:8080/chain'$ curl 'localhost:8080/covers?host=a.b.shop.example.com'

Source

x509.qwer
//! Read X.509 certificates (PEM): subject, issuer, the names it covers, when it starts and ends, days left, fingerprint; for expiry alerts and checking what a partner sent.//! version: 1.0.0//!//! [dependencies]//! x509-parser = "0.16"//! sha2 = "0.10"use serde_json::{json, Value};use sha2::{Digest, Sha256};use x509_parser::extensions::GeneralName;use x509_parser::pem::Pem;use x509_parser::prelude::X509Certificate;fn now() -> i64 {    std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_secs() as i64)}/// Unix seconds as `2026-11-02T09:00:00Z`.fn iso(t: i64) -> String {    let (days, secs) = (t.div_euclid(86400), t.rem_euclid(86400));    // Howard Hinnant's civil_from_days.    let z = days + 719468;    let era = z.div_euclid(146097);    let doe = z - era * 146097;    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);    let mp = (5 * doy + 2) / 153;    let d = doy - (153 * mp + 2) / 5 + 1;    let m = if mp < 10 { mp + 3 } else { mp - 9 };    let y = yoe + era * 400 + if m <= 2 { 1 } else { 0 };    format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", y, m, d, secs / 3600, secs % 3600 / 60, secs % 60)}fn names(cert: &X509Certificate) -> Vec<String> {    let mut out = vec![];    if let Ok(Some(san)) = cert.subject_alternative_name() {        for n in &san.value.general_names {            match n {                GeneralName::DNSName(s) => out.push(s.to_string()),                GeneralName::RFC822Name(s) => out.push(format!("email:{}", s)),                GeneralName::URI(s) => out.push(format!("uri:{}", s)),                GeneralName::IPAddress(b) if b.len() == 4 => out.push(std::net::Ipv4Addr::new(b[0], b[1], b[2], b[3]).to_string()),                GeneralName::IPAddress(b) if b.len() == 16 => {                    let a: [u8; 16] = (*b).try_into().unwrap_or([0; 16]);                    out.push(std::net::Ipv6Addr::from(a).to_string())                }                _ => {}            }        }    }    out}fn common_name(name: &x509_parser::x509::X509Name) -> Option<String> {    name.iter_common_name().next().and_then(|cn| cn.as_str().ok()).map(str::to_string)}fn describe(der: &[u8], cert: &X509Certificate) -> Value {    let v = cert.validity();    let (start, end) = (v.not_before.timestamp(), v.not_after.timestamp());    let fp: Vec<String> = Sha256::digest(der).iter().map(|b| format!("{:02X}", b)).collect();    json!({        "subject": cert.subject().to_string(),        "common_name": common_name(cert.subject()),        "issuer": cert.issuer().to_string(),        "issuer_common_name": common_name(cert.issuer()),        "names": names(cert),        "not_before": iso(start),        "not_after": iso(end),        "days_left": (end - now()).div_euclid(86400),        "valid_now": now() >= start && now() <= end,        "self_signed": cert.subject().as_raw() == cert.issuer().as_raw(),        "is_ca": cert.is_ca(),        "serial": cert.raw_serial_as_string(),        "sha256_fingerprint": fp.join(":"),        "key_type": match cert.public_key().algorithm.algorithm.to_id_string().as_str() {            "1.2.840.113549.1.1.1" => "RSA".to_string(),            "1.2.840.10045.2.1" => "EC".to_string(),            "1.3.101.112" => "Ed25519".to_string(),            other => other.to_string(),        },    })}fn each(pem_text: &str, max: usize) -> Result<Vec<Value>, String> {    if pem_text.len() > 4_000_000 {        return Err("PEM text is at most 4 MB".into());    }    let mut out = vec![];    for pem in Pem::iter_from_buffer(pem_text.as_bytes()).take(max) {        let pem = pem.map_err(|e| format!("not PEM: {}", e))?;        if pem.label != "CERTIFICATE" {            continue;        }        let cert = pem.parse_x509().map_err(|e| format!("not a certificate: {}", e))?;        out.push(describe(&pem.contents, &cert));    }    if out.is_empty() {        return Err("no `-----BEGIN CERTIFICATE-----` in the text".into());    }    Ok(out)}/// The first certificate in `pem`: `{ subject, common_name, issuer, issuer_common_name, names, not_before, not_after, days_left,/// valid_now, self_signed, is_ca, serial, sha256_fingerprint, key_type }`. `names` are the subject alternative names (DNS names, IPs,/// `email:` and `uri:` ones); times are UTC ISO 8601; `days_left` is negative once it has expired. Nothing checks who signed it.pub fn x509_read(pem: &str) -> Result<Value, String> {    Ok(each(pem, 1)?.remove(0))}/// Every certificate in `pem` (a chain, leaf first, or a CA bundle), each as x509_read gives it; at most 500.pub fn x509_read_all(pem: &str) -> Result<Vec<Value>, String> {    each(pem, 500)}/// Whether the first certificate names `host` among its DNS names or IPs, a `*.` wildcard covering one label (`*.example.com`/// covers `www.example.com`, not `example.com` or `a.b.example.com`). Case does not matter.pub fn x509_covers(pem: &str, host: &str) -> Result<bool, String> {    let c = x509_read(pem)?;    let host = host.trim().trim_end_matches('.').to_lowercase();    Ok(c["names"].as_array().into_iter().flatten().filter_map(|n| n.as_str()).any(|n| {        let n = n.to_lowercase();        match n.strip_prefix("*.") {            Some(rest) => host.split_once('.').is_some_and(|(first, tail)| !first.is_empty() && tail == rest),            None => n == host,        }    }))}
TLS certificate checks tls_expiry.qwer · 1.0.0A server's certificate chain fetched from host:port and checked as browsers do: trusted by Mozilla's roots or yours, in order, covering the name, and the days until the first certificate in it expires.tlscertificatesexpirymonitoring8 views · 6 downloadsACME certificates acme.qwer · 1.0.0Let's Encrypt's ACME v2 by hand: account, order, HTTP-01 and DNS-01 values, JWS with ES256, nonces kept and retried on badNonce, finalize with a CSR made here, the chain downloaded, its dates read, and revocation.tlscertificateslets-encrypthttps3 views · 7 downloadsCertificate Transparency ct_logs.qwer · 1.0.0Read the SCTs in a certificate and check them against each log's key, ask a log for tree heads, entries and proofs, and verify Merkle audit and consistency proofs, so a certificate is shown to be logged and a log not to have rewritten history.certificatestlsmerkleaudit5 views · 7 downloadsOCSP ocsp.qwer · 1.0.0Ask whether a certificate is still good: URLs read from the certificate, OCSP requests built with a nonce, responses read with status, reason and times, and the signature checked against the issuer or a delegated responder it authorized.certificatestlsrevocation3 views · 8 downloadsPKCS#12 files pkcs12.qwer · 1.0.0Read .p12 and .pfx files in pure Rust, key and chain out in order, MAC and password checked, modern PBES2 with AES, and write ones OpenSSL opens. The old RC2, RC4 and 3DES ciphers are refused by name, with how to convert the file.certificateskeystlsfiles2 views · 8 downloadsSSH certificates ssh_certs.qwer · 1.0.0Read OpenSSH certificates with their principals, validity, critical options and CA, check them as sshd would, and sign new ones with a CA key, for short-lived access. Differs from ssh_keys, which reads plain keys.sshcertificateskeysauthentication4 views · 6 downloads
Thread
76Useful? Like it, or add to its thread below.

Thread 0 posts

On X.509 certificates: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.