Skip to content
KEDBYTE
How Identity Works
Chapter
27

The Chain

Part III · The Certificate and the Signature|11,998 words|about 52 min read|Volume 3

27.0 What this chapter gives you#

  1. You will be able to explain why a website certificate is almost never signed directly by anything your computer trusts, and why that is deliberate.
  2. You will be able to state the difference between path building and path validation, name the document governing each, and say why confusing them is the commonest cause of “it works in my browser but not in my program”.
  3. You will be able to walk the RFC 5280 section 6.1 validation algorithm as a list of steps, naming the state variables it carries and the checks that terminate it.
  4. You will be able to draw the certificate graph for a cross-signed authority and count the valid paths through it.
  5. You will be able to say exactly what happened on 30 September 2021, which software broke, which did not, and why the two groups differed.
  6. You will be able to read a name constraints extension, predict which certificates it will and will not stop, and say why so few authorities carry one.
  7. You will be able to compute, from a printed chain, whether a path length constraint has been breached, and reproduce the breach on your own machine.
  8. You will be able to diagnose a broken chain from an error code and know which of six faults you are looking at.
  9. You will be able to verify a real, currently deployed chain by hand, including checking one signature yourself with about twenty lines of arithmetic.

A certificate is a signed statement that a particular name goes with a particular key, and chapter 26 took one apart field by field. This chapter is about what happens next, which is harder than it looks, because a single certificate is almost never enough. The certificate your bank’s website sends you was signed by an authority your computer has never heard of, which holds a certificate signed by another authority, which may or may not be one your computer has heard of. Somewhere at the end of that line of signatures there has to be a key your machine already trusts, not because of any signature but because it was shipped that way. Finding a line of signatures that reaches such a key is the whole job.

The word people use for that line is “the chain”, and the word is doing damage. It suggests a single object, fixed in advance, that the server hands you and that you simply check. None of that is true. A server sends a bag of certificates. There may be several routes through that bag to a key you trust, or none, or one route today and a different one after Tuesday. Two correct programs, given the same bag and the same trusted keys, can reach different answers and both be right by the letter of the specification. That is not a hypothetical: it is why large parts of the internet stopped working on 30 May 2020 and again on 30 September 2021, on both occasions without any certificate being forged, stolen or revoked. Something expired, and half the world’s software could not find its way around it.

So this chapter says something the diagrams do not. Trust does not travel in a straight line from a root to a site. There is no line until somebody builds one, the building is a search through a graph, and the search is where implementations genuinely disagree. The checking that comes afterwards is written down in exact detail in a standards document and is broadly agreed. The searching is described only in an informational document nobody is obliged to follow, and the differences between one program and the next decide whether your customers can reach your site.

Two boundaries, stated once. What is inside a single certificate is chapter 26, “The Certificate”, and we assume nothing from it except that a certificate carries an issuer name, a subject name, a public key, a validity window and a signature. Who may sign certificates, what they must check first, and how root programmes admit and remove them, is chapter 28, “The Certificate Authority”. This chapter lives strictly in between: given a pile of certificates and a list of trusted keys, how does a machine get from one to the other, and how does that go wrong.

The plain version#

A letter that nobody has to telephone about#

Meera has finished college in Kochi and moved to Pune for a job. On her first morning she walks into a bank to open an account. The clerk has never seen her and has no reason to believe anything she says.

She has brought a letter. Her college principal wrote it: Meera Nair studied here from 2022 to 2026, this is her signature, and I vouch for her, and signed it at the bottom.

This is no help by itself. The clerk in Pune has never seen the principal’s signature and cannot tell it from a doodle. He could telephone the college, but there are four hundred colleges and he is not going to spend his morning on the phone.

So the letter comes with a second sheet. The university registrar, who sits above all the colleges in the state, has written: the signature on the letter above belongs to the principal of St Thomas College, who is one of ours. And the registrar has signed that.

Still no help, unless the clerk knows the registrar’s signature. Here is the trick that makes the whole thing work: he does. Behind the counter is a ledger, printed and bound and delivered to every branch of the bank, holding about a hundred and fifty specimen signatures of people the bank has decided to believe. The registrar is in it. Nobody signed the ledger; the bank chose to print it and send it out, and every clerk treats what is inside it as true because it arrived by a route the bank controls.

Now the clerk can work backwards. The registrar’s signature matches the ledger, so the registrar’s note is genuine; the note vouches for the principal’s signature, so the principal’s letter is genuine; the letter vouches for Meera. Three documents, two signature checks, one lookup, and a stranger has been introduced to a stranger without a telephone call.

That is a certificate chain. The bound ledger is your computer’s list of trusted keys, usually called a root store. The registrar is a root, and “root” here means one thing only: somebody in the ledger. The principal is an intermediate, not in the ledger but holding a note from somebody who is. Meera’s letter is the leaf, or end-entity certificate, and it is the one that names the thing you care about.

Why the registrar does not sign every letter#

The obvious question is why the registrar does not sign Meera’s letter directly and save a step. The answer is the reason intermediates exist at all, and it is not a technical nicety. It is about where you keep the pen.

The registrar’s seal is not on the registrar’s desk. It lives in a basement safe with two locks and two keyholders, opened three or four times a year with witnesses and a written record of every impression made. That is what makes the registrar’s signature worth putting in a bank’s ledger. If the seal sat on the desk, somebody would eventually walk off with it, and every letter it had ever signed would become worthless overnight.

You cannot run a service that way; Meera needs her letter today. So on one of those rare basement days the registrar signs a small number of notes delegating authority to the principals, and the seal goes back in the safe. The principals sign letters all day, from a desk, with an ordinary pen, because a principal’s pen going missing is a bad afternoon rather than an extinction event: the registrar cancels that principal’s note, everyone stops honouring letters from that college, and the ledger in every branch is untouched.

That is the whole argument for intermediates. The key everyone has pre-loaded can never be replaced quickly, so it must almost never be used; the key used constantly must be replaceable, so it must not be the pre-loaded one. An intermediate is the joint between those two requirements. It also lets one root support several separate signing operations, so a failure in one does not contaminate the others, and it lets the root stay physically offline.

A pile of paper, not a chain#

Here is where the ordinary explanation starts to mislead, so we will be careful now rather than later.

Meera does not hand the clerk a chain. She hands him an envelope with several sheets, and the clerk must work out which sheet vouches for which and whether the sequence ends in the ledger. Most days the sheets are in a sensible order and this is trivial. Two complications turn a trivial job into a search.

The first is that a person can have more than one note. The registrar retired last year and a new one took over, and the new registrar’s specimen signature is in the newest ledgers but not the old ones still sitting in small branches upcountry. So before leaving, the old registrar signed a note saying the new registrar’s signature is good, and I vouch for it. Now there are two ways to believe the new registrar: a branch with a new ledger recognizes the signature directly, and a branch with an old ledger reaches her through the old registrar’s note. One person, two notes, two routes. This is cross-signing, and it is not an edge case; it is how every large authority manages the move from an old key to a new one, because trust ledgers take ten years to be replaced everywhere in the world.

The second complication is that the clerk’s ledger and your ledger are not the same ledger. The bank next door prints a different one, and a branch that has not had a delivery since 2019 has an older one, so a route that works at one counter fails at another using exactly the same envelope. The person who wrote the letter cannot know which ledger it will be read against, so the honest thing is to put in every sheet that might help and let each clerk find his own way through.

Once you accept those two facts, the job stops being “check the chain” and becomes something else: from the sheets in the envelope, plus the sheets I have in my own drawer, find some sequence that ends in my ledger. There may be several. One may look promising and then turn out to be signed by a registrar whose registration lapsed last month, in which case I must go back and try another. A clerk who gives up at the first dead end will turn away a customer whose paperwork is perfectly good.

