Section 01

The TLS Certificate Chain

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.

Root CA
GlobalSign Root CA
Self-signed — trusted by OS — IsCA=true
Pre-installed in OS trust store
โ†“
signs
Intermediate CA
WR2 (Google Trust Services)
Issued by root — IsCA=true — Expires 2029-02-20
โ†“
signs
Leaf Certificate (Server)
*.google.com
Issued to server — IsCA=false — Expires 2026-06-22

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.

Why does this matter? When you're configuring a service that needs to trust a particular CA โ€” a corporate PKI, a private service, an internal API โ€” you need the CA certificate(s) in PEM format. That's exactly what tls-ca-fetch extracts from the live TLS handshake.
Section 02

What Servers Actually Send

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
Tip: If you're targeting an internal corporate server that presents only a self-signed cert (no chain at all), use -insecure to skip verification and -all to capture the leaf cert itself โ€” which is effectively the CA for that environment.
Section 03

The AIA Extension

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.

example โ€” AIA URL from a real cert
# 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.

Security note: AIA URLs are served over plain HTTP (not HTTPS) by design โ€” the certificate itself is cryptographically signed, so the transport doesn't need to be encrypted. The signature on the cert is the proof of authenticity, not TLS.
Section 04

Why Go? Why No Dependencies?

Go's standard library has everything needed to implement tls-ca-fetch at high quality, with no third-party packages:

go โ€” stdlib packages used
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.

Linux amd64 Most servers, CI, WSL
GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o tls-ca-fetch .
Linux arm64 AWS Graviton, Pi, etc.
GOOS=linux GOARCH=arm64 go build -ldflags="-s -w" -o tls-ca-fetch .
macOS Apple Silicon M1/M2/M3/M4
GOOS=darwin GOARCH=arm64 go build -ldflags="-s -w" -o tls-ca-fetch .
Windows amd64
GOOS=windows GOARCH=amd64 go build -ldflags="-s -w" -o tls-ca-fetch.exe .
Cross-compilation is native in Go. All four commands above run from any single machine with Go installed โ€” no cross-compiler toolchain, no Docker build containers, no emulation. GOOS and GOARCH are just environment variables that switch Go's built-in code generation backend.
Section 05

Flags Reference

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.
shell โ€” combining flags
# 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