TLS and SSL Certificates Explained: A Developer's Guide to HTTPS, X.509, and Certificate Chains

Every developer has stared at NET::ERR_CERT_AUTHORITY_INVALID, x509: certificate has expired, or SSL_ERROR_BAD_CERT_DOMAIN at some point and silently hoped a redeploy would make it go away. Understanding what these errors actually mean — and how the certificate machinery works underneath — turns each of them from a mystery into a 30-second fix.

This guide walks through TLS and SSL from the ground up: what a certificate is, how the chain of trust works, what happens during the TLS handshake, and how to read certificates from the command line.

SSL vs TLS — A Note on Terminology

You'll hear "SSL certificate", "TLS certificate", and "SSL/TLS certificate" used interchangeably. They refer to the same thing: an X.509 certificate used by a Transport Layer Security handshake.

Despite being deprecated for over a decade, "SSL certificate" remains the popular term simply because the marketing stuck. They are TLS certificates.

What Is an X.509 Certificate?

A certificate is a small file that says, in cryptographic terms: "I am the public key for <hostname>, and a certificate authority you trust has signed this claim."

Inside an X.509 v3 certificate:

FieldMeaning
SubjectWho the certificate is for. Includes the Common Name (CN) and other identifying fields.
Subject Alternative Name (SAN)The hostnames the certificate is valid for. This is what browsers actually check.
IssuerThe Certificate Authority (CA) that signed it.
Public KeyThe server's public key (usually RSA 2048+ or ECDSA P-256/P-384).
ValidityNot Before and Not After timestamps.
Serial NumberUnique ID assigned by the CA.
SignatureThe CA's cryptographic signature over everything above.
ExtensionsKey usage, extended key usage, CRL/OCSP URLs, SCTs (Certificate Transparency).

Common Name is dead. Modern browsers (Chrome since 2017, Firefox since 2019, Safari since iOS 13) ignore the CN field and only validate against the Subject Alternative Name. If your cert has the hostname in CN but no matching SAN entry, browsers will reject it with ERR_CERT_COMMON_NAME_INVALID. Always include all hostnames in the SAN.

Certificate Files and Formats

Same data, multiple file formats. This is a frequent source of confusion:

ExtensionFormatContains
.pem, .crt, .cerPEM (Base64-encoded, text)Cert(s), often a chain
.derDER (binary)Single cert
.keyPEMPrivate key (keep secret!)
.csrPEMCertificate Signing Request
.p12, .pfxPKCS#12 (binary)Cert + private key, password-protected
.jksJava KeyStoreJava-specific bundle

A PEM file is just Base64 between -----BEGIN CERTIFICATE----- and -----END CERTIFICATE----- markers. You can have multiple blocks concatenated to form a chain:

