A ground-up explanation of TLS certificate chains, what servers actually send, and how tls-ca-fetch extracts and reconstructs the CA chain automatically.
Every time you connect to an HTTPS server, it presents not just its own certificate, but a chain of certificates. This chain establishes trust by linking the server's identity all the way back to a CA that your operating system already trusts.
At the bottom of the chain is the leaf certificate โ issued directly to the server, bearing its hostname (e.g. *.google.com). Above it sit one or more intermediate CAs, which bridge the gap to the root. At the top is the root CA โ a self-signed certificate whose public key is embedded directly in your OS or browser trust store.
Verification works bottom-up: your TLS client checks that the leaf cert's signature matches the intermediate's public key, then that the intermediate's signature matches the root, and finally that the root is in the local trust store. If any link in that chain is missing or invalid, the connection is rejected.
Here's the practical reality: the TLS spec says servers should send the full chain, but they're not required to include the root CA. Root certs are large, static, and assumed to be pre-installed everywhere โ sending them is considered wasteful. So most well-configured servers send:
This means tls-ca-fetch will typically capture the intermediate CA(s) from the TLS handshake directly. The root can be fetched separately using the -fetch-root flag via the AIA extension (see Section 3).
| Scenario | Certs Sent by Server | tls-ca-fetch behaviour |
|---|---|---|
| Well-configured | Leaf + 1โ2 intermediates | Saves intermediates to .pem |
| Root-only chain (rare) | Leaf + root | Saves root CA directly |
| Misconfigured | Leaf only | Warns, shows AIA URL for manual fetch |
| -fetch-root flag | Leaf only (server is stingy) | Downloads root via AIA URL automatically |
-insecure to skip verification and -all to capture the leaf cert itself โ which is effectively the CA for that environment.
X.509 certificates carry an optional extension called Authority Information Access (AIA). When present, it contains a URL pointing to the issuing CA's certificate. Browsers and TLS clients use this as a fallback mechanism to reconstruct missing chain links at verification time.
tls-ca-fetch reads this extension from the certificates it receives during the TLS handshake. When you pass -fetch-root, it follows the AIA URL of the topmost CA cert in the chain to download the root from the issuing CA's own server.
# This URL lives inside the intermediate CA cert's AIA extension. # It points to the root CA cert in DER (binary) format. http://i.pki.goog/wr2.crt # tls-ca-fetch fetches this via plain HTTP, decodes the DER, # converts it to PEM, and appends it to the output file. # You can inspect an AIA URL manually with openssl: curl -s http://i.pki.goog/wr2.crt | openssl x509 -inform DER -text -noout
AIA URLs always serve DER-encoded certificates (binary format), not PEM. tls-ca-fetch handles the DER-to-PEM conversion automatically before writing to disk, so the output file is always a standard PEM that curl, openssl, Python's ssl module, and every other tool can consume directly.
Go's standard library has everything needed to implement tls-ca-fetch at high quality, with no third-party packages:
import ( "crypto/tls" // TLS dial + PeerCertificates (full chain) "crypto/x509" // Parse DER bytes โ *x509.Certificate "encoding/pem" // Encode certs as PEM blocks for output "net/http" // Fetch AIA URLs (plain HTTP, DER response) "os" // Write output file "fmt" // Terminal output formatting )
The key insight is tls.Conn.ConnectionState().PeerCertificates: after a TLS handshake, Go exposes the full certificate chain the server presented as a slice of *x509.Certificate structs. Each struct already has parsed fields โ IsCA, Issuer, NotAfter, IssuingCertificateURL (AIA) โ ready to use with no manual ASN.1 parsing.
Being CGo-free means the resulting binary has no dynamic library dependencies. No glibc version requirement, no OpenSSL version pinning, no runtime that must be pre-installed. Copy the binary to any Linux box made in the last decade and it runs โ even in a scratch Docker container with no base image.
The -ldflags="-s -w" build option strips the DWARF debug symbol table and Go symbol table, shrinking the binary from ~9MB to ~4.7MB without affecting runtime behaviour.
All flags are optional. The only required argument is the hostname. Port defaults to 443.
| Flag | Default | Description |
|---|---|---|
| -port | 443 | TLS port to connect to. Useful for non-standard ports like 8443 or custom internal services. |
| -o | <hostname>-ca.pem | Output file path. Defaults to a filename derived from the target hostname. Use - to write to stdout. |
| -fetch-root | false | Follow the AIA extension URL in the topmost CA cert to download the root CA. Appends it to the output file. |
| -insecure | false | Skip TLS certificate verification. Use for private CAs, self-signed certs, or internal servers not in any public trust store. |
| -all | false | Save the full chain including the leaf (server) certificate. Default behaviour saves only CA certs (non-leaf). |
| -timeout | 10 | Connection timeout in seconds. Increase for slow networks or distant hosts; lower for fast-fail scripting. |
| -version | — | Print the version string and exit immediately. No network connection is made. |
# Download root CA, skip verification, save to specific file, 30s timeout tls-ca-fetch -insecure -fetch-root -o /etc/ssl/certs/corp-root.pem \ -timeout 30 internal.corp.example 8443 # Full chain to stdout โ pipe directly into another tool tls-ca-fetch -all -o - api.example.com | openssl crl2pkcs7 -nocrl -certfile /dev/stdin