#
Certificate Chain of Trust (PKI)
PUBLIC KEY INFRASTRUCTURE X.509 TRUST FOUNDATION
The Foundation of HTTPS Security
PKI (Public Key Infrastructure) is the trust system that makes HTTPS work. It's how your browser knows that bank.com is really bank.com and not an imposter. Understanding certificate chains is essential for web security.
#
What is PKI?
CORE DEFINITION
Simple Definition: Public Key Infrastructure (PKI) is the framework of policies, procedures, hardware, software, and people used to create, manage, distribute, use, store, and revoke digital certificates. It's the trust system that powers HTTPS, code signing, email encryption, and digital signatures.
What PKI Provides:
- Identity Verification - Proves a server is who it claims to be
- Encryption Key Distribution - Securely shares public keys
- Secure Communications - Enables HTTPS and TLS
- Digital Signatures - Proves authenticity and integrity
- Centralized Trust Management - Trusted Certificate Authorities
Real-World Analogy
Think of PKI like a passport system:
- Government (Root CA) - Issues passports and is trusted by other countries
- Regional office (Intermediate CA) - Actually processes passport applications daily
- Your passport (Certificate) - Proves your identity, signed by government
- Immigration officer (Browser) - Verifies passport is legitimate by checking signature
- Expiration date - Passports (certificates) expire and must be renewed
- Revocation - Stolen passports (compromised certificates) can be canceled
Just as you trust a passport because you trust the issuing government, browsers trust certificates because they trust the Certificate Authority that signed them!
#
The Certificate Hierarchy
#
Understanding the Chain
┌─────────────────────────────────────────┐
│ ROOT CA (Self-Signed) │ Level 1: Trust Anchor
│ ┌───────────────────────────────┐ │
│ │ DigiCert Global Root CA │ │ ← Pre-installed in
│ │ Valid: 2006-2031 │ │ OS/Browser
│ │ Self-signed │ │ (Ultimate trust)
│ └───────────────┬───────────────┘ │
└────────────────────┼───────────────────┘
│ Issues & Signs
▼
┌─────────────────────────────────────────┐
│ INTERMEDIATE CA │ Level 2: Operational CA
│ ┌───────────────────────────────┐ │
│ │ DigiCert SHA2 Secure │ │ ← Daily operations
│ │ Server CA │ │ Kept online
│ │ Valid: 2013-2023 │ │ Issues end-entity
│ │ Signed by Root CA │ │ certificates
│ └───────────────┬───────────────┘ │
└────────────────────┼───────────────────┘
│ Issues & Signs
▼
┌─────────────────────────────────────────┐
│ END-ENTITY CERTIFICATE (Leaf) │ Level 3: Server Cert
│ ┌───────────────────────────────┐ │
│ │ www.example.com │ │ ← Your website
│ │ Valid: 2024-2025 │ │ What clients
│ │ Signed by Intermediate CA │ │ validate
│ └───────────────────────────────┘ │
└─────────────────────────────────────────┘
#
Why This Structure?
1. Root CA Protection
- Private key kept offline (cold storage)
- Physically secured in HSM (Hardware Security Module)
- Rarely used (only to sign intermediate CAs)
- If compromised, entire PKI collapses
2. Intermediate CA Flexibility
- Private key online for daily operations
- Can be revoked without affecting root
- Multiple intermediates for different purposes
- Easier to replace if compromised
3. Separation of Duties
- Root CA: High-security, low-frequency operations
- Intermediate CA: Regular certificate issuance
- End-entity: Single website/server
Root CA
|
┌────────────────┼────────────────┐
▼ ▼ ▼
Server CA Code Sign CA Email CA
| | |
┌───┴───┐ ┌───┴───┐ ┌───┴───┐
▼ ▼ ▼ ▼ ▼ ▼
Web1 Web2 Software App Email1 Email2
Cert Cert Cert Cert Cert Cert
Different CAs for:
- TLS/SSL certificates (web servers)
- Code signing certificates
- Email (S/MIME) certificates
- Document signing
- Time stamping
#
X.509 Certificate Structure
#
Anatomy of a Certificate
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
0e:fd:a3:84:6c:5f:4e:3d:52:a0:8d:0d:38:9e:f0:b7
Signature Algorithm: sha256WithRSAEncryption
Issuer: C=US, O=DigiCert Inc, CN=DigiCert SHA2 Secure Server CA
Validity
Not Before: Jan 1 00:00:00 2024 GMT
Not After : Jan 1 23:59:59 2025 GMT
Subject: C=US, ST=California, L=San Francisco,
O=Example Inc, CN=www.example.com
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public-Key: (2048 bit)
Modulus:
00:b4:31:98:0a:c3:fa:1c:73:4d:89:2b:e0:cc:84:
6c:4a:6d:2f:be:0e:95:a1:cf:09:28:19:8a:64:d5:
...
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Key Usage: critical
Digital Signature, Key Encipherment
X509v3 Extended Key Usage:
TLS Web Server Authentication, TLS Web Client Authentication
X509v3 Subject Alternative Name:
DNS:example.com, DNS:www.example.com,
DNS:api.example.com, DNS:*.example.com
X509v3 CRL Distribution Points:
Full Name:
URI:http://crl3.digicert.com/sha2-ev-server-g2.crl
Authority Information Access:
OCSP - URI:http://ocsp.digicert.com
CA Issuers - URI:http://cacerts.digicert.com/DigiCertSHA2SecureServerCA.crt
X509v3 Subject Key Identifier:
4E:46:D7:F3:89:2F:7C:69:7A:0E:10:F4:F0:F6:5A:65:C1:6F:C9:95
X509v3 Authority Key Identifier:
keyid:0F:80:61:1C:82:31:61:D5:2F:28:E7:8D:46:38:B4:2C:E1:C6:D9:E2
CT Precertificate SCTs:
Signed Certificate Timestamp:
Version : v1 (0x0)
Log ID : B2:1E:05:CC:8B:A2:CD:8A:20:4E:87:66:F9:2B:B9:8A:
25:20:67:6B:DA:FA:70:E7:B2:49:53:2D:EF:8B:90:5E
Timestamp : Jan 1 10:30:00.000 2024 GMT
Signature Algorithm: sha256WithRSAEncryption
3d:4a:c0:5b:7e:8f:9a:12:d7:c1:ef:10:6f:7a:b4:4c:89:3c:
d2:68:8f:0e:67:32:9c:2c:7e:35:fb:cc:d1:2a:72:94:35:ed:
...
#
Key Certificate Fields
#
Certificate Validation
#
The Complete Validation Process
When a browser connects to https://example.com, it must validate the certificate:
Step 1: Receive Certificate Chain
Server sends:
1. Leaf certificate (example.com)
2. Intermediate certificate(s)
3. (Root certificate not sent - already trusted)
Step 2: Build Certificate Chain
def build_chain(leaf_cert, intermediates, trust_store):
chain = [leaf_cert]
current = leaf_cert
while current.issuer != current.subject: # Until self-signed
# Find issuer in intermediates or trust store
issuer = find_issuer(current.issuer, intermediates + trust_store)
if not issuer:
raise ValidationError("Incomplete chain")
chain.append(issuer)
current = issuer
return chain
Step 3: Verify Signatures
def verify_signatures(chain):
for i in range(len(chain) - 1):
cert = chain[i]
issuer_cert = chain[i + 1]
# Verify cert was signed by issuer
if not verify_signature(cert, issuer_cert.public_key):
raise ValidationError(f"Invalid signature on {cert.subject}")
Step 4: Check Trust Anchor
def check_trust(chain, trust_store):
root_cert = chain[-1]
if root_cert not in trust_store:
raise ValidationError("Untrusted root CA")
# Verify root is self-signed
if not verify_signature(root_cert, root_cert.public_key):
raise ValidationError("Invalid root signature")
Step 5: Validate Certificate Fields
def validate_fields(cert, hostname):
# Check expiration
now = datetime.now()
if now < cert.not_before or now > cert.not_after:
raise ValidationError("Certificate expired or not yet valid")
# Check hostname
if not matches_hostname(cert.subject_alt_names, hostname):
raise ValidationError("Hostname mismatch")
# Check key usage
if not has_key_usage(cert, "digitalSignature", "keyEncipherment"):
raise ValidationError("Invalid key usage")
# Check extended key usage
if not has_ext_key_usage(cert, "serverAuth"):
raise ValidationError("Not valid for TLS server auth")
Step 6: Check Revocation Status
def check_revocation(cert):
# Try OCSP first
if cert.has_aia_ocsp():
status = check_ocsp(cert)
if status == "revoked":
raise ValidationError("Certificate revoked (OCSP)")
# Fall back to CRL
elif cert.has_crl_url():
status = check_crl(cert)
if status == "revoked":
raise ValidationError("Certificate revoked (CRL)")
#
Common Validation Errors
Certificate: CN=www.example.com, SAN=example.com, www.example.com
User visits: https://api.example.com
Error: ERR_CERT_COMMON_NAME_INVALID
Solution: Add all hostnames to Subject Alternative Names (SAN):
SAN: DNS:example.com, DNS:www.example.com, DNS:api.example.com
Certificate validity: 2024-01-01 to 2024-12-31
Current date: 2025-01-15
Error: ERR_CERT_DATE_INVALID
Solution: Renew certificate before expiration (automated with Let's Encrypt).
Certificate chain:
Leaf → Intermediate → Unknown Root CA
Error: ERR_CERT_AUTHORITY_INVALID
Solution: Use well-known CA (DigiCert, Let's Encrypt, etc.) or install root in trust store.
Server sends: Leaf certificate only
Missing: Intermediate certificate
Error: ERR_CERT_AUTHORITY_INVALID
Solution: Configure server to send complete chain:
ssl_certificate /path/to/fullchain.pem; # Includes intermediates
#
Root Certificate Stores
#
Where Root CAs Are Trusted
Windows:
certmgr.msc → Trusted Root Certification Authorities
Location: C:\Windows\System32\certsrv.msc
macOS:
Keychain Access → System Roots
Location: /System/Library/Keychains/SystemRootCertificates.keychain
Linux:
Debian/Ubuntu: /etc/ssl/certs/ca-certificates.crt
RHEL/CentOS: /etc/pki/tls/certs/ca-bundle.crt
Chrome/Edge:
- Uses OS trust store (Windows/macOS)
- Uses NSS trust store (Linux)
Firefox:
- Uses own trust store (NSS)
- Independent of OS
about:preferences#privacy→ View Certificates
Safari:
- Uses macOS Keychain
#
Major Certificate Authorities
#
Working with Certificates
#
Viewing Certificates
# View certificate from file
openssl x509 -in cert.pem -text -noout
# View certificate from server
openssl s_client -connect example.com:443 -showcerts
# Extract specific fields
openssl x509 -in cert.pem -noout -subject
openssl x509 -in cert.pem -noout -issuer
openssl x509 -in cert.pem -noout -dates
openssl x509 -in cert.pem -noout -fingerprint
from cryptography import x509
from cryptography.hazmat.backends import default_backend
# Load certificate
with open('cert.pem', 'rb') as f:
cert = x509.load_pem_x509_certificate(f.read(), default_backend())
# Extract fields
print(f"Subject: {cert.subject}")
print(f"Issuer: {cert.issuer}")
print(f"Not Before: {cert.not_valid_before}")
print(f"Not After: {cert.not_valid_after}")
print(f"Serial: {cert.serial_number}")
# Get Subject Alternative Names
san_ext = cert.extensions.get_extension_for_class(
x509.SubjectAlternativeName
)
for san in san_ext.value:
print(f"SAN: {san.value}")
const tls = require('tls');
const fs = require('fs');
// Load certificate from file
const certPem = fs.readFileSync('cert.pem');
const cert = tls.parseCertificate(certPem);
console.log('Subject:', cert.subject);
console.log('Issuer:', cert.issuer);
console.log('Valid From:', cert.valid_from);
console.log('Valid To:', cert.valid_to);
console.log('Serial Number:', cert.serialNumber);
// Get from server
const socket = tls.connect(443, 'example.com', () => {
const cert = socket.getPeerCertificate();
console.log('Server cert:', cert.subject);
console.log('SANs:', cert.subjectaltname);
socket.end();
});
#
Building Certificate Chains
# Combine leaf + intermediate + root
cat leaf.pem intermediate.pem root.pem > fullchain.pem
# Or just leaf + intermediate (root not needed)
cat leaf.pem intermediate.pem > chain.pem
# Verify chain
openssl verify -CAfile root.pem -untrusted intermediate.pem leaf.pem
# Output: leaf.pem: OK
#
Testing Certificate Chains
# Test server's certificate chain
openssl s_client -connect example.com:443 -showcerts
# Check certificate chain is complete
curl --verbose https://example.com 2>&1 | grep -A 10 "SSL certificate"
# Use SSL Labs for comprehensive test
# Visit: https://www.ssllabs.com/ssltest/analyze.html?d=example.com
#
Certificate Formats
#
Common Formats
#
Converting Between Formats
# PEM to DER
openssl x509 -in cert.pem -outform DER -out cert.der
# DER to PEM
openssl x509 -in cert.der -inform DER -out cert.pem -outform PEM
# PEM to PKCS#7 (chain)
openssl crl2pkcs7 -nocrl -certfile fullchain.pem -out cert.p7b
# PKCS#7 to PEM
openssl pkcs7 -in cert.p7b -print_certs -out cert.pem
# PEM to PKCS#12 (includes private key)
openssl pkcs12 -export -out cert.pfx \
-inkey private.key \
-in cert.pem \
-certfile chain.pem
# PKCS#12 to PEM
openssl pkcs12 -in cert.pfx -out cert.pem -nodes
#
Certificate Pinning
#
What is Certificate Pinning?
Definition: Hardcoding or caching a certificate/public key in your application to prevent man-in-the-middle attacks.
Without Pinning:
App → Trusts any certificate signed by trusted CA
Attacker → Gets certificate from compromised CA
App → Accepts attacker's certificate ❌
With Pinning:
App → Only trusts specific certificate/public key
Attacker → Gets certificate from compromised CA
App → Rejects certificate (doesn't match pin) ✓
#
Implementation
import ssl
import hashlib
import socket
# Calculate certificate fingerprint
def get_cert_fingerprint(hostname, port=443):
cert_pem = ssl.get_server_certificate((hostname, port))
cert_der = ssl.PEM_cert_to_DER_cert(cert_pem)
return hashlib.sha256(cert_der).hexdigest()
# Expected fingerprint (pinned)
PINNED_FINGERPRINT = "a1b2c3d4e5f6..."
# Verify pinned certificate
def verify_pinned_cert(hostname):
actual_fingerprint = get_cert_fingerprint(hostname)
if actual_fingerprint != PINNED_FINGERPRINT:
raise Exception("Certificate pin mismatch!")
return True
const https = require('https');
const crypto = require('crypto');
const PINNED_PUBLIC_KEY = 'sha256/AAAAAAAAAA...';
const options = {
hostname: 'example.com',
port: 443,
path: '/',
method: 'GET',
checkServerIdentity: (hostname, cert) => {
// Calculate public key hash
const pubkey = cert.pubkey;
const hash = crypto.createHash('sha256').update(pubkey).digest('base64');
const pin = `sha256/${hash}`;
if (pin !== PINNED_PUBLIC_KEY) {
throw new Error('Public key pin mismatch!');
}
}
};
https.request(options, (res) => {
console.log('Connected with valid pin');
}).end();
import android.net.http.HttpsURLConnection;
import java.security.cert.Certificate;
import java.security.MessageDigest;
public class PinningExample {
private static final String PINNED_HASH = "a1b2c3d4...";
public void makeRequest() throws Exception {
URL url = new URL("https://example.com");
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.connect();
// Get certificate
Certificate[] certs = conn.getServerCertificates();
Certificate cert = certs[0];
// Calculate hash
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] hash = md.digest(cert.getEncoded());
String hashHex = bytesToHex(hash);
// Verify pin
if (!PINNED_HASH.equals(hashHex)) {
throw new Exception("Certificate pin mismatch!");
}
// Proceed with request
}
}
#
Certificate vs Public Key Pinning
Recommendation: Pin public key, not certificate.
#
Pinning Best Practices
Pin multiple keys - Primary + backup Include intermediate CA - More flexible than leaf Plan for rotation - Have update mechanism Set expiration - Pins should expire Avoid root CA pinning - Too inflexible Don't pin in browsers - Use HPKP header (deprecated) or Expect-CT
#
Best Practices
#
Certificate Management
- Use reputable CAs (Let's Encrypt, DigiCert, etc.)
- Automate renewal (certbot for Let's Encrypt)
- Monitor expiration (alert 30 days before)
- Include all SANs (all domains/subdomains)
- Send complete chain (leaf + intermediates)
- Use 2048-bit RSA or 256-bit ECC minimum
- Enable OCSP stapling (faster, private)
- Rotate certificates regularly
- Keep private keys secure (HSM, encrypted storage)
- Use Certificate Transparency (detect mis-issuance)
#
Validation
- Test with SSL Labs (https://www.ssllabs.com/ssltest/)
- Verify complete chain locally
- Check all SANs work correctly
- Test OCSP/CRL revocation checking
- Validate in all browsers (Chrome, Firefox, Safari, Edge)
- Test mobile devices (iOS, Android)
#
Next Steps
#
Related Topics
TLS/SSL Basics - Understanding HTTPS HSTS - Force HTTPS connections Certificate Revocation - CRL and OCSP Cipher Suites - Encryption configuration
#
Tools & Resources
SSL Labs - Test certificate configuration crt.sh - Certificate Transparency search Let's Encrypt - Free automated certificates
#
Protected by Layerd AI
Layerd AI Guardian Proxy provides certificate management:
Certificate Validation - Verifies complete chain Expiration Monitoring - Alerts before expiration Pin Management - Enforces certificate pinning CT Log Monitoring - Detects unauthorized certificates
Learn more about Layerd AI Protection →
Last updated: November 2025