-----BEGIN CERTIFICATE-----
MIIDxTCCAq2gAwIBAgIQAqxN... (your server's cert)
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIEpDCCA4ygAwIBAgIQc6N5... (intermediate CA)
-----END CERTIFICATE-----

The Chain of Trust

Browsers don't trust your server's certificate directly. They trust a chain of certificates that ends at a root CA already in the operating system or browser trust store.

┌──────────────────────┐
│  Root CA             │  ← Pre-installed in your OS/browser
│  (self-signed)       │
└──────────┬───────────┘
           │ signs
           ▼
┌──────────────────────┐
│  Intermediate CA     │  ← Served by you, alongside leaf
│                      │
└──────────┬───────────┘
           │ signs
           ▼
┌──────────────────────┐
│  Your server's cert  │  ← The "leaf" certificate
│  (boltkit.app)       │
└──────────────────────┘

Roots are kept offline; intermediates do the day-to-day signing. The OS/browser trust store contains hundreds of root CAs operated by Let's Encrypt, DigiCert, Sectigo, GlobalSign, etc.

The Most Common Misconfiguration

The leading cause of "untrusted certificate" errors in production is a missing intermediate. Your server must serve the full chain (leaf + intermediates) to the client. Browsers usually fetch missing intermediates over the network — many other clients (Java, Go, curl on minimal containers, mobile apps) don't.

Test with:

openssl s_client -connect example.com:443 -servername example.com -showcerts

If Verify return code: 21 (unable to verify the first certificate) appears, your chain is incomplete.

The TLS 1.3 Handshake (Simplified)

When a client connects to https://example.com:

  1. ClientHello — client sends supported TLS versions, cipher suites, a random nonce, and the server name (SNI) it wants to talk to.
  2. ServerHello — server picks a cipher suite, returns its certificate chain, and a key share for ephemeral Diffie-Hellman key agreement.
  3. Validation — client checks: (1) chain leads to a trusted root, (2) leaf's SAN matches the hostname, (3) current time falls inside the validity window, (4) cert isn't revoked (OCSP/CRL/CT), (5) signatures are valid.
  4. Key derivation — both sides compute the shared secret. Application data starts flowing, encrypted.

TLS 1.3 cuts a full round trip compared to TLS 1.2 by sending the key share in the very first ClientHello.

Reading Certificates From the Command Line

# Inspect a remote server's certificate
openssl s_client -connect example.com:443 -servername example.com </dev/null \
  | openssl x509 -text -noout

# Inspect a local PEM file
openssl x509 -in cert.pem -text -noout

# Just the SAN entries
openssl x509 -in cert.pem -noout -ext subjectAltName

# Just the expiry date
openssl x509 -in cert.pem -noout -dates

# Days until expiry
openssl x509 -in cert.pem -noout -checkend 0     # Returns 0 if valid
openssl x509 -in cert.pem -noout -checkend 2592000  # Valid for 30+ days?

# Verify chain locally
openssl verify -CAfile chain.pem cert.pem

# What's in a private key?
openssl rsa -in key.pem -text -noout
openssl ec -in key.pem -text -noout

# Match a key to its certificate (modulus must match)
openssl x509 -in cert.pem -noout -modulus | openssl md5
openssl rsa  -in key.pem  -noout -modulus | openssl md5

Common Certificate Errors and Fixes

NET::ERR_CERT_DATE_INVALID / x509: certificate has expired

The cert is past its Not After date, or the client clock is wrong. Renew the cert; if the client clock is wrong (common on embedded devices, Raspberry Pis without RTC), fix NTP first.

SSL_ERROR_BAD_CERT_DOMAIN / ERR_CERT_COMMON_NAME_INVALID

The hostname being connected to isn't in the SAN. Reissue with the correct SAN list. If you're behind a CDN, make sure the cert covers the apex and any subdomains in use.

NET::ERR_CERT_AUTHORITY_INVALID / unable to get local issuer certificate

Either the chain is incomplete (most common — see above) or you're using a self-signed cert that the client doesn't trust. For corporate/internal CAs, you'll need to install the root in the client's trust store.

SSL_ERROR_PROTOCOL_VERSION_ALERT

Client and server can't agree on a TLS version. Almost always means an old client trying TLS 1.0/1.1 against a modern server. Upgrade the client.

SEC_ERROR_REVOKED_CERTIFICATE

The CA published a revocation. Reissue the certificate; if the private key was compromised, generate a new key first.

Let's Encrypt and Automated Certificates

Let's Encrypt changed everything in 2016 by making CA-signed certificates free and automated. The trade-off: certs are valid for 90 days (much shorter than commercial 1-year+ certs), so you must automate renewal.

The most common tools:

Industry direction: shorter lifetimes. The CA/Browser Forum has approved a phased reduction of the maximum certificate lifetime to 47 days by 2029. If you're still managing certs by hand, you're already behind. Automate now.

Mutual TLS (mTLS)

Standard TLS authenticates the server to the client. Mutual TLS also authenticates the client to the server, by issuing each client a certificate (typically from a private CA) and validating it during the handshake.

mTLS is widely used for:

Self-Signed Certs and Local Development

For localhost and local dev, real CAs won't sign you a cert. Options:

Auditing Your Fleet's TLS Posture

Knowing how certificates work is one thing; actually verifying that every internal service, dev VM, and homelab box is up-to-date is another. Common findings on infrastructure that hasn't been audited recently:

If you run a homelab or a small VPS fleet and want this audited continuously rather than by remembering to do it, Noxen — a Mac-native security audit tool — runs nightly checks for deprecated TLS, weak SSH, exposed services, and CVEs across every host you own. No agents, no Docker, no SaaS — it just SSHes and reports.

Quick Reference

Inspect Tokens, Headers, and More On Your Phone

BoltKit's JWTInspect decodes auth tokens, HTTPCodes documents every status, DNSLookup resolves records over HTTPS. 10 essential dev tools, free on iPhone, iPad, and Mac.

Get BoltKit Free