Secrets, PKI & CertificatesIV · PKI FoundationsPKI
Certification path building and path validation
What you'll learn
- Separate path building from path validation and say which artefacts each one uses
- Recite the RFC 5280 basic checks applied to every certificate in a path
- Map a verifier error code onto the phase that produced it
- Explain why two clients on one host can disagree about the same wire bytes
Prerequisites
Verified against OpenSSL 3.5.x teaching target; 3.0+ minimum · OpenSSH 10.x teaching target; 8.2+ minimum for certificate workflows · OpenBao 2.6.x · Smallstep step-ca 0.30.x · Certbot / Pebble Certbot current release; Pebble 2.10.x ACME test server · Kubernetes (cross-course target) 1.36.x · PostgreSQL 17.x · 2026-08-26
Ask most engineers how a client validates a certificate and you get one process. There are two, they run in that order, and they fail differently. RFC 5280 section 6.1 names nine inputs to path validation, and the very first one is a prospective certification path of length n. The path is an input. Something else had to produce it, and that something is where a large share of production TLS failures live.
Path building is a search, not a lookup
Assembling a candidate path is the subject of its own document, RFC 4158, which states plainly that path validation is described elsewhere and is not repeated there. The separation is deliberate, because building is a graph search with backtracking and validation is a linear check.
The builder starts from the leaf and works towards something it already
trusts. Its raw material is whatever it can lay hands on: the
certificates the peer sent in the handshake, the local trust store, any
intermediates cached from previous connections, and in some
implementations a certificate fetched from the caIssuers URL in the
authority information access extension.
Matching is by name first. The builder looks for a certificate whose subject equals the current certificate’s issuer field, then narrows the candidates using the authority key identifier and subject key identifier extensions. That narrowing is a hint, not a rule. A builder that treats the key identifier as a hard filter will refuse paths that a more forgiving builder completes, which is one reason two clients given identical bytes can reach opposite conclusions.
- Several certificates can share a subject name. A CA that renewed its own certificate, or was cross-signed by a second root, is represented by more than one file with the same subject.
- Candidates form a graph, not a list. More than one route from leaf to anchor may exist, and only some of them validate.
- Backtracking is expected behaviour. Rejecting the first candidate and trying another is normal, and a builder that does not backtrack fails on chains that are perfectly sound.
flowchart LR
A["Leaf certificate"] --> B["Build: find candidate issuers"]
B --> C{"Candidate reaches an anchor?"}
C -- "no" --> D["Backtrack or fail: no path"]
C -- "yes" --> E["Validate: RFC 5280 section 6.1"]
E --> F{"All checks pass?"}
F -- "no" --> G["Reject the path"]
F -- "yes" --> H["Then check service identity"]
Read the diagram as three distinct opportunities to fail. The search can fail to produce any candidate at all. A candidate can be produced and then rejected by the validation algorithm. And a path can validate completely and still be useless because the name it carries is not the name you asked for, which is a separate check that happens afterwards.
What the validation algorithm actually checks
Path validation takes the assembled path plus the current date and time, the trust anchor information, an initial policy set and several policy-processing flags. RFC 5280 notes that conforming implementations are not required to support setting all of those inputs, which is why policy handling varies so much between libraries.
For every certificate in the path, four basic checks must all hold. The signature on the certificate verifies using the working public key algorithm, the working public key and the working public key parameters. The certificate validity period includes the current time. At the current time, the certificate is not revoked. The certificate issuer name equals the working issuer name.
Those four are then joined by name constraint processing, applied both to the subject distinguished name and to the entries in the subject alternative name extension, whether that extension is marked critical or not. The algorithm proceeds through initialisation, basic processing for each certificate, preparation for the next certificate, wrap-up and outputs.
Two recent amendments mean RFC 5280 on its own is no longer the whole
algorithm. RFC 9618 replaced the policy validation procedure with an
equivalent but more efficient one, because the original built a
structure that scaled exponentially in the worst case and left
implementations open to denial of service. RFC 10007, published in June
2026, requires a check that RFC 5280 had left implicit: the presence of
the cRLSign key usage bit when validating a CRL.
Reading the error back to the phase that produced it
The practical payoff is diagnostic. Verifier messages look interchangeable and are not. These five come from a real two-tier lab PKI, exercised deliberately.
| Observed message | Phase | What is actually missing |
|---|---|---|
error 20 at 0 depth lookup: unable to get local issuer certificate | building | No candidate issuer for the leaf is available anywhere |
verify error:num=21:unable to verify the first certificate | building | The chain the peer sent cannot be completed |
error 2 at 1 depth lookup: unable to get issuer certificate | building | The path reached a certificate whose issuer is not an anchor |
error 62 at 0 depth lookup: hostname mismatch | identity | Path validated, then the SAN did not match the requested name |
error 26 at 0 depth lookup: unsuitable certificate purpose | validation | Path validated, then the extended key usage rejected the use |
The depth number carries information people ignore. Depth 0 is the leaf. Depth 1 is its issuer. An error 20 at depth 0 says the builder could not get past the leaf, so the intermediate is missing. An error 2 at depth 1 says the builder did get past the leaf, reached the issuing CA, and found no anchor above it, so the root is absent from the store. Those two symptoms are frequently reported as the same ticket and have opposite remedies.
You can separate them on purpose by controlling the inputs:
LEAF=app.crt
INTERMEDIATE=srv-ca.crt
ANCHOR=root.crt
# Supply the anchor only. If this fails, the intermediate is the gap.
openssl verify -CAfile "$ANCHOR" "$LEAF"
# Supply both. If this succeeds, the material is sound and the fault
# is in what the server sends or what the client store holds.
openssl verify -CAfile "$ANCHOR" -untrusted "$INTERMEDIATE" "$LEAF"
Run against a leaf whose intermediate is genuinely absent, those two commands produce exactly this pair of results:
CN=app.lab.example
error 20 at 0 depth lookup: unable to get local issuer certificate
error app.crt: verification failed
app.crt: OK
The second command returning app.crt: OK while the first returns
error 20 at 0 depth lookup: unable to get local issuer certificate is
a complete diagnosis on its own. The certificates are fine. The path
could not be built from what the client had.
Why two clients disagree about identical bytes
The building phase depends on local state, and local state differs. Four mechanisms account for nearly every case where one client accepts a chain another rejects, and none of them involve a bad certificate.
- Cross-signing. One CA key can hold two certificates, issued by two different roots, so the same leaf genuinely has two valid routes upward. A client that trusts only one of those roots must build through the matching intermediate, and a builder that stops at the first candidate can pick the wrong one.
- Cached intermediates. A client that has previously connected to another service under the same issuing CA may already hold the intermediate, and will complete a chain that the server never sent. This is why an engineer’s workstation is the worst possible place to confirm a chain fix.
- Fetching from authority information access. Some verifiers will
retrieve the issuer from the
caIssuersURL when the peer did not supply it. That converts a permanent server misconfiguration into an intermittent client-side one, dependent on egress rules, proxy policy and the CA’s availability, and it fails outright on hosts with no outbound access. - Key identifier strictness. Builders differ in how hard they filter candidates on the authority key identifier. A strict builder rejects a candidate that a lenient one accepts, from exactly the same input.
The operational conclusion is uncomfortable but firm: a successful connection from one client is not evidence that a chain is correct. The evidence is what the server presents, captured from the wire, checked against a verifier holding nothing but the anchor.
Production discipline
- Record the exact error string, not a paraphrase. The number and the depth are the diagnosis; a ticket saying certificate error discards both.
- Reproduce with an explicit anchor before changing anything. Isolating the material from the store answers the building question in one command.
- Compare clocks first when an expiry error appears. Skew and expiry present identically and are fixed in different places.
- Do not conclude a certificate is untrusted because one client rejected it. Different builders hold different caches and apply key identifier hints differently, so confirm against a second independent verifier.
Cross-course references
- Linux for Production Sysadmins - Part XXIV (Time) covers the clock synchronisation that decides whether the current time input to validation is trustworthy at all.
- Observability for Production Sysadmins - Part CVIII (ClockSkew) covers detecting skew across a fleet, which converts the silent input above into something you can alert on.
- Kubernetes for Production Sysadmins - Part CXIV (TLS) covers in-cluster verifiers whose trust material arrives as mounted volumes, where the building phase depends on what a controller wrote.
Quiz
Knowledge check · 4 questions
Q1. A verifier reports error 2 at depth 1, unable to get issuer certificate. What is missing?
Q2. RFC 5280 section 6.1 includes finding a suitable certification path among the steps it defines.
Q3. State the four basic checks RFC 5280 applies to every certificate in a prospective path.
Q4. Explain how one endpoint can be accepted by one client and rejected by another, and what to do about it.
At 03:15 UTC a batch job on db-03 begins failing against https://api.example.com/ with unable to get local issuer certificate at depth 0. A browser on an engineer's machine loads the same URL without complaint, and so does curl from web-01. Nothing was deployed to api.example.com in the previous fortnight.
Passing score: 75%. Answers are checked in this browser.