Phone:

Hidden from the page source until you click: friction against scrapers, not a guarantee.

Email:

[email protected]

Category:

Security

Published:

Tags:
  • tls
  • certificate-transparency
  • security
  • pki
  • cryptography
  • x509

Certificate Transparency Logs: What They Actually Prove About a TLS Certificate, and What They Don't

Search crt.sh for your own domain and you'll usually find more certificates than you expected: old ones from a hosting provider you've forgotten about, wildcard certs someone issued for a subdomain you don't remember creating, maybe an entry from a CDN you switched away from two years ago. The instinct is to treat this list as a security audit trail: if a certificate is in there, someone somewhere vetted it. That instinct is only half right, and the half that's wrong matters.

Certificate Transparency (CT) is a detection mechanism, not a validation mechanism. It doesn't stop a certificate authority (CA) from issuing a bad certificate. It makes sure that if one does, the evidence is public and cannot quietly disappear. Those are very different guarantees, and conflating them is where most misunderstandings about CT start.

What a CT log actually is

A CT log is an append-only Merkle tree of certificates, run by an independent operator, that anyone can query and no one (not even the operator) can retroactively edit without the tampering being provable. The original design is RFC 6962 from 2013, written in response to CA mis-issuance incidents (DigiNotar being the one everyone cites) where a compromised or careless CA issued certificates for domains it had no business certifying, and nobody found out until real damage was done. The current version, RFC 9162 (Certificate Transparency v2.0, December 2021), obsoletes 6962 and cleans up the log format, adds better auditing primitives, and formalises things the original spec left vague.

The core data structure is a Merkle hash tree. Every certificate submitted gets a leaf; the log periodically commits to a new root hash covering everything logged so far. This gives you two useful cryptographic proofs:

  • An inclusion proof: given a certificate and a tree root, prove that certificate is actually in the tree, without downloading the whole log.
  • A consistency proof: given two tree roots at different points in time, prove the later tree is a strict append of the earlier one, i.e. nothing was deleted or rewritten.

When a CA submits a certificate (or, more precisely, a "precertificate", a placeholder used to get logged before the real cert is signed) to a log, the log returns a Signed Certificate Timestamp (SCT): a promise, cryptographically signed by the log's key, that the certificate will appear in the tree within a bounded time (the "maximum merge delay"). The CA embeds that SCT (or several, from different logs) into the final certificate.

How enforcement actually bites

None of this matters unless something checks for it. Browsers do the checking. Chrome has required CT compliance for all publicly trusted certificates since April 2018, and the current policy requires at least two embedded SCTs for certificates valid under 180 days, and three for longer-lived ones, with the requirement that they come from distinct log operators rather than just distinct logs run by the same organisation. As of Chrome 148 (May 2026), SCT delivery via stapled OCSP responses was dropped entirely, so in practice an SCT has to be embedded directly in the certificate or delivered via the TLS handshake extension. A certificate that doesn't meet this bar gets a hard CT error in Chrome, no user override. You can see the raw evidence sitting in the certificate itself. SCTs travel in an X.509 extension with OID 1.3.6.1.4.1.11129.2.4.2. Go's standard library doesn't parse the SCT list for you (there's no exported struct for it in crypto/x509), but you can check whether it's present:

package main

import (
	"crypto/x509"
	"encoding/asn1"
	"encoding/pem"
	"fmt"
	"os"
)

var sctListOID = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 11129, 2, 4, 2}

func main() {
	data, err := os.ReadFile(os.Args[1])
	if err != nil {
		fmt.Fprintln(os.Stderr, "read cert:", err)
		os.Exit(1)
	}

	block, _ := pem.Decode(data)
	if block == nil {
		fmt.Fprintln(os.Stderr, "no PEM block found")
		os.Exit(1)
	}

	cert, err := x509.ParseCertificate(block.Bytes)
	if err != nil {
		fmt.Fprintln(os.Stderr, "parse cert:", err)
		os.Exit(1)
	}

	for _, ext := range cert.Extensions {
		if ext.Id.Equal(sctListOID) {
			fmt.Printf("SCT list present, %d bytes (unparsed, unverified)\n", len(ext.Value))
			return
		}
	}
	fmt.Println("no embedded SCT list")
}

