CMPUT-331 · Lesson 6 of 12 · 35 min

Certificates, public key infrastructure and TLS

Public-key cryptography lets you verify a signature with a public key, but it leaves a basic question open: how do you know that the public key really belongs to the bank, the government portal or the software vendor you think you are talking to? Public key infrastructure (PKI) answers that question with certificates: signed statements, issued by certificate authorities, that bind a public key to a name. Transport Layer Security (TLS) then uses certificates and the primitives from Lessons 3 to 5 to protect almost all web, email and API traffic. This lesson explains how certificates are structured and validated, how trust is anchored and revoked, and what happens in a TLS 1.3 handshake, with a complete handshake run in memory between two Python endpoints.

CMPUT-313 described TLS from the networking side, as a layer between TCP and HTTP that adds about one round trip. CISSP-112 described PKI governance, certificate policies and key ceremonies. Here we focus on the mechanisms and on the checks that application code must not skip.

What a certificate says

An X.509 certificate is a signed data structure. Its main fields are the subject (who the certificate is about), the subject's public key, the issuer (who signed it), a serial number, a validity period, and extensions. For websites the most important extension is the subject alternative name (SAN), which lists the DNS names the certificate is valid for; browsers ignore the older common name field for this purpose. Other extensions say whether the certificate may itself sign certificates (basic constraints), what the key may be used for (key usage and extended key usage) and where to find revocation information. The issuer signs a hash of all these fields with its private key, so any change to the certificate breaks the signature.

The code below builds a small private PKI: a root certificate authority that signs its own certificate, and a server certificate for a test domain signed by that root. It prints the fields a client checks and verifies the server certificate's signature against the root's public key. The validity dates are fixed so the output is reproducible.

Building a root CA and a server certificate with the cryptography library, then checking the signature
import datetime, tempfile, os, ssl
from cryptography import x509
from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
T0 = datetime.datetime(2026, 10, 1, tzinfo=datetime.timezone.utc)
def name(cn): return x509.Name([x509.NameAttribute(NameOID.COUNTRY_NAME, "CA"),
                                x509.NameAttribute(NameOID.ORGANIZATION_NAME, "HSM Demo"),
                                x509.NameAttribute(NameOID.COMMON_NAME, cn)])
ca_key = ec.generate_private_key(ec.SECP256R1())
ca = (x509.CertificateBuilder().subject_name(name("HSM Demo Root CA")).issuer_name(name("HSM Demo Root CA"))
      .public_key(ca_key.public_key()).serial_number(1001)
      .not_valid_before(T0).not_valid_after(T0 + datetime.timedelta(days=3650))
      .add_extension(x509.BasicConstraints(ca=True, path_length=0), critical=True)
      .add_extension(x509.KeyUsage(digital_signature=False, content_commitment=False, key_encipherment=False,
                     data_encipherment=False, key_agreement=False, key_cert_sign=True, crl_sign=True,
                     encipher_only=False, decipher_only=False), critical=True)
      .sign(ca_key, hashes.SHA256()))
srv_key = ec.generate_private_key(ec.SECP256R1())
srv = (x509.CertificateBuilder().subject_name(name("portal.example.test")).issuer_name(ca.subject)
       .public_key(srv_key.public_key()).serial_number(2002)
       .not_valid_before(T0).not_valid_after(T0 + datetime.timedelta(days=90))
       .add_extension(x509.SubjectAlternativeName([x509.DNSName("portal.example.test")]), critical=False)
       .add_extension(x509.ExtendedKeyUsage([ExtendedKeyUsageOID.SERVER_AUTH]), critical=False)
       .sign(ca_key, hashes.SHA256()))
print("subject :", srv.subject.rfc4514_string())
print("issuer  :", srv.issuer.rfc4514_string())
print("valid   :", srv.not_valid_before_utc.date(), "to", srv.not_valid_after_utc.date())
print("SAN     :", srv.extensions.get_extension_for_class(x509.SubjectAlternativeName).value.get_values_for_type(x509.DNSName))
srv.verify_directly_issued_by(ca)
print("chain   : server certificate verifies against the root's public key")
Output
subject : CN=portal.example.test,O=HSM Demo,C=CA
issuer  : CN=HSM Demo Root CA,O=HSM Demo,C=CA
valid   : 2026-10-01 to 2026-12-30
SAN     : ['portal.example.test']
chain   : server certificate verifies against the root's public key

