Certificate Transparency
31.0 What this chapter gives you#
- You will be able to explain without jargon why the owner of a domain name once had no way at all of learning that a certificate had been issued for it.
- You will be able to build a Merkle hash tree by hand from the rules in RFC 6962 section 2.1 and say why the two hash prefixes exist.
- You will be able to produce and check an inclusion proof and a consistency proof on a seven-entry tree, with the real hash values printed out.
- You will be able to name every field of a signed certificate timestamp, give its size in bytes, and say what the log’s signature covers.
- You will be able to list the three places an SCT can be delivered, and say which one almost everybody uses and why.
- You will be able to state, with numbers and dates, what Chrome, Apple and Firefox each require of a certificate as of August 2026.
- You will be able to describe the log operator, monitor and auditor roles and say which of the three is mostly missing.
- You will be able to recount five real misissuance incidents the public logs exposed, with the CA named, the dates given and the outcome stated.
- You will be able to query crt.sh and the Cert Spotter API for your own domain, read the fields that come back, and set up a standing watch.
- You will be able to say precisely what Certificate Transparency does not do, which is a longer and more useful list than what it does.
A certificate is a signed statement by a certificate authority that a particular public key belongs to a particular name. Somewhere between one and two hundred organizations in the world are trusted by browsers to make that statement about any name at all. That has always been the uncomfortable arithmetic of the public web: the security of every site is the security of the weakest of the authorities, because the browser accepts a signature from any of them.
Until about 2013 a second, quieter problem sat on top of that one, and it was worse. If an authority signed a certificate for your domain that you had not asked for, you would not find out. There was no register to consult and no notification. The certificate existed as a private arrangement between the authority and whoever had asked for it, and the first sign you would get was somebody complaining that your website looked strange. Misissuance was not merely possible. It was invisible.
Certificate Transparency is the industry’s answer to that specific problem, and only to that problem. It is a set of public, append-only logs. Every publicly trusted certificate is written into at least two of them before it can be used, the log gives back a signed receipt that travels with the certificate, and the browser refuses the certificate if the receipts are missing. The logs are built on a data structure, the Merkle hash tree, that lets a log prove two things cheaply: that a particular certificate is in it, and that it has only ever been added to. Nobody has to trust the log operator; the mathematics is checkable by anyone.
This chapter builds the whole thing from the ground up: a real seven-entry log with real SHA-256 values, an inclusion proof and a consistency proof both verified, an SCT taken apart field by field, the browser policies as they stand in August 2026, the logs that actually exist today, five incidents in which the logs caught an authority misbehaving, and a full section on what the system cannot do, because that list is where most of the confusion lives. Three boundaries, stated once: cancelling a certificate after you have found it is chapter 30, “Revocation”, and the short answer there is that cancellation mostly does not work, which is part of why finding it early matters; what it means for a key to sign bytes at all is chapter 32, “The Digital Signature”; and removing an authority from the trust stores entirely is chapter 35, “When a Certificate Authority Fails”.
The plain version#
The letter that anybody can write about you#
Latha runs a silk shop on Gandhi Road. The shop is called Latha Silks, and the name is worth money, because people come from three districts to buy from it.
In this town, business is done with sealed letters. If you want to trade with somebody far away, you go to a letter office, prove who you are, and the office writes a letter that says “the bearer of this letter is Latha Silks of Gandhi Road” and presses its seal into the wax at the bottom. Anybody who receives the letter looks at the seal, recognizes the office, and deals with the bearer as if they were dealing with Latha.
There are roughly a hundred and fifty letter offices spread across the country. Some are large and careful. Some are small. One or two are in places where a determined official can lean on the manager. Any one of them can write a letter about Latha Silks. They do not have to ask Latha. They are supposed to check that the person in front of them really is from Latha Silks, but “supposed to” is doing a great deal of work in that sentence.
Now the problem, which is the entire subject of this chapter. Suppose one of those offices writes a letter about Latha Silks for somebody who is not Latha. The seal is genuine. The letter is genuine. It works perfectly on everybody who receives it. And Latha has no way whatsoever of learning that it exists. There is no place she can go and look. The letter office keeps its own records and does not publish them. The only way she finds out is when a customer three districts away complains that Latha Silks took their money and vanished.
That is what misissuance looked like before 2013. Not “hard to detect”. Undetectable, by design, because nobody had ever built the place where you would look.
The bound ledger in the town square#
The fix, when somebody finally proposed it, was almost insultingly simple.
Put a ledger in the town square. Rule: a letter is worthless unless a copy of it has been written into the ledger first. When the letter office writes the copy in, the clerk hands back a small numbered slip, signed, saying “a copy of this letter went into my ledger at eleven minutes past nine this morning”. The office staples the slip to the letter. Anybody receiving a letter now checks two things: the office’s seal, and the clerk’s slip. No slip, no deal.
Latha can now read the ledger. Every morning she walks to the square, turns to yesterday’s pages and looks for her own name. If a letter about Latha Silks appears that she did not ask for, she sees it the next morning instead of never.
That is Certificate Transparency in one paragraph. Everything that follows is engineering to make it work at the scale of the whole internet, where the ledger takes several million new entries a day and nobody, including Latha, is willing to trust the clerk.
Why you cannot simply trust the clerk#
Two things can go wrong with a clerk. The first is that the clerk hands out slips without actually writing the copies in, so a crooked letter office gets a slip to staple to its forged letter and the copy never appears for Latha to find. The second is that the clerk writes the copy in and later removes it, replaces a page, or keeps two ledgers: a clean one to show Latha and a dirty one for everybody else.
The design has to make both of those detectable by ordinary people with no special access. The trick that does it is a way of fingerprinting the whole ledger, and it is the one genuinely clever idea in this chapter.
Fingerprinting a book without reading it#
A fingerprint, here, means a short code calculated from a piece of text. Feed in the text, turn a handle, and out comes a fixed-length code. Two rules make it useful. Change one character of the text and the code changes completely. And nobody can work backwards from a code to find text that produces it.
You could fingerprint the entire ledger by feeding in every page one after another. That gives one code for the whole book, which tells Latha whether the book has changed. It is nearly useless in practice, because to check the code she would have to read the whole book, and with five billion pages she is not going to.
So instead the clerk builds the fingerprint in layers. Take the pages, in order, and fingerprint each one: that gives one code per page. Now take those codes two at a time, in order, glue each pair together, and fingerprint the glued pair. That halves the number of codes. Do it again, and again, until you are down to a single code at the top. That code is the fingerprint of the whole ledger, but unlike the flat version it has a shape underneath it, and the shape is what makes everything possible.
Here is that shape for a small ledger of seven pages. Read it from the bottom up. The seven pages are on the bottom row, the codes for each page are the row above, and each row after that is made by gluing pairs together.
root
/ \
/ \
k l
/ \ / \
/ \ / \
g h i j
/| |\ |\ |
a b c d e f |
| | | | | | |
p0 p1 p2 p3 p4 p5 p6
Page 6 has no partner, so its code, j, is just carried up unchanged until it finds one. That is why the right side of the picture looks lopsided. The shape of the tree is decided entirely by how many pages there are, and everybody who knows the number of pages draws the same shape.
Proving that one page is in the book#
Latha finds a letter in the ledger that she never asked for. She photographs page 3 and goes to complain, and the letter office says: that page is not in the ledger, you made it up. The clerk can settle this in four numbers, without anybody reading the book.
Look at the picture again. To get from page 3 to the root you need exactly three things you do not already have: the code c, which is page 3’s partner; the code g, which is the whole left half of the left half; and the code l, which is the whole right half. With those three, and page 3 in her hand, Latha does the arithmetic herself:
- Fingerprint page 3. Call it d.
- Glue c and d together, fingerprint that, and you should get h.
- Glue g and h together, fingerprint that, and you should get k.
- Glue k and l together, fingerprint that, and you should get the root.
If the root she computes matches the root the clerk has published, page 3 is in the book. There is no other way to arrive at the same root. She has proved membership in a five-billion-page ledger with four small calculations. That list of three helper codes is called an inclusion proof. The number of helpers grows like the number of times you can halve the book, so a ledger of a billion pages needs about thirty of them.
Proving that the book was only ever added to#
The second worry was the clerk quietly changing an old page, and it is handled by a different set of helpers.
Suppose yesterday the clerk published a root for a book of six pages and Latha wrote it down. Today the clerk publishes a root for a book of seven. Is today’s book yesterday’s with one page added, or has the clerk rewritten history?
The clerk sends three codes: i, j and k. Latha checks two things with them. First, gluing k and i together and fingerprinting gives yesterday’s root, so those two codes really do stand for yesterday’s six pages. Second, gluing i and j gives l, and gluing k and l gives today’s root. The same k and i are sitting inside today’s book, untouched, so yesterday’s six pages are the first six pages of today’s seven. That set of helpers is a consistency proof, and a clerk who has removed or altered a page cannot produce one, because no set of codes makes the old root and the new root agree.
Put those two proofs together and you have a ledger nobody has to trust: a public book that can be proved to contain what it says it contains, and proved never to have been edited.
The slip is a promise, not a proof#
Here is the part that most short explanations skip, and it matters.
When the letter office hands its copy to the clerk, the clerk does not stop everything, rebuild the whole tree and hand back an inclusion proof. That would be slow, and letters are written thousands of times a minute. Instead the clerk signs a slip saying “I have received this and I promise to have it in the book within one day”, and hands it over immediately. The writing-in happens later, in batches.
So the slip that travels with the letter is a signed promise about the future. It is not evidence that the page is in the book. It is evidence that the clerk has publicly and unforgeably committed to putting it there, and that anybody can prove the promise broken by asking for an inclusion proof after the deadline and not getting one. The deadline has a name, the maximum merge delay, and for the traditional logs it is one day. Promise now, proof later is the single most misunderstood thing about this system.
Three jobs, and one of them is yours#
Three different kinds of organization make this work, and they do genuinely different things.
The log operator is the clerk. It keeps the book, hands out slips, publishes the root, answers questions about the shape of the tree. There are eight of them for the whole public web as of August 2026.
The monitor is a reader. It downloads every new page from every ledger and looks for names it cares about. Some monitors will watch your names for you and email you; some are search tools you can query by hand. A monitor is what turns the ledger from a pile of paper into an alarm.
The auditor is an inspector. It does not care what the letters say. It cares whether the clerk is honest: whether the promises are kept, whether the book is only ever added to, whether the clerk shows the same book to everybody. It is the role that keeps the design honest, and it is the thinnest on the ground, for reasons we will get to.
And then there is Latha. The ledger does not read itself. Every incident in this chapter was found because a specific person went and looked. The system makes misissuance visible; it does not make anybody see it.
Where the plain version stops being true#
The ledger does not stop the letter being written#
The most important limitation is the one the analogy hides. Nothing in this design prevents an authority from issuing a certificate it should not have. The authority signs it, logs it, gets its receipts, and the certificate works in every browser on earth. CT is a detection system, and the gap between issuance and detection is a real window during which a fraudulent certificate is fully effective.
The honest version: the guarantee is not “misissuance cannot happen” but “misissuance that is useful against a modern browser cannot happen secretly”, and the difference between those two sentences is where the whole practical value, and the whole practical disappointment, of the system lives.
The receipt is not checked by the browser#
In the analogy, Latha checks that the page is really in the book. Your browser does not.
When Chrome or Safari or Firefox validates a certificate, it checks that the receipts are present, that they are signed by logs it recognizes, that there are enough of them, and that they come from enough different operators. It does not then ask the log for an inclusion proof. It does not verify that the certificate was ever written into the tree.
The reason is privacy. Asking a log “prove that this certificate is in your tree” tells the log operator which site you are visiting, from your address, at that moment. That is the same leak that killed online revocation checking, and the browsers were not willing to pay it twice. Chrome does perform a limited, sampled, privacy-protected version of the check for some users through its Safe Browsing infrastructure, but it is a sample, not a rule.
The honest version: for the overwhelming majority of connections, the browser’s CT check proves that a log signed a promise, not that the certificate is in any log. That gap is covered by monitors and auditors watching from outside, and covered well only to the extent that they exist and are watching.
There is not one ledger, and the ledgers come and go#
There is no single town square. As of August 2026 there are eight recognized log operators and about forty-eight separate log instances on Chrome’s list, and the set changes constantly. Logs are sharded by year or half-year, removed when their window passes, removed for misbehaviour, and frozen read-only when an operator stops accepting submissions. That churn is the reason browsers publish a log list with states attached, the reason a receipt has to be judged against the state of its log at two different moments, and the reason a certificate issued today can stop being acceptable tomorrow because a log fell over.
What is logged is not the certificate you are shown#
The copy in the ledger is not the letter. It is a near-identical draft.
Embedding receipts inside a certificate is a chicken-and-egg problem: the receipt has to be inside the certificate, but it is issued in response to the certificate. The solution is a precertificate, the certificate as it will be issued plus one extra extension that deliberately makes it invalid, so it can never be used for real. The authority logs the precertificate, collects the receipts, embeds them, and signs the real certificate.
So when you look up your domain in a log, some of what you see are precertificates, some are final certificates, and often both for one issuance. That is why search tools appear to show duplicates. They are not duplicates. They are the draft and the finished letter.
Nobody is obliged to read the ledger#
The analogy has Latha walking to the square every morning. In real life almost nobody does.
In September 2025 Cloudflare published an incident report about twelve certificates issued by a Croatian certificate authority for 1.1.1.1, the address of its public DNS resolver, between February 2024 and August 2025. Every one of them sat in the public logs, exactly as designed, for as long as eighteen months. They were found because an outsider went looking and posted to a mailing list, and Cloudflare’s own report said plainly that it had failed to monitor certificates for its own names.
If Cloudflare was not watching, the assumption that anybody is watching your domain deserves to be examined.
Logging everything publishes everything#
Every hostname in every publicly trusted certificate is now a matter of public record, searchable by anybody, for ever. That includes staging.internal-billing.yourcompany.example, and the name of the acquisition target whose subdomain appeared on your load balancer a month before the announcement. Proposals to redact names inside logged precertificates were argued over in the IETF for years and did not survive. The practical mitigation is a wildcard certificate, and the practical consequence is that competitive intelligence teams read certificate logs.
The ledger covers the public web and nothing else#
Certificate Transparency applies to certificates that chain to a root the browser ships with. It does not apply to a root that an administrator installed on the machine. That exemption is deliberate, and it is in the policies of every browser that enforces CT, because otherwise every corporate interception proxy and every home laboratory would break. The certificates presented by a corporate network that inspects its employees’ encrypted traffic never appear in any log and never will. If your threat model includes the machine’s own administrator, transparency logs are not looking at your problem.
The inspectors were never really built#
The design assumes that clients gossip: that browsers and monitors compare the roots they have seen, so that a log showing one version of the tree to one party and a different version to another is caught immediately. Two conflicting signed roots from the same log are unforgeable proof of misbehaviour.
Gossip was specified as an IETF draft, never finished, and no browser ships it. What exists instead is smaller: a handful of monitors and log operators who watch each other, Google’s compliance infrastructure, which tests every included log continuously, and, recently, independent witnesses that co-sign checkpoints.
The honest version: the cryptography makes cheating detectable in principle. Detection in practice depends on a handful of people running software they are not paid to run.
The technical version#
What misissuance is, and why the domain owner is blind#
A certificate is misissued when a publicly trusted certificate authority signs a certificate that it should not have signed. The category is broader than fraud, and the logs catch all of it. A certificate can be misissued because the domain validation was wrong or was bypassed, so the applicant did not control the name. It can be misissued because the contents violate the CA/Browser Forum Baseline Requirements: a missing extension, a bad key usage, a serial number with insufficient entropy, a validity period longer than the rules allow. It can be misissued because the key is known to be compromised. Or it can be a test certificate that escaped a test environment, which is the single most common story in this chapter.
Before 2013, the domain owner’s visibility into all of that was zero. There is no field in DNS that says “certificates have been issued for this name”. There is no obligation on an authority to notify the subject. Two authorities can issue for the same name simultaneously without either knowing about the other, and that is not an error, it is normal operation. CAA records let a domain owner say in advance which authorities are allowed to issue, but a CAA record is a rule, not a receipt: it tells authorities what to do and tells the domain owner nothing about what they did.
The gap is structural. The relying party is the browser, the subject is the domain owner, and the customer is whoever paid. Nothing in the X.509 model connects the subject to the record of issuance. Certificate Transparency is a bolt-on that creates that connection from the outside.
Merkle hash trees, from the rules up#
RFC 6962 section 2.1 defines the tree completely in about fifteen lines, and it is worth having those lines exactly, because every implementation detail in the rest of this chapter falls out of them.
The hash function is SHA-256. Fixed, for version 1. The input is an ordered list of n data entries, written D[n] = {d(0), d(1), ..., d(n-1)}. The output, called the Merkle Tree Hash and written MTH, is a single 32-byte value. The definition is recursive and has three cases.
MTH({}) = SHA-256("")
MTH({d(0)}) = SHA-256(0x00 || d(0))
MTH(D[n]) = SHA-256(0x01 || MTH(D[0:k])
|| MTH(D[k:n]))
In the third line, k is the largest power of two strictly smaller than n, and the double bar means concatenation. For a tree of seven entries k is four: the left subtree holds entries 0 to 3 and the right holds 4 to 6. For six or five entries k is also four. The shape is decided entirely by n, so any two parties who agree on n draw the same tree.
The two prefix bytes are the part people leave out, and leaving them out breaks the whole structure. A leaf is hashed with 0x00 in front of it; an internal node with 0x01. Without that separation, data could be crafted whose leaf hash equals the hash of some internal node, and an attacker could present a subtree as a single entry, or the reverse. The RFC calls this domain separation and says it is required for second-preimage resistance. It costs one byte and it is not optional.
The empty tree hashes to the SHA-256 of the empty string, a constant every implementer memorizes:
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
A real seven-entry log, computed#
Everything from here to the end of the proof sections uses one laboratory log of seven entries. The entry data are short strings so that you can reproduce every number yourself in a few lines of code. In a production log each d(i) is a serialized MerkleTreeLeaf structure, which we take apart later; nothing about the tree arithmetic changes.
Every hash in this chapter is a real SHA-256 value computed on 18 August 2026. We print the first eight bytes, because sixteen hex characters is enough to follow the arithmetic and sixty-four is not readable on a page.
| Entry | Data | Leaf hash, first 8 bytes |
|---|---|---|
| d0 | cert-0 lab.kedbyte.test | b6e1519f56ef56a2 |
| d1 | cert-1 www.lab... | 411e82caa60db13d |
| d2 | cert-2 api.lab... | 7c62f465af69071a |
| d3 | cert-3 pay.lab... | 5d68c3549e1965bd |
| d4 | cert-4 mail.lab... | cb18f6bd7aa9c48a |
| d5 | cert-5 vpn.lab... | c449e8f81f5cb82e |
| d6 | cert-6 cdn.lab... | 7a90fbec2e3b8b53 |
Each of those is SHA-256 of a 0x00 byte followed by the data bytes. The internal nodes are each SHA-256 of a 0x01 byte followed by the left child’s 32 bytes and the right child’s 32 bytes. The node letters are those of the example in RFC 6962 section 2.1.3, so you can read the RFC alongside this page.
| Node | Built from | Value, first 8 bytes |
|---|---|---|
| g | a and b | 5a826a3444f0e9ff |
| h | c and d | eb1b83cbe70d3396 |
| i | e and f | deac3e45beb02566 |
| j | d6 alone | 7a90fbec2e3b8b53 |
| k | g and h | e12a72574685c3a4 |
| l | i and j | 118dd32d7e8610e5 |
| root | k and l | 3505562e9175576b |
Note j. Entry 6 is alone in its subtree, so the one-element rule applies and j is simply the leaf hash of d6, promoted without further hashing. That is the rule people get wrong most often in their own implementations: a lone node is carried up, not hashed with itself and not padded.
The full root of the seven-entry tree, and the same value as a log would print it in a get-sth response:
3505562e9175576bcb2bba450438a456391c224eb05ab578
64a3d1dc0b29b7e4
NQVWLpF1V2vLK7pFBDikVjkcIk6wWrV4ZKPR3Aspt+Q=
Here is the tree drawn out, with the entry indexes on the bottom.
root
____/ \____
/ \
k l
/ \ / \
g h i j
/ \ / \ / \ |
a b c d e f |
| | | | | | |
d0 d1 d2 d3 d4 d5 d6
And the same seven entries at earlier moments in the log’s life, because we will need those roots in a moment:
| Tree size | Root, first 8 bytes | Note |
|---|---|---|
| 3 | 2c6567d0f5279c1d | lopsided |
| 4 | e12a72574685c3a4 | equals node k |
| 6 | 3e380f23407998f1 | lopsided |
| 7 | 3505562e9175576b | current |
The root at size four is exactly the node k of the size-seven tree. That is the property that makes consistency proofs small: a complete power-of-two subtree, once built, is never rebuilt.
Inclusion proofs, worked and checked#
An inclusion proof, called a Merkle audit path in RFC 6962 section 2.1.1, is the shortest list of nodes needed to recompute the root from one leaf. The recursive definition is:
PATH(0, {d(0)}) = {}
PATH(m, D[n]) = PATH(m, D[0:k]) : MTH(D[k:n])
for m < k
PATH(m, D[n]) = PATH(m-k, D[k:n]) : MTH(D[0:k])
for m >= k
Read plainly: at each level, you take the sibling of the subtree you are in and put it in the list, then recurse into your own half. Let us do it for d3, our pay.lab.kedbyte.test entry, in the seven-entry tree.
n is 7, so k is 4, and m is 3, which is less than k, so we recurse into the left half, entries 0 to 3, and append the hash of the right half, which is l. Inside the left half, n is 4, k is 2, m is 3, not less than k, so we recurse into entries 2 to 3 and append the hash of entries 0 to 1, which is g. Inside that, n is 2, k is 1, m is 1, so we recurse into entry 3 alone and append the hash of entry 2, which is c. The recursion ends with an empty list.
Collecting in order, the audit path for d3 is [c, g, l], exactly what the RFC’s own example says it should be. In wire terms:
GET /ct/v1/get-proof-by-hash?hash=<b64 leaf hash>
&tree_size=7
{
"leaf_index": 3,
"audit_path": [
"fGL0Za9pBxr...",
"WoJqNETw6f8...",
"EY3TLX6GEOU..."
]
}
Verification is four hashes and a comparison, working from the leaf upwards with the real values from our tree:
step 0 leaf(d3) = 5d68c3549e1965bd (d)
step 1 H(0x01 || c || d) = eb1b83cbe70d3396 (h)
step 2 H(0x01 || g || h) = e12a72574685c3a4 (k)
step 3 H(0x01 || k || l) = 3505562e9175576b (root)
The computed root equals the published root, so d3 is in the tree of size seven. Notice what the verifier needed: the leaf, three 32-byte hashes, and a root it already trusted. It did not need to see any other entry. The index matters as well as the values, because the verifier must know whether each sibling goes on the left or the right, and that is determined by the bits of the leaf index rather than by anything in the proof itself. An implementation that hashes the siblings in the wrong order will happily verify a proof for the wrong tree.
Proof size grows as the base-two logarithm of the tree size. A log holding one billion entries needs thirty hashes, which is 960 bytes.
Consistency proofs, and what append-only really means#
An inclusion proof says a thing is in the tree. A consistency proof, RFC 6962 section 2.1.2, says the tree has only grown, which turns “we promise we do not edit the log” into something checkable. The definition is again recursive, built from a helper called SUBPROOF:
PROOF(m, D[n]) = SUBPROOF(m, D[n], true)
SUBPROOF(m, D[m], true) = {}
SUBPROOF(m, D[m], false) = {MTH(D[m])}
for m <= k:
SUBPROOF(m, D[n], b)
= SUBPROOF(m, D[0:k], b) : MTH(D[k:n])
for m > k:
SUBPROOF(m, D[n], b)
= SUBPROOF(m-k, D[k:n], false) : MTH(D[0:k])
Take our log. Yesterday it had six entries and published a root of 3e380f23407998f1. Today it has seven and publishes 3505562e9175576b. The consistency proof between them is [i, j, k], which is what RFC 6962 section 2.1.3 gives for PROOF(6, D[7]).
Now the verification, which has two halves and this is the part worth being careful about. The verifier must reconstruct both roots from the same proof.
old root from k and i:
H(0x01 || k || i) = 3e380f23407998f1 matches
yesterday
new root from k, i and j:
H(0x01 || i || j) = 118dd32d7e8610e5 (l)
H(0x01 || k || l) = 3505562e9175576b matches
today
The same two nodes, k and i, appear in both computations. That is the proof: k covers entries 0 to 3 and i covers entries 4 to 5, so all six of yesterday’s entries are inside today’s tree, in the same positions, with the same contents. The only new material is j, which is entry 6. A log that had altered entry 2 would need a different k, and the old root would not come out right. A log that had removed entry 5 would need a different i. No set of hashes satisfies both equations for an edited history, unless SHA-256 is broken.
A simpler case makes the shape clearer. Between tree size four and tree size seven the consistency proof is a single node, [l]. The old root was k, so the verifier need only check that hashing k with l gives the new root. When the old tree is a complete power of two, the proof is one hash however much the log has grown since.
| From size | To size | Proof nodes | Count |
|---|---|---|---|
| 3 | 7 | c, d, g, l | 4 |
| 4 | 7 | l | 1 |
| 6 | 7 | i, j, k | 3 |
| 7 | 7 | none | 0 |
An auditor that keeps every signed root it has ever seen and checks consistency between consecutive ones has verified, cryptographically, that the log has never edited or reordered anything across that entire span. That is the whole append-only guarantee, and it costs a few dozen bytes per check.
Precertificates, the poison extension and the Merkle leaf#
Now the awkward engineering. The receipts have to be inside the certificate, because the alternative delivery mechanisms are barely deployed. But the log cannot issue a receipt for a certificate that does not exist yet, and the certificate cannot exist until it contains the receipts. RFC 6962 section 3.1 solves it with the precertificate. The authority takes the TBSCertificate, the to-be-signed body of the certificate it intends to issue, and adds one extra extension:
OID 1.3.6.1.4.1.11129.2.4.3
critical: TRUE
extnValue: OCTET STRING containing 05 00
(an ASN.1 NULL)
That is the poison extension, and its whole purpose is to make the object unusable. It is marked critical, and RFC 5280 requires a client to reject a certificate containing a critical extension it does not recognize. No normal X.509 client recognizes this one, so a precertificate, even carrying a real signature from a real CA, cannot be used to serve traffic. It exists only to be logged.
The authority signs the poisoned body, submits it with add-pre-chain, collects the SCTs, deletes the poison extension, inserts an SCT list extension in its place, and signs the result. That final object is the certificate you are served.
The entry that goes into the tree is not the precertificate as submitted. It is a MerkleTreeLeaf, defined in RFC 6962 section 3.4:
struct {
Version version; // 1 byte, v1=0
MerkleLeafType leaf_type; // 1 byte, 0
TimestampedEntry entry;
} MerkleTreeLeaf;
struct {
uint64 timestamp; // 8 bytes, ms
LogEntryType entry_type; // 2 bytes
...signed_entry... // cert or PreCert
CtExtensions extensions; // 2-byte length
} TimestampedEntry;
entry_type is 0 for a final certificate submitted with add-chain, and 1 for a precertificate submitted with add-pre-chain. For entry type 1 the signed_entry is a PreCert structure, which is the SHA-256 hash of the issuing CA’s public key, 32 bytes, followed by the TBSCertificate with both the poison extension and the SCT list extension removed. That normalization is what makes the precertificate and the final certificate hash to the same thing, and it is why a monitor can tell that two log entries describe one act of issuance.
Here is the start of a real MerkleTreeLeaf for a precertificate entry timestamped at 09:15:22.417 UTC on 18 August 2026:
00 version v1
00 leaf_type timestamped_entry
000001a0142769b1 timestamp = 1787044522417 ms
0001 entry_type precert_entry
<32 bytes> issuer_key_hash
<3-byte len><DER> tbs_certificate
0000 extensions, empty
The leaf hash that goes into the tree is SHA-256 of a 0x00 byte followed by that entire structure. Getting this serialization right byte for byte is the commonest source of bugs in home-grown CT code, because a leaf hash that is off by one byte will never be found in any log and the error you get is “not found”.
The signed certificate timestamp, and where it goes#
An SCT is the receipt. RFC 6962 section 3.2 defines it:
struct {
Version sct_version; // 1 byte, v1 = 0
LogID id; // 32 bytes
uint64 timestamp; // 8 bytes, ms
CtExtensions extensions; // 2-byte len + data
digitally-signed struct {
Version sct_version;
SignatureType signature_type; // = 0
uint64 timestamp;
LogEntryType entry_type;
...signed_entry...
CtExtensions extensions;
};
} SignedCertificateTimestamp;
Field by field. The LogID is the SHA-256 hash of the log’s public key over the DER encoding of the SubjectPublicKeyInfo. It is how a client works out which log signed the receipt without being told: it hashes each public key in its log list and compares. The timestamp is milliseconds since the Unix epoch, ignoring leap seconds. The extensions field is two bytes of length followed by that many bytes; for RFC 6962 logs it is always the two bytes 00 00.
The signature is over a re-serialization that includes the certificate itself, and that is the important part. The log is not signing “I saw a hash”. It is signing “at this millisecond I received this specific certificate body”. A client verifying an SCT must rebuild that signed structure from the certificate it was given, which for an embedded SCT means removing the SCT list extension first. Get that wrong and every signature fails.
The whole thing is typically about 118 bytes when the log signs with ECDSA P-256, of which around 71 bytes is the DER-encoded signature. RFC 6962 section 2.1.4 permits exactly two algorithms: ECDSA over NIST P-256, and RSASSA-PKCS1-v1_5 with SHA-256 and a key of at least 2048 bits. Essentially every log in the Chrome list today uses P-256.
Static CT API logs add one extension: a leaf_index of type 0, a 40-bit big-endian integer giving the entry’s position, so the extensions field is 8 bytes rather than 2. The log therefore cannot issue the SCT until it has sequenced the entry, which drives the merge delay to zero by construction.
RFC 6962 section 3.3 defines three delivery channels for that SCT and requires TLS clients to implement all three and servers at least one. One of them won completely.
| Channel | Carrier | Deployment |
|---|---|---|
| Embedded | X.509 extension | Nearly universal |
| TLS extension | Handshake, type 18 | Rare |
| OCSP stapling | OCSP extension | Effectively dead |
The embedded channel puts a SignedCertificateTimestampList inside the certificate itself, as an X.509v3 extension with OID 1.3.6.1.4.1.11129.2.4.2: an ASN.1 OCTET STRING wrapping a two-byte total length followed by a sequence of two-byte-length-prefixed serialized SCTs. This is the channel that requires the precertificate dance, and the one everybody uses, because it needs no change to the web server at all.
The TLS channel uses extension type 18, named signed_certificate_timestamp, allocated by IANA at RFC 6962’s request. The client advertises support with an empty extension in the ClientHello and the server returns the SCT list. It is technically cleaner, because a server can swap receipts without reissuing the certificate, which matters when a log is removed from a browser’s list. Almost no server software makes it easy, so almost nobody does it.
The OCSP channel carried the SCT list in an OCSP response extension with OID 1.3.6.1.4.1.11129.2.4.5. It is now gone: Chrome’s CT policy states that only versions before Chrome 148 accept SCTs delivered this way, and stable Chrome was at version 152 as of 12 August 2026.
The log API: RFC 6962 endpoints and the static tiles#
An RFC 6962 log is an HTTPS service with eight endpoints under the path prefix /ct/v1/, taking form-encoded parameters and returning JSON with binary values base64-encoded.
| Endpoint | Method | What it does |
|---|---|---|
| add-chain | POST | Log a certificate |
| add-pre-chain | POST | Log a precertificate |
| get-sth | GET | Current signed tree head |
| get-sth-consistency | GET | Consistency proof |
| get-proof-by-hash | GET | Inclusion proof |
| get-entries | GET | Raw entries by index |
| get-roots | GET | Accepted root CAs |
| get-entry-and-proof | GET | Both, for debugging |
A signed tree head, RFC 6962 sections 3.5 and 4.3, is four values: the tree size, a timestamp, the 32-byte root hash, and a signature over all three plus a version and a type byte. It is the log’s public commitment to the state of its tree at a moment, and it is the anchor for every proof.
{
"tree_size": 7,
"timestamp": 1787044522417,
"sha256_root_hash":
"NQVWLpF1V2vLK7pFBDikVjkcIk6wWrV4ZKPR3Aspt+Q=",
"tree_head_signature": "BAMARzBFAiEA..."
}
The design has a well-known operational problem: get-entries is a dynamic query that cannot be cached, so a monitor wanting every entry from every log must make an enormous number of requests to origin servers that compute each answer. Running a large log became expensive enough to discourage new operators, and a few large companies ended up carrying the ecosystem.
The answer, developed from 2024 onwards, is the Static CT API, published at c2sp.org as static-ct-api and maintained by Andrew Ayer, Filippo Valsorda and Matthew McPherrin; version 1.1.0 is dated 20 March 2026. It keeps the RFC 6962 write path unchanged, so submitters and browsers need no modification, and replaces the read path with immutable static files that any content delivery network can serve.
<monitoring>/checkpoint signed tree head
<monitoring>/tile/<L>/<N> Merkle tree tile
<monitoring>/tile/data/<N> the entries
<monitoring>/issuer/<sha256> an issuing CA cert
A tile is 256 consecutive hashes at one level of the tree, exactly 8,192 bytes when full. Levels run from 0 to 5. Level 0 tiles are leaf hashes; each hash at level 1 covers a full level 0 tile below it, and so on. A client fetches the tiles it needs in parallel and computes any proof it likes locally, rather than asking the log to compute proofs for it. Tiles never change once full, so they cache for ever. Partial tiles at the right-hand edge carry a suffix giving their width.
Chrome began accepting applications from static-ct-api logs on 1 April 2025, initially requiring that at least one SCT still come from an RFC 6962 log, and its log policy requires a static-ct-api log to declare a maximum merge delay of no more than one minute. The effect is visible in the current log list: several of the eight recognized operators now run tiled logs, and Let’s Encrypt has moved to them entirely.
Log operators, monitors and auditors#
RFC 6962 section 5 separates the client roles, and the separation is real.
A log operator runs the tree. Its obligations under Chrome’s CT Log Policy are specific: accept submissions from the roots in the browser root programmes, incorporate any certificate for which it has issued an SCT within the maximum merge delay, maintain 99 per cent availability measured per endpoint over a rolling 90 days, never present two conflicting views of the tree, and accept certificates from Google’s Merge Delay Monitor Root so that Google can test it continuously. Availability below 95 per cent over any 90-day window, or failing to include a certificate for which an SCT was issued, will typically get a log removed.
A monitor watches the contents. The RFC’s algorithm in section 5.3 is a loop: fetch the current signed tree head, verify its signature, fetch all entries up to it, confirm they rebuild the same root, then poll for a new head, fetch a consistency proof between old and new, verify it, and check that the new entries produce the nodes the proof commits to. A monitor doing that properly watches for certificates of interest and audits the log at once. Sectigo’s crt.sh, SSLMate’s Cert Spotter, Censys and Cloudflare’s Merkle Town all do versions of this.
An auditor checks the log’s honesty without caring about contents. It verifies signed tree heads, requests consistency proofs between them, requests inclusion proofs for SCTs it has seen once the merge delay has elapsed, and compares the heads it has seen with the heads other parties have seen. Section 5.4 describes it; the crucial sentence is that any pair of tree heads from the same log can be checked for consistency by anybody.
The uncomfortable truth, stated plainly: the auditor role is the weakest link in deployed CT. Browsers do not audit, for the privacy reason given earlier. What auditing happens is Google monitoring the logs it recognizes, operators and monitors watching each other, and a small number of independent parties. No protocol running in a billion browsers would catch a log presenting different trees to different people.
Browser policy as of August 2026#
Three browser policies matter, and they are close but not identical. A certificate has to satisfy all of them to work everywhere, so in practice authorities aim at the strictest.
Chrome. The Chrome Certificate Transparency Policy defines six log states: Pending, Qualified, Usable, ReadOnly, Retired and Rejected. For embedded SCTs a certificate is compliant if it has at least one SCT from a log that was Qualified, Usable or ReadOnly at the time of checking; SCTs from at least N distinct logs that were Qualified, Usable, ReadOnly or Retired at that time, where N is 2 for a lifetime of 180 days or less and 3 for anything longer; and among those, at least two from distinct log operators. For SCTs delivered over TLS the test is at least two SCTs from logs that were Qualified, Usable or ReadOnly, from at least two distinct operators. Chrome also stops enforcing CT entirely if its log list is more than 70 days old, a deliberate safety valve so that a newly qualified log becomes safe to use within a bounded time.
Apple. Apple’s policy, last published on 21 April 2025, requires at least two SCTs from logs once approved or currently approved, and then either at least two SCTs from currently approved logs with at least one delivered by TLS extension or OCSP stapling, or at least one embedded SCT from a currently approved log plus a count that depends on lifetime. Apple also caps how many SCTs from a single operator can count.
| Lifetime | SCTs, distinct logs | Max per operator |
|---|---|---|
| 180 days or less | 2 | 1 |
| 181 to 398 days | 3 | 2 |
Apple adds one requirement nobody else has: at least one SCT must come from a log that is compliant with RFC 6962, which is why Chrome’s initial tiled-log policy also kept an RFC 6962 log in the mix.
Firefox. Mozilla enforced CT on desktop from Firefox 135, in February 2025, having enabled it in Nightly at 133 and Beta at 134. Mozilla runs no log application process of its own; it takes Chrome’s list and treats logs marked qualified, usable or readonly as Admissible. For embedded SCTs it requires N SCTs from distinct Admissible-or-Retired logs, N being 2 at 180 days or less and 3 otherwise, with at least one from an Admissible log and at least two from distinct operators. Its log information also expires after 10 weeks.
| Browser | 180 days or less | Over 180 days |
|---|---|---|
| Chrome | 2 SCTs | 3 SCTs |
| Apple | 2 SCTs | 3 SCTs |
| Firefox | 2 SCTs | 3 SCTs |
The enforcement history explains why certificates issued at different times behave differently. Older policies once demanded up to five SCTs, and Chrome once required one from a Google log, a rule dropped in 2022 in favour of operator diversity.
| Date | Change |
|---|---|
| Jan 2015 | Chrome requires CT for EV |
| Jun 2016 | All Symantec certs logged |
| Apr 2018 | Chrome requires CT for all |
| Oct 2018 | Apple begins requiring CT |
| Feb 2025 | Firefox 135 enforces CT |
Pin the exact October 2018 day down before you rely on it [UNVERIFIED: Apple’s original CT enforcement start date of 15 October 2018]. Chrome’s requirement is usually cited as covering certificates issued after 30 April 2018; the ct-policy announcement of 24 October 2016 gave only the month.
Those policies are only as good as the logs behind them, and the shape of that ecosystem is not obvious from the specifications. On 17 August 2026 the Chrome log list, version 89.22, held entries from eight recognized operators.
| Operator | RFC 6962 logs | Tiled logs |
|---|---|---|
| 4 | 6 | |
| Sectigo | 8 | 0 |
| DigiCert | 6 | 0 |
| Let’s Encrypt | 1 retired | 6 |
| TrustAsia | 3 | 1 |
| IPng Networks | 1 placeholder | 6 |
| Geomys | 1 placeholder | 3 |
| Cloudflare | 2 | 0 |
Eight operators is not many. Chrome requires SCTs from two distinct operators, so the loss of any one is felt across the whole web immediately. That is why the Chrome CT Log Policy spends as much space on availability and incident response as on cryptography. The two entries marked placeholder are retired logs named “Bogus placeholder log to unbreak misbehaving CT libraries”, which exist only because some client libraries crash when an operator has no RFC 6962 logs at all.
Logs are sharded by the expiry date of the certificates they accept. Google’s Argon2026h2 accepts certificates whose notAfter falls between 1 July 2026 and 1 January 2027, and nothing else. Chrome’s log policy requires new logs to be temporally sharded with a range no longer than one calendar year and no shorter than three months, the ranges of a single operator forming a contiguous span three to four years into the future. Sharding is what stops logs growing without bound: when a shard’s window passes, every certificate in it has expired and the log can be switched off.
The read-only entries carry a final tree head, and those numbers give the scale. Sectigo’s Sabre2026h2 was frozen on 18 September 2025 at 68,732,791 entries; Mammoth2026h2 at 57,634,084. Those are single half-year shards from one operator.
The maximum merge delay differs sharply between the two log designs, and it is the most consequential number in this section.
| Log type | MMD | In practice |
|---|---|---|
| RFC 6962 | 86,400 s | Up to 24 hours |
| static-ct-api | 60 s | Effectively zero |
A tiled log must sequence the entry before it can fill in the SCT’s leaf_index extension, so the entry is in the tree by the time the receipt is handed over. The window in which a misissued certificate exists but is invisible to a monitor shrinks from a day to nothing. That is a larger improvement than it sounds, because a day is long enough to use a certificate and discard it.
What Certificate Transparency actually caught#
Five incidents, in order, with what happened and what it cost.
Symantec, September 2015. On 14 September 2015 at about 19:20 GMT, Google’s monitoring of the CT logs found an Extended Validation precertificate for google.com and www.google.com issued by Symantec’s Thawte-branded authority and never requested by Google. Chrome had required CT for EV certificates since 1 January 2015, which is the only reason it was in a log at all. Google published on 18 September 2015; Symantec said the issuance happened during an internal testing process, and dismissed several employees.
The follow-up mattered more than the incident. On 28 October 2015 Ryan Sleevi published Google’s findings: Symantec’s initial disclosure of 23 test certificates across five organizations, including Google and Opera, had grown, after Symantec’s own audit, to 164 further certificates covering 76 domains, plus 2,458 certificates issued for domains that had never been registered by anybody. Google’s remedy was transparency itself: from 1 June 2016, every certificate issued by Symantec had to support CT, or Chrome would warn. Symantec also had to produce a post-mortem, a point-in-time readiness assessment and a third-party security audit.
Symantec again, January 2017 onwards. On 19 January 2017 an independent researcher, Andrew Ayer, reported to Mozilla’s security policy list a set of misissued Symantec certificates that he had found by reading the CT logs. The figure Google cited was 127. On 23 March 2017 Ryan Sleevi opened Chrome’s intent to distrust Symantec’s existing certificates with the sentence that defines this chapter’s thesis: “an initial set of reportedly 127 certificates has expanded to include at least 30,000 certificates, issued over a period spanning several years.”
Symantec was not a small authority. Sleevi’s post noted that in January 2015 Symantec-issued certificates were over 30 per cent of all valid certificates by volume, and that Firefox telemetry attributed 42 per cent of certificate validations to them. The outcome: on 2 August 2017 DigiCert agreed to acquire Symantec’s website security business, completing on 31 October 2017; a joint plan was published by Devon O’Brien, Ryan Sleevi and Andrew Whalley on 11 September 2017; Chrome 66 removed trust in Symantec certificates issued before 1 June 2016, reaching stable about 17 April 2018; and Chrome 70 removed trust in the rest, reaching stable about 23 October 2018. The largest authority on the web was removed from the trust stores, and the evidence that made it possible was public and checkable by anybody.
WoSign and StartCom, 2016. The Chinese authority WoSign, which had also quietly acquired the Israeli authority StartCom, was found to have issued certificates that violated the industry-wide ban on SHA-1 issuance after 1 January 2016 by backdating the notBefore field. Backdating is invisible if the only evidence is the certificate itself, because the certificate says whatever the issuer wrote in it. It is not invisible when the certificate is in a public log, because the log’s own timestamp records when the object actually appeared. Mozilla announced on 24 October 2016 that it would distrust new WoSign and StartCom certificates. The general lesson stuck: the SCT timestamp is a more trustworthy statement of when a certificate came into existence than anything inside the certificate.
Entrust, June 2024. Google announced on 27 June 2024 that, because of a sustained pattern of compliance failures and unsatisfactory incident reports, Chrome would stop trusting TLS server certificates chaining to nine Entrust and AffirmTrust roots. The mechanism is the point: the cut-off was expressed as certificates “whose earliest Signed Certificate Timestamp is dated after November 11, 2024, 11:59:59 PM UTC”, enforced from Chrome 131 on 12 November 2024, and not as the notBefore date, which the authority controls. The SCT timestamp it does not control. Certificates issued before the deadline kept working until they expired. CT had become the ecosystem’s clock.
Fina and 1.1.1.1, 2024 to 2025. The Croatian authority Fina, which is in the Microsoft root program and on the European Union trusted list, issued twelve certificates for the IP address 1.1.1.1, Cloudflare’s public DNS resolver, between February 2024 and August 2025 without Cloudflare’s involvement. The first was issued on 18 February 2024 and revoked 33 minutes later. Every one of them was submitted to the public logs.
Nobody noticed for eighteen months. The certificates were found by an outsider, discussed publicly on 1 September 2025, reported twice to Cloudflare’s bug bounty programme on 2 and 3 September where they were mistriaged, and finally posted to the public certificate-transparency mailing list at 10:59 UTC on 3 September 2025. Cloudflare declared an incident at 12:08 UTC, had identified nine of the certificates by 12:23 UTC, and Fina revoked all of them by 06:13 UTC the next day. Cloudflare’s own report of 4 September 2025 stated that it had failed to properly monitor certificates for its own names, listing among its failures an inability to alert on certificates issued for IP addresses rather than domain names.
Read those five together and the pattern is clear. CT stopped none of them. It made all of them findable, and in four cases somebody found them; in the fifth the logs worked perfectly and the watching did not.
Auditing your own domain#
This is the part you can act on today, and it takes about ten minutes. The simplest tool is crt.sh, a public search over CT log contents run by Sectigo and built by Rob Stradling, with a web interface and a JSON output mode. Here is a real query and a real record returned on 18 August 2026:
curl -s 'https://crt.sh/?q=example.com&output=json' \
| python3 -m json.tool | head -40
{
"issuer_ca_id": 413868,
"issuer_name": "C=US, O=SSL Corporation,
CN=Cloudflare TLS Issuing ECC CA 3",
"common_name": "example.com",
"name_value": "*.example.com\nexample.com",
"id": 28361996564,
"entry_timestamp": "2026-07-29T22:20:19.316",
"not_before": "2026-07-29T22:10:08",
"not_after": "2026-10-27T22:17:21",
"serial_number":
"0624d0ab311558780b7d5213b9631831",
"result_count": 3
}
Three things to read in that record. The entry_timestamp is when the object entered a log, not when the certificate was issued; not_before is the certificate’s own claim. The serial_number is the key for de-duplication: in the same response, a second record with id 28361964045 and an earlier entry_timestamp of 22:20:12.47 carries the same serial number. That is one act of issuance recorded twice. Count records and call it “certificates issued” and you over-count two or three times; count distinct serial numbers per issuer instead.
The query parameters worth knowing are q for a search term, output=json, exclude=expired to drop dead certificates, and match=any or match== to choose between substring and exact matching. crt.sh also runs certificate linters over what it stores and shows the errors, which turns the log into an automated compliance checker that anybody can consult.
For a standing watch rather than a one-off look, SSLMate’s Cert Spotter API is designed for exactly this. It coalesces the precertificate and the certificate into one issuance object, so the double-counting problem disappears, and it is built for polling.
curl -s 'https://api.certspotter.com/v1/issuances\
?domain=example.com&include_subdomains=true\
&expand=dns_names&expand=issuer' \
-H 'Authorization: Bearer YOUR_API_KEY'
An issuance object carries a tbs_sha256, the hash of the CT TBSCertificate as defined in RFC 6962 section 3.2, which uniquely identifies an issuance regardless of how many log entries it produced; a cert_sha256; dns_names; not_before and not_after; a revoked flag; and an issuer object. To poll, take the id of the last issuance you saw and pass it as the after parameter; when you get an empty array, wait for the number of seconds in the Retry-After header and try again.
The single most useful field is issuer.caa_domains. It gives the domain names that would appear in a CAA record authorizing that issuer. Compare it against your own CAA records and you have an automatic answer to the only question that matters: was this certificate issued by an authority I allow, or by one I do not.
A practical setup, in order of effort:
- Search crt.sh for every domain you own, once, today. Look for names you do not recognize, issuers you do not use, and validity periods that look wrong.
- Set up a polling monitor, from Cert Spotter, your certificate authority, your CDN, or a script against the firehose or a log’s tiles, and route the alerts somewhere a human reads.
- Write down the authorities you actually use, publish them as CAA records, and alert on any issuance whose caa_domains do not intersect that list.
- Alert on certificates issued for IP addresses as well as names, which is the specific gap Cloudflare identified in itself in September 2025.
- Decide in advance what you will do when the alert fires, because the answer runs through the CA’s problem reporting process and possibly a public incident report.
If you run a private certificate authority for internal systems, none of this applies by default. Internal CAs do not log to public CT and should not, because the names would be published. Some organizations run a private transparency log over the same Merkle machinery, which buys the audit property without the disclosure.
What Certificate Transparency does not do#
Stated flatly, because every one of these has been misunderstood in public by people who should know better.
It does not prevent misissuance. An authority can still issue a certificate it should not, log it correctly, and have it accepted everywhere. Detection is after the fact by design.
It does not tell you whether a certificate is authorized. The log records what was issued, not what was permitted. Deciding that a logged certificate is wrong is your job, and requires you to know what right looks like.
It does not check inclusion in your browser. Your browser checks that receipts exist and are signed by recognized logs, not that the certificate is in any tree; that is delegated to monitors and auditors who are not you.
It does not notify anybody. There is no push. The logs are a public record, and a public record with no reader is a filing cabinet. The Fina certificates sat in plain sight for eighteen months.
It does not cover private or locally installed roots. Corporate interception proxies, home laboratories, and anything using a root the administrator installed are all outside CT by explicit policy in every browser.
It does not revoke anything. Finding a bad certificate and stopping it working are entirely separate problems, and chapter 30 explains why the second is in much worse shape than the first.
It does not protect the privacy of your infrastructure. Every name you certify is published for ever. Redaction proposals were debated in the IETF and did not survive; wildcards are the practical answer.
It does not, on its own, catch a log that lies to different people differently. That requires gossip between clients, which was never deployed, or independent witnesses, which are new and few.
It does not tell you the certificate is trustworthy. An SCT means logged, not validated correctly, not safe, not competent. It means somebody wrote it down.
31.98 Common wrong ideas#
Wrong: Certificate Transparency stops certificate authorities issuing bad certificates. Right: It prevents nothing; it makes issuance publicly recorded so that a misissued certificate can be discovered after the fact by anybody who reads the logs, which is why every incident here describes damage already done before it was found.
Wrong: My browser checks that the certificate is really in a log. Right: Your browser checks that the certificate carries enough validly signed receipts from enough distinct recognized log operators; it does not request an inclusion proof, because that would tell the log which site you are visiting, so checking that the log kept its promise is left to monitors and auditors outside the browser.
Wrong: A signed certificate timestamp proves the certificate is in the log. Right: An SCT is a signed promise that the log will incorporate the entry within its maximum merge delay, 24 hours for an RFC 6962 log and at most 60 seconds for a static-ct-api log; only an inclusion proof against a signed tree head proves the entry is there.
Wrong: The log entry and the certificate my server sends are the same object. Right: For embedded SCTs the logged object is usually a precertificate, the same to-be-signed body plus a critical poison extension with OID 1.3.6.1.4.1.11129.2.4.3 that makes it unusable, and the normalization rules in RFC 6962 section 3.2 are what let a monitor recognize the precertificate and the final certificate as one issuance.
Wrong: Counting records in crt.sh gives the number of certificates issued for my domain. Right: One issuance normally appears several times, as a precertificate and as the final certificate, and once per log that accepted it, so you must de-duplicate on serial number and issuer, or use an API such as Cert Spotter that coalesces entries into issuance objects.
Wrong: RFC 9162 replaced RFC 6962, so version 2 is what runs today. Right: RFC 9162, “Certificate Transparency Version 2.0”, was published in December 2021 and formally obsoletes RFC 6962, but it is Experimental and no production log implements it; Chrome’s log policy admits logs conforming to RFC 6962 or to C2SP static-ct-api, and the live ecosystem is entirely version 1.
Wrong: The Merkle tree makes the log trustworthy. Right: It makes the log’s claims checkable, which is a different property: it becomes trust only when somebody fetches tree heads, requests consistency proofs between them, compares those heads against the heads other parties saw, and complains when the numbers disagree.
Wrong: If a log misbehaves, the browsers will catch it automatically. Right: Browsers do not audit logs; Google’s compliance infrastructure tests the logs on Chrome’s list continuously and monitors and operators watch each other, but the client-side gossip protocol meant to make split-view detection automatic was drafted in the IETF TRANS working group and never finished, so detection depends on a few organizations choosing to look.
Wrong: Getting an SCT means the certificate was validated properly. Right: A log verifies only that the submission chains to a root it accepts; it makes no judgement about whether the applicant controlled the name, whether the fields comply with the Baseline Requirements, or whether the authority followed its own procedures, and logs may accept expired and revoked certificates.
31.99 Chapter summary in 20 lines#
- Before Certificate Transparency a domain owner had no mechanism at all for learning that a certificate had been issued for their name.
- CT, specified in RFC 6962 of June 2013 by Ben Laurie, Adam Langley and Emilia Kasper, is a set of public append-only logs.
- Every publicly trusted TLS certificate must now be logged before Chrome, Safari or Firefox will accept it.
- The log is a binary Merkle hash tree over SHA-256, a leaf hashed with a 0x00 prefix and an internal node with a 0x01 prefix.
- That one-byte domain separation is required for second-preimage resistance and is the detail most often omitted by people implementing CT themselves.
- An inclusion proof is the list of sibling hashes needed to recompute the root from one leaf, and it grows as the logarithm of the tree size.
- In our seven-entry laboratory log the audit path for entry three is c, g and l, and four hash operations reproduce the root.
- A consistency proof shows an older tree is a prefix of a newer one; for sizes six to seven it is the three nodes i, j and k.
- A signed certificate timestamp is the log’s promise to include an entry within its maximum merge delay, not proof that it did.
- That delay is 86,400 seconds for an RFC 6962 log and at most 60 seconds for a static-ct-api log, which sequences entries before returning the receipt.
- SCTs can be delivered embedded in the certificate, in TLS extension 18, or in a stapled OCSP response, and almost all are embedded.
- Embedding requires a precertificate carrying a critical poison extension, OID 1.3.6.1.4.1.11129.2.4.3, so the unfinished object cannot be used.
- RFC 6962 defines the log operator, monitor and auditor roles, and the auditor role is the one that was never properly deployed.
- As of August 2026 Chrome, Apple and Firefox all require two SCTs from distinct logs at 180 days or less, and three above that.
- All three also require SCTs from at least two distinct log operators, and there are only eight recognized operators.
- In September 2015 CT caught Symantec’s Thawte issuing an EV precertificate for google.com, which by March 2017 had grown to at least 30,000 certs.
- Chrome distrusted Symantec’s public PKI in two stages, in Chrome 66 around 17 April 2018 and Chrome 70 around 23 October 2018.
- Chrome’s 2024 distrust of Entrust used the earliest SCT timestamp, not the certificate’s own notBefore field, as the authoritative issuance date.
- Twelve unauthorized certificates for 1.1.1.1 sat in the public logs from February 2024 until September 2025 because nobody at Cloudflare was reading.
- Certificate Transparency makes misissuance visible; it does not make anyone look, does not prevent issuance, and does not revoke anything.
Chapter sources: RFC 6962, “Certificate Transparency”, Laurie, Langley and Kasper, June 2013, Experimental, in particular section 2.1 for the Merkle Tree Hash, 2.1.1 for audit paths, 2.1.2 for consistency proofs, 2.1.3 for the seven-leaf example whose node labels this chapter reuses, 2.1.4 for permitted signature algorithms, 3.1 for log entries and the poison extension OID 1.3.6.1.4.1.11129.2.4.3, 3.2 for the SignedCertificateTimestamp, 3.3 and 3.3.1 for the three delivery channels and TLS extension type 18, 3.4 for the MerkleTreeLeaf, 3.5 for the Signed Tree Head, 4.1 to 4.8 for the eight client endpoints, 5.1 to 5.4 for the submitter, TLS client, monitor and auditor roles, and 7.1 to 7.3 for the analysis of misissuance and misbehaving logs; RFC 9162, “Certificate Transparency Version 2.0”, Laurie, Messeri and Stradling, December 2021, Experimental, which formally obsoletes RFC 6962 and which no production log implements; RFC 5280 for the certificate profile and unrecognized critical extensions; the C2SP Static Certificate Transparency API, version 1.1.0 of 20 March 2026, maintained by Andrew Ayer, Filippo Valsorda and Matthew McPherrin, for checkpoints, tiles, data tiles, the issuer endpoint and the leaf_index SCT extension; the Chrome Certificate Transparency Policy, the Chrome Certificate Transparency Log Policy and the Certificate Transparency Log Lifecycle explainer for the SCT counts, the operator-diversity rule, the six log states, temporal sharding, the 99 per cent availability requirement, the one-minute maximum merge delay for static-ct-api logs and the 70-day enforcement timeout; the Chrome CT log list version 89.22, log_list_timestamp 17 August 2026, for the operator counts, shard windows and the final tree heads of Sectigo’s Sabre2026h2 and Mammoth2026h2, and the Chrome announcement accepting static-ct-api logs from 1 April 2025; Apple’s Certificate Transparency policy support document of 21 April 2025; the Mozilla wiki page on Certificate Transparency for the Firefox 133, 134 and 135 rollout and the Admissible log state; Google’s Online Security Blog, “Improved Digital Certificate Security”, Somogyi and Eijdenberg, 18 September 2015, and “Sustaining Digital Certificate Security”, Sleevi, 28 October 2015, for the google.com test certificate and the 23, 164 and 2,458 figures; Sleevi’s blink-dev “Intent to Deprecate and Remove: Trust in existing Symantec-issued Certificates”, 23 March 2017, for the 127 and 30,000 figures and the 30 and 42 per cent market shares; “Chrome’s Plan to Distrust Symantec Certificates”, O’Brien, Sleevi and Whalley, 11 September 2017, for the Chrome 66 and Chrome 70 dates; Mozilla’s “Distrusting New WoSign and StartCom Certificates”, 24 October 2016; Google’s “Sustaining Digital Certificate Security - Entrust Certificate Distrust”, 27 June 2024, for the nine roots and the SCT cut-off of 11 November 2024; Cloudflare’s post of 4 September 2025 on the unauthorized issuance of certificates for 1.1.1.1, for the twelve Fina certificates and the response timeline; the SSLMate Certificate Transparency Search API version 1 reference; and a live crt.sh JSON query for example.com made on 18 August 2026. All Merkle tree values here were computed with SHA-256 on 18 August 2026 over the seven laboratory entries in the text.