What the clerk checks, in order#

Suppose the clerk has found a promising sequence. He goes through it from the top down, from the ledger entry towards Meera, carrying a few running notes on a scrap of paper. At every sheet he asks:

  • Does the signature on this sheet match the signature of the person named on the sheet above it, whom I have already accepted?
  • Is today inside the dates written on this sheet?
  • Has this sheet been cancelled since it was written?
  • Does the name of the person who signed it match, exactly, the name of the person I accepted on the previous step?
  • If a sheet above said “only for students of the science faculty”, is this person inside that limit?
  • If a sheet above said “you may pass this authority on at most once more”, have I now gone one step too far?

If any answer is wrong, this sequence is dead. Not the customer, not the letter: this sequence. He puts it aside and looks for another. Only when every sequence is dead does he turn Meera away.

Notice what is not on that list. He does not ask whether he likes the college, whether Meera seems trustworthy, or whether the principal did a careful job of confirming who she is. Those are real questions and somebody’s job, but not his. His job is narrow and mechanical: is there an unbroken, in-date, uncancelled, in-bounds line of signatures from my ledger down to this letter.

The day the old registrar’s registration lapsed#

One more scene, because it is the story the rest of this chapter is built on, and it really happened.

The old registrar’s certificate of office expired on 30 September 2021. Everybody knew; the date had been printed on the document since the year 2000. Long before that day the new registrar was in every fresh ledger, so almost every branch could recognize her signature directly. But many very old branches, mostly ones that had stopped receiving new ledgers, still had only the old registrar, so the envelopes handed out contained both notes.

On the morning of 30 September 2021 the old registrar’s certificate expired. Branches with a modern ledger were unaffected: they recognized the new registrar directly and never needed the old note. Branches with only the old ledger were, as expected, stuck.

And then a third group broke, which nobody wanted and which is the interesting part. These were branches that had the new registrar in their ledger and could perfectly well have accepted the letter, but whose clerks had been trained to follow every sheet in the envelope to the end, no matter what. Handed an envelope that ended with the old registrar’s expired document, they read to the bottom, saw an expired document, and refused the customer. They were holding a perfectly good route in their own hands and never looked at it, because they had been taught that there is one chain and this is it.

That is what this chapter is really about. Those clerks were not stupid and their software was not broken in any obvious way. They had implemented “check the chain” instead of “find a chain”. Everything from here on is the precise version of that distinction.

Where the plain version stops being true#

There is no “the chain”; there is a bag#

The envelope analogy is close, but the sensible order is not guaranteed and never was.

The honest version: a TLS server sends a list in which only the first entry is fixed. RFC 8446, the TLS 1.3 specification of August 2018, states in section 4.4.2 that “The sender’s certificate MUST come in the first CertificateEntry in the list. Each following certificate SHOULD directly certify the one immediately preceding it.” SHOULD is not MUST, and the same section tells implementations to cope: “all implementations SHOULD be prepared to handle potentially extraneous certificates and arbitrary orderings from any TLS version, with the exception of the end-entity certificate which MUST be first.”

So the correct model is a bag with one labelled item. The first certificate is the server’s own. Everything else is raw material: possibly in order, possibly not, possibly irrelevant, possibly missing the one thing that mattered.

Trust anchors are not root certificates#

In the analogy the ledger contains specimen signatures. In real systems people say the trust store contains root certificates, and that is a convenient lie.

The honest version: what path validation needs from an anchor is a name and a public key. RFC 5280 section 6.1.1 lists the trust anchor input as “the trusted issuer name, the trusted public key algorithm, the trusted public key, and, optionally, the trusted public key parameters”, then says it “may be provided to the path processing procedure in the form of a self-signed certificate”. May. The certificate is packaging. Ryan Sleevi put it plainly in June 2020: a trust anchor “is, at the core, just a CA name and a public key”, it “doesn’t have to be a root certificate, or even a certificate for that matter”.

This matters in practice. If your anchors are names and keys, then on meeting a certificate whose subject and key match an anchor you are finished: you do not care who signed it or when it expires, because you are not relying on it. If your anchors are certificates and you insist on walking to a self-signed one, you will walk straight past a perfectly good anchor. That is the mistake that broke things in 2020 and 2021.

“Root” is a position, not a property#

Nothing in a certificate says “I am a root”. A root and an intermediate have the same fields, and often the very same subject name and key appear in both a self-signed certificate and one signed by somebody else.

The honest version: root, intermediate and leaf are positions in a particular path, not classes of object. The same certificate can be an anchor in one program and an ordinary link in another, on the same machine, in the same second. Let’s Encrypt publishes an authority it calls Root YR, and as of August 2026 there are two certificates for it: one self-signed, one signed by ISRG Root X1. In a store that has Root YR the first is an anchor; in today’s stores, which do not yet have it, the second sits in the middle of a chain and is validated like anything else.

Expiry of an anchor is a matter of opinion#

Everyone agrees that an expired leaf must be rejected. What happens when the thing that has expired is a trust anchor is not settled.

The honest version: RFC 6818 of January 2013, by Peter Yee, exists partly to clarify this, and its clarification is that using the additional information in a self-signed anchor certificate during path validation is optional. So one implementation may enforce the anchor’s notAfter and another may ignore it, and both conform. Android deliberately ignores it. Let’s Encrypt built an entire compatibility strategy on that fact in December 2020, writing that “Android has intentionally chosen not to use the notAfter field of trust anchors”. OpenSSL, by contrast, enforces expiry on anchors. This is an established difference of opinion between correct implementations, not a bug in either.

Building and validating are two algorithms, and only one is standardized#

The plain version had the clerk find a sequence and then check it. That sounds like one activity. It is two, with different literature and very different levels of agreement.

The honest version: path validation is specified in RFC 5280 section 6, is normative, and is broadly implemented the same way. Path building is described in RFC 4158 of September 2005 by Cooper, Dzambasow, Hesse, Joseph and Nicholas, which is Informational, describes strategies rather than requirements, and lists twenty-one optional sorting heuristics that an implementation may use or ignore. Two conforming implementations can therefore search in different orders, give up at different points, and return different answers on the same input.

Worse, many libraries treat them as separate phases: build one chain, hand it to the validator, report what it says. In Sleevi’s phrase the builder “finds a chain” and completes, and if validation says no, nothing goes back to try another route. That is the architecture that fails.

The plain list of checks left out the hard half#

The six questions the clerk asked are real, but RFC 5280’s algorithm has a second half the analogy cannot carry: certificate policy processing, with a policy tree that grows and is pruned at every step, policy mapping between different authorities’ policy identifiers, and three counters called explicit_policy, policy_mapping and inhibit_anyPolicy that decide whether any of it is enforced.

The honest version: on the public web this machinery is almost entirely inert, because clients supply a user-initial-policy-set of any-policy and never demand an explicit policy, so the tree is carried along and never consulted. Inside government and enterprise bridges it is the point of the whole exercise. If half of section 6 reads as irrelevant, that is because on the web it is, and in the world section 6 was written for it is the interesting part.

One certificate does not have one chain#

The largest correction. People speak of “the chain for this certificate” as though it were a property of the certificate, like its serial number.

The honest version: a certificate has as many chains as the graph allows, and the number is a function of the whole ecosystem, not of the certificate. RFC 4158 gives a worked figure with three authorities in which one end-entity certificate has four distinct paths to a trust anchor. Sleevi’s 2020 survey of the Mozilla-trusted graph found four nodes connected by eleven different paths, and that every major certificate authority has at least one authority with two or more issuers. The right question is never “what is the chain” but “which paths exist, and which will this client accept today”.