Chains of trust

Clients do not trust server certificates directly. They trust a small set of root certificates shipped by the operating system or browser vendor, the trust store. Root keys are kept offline, so public certificate authorities issue end-entity certificates from intermediate CAs whose certificates are signed by the root. A server sends its own certificate and the intermediates; the client builds a chain up to a trusted root and validates every link.

A chain of three certificates. At the bottom, the server certificate for portal.example.test is signed by an intermediate CA. The intermediate CA certificate is signed by a root CA. The root certificate is self-signed and sits in the client trust store. Arrows labelled signed by point upward, and a checklist beside the chain lists signature, validity dates, name match, key usage and revocation.
A certificate chain from server to trusted root, with the checks made at each link

Libraries perform these checks for you, but only if you let them. Among the most common real-world TLS flaws is application code that disables verification to make an error go away, for example by passing verify=False to an HTTP client or by installing a trust-everything certificate manager. That turns TLS into encryption to whoever answers, which an attacker on the network can trivially be. The correct fix for an untrusted internal certificate is to add your organization's root to the client's trust store, not to switch validation off.

Who may issue certificates

Any of the hundreds of trusted CAs can issue a certificate for any domain, so the system is only as strong as its weakest CA. Several controls limit the damage. Domain validation requires the CA to check that the requester controls the domain, typically through the automated ACME protocol (RFC 8555) used by Let's Encrypt, by placing a token in DNS or on the web server. CAA DNS records let a domain owner list which CAs may issue for it. Certificate Transparency (RFC 6962) requires publicly trusted certificates to be logged in append-only public logs before browsers accept them, so domain owners can monitor for certificates they did not request; the 2011 DigiNotar compromise, in which an attacker issued fraudulent certificates for major sites, is a large part of why these measures exist. Browser root programs remove CAs that fail audits or mis-issue.

Revocation and short lifetimes

If a private key is stolen, the certificate must stop being trusted before it expires. Revocation has always been the weak point of PKI. Certificate revocation lists grow large, online status checks (OCSP) add latency and leak browsing history to the CA, and clients often fail open when a check cannot be completed. OCSP stapling, where the server attaches a recent signed status response, helps. The industry's main answer, however, is to make certificates short-lived so that a compromised certificate expires quickly anyway, combined with full automation of renewal. In April 2025 the CA/Browser Forum adopted ballot SC-081, which steps down the maximum lifetime of public TLS certificates.

Maximum public TLS certificate lifetimes under CA/Browser Forum ballot SC-081
import datetime
# Maximum public TLS certificate lifetimes under CA/Browser Forum ballot SC-081 (2025)
schedule = [(datetime.date(2026, 3, 15), 200), (datetime.date(2027, 3, 15), 100), (datetime.date(2029, 3, 15), 47)]
def max_days(issued: datetime.date) -> int:
    days = 398
    for start, limit in schedule:
        if issued >= start:
            days = limit
    return days
for d in (datetime.date(2025, 6, 1), datetime.date(2026, 10, 24), datetime.date(2028, 1, 10), datetime.date(2029, 6, 1)):
    m = max_days(d)
    print(f"issued {d}: at most {m:>3} days -> renewals per year about {365 / m:.1f}")
Output
issued 2025-06-01: at most 398 days -> renewals per year about 0.9
issued 2026-10-24: at most 200 days -> renewals per year about 1.8
issued 2028-01-10: at most 100 days -> renewals per year about 3.6
issued 2029-06-01: at most  47 days -> renewals per year about 7.8

With lifetimes heading towards 47 days, manual renewal becomes impractical. Organizations need an inventory of their certificates, automated issuance and deployment with ACME or a private CA's API, and monitoring that alerts well before expiry. Expired certificates still cause high-profile outages every year.

The TLS 1.3 handshake

