Short answer. A TLS client identifies a server only by the Subject Alternative Name: the dNSName and iPAddress entries, never the subject's Common Name. Key Usage says what the key may do mathematically (sign, encrypt, sign certificates), Extended Key Usage says for which application (serverAuth, clientAuth, code signing). Basic Constraints CA:TRUE is what allows a certificate to issue others; a leaf has CA:FALSE. Authority and Subject Key Identifiers are hints that help a client find the issuer, not security checks. CRL Distribution Points and Authority Information Access tell a client where to check revocation and fetch a missing intermediate; Let's Encrypt certificates no longer carry an OCSP URL. SCTs prove the certificate was logged in Certificate Transparency, which Chrome and Safari require.
Getting a real certificate to look at
openssl s_client -showcerts prints every certificate the server sends; openssl x509 -text decodes one. The lab ran in debian:forky-20261005-slim with OpenSSL 3.6.5 and pyca/cryptography 49.0.0:
openssl s_client -connect letsencrypt.org:443 -servername letsencrypt.org -showcerts </dev/null 2>/dev/null \
| awk '/-----BEGIN CERTIFICATE-----/{n++; f=sprintf("letsencrypt.org-%d.pem", n); p=1} p{print >> f} /-----END CERTIFICATE-----/{p=0; close(f)}'
openssl x509 -in letsencrypt.org-1.pem -noout -text
-servername sends SNI, without which many servers return a default certificate for another name. The leaf certificate of letsencrypt.org, as served that day (the public key is shown in full; the two SCT signatures are shortened):
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
05:43:93:3b:e3:86:b3:a2:b3:ad:d5:34:f2:10:3b:c8:b6:5f
Signature Algorithm: ecdsa-with-SHA384
Issuer: C=US, O=Let's Encrypt, CN=YE2
Validity
Not Before: Sep 4 14:34:32 2026 GMT
Not After : Dec 3 14:34:31 2026 GMT
Subject: CN=letsencrypt.org
Subject Public Key Info:
Public Key Algorithm: id-ecPublicKey
Public-Key: (256 bit)
pub:
04:e9:e5:40:7b:43:1e:46:b9:8e:45:e4:aa:cb:40:
ab:e6:3f:99:27:b8:b7:81:e3:32:56:e7:14:74:5f:
fa:43:5b:52:c6:a6:b5:66:c5:56:0e:07:21:56:e3:
4c:fe:35:55:4f:f0:92:2f:eb:22:4a:f1:7a:39:51:
39:04:c2:b3:0d
ASN1 OID: prime256v1
NIST CURVE: P-256
X509v3 extensions:
X509v3 Key Usage: critical
Digital Signature
X509v3 Extended Key Usage:
TLS Web Server Authentication
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Subject Key Identifier:
16:68:8D:1E:2A:32:48:8C:B5:F1:10:0A:16:08:37:08:3B:9D:30:23
X509v3 Authority Key Identifier:
B9:59:F2:8E:CF:22:F0:86:D3:37:48:FF:76:14:18:BA:82:D8:55:87
Authority Information Access:
CA Issuers - URI:http://ye2.i.lencr.org/
X509v3 Subject Alternative Name:
DNS:cp.letsencrypt.org, DNS:cp.root-x1.letsencrypt.org, DNS:cps.letsencrypt.org, DNS:cps.root-x1.letsencrypt.org, DNS:lencr.org, DNS:letsencrypt.com, DNS:letsencrypt.org, DNS:www.lencr.org, DNS:www.letsencrypt.com, DNS:www.letsencrypt.org
X509v3 Certificate Policies:
Policy: 2.23.140.1.2.1
X509v3 CRL Distribution Points:
Full Name:
URI:http://ye2.c.lencr.org/96.crl
CT Precertificate SCTs:
Signed Certificate Timestamp:
Version : v1 (0x0)
Log ID : D8:09:55:3B:94:4F:7A:FF:C8:16:19:6F:94:4F:85:AB:
B0:F8:FC:5E:87:55:26:0F:15:D1:2E:72:BB:45:4B:14
Timestamp : Sep 4 15:33:02.377 2026 GMT
Extensions: none
Signature : ecdsa-with-SHA256
30:46:02:21:00:D0:2E:25:A4:9D:7F:C8:18:D6:7B:5E:...
Signed Certificate Timestamp:
Version : v1 (0x0)
Log ID : 46:AF:86:3D:3B:3E:E5:9F:A5:77:DE:A8:24:5D:36:B0:
D9:ED:22:A2:23:F4:61:77:41:22:94:52:EE:95:50:5F
Timestamp : Sep 4 15:33:02.552 2026 GMT
Extensions: 00:00:05:00:24:DF:DB:0C
Signature : ecdsa-with-SHA256
30:44:02:20:31:2F:83:D6:42:65:26:0D:58:D7:DD:C3:...
Signature Algorithm: ecdsa-with-SHA384
Signature Value:
30:65:02:31:00:ee:1d:b0:7c:35:88:98:15:57:b8:88:73:23:
92:a9:bf:40:bf:e8:ae:02:48:e6:f4:7a:2d:18:fc:43:52:d3:
80:9a:ff:ce:e0:2a:0d:4a:29:e6:80:a2:94:f5:4f:14:d9:02:
30:35:08:99:3b:bc:89:5b:23:a0:ef:64:24:e2:b2:b3:b8:c2:
d8:d5:95:45:de:f3:03:e8:e7:82:78:a5:9e:c2:1f:ad:eb:14:
fc:39:51:c7:e1:18:e9:16:b4:a5:48:de:09
The rest of this page walks through it top to bottom. The structure itself is defined in RFC 5280 (§4.1 for the fields, §4.2 for the extensions); what a publicly trusted TLS certificate may contain is in the CA/Browser Forum Baseline Requirements (BR), version 2.3.1 of 4 October 2026, §7.1.
Version, serial, signature algorithm, validity
- Version 3 is the only version that has extensions; the BR require it.
- Serial number. Unique per issuer and, for public CAs, "a non-sequential number greater than zero (0) and less than 2¹⁵⁹ containing at least 64 bits of output from a CSPRNG" (BR §7.1.2.7). Randomness here is a defense against chosen-prefix collision attacks on the signature hash; it is why serials look like noise. A serial is what CRLs list.
- Signature Algorithm appears twice, inside the signed part and next to the signature, and the two must be byte-identical.
ecdsa-with-SHA384here tells you about the issuer's key (YE2 is a P-384 key), not the subject's. The signature value at the end is an ECDSA signature in DER, the format explained in ECDSA signature formats. - Validity.
notBeforeandnotAfterare inclusive. This certificate lives 90 days; the BR maximum is 200 days for certificates issued since 15 March 2026, 100 days from 15 March 2027 and 47 days from 15 March 2029 (BR §6.3.2). Let's Encrypt already issues 45-day certificates on its opt-intlsserverprofile and plans to move its default to 64 days on 10 February 2027 and 45 days on 16 February 2028.
Subject and Subject Alternative Name
The subject is a distinguished name: here only CN=letsencrypt.org. Domain-validated certificates carry nothing else, because the CA validated nothing else. The names a client checks live in the Subject Alternative Name (SAN): ten DNS: entries for this certificate.
The Common Name stopped counting a long time ago. RFC 9525 (2023), which replaced RFC 6125, says "The Common Name RDN MUST NOT be used to identify a service". Chrome stopped matching it in version 58 (2017), and Go refuses certificates that only have a CN with x509: certificate relies on legacy Common Name field. The BR keep the CN only as a copy: if present, it "MUST contain exactly one entry that is one of the values contained in the Certificate's subjectAltName extension" (§7.1.4.3).
A SAN can also hold IP addresses. The certificate of Cloudflare's resolver:
subject=C=US, ST=California, L=San Francisco, O=Cloudflare, Inc., CN=cloudflare-dns.com
X509v3 Subject Alternative Name:
DNS:cloudflare-dns.com, DNS:*.cloudflare-dns.com, IP Address:1.0.0.1, IP Address:1.1.1.1, IP Address:162.159.36.1, IP Address:162.159.46.1, IP Address:2606:4700:4700:0:0:0:0:1001, IP Address:2606:4700:4700:0:0:0:0:1111, IP Address:2606:4700:4700:0:0:0:0:64, IP Address:2606:4700:4700:0:0:0:0:6400, DNS:one.one.one.one
https://1.1.1.1 works because 1.1.1.1 is an iPAddress entry; a DNS:1.1.1.1 entry would not match an IP address, which is what Go's cannot validate certificate for … because it doesn't contain any IP SANs is about. The subject here also carries organization and locality, and the certificate asserts the OV policy 2.23.140.1.2.2 (see DV vs OV vs EV); the browser shows none of it. It was issued on 31 December 2025 for 355 days, before the 200-day limit took effect.
A wildcard such as *.cloudflare-dns.com matches exactly one label: a.cloudflare-dns.com, but not cloudflare-dns.com itself and not a.b.cloudflare-dns.com (RFC 9525 §6.3). OpenSSL checks names the same way:
Hostname www.lencr.org does match certificate
Hostname lencr.org.evil.test does NOT match certificate
Hostname mail.letsencrypt.org does NOT match certificate
Subject Public Key Info
The subjectPublicKeyInfo (SPKI) holds the algorithm and the key: an uncompressed P-256 point (04 followed by 32-byte x and y). It is the only part of the certificate the subject's private key relates to, which is why public key pinning hashes the SPKI rather than the whole certificate: a renewal with the same key keeps the pin (see certificate pinning in 2026).
Key Usage and Extended Key Usage
Two different questions:
- Key Usage (RFC 5280 §4.2.1.3) restricts the key's cryptographic operations:
digitalSignature,keyEncipherment,keyAgreement,keyCertSign,cRLSignand a few rarer bits. For an ECDSA leaf the BR requiredigitalSignatureand forbidkeyEncipherment(an EC key cannot encrypt). An RSA leaf may addkeyEnciphermentfor old TLS 1.2 RSA key exchange, which the BR call support for "insecure ciphersuites". The BR also say the extension SHOULD be present and marked critical, as here. - Extended Key Usage (§4.2.1.12) names applications:
serverAuth(1.3.6.1.5.5.7.3.1),clientAuth(…3.2),codeSigning(…3.3),emailProtection(…3.4),timeStamping(…3.8). A TLS server certificate MUST containserverAuth, MUST NOT contain code signing, email, time stamping, OCSP signing oranyExtendedKeyUsage(BR §7.1.2.7.10).
clientAuth is the part that changed. The BR still say MAY, but the Chrome Root Program Policy 1.8 requires hierarchies "dedicated to TLS server authentication": subordinate CAs disclosed from 15 June 2026 may assert only serverAuth, and subscriber certificates issued from 15 March 2027 must too. Let's Encrypt removed clientAuth from its default profile on 11 February 2026 and stopped issuing it entirely on 8 July 2026; the letsencrypt.org leaf above has serverAuth only. If your service authenticated clients with public TLS certificates, it needs a private CA (see mTLS explained).
The EKU is enforced by verifiers. With a throwaway CA, a certificate whose EKU is clientAuth only:
CN=worker-7
error 26 at 0 depth lookup: unsuitable certificate purpose
error worker-7.pem: verification failed
worker-7.pem: OK
That is OpenSSL's error 26; Go reports the same condition as x509: certificate specifies an incompatible key usage.
Basic Constraints: who may issue certificates
Basic Constraints carries one boolean and one number. CA:TRUE allows the certificate's key to sign other certificates; pathlen limits how many intermediate CAs may follow below it. RFC 5280 §4.2.1.9 is explicit that without cA asserted, "the certified public key MUST NOT be used to verify certificate signatures". Up the letsencrypt.org chain:
| Certificate | Basic Constraints | Key Usage |
|---|---|---|
CN=letsencrypt.org | critical, CA:FALSE | Digital Signature |
CN=YE2 (intermediate) | critical, CA:TRUE, pathlen:0 | Digital Signature, Certificate Sign, CRL Sign |
CN=Root YE (cross-signed by ISRG Root X2) | critical, CA:TRUE | Certificate Sign, CRL Sign |
pathlen:0 on YE2 means it can issue leaf certificates only. To see enforcement, the lab used the key of an ordinary server certificate (CA:FALSE, Key Usage digitalSignature) to sign another certificate:
CN=api.example.test
error 79 at 1 depth lookup: invalid CA certificate
CN=api.example.test
error 32 at 1 depth lookup: key usage does not include certificate signing
error forged.example.test.pem: verification failed
Both rules fire: error 79 for the missing CA:TRUE and error 32 for the missing keyCertSign. Skipping this check is how Internet Explorer accepted any leaf certificate as a CA in 2002. Python 3.13 and later also enable OpenSSL's strict mode, which additionally rejects CA certificates whose Basic Constraints are not marked critical.
Authority and Subject Key Identifiers
The Subject Key Identifier names the certificate's own key; the Authority Key Identifier names the key that signed it. Each AKI equals the SKI of the next certificate up:
subject=CN=letsencrypt.org
16:68:8D:1E:2A:32:48:8C:B5:F1:10:0A:16:08:37:08:3B:9D:30:23
B9:59:F2:8E:CF:22:F0:86:D3:37:48:FF:76:14:18:BA:82:D8:55:87
subject=C=US, O=Let's Encrypt, CN=YE2
B9:59:F2:8E:CF:22:F0:86:D3:37:48:FF:76:14:18:BA:82:D8:55:87
A3:C8:26:5A:8E:A1:4C:D0:35:63:FC:9B:23:C8:3A:AE:56:F3:4F:56
subject=C=US, O=ISRG, CN=Root YE
A3:C8:26:5A:8E:A1:4C:D0:35:63:FC:9B:23:C8:3A:AE:56:F3:4F:56
7C:42:96:AE:DE:4B:48:3B:FA:92:F8:9E:8C:CF:6D:8B:A9:72:37:95
subject=C=US, O=Internet Security Research Group, CN=ISRG Root X2
7C:42:96:AE:DE:4B:48:3B:FA:92:F8:9E:8C:CF:6D:8B:A9:72:37:95
79:B4:59:E6:7B:B6:E5:E4:01:73:80:08:88:C8:1A:58:F6:E9:9B:6E
Key identifiers help a verifier pick the right issuer when several CA certificates share a name (re-keyed or cross-signed CAs, such as the two ISRG Root X2 certificates in existence). They prove nothing: the signature check does. RFC 5280 requires the AKI in every certificate except self-signed roots and the SKI in every CA certificate; RFC 5280 suggests a SHA-1 hash of the key as one way to compute the SKI, but issuers may use any unique value. The BR make the leaf SKI "NOT RECOMMENDED"; both Let's Encrypt and Sectigo still include one. Strict verifiers fail on a CA certificate without an AKI, as Python's Missing Authority Key Identifier shows.
The chain also shows that the path you validate is not always the path the server sends. letsencrypt.org sends four certificates, ending with ISRG Root X2 cross-signed by ISRG Root X1; OpenSSL with Debian's trust store stops at the X2 root it already trusts:
letsencrypt.org-1.pem: OK
Chain:
depth=0: CN=letsencrypt.org (untrusted)
depth=1: C=US, O=Let's Encrypt, CN=YE2 (untrusted)
depth=2: C=US, O=ISRG, CN=Root YE (untrusted)
depth=3: C=US, O=Internet Security Research Group, CN=ISRG Root X2
CRL Distribution Points, AIA and OCSP
Two extensions point at URLs:
- CRL Distribution Points (RFC 5280 §4.2.1.13): where the issuer publishes its CRL. The BR require it unless the certificate is short-lived (7 days or less since 15 March 2026) or carries an OCSP URL, and the URL must be plain
http: the CRL is signed, so TLS adds nothing and would create a loop. - Authority Information Access (§4.2.2.1):
CA Issuersis where to download the issuing CA's certificate, andOCSPis the issuer's OCSP responder.
Let's Encrypt's leaf has a CRL URL and no OCSP URL: it dropped OCSP URLs from certificates in May 2025 and shut down its responders on 6 August 2025, after the BR made OCSP optional. Sectigo still runs OCSP; the github.com leaf has both access methods and two policies:
X509v3 Certificate Policies:
Policy: 1.3.6.1.4.1.6449.1.2.2.7
CPS: https://sectigo.com/CPS
Policy: 2.23.140.1.2.1
Authority Information Access:
CA Issuers - URI:http://crt.sectigo.com/SectigoPublicServerAuthenticationCADVE36.crt
OCSP - URI:http://ocsp.sectigo.com
The CRL behind the letsencrypt.org leaf, downloaded and checked against the YE2 certificate:
crl=$(openssl x509 -in letsencrypt.org-1.pem -noout -ext crlDistributionPoints | grep -o 'http://[^ ]*')
curl -sS -o leaf.crl "$crl"
openssl crl -inform DER -in leaf.crl -noout -issuer -lastupdate -nextupdate
openssl crl -inform DER -in leaf.crl -noout -text | grep -c 'Serial Number:'
openssl crl -inform DER -in leaf.crl -noout -CAfile letsencrypt.org-2.pem
issuer=C=US, O=Let's Encrypt, CN=YE2
lastUpdate=Oct 20 15:03:57 2026 GMT
nextUpdate=Oct 29 15:03:56 2026 GMT
1590
verify OK
The 96.crl in the URL is a shard: the CA splits its revocations over many small CRLs and names the right one in each certificate, so a client downloads 1,590 entries instead of every revocation. Browsers rarely fetch CRLs live; Chrome and Firefox push revocation data to clients themselves. The trade-offs are in CRL vs OCSP.
CA Issuers matters when a server forgets to send its intermediate. Browsers and the Windows and macOS platform verifiers follow it ("AIA chasing"); OpenSSL (and with it curl, Python and PHP on Linux), Go on Linux and Java with default settings do not, so the same misconfigured server works in a browser and fails in code with "unable to get local issuer certificate".
Certificate Policies
Every public TLS certificate asserts exactly one CA/Browser Forum policy identifier saying how the subject was validated: 2.23.140.1.2.1 domain validated, 2.23.140.1.2.2 organization validated, 2.23.140.1.2.3 individual validated, 2.23.140.1.1 extended validation (BR §7.1.6.1). CAs add their own policy OIDs and a link to their practice statement (CPS), like Sectigo above. Clients that display validation levels read this extension, not the subject.
Signed Certificate Timestamps
Before issuing, the CA submits a precertificate (the same content with a poison extension) to Certificate Transparency logs, and each log answers with a signed promise to publish it, the SCT. The CA embeds those SCTs in the final certificate (OID 1.3.6.1.4.1.11129.2.4.2, RFC 6962 §3.3). The log ID is a hash of the log's key; Chrome's log list maps it to a name:
"""List the SCTs embedded in a certificate and name their logs from Chrome's CT log list."""
import base64
import json
import sys
from cryptography import x509
log_list = json.load(open(sys.argv[1]))
logs = {}
for operator in log_list["operators"]:
for log in operator["logs"] + operator.get("tiled_logs", []):
logs[base64.b64decode(log["log_id"])] = (operator["name"], log["description"])
cert = x509.load_pem_x509_certificate(open(sys.argv[2], "rb").read())
scts = cert.extensions.get_extension_for_class(x509.PrecertificateSignedCertificateTimestamps).value
for sct in scts:
operator, name = logs.get(sct.log_id, ("unknown operator", "unknown log"))
print(f"{sct.timestamp:%Y-%m-%d %H:%M:%S} UTC {operator}: {name} ({sct.entry_type.name})")
2026-09-04 15:33:02 UTC Google: Google Xenon2026h2 (PRE_CERTIFICATE)
2026-09-04 15:33:02 UTC Geomys: Geomys Tuscolo2026h2 (PRE_CERTIFICATE)
2026-09-01 00:10:32 UTC Google: Google Argon2026h2 (PRE_CERTIFICATE)
2026-09-01 00:10:31 UTC Sectigo: Sectigo Tiger2026h2 (PRE_CERTIFICATE)
The log list was downloaded from https://www.gstatic.com/ct/log_list/v3/log_list.json the same day. Each certificate has two SCTs from two different operators, which is what Chrome's CT policy asks for a certificate valid 180 days or less (three for longer ones). The Extensions: 00:00:05:00:24:DF:DB:0C on the Tuscolo SCT is the leaf_index extension that logs implementing the static CT API must add: type 0, length 5, and a 40-bit index, here entry 618,650,380 of that log. The SCTs are why anyone can search all certificates issued for their domain, and why a misissued certificate cannot stay secret. The SCT decoder decodes them without a command line.
Common mistakes
- A certificate with the name only in the CN. Browsers report NET::ERR_CERT_COMMON_NAME_INVALID, Java says No subject alternative names present. Every name, including the one in the CN, goes into the SAN. This bites most often with internal CAs and
openssl reqone-liners that set only-subj. - Missing intermediate. Works in browsers and the Windows and macOS platform verifiers thanks to AIA chasing, fails in OpenSSL-based clients, Go on Linux and Java. Serve the full chain (leaf first, root optional).
- Relying on
clientAuthin a public certificate. It is disappearing from public TLS hierarchies; partner integrations that pinned "any public certificate withclientAuth" need a private CA. - A CA certificate made with
CA:TRUEbut withoutkeyCertSign, or without being critical. Strict verifiers (Python 3.13+, Go for some fields) reject it; Go's version is parent certificate cannot sign this kind of certificate. - Treating the SKI as a fingerprint. It is whatever the CA put there. To identify a certificate, hash the whole DER (
openssl x509 -fingerprint -sha256); to identify a key, hash the SPKI. - Reading the validity in local time.
notBeforeandnotAfterare UTC. A server clock a few hours behind rejects a freshly issued certificate as not yet valid.
Sources
- RFC 5280: §4.1 (certificate fields), §4.2.1.1–4.2.1.13 (AKI, SKI, Key Usage, SAN, Basic Constraints, EKU, CRL DP), §4.2.2.1 (AIA)
- RFC 9525: service identity in TLS (§2, §6.3)
- RFC 6962: Certificate Transparency (§3.3) and the static CT API
- CA/Browser Forum Baseline Requirements 2.3.1 (4 October 2026): §6.3.2, §7.1.2.7, §7.1.2.10, §7.1.2.11, §7.1.4.3, §7.1.6.1
- Chrome Root Program Policy 1.8 and Chrome CT policy
- Let's Encrypt: Ending OCSP support in 2025, Ending TLS client authentication certificate support in 2026, Chains of trust