The technical version#

Roles: root, intermediate, leaf, and what an intermediate buys#

A certification path is an ordered sequence of certificates. RFC 5280 numbers them 1 to n, where certificate 1 is issued by the trust anchor and certificate n is the target, usually the server’s own. The three names in common use map onto that as follows, and describe positions, not types.

Role Signed by Key kept Typical life
Root Itself Offline HSM 20 to 25 years
Intermediate A root Online HSM 3 to 10 years
Leaf An intermediate On the server Days to 1 year

The reason for the middle row is operational. A root key must be pre-installed in hundreds of millions of devices, so it cannot be replaced on any useful timescale: a trust store update that reaches most of the world takes years, and a long tail of devices never updates at all. A key that cannot be replaced must almost never be exposed, so it is generated inside a hardware security module in a room with controlled access, used in ceremonies a handful of times a year under multi-person control and video record, and otherwise powered off.

Nothing that issues millions of certificates a day can work like that. So the root is used, rarely, to sign intermediate certificates, and the intermediates do the daily work from an online module. If an intermediate key is compromised, the authority revokes it and starts using another; the root never moves and every device’s trust store is untouched. That single property is why intermediates exist, and everything else about them is a consequence.

Three secondary benefits follow. Different intermediates can carry different constraints, so an authority can limit one line of issuance without limiting another; they can carry different key types, which is why an authority runs RSA and elliptic curve intermediates side by side; and several in rotation mean revoking one does not stop issuance.

Here is the shape as of August 2026 for one authority, drawn as a graph rather than a line, because that is what it is:

     ISRG Root X1 (a trust anchor today)
          |    |
          |    +------------------+
          v                       v
     ISRG Root X2 signed      Root YR signed
     by X1                    by X1
          |                       |
          v                       v
     Root YE signed by X2     YR1, YR2, YR3
          |                    (RSA 2048)
          v                       |
     YE1, YE2, YE3                v
     (ECDSA P-384)             leaf (RSA)
          |
          v
     leaf (ECDSA)

Every arrow is a certificate. Root YE and Root YR each also have a self-signed certificate of their own, so each appears in the world twice. That duplication is the subject of the cross-signing section below.

Two algorithms, and why the distinction is the whole chapter#

Path validation answers: given an ordered sequence of certificates, a trust anchor and a time, is this sequence acceptable. It is a straight-line computation with a yes or no answer, specified in RFC 5280 section 6, of May 2008, by Cooper, Santesson, Farrell, Boeyen, Housley and Polk. It is normative: conforming implementations must produce the same verdict.

Path building answers: given a target certificate, a bag of other certificates, a set of trust anchors and possibly some way of fetching more, is there any ordered sequence at all that path validation will accept, and what is it. It is a search through a directed graph, described in the Informational RFC 4158 of September 2005. Nobody is required to implement it, and the document itself says the method of discovery does not affect the correctness of validation.

The graph has certificates for nodes and issuer-subject relationships for edges. RFC 4158’s worked figure has three authorities, A, B and C, each with more than one certificate, and one end-entity certificate called EE. There are four paths from EE to the trust anchor, written here with the arrow meaning “was signed by”:

EE -> B1 -> A1 -> anchor
EE -> B1 -> A2 -> C1 -> anchor
EE -> B2 -> C1 -> anchor
EE -> B2 -> C2 -> A1 -> anchor

If C1 is expired, two of those four paths die and two survive. An implementation that finds the second path first, hands it to the validator and reports its “no” has failed at a problem with two correct answers sitting next to it. One that treats failure as a signal to backtrack finds one of them.

The rule to remember: verification must be part of the search, not a step after it. Sleevi’s list of properties for a robust implementation puts it as “integrate any checks as part of path building, such that path verification is merely a part of path building”, adding that “if a chain doesn’t verify, path building continues and tries to exhaust all paths before returning”.

A second practical rule: the search must be bounded. A hostile or merely silly collection of certificates can produce an enormous graph, and cycles are possible because two authorities can cross-sign each other. RFC 4158 forbids repeating an identical certificate in a path and recommends the stronger rule of never repeating the same public key and subject name pair, which kills the superfluous paths that bridged environments generate. Real implementations add hard limits: Go’s crypto/x509 defines maxChainSignatureChecks = 100, with a comment noting that most chains are under fifteen certificates long so the budget leaves room for several candidates and for wasted checks caused by different intermediates sharing a subject name, and exposes a MaxConstraintComparisions option because “this limit prevents pathological certificates from consuming excessive amounts of CPU time when validating”.

RFC 4158’s twenty-one heuristics in section 3.5 make the search fast rather than correct: prefer candidates whose basicConstraints has the cA flag set, whose keyUsage is right, whose validity covers now, whose key identifiers match, whose names look similar, and whose signatures you have already verified and cached. None of these change the answer. They change how long you take to find it, and whether you find it before your budget runs out.

RFC 5280 section 6.1, stated as steps#

What follows is the normative algorithm, compressed but not simplified. The section numbers are exact.

6.1.1 Inputs. Nine inputs, in order: a prospective certification path of length n; the current date and time; the user-initial-policy-set, being the policy identifiers acceptable to the caller, which contains the special value any-policy if the caller does not care; trust anchor information, comprising the trusted issuer name, the trusted public key algorithm, the trusted public key and optionally its parameters; and five flags and sets, being initial-policy-mapping-inhibit, initial-explicit-policy, initial-any-policy-inhibit, initial-permitted-subtrees and initial-excluded-subtrees. The last two were added in RFC 5280 and let a caller constrain a path from outside the certificates, which is how a root programme can pin an authority to a set of names without its cooperation.

6.1.2 Initialization. Eleven state variables are set up before the first certificate is touched. Those carrying the checks a web client cares about:

Variable Initial value
permitted_subtrees initial-permitted-subtrees
excluded_subtrees initial-excluded-subtrees
working_issuer_name anchor’s name
working_public_key anchor’s key
max_path_length n

The remaining six are the policy machinery: valid_policy_tree, explicit_policy, inhibit_anyPolicy, policy_mapping, working_public_key_algorithm and working_public_key_parameters.

6.1.3 Basic certificate processing. For each certificate i from 1 to n, working downwards from the anchor:

  1. Step (a) verifies the basic certificate information, and the certificate must satisfy all four of: (1) the signature on the certificate verifies using working_public_key_algorithm, working_public_key and working_public_key_parameters; (2) the certificate validity period includes the current time; (3) at the current time the certificate is not revoked, which may be determined by a certificate revocation list under section 6.3, by status information, or out of band; and (4) the certificate issuer name is the working_issuer_name.
  2. Step (b) checks, unless the certificate is self-issued and not the last in the path, that the subject name is within one of the permitted_subtrees for distinguished names, and that each alternative name in the subjectAltName extension, critical or not, is within one of the permitted_subtrees for that name type.
  3. Step (c) does the mirror image for excluded_subtrees: the subject name and each subject alternative name must not fall inside any excluded subtree.
  4. Step (d) processes the certificate policies extension against the valid_policy_tree, creating and pruning nodes.
  5. Step (e) sets valid_policy_tree to NULL if the certificate policies extension is absent.
  6. Step (f) verifies that either explicit_policy is greater than zero or the valid_policy_tree is not NULL.

If step (a), (b), (c) or (f) fails, the procedure terminates with a failure and a reason. Note what is in step (a): signature, dates, revocation and issuer name matching. Note equally what is not: nothing here checks the hostname. That is a separate operation the application performs on the final certificate, and a chain can validate perfectly for a name you did not ask for.