Note what this program does and doesn't tell you. It tells you an SCT list extension exists and how many bytes it is. It doesn't verify the signatures inside it, check them against known log public keys, confirm the timestamp is sane, or confirm the log actually included the certificate rather than just promising to. A real client-side CT check (which Chrome does, and which Go's own TLS stack does not do for you automatically) has to fetch the log's public key, verify the SCT signature, and ideally check an inclusion proof against a recent signed tree head. Printing "SCT list present" is closer to "an envelope with a stamp on it exists" than "the letter inside says what it claims".

What this actually proves

Put together, a validly logged and browser-accepted certificate proves:

  • Some CA issued this certificate for this name and this public key at roughly this time.
  • That issuance is recorded in at least two (usually three) independently operated public logs.
  • Anyone can retrieve an inclusion proof tying the certificate to a specific, published, monotonically-appending Merkle tree.
  • If the certificate is ever found to be fraudulent, there is a permanent, tamper-evident public record of exactly when it was issued and by whom, which cannot be quietly scrubbed after the fact.

That last point is the actual point of CT. It shifts the CA's incentive from "we probably won't get caught" to "we will definitely be caught, the only question is how fast". It's a detection and accountability layer bolted onto a system, the public CA hierarchy, that has no other reliable way of noticing misbehaviour until a browser vendor happens to stumble on it.

What it does not prove

This is the list that actually matters day to day:

  • It doesn't prove domain control was validated correctly. CT logs whatever the CA submits. If a CA's domain validation process is broken, compromised, or was tricked (BGP hijack during a DNS-01 challenge, for instance), the resulting certificate gets logged just as faithfully as a legitimate one. CT makes the mistake visible; it does not prevent it.
  • It says nothing about revocation. A certificate that was logged in 2023 and revoked in 2024 still has a perfectly valid CT inclusion proof. CT and revocation (CRLs, OCSP, or Chrome's CRLSet) are separate mechanisms answering separate questions: "was this issued and recorded" versus "is this still trusted now".
  • It doesn't verify the log itself is honest in real time. A log operator could, in principle, present a different (but internally consistent) view of its tree to different observers, a "split-view" attack, showing a client one root while showing the wider world another. The countermeasure is CT gossip, where different parties compare signed tree heads with each other so a split view gets detected through disagreement. It's a good design and it's genuinely interesting how much of the trust model rests on it, but full deployment of gossip protocols has lagged behind the rest of CT for years, so in practice this defence is thinner than the Merkle tree maths suggests.
  • It says nothing about the server you're actually talking to right now. That guarantee comes from the TLS handshake itself, the server proving possession of the private key matching the certificate. CT is entirely about the paper trail of issuance; it has no opinion on today's live connection.
  • It doesn't stop mis-issuance from happening. This is worth repeating because the name invites the opposite assumption. "Transparency" is not "prevention". A malicious or compromised CA can still successfully issue and log a fraudulent certificate for your domain. What CT guarantees is that the certificate will show up somewhere you (or a monitoring service) can find it, usually within hours.

That last property is the genuinely actionable one. Services like crt.sh, Cloudflare's CT monitoring, or Facebook's certificate transparency monitoring tool let you register a watch on your own domains and get notified the moment anything gets logged for them, legitimate or not. Given that CT can't prevent mis-issuance, the only useful response left to you is to catch it fast, and that's a solvable problem: set up the monitor once, and treat every unexpected notification as an incident to investigate, not noise to dismiss. The certificate authority hierarchy still runs on trust; CT just makes sure that trust comes with a receipt.