from cryptography import x509from cryptography.x509.verification import PolicyBuilder, Store, VerificationError def load(path: str) -> x509.Certificate: with open(path, "rb") as f: return x509.load_pem_x509_certificate(f.read()) def verify_chain(leaf_path, intermediate_paths, root_path, host): store = Store([load(root_path)]) # only this root is trusted verifier = PolicyBuilder().store(store).build_server_verifier(x509.DNSName(host)) # verify() builds the path, checks signatures, validity, key usage and the host name return verifier.verify(load(leaf_path), [load(p) for p in intermediate_paths]) chain = verify_chain("leaf.pem", ["intermediate.pem"], "root.pem", "api.example.com")print("valid:", " -> ".join(cert.subject.rfc4514_string() for cert in chain)) for label, leaf, intermediates, root in [ ("impostor root", "leaf.pem", ["intermediate.pem"], "impostor-root.pem"), ("expired leaf", "expired.pem", ["intermediate.pem"], "root.pem"), ("missing intermediate", "leaf.pem", [], "root.pem"),]: try: verify_chain(leaf, intermediates, root, "api.example.com" if leaf == "leaf.pem" else "expired.example.com") except VerificationError as error: print(f"{label}: {error}") How it works
Store([root])holds the trust anchors; only these roots are trusted.PolicyBuilder().store(store).build_server_verifier(x509.DNSName(host))creates a verifier for TLS server certificates: it checks the signatures, validity periods, basic constraints, key usage and extended key usage along the path, and the host name.verifier.verify(leaf, intermediates)builds the path through the intermediates and returns it, leaf first; the run printsapi.example.com → Issuing CA 1 → Root CA.PolicyBuilder().time(...)validates at a given moment instead of now, andbuild_client_verifier()checks client certificates (clientAuth) instead.
Gotchas
- A look-alike root with the same name fails with
validation failed: candidates exhausted: signature does not match— matching names is not enough, the key must have signed the intermediate. - The expired leaf fails with
cert is not valid at validation time, and the missing intermediate withcandidates exhausted: all candidates exhausted with no interior errors— the verifier does not download missing issuers. VerificationErrormessages are for logs, not for logic: the look-alike root and the missing intermediate both start withcandidates exhausted. Treat any exception as "not trusted".
Related recipes
- How to verify a certificate chain against your own trust store with OpenSSL
- How to verify a certificate chain against your own trust store in Go
- How to verify a certificate chain against your own trust store in Node.js
- How to check SSL certificate expiration date in Python
- How to decode an X.509 certificate in Python