6.1.4 Preparation for certificate i+1. If certificate i was not the last, fifteen steps labelled (a) to (o) prepare the state. Those that matter outside policy processing are:

  1. Step (c) assigns the certificate’s subject name to working_issuer_name.
  2. Step (d) assigns the certificate’s subjectPublicKey to working_public_key, and steps (e) and (f) carry the algorithm and parameters, with null or omitted parameters inherited only if the algorithm is the same.
  3. Step (g) folds a name constraints extension into the state: permitted subtrees are intersected with the previous value, excluded subtrees are unioned with it. The specification’s own examples are that the intersection of example.com and foo.example.com is foo.example.com, the intersection of example.com and example.net is the empty set, and the union of example.com and foo.example.com is example.com.
  4. Step (k) verifies, for a version 3 certificate, that basicConstraints is present and that cA is TRUE.
  5. Step (l) verifies that max_path_length is greater than zero and decrements it by one, for any certificate that is not self-issued.
  6. Step (m) sets max_path_length to pathLenConstraint if that field is present and smaller than the current value.
  7. Step (n) verifies, if a key usage extension is present, that the keyCertSign bit is set.
  8. Step (o) recognizes and processes any other critical extension. An unrecognized critical extension is a hard failure; that is what critical means.

If check (a), (k), (l), (n) or (o) fails, the procedure terminates with a failure. Otherwise i is incremented and 6.1.3 runs again.

6.1.5 Wrap-up. Seven steps labelled (a) to (g) close out the target certificate: decrement explicit_policy, honour a requireExplicitPolicy of zero, carry the final public key and algorithm forward, process remaining critical extensions, and intersect the valid_policy_tree with the user-initial-policy-set. 6.1.6 Outputs are success or failure, plus, on success, the final public key and the policy information.

Two structural observations people miss. First, the algorithm walks from the anchor towards the leaf, the opposite direction from how a builder searches, since a builder works up from the leaf because that is the certificate it holds. Second, the algorithm takes a path as input and has nothing to say about where the path came from or what to do if it fails. That silence is the gap RFC 4158 exists to fill, and the gap badly designed libraries fall into.

Cross-signing, and how one certificate acquires several paths#

An authority is a name and a key; a certificate is a statement about a name and a key. No rule says only one such statement may exist, and there are excellent reasons to have several.

The mechanism: two different issuers each sign a certificate containing the same subject name and the same subjectPublicKeyInfo. The two differ in issuer, serial number, validity, extensions and signature, and both are entirely valid. Anything signed by that authority’s private key now verifies under either, because both carry the same public key.

The three reasons, in order of how often you meet them:

  1. Bootstrapping a new root. A new root takes five to ten years to reach most devices. Until then, an old root that is already everywhere signs a certificate for the new root’s name and key. New devices use the new root directly; old devices reach it through the old one.
  2. Bridging two hierarchies. Two organizations each cross-certify the other’s authority so that relying parties in either can validate the other’s certificates. This is the world RFC 4158 was written for, and where the policy machinery earns its keep.
  3. Algorithm migration. An elliptic curve root is small and fast but young; cross-signing it from an established RSA root lets a subscriber offer a short chain to modern clients and a longer one to everything else.

The live example, as of August 2026, shows all three ideas at once. Let’s Encrypt’s chains-of-trust page, last updated 8 July 2026, lists two chains a subscriber may be given for its RSA intermediate YR1 and three for its elliptic curve intermediate YE1:

Intermediate Chain offered
YR1 YR1, Root YR, X1 (default)
YR1 YR1, Root YR
YE1 YE1, YE, X2, X1 (default)
YE1 YE1, YE, X2
YE1 YE1, YE

Read the first column as the certificate that signed the leaf, and each row as what the server would send after the leaf. The default RSA chain is three certificates deep after the leaf; the default elliptic curve chain is four. Root YE and Root YR are called roots, yet in the default chains they appear as ordinary intermediates, because as of August 2026 they are not yet in any major trust store: chains terminating there “are not expected to work with any of the major trust stores, as those roots have not yet been incorporated”. They exist now so that in five years they can be anchors. The subscriber is choosing between a long chain that works on more clients and costs more bytes, and a short one that fails on anything whose store predates the newer root. There is no correct answer, only a trade against your own visitors.

A real chain, printed and validated by hand#

Everything here was produced by running the commands shown, in August 2026, against files published by the issuing authority and the root store in Debian’s ca-certificates package version 20240203, which holds 146 roots. The tool is OpenSSL 3.0.13. The path has three certificates: the anchor ISRG Root X1, in the store; a certificate for Root YR signed by ISRG Root X1; and the intermediate YR1 signed by Root YR:

for f in isrgrootx1 root-yr-by-x1 int-yr1; do
  openssl x509 -in $f.pem -noout -subject -issuer -dates
done
subject= CN = ISRG Root X1
issuer=  CN = ISRG Root X1
notBefore=Jun  4 11:04:38 2015 GMT
notAfter =Jun  4 11:04:38 2035 GMT

subject= CN = Root YR
issuer=  CN = ISRG Root X1
notBefore=May 13 00:00:00 2026 GMT
notAfter =Sep  2 23:59:59 2032 GMT

subject= CN = YR1
issuer=  CN = Root YR
notBefore=Sep  3 00:00:00 2025 GMT
notAfter =Sep  2 23:59:59 2028 GMT

The organization fields have been dropped to fit the page; in full they are “Internet Security Research Group”, “ISRG” and “Let’s Encrypt”. Two things to notice. The subject of each is the issuer of the next, which is what RFC 5280 step 6.1.3(a)(4) requires. And the middle certificate begins on 13 May 2026, more than eight months after the one below it begins; a cross-signature can be issued long after the thing it vouches for.

Now the linkage that lets a builder find these at all. Chapter 26 covered these extensions in detail; here they are as search keys:

ISRG Root X1     SKI 79:B4:59:E6:7B:B6:E5:E4:01:73:
                     80:08:88:C8:1A:58:F6:E9:9B:6E
Root YR (cross)  AKI 79:B4:59:E6:...:9B:6E   (matches above)
                 SKI DE:E7:5B:60:D0:22:6D:40:28:7D:
                     3F:0D:01:FE:A4:B5:52:B4:51:94
YR1              AKI DE:E7:5B:60:...:51:94   (matches above)
                 SKI 1F:2F:35:BE:46:14:82:CD:40:B1:
                     AE:79:2C:55:78:FA:F7:D4:68:FB

Those identifiers are hints, not proof. A builder uses them to shortlist candidate parents and must still check the signature, because anybody can write any value they like into a certificate they mint themselves.

The constraints each carries:

ISRG Root X1     basicConstraints critical CA:TRUE
                 keyUsage critical keyCertSign, cRLSign
Root YR (cross)  basicConstraints critical CA:TRUE
                 keyUsage critical keyCertSign, cRLSign
                 extendedKeyUsage serverAuth
YR1              basicConstraints critical CA:TRUE,
                                  pathlen:0
                 keyUsage critical digitalSignature,
                          keyCertSign, cRLSign
                 extendedKeyUsage serverAuth

Now run the algorithm the easy way, asking the tool to show its work:

openssl verify -show_chain -CAfile isrgrootx1.pem \
  -untrusted root-yr-by-x1.pem int-yr1.pem
int-yr1.pem: OK
Chain:
depth=0: CN = YR1 (untrusted)
depth=1: CN = Root YR (untrusted)
depth=2: CN = ISRG Root X1

Take away the cross-signature and there is no path at all, because ISRG Root X1 did not sign YR1 and nothing bridges the gap:

openssl verify -CAfile isrgrootx1.pem int-yr1.pem
CN = YR1
error 20 at 0 depth lookup:
  unable to get local issuer certificate
