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.
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.rootsortls.Options.pins.
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:
| Code | Meaning |
|---|---|
BAD_CERTIFICATE | The bytes are not a valid certificate. |
UNSUPPORTED_ALGORITHM | A signature or key this module cannot check. |
EXPIRED | The certificate’s end date has passed. |
NOT_YET_VALID | The certificate’s start date has not arrived. |
UNTRUSTED_ROOT | No chain leads to an authority you trust. |
HOSTNAME_MISMATCH | The certificate is valid, but for a different server. |
CONSTRAINT_VIOLATION | A certificate was used for something it is not allowed to do. |
BAD_SIGNATURE | A 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:
| Field | Type | Description |
|---|---|---|
subject, issuer | String | Who the certificate is for, and who signed it. |
serialNumber | String | The serial number, in lower-case hex. |
notBefore, notAfter | DateTime | The dates it is valid between. |
dnsNames, ipAddresses, emailAddresses | String[] | The names it is valid for. |
isCA, maxPathLen | bool, int | Whether it may sign other certificates, and how long a chain below it may be (-1 for no limit). |
keyUsage | int | What its key may be used for; test it with cert.hasKeyUsage(bit). |
extKeyUsage | String[] | Further uses, as identifiers such as "1.3.6.1.5.5.7.3.1" (server authentication). |
signatureAlgorithm, publicKeyAlgorithm | String | Identifiers for how it was signed and what kind of key it holds. |
rsaModulus, rsaExponent | byte[], int | The key, when it is an RSA key. |
ecCurve, ecPoint | String, byte[] | The key, when it is an ECDSA key: "p256" or "p384", and the point. |
extensions | x509.Extension[] | Every extension, including ones this module does not interpret. |
unhandledCritical | String[] | Required extensions this module does not understand. |
nameConstraintsPresent, permittedSubtrees, excludedSubtrees, unsupportedConstraints | The name restrictions it places on certificates below it. See Name constraints. | |
raw, rawTbs, rawIssuer, rawSubject, rawSpki | byte[] | 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:
| Field | Type | When left out |
|---|---|---|
dnsName | String? | The host name is not checked. A tool inspecting a chain may want that; a client never does. |
roots | x509.Certificate[]? | The authorities your system trusts are used (x509.systemRoots()). |
at | DateTime? | 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 certificate | Matches | Does not match |
|---|---|---|
*.example.com | www.example.com | example.com, a.b.example.com |
*.com | nothing: too broad | |
x*.example.com | nothing: 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:
| Kind | A restriction covers |
|---|---|
| Host names | the name itself and every name below it, compared label by label, so notexample.com is not inside example.com |
| Email addresses | one 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 addresses | addresses 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 -sha256pinned 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");
}