TLS 1.3 (RFC 8446, 2018) simplified the protocol and removed weak options. The client's first message, ClientHello, offers cipher suites and includes a key share: an ephemeral X25519 public value, increasingly combined with ML-KEM for post-quantum protection. The server replies with ServerHello and its own key share, so both sides can compute a shared secret immediately and encrypt everything that follows. The server then sends its certificate chain, a CertificateVerify message signing a hash of the whole handshake so far with its certificate's private key, and a Finished message with a MAC over the transcript. The client checks the chain, the hostname, the signature and the MAC, sends its own Finished, and application data flows. A full handshake takes one round trip.

A sequence diagram between client and server. The client sends ClientHello with a key share. The server replies with ServerHello and its key share, then, encrypted, its certificate, CertificateVerify and Finished. The client replies with Finished, and encrypted application data follows. A brace marks the exchange as one round trip.
The TLS 1.3 full handshake: key exchange in the first round trip, everything after ServerHello encrypted

Each element defends against a specific threat. The ephemeral key exchange gives forward secrecy. Signing the transcript authenticates the server and binds the key exchange to its certificate, defeating the person-in-the-middle from Lesson 5. MACing the transcript detects any attempt to tamper with the negotiation, such as downgrading to weaker options. TLS 1.3 also removed RSA key transport, static Diffie–Hellman, CBC mode, RC4, SHA-1 and compression; all five of its cipher suites are AEAD. An optional zero round-trip mode (0-RTT) lets returning clients send data in their first flight, at the cost of replay risk, so it should only carry idempotent requests.

The example below generates a fresh root and server certificate, then runs a real TLS 1.3 handshake between a Python client and server through in-memory buffers, with no network at all. The client verifies the chain and hostname. A second attempt asks for a different hostname and is rejected, exactly as a browser would refuse a certificate issued for another site.

A complete TLS 1.3 handshake in memory, with certificate and hostname verification
import datetime, tempfile, os, ssl
from cryptography import x509
from cryptography.x509.oid import NameOID
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
now = datetime.datetime.now(datetime.timezone.utc)
def make(cn, issuer_name, issuer_key, key, ca):
    b = (x509.CertificateBuilder().subject_name(x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, cn)]))
         .issuer_name(issuer_name).public_key(key.public_key()).serial_number(x509.random_serial_number())
         .not_valid_before(now - datetime.timedelta(days=1)).not_valid_after(now + datetime.timedelta(days=30))
         .add_extension(x509.BasicConstraints(ca=ca, path_length=None), critical=True))
    b = b.add_extension(x509.SubjectKeyIdentifier.from_public_key(key.public_key()), critical=False)
    b = b.add_extension(x509.AuthorityKeyIdentifier.from_issuer_public_key(issuer_key.public_key()), critical=False)
    if ca:
        b = b.add_extension(x509.KeyUsage(digital_signature=False, content_commitment=False, key_encipherment=False,
            data_encipherment=False, key_agreement=False, key_cert_sign=True, crl_sign=True,
            encipher_only=False, decipher_only=False), critical=True)
    else:
        b = b.add_extension(x509.SubjectAlternativeName([x509.DNSName(cn)]), critical=False)
    return b.sign(issuer_key, hashes.SHA256())
ca_key, srv_key = ec.generate_private_key(ec.SECP256R1()), ec.generate_private_key(ec.SECP256R1())
ca_name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "Demo Root CA")])
ca = make("Demo Root CA", ca_name, ca_key, ca_key, True)
srv = make("portal.example.test", ca_name, ca_key, srv_key, False)
d = tempfile.mkdtemp()
pem = lambda c: c.public_bytes(serialization.Encoding.PEM)
open(f"{d}/ca.pem", "wb").write(pem(ca))
open(f"{d}/srv.pem", "wb").write(pem(srv) + srv_key.private_bytes(serialization.Encoding.PEM,
     serialization.PrivateFormat.PKCS8, serialization.NoEncryption()))
server_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER); server_ctx.load_cert_chain(f"{d}/srv.pem")
client_ctx = ssl.create_default_context(cafile=f"{d}/ca.pem")
client_ctx.minimum_version = ssl.TLSVersion.TLSv1_3
def handshake(server_hostname):
    c_in, c_out, s_in, s_out = ssl.MemoryBIO(), ssl.MemoryBIO(), ssl.MemoryBIO(), ssl.MemoryBIO()
    client = client_ctx.wrap_bio(c_in, c_out, server_hostname=server_hostname)
    server = server_ctx.wrap_bio(s_in, s_out, server_side=True)
    for _ in range(10):                       # shuttle handshake bytes between the two ends in memory
        for end in (client, server):
            try: end.do_handshake()
            except ssl.SSLWantReadError: pass
        s_in.write(c_out.read()); c_in.write(s_out.read())
        if client.version() and server.version(): break
    return client