error int-yr1.pem: verification failed

Now the part usually taken on faith: check one signature by hand. The claim is that the bytes of YR1’s tbsCertificate, hashed with SHA-256 and signed with Root YR’s private key, produce the signature at the end of the YR1 file. We have Root YR’s public key, so we can run the arithmetic backwards. RSA verification is one modular exponentiation: raise the signature, read as a large integer, to the public exponent, modulo the modulus, and see what falls out.

import hashlib
tbs = leaf_der[4:4+711]          # tbsCertificate, exactly
s   = int.from_bytes(sig, 'big') # 512-byte signature
m   = pow(s, e, n)               # e = 65537, n = 4096 bits
mb  = m.to_bytes(512, 'big')
print(mb[:8].hex())              # padding
print(mb[-32:].hex())            # the digest
print(hashlib.sha256(tbs).hexdigest())
0001ffffffffffff
bb75ce0adb0bc8b9338263d134b2e83e
4f828d882384bac4c7236da3b8bc2749
bb75ce0adb0bc8b9338263d134b2e83e
4f828d882384bac4c7236da3b8bc2749

The digest recovered from the signature and the digest we computed ourselves are the same 32 bytes, split across two lines here only to fit the page. That is step 6.1.3(a)(1) performed by hand on a certificate in live use. The bytes immediately before the digest are 30 31 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20, the PKCS number 1 version 1.5 DigestInfo structure naming SHA-256; the long run of ff before that is padding.

Walk the rest of section 6.1 by hand, with n equal to 2 because there are two certificates below the anchor:

Step Certificate 1 (Root YR) Result
6.1.3(a)(1) Signed by X1 key pass
6.1.3(a)(2) 2026 in 2026-2032 pass
6.1.3(a)(4) Issuer = ISRG Root X1 pass
6.1.4(k) cA TRUE pass
6.1.4(l) max_path_length 2 -> 1 pass
6.1.4(m) No pathLenConstraint 1
6.1.4(n) keyCertSign set pass

And for the second certificate:

Step Certificate 2 (YR1) Result
6.1.3(a)(1) Signed by Root YR pass
6.1.3(a)(2) 2026 in 2025-2028 pass
6.1.3(a)(4) Issuer = Root YR pass
6.1.5 Target reached OK

Had the target been a server certificate issued by YR1, there would be a third pass through 6.1.3, and before it a pass through 6.1.4 for YR1 in which step (l) takes max_path_length from 1 to 0 and step (m) keeps it at 0 because YR1’s pathLenConstraint is 0. A fourth certificate would then fail at step (l).

The DST Root expiry of 30 September 2021, worked#

This is the case everyone half-remembers. Here it is with the dates and the certificates. DST Root CA X3 was a root certificate operated by IdenTrust, valid from 30 September 2000 at 21:12:19 GMT to 30 September 2021 at 14:01:15 GMT. It had been in trust stores for two decades, including in devices that would never receive another update. Let’s Encrypt’s own root, ISRG Root X1, was generated on 4 June 2015 and had been accumulating inclusions since, but a large population of devices, particularly Android handsets older than version 7.1.1, would never receive it.

On 6 November 2020 Let’s Encrypt announced a plan to stand on its own root from 11 January 2021. On 21 December 2020 Josh Aas and Aaron Gable published “Extending Android Device Compatibility for Let’s Encrypt Certificates” and cancelled it, because a better plan had appeared: IdenTrust agreed to issue a three-year cross-signature for ISRG Root X1 from DST Root CA X3, and that cross-signature would be allowed to outlive its own issuer. Their reasoning was stated plainly: root stores “contain trust anchors”, not certificates, “and the standards for verifying certificates allow implementations to choose whether or not to use fields on trust anchors. Android has intentionally chosen not to use the notAfter field of trust anchors.”

That cross-signature can be read. It runs from 20 January 2021 at 19:14:03 GMT to 30 September 2024 at 18:14:03 GMT, its issuer is “O = Digital Signature Trust Co., CN = DST Root CA X3” and its subject is ISRG Root X1. It outlived its issuer’s own self-signed certificate by three years to the day.

The chain the server sent changed shape three times:

From Default chain after leaf
Before Dec 2020 R3, DST Root CA X3
Early 2021 R3, X1-cross, DST X3
After Feb 2024 R3 alone, up to X1

The middle row is the interesting one. The December 2020 post spelled it out as “Subscriber Certificate, R3, ISRG Root X1, DST Root CA X3”, noting that this made the chain larger by one certificate and handshakes correspondingly less efficient, and that the trade was worth it.

Now the morning itself. Four populations of software, four outcomes.

  1. Modern clients with ISRG Root X1 in the store. They received the long chain, recognized ISRG Root X1 by name and key as an anchor they already held, stopped there, and never examined the expired DST certificate. This is the large majority, and nothing happened.
  2. Old Android devices without ISRG Root X1. They walked the full chain to DST Root CA X3, found it expired, and ignored the expiry because Android does not enforce notAfter on anchors. Nothing happened, which was the entire point of the December 2020 exercise.
  3. Old clients with neither ISRG Root X1 nor Android’s leniency. They broke, as announced. Devices that stopped updating before ISRG Root X1 shipped had no route.
  4. Clients that had ISRG Root X1 and broke anyway. The group that made the day famous. Let’s Encrypt’s documentation is direct: “if clients of your API are using OpenSSL, they must use version 1.1.0 or later. In OpenSSL 1.0.x, a quirk in certificate verification means that even clients that trust ISRG Root X1 will fail when presented with the Android-compatible certificate chain we are recommending by default.”

Group four is the lesson: they held the answer and could not see it. Their verifier insisted on walking to a self-signed certificate, so on reaching the cross-signed ISRG Root X1 it did not stop, went one link further to DST Root CA X3, and applied the expiry check to a certificate it did not need.

We can reproduce all of this in a laboratory in a few minutes. Build an expired old root, a new root with two certificates for the same key, an intermediate under the new root and a leaf, then serve the chain the way Let’s Encrypt served it in 2021:

cat inter.pem newroot-cross.pem > served.pem
cat oldroot.pem newroot.pem     > store-both.pem
openssl verify -show_chain -CAfile store-both.pem \
  -untrusted served.pem leaf.pem
leaf.pem: OK
Chain:
depth=0: CN = shop.kedbyte.example (untrusted)
depth=1: CN = KedByte CA G1 (untrusted)
depth=2: CN = New Root CA

The verifier stopped at depth 2, at the new root, even though the bag contained a further certificate leading to the expired old root. Now take the new root out of the store, leaving only the expired old one:

openssl verify -CAfile oldroot.pem \
  -untrusted served.pem leaf.pem
CN = Old Root CA
error 10 at 3 depth lookup: certificate has expired
error leaf.pem: verification failed

Depth 3 is the anchor itself. That line is what several million devices printed on 30 September 2021, and what a badly built verifier printed even with the new root sitting in its store.

The ending shows the cost of compatibility. On 10 July 2023 Aaron Gable published “Shortening the Let’s Encrypt Chain of Trust”, reporting that the proportion of Android devices trusting ISRG Root X1 had risen from 66 per cent to 93.9 per cent, and announcing the retirement. On 8 February 2024 Let’s Encrypt stopped providing the cross-signature by default, and it expired on 30 September 2024. The manoeuvre bought three years and nine months of compatibility for devices that could not be updated, at a cost of one extra certificate in every handshake in the world.

Name constraints, and why almost nobody uses them#

A name constraints extension lets an issuer write into a certificate the set of names its holder may certify. RFC 5280 section 4.2.1.10 defines it, object identifier 2.5.29.30, and the syntax is small:

NameConstraints ::= SEQUENCE {
  permittedSubtrees [0] GeneralSubtrees OPTIONAL,
  excludedSubtrees  [1] GeneralSubtrees OPTIONAL }

GeneralSubtree ::= SEQUENCE {
  base    GeneralName,
  minimum [0] BaseDistance DEFAULT 0,
  maximum [1] BaseDistance OPTIONAL }

The specification is unusually blunt. The extension “MUST be used only in a CA certificate”; conforming authorities “MUST mark this extension as critical”; and within the profile “the minimum and maximum fields are not used with any name forms, thus, the minimum MUST be zero, and maximum MUST be absent”, so a GeneralSubtree is in practice just a name. An empty sequence is forbidden.

The matching rules per name type are worth memorizing, because guessing them wrongly is how people build constraints that do not constrain:

Name type Constraint Matches
dNSName host.example.com www.host.example.com
dNSName host.example.com not host1.example.com
URI .example.com my.host.example.com
URI .example.com not example.com
iPAddress 8 octets IPv4 CIDR range

For DNS names, “any DNS name that can be constructed by simply adding zero or more labels to the left-hand side of the name satisfies the name constraint”, so example.com permits itself and everything under it. For URIs and mail addresses a leading full stop means “strictly below this”, a different rule in the same extension, and that asymmetry is a genuine trap. For IP addresses the encoding is a range, not an address: an IPv4 constraint is eight octets, address then mask, so 192.0.2.0/24 is C0 00 02 00 FF FF FF 00, and an IPv6 constraint is thirty-two octets.

Section 6.1.4(g) defines the accumulation as the path descends: permitted subtrees intersect, excluded subtrees union. Intersection means constraints can only ever get tighter going down a path, never looser, which is the property that makes the mechanism safe.

Here is the extension working. In a laboratory hierarchy a subordinate is issued with a critical name constraints extension whose permittedSubtrees holds one dNSName, kedbyte.example, and then issues two leaf certificates, one inside that subtree and one outside:

openssl verify -CAfile root.pem -untrusted sub.pem in.pem
openssl verify -CAfile root.pem -untrusted sub.pem out.pem
in.pem: OK

CN = shop.example.org
error 47 at 0 depth lookup: permitted subtree violation
error out.pem: verification failed

The subordinate’s private key signed both certificates and both signatures are mathematically perfect. The second is refused because the verifier read a constraint written by the authority above it. That containment is what makes a compromised subordinate a limited problem instead of an unlimited one.

So why is the public web not full of name-constrained authorities. There are five reasons, and only the first is technical.

  1. The “restrictions apply only when the specified name form is present” rule. RFC 5280 says it in those words and adds: “If no name of the type is in the certificate, the certificate is acceptable.” A dNSName constraint therefore does nothing to a certificate that carries no dNSName. For years this interacted horribly with the Common Name field: a client that falls back to reading a hostname out of the Common Name while applying dNSName constraints only to subjectAltName can be walked straight past the constraint. Sleevi’s warning is exact: if a library supports that fallback “and doesn’t enforce these constraints, the CA can bypass the nameConstraints entirely”. Chrome removed the fallback in version 58 in April 2017, but the shape of the hole is worth remembering.
  2. Patchy support, historically. RFC 5280 only requires implementations to process constraints on the directoryName form and merely says they SHOULD handle rfc822Name, uniformResourceIdentifier, dNSName and iPAddress. Older OpenSSL releases and older macOS did not implement them at all, so a critical name constraints extension made a certificate fail everywhere. The CA/Browser Forum’s response was to permit the extension to be non-critical, on the grounds that a constraint honoured by half the world beats no constraint at all. That decision is a fair summary of the whole subject.
  3. Silent failure is the default failure. Get a constraint wrong and nothing happens until a certificate is refused in the field, often months later and on one platform only. Netflix’s bettertls project exists because implementations disagree here and the disagreements are invisible without a test suite.
  4. Commercial reality. A public authority sells the ability to certify any name in the world; constraining a subordinate to one customer’s domains is the right security decision and a commercially narrow product.
  5. The paperwork is strict. The CA/Browser Forum defines a technically constrained subordinate in its TLS Baseline Requirements: up to about version 1.7.0 the rule sat in section 7.1.5, and in the current profile the substance is in section 7.1.2.5.2, “Technically Constrained TLS Subordinate CA Name Constraints”, with the field table in 7.1.2.10.8. Such a subordinate must carry an extended key usage limiting it to server authentication, and its constraint must close off every namespace it does not open: with no dNSName in permittedSubtrees it “MUST include a zero-length dNSName to indicate no domain names are permitted”, and with no IPv4 address it must exclude 0.0.0.0/0 by including “an iPAddress of 8 zero octets”.

Where name constraints do get used they matter, and the clearest public case is the ANSSI incident. On 7 December 2013 Google’s security blog reported that an intermediate certificate signed by ANSSI, the French network and information security agency, had been used to issue certificates for Google domains; ANSSI said the intermediate had been placed in a commercial traffic-inspection device on a private network. Chrome blocked the intermediate, and then, in an update dated 12 December 2013, restricted the ANSSI authority in future versions of Chrome to a list of French-related top-level domains: fr, gp, gf, mq, re, yt, pm, bl, mf, wf, pf, nc and tf. That is a name constraint applied by the relying party rather than by the issuer, which is exactly what RFC 5280’s initial-permitted-subtrees input to section 6.1.1 is for. Root programmes apply similar restrictions to other government authorities; the CCADB report of certificates in Firefox notes one that is “technically constrained with restrictions to issue SSL Certificate for domain names under the top level domain .tn”.

Path length constraints and their enforcement#

The path length constraint is the smaller sibling of name constraints and is enforced far more consistently. It lives inside basic constraints, RFC 5280 section 4.2.1.9, object identifier 2.5.29.19:

BasicConstraints ::= SEQUENCE {
  cA                BOOLEAN DEFAULT FALSE,
  pathLenConstraint INTEGER (0..MAX) OPTIONAL }

The specification’s own words repay careful reading: “The pathLenConstraint field is meaningful only if the cA boolean is asserted and the key usage extension, if present, asserts the keyCertSign bit. In this case, it gives the maximum number of non-self-issued intermediate certificates that may follow this certificate in a valid certification path.” Then the parenthesis that catches everybody: “Note: The last certificate in the certification path is not an intermediate certificate, and is not included in this limit.”

So pathLenConstraint counts intermediates below this certificate, not certificates. Zero means this authority may issue end-entity certificates and nothing else; one means it may issue one more layer of authority beneath it. Two further rules matter: the field must be zero or more, and self-issued certificates, where issuer and subject are the same name, do not count against the budget, so that an authority can roll its own key over without consuming path length.

Enforcement is section 6.1.4 steps (l) and (m). The state variable max_path_length starts at n, the length of the path. Step (l) decrements it by one for each non-self-issued certificate and fails if it is already zero. Step (m) lowers it to any smaller pathLenConstraint it meets. Because step (m) only ever lowers, the constraint is monotonic: a certificate cannot raise the budget its parent set.

The real intermediate we printed earlier carries CA:TRUE, pathlen:0, which is the normal setting for a public web authority and means YR1 may issue server certificates and may not issue another authority. Here is what happens if an authority ignores that. In the laboratory, a subordinate with pathlen:0 issues a grandchild authority, which issues a leaf, giving a four-certificate path:

openssl verify -CAfile root.pem -untrusted sub.pem \
  -untrusted grand.pem deep.pem
CN = Lab Sub CA
error 25 at 2 depth lookup:
  path length constraint exceeded
