Certificate chain: intermediate certificates and the chain of trust
Your SSL certificate is not trusted on its own. It is trusted because something your visitor's browser already trusts vouched for it, possibly through several steps. That sequence is the HTTPS certificate chain: your leaf certificate, one or more intermediate certificates, and a root in the browser's trust store. Your server has to send every link except the root, and a missing link breaks the connection for anyone whose client cannot fill the gap itself.
- Order on the wire
- your certificate first, then each certificate that signed the one before it: MUST in TLS 1.2 (RFC 5246) [3], SHOULD in TLS 1.3 (RFC 8446 §4.4.2) [2]
- Each link
- its issuer is the subject of the next certificate up, its signature verifies with that certificate's key, and it is within its validity period (RFC 5280 §6.1) [1]
- Intermediates
- must be CA certificates, with cA set to TRUE in basic constraints [1]
- Missing intermediate in OpenSSL
error 20 at 0 depth lookup: unable to get local issuer certificate[12] [13]- Unknown issuer in Firefox
SEC_ERROR_UNKNOWN_ISSUER[10]
The three kinds of certificate
root certificate in the browser's trust store, trusted outright
│ signs
intermediate certificate vouched for by the root
│ signs
leaf certificate yours, for your namesThe root sits in a trust store shipped with the browser or the operating system. It is trusted because it is there, not because anything vouched for it, which makes the trust store the actual foundation of the whole system.
The intermediate is signed by the root and signs certificates for customers. A certificate authority works through intermediates because the Baseline Requirements forbid a root's key from signing customer certificates directly (§6.1.7) [5]. If an intermediate's key is compromised, the authority revokes that intermediate (§4.9.1.2) [5], and the root installed on every device stays as it is.
The leaf is yours. It covers your names and is signed by an intermediate. A leaf from a free certificate authority is chained the same way as a paid one, which is why browsers trust it equally. The same holds for every type of SSL certificate: a browser follows the chain of a DV, IV, OV or EV leaf up to a trusted root in the same way.
How a client checks the chain
A client validates a certification path: a list of certificates from the one just below the trust anchor down to your leaf. RFC 5280 sets the conditions in §6.1. The subject of each certificate is the issuer of the next one down. Every signature verifies with the public key of the certificate above it. Every certificate in the path is within its validity period at the time of the check, and every certificate except your leaf is a CA certificate, with cA set to TRUE [1].
For how such a path fits into the wider system, see the PKI chain of trust.
The root itself is not part of that list. A self-signed trust anchor is an input to the check, which is why its presence in the client's store matters and its presence in your server's response does not [1].
If the chain ends at a GlobalSign root, the roots R1, R3, R5, R6, R46 and E46 are told apart on the GlobalSign root page.
How the client finds the certificates for that list is, in the words of RFC 5280, outside the scope of the specification [1]. Clients take them from what the server sent, from their own stores and caches, and some from the address inside the certificate. That freedom is where the differences between clients come from.
Reading the chain in OpenSSL
We built a lab chain, with our own test root, a test intermediate and a leaf for www.example.com, served it locally and connected with openssl s_client on OpenSSL 3.0.13. The output below is trimmed to the depth, chain and verdict lines [13]:
$ openssl s_client -connect 127.0.0.1:8443 -servername www.example.com -CAfile root.pem
depth=2 CN = Example Lab Root
depth=1 CN = Example Lab Intermediate
depth=0 CN = www.example.com
0 s:CN = www.example.com
i:CN = Example Lab Intermediate
1 s:CN = Example Lab Intermediate
i:CN = Example Lab Root
Verify return code: 0 (ok)Read it in pairs. Each i: line, the issuer, names the subject on the next s: line. Depth counts up from your certificate at 0 [12]. The server sent two certificates; the root at depth 2 came from the client's own trust file.
What the server has to send
Your server presents its own certificate plus the intermediates, as one list, in the TLS handshake. RFC 8446 for TLS 1.3 says the sender's certificate MUST come first and each following certificate SHOULD directly certify the one before it [2]. RFC 5246 for TLS 1.2 made that order a MUST [3]. TLS 1.3 also asks clients to cope with extra certificates and any order, except that the first certificate must be yours [2]. Send them in order anyway: a TLS 1.2 client is entitled to expect it.
The first certificate in the chain is the CA-signed certificate itself, and that page explains how to confirm who signed it.
If the server presents only its own certificate, a client that cannot find the intermediate has a leaf it cannot connect to anything it trusts. It refuses, and a Chromium browser reports the error described on ERR_CERT_AUTHORITY_INVALID [8]. Firefox names the same fault SEC_ERROR_UNKNOWN_ISSUER; the NSS error text reads "Peer's Certificate issuer is not recognized" [10].
On disk the order is the same. An ACME server delivers the certificate as a chain file whose first certificate is the end-entity certificate (RFC 8555, §9.1) [4]. nginx expects your certificate before the intermediates in the file named by ssl_certificate; with the order reversed, it refuses to start with a key values mismatch error [6].
Should you send the root?
No. Both TLS specifications let the server leave out the certificate of the trust anchor, because the client must already have it to validate anything [2] [3]. Sending it does not create trust either. In our lab we gave OpenSSL the intermediate and the test root as the certificates a server would send, with no trust store of its own, and the check still failed [13]:
$ openssl verify -no-CAfile -no-CApath -no-CAstore -untrusted inter_root.pem leaf.pem
CN = Example Lab Root
error 19 at 2 depth lookup: self-signed certificate in certificate chain
error leaf.pem: verification failedWith the root in its trust file, the result was OK with or without the root among the sent certificates [13]. Including it only adds bytes to every handshake.
Why a missing intermediate works on your machine
Because the gap in an incomplete certificate chain gets filled on the machine where the work was done.
Browsers keep intermediates. The nginx documentation puts it this way: "actively used browsers may already have the required intermediate certificates" [6]. Firefox on desktop downloads the intermediates of the public web PKI in advance into a local cache; they are not trusted on their own, and trust still comes from a root certificate in the trust store [7]. Chromium's certificate verifier can retrieve a missing issuer from the authority information access address in the certificate [8], the caIssuers entry defined in RFC 5280 [1].
Command-line tools may not. curl, for one, does not follow that address: support for it sits on the project's to-do list, which calls on-demand downloading "a serious privacy leak" [9]. With nothing to fill the gap, a script fails where a browser succeeds.
What a missing intermediate looks like
Our lab server sent the leaf alone, without the intermediate certificate, to a client holding only the test root certificate in its trust file (output trimmed as above):
$ openssl s_client -connect 127.0.0.1:8443 -servername www.example.com -CAfile root.pem
depth=0 CN = www.example.com
verify error:num=20:unable to get local issuer certificate
verify error:num=21:unable to verify the first certificate
0 s:CN = www.example.com
i:CN = Example Lab Intermediate
Verify return code: 21 (unable to verify the first certificate)The issuer line still names the intermediate, and nothing in the list carries that name as a subject [13]. Works in a browser, fails from a script: check the chain first.
Chain versus root store
The chain is what the server sends: a path from your certificate up towards a root. It is data in transit, and you control it.
The root store is the list of certificates a client trusts outright. It ships with browsers and operating systems, its vendors maintain it, and you do not control it at all.
The distinction matters in two practical ways. A chain can be complete and still fail, if it ends at a root that a particular client does not have. And clients on the same machine can use different stores: the roots Mozilla curates for Firefox ship inside Firefox itself, in its own library file [10].
So the chain is your responsibility and the root store is your constraint.
When a chain ends at the USERTrust RSA Certification Authority, the page on that root shows how to tell the self-issued root from the cross-certificate that carries the same name.
When the chain changes without you
Authorities issue new certificates under whichever intermediate is current. Let's Encrypt, for one, keeps several intermediates in rotation and states that it may begin issuing from a backup intermediate "at any time, without warning" [11]. A renewal can therefore arrive signed by a different intermediate than the previous one, and a bundle assembled by hand no longer matches the certificate it accompanies. The symptom is a trust error right after a renewal that otherwise went perfectly. An ACME server may also offer alternative chains for the same certificate, with paths to different trust anchors (RFC 8555, §7.4.2) [4].
An intermediate past its end date fails validation the same way an expired HTTPS certificate does, because every certificate below the root, yours included, must be within its validity period. In our lab the intermediate expired before the leaf; checked at a moment between the two end dates, OpenSSL pointed at depth 1 [13]:
$ openssl verify -attime $AT -CAfile root.pem -untrusted inter.pem leaf.pem
CN = Example Lab Intermediate
error 10 at 1 depth lookup: certificate has expired
error leaf.pem: verification failedWhatever installs the certificate should install the chain that came with it, in the same operation. A bundle maintained separately from the certificate is a pair of things that must be kept in step, and pairs like that drift.
How to check your chain
Check from something that does not fill gaps. An external checker reports what the server actually presents, in order, and states whether the chain resolves to a trusted root. Run it after every certificate change.
Your own browser is the least informative test available to you. It may already hold the intermediate whose absence you are trying to detect.
From a terminal, OpenSSL lists the certificates the server sends, each with its issuer. If the list stops at your own certificate, the intermediates are missing.
$ openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/nullThe -showcerts option prints the certificates in the order the server sent them; it does not show a verified chain [12]. Read the Verify return code line at the end for the verdict.
For a certificate you hold as a file, the issuer line tells you which intermediate you need if you are assembling the bundle by hand.
Showing the chain OpenSSL builds from files
The s_client command above shows what a server sends. For certificate files you hold, openssl verify with the -show_chain option prints the chain that OpenSSL built when the check succeeds, and its manual says that certificates which came from the untrusted list are flagged as untrusted [12]. In our lab we gave it the root as the trust file and the intermediate as an untrusted certificate [13]:
$ openssl verify -show_chain -CAfile root.pem -untrusted inter.pem leaf.pem
leaf.pem: OK
Chain:
depth=0: CN = www.example.com (untrusted)
depth=1: CN = Example Lab Intermediate (untrusted)
depth=2: CN = Example Lab RootDepth counts up from the leaf at 0, as in the server output earlier. The flag says where OpenSSL got a certificate, not what it decided: the leaf and the intermediate carry it, and the root, which came from the trust file, does not [13]. The verdict is the OK line. Without the intermediate, the same root and leaf files fail with error 20 at depth 0, unable to get local issuer certificate [13].
Chains on Bridge CDN
On Bridge CDN, TLS is terminated at the edge: there is no certificate file on your machine and no chain to assemble. The certificate is used where it is issued, so a renewal cannot leave a stale bundle behind [14].
FAQ
What is an SSL certificate chain?
The sequence that connects your certificate to a root the client already trusts, passing through one or more intermediates. Each link is a signature: the root signed the intermediate, and the intermediate signed your certificate. A client that cannot walk that sequence to a root it holds has no way to decide whether your certificate means anything.
Who is supposed to send the intermediates?
Why does an incomplete chain work for me and fail for visitors?
What is the difference between the chain and the root store?
The chain is what your server sends, and you control it completely. The root store is the set of authorities the client already trusts, and you control none of it: it ships with the operating system or the browser and updates on their schedule.
How do I verify the chain is complete?
Check from something that has never visited your site, using an external checker or a command-line client such as openssl s_client. Your own browser is the least reliable witness available, because it is the one most likely to hold the very intermediate whose absence you are trying to detect.
What are chained certificates?
The term does not name a separate kind of certificate: chained certificates are the intermediate certificates that sit between your leaf and the root in the certificate chain. The nginx documentation describes the authority providing a bundle of chained certificates to be concatenated to the signed server certificate, and says the server certificate must appear before the chained certificates in the combined file. Your leaf is the same ordinary certificate with or without that bundle; what differs is whether its intermediates travel with it, so that the client can follow the chain from the leaf up to a root it holds. How such a file is stored and converted is covered under certificate formats.
Sources
IETF, RFC 5280, Internet X.509 Public Key Infrastructure Certificate and CRL Profile: §4.2.2.1 (authority information access), §6.1, §6.1.3, §6.1.4.
IETF, RFC 8446, The Transport Layer Security (TLS) Protocol Version 1.3: §4.4.2.
IETF, RFC 5246, The Transport Layer Security (TLS) Protocol Version 1.2: §7.4.2.
IETF, RFC 8555, Automatic Certificate Management Environment (ACME): §7.4.2, §9.1.
CA/Browser Forum, Baseline Requirements for the Issuance and Management of Publicly-Trusted TLS Server Certificates, version 2.3.1: §4.9.1.2, §6.1.7.
nginx documentation, Configuring HTTPS servers: SSL certificate chains.
Mozilla Security Blog, Preloading Intermediate CA Certificates into Firefox.
Chromium source: net/base/net_error_list.h (CERT_AUTHORITY_INVALID) and net/cert/internal/cert_issuer_source_aia.h.
curl project, TODO: Support Authority Information Access certificate extension (AIA).
NSS source, lib/util/SECerrs.h (SEC_ERROR_UNKNOWN_ISSUER); Mozilla wiki, CA and CA/FAQ: the NSS root store and where Firefox keeps it.
Let's Encrypt, Chains of Trust: subordinate (intermediate) CAs.
OpenSSL 3.0 manual: openssl-s_client, openssl-verify, openssl-verification-options (-attime), X509_STORE_CTX_get_error.
Our runs: OpenSSL 3.0.13 on Linux, a lab chain with our own test root, intermediate and leaf for example.com names.
Bridge SSL, home page FAQ: What happens on my server?