tls = handshake("portal.example.test")
print("protocol:", tls.version())
print("cipher  :", tls.cipher()[0])
print("peer    :", dict(x[0] for x in tls.getpeercert()["subject"]))
try:
    handshake("evil.example.test")
except ssl.SSLCertVerificationError as e:
    print("wrong name rejected:", e.verify_message)
Output
protocol: TLSv1.3
cipher  : TLS_AES_256_GCM_SHA384
peer    : {'commonName': 'portal.example.test'}
wrong name rejected: Hostname mismatch, certificate is not valid for 'evil.example.test'.

Beyond the web: code signing and supply chains

The same ideas protect software. Operating systems and app stores verify code-signing certificates before running programs; package ecosystems publish digests and signatures; secure boot checks a chain of signatures from firmware to kernel. Newer systems such as Sigstore issue short-lived signing certificates tied to a developer's identity and record every signature in a transparency log, applying Certificate Transparency's idea to software. At minimum, verify digests of downloads against values published on a signed or HTTPS page.

Verifying a download against a published digest, and the format of a public-key pin
import hashlib, base64
# Checking a downloaded file against a digest published on a signed release page
release_bytes = b"#!/bin/sh\necho 'hsm-tool 2.4.1 installer (demo payload)'\n"
published = hashlib.sha256(release_bytes).hexdigest()       # what the project publishes and signs
def verify_download(data: bytes, expected_hex: str) -> str:
    actual = hashlib.sha256(data).hexdigest()
    return "OK, digest matches" if actual == expected_hex else "REJECT: digest mismatch, do not run"
print(verify_download(release_bytes, published))
print(verify_download(release_bytes + b"echo line added by an unknown mirror\n", published))
# A public-key pin is the base64 SHA-256 of the key's SubjectPublicKeyInfo (as used by HPKP and some apps)
spki = b"0Y0\x13\x06\x07*\x86H\xce=\x02\x01..." # truncated demo bytes
print("pin format: sha256/" + base64.b64encode(hashlib.sha256(spki).digest()).decode())
Output
OK, digest matches
REJECT: digest mismatch, do not run
pin format: sha256/aZLhnYBk8VKtw0vkmwUuLiWGMRzaEOYaBE08MCEhbVY=

For Government of Canada systems, the Canadian Centre for Cyber Security's guidance (ITSP.40.062) requires TLS 1.2 or 1.3 with approved cipher suites and recommends TLS 1.3 where available, and departments manage certificates for their web properties under Treasury Board direction that all public websites use HTTPS only. Organizations outside government commonly adopt the same baseline.

Worked examples

Worked problem

Diagnosing a certificate error

Users report a browser warning on https://intranet.example.ca. The certificate shows subject CN=intranet, SAN entries www.example.ca and example.ca, valid dates that include today, and a chain to a trusted public root. What is wrong and how is it fixed?

Show solution
  1. The browser matches the requested hostname intranet.example.ca against SAN entries only; the common name is ignored.
  2. Neither www.example.ca nor example.ca matches intranet.example.ca, so validation fails on the name check.
  3. Reissue the certificate with intranet.example.ca in the SAN (or a wildcard *.example.ca if policy allows) and automate renewal.

Answer: The hostname is not in the subject alternative names; reissue with the correct SAN.

Worked problem

A developer disables verification

An internal script calls requests.get(url, verify=False) because the internal service uses a certificate from the company's private CA. What risk does this create and what is the right fix?

Show solution
  1. With verification off, the script accepts any certificate, so anyone who can intercept traffic can impersonate the service and read or alter data.
  2. The certificate is not invalid, merely issued by a CA the script does not trust.
  3. Fix: distribute the company root certificate and point the client at it (verify="/etc/ssl/company-root.pem"), or add the root to the system trust store.

Answer: Disabling verification invites person-in-the-middle attacks; trust the private root explicitly instead.