error deep.pem: verification failed

The error names depth 2, the subordinate whose constraint was breached, not the leaf; that is the right place to report it and to fix it. Now the mirror image, which is the trap in enterprise deployments. Trust the grandchild authority directly, as a partial chain, and the same leaf verifies:

openssl verify -partial_chain -CAfile grand.pem deep.pem
deep.pem: OK

Nothing about the leaf changed. What changed is where the path starts, and therefore whether the constraint is on the path at all. A constraint written by an authority you have bypassed constrains nothing. This is the same principle as the trust anchor discussion earlier, seen from the other side: the checks that protect you are the checks on the path you actually built.

Chain building failures in the wild, and how to diagnose them#

Almost every real chain failure is one of six things. Here they are with the verdict a common tool gives, so you can recognize them by sight. The codes and messages are OpenSSL 3.0.13’s:

Code Message Usual cause
20 unable to get local issuer Missing intermediate
10 certificate has expired Anything on the path
19 self-signed cert in chain Untrusted root sent
18 self-signed certificate Wrong file entirely
25 path length exceeded Over-deep hierarchy
47 permitted subtree violation Name constraint hit

The six faults, in the order to suspect them:

  1. The missing intermediate. The server sends only its own certificate. It works in Chrome on Windows and fails in curl on Linux, and the operator concludes the Linux box is broken. It is not: Windows and macOS verifiers fetch a missing issuer over the network using the authorityInfoAccess extension’s caIssuers pointer, and Firefox holds a preloaded cache of intermediates disclosed to the CCADB. OpenSSL fetches nothing and caches nothing. Configure the server correctly rather than relying on the client’s generosity, because the set of clients that will rescue you is not one you control.
  2. The expired link. Something on the path has run out, very often not the leaf, because the leaf is the one thing renewed automatically. An expired intermediate, cross-signature or anchor all give code 10 at a different depth. Read the depth.
  3. The wrong intermediate. The authority rotated intermediates and the deployment pipeline pinned a copy on disk years ago, so the file vouches for nothing. This also gives code 20, which is why diagnosis must include printing the issuer of the leaf and the subject of everything the server sent, and checking that they line up.
  4. The unusable extra. The server sends the root too. Usually harmless and merely wasteful; occasionally fatal, when the root sent is an expired cross-signed one and the client is naive. That is the September 2021 shape.
  5. The constraint. Codes 25 and 47. Rare on the public web, common in corporate hierarchies where somebody built three layers under an authority that permits one.
  6. The client that cannot backtrack. The hardest to see, because the server is correct and most clients are fine. The signature is a failure specific to one library or one old version, on a chain where an alternative path demonstrably exists.

A diagnostic procedure that resolves all six runs in four commands. First ask what the server actually sends and in what order; then ask what each certificate claims; then ask whether the bag stands up against your store; then ask whether it stands up against a different store.

openssl s_client -connect example.com:443 \
  -servername example.com -showcerts </dev/null

openssl x509 -in cert.pem -noout -subject -issuer -dates

openssl verify -show_chain -CAfile roots.pem \
  -untrusted chain.pem leaf.pem

openssl verify -show_chain -CAfile other-roots.pem \
  -untrusted chain.pem leaf.pem

The first command is the one people skip and the one that answers the question: -showcerts prints the bag exactly as it arrived, in the order it arrived. If the bag is missing a certificate, no client-side investigation will help. The last command separates fault six: if the same bag verifies against one store and not another you have a trust store difference, and if it fails against a store that demonstrably contains a usable anchor you have a builder that cannot backtrack. Two habits are worth having: keep the leaf and the chain in separate files, because concatenating them hides ordering mistakes, and test from outside your own network, because a corporate inspection appliance substitutes its own chain and gives you a confident, irrelevant answer.

Where implementations actually disagree#

“Browsers are lenient and libraries are strict” is not the shape of the disagreement, so it is worth being concrete about who does what.

The 2020 AddTrust expiry is the cleanest natural experiment. The self-signed AddTrust External CA Root expired on 30 May 2020 at 10:48 GMT, an event scheduled twenty years in advance. In RFC 4158’s terms it was the expiring certificate, and USERTrust RSA Certification Authority was the authority reachable by two routes: through AddTrust, or directly, because USERTrust was itself already a trust anchor in modern stores. Correct implementations swapped one for the other and noticed nothing. The failures, catalogued by Sleevi on 24 June 2020 and by Andrew Ayer in a companion piece, were not confined to obscure embedded software: they included devices shipping OpenSSL 1.0.2h, the version of curl distributed with macOS, which used LibreSSL, and Debian’s apt, which used GnuTLS.

The survivors all treat the problem as graph traversal with verification folded into the search, as RFC 4158 describes. Sleevi’s note that Go’s verifier “leaves a number of optimizations on the table” and still does the search “in a little less than 400 lines of code” answers any claim that this is hard.

The differences that matter in 2026 are these, each dated because each moves:

Verifier Fetches missing issuers
Windows CryptoAPI Yes, over the network
Apple Security framework Yes, over the network
Firefox No; uses a preload cache
OpenSSL No

Firefox’s preloading of intermediate certificates disclosed to the Common CA Database, delivered through its Remote Settings channel, began landing in 2019; Mozilla’s stated motivation was that the unknown-issuer error was its most common secure connection failure. Chrome went the other way: the Chrome Root Program was announced in September 2022, and the Chrome Root Store and Certificate Verifier began rolling out on Windows and macOS in Chrome 105, replacing the platform verifier with Chrome’s own so that the same chain logic runs everywhere Chrome runs. As of August 2026 the consequence is that a chain which works in Chrome may still fail in a command-line tool on the same machine, because they no longer share a verifier or a store.

One more difference will surprise you. Two clients on the same machine can disagree about whether a certificate is expired, because they disagree about whether the expired thing is on the path. That is not a clock problem. It is a path building problem wearing a clock problem’s clothes.

Serving the right chain#

The practical advice that falls out of all of this is short.

Send the intermediates and do not send the root. The root is either in the client’s store, in which case sending it wastes bytes in every handshake, or it is not, in which case it proves nothing. The exception is the case engineered in 2021, where the extra certificate is a cross-signature a specific population needs: a deliberate compatibility decision with a measured cost, not a default.

Deploy the chain your issuer gives you at issuance, not a copy of an intermediate saved on disk years ago. Let’s Encrypt’s guidance to client developers in December 2020 was exactly this: use “the intermediate certificate provided by the ACME API at the end of issuance”, and do not retrieve intermediates “by other means, e.g. hardcoding them, reusing what is on disk already, or fetching from AIA URLs”. A chain refreshed only when a human remembers will eventually be wrong.

Choose between alternative chains on evidence about your own visitors. Longer chains buy compatibility with old devices and cost bytes on every connection; shorter chains do the reverse. The mechanism exists in ACME clients, for example Certbot’s --preferred-chain option, and chapter 28, “The Certificate Authority”, covers the issuance protocol itself.

Finally, monitor the expiry of every certificate on the path, not just the leaf, because the leaf is the one thing that renews itself. Every large chain failure of the last decade has been something above the leaf running out on a date printed on it years in advance that nobody had put in a calendar.

27.98 Common wrong ideas#

Wrong: A certificate has a chain, and the server sends it. Right: A certificate has as many chains as the graph allows, the server sends an unordered bag in which only the first entry is guaranteed to be its own certificate, and RFC 8446 section 4.4.2 tells clients to expect extraneous certificates and arbitrary orderings.

Wrong: Path validation is the algorithm that checks certificates. Right: Path validation, RFC 5280 section 6, takes a finished path as input and returns yes or no; finding a path in the first place is a separate graph search described only in the Informational RFC 4158 of September 2005, and it is in that unstandardized half that implementations differ enough to break the web.

Wrong: If validation fails, the certificate is bad. Right: Validation fails for one path, and a correct implementation treats that as a signal to backtrack and try another; the AddTrust failures of 30 May 2020 and the OpenSSL 1.0.x failures of 30 September 2021 were both cases where a valid path existed and the software never looked at it.

Wrong: A root certificate is a special kind of certificate. Right: Root, intermediate and leaf are positions in a particular path, not properties of an object; what a validator actually needs from an anchor is a name and a public key, and RFC 5280 section 6.1.1 says the self-signed certificate is merely one optional way of packaging that information.

Wrong: An expired root breaks everything that chains to it. Right: Whether an anchor’s own validity dates are enforced is implementation defined, clarified as optional by RFC 6818 of January 2013; Android deliberately ignores notAfter on trust anchors, which is the entire reason the DST Root CA X3 cross-signature of 20 January 2021 was allowed to run until 30 September 2024, three years past its issuer’s own expiry.

Wrong: pathLenConstraint limits the total number of certificates in a chain. Right: It gives “the maximum number of non-self-issued intermediate certificates that may follow this certificate”, the final certificate in the path is explicitly excluded from the count, and self-issued certificates do not consume the budget, so pathlen:0 permits exactly one more certificate below it and that certificate must be an end entity.

Wrong: A name constraint on an authority stops it issuing for other names. Right: It stops a conforming verifier accepting such a certificate, which is not the same thing; the private key will sign anything, RFC 5280 says constraints “apply only when the specified name form is present” so a certificate carrying no name of that type is unaffected, and a verifier that reads hostnames from the Common Name without applying dNSName constraints there can be walked around the constraint.

Wrong: If the chain works in the browser, the server is configured correctly. Right: Windows and Apple verifiers fetch missing issuers over the network from the authorityInfoAccess pointer and Firefox fills gaps from a preloaded cache, so a server that omits its intermediate can look perfect in a browser and fail in curl, in a Java client and in every embedded device.

27.99 Chapter summary in 20 lines#

  1. A certificate is almost never signed by a key your device already trusts, so getting from one to the other requires a sequence of certificates.
  2. Intermediates exist because a root key cannot be replaced quickly and so must almost never be used, while a key used millions of times a day must be replaceable quickly.
  3. Root, intermediate and leaf name positions in one path, not classes of certificate, and one certificate can occupy different positions for different clients.
  4. A TLS server sends an unordered bag of certificates in which only the first entry is guaranteed, as RFC 8446 section 4.4.2 states.
  5. A trust anchor is fundamentally a name and a public key; RFC 5280 section 6.1.1 treats the self-signed certificate as one optional way to supply that.
  6. Path building and path validation are two different algorithms, and only the second of them is standardized.
  7. Path validation is RFC 5280 section 6, of May 2008, and takes a finished ordered path plus an anchor plus a time, returning yes or no.
  8. Path building is described in the Informational RFC 4158 of September 2005, which models it as a depth-first search of a directed graph with twenty-one optional sorting heuristics.
  9. Section 6.1.3 step (a) checks signature, validity period, revocation status and issuer name match; steps (b) and (c) apply name constraints.
  10. Section 6.1.4 step (k) requires cA to be TRUE, step (l) decrements and tests max_path_length, step (m) lowers it to any smaller pathLenConstraint, and step (n) requires the keyCertSign bit.
  11. Cross-signing gives one authority two or more certificates for the same name and key, multiplying the valid paths to everything it ever signs.
  12. RFC 4158’s worked example shows one end-entity certificate with four distinct valid paths through only three authorities.
  13. A robust verifier folds validation into the search and backtracks on failure; a fragile one builds one chain, validates it once, and gives up.
  14. DST Root CA X3 expired on 30 September 2021 at 14:01:15 GMT, twenty-one years after it was issued on 30 September 2000.
  15. IdenTrust issued a cross-signature for ISRG Root X1 valid from 20 January 2021 to 30 September 2024, deliberately outliving its own issuer, because Android does not enforce notAfter on trust anchors.
  16. Clients holding ISRG Root X1 but unable to stop at it walked on to the expired root and failed, which is why Let’s Encrypt told API operators to require OpenSSL 1.1.0 or later.
  17. Name constraints, RFC 5280 section 4.2.1.10, must be marked critical, may appear only in a CA certificate, and apply only when the constrained name form is present in the certificate being checked.
  18. They are under-used because of that presence rule, historically patchy support, silent failure modes, commercial disincentives and the strictness of the Baseline Requirements profile.
  19. Path length constraints are enforced consistently and count non-self-issued intermediates below the certificate, excluding the final one in the path.
  20. Nearly every chain failure in the wild is one of six faults, and the first command in every diagnosis is to look at the bag the server actually sent.

Chapter sources: RFC 5280, “Internet X.509 Public Key Infrastructure Certificate and Certificate Revocation List (CRL) Profile”, Cooper, Santesson, Farrell, Boeyen, Housley and Polk, May 2008, sections 4.2.1.9, 4.2.1.10 and 6.1.1 through 6.1.6, from which the validation steps are quoted; RFC 4158, “Internet X.509 Public Key Infrastructure: Certification Path Building”, Cooper, Dzambasow, Hesse, Joseph and Nicholas, Informational, September 2005, for the graph model, the four-path figure of section 1.5.3 and the twenty-one heuristics of section 3.5; RFC 6818, Peter Yee, January 2013, on self-signed trust anchor certificates; RFC 8446, Eric Rescorla, August 2018, section 4.4.2 on certificate list ordering; Let’s Encrypt, “Extending Android Device Compatibility for Let’s Encrypt Certificates”, Josh Aas and Aaron Gable, 21 December 2020; the Let’s Encrypt documentation page “DST Root CA X3 Expiration (September 2021)”, updated 30 September 2021 and 5 February 2024, for the OpenSSL 1.1.0 requirement; “Shortening the Let’s Encrypt Chain of Trust”, Aaron Gable, 10 July 2023, for the 8 February 2024 default change, the 30 September 2024 cross-sign expiry and the rise in Android trust of ISRG Root X1 from 66 per cent to 93.9 per cent; the Let’s Encrypt “Chains of Trust” page as last updated 8 July 2026, for the ISRG Root X1, ISRG Root X2, Root YE and Root YR hierarchy and the alternative chains for YE1 and YR1; the published certificate for ISRG Root X1 cross-signed by DST Root CA X3, serial 40:01:77:21:37:d4:e9:42:b8:ee:76:aa:3c:64:0a:b7; Ryan Sleevi, “Path Building vs Path Verifying: The Chain of Pain”, 24 June 2020, and its companion “Implementation Showdown”, for the AddTrust External CA Root expiry of 30 May 2020 at 10:48 GMT and the survey of affected libraries; Andrew Ayer, “Fixing the AddTrust Root Expiration”, 2020; Google Security Blog, “Further improving digital certificate security”, 7 December 2013 with its update of 12 December 2013, for the ANSSI intermediate and the thirteen French-related top-level domains; the Chromium blog announcement of the Chrome Root Program, September 2022, for the Chrome Root Store and Certificate Verifier rollout beginning in Chrome 105; the CA/Browser Forum TLS Baseline Requirements, sections 7.1.2.5.2 and 7.1.2.10.8 in the current profile and section 7.1.5 up to version 1.7.0; the Go standard library source for crypto/x509, for maxChainSignatureChecks equal to 100; and OpenSSL 3.0.13 with Debian ca-certificates version 20240203, containing 146 root certificates, against which every command output printed here was produced in August 2026.