A TLS client never trusts a server certificate on its own. It verifies a chain of trust: the leaf (server) certificate was signed by an intermediate CA's private key, that intermediate's own certificate was signed by the next CA up, and so on until the chain reaches a root CA already present in the client's local trust store. Verification walks the chain from the leaf upward — at every hop it checks the issuer's signature, the certificate's validity window, and its revocation status (CRL/OCSP) — and stops the instant any check fails, so trust never "skips over" a broken link.
trusted = ∀ certᵢ ∈ chain[leaf..root]:
verifySignature(certᵢ, pubKey(issuer(certᵢ)))
∧ now ∈ [notBefore(certᵢ), notAfter(certᵢ)]
∧ ¬revoked(certᵢ)
∧ root(chain) ∈ localTrustStore
- 2 vs 3 hops — a longer chain (Root → Intermediate → Sub-intermediate → Leaf) mirrors how real CAs keep the root offline and delegate day-to-day issuance to intermediates, so a compromise only revokes a narrow branch.
- Expire leaf cert — the client's own clock check fails at hop 0; nothing further up the chain is even consulted.
- Revoke intermediate CA — simulates a CA compromise: every certificate that intermediate ever issued becomes untrusted the moment its revocation propagates, even though its own signature still verifies mathematically.
- Root not in trust store — every signature in the chain can verify perfectly and the chain can still be rejected, because trust is anchored by policy (an operating system or browser's shipped root list), not by cryptography alone.
This is exactly what ACME-automated CAs like Let's Encrypt shortcut for the leaf (automatic issuance + short lifetimes) while leaving the chain-walk itself untouched — the client-side validation logic here is identical to a real TLS handshake's certificate check.