Worked problem

Planning for 47-day certificates

A university runs 300 public TLS certificates renewed by hand once a year. What changes are needed before the SC-081 limits reach 47 days in 2029?

Show solution
  1. At 47 days each certificate needs about eight renewals a year, so 300 certificates mean about 2,300 renewals: manual work will fail.
  2. Build an inventory from Certificate Transparency logs and internal scanning so no certificate is forgotten.
  3. Automate issuance and deployment with ACME clients or the CA's API, and monitor expiry with alerts at least two weeks ahead.

Answer: Inventory every certificate, automate renewal with ACME, and alert on expiry well in advance.

Common mistakes

  • MistakeIf the connection is encrypted, the server must be genuine.

    InsteadEncryption without certificate and hostname verification protects you only from passive eavesdroppers, not impersonators.

  • MistakeThe common name field decides which hostnames a certificate covers.

    InsteadBrowsers and libraries match the subject alternative name extension.

  • MistakeSetting verify=False is a safe way to use internal services.

    InsteadTrust the internal root explicitly; never disable verification.

  • MistakeRevocation reliably stops stolen certificates.

    InsteadRevocation checking is often soft-fail; short lifetimes and automation are the main protection.

  • MistakeTLS 1.2 and 1.3 are equally strong in any configuration.

    InsteadPrefer TLS 1.3; if 1.2 is needed, allow only AEAD suites with forward-secret key exchange.

Summary and key terms

  • An X.509 certificate binds a public key to names, signed by an issuer; SANs list the valid hostnames.
  • Clients build a chain to a trusted root and check signatures, dates, names, constraints and revocation.
  • Never disable verification; trust private roots explicitly instead.
  • ACME, CAA records and Certificate Transparency constrain and monitor certificate issuance.
  • Revocation is weak in practice, so lifetimes are shrinking: 200 days from 2026 to 47 days from 2029.
  • The TLS 1.3 handshake uses ephemeral key exchange, a signed transcript and Finished MACs in one round trip.
  • Code signing, secure boot and transparency logs apply PKI ideas to software supply chains.
X.509 certificate
A signed structure binding a public key to an identity, with validity dates and extensions.
Certificate authority
An entity trusted to issue and sign certificates.
Trust store
The set of root certificates a client trusts.
Subject alternative name
The certificate extension listing the DNS names or addresses it is valid for.
Intermediate CA
A CA whose certificate is signed by a root and which issues end-entity certificates.
Certificate Transparency
Public append-only logs of issued certificates that owners can monitor.
OCSP stapling
A server attaching a recent signed revocation status to its handshake.
ACME
The automated protocol for proving domain control and obtaining certificates.
CertificateVerify
The TLS handshake message in which the server signs the transcript with its certificate key.
0-RTT
TLS 1.3 early data sent with the first flight, which can be replayed.

Practice problems

Work each problem on paper first, then reveal the answer to check.

  1. Which certificate field do clients match against the hostname?

    Show answer

    The subject alternative name (SAN) extension.

  2. What does a root CA certificate sign?

    Show answer

    Itself, and the certificates of intermediate CAs.

  3. Why are root CA keys kept offline?

    Show answer

    Their compromise would let an attacker issue trusted certificates for any domain; offline storage reduces that risk.

  4. What problem does Certificate Transparency address?

    Show answer

    Mis-issued certificates going unnoticed; public logs let domain owners detect them.

  5. What does a CAA record do?

    Show answer

    It lists which certificate authorities may issue certificates for a domain.

  6. Why is OCSP often described as soft-fail?

    Show answer

    If the status check cannot be completed, clients usually proceed anyway.

  7. What is the maximum public certificate lifetime from March 2027 under SC-081?

    Show answer

    100 days.

  8. Which TLS 1.3 message authenticates the server?

    Show answer

    CertificateVerify, a signature over the handshake transcript.

  9. Why should 0-RTT data carry only idempotent requests?

    Show answer

    It can be replayed by an attacker.

  10. Give the safe alternative to verify=False for an internal service.

    Show answer

    Configure the client to trust the organization's root certificate.

Lesson quiz · 5 questions

Score 60% or more to complete this lesson.

Take the quiz