Skip to Content

X509

Introduction

import 'x509';

The x509 module answers one question: does this certificate really belong to the server I meant to reach? It reads certificates, builds a chain from the server’s certificate up to an authority you trust, checks every signature along the way, and checks that the certificate is valid for the host name.

tls.connect does all of this for you, so most programs never import this module. It is here for programs that want to look inside a certificate, trust a private certificate authority, or check a chain they received some other way.

import 'x509'; import 'tls'; import 'time'; import 'string'; void async inspect() { tls.Conn c = await tls.connect("example.com:443", "example.com", null) catch e { return; }; x509.Certificate leaf = x509.parseCertificate(c.peerCertificates[0]) catch e { return; }; print(leaf.subject); // CN=example.com print(string.join(leaf.dnsNames, ", ")); // example.com, *.example.com print(time.date.toText(leaf.notAfter)); // 2026-10-27T22:17:20Z await c.close(); }

Notes

Every chain is checked for all of these:

  • every signature, from the server’s certificate up to the root, is made by the certificate above it;
  • every certificate is inside its validity dates;
  • every certificate that signs another is allowed to, and the chain is no longer than the signers allow;
  • every certificate is within the names its signers are restricted to — see Name constraints;
  • the server’s certificate is valid for the host name you asked for;
  • no certificate carries a required (“critical”) extension this module does not understand.

Signatures made with RSA (SHA-256 or SHA-384) and ECDSA (on the P-256 and P-384 curves) are supported, in any mix — real chains often combine both. Anything else, including signatures made with the broken SHA-1 and MD5 hashes, is refused rather than trusted unchecked.

Warning

Revocation is not checked. A certificate that its authority withdrew before it expired is still accepted. Go, Node.js and Python’s ssl module all behave the same way by default, for a good reason: an attacker able to use a withdrawn certificate is usually also able to block the revocation check, and the client then carries on anyway. Browsers replaced revocation checks with lists they download separately. Two things protect you better:

  • Short certificate lifetimes, which the industry is moving to, shrink the window that revocation was meant to cover.
  • Narrowing trust with tls.Options.roots or tls.Options.pins.
Note

This module checks certificates; it cannot create or sign them. There is no signing anywhere in the standard library, which is also why there is no TLS server.

Failures carry an x509.ErrorCode, so a program can tell the user exactly what is wrong. These codes pass unchanged out of tls.connect and http.request:

CodeMeaning
BAD_CERTIFICATEThe bytes are not a valid certificate.
UNSUPPORTED_ALGORITHMA signature or key this module cannot check.
EXPIREDThe certificate’s end date has passed.
NOT_YET_VALIDThe certificate’s start date has not arrived.
UNTRUSTED_ROOTNo chain leads to an authority you trust.
HOSTNAME_MISMATCHThe certificate is valid, but for a different server.
CONSTRAINT_VIOLATIONA certificate was used for something it is not allowed to do.
BAD_SIGNATUREA signature does not match.

This module reads data sent by strangers, so it is deliberately strict: a malformed or unusually encoded certificate is refused rather than guessed at, and every size and search is limited, so a hostile certificate cannot crash your program or keep it busy.

x509.parseCertificate()

x509.Certificate x509.parseCertificate(byte[] data)

Reads one certificate in DER form (the binary form; for the text form with -----BEGIN CERTIFICATE----- lines, use x509.parsePem() first). It fails with BAD_CERTIFICATE if the data is not a valid certificate.

The result has these fields:

FieldTypeDescription
subject, issuerStringWho the certificate is for, and who signed it.
serialNumberStringThe serial number, in lower-case hex.
notBefore, notAfterDateTimeThe dates it is valid between.
dnsNames, ipAddresses, emailAddressesString[]The names it is valid for.
isCA, maxPathLenbool, intWhether it may sign other certificates, and how long a chain below it may be (-1 for no limit).
keyUsageintWhat its key may be used for; test it with cert.hasKeyUsage(bit).
extKeyUsageString[]Further uses, as identifiers such as "1.3.6.1.5.5.7.3.1" (server authentication).
signatureAlgorithm, publicKeyAlgorithmStringIdentifiers for how it was signed and what kind of key it holds.
rsaModulus, rsaExponentbyte[], intThe key, when it is an RSA key.
ecCurve, ecPointString, byte[]The key, when it is an ECDSA key: "p256" or "p384", and the point.
extensionsx509.Extension[]Every extension, including ones this module does not interpret.
unhandledCriticalString[]Required extensions this module does not understand.
nameConstraintsPresent, permittedSubtrees, excludedSubtrees, unsupportedConstraintsThe name restrictions it places on certificates below it. See Name constraints.
raw, rawTbs, rawIssuer, rawSubject, rawSpkibyte[]The original bytes of the certificate and of its parts.
import 'x509'; import 'fs'; void describe(String file) { x509.Certificate cert = x509.parseCertificate(fs.readFile(file)) catch e { print("not a certificate: " + e.message); return; }; print(cert.subject); print(cert.isCA); }

x509.verify() and x509.verifyChain()

x509.Certificate[] x509.verify(x509.Certificate[] chain, x509.Options? options) x509.Certificate[] x509.verifyChain(byte[][] ders, String? dnsName, x509.Certificate[]? roots, DateTime? at)

Both check a chain, given with the server’s own certificate first, and return the chain they built, ending at the authority that vouches for it. verifyChain takes the raw certificates and does everything in one call, which is what tls uses; verify takes certificates you have already parsed.

The certificates a server sends are only suggestions: the returned chain is the one this module put together and checked, and it may differ from what was sent.

x509.Options holds the three settings, all optional:

FieldTypeWhen left out
dnsNameString?The host name is not checked. A tool inspecting a chain may want that; a client never does.
rootsx509.Certificate[]?The authorities your system trusts are used (x509.systemRoots()).
atDateTime?Validity is judged as of now.
import 'x509'; void check(byte[][] sent) { x509.Certificate[] chain = x509.verifyChain(sent, "example.com", null, null) catch e { print("not trusted: " + e.message); return; }; print(chain[0].subject); // the server's certificate print(chain[chain.length - 1].subject); // the authority that vouches for it }

x509.matchHostname()

void x509.matchHostname(x509.Certificate cert, String host)

Checks that cert is valid for host, and fails with HOSTNAME_MISMATCH if it is not. Only the certificate’s list of names (dnsNames and ipAddresses) is used; the old “common name” in the subject is ignored, as every major browser does today.

Upper and lower case do not matter, and a trailing dot is allowed. Wildcards are limited on purpose:

Name in the certificateMatchesDoes not match
*.example.comwww.example.comexample.com, a.b.example.com
*.comnothing: too broad
x*.example.comnothing: a wildcard must be a whole label

An IP address matches only the certificate’s ipAddresses, never a wildcard name.

Name constraints

An authority’s certificate can restrict which names the certificates below it may use — for example, a company authority that may only issue certificates under internal.example.com. This module enforces those restrictions for the three kinds of name it reads:

KindA restriction covers
Host namesthe name itself and every name below it, compared label by label, so notexample.com is not inside example.com
Email addressesone whole address; or a host, meaning addresses at exactly that host; or a domain written with a leading dot, meaning addresses at hosts below it
IP addressesaddresses in the given range

Four rules decide the outcome:

  • Exclusions win. A name inside an excluded range is refused, whatever the permitted list says.
  • A restriction only affects its own kind of name. A certificate with no email addresses is not affected by email restrictions.
  • An authority re-issuing its own certificate is not held to its own new restriction, so it can add one.
  • A restriction this module cannot check blocks the authority entirely. It is listed in cert.unsupportedConstraints, and nothing that authority signed is accepted, because nothing can be shown to be allowed.

You can read the restrictions yourself too:

import 'x509'; void showLimits(x509.Certificate ca) { if (ca.nameConstraintsPresent) { forEach(ca.permittedSubtrees, sub) { if (sub.kind == x509.GN_DNS) { print("permits " + sub.name); } } } }

x509.spkiPin() and x509.pinned()

byte[] x509.spkiPin(x509.Certificate cert) bool x509.pinned(x509.Certificate[] chain, byte[][] pins)

A pin is the SHA-256 hash of a certificate’s public key. Because it covers only the key, a certificate renewed with the same key keeps its pin. spkiPin computes one; it gives the same 32 bytes as this openssl command:

openssl x509 -in server.der -inform DER -pubkey -noout \ | openssl pkey -pubin -outform DER | openssl dgst -sha256

pinned reports whether any certificate in a chain has one of the pinned keys. Use it only on a chain that has already been verified: a pin narrows which trusted chains you accept, and does not make a chain trusted. An empty list of pins matches nothing.

import 'x509'; void checkPin(byte[][] sent, byte[][] myPins) { x509.Certificate[] chain = x509.verifyChain(sent, "example.com", null, null) catch e { return; }; if (!x509.pinned(chain, myPins)) { print("trusted, but not a key I pinned"); } }

For connections, tls.Options.pins does this check during the connection, which is where you usually want it.

x509.parsePem()

byte[][] x509.parsePem(String text)

Reads every certificate out of a PEM file — the text form, with -----BEGIN CERTIFICATE----- and -----END CERTIFICATE----- lines — and returns each one’s bytes, ready for parseCertificate. Text between the blocks, and blocks of other kinds such as keys, are skipped. A block that never ends, or whose contents are not valid base64, is an error rather than being quietly skipped.

import 'x509'; import 'fs'; import 'string'; void loadBundle(String file) { byte[][] ders = x509.parsePem(string.fromByteArray(fs.readFile(file) catch [])) catch e { return; }; print(ders.length); }

x509.systemRoots()

x509.Certificate[] x509.systemRoots()

Returns the certificate authorities your system trusts, read from the usual places on macOS and Linux (such as /etc/ssl/cert.pem). It fails rather than returning an empty list, because an empty list would quietly trust nobody. It does not work on Windows, where the trusted authorities are only available through a system interface rather than a file.

A single certificate in the system file that cannot be read is skipped, so the rest still load.

x509.checkSignature()

void x509.checkSignature(x509.Certificate child, x509.Certificate parent)

Checks that child was signed with parent’s key. It fails with BAD_SIGNATURE if it was not, and with UNSUPPORTED_ALGORITHM when the signature is of a kind this module cannot check. verify does this for every link of a chain; this function checks one link on its own.

x509.rsaPublicKey(), x509.ecdsaPublicKey() and x509.parseEcdsaSignature()

x509.RsaPublicKey x509.rsaPublicKey(x509.Certificate cert) x509.EcdsaPublicKey x509.ecdsaPublicKey(x509.Certificate cert) x509.EcdsaSignature x509.parseEcdsaSignature(byte[] data)

For checking a signature that is not a certificate’s own — for example one a server makes during a TLS handshake. The two key functions return a certificate’s key in the form the crypto verification functions take, and each fails if the certificate holds the other kind of key. parseEcdsaSignature splits an encoded ECDSA signature into the two numbers crypto.ecdsaVerify expects.

import 'x509'; import 'crypto'; void checkSigned(x509.Certificate cert, byte[] message, byte[] signature) { x509.EcdsaPublicKey key = x509.ecdsaPublicKey(cert) catch e { return; }; x509.EcdsaSignature sig = x509.parseEcdsaSignature(signature) catch e { return; }; crypto.ecdsaVerify(key.curve, key.point, crypto.sha256(message), sig.r, sig.s) catch e { print("not signed by that key"); return; }; print("signed"); }