<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Parag Mali - tag: web-security</title><description>Posts tagged web-security.</description><link>https://paragmali.com/</link><language>en-US</language><lastBuildDate>Sat, 25 Jul 2026 08:57:44 GMT</lastBuildDate><atom:link href="https://paragmali.com/tags/web-security/rss.xml" rel="self" type="application/rss+xml"/><item><title>Two Identical Requests: How Web Bot Auth and HTTP Message Signatures Let Servers Trust the Right Bots</title><link>https://paragmali.com/blog/two-identical-requests-how-web-bot-auth-and-http-message-sig/</link><guid isPermaLink="true">https://paragmali.com/blog/two-identical-requests-how-web-bot-auth-and-http-message-sig/</guid><description>How RFC 9421 and Web Bot Auth give bots and AI agents a standardized, directory-backed cryptographic identity -- and why it proves who, never whether-allowed.</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>
For thirty years the web authenticated humans with ever-stronger cryptography while governing bots with etiquette (`robots.txt`), self-declared names (`User-Agent`), and network guesswork (IP allowlists) -- none of which can tell a legitimate AI agent apart from a scraper. RFC 9421 (HTTP Message Signatures, February 2024) [@rfc9421] and the Cloudflare and Google **Web Bot Auth** drafts [@draft-webbotauth-arch] fix this: a bot signs selected *components* of each request with an Ed25519 key and publishes its public keys in a discoverable JWKS directory, so any origin can verify a stranger bot on first contact. The genuine novelty is not the cryptography -- machines have signed requests since AWS SigV4 and ActivityPub [@cf-blog-webbotauth] -- but a **standardized, directory-backed, open-web identity anyone can look up**. And it proves exactly one thing: *who signed*, never *whether they are allowed*.
&lt;h2&gt;1. Two Identical Requests&lt;/h2&gt;
&lt;p&gt;Two HTTP requests hit your server in the same second. Both carry the identical Chrome &lt;code&gt;User-Agent&lt;/code&gt;. Both arrive from the same shared cloud IP range. Both ask for the same page. One is OpenAI&apos;s Operator, fetching that page on behalf of a paying customer. The other is a scraper, harvesting your content to resell. You have milliseconds to decide which one to admit -- and with everything the web handed you across its first thirty years, you literally cannot tell them apart.&lt;/p&gt;
&lt;p&gt;That is not a failure of effort. It is a failure of design. For decades the web learned to authenticate &lt;em&gt;humans&lt;/em&gt; with steadily stronger cryptography: passwords gave way to asymmetric keys, and asymmetric keys gave way to passkeys bound to a specific origin. Machines got none of that. The web still governs automated clients the way a library governs its patrons -- with a posted set of rules and the hope that visitors behave. This article is the story of the layer that finally closes the gap -- and the one thing it still refuses to prove.&lt;/p&gt;
&lt;p&gt;Start with why the problem is genuinely hard. Cloudflare, whose network sits in front of a large fraction of the web, framed it bluntly in a 2025 proposal titled &quot;Forget IPs&quot;: the three signals an origin has today are each broken in a different way [@cf-blog-webbotauth].&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;User-Agent&lt;/code&gt; string is &lt;em&gt;spoofable&lt;/em&gt; -- just text the client types, and agents deliberately send a real Chrome string. The source IP is &lt;em&gt;brittle&lt;/em&gt; -- one address is routinely shared across unrelated users, privacy proxies, and whole companies renting the same cloud. A pre-shared secret, the classic API-key answer, is &lt;em&gt;pairwise&lt;/em&gt; -- it forces every bot to arrange a separate credential with every site it might visit. None of the three answers the only question that matters: &lt;em&gt;who is actually sending this request?&lt;/em&gt;&lt;/p&gt;

The set of Cloudflare- and Google-authored IETF Internet-Drafts that profile RFC 9421 (HTTP Message Signatures) for the specific job of bot identity, and add the piece the RFC leaves out -- a discoverable public key directory. Together they let an origin cryptographically verify *which* automated client sent a request, with no prior arrangement.
&lt;p&gt;Before going further, fix the cast of characters. An &lt;strong&gt;AI agent&lt;/strong&gt; here means an orchestrated browser -- often a real, automated instance of Chrome -- acting on a person&apos;s behalf: booking a flight, comparing prices, reading a page and summarizing it. From the server&apos;s side it looks exactly like a human&apos;s browser, because it frequently &lt;em&gt;is&lt;/em&gt; one. That is precisely why the old signals collapse: the agent is not pretending to be Chrome, it &lt;em&gt;is&lt;/em&gt; Chrome, driven by software instead of hands.&lt;/p&gt;

flowchart TD
    A[&quot;AI agent acting for a user&lt;br /&gt;real Chrome User-Agent, shared cloud IP&quot;] --&amp;gt; O{&quot;Origin: which do I admit?&quot;}
    B[&quot;Scraper reselling content&lt;br /&gt;real Chrome User-Agent, shared cloud IP&quot;] --&amp;gt; O
    O --&amp;gt; Q[&quot;No verifiable identity signal&lt;br /&gt;the two requests are indistinguishable&quot;]
&lt;p&gt;There is a deeper idea hiding in that question, and the whole article turns on it. &quot;Who is sending this?&quot; is a question about &lt;em&gt;identity&lt;/em&gt;. &quot;Should I let them in?&quot; is a question about &lt;em&gt;authorization&lt;/em&gt;. They feel like one decision at the door, but they are two, and conflating them is the mistake that makes bot management so miserable. Everything that follows gives the web a way to answer the first question with cryptographic certainty -- while being scrupulously honest that it never answers the second.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; A signature can prove &lt;em&gt;who&lt;/em&gt; sent a request without proving the sender is &lt;em&gt;allowed&lt;/em&gt; to make it. Verifiable identity and authorization are different problems. Web Bot Auth solves the first and, by design, leaves the second to policy. Hold on to that distinction -- it is the hinge every later section turns on.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So the refined claim this article defends is narrower and more interesting than the headline. After decades of hardening how it authenticates people, the web is finally giving agents a cryptographic identity that is &lt;em&gt;standardized and discoverable&lt;/em&gt; -- not the first-ever cryptography for machines, but the first kind any origin can simply look up. If you cannot trust what a bot &lt;em&gt;says&lt;/em&gt; it is, and you cannot trust &lt;em&gt;where&lt;/em&gt; it comes from, what is left to trust? The answer took the web thirty years to build. To see why it was so hard, we have to go back to 1994.&lt;/p&gt;
&lt;h2&gt;2. Identity for Humans, Politeness for Machines&lt;/h2&gt;
&lt;p&gt;In 1993 and 1994, the first web robots &quot;swamped servers with rapid-fire requests&quot; and wandered into corners of sites they should have left alone [@robotstxt-orig]. There was no way for a server to tell a robot where not to go. Martijn Koster, working with a mailing list of the era&apos;s robot authors, proposed a fix and the group reached consensus on it on 30 June 1994. His fix was not a lock. It was a polite note taped to the door.&lt;/p&gt;
&lt;p&gt;That note is &lt;code&gt;robots.txt&lt;/code&gt;: a single file at a well-known path listing the URL paths a crawler is &lt;em&gt;requested&lt;/em&gt; not to fetch. A well-behaved crawler downloads it once, reads the section addressed to its own self-declared name, and voluntarily stays out of the listed areas.&lt;/p&gt;
&lt;p&gt;The founding text is remarkably candid about what it is not. The protocol, it says, &quot;is not an official standard backed by a standards body,&quot; and it &quot;is not enforced by anybody, and there [is] no guarantee that all current and future robots will use it&quot; [@robotstxt-orig]. It was standardized twenty-eight years later as RFC 9309, which records that the protocol was &quot;originally defined by Martijn Koster in 1994&quot; [@rfc9309].&lt;/p&gt;
&lt;p&gt;Read those words carefully, because they contain the distinction the rest of this article depends on. &lt;code&gt;robots.txt&lt;/code&gt; never authenticates anyone. The server advertises a policy and hopes; it never learns &lt;em&gt;who&lt;/em&gt; is asking. RFC 9309 draws the line in one sentence: its rules &quot;are not a form of access authorization&quot; [@rfc9309]. This is etiquette, not identity, and certainly not enforcement. A hostile scraper reads &lt;code&gt;Disallow: /&lt;/code&gt;, crawls the whole site anyway, and the origin is left with neither a barrier nor a name.&lt;/p&gt;

While machines got etiquette, people got cryptography -- and steadily more of it. The web moved from passwords (a shared secret the server stores and the user retypes), to asymmetric second factors, to [WebAuthn and passkeys](/blog/webauthn-and-passkeys-on-windows-from-ctap-to-the-credential/), where the browser holds a private key bound to a specific origin and proves possession without ever transmitting the secret. That is the exact shape of guarantee -- a private key that signs, a public key that verifies, tied to an identity -- that bots never received. This blog&apos;s field guide on passkeys covers the human side; Web Bot Auth is, in a real sense, the same idea arriving decades late for machines.
&lt;p&gt;If a policy file cannot identify anyone, the obvious next step is to let the client &lt;em&gt;say&lt;/em&gt; who it is. The web already had a place for that: the &lt;code&gt;User-Agent&lt;/code&gt; header, present since HTTP/1.0 in 1996 [@rfc1945] and given its current normative definition in RFC 9110, section 10.1.5, in June 2022 [@rfc9110].&lt;/p&gt;
&lt;p&gt;A crawler announces itself -- &lt;code&gt;User-Agent: MyBotCrawler/1.1&lt;/code&gt; -- and the origin can, in principle, allow the crawlers it trusts and block the rest. For the first time the server has an identity claim it can act on, per request, without relying on the client to police its own behavior.&lt;/p&gt;
&lt;p&gt;There is one catch, and it is fatal: the claim is unauthenticated. A header is just a string, and anyone can send any string.&lt;/p&gt;
&lt;p&gt;The specification itself concedes the point. If a user agent &quot;masquerades as a different user agent,&quot; it notes, recipients simply have to assume the client meant to be treated as whatever it claimed [@rfc9110]. Cloudflare states the operational consequence without euphemism: user-agent strings &quot;alone are easily spoofed and are therefore insufficient for reliable identification,&quot; and agents &quot;often use the Chrome user agent for this very reason&quot; [@cf-blog-webbotauth].The masquerade language lives in RFC 9110 section 10.1.5; the same sentence is reproduced, with that section citation, in Cloudflare&apos;s &quot;Forget IPs&quot; post [@cf-blog-webbotauth], which is where many practitioners first meet it.&lt;/p&gt;
&lt;p&gt;So the second generation of bot identity answers a subtly wrong question. &lt;code&gt;User-Agent&lt;/code&gt; tells you &lt;em&gt;who the client says it is&lt;/em&gt;, never &lt;em&gt;who it is&lt;/em&gt;. And that is exactly the hook from the previous section: a legitimate agent and a scraper both send a real Chrome string, and the origin cannot separate them. A name anyone can type is not an identity. So perhaps the answer is to stop trusting what the bot writes and start trusting something it cannot simply type -- the network it comes from.&lt;/p&gt;
&lt;h2&gt;3. Anchoring Identity to the Network&lt;/h2&gt;
&lt;p&gt;If a bot can lie about its name, tie its identity to something it cannot simply write down: the network it lives on. A random impostor can set any &lt;code&gt;User-Agent&lt;/code&gt;, but it cannot easily forge control of a vendor&apos;s DNS or occupy a specific block of IP addresses. For roughly a decade, this was the strongest bot-verification signal the web had.&lt;/p&gt;
&lt;p&gt;Google pioneered the technique for Googlebot, and the canonical version has two composable checks. The first is forward-confirmed reverse DNS.&lt;/p&gt;

Run a reverse (PTR) lookup on the source IP to get a hostname -- for Googlebot, something like `crawl-66-249-66-1.googlebot.com` -- then forward-resolve that hostname and confirm it maps back to the same IP. Passing both directions proves the request came from network infrastructure the claimed operator controls. It proves network control, not identity.
&lt;p&gt;Forging that requires controlling both directions of DNS for the vendor&apos;s domain, which a casual spoofer cannot do [@google-verify]. The second check is a published IP allowlist: the vendor publishes its address ranges as CIDR blocks in a file, and the origin checks whether the source IP falls inside them [@cf-blog-webbotauth]. Google documents both paths today -- common crawlers reverse-resolve under &lt;code&gt;googlebot.com&lt;/code&gt; and appear in a published &lt;code&gt;common-crawlers.json&lt;/code&gt;, while special-case fetchers use a separate &lt;code&gt;google.com&lt;/code&gt; mask [@google-verify].&lt;/p&gt;
&lt;p&gt;This was a real improvement. Identity was now anchored to something the requester could not self-assert; the request had to originate from Google&apos;s footprint. &quot;Verify Googlebot&quot; became standard operating procedure, and for years it worked.The mechanism is a little older than any dated write-up, but Google&apos;s &quot;How to verify Googlebot&quot; post, dated 20 September 2006, is the first primary source that spells it out and calls reverse-plus-forward DNS &quot;an official way to authenticate Googlebot&quot; [@google-2006-blog]. That is the debut this article pins to.&lt;/p&gt;
&lt;p&gt;And then it broke -- not at the edges, but at its foundation. Reverse DNS and IP lists prove &lt;em&gt;network location&lt;/em&gt;, and location is not identity. The entire premise is that a source IP maps to a single, identifiable operator, and modern infrastructure dissolves that mapping.&lt;/p&gt;
&lt;p&gt;Cloudflare puts the failure plainly: the same IP address &quot;might be shared by multiple users or multiple services within the same company, or even by multiple companies when hosting infrastructure is shared (like Cloudflare Workers)&quot; [@cf-blog-webbotauth]. Privacy proxies and VPNs interpose addresses that belong to neither the operator nor any stable identity. Serverless platforms hand out egress addresses that churn with the underlying infrastructure, which is why vendors resort to constantly-updated CIDR lists [@cf-blog-webbotauth].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Every network-anchored check answers the question &quot;what network did this packet come from?&quot; That was a useful proxy for identity when an operator owned its addresses. The moment agents began running on shared clouds, privacy relays, and serverless platforms, the proxy stopped pointing at anything. The signal did not weaken -- it started measuring the wrong thing.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Picture the concrete case that matters now. An AI agent runs on a serverless platform and shares its outbound address with thousands of unrelated tenants. A reverse lookup on that address resolves to the cloud provider, not to the agent&apos;s operator. The origin can confirm &quot;this came from a big cloud&quot; and learns nothing about &lt;em&gt;which&lt;/em&gt; automated client sent it.&lt;/p&gt;
&lt;p&gt;Worse, the scheme is per-vendor and brittle in both directions: every operator must publish and maintain its own ranges, and every origin must track every operator&apos;s ever-shifting lists. Onboarding a new verified crawler historically meant hand-checking IP ranges and autonomous-system numbers -- exactly the manual toil the cryptographic approach was later built to retire [@cf-blog-verifiedbots].&lt;/p&gt;
&lt;p&gt;The flaw, then, is not the mechanism. It is the premise. An IP address was never an identity; it was a stand-in for one, and stand-ins leak. To bind identity to the &lt;em&gt;operator itself&lt;/em&gt; -- independent of whatever address it happens to borrow today -- you need something only that operator holds and can present on demand. You need a secret. Or, better still, a key.&lt;/p&gt;
&lt;h2&gt;4. Generation by Generation&lt;/h2&gt;
&lt;p&gt;Step back from the individual mechanisms and a pattern appears: each one failed in a way that named its successor. &lt;code&gt;robots.txt&lt;/code&gt; had no identity, so we added a self-declared name. The name was spoofable, so we anchored to the network. The network proved location instead of identity, so we reached for a secret. Six generations, each a better answer to the same stubborn question, each breaking in a way that forced the next.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Generation&lt;/th&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;What it added&lt;/th&gt;
&lt;th&gt;The failure that forced the next&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;1 (1994)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;robots.txt&lt;/code&gt; honor system&lt;/td&gt;
&lt;td&gt;A shared vocabulary for addressing crawlers&lt;/td&gt;
&lt;td&gt;No authentication, no enforcement -- the server never learns who is asking [@robotstxt-orig]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2 (1996)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;User-Agent&lt;/code&gt; header&lt;/td&gt;
&lt;td&gt;A per-request identity claim to act on&lt;/td&gt;
&lt;td&gt;Self-declared and trivially spoofed; a real Chrome string hides everything [@rfc9110]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3 (~2006)&lt;/td&gt;
&lt;td&gt;Reverse DNS + IP allowlists&lt;/td&gt;
&lt;td&gt;Identity anchored to network infrastructure&lt;/td&gt;
&lt;td&gt;Proves location, not who; collapses on shared clouds, VPNs, serverless [@cf-blog-webbotauth]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 (2012)&lt;/td&gt;
&lt;td&gt;Bearer tokens / API keys&lt;/td&gt;
&lt;td&gt;Portable, IP-independent possession&lt;/td&gt;
&lt;td&gt;Possession is not proof, and secrets are pairwise -- N x M distribution [@rfc6750]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5 (2013-19)&lt;/td&gt;
&lt;td&gt;Per-vendor request signing (SigV4, draft-cavage, Stripe, mTLS)&lt;/td&gt;
&lt;td&gt;Real asymmetric cryptography plus integrity&lt;/td&gt;
&lt;td&gt;No shared format, no interoperable key discovery -- every scheme an island [@cf-blog-webbotauth]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6 (2024-26)&lt;/td&gt;
&lt;td&gt;RFC 9421 + Web Bot Auth + JWKS directory&lt;/td&gt;
&lt;td&gt;A standard signature and a discoverable public key&lt;/td&gt;
&lt;td&gt;Not superseded; its limit is fundamental -- it proves who, never whether-allowed [@draft-webbotauth-arch]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Generation 4 deserves a moment, because it is where cryptography first almost arrived. A Bearer token or API key is a secret string issued out of band; the bot presents it on every request and the origin checks it against what it handed out. Identity now travels with the credential instead of the address, so changing IPs no longer breaks it.&lt;/p&gt;
&lt;p&gt;But RFC 6750 is unusually honest about the model&apos;s ceiling: &quot;Any party in possession of a bearer token (a &apos;bearer&apos;) can use it to get access to the associated resources (without demonstrating possession of a cryptographic key)&quot; [@rfc6750]. Possession is not proof -- anyone who intercepts or leaks the token can replay it.&lt;/p&gt;
&lt;p&gt;And the secret is &lt;em&gt;shared&lt;/em&gt; between exactly two parties, so a bot must arrange a distinct one with every origin it visits. Cloudflare names the wall directly: &quot;You go to every website and share a secret ... This is impractical at scale because it requires developers to maintain separate tokens for each website their bot will visit&quot; [@cf-blog-webbotauth].&lt;/p&gt;

flowchart LR
    G1[&quot;1994&lt;br /&gt;robots.txt&lt;br /&gt;honor system&quot;] --&amp;gt; G2[&quot;1996&lt;br /&gt;User-Agent&lt;br /&gt;self-declared&quot;]
    G2 --&amp;gt; G3[&quot;2006&lt;br /&gt;reverse DNS + IP&lt;br /&gt;network-anchored&quot;]
    G3 --&amp;gt; G4[&quot;2012&lt;br /&gt;Bearer / API keys&lt;br /&gt;shared secret&quot;]
    G4 --&amp;gt; G5[&quot;2013-2019&lt;br /&gt;SigV4, cavage, Stripe, mTLS&lt;br /&gt;fragmented crypto&quot;]
    G5 --&amp;gt; G6[&quot;2024-2026&lt;br /&gt;RFC 9421 + Web Bot Auth&lt;br /&gt;standardized + discoverable&quot;]
&lt;p&gt;The fix for a leakable, pairwise secret is an asymmetric keypair: the bot signs with a private key it never discloses, and any origin verifies with the matching public key. One keypair works everywhere, interception no longer grants impersonation, and the secret never leaves the signer.&lt;/p&gt;
&lt;p&gt;And here is the part most summaries get wrong -- &lt;strong&gt;the industry built exactly this, repeatedly, more than a decade before Web Bot Auth.&lt;/strong&gt; AWS Signature Version 4 signed canonical requests at cloud scale [@aws-sigv4]. Stripe authenticated webhooks with an HMAC over a timestamped payload [@stripe-webhooks]. &lt;code&gt;draft-cavage-http-signatures&lt;/code&gt;, first published in May 2013, signed selected HTTP headers with an asymmetric key and was adopted de-facto across the ActivityPub and Mastodon Fediverse [@draft-cavage]. Mutual TLS presented client certificates. Cryptographic request identity for machines was not missing. It was everywhere -- and useless for the open web.&lt;/p&gt;

&quot;It&apos;s not the only way that developers can sign requests ... AWS has used Signature v4, and Stripe has a framework for authenticating webhooks -- but Message Signatures is a published standard.&quot; -- Cloudflare, &quot;Forget IPs&quot; [@cf-blog-webbotauth]
&lt;p&gt;Why useless, if the cryptography was sound? Because every scheme was an island, and two gaps blocked open-web identity. First, no shared signature format: SigV4, Stripe, and draft-cavage each canonicalize and encode differently, so a stranger origin has no common way even to &lt;em&gt;parse&lt;/em&gt; an unknown crawler&apos;s signature.&lt;/p&gt;
&lt;p&gt;Second -- and this is the decisive gap -- no interoperable key discovery. The symmetric schemes hide a shared secret, which drags Generation 4&apos;s pairwise problem right back in. mTLS demands client-certificate provisioning that is famously awkward on the open web. And draft-cavage, though genuinely asymmetric, discovered keys in an application-specific way: a Mastodon server fetches the sender&apos;s actor document and reads its &lt;code&gt;publicKeyPem&lt;/code&gt; field, which works only inside the Fediverse, not for an arbitrary origin meeting an arbitrary bot [@w3c-activitypub].&lt;/p&gt;
&lt;p&gt;The successor spec states the gap in standards language: RFC 9421 &quot;assumes verifiers have prior knowledge of signers&apos; key material, requiring out-of-band key distribution ... This creates deployment friction&quot; [@draft-httpsig-directory].AWS&apos;s documentation describes the SigV4 protocol but never states a launch year; it is widely dated to roughly 2013-2014, and this article keeps that hedge rather than asserting a precise date [@aws-sigv4].&lt;/p&gt;
&lt;p&gt;Read the chain along two axes and the story sharpens. &lt;strong&gt;Unforgeability&lt;/strong&gt; climbs the whole way down: from a string anyone can write, to a network position, to a leakable secret, to an unleakable private key. &lt;strong&gt;Interoperability&lt;/strong&gt; does the opposite -- it &lt;em&gt;regresses&lt;/em&gt; at Generation 5, which has the best cryptography of any generation before it and yet fails precisely because it has no common standard and no discovery.&lt;/p&gt;
&lt;p&gt;The entire arc is a migration in key-distribution cost, from $O(N \times M)$ pairwise secrets and brittle per-vendor state toward a single $O(1)$ keypair backed by an $O(k)$ public directory. There is even a literal human through-line: Manu Sporny co-authored both the 2013 &lt;code&gt;draft-cavage&lt;/code&gt; [@draft-cavage] and the 2024 RFC 9421 [@rfc9421] -- the same person carrying the same idea from pre-standard sketch to real standard.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The breakthrough was never &quot;add cryptography to bot requests&quot; -- machines had signed HTTP requests since 2013. The novelty is two acts of standardization: a signature construction any verifier can parse (RFC 9421), and a public directory any origin can look up (Web Bot Auth). Interoperability and discovery, not signing, are the news.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Generation 5 had the cryptography right a full decade early. So why did signing a request fail to give bots an open-web identity? Because no two islands agreed on &lt;em&gt;how&lt;/em&gt; to build a signature -- and the fix starts with one deceptively simple decision about what a signature should even cover.&lt;/p&gt;
&lt;h2&gt;5. RFC 9421 and the Signature Base&lt;/h2&gt;
&lt;p&gt;The genius of RFC 9421 is a single, almost counterintuitive decision: do not sign the message. Sign a &lt;em&gt;description&lt;/em&gt; of the message. Once you see why, the whole design of Generation 6 falls into place.&lt;/p&gt;

An IETF Standards-Track mechanism, published February 2024, for creating and verifying signatures over *selected components* of an HTTP message -- specific fields and derived values the signer chooses -- rather than over the raw byte stream of the request.
&lt;p&gt;Here is the problem with signing raw bytes. An HTTP request rarely reaches its destination unchanged. Proxies reorder headers, add their own, re-encode bodies, and normalize whitespace. A signature over the literal bytes would shatter the moment any intermediary touched the request. So RFC 9421 inverts the approach: the signer names exactly which parts of the message it wants to commit to, and signs a canonical rendering of just those parts.&lt;/p&gt;

A signable pseudo-field computed from the message itself rather than sent as a header -- `@method`, `@authority`, `@path`, `@query`, `@target-uri`, `@status`. The leading `@` marks it as derived, distinguishing it from ordinary HTTP fields like `content-type`.
&lt;p&gt;The signer chooses its &lt;strong&gt;covered components&lt;/strong&gt; -- any mix of ordinary fields and derived components -- and appends one trailing line, &lt;code&gt;@signature-params&lt;/code&gt;, that carries the metadata: &lt;code&gt;created&lt;/code&gt;, &lt;code&gt;expires&lt;/code&gt;, &lt;code&gt;keyid&lt;/code&gt;, an optional &lt;code&gt;alg&lt;/code&gt;, an optional &lt;code&gt;nonce&lt;/code&gt;, and a &lt;code&gt;tag&lt;/code&gt;. All of it serializes into a canonical string.&lt;/p&gt;

The canonical, newline-delimited string assembled from the covered components plus the trailing `@signature-params` line. The signature base -- not the raw request -- is precisely what the private key signs and what the verifier independently reconstructs to check.
&lt;p&gt;A signature base for a simple request looks like this. Each covered component is one line; the last line records the recipe itself:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;@method&quot;: GET
&quot;@authority&quot;: example.com
&quot;@path&quot;: /articles/latest
&quot;@signature-params&quot;: (&quot;@method&quot; &quot;@authority&quot; &quot;@path&quot;);created=1618884473;expires=1618884773;keyid=&quot;poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U&quot;;tag=&quot;web-bot-auth&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That block is the entire mechanism in miniature. The private key signs exactly those four lines. The result travels in two Structured-Field headers: &lt;code&gt;Signature-Input&lt;/code&gt;, the &lt;code&gt;@signature-params&lt;/code&gt; recipe so a verifier knows what to rebuild, and &lt;code&gt;Signature&lt;/code&gt;, which carries the signature bytes [@rfc9421].&lt;/p&gt;
&lt;p&gt;When the request has a body worth protecting, the signer adds a &lt;code&gt;content-digest&lt;/code&gt; field -- a hash of the body defined by RFC 9530 -- to the covered set, so the body is committed to as well [@rfc9530]. The serialization rules all come from Structured Field Values, RFC 8941, which give headers a strict, unambiguous grammar of items, lists, and dictionaries [@rfc8941].Without a strict grammar, two implementations could serialize the same header differently and never agree on the signature base -- which is precisely how the Generation 5 islands failed to interoperate.&lt;/p&gt;

flowchart TD
    R[&quot;HTTP request&quot;] --&amp;gt; C[&quot;Pick covered components&lt;br /&gt;@method, @authority, @path, chosen fields&quot;]
    C --&amp;gt; P[&quot;Append @signature-params&lt;br /&gt;created, expires, keyid, tag&quot;]
    P --&amp;gt; B[&quot;Canonical signature base&lt;br /&gt;one line per covered component&quot;]
    B --&amp;gt; S[&quot;Ed25519 sign the base&quot;]
    S --&amp;gt; H[&quot;Emit Signature-Input (recipe) and Signature (bytes)&quot;]
&lt;p&gt;Now the payoff, and it is the aha of the whole standard. Because the signature commits to &lt;em&gt;chosen components&lt;/em&gt; rather than raw bytes, it survives intermediaries, re-encodings, and proxies -- the signer decides exactly what the signature guarantees and what it deliberately leaves free to change.&lt;/p&gt;
&lt;p&gt;This is the precise property every Generation 5 island lacked. SigV4 and draft-cavage each had their own private notion of what to canonicalize; RFC 9421 makes that notion a published standard, so any conforming verifier -- in any language, at any origin -- can reconstruct the base and check the signature without prior agreement with the signer.&lt;/p&gt;

A fast, deterministic elliptic-curve signature scheme -- EdDSA using the edwards25519 curve, the twisted-Edwards curve birationally equivalent to Curve25519 -- with compact 32-byte keys. It is Web Bot Auth&apos;s deployed default; this blog&apos;s field guide on digital signatures covers the underlying curve mathematics.
&lt;p&gt;RFC 9421 is deliberately algorithm-agnostic. &lt;a href=&quot;https://paragmali.com/blog/the-math-held-the-interface-leaked-a-field-guide-to-digital-/&quot; rel=&quot;noopener&quot;&gt;Ed25519&lt;/a&gt; is the deployed default because it is fast, deterministic, and small enough to run per request at internet scale, but the standard commits to no single scheme.&lt;/p&gt;
&lt;p&gt;The IANA &quot;HTTP Signature Algorithms&quot; registry -- Specification Required, so it can grow -- also profiles RSA-PSS-SHA512, RSA-PKCS1-v1.5-SHA256, ECDSA over P-256 and P-384, and HMAC-SHA256 [@iana-httpsig-registry]. The &lt;code&gt;alg&lt;/code&gt; is a property of the key, not a mandate of the protocol, which matters later when quantum-resistant schemes need a home.RFC 9421 was edited by Annabelle Backman (Amazon), Justin Richer (Bespoke Engineering), and Manu Sporny (Digital Bazaar) -- the same Sporny who co-authored the 2013 &lt;code&gt;draft-cavage&lt;/code&gt;, which is why the standard is best read as a revival rather than an invention [@rfc9421].&lt;/p&gt;
&lt;p&gt;One more precision, because the record is muddled. RFC 9421 is a Standards-Track document published in February 2024.Several AI-generated summaries date RFC 9421 to June 2023; the RFC Editor and IETF Datatracker both settle it as February 2024 [@rfc9421]. When a source and a chatbot disagree on a standard&apos;s date, trust the RFC Editor.&lt;/p&gt;
&lt;p&gt;RFC 9421 tells you &lt;em&gt;how&lt;/em&gt; to sign a request so that anyone can verify it. But &quot;anyone can verify it&quot; hides a trapdoor. To check a signature, a verifier needs the signer&apos;s public key -- and RFC 9421 says nothing about where to find it. It &quot;assumes verifiers have prior knowledge of signers&apos; key material, requiring out-of-band key distribution&quot; [@draft-httpsig-directory]. For a paying customer&apos;s agent meeting an origin it has never contacted, that missing half is not a detail. It is the entire problem. Solving it is what turns a signature into an identity.&lt;/p&gt;
&lt;h2&gt;6. Web Bot Auth: From a General Tool to a Bot Identity&lt;/h2&gt;
&lt;p&gt;RFC 9421 is a general engine. Web Bot Auth is what happens when Cloudflare and Google point that engine at one specific job -- proving which bot sent a request -- and bolt on the piece the RFC deliberately left out. The result is authored by Thibault Meunier of Cloudflare and Sandor Major of Google, and it answers the trapdoor question from the last section head-on: how does a stranger find your key?&lt;/p&gt;
&lt;p&gt;Start with the cast. Web Bot Auth defines a &lt;strong&gt;three-actor model&lt;/strong&gt;. A &lt;em&gt;User&lt;/em&gt; delegates a task. An &lt;em&gt;Agent&lt;/em&gt; carries it out and signs each outbound request with its private key. An &lt;em&gt;Origin&lt;/em&gt; receives the request and verifies the signature. The clean separation matters: the party that signs is the agent operator, not the human, and that distinction returns to haunt the governance discussion later.Because the operator signs, one key can sit in front of thousands of different end users -- the seed of the intermediary-trust problem that resurfaces in the open problems.&lt;/p&gt;
&lt;p&gt;On top of RFC 9421&apos;s freedom, the profile pins down the parts that must be fixed for strangers to interoperate.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component or parameter&lt;/th&gt;
&lt;th&gt;RFC 9421 says&lt;/th&gt;
&lt;th&gt;Web Bot Auth requires&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Covered components&lt;/td&gt;
&lt;td&gt;Signer&apos;s free choice&lt;/td&gt;
&lt;td&gt;MUST cover at least &lt;code&gt;@authority&lt;/code&gt; (or &lt;code&gt;@target-uri&lt;/code&gt;) [@draft-webbotauth-arch]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;created&lt;/code&gt; / &lt;code&gt;expires&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Optional metadata&lt;/td&gt;
&lt;td&gt;Both MUST be present; expiry RECOMMENDED ≤ 24 hours [@draft-webbotauth-arch]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;keyid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Opaque signer-chosen string&lt;/td&gt;
&lt;td&gt;MUST be the base64url JWK SHA-256 Thumbprint [@rfc7638]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;tag&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Optional label&lt;/td&gt;
&lt;td&gt;MUST be &lt;code&gt;web-bot-auth&lt;/code&gt; [@draft-webbotauth-arch]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nonce&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Optional&lt;/td&gt;
&lt;td&gt;A 64-byte nonce is the recommended anti-replay tool [@draft-webbotauth-arch]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key discovery&lt;/td&gt;
&lt;td&gt;Out of scope&lt;/td&gt;
&lt;td&gt;JWKS at a well-known path, found via &lt;code&gt;Signature-Agent&lt;/code&gt; [@draft-httpsig-directory]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;The last row is the whole point -- the genuine novelty of the entire effort.&lt;/p&gt;

A JSON document listing public keys, each expressed as a JSON Web Key. The Web Bot Auth directory is a JWKS: a signer&apos;s public keys, published for anyone to fetch.
&lt;p&gt;A signer publishes its public keys as a JWKS at the well-known path &lt;code&gt;/.well-known/http-message-signatures-directory&lt;/code&gt;, and points to that directory in-band with a request header.&lt;/p&gt;

An in-band request header whose value is a Structured-Dictionary URI pointing at the signer&apos;s key directory. Because Web Bot Auth lists `Signature-Agent` among the covered components, an attacker cannot swap the pointer without invalidating the signature.
&lt;p&gt;That self-protecting loop is the elegant part. The header says where the keys live; the signature covers the header; so the pointer to the keys is itself signed. And the directory response must in turn be signed -- with &lt;code&gt;tag=&quot;http-message-signatures-directory&quot;&lt;/code&gt;, one signature per listed key -- so no one can mirror your directory at their own host and enroll as you [@cf-docs-webbotauth]. The &lt;code&gt;keyid&lt;/code&gt; closes the loop by naming the exact key without any external registry.&lt;/p&gt;

A stable, self-certifying key identifier: the base64url-encoded SHA-256 hash over a JSON Web Key&apos;s required members in canonical form. It is what `keyid` carries, so a key effectively names itself.
&lt;p&gt;You can watch a &lt;code&gt;keyid&lt;/code&gt; derive itself. The snippet below takes the real Ed25519 public key served by Cloudflare&apos;s live debug directory, canonicalizes its required members exactly as RFC 7638 prescribes, hashes, and base64url-encodes the result -- and out falls the same &lt;code&gt;kid&lt;/code&gt; the directory advertises [@cf-live-directory].&lt;/p&gt;
&lt;p&gt;{&lt;code&gt;const NL = String.fromCharCode(10); function rotr(n, x) { return (x &amp;gt;&amp;gt;&amp;gt; n) | (x &amp;lt;&amp;lt; (32 - n)); } function sha256(bytes) {   const K = [     0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,     0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,     0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,     0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,     0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,     0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,     0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,     0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2];   let h = [0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19];   const l = bytes.length, withOne = l + 1;   const pad = (56 - (withOne % 64) + 64) % 64, total = withOne + pad + 8;   const m = new Uint8Array(total); m.set(bytes); m[l] = 0x80;   const dv = new DataView(m.buffer);   dv.setUint32(total - 4, (l * 8) &amp;gt;&amp;gt;&amp;gt; 0);   dv.setUint32(total - 8, Math.floor(l * 8 / 0x100000000));   const w = new Uint32Array(64);   for (let i = 0; i &amp;lt; total; i += 64) {     for (let t = 0; t &amp;lt; 16; t++) w[t] = dv.getUint32(i + t * 4);     for (let t = 16; t &amp;lt; 64; t++) {       const s0 = rotr(7,w[t-15]) ^ rotr(18,w[t-15]) ^ (w[t-15] &amp;gt;&amp;gt;&amp;gt; 3);       const s1 = rotr(17,w[t-2]) ^ rotr(19,w[t-2]) ^ (w[t-2] &amp;gt;&amp;gt;&amp;gt; 10);       w[t] = (w[t-16] + s0 + w[t-7] + s1) &amp;gt;&amp;gt;&amp;gt; 0;     }     let a = h[0], b = h[1], c = h[2], d = h[3], e = h[4], f = h[5], g = h[6], hh = h[7];     for (let t = 0; t &amp;lt; 64; t++) {       const S1 = rotr(6,e) ^ rotr(11,e) ^ rotr(25,e);       const t1 = (hh + S1 + ((e &amp;amp; f) ^ (~e &amp;amp; g)) + K[t] + w[t]) &amp;gt;&amp;gt;&amp;gt; 0;       const S0 = rotr(2,a) ^ rotr(13,a) ^ rotr(22,a);       const t2 = (S0 + ((a &amp;amp; b) ^ (a &amp;amp; c) ^ (b &amp;amp; c))) &amp;gt;&amp;gt;&amp;gt; 0;       hh=g; g=f; f=e; e=(d+t1)&amp;gt;&amp;gt;&amp;gt;0; d=c; c=b; b=a; a=(t1+t2)&amp;gt;&amp;gt;&amp;gt;0;     }     h = [(h[0]+a)&amp;gt;&amp;gt;&amp;gt;0,(h[1]+b)&amp;gt;&amp;gt;&amp;gt;0,(h[2]+c)&amp;gt;&amp;gt;&amp;gt;0,(h[3]+d)&amp;gt;&amp;gt;&amp;gt;0,(h[4]+e)&amp;gt;&amp;gt;&amp;gt;0,(h[5]+f)&amp;gt;&amp;gt;&amp;gt;0,(h[6]+g)&amp;gt;&amp;gt;&amp;gt;0,(h[7]+hh)&amp;gt;&amp;gt;&amp;gt;0];   }   const out = new Uint8Array(32), odv = new DataView(out.buffer);   for (let i = 0; i &amp;lt; 8; i++) odv.setUint32(i*4, h[i]);   return out; } function base64url(bytes) {   let s = &apos;&apos;;   for (const b of bytes) s += String.fromCharCode(b);   return btoa(s).split(&apos;+&apos;).join(&apos;-&apos;).split(&apos;/&apos;).join(&apos;_&apos;).split(&apos;=&apos;).join(&apos;&apos;); } // The Ed25519 public key served by Cloudflare&apos;s live Web Bot Auth directory: const jwk = { crv: &apos;Ed25519&apos;, kty: &apos;OKP&apos;, x: &apos;JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs&apos; }; // RFC 7638: keep only the required members, sort them, no whitespace. const canonical = &apos;{&quot;crv&quot;:&quot;&apos; + jwk.crv + &apos;&quot;,&quot;kty&quot;:&quot;&apos; + jwk.kty + &apos;&quot;,&quot;x&quot;:&quot;&apos; + jwk.x + &apos;&quot;}&apos;; const thumbprint = base64url(sha256(new TextEncoder().encode(canonical))); console.log(&apos;Canonical JWK : &apos; + canonical); console.log(&apos;Thumbprint    : &apos; + thumbprint); console.log(&apos;Directory kid : poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U&apos;); console.log(&apos;Identical?    : &apos; + (thumbprint === &apos;poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U&apos;));&lt;/code&gt;}&lt;/p&gt;
&lt;p&gt;With the signature standard and the directory in place, the verifier&apos;s job is mechanical: fetch the directory, match the &lt;code&gt;keyid&lt;/code&gt; thumbprint, reconstruct the signature base, verify the Ed25519 signature, and enforce the validity window. Only then -- as a wholly separate step -- does it decide policy.&lt;/p&gt;

sequenceDiagram
    participant U as User
    participant A as Agent
    participant O as Origin
    participant D as Key directory
    U-&amp;gt;&amp;gt;A: Delegate a task, fetch this page
    A-&amp;gt;&amp;gt;A: Build signature base, Ed25519 sign
    A-&amp;gt;&amp;gt;O: Send request with Signature-Input, Signature, Signature-Agent
    O-&amp;gt;&amp;gt;D: Fetch or read cached directory at the well-known path
    D--&amp;gt;&amp;gt;O: Return signed JWKS of public keys
    O-&amp;gt;&amp;gt;O: Match keyid thumbprint, reconstruct base, verify
    O-&amp;gt;&amp;gt;O: Enforce created ≤ now ≤ expires
    O--&amp;gt;&amp;gt;A: Identity established, then apply policy separately
&lt;p&gt;Ed25519 is the deployed default here, but the mechanism stays algorithm-agnostic: the same registry that lets RFC 9421 carry RSA-PSS or ECDSA lets Web Bot Auth carry them too [@iana-httpsig-registry]. Deployment is real but early, and precision matters.The directory draft registers the plural path &lt;code&gt;/.well-known/http-message-signatures-directory&lt;/code&gt; [@draft-httpsig-directory], while some Cloudflare prose uses a singular alias; the live server answers on both, and this article uses the registered plural form [@cf-blog-verifiedbots]. The strongest independent signal that this is not vendor theater is OpenAI.&lt;/p&gt;

&quot;With HTTP Message Signatures (RFC 9421), OpenAI signs all Operator requests so site owners can verify they genuinely originate from Operator and haven&apos;t been tampered with.&quot; -- Eugenio, Engineer, OpenAI [@cf-blog-webbotauth]

Cloudflare moved this from a closed beta announced in May 2025 [@cf-blog-webbotauth] to integration in its Verified Bots Program in July 2025, where its edge automatically validates signatures and marks matching traffic as verified [@cf-blog-verifiedbots]. As of a July 2026 taxonomy update, Web Bot Auth is one of two accepted validation methods for becoming a Verified bot [@cf-docs-verifiedbots]. But the profile is still a set of Individual Internet-Drafts, not RFCs, and edge validation is ramping rather than generally available. This is production-adjacent, not finished.
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Cloudflare&apos;s production implementation pins to specific earlier draft revisions -- roughly &lt;code&gt;directory-03&lt;/code&gt; and &lt;code&gt;architecture-02&lt;/code&gt; -- while the latest published drafts are at &lt;code&gt;-05&lt;/code&gt;, and the architecture draft was renamed mid-flight to &lt;code&gt;draft-meunier-webbotauth-httpsig-protocol&lt;/code&gt; [@cf-docs-webbotauth] [@draft-webbotauth-protocol]. Interop can break silently across revisions, so verify which draft your counterparty implements before assuming compatibility.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;With a signature &lt;em&gt;and&lt;/em&gt; a directory, an origin can verify a bot it has never met, on first contact, with zero prior arrangement. That is the headline. But Cloudflare shipped a &lt;em&gt;second&lt;/em&gt; proposal in the very same blog, and there is a whole family of rival ways to prove a machine&apos;s identity. Why did signatures win?&lt;/p&gt;
&lt;h2&gt;7. Signatures, Certificates, and the Other Signers&lt;/h2&gt;
&lt;p&gt;Cloudflare proposed two mechanisms in the same post, and the one it did &lt;em&gt;not&lt;/em&gt; prioritize explains why the one it did is pulling ahead. The road not taken is request mTLS.&lt;/p&gt;

A handshake in which both peers present X.509 certificates, so each authenticates the other at the transport layer, beneath HTTP. The resulting identity is bound to the TLS *connection*, not to any individual request on it.
&lt;p&gt;Classic mTLS is decades old and rock-solid inside a managed setting where both sides already run a private certificate authority. The problem is the open web. If an origin asks for a client certificate and the client has none, the browser shows &quot;an inscrutable&quot; and unskippable error [@cf-blog-webbotauth]. To soften that, &lt;code&gt;draft-jhoyla-req-mtls-flag&lt;/code&gt; adds a TLS flag -- an experimental extension -- by which a client &lt;em&gt;signals&lt;/em&gt; it holds a suitable certificate, so the server can request one only when it will not break an ordinary visitor [@draft-jhoyla-mtls].&lt;/p&gt;
&lt;p&gt;It is a real, live parallel track. But Cloudflare is candid about ranking it second: it prioritizes Message Signatures because that approach &quot;relies on the previously adopted RFC 9421 with several reference implementations, and works at the HTTP layer&quot; [@cf-blog-webbotauth]. The reference-implementation point is concrete -- RFC 9421 already has an implementations hub and independent libraries to build on [@httpsig-org].&lt;/p&gt;

flowchart TD
    subgraph HL[&quot;HTTP layer -- Web Bot Auth&quot;]
      A1[&quot;Signature travels inside headers&quot;] --&amp;gt; A2[&quot;Survives CDNs and reverse proxies&quot;]
    end
    subgraph TL[&quot;Transport layer -- request mTLS&quot;]
      B1[&quot;Identity bound to the TLS connection&quot;] --&amp;gt; B2[&quot;Lost when a proxy terminates TLS upstream&quot;]
    end
&lt;p&gt;The layer difference is not cosmetic. A signature lives in HTTP headers, so it rides through content delivery networks and reverse proxies untouched and can be verified at the edge or the origin. A client certificate binds to the connection, so a reverse proxy that terminates TLS -- how much of the web is commonly served -- strips the identity before the request reaches the application. For an internet built on intermediaries, HTTP-layer identity is simply the one that arrives.&lt;/p&gt;
&lt;p&gt;Web Bot Auth also clarifies its relationship to the Generation 5 signers. It keeps draft-cavage&apos;s asymmetric idea but replaces the Fediverse-specific &lt;code&gt;publicKeyPem&lt;/code&gt; lookup with a standard format and an open-web directory [@draft-cavage] [@w3c-activitypub]; and unlike SigV4 or Stripe, it needs no shared secret, only a public key the origin can fetch. Every path so far -- reverse DNS, mTLS, SigV4, Web Bot Auth -- differs less in its cryptography than in the row of the table that has no cryptography at all: how a verifier discovers the key.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;A. Web Bot Auth&lt;/th&gt;
&lt;th&gt;B. Request mTLS&lt;/th&gt;
&lt;th&gt;C. Reverse DNS + IP&lt;/th&gt;
&lt;th&gt;D. Privacy Pass&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;What it proves&lt;/td&gt;
&lt;td&gt;Who signed, plus integrity of covered parts&lt;/td&gt;
&lt;td&gt;Who holds the client certificate&lt;/td&gt;
&lt;td&gt;Which network sent the packet&lt;/td&gt;
&lt;td&gt;A property, unlinkably&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Layer&lt;/td&gt;
&lt;td&gt;HTTP (application)&lt;/td&gt;
&lt;td&gt;TLS (transport)&lt;/td&gt;
&lt;td&gt;Network / DNS&lt;/td&gt;
&lt;td&gt;HTTP (application)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cryptography&lt;/td&gt;
&lt;td&gt;Asymmetric, Ed25519 default&lt;/td&gt;
&lt;td&gt;Asymmetric, X.509&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Blind signatures / VOPRF&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key discovery&lt;/td&gt;
&lt;td&gt;Public JWKS + &lt;code&gt;Signature-Agent&lt;/code&gt;, in-band&lt;/td&gt;
&lt;td&gt;PKI / CA, out-of-band&lt;/td&gt;
&lt;td&gt;Published range files, per-vendor&lt;/td&gt;
&lt;td&gt;Issuer directory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replay protection&lt;/td&gt;
&lt;td&gt;Validity window + optional nonce&lt;/td&gt;
&lt;td&gt;TLS channel binding&lt;/td&gt;
&lt;td&gt;None at the IP layer&lt;/td&gt;
&lt;td&gt;Token single-use rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Survives reverse proxies&lt;/td&gt;
&lt;td&gt;Yes, an HTTP header&lt;/td&gt;
&lt;td&gt;No, if TLS is terminated upstream&lt;/td&gt;
&lt;td&gt;Only with trusted forwarding&lt;/td&gt;
&lt;td&gt;Yes, an HTTP header&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Privacy&lt;/td&gt;
&lt;td&gt;Identifying by design&lt;/td&gt;
&lt;td&gt;Identifying&lt;/td&gt;
&lt;td&gt;Coarsely identifying&lt;/td&gt;
&lt;td&gt;Unlinkable by design&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best suited for&lt;/td&gt;
&lt;td&gt;Open-web stranger identity&lt;/td&gt;
&lt;td&gt;Managed B2B and API&lt;/td&gt;
&lt;td&gt;Legacy search crawlers&lt;/td&gt;
&lt;td&gt;Privacy-preserving attestation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Method D in that table is not really a rival at all -- it answers the opposite question.&lt;/p&gt;

Privacy Pass and Apple&apos;s Private Access Tokens, standardized in June 2024 as RFCs 9576, 9577, and 9578, prove a *property* about a request -- &quot;a human or an attested device stands behind this&quot; -- without revealing who, and without letting the origin link one request to another [@rfc9576] [@rfc9577] [@rfc9578]. That is the mirror image of a signature. Web Bot Auth maximizes accountable identity; Privacy Pass maximizes unlinkable anonymity. They sit at opposite ends of the same axis and coexist because they serve different jobs: one names the bot, the other refuses to.
&lt;p&gt;Line the methods up by how a verifier learns the key and the pattern is stark. mTLS and SigV4 distribute keys out of band. IP schemes distribute a public list of ranges. Only Web Bot Auth carries an in-band pointer to a standardized, discoverable directory -- the one design choice, containing no new cryptography, that makes stranger-to-stranger verification possible on the open web.&lt;/p&gt;
&lt;p&gt;And yet notice what unites every row, signatures included. Each mechanism proves exactly one kind of thing: &lt;em&gt;who&lt;/em&gt;, or &lt;em&gt;what network&lt;/em&gt;, or &lt;em&gt;what property&lt;/em&gt;. Not one of them, however strong its cryptography, can tell you whether the bot should be admitted. That gap is not an engineering oversight to be patched in the next draft. It is a limit on what a signature &lt;em&gt;is&lt;/em&gt; -- and it is time to look at it directly.&lt;/p&gt;
&lt;h2&gt;8. What a Signature Cannot Prove&lt;/h2&gt;
&lt;p&gt;Here is the sentence the whole article has been walking toward. A valid signature proves who signed and what they covered -- and nothing else. Not that the bot is well-behaved. Not that it respects your rate limits. Not that it is allowed anywhere near your origin.&lt;/p&gt;
&lt;p&gt;This is not a shortcoming to be engineered away in a later draft; it is the definition of what a signature is. A digital signature is a proof of key possession over a covered message. It can establish &lt;em&gt;authenticity&lt;/em&gt; -- which key signed -- and &lt;em&gt;integrity&lt;/em&gt; -- that the covered components were not altered. It is information-theoretically silent on everything else.&lt;/p&gt;
&lt;p&gt;&quot;Allowed&quot; is a predicate over policy and behavior, not over bytes, so no amount of cryptographic strengthening can make a signature answer it. The web already learned this once: RFC 9309 says the rules in a &lt;code&gt;robots.txt&lt;/code&gt; file &quot;are not a form of access authorization&quot; [@rfc9309]. Web Bot Auth simply makes the &lt;em&gt;identity&lt;/em&gt; half verifiable while leaving that same authorization line exactly where it was.&lt;/p&gt;

flowchart TD
    R[&quot;Signed request arrives&quot;] --&amp;gt; V[&quot;Verify signature and validity window&quot;]
    V --&amp;gt; I[&quot;Identity established: who signed, what was covered&quot;]
    I --&amp;gt; G{&quot;Policy gate, not cryptography&quot;}
    G --&amp;gt;|trusted and well-behaved| Allow[&quot;Admit&quot;]
    G --&amp;gt;|unknown or abusive| Deny[&quot;Deny or rate-limit&quot;]
&lt;p&gt;Three concrete limits make the boundary vivid, and each is fundamental rather than incidental.&lt;/p&gt;
&lt;p&gt;First, &lt;strong&gt;replay&lt;/strong&gt;. A signature with a validity window can be captured and replayed by anyone until it expires. The only defenses are a short window and a server-tracked &lt;code&gt;nonce&lt;/code&gt; -- and the moment the server tracks a nonce, it is keeping state. This is a genuine lower bound, not a missing feature: stateless anti-replay is impossible. You trade window size against verifier memory, and Web Bot Auth&apos;s mandatory &lt;code&gt;created&lt;/code&gt;/&lt;code&gt;expires&lt;/code&gt; plus optional 64-byte nonce sit exactly on that trade-off. A verifier that admits a request only when $created \le now \le expires$ has narrowed the replay opportunity, never closed it [@cf-blog-webbotauth].&lt;/p&gt;
&lt;p&gt;Second, &lt;strong&gt;uncovered components&lt;/strong&gt;. A signature commits only to what it lists. Anything the signer left out of the covered set can be altered in flight without breaking verification. That is why the profile insists on covering &lt;code&gt;@authority&lt;/code&gt; and &lt;code&gt;Signature-Agent&lt;/code&gt; -- so the target host and the pointer to the key directory cannot be quietly swapped. Coverage is a choice, and everything outside it is unprotected by construction.&lt;/p&gt;
&lt;p&gt;Third, the &lt;strong&gt;trust-anchor problem&lt;/strong&gt;, which is the deepest of the three. A verified signature does not create trust; it &lt;em&gt;relocates&lt;/em&gt; it. &quot;Do I trust this request?&quot; becomes &quot;do I trust the directory behind this key?&quot; Cryptography cannot manufacture the first bit of trust in &lt;em&gt;which signers matter&lt;/em&gt; -- that decision has to come from somewhere outside the math. It is the same unanswered question that sits under the entire web public-key infrastructure: who gets to be a certificate authority? Web Bot Auth relocates that question to &quot;whose directory do I honor?&quot; and, by design, leaves it open [@draft-httpsig-directory].&lt;/p&gt;
&lt;p&gt;One more frontier the standards make concrete by existing side by side: Web Bot Auth points toward accountable, linkable identity, Privacy Pass toward unlinkable anonymity [@rfc9576]. You can pick a point on that axis, but not both endpoints in one token -- a design decision, not a bug awaiting a fix.&lt;/p&gt;

A valid signature proves who signed and what was covered -- never whether the bot is allowed. Signing makes a bad bot accountable, not good.
&lt;p&gt;It is worth naming what these limits are &lt;em&gt;not&lt;/em&gt;. This domain has no impossibility theorems in the Turing sense, no proven complexity lower bounds, no hardness results. The honest &quot;bounds&quot; here are conceptual: they describe what a signature can attest, not how expensive it is to compute. Pretending otherwise -- dressing a category boundary up as a big-O result -- would be a fabrication. The real limit is cleaner and more absolute than any asymptotic one.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Identity and authorization are not two ends of one dial. They are different categories. A signature is silent on &quot;allowed&quot; the way a photograph is silent on &quot;true&quot; -- not because it is a weak signature, but because &quot;allowed&quot; is not the kind of thing a signature can encode. This is a boundary, not a backlog item.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So where does &quot;allowed&quot; get decided? Not here.&lt;/p&gt;

Deciding whether an identified agent may perform an action is the job of an authorization system -- the concern of standards like [AuthZEN](/blog/who-authorized-this-tool-call-openid-authzen-the-mcp-profile/), covered in this blog&apos;s agent-authorization post. The clean handoff is the point: cryptography answers &quot;who,&quot; and policy answers &quot;allowed,&quot; and keeping them separate is what lets each be reasoned about honestly. A further layer sits beyond &quot;allowed&quot; -- proving *what* a user authorized an agent to do, such as a payment -- the domain of this blog&apos;s companion post on agentic payment intent ([FIDO AP2 / Verifiable Intent](/blog/the-mandate-that-replaced-the-checkout-button-how-fido-ap2-a/)). A signed request is the beginning of an access decision, never the whole of it.
&lt;p&gt;If a signature only proves &lt;em&gt;who&lt;/em&gt;, then &quot;who do we trust, and who decides?&quot; becomes the entire game. And nobody has fully answered it. That unfinished question opens onto the frontier of governance.&lt;/p&gt;
&lt;h2&gt;9. Governance, Revocation, and the Legitimacy Gap&lt;/h2&gt;
&lt;p&gt;Verifiable identity solves the easy half of the problem. The hard half -- who &lt;em&gt;deserves&lt;/em&gt; trust, and how you take it back -- is wide open, and almost none of it is a cryptography question. Here are the eight problems that will decide whether Web Bot Auth becomes the default or a footnote.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Signer accreditation.&lt;/strong&gt; A signature proves who signed, never whether that signer deserves trust. Someone still has to decide which directories an origin honors. Cloudflare&apos;s Verified Bots Program, backed by its BotBase and Radar data, is a de-facto answer -- but a single-vendor one [@cf-docs-verifiedbots]. The open problem is a neutral, multi-party accreditation model, without which every origin rebuilds the per-vendor allowlist that this whole effort set out to retire.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Revocation and rotation at web scale.&lt;/strong&gt; A leaked key needs to be pulled fast and globally. Short &lt;code&gt;expires&lt;/code&gt; windows bound the exposure, and a signer can drop a key from its JWKS, but there is no low-latency revocation or transparency mechanism -- nothing like the certificate world&apos;s revocation lists or transparency logs -- for these directories yet [@draft-httpsig-directory].&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Directory availability and denial of service.&lt;/strong&gt; Verification depends on fetching a third party&apos;s well-known directory. When that directory is slow, down, or flooded, the request hot path inherits a new availability and amplification surface. Signing the directory response makes cached copies tamper-evident, but cache-lifetime and denial-of-service guidance is still thin [@cf-docs-webbotauth].&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The legitimacy gap.&lt;/strong&gt; A signed bot can still scrape aggressively, ignore &lt;code&gt;robots.txt&lt;/code&gt;, or blow past rate limits. Signing makes it accountable, not good. Cloudflare&apos;s response is to bundle identity with a &lt;em&gt;behavioral&lt;/em&gt; bar -- a Verified bot must both self-identify honestly and be non-abusive, obeying &lt;code&gt;robots.txt&lt;/code&gt; at reasonable rates [@cf-docs-verifiedbots]. That is policy bolted on top of identity, exactly as the identity-versus-authorization spine predicts.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Transitive trust for &lt;a href=&quot;https://paragmali.com/blog/agentic-identity-on-windows-when-the-process-acting-on-your-/&quot; rel=&quot;noopener&quot;&gt;intermediary agents&lt;/a&gt;.&lt;/strong&gt; An &quot;agent mode&quot; browser is driven by many different end users, so trusting the operator&apos;s key is not the same as trusting every person behind it. The party that signs is not the party ultimately responsible for a given action. Cloudflare&apos;s July 2026 taxonomy adds a Direct-versus-Intermediary label and is experimenting with forwarding end-user information in the standard &lt;code&gt;Forwarded&lt;/code&gt; header, so an origin can apply policy to the responsible party [@cf-docs-verifiedbots].&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Adoption bootstrapping.&lt;/strong&gt; Signers gain little until origins verify, and origins gain little until signers sign -- the classic two-sided chicken-and-egg. The escape is asymmetric scale: a large reverse proxy verifying by default seeds millions of origins at once [@cf-blog-verifiedbots], while a marquee signer like OpenAI&apos;s Operator seeds the client side [@cf-blog-webbotauth].&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Standardization status and version skew.&lt;/strong&gt; The profile is a set of Individual Internet-Drafts, not RFCs, and it is a moving target: the architecture draft was renamed mid-flight to &lt;code&gt;draft-meunier-webbotauth-httpsig-protocol&lt;/code&gt; [@draft-webbotauth-protocol], while production pins to earlier revisions than the latest. Interop can break silently between draft versions until a working group freezes the text.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Post-quantum migration.&lt;/strong&gt; Ed25519, RSA, and ECDSA are all breakable by a sufficiently large quantum computer. A long-lived signing infrastructure should become crypto-agile before it must. RFC 9421&apos;s algorithm registry is extensible by design, and post-quantum signature schemes are being explored for it, but none is the deployed default today [@iana-httpsig-registry].&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Notice that not one item on this list is fixed by a better signature. Accreditation, revocation, availability, legitimacy, delegation, adoption, standardization, and quantum-readiness are questions of governance and process. Cryptography set the identity floor; everything above it is human coordination -- and coordination moves slower than code.The datatracker landing pages for these drafts are version-agnostic and stable even as the documents are renamed, which is why this article cites them rather than any single versioned snapshot [@draft-webbotauth-protocol].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;None of these are reasons to wait. The identity layer they sit on is already real, already standardized at its base, and already signing live traffic from a marquee agent. Which leaves one intensely practical question, the one an engineer actually needs answered: how do you &lt;em&gt;do&lt;/em&gt; this, on the wire?&lt;/p&gt;
&lt;h2&gt;10. Doing It on the Wire&lt;/h2&gt;
&lt;p&gt;Enough theory. Here is the whole protocol as an engineer meets it, in two roles.&lt;/p&gt;
&lt;p&gt;If you are building a bot, there are four steps. Generate an Ed25519 keypair. Publish your public keys as a JWKS at &lt;code&gt;/.well-known/http-message-signatures-directory&lt;/code&gt;, and sign that directory response itself with &lt;code&gt;tag=&quot;http-message-signatures-directory&quot;&lt;/code&gt;, one signature per key [@cf-docs-webbotauth]. On each outbound request, build a signature base that covers at least &lt;code&gt;@authority&lt;/code&gt; and &lt;code&gt;signature-agent&lt;/code&gt;, set a short &lt;code&gt;created&lt;/code&gt;/&lt;code&gt;expires&lt;/code&gt; window, set &lt;code&gt;keyid&lt;/code&gt; to your key&apos;s JWK Thumbprint [@rfc7638], and set &lt;code&gt;tag=&quot;web-bot-auth&quot;&lt;/code&gt;. Then emit three headers -- &lt;code&gt;Signature-Agent&lt;/code&gt;, &lt;code&gt;Signature-Input&lt;/code&gt;, and &lt;code&gt;Signature&lt;/code&gt;. On the wire, using the current directory draft&apos;s Structured-Dictionary URI for &lt;code&gt;Signature-Agent&lt;/code&gt; [@draft-httpsig-directory], that is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Signature-Agent: sig=&quot;https://signer.example.com&quot;
Signature-Input: sig=(&quot;@authority&quot; &quot;signature-agent&quot;;key=&quot;sig&quot;);created=1700000000;expires=1700011111;keyid=&quot;ba3e64==&quot;;tag=&quot;web-bot-auth&quot;
Signature: sig=abc==
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Three lines carry the entire identity claim [@cf-blog-webbotauth]. &lt;code&gt;Signature-Agent&lt;/code&gt; says where the keys live, &lt;code&gt;Signature-Input&lt;/code&gt; is the recipe, and &lt;code&gt;Signature&lt;/code&gt; is the proof.&lt;/p&gt;
&lt;p&gt;If you are the origin, the flow reverses. Parse &lt;code&gt;Signature-Agent&lt;/code&gt; to learn where the directory is. Fetch it -- or reuse a cached copy -- and match the request&apos;s &lt;code&gt;keyid&lt;/code&gt; to a published key. Reconstruct the signature base byte-for-byte from the same covered components, verify the Ed25519 signature against the matched key, and enforce that the request falls inside its &lt;code&gt;created&lt;/code&gt;/&lt;code&gt;expires&lt;/code&gt; window. Only after all of that succeeds do you reach the separate, non-cryptographic step: apply policy.&lt;/p&gt;
&lt;p&gt;The one part worth running yourself is the signature base, because byte-identical reconstruction is where implementations most often fail. The snippet below assembles exactly the string a signer signs, then runs the validity-window check the verifier performs.&lt;/p&gt;
&lt;p&gt;{&lt;code&gt;const NL = String.fromCharCode(10); // Build the RFC 9421 signature base: the exact string a signer signs // and a verifier must reconstruct byte-for-byte. function componentId(c) {   return &apos;&quot;&apos; + c.name + &apos;&quot;&apos; + (c.key ? &apos;;key=&quot;&apos; + c.key + &apos;&quot;&apos; : &apos;&apos;); } function buildSignatureBase(covered, values, params) {   const lines = [];   for (const c of covered) {     lines.push(componentId(c) + &apos;: &apos; + values[c.name]);   }   const innerList = &apos;(&apos; + covered.map(componentId).join(&apos; &apos;) + &apos;)&apos;;   const sigParams = innerList     + &apos;;created=&apos; + params.created     + &apos;;expires=&apos; + params.expires     + &apos;;keyid=&quot;&apos; + params.keyid + &apos;&quot;&apos;     + &apos;;tag=&quot;&apos; + params.tag + &apos;&quot;&apos;;   lines.push(&apos;&quot;@signature-params&quot;: &apos; + sigParams);   return lines.join(NL); } // Web Bot Auth MUST cover @authority; draft-05 covers the signature-agent ;key=&quot;sig&quot; member. const covered = [   { name: &apos;@authority&apos; },   { name: &apos;signature-agent&apos;, key: &apos;sig&apos; } ]; const values = { &apos;@authority&apos;: &apos;example.com&apos;, &apos;signature-agent&apos;: &apos;&quot;https://signer.example.com&quot;&apos; }; const params = {   created: 1700000000,   expires: 1700000300,   keyid: &apos;poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U&apos;,   tag: &apos;web-bot-auth&apos; }; const base = buildSignatureBase(covered, values, params); console.log(&apos;--- signature base (input to Ed25519) ---&apos;); console.log(base); // The origin enforces the validity window BEFORE trusting the signature. const now = 1700000300; const withinWindow = params.created &amp;lt;= now &amp;amp;&amp;amp; now &amp;lt;= params.expires; console.log(&apos;&apos;); console.log(&apos;created &amp;lt;= now &amp;lt;= expires : &apos; + (withinWindow ? &apos;VALID&apos; : &apos;REJECT&apos;)); console.log(&apos;the real signing is then one call: ed25519.sign(privateKey, base)&apos;);&lt;/code&gt;}&lt;/p&gt;
&lt;p&gt;That is genuinely the whole computation. The actual signing is a single library call on the string the snippet builds, and verification is the same string plus one &lt;code&gt;verify&lt;/code&gt; call.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Sign the directory response itself, or an attacker can mirror your directory and enroll as you [@cf-docs-webbotauth]. Reconstruct the signature base byte-for-byte -- the Structured Field grammar of RFC 8941 is strict, and a stray space or a reordered field breaks verification [@rfc8941]. Always cover &lt;code&gt;@authority&lt;/code&gt; and &lt;code&gt;Signature-Agent&lt;/code&gt; so the host and the key-directory pointer cannot be swapped. Watch clock skew against short windows -- a five-minute expiry is safer against replay but less forgiving of unsynchronized clocks. Pin the draft revision your counterparty implements. And deploy Ed25519: the standard is algorithm-agile, but Ed25519 is what interoperates today [@draft-webbotauth-arch].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;You do not have to build this from scratch. The reference implementations are first-party: an npm &lt;code&gt;web-bot-auth&lt;/code&gt; package [@npm-webbotauth], a Rust crate, a Cloudflare Workers verifier, and a Caddy plugin, all in the public repositories [@gh-cloudflareresearch] [@gh-cloudflare-webbotauth], plus the broader RFC 9421 implementations hub [@httpsig-org]. Best of all, there is a live directory you can hit right now.Cloudflare runs a live debug directory at &lt;code&gt;http-message-signatures-example.research.cloudflare.com&lt;/code&gt;; its well-known path serves a real Ed25519 JWK whose &lt;code&gt;kid&lt;/code&gt; is &lt;code&gt;poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U&lt;/code&gt; -- the very key the earlier thumbprint snippet reproduces [@cf-live-directory].&lt;/p&gt;
&lt;p&gt;Fetch that directory, feed its key into the verifier, and you have reproduced the entire mechanism end to end. The discovery layer that RFC 9421 left out is the part you can now watch working, live [@draft-httpsig-directory].&lt;/p&gt;

Run `curl` against the debug host&apos;s well-known path -- `http-message-signatures-example.research.cloudflare.com/.well-known/http-message-signatures-directory` -- and the response is a signed JWKS containing a single Ed25519 key. Its `kid` is the exact thumbprint the earlier snippet computes [@cf-live-directory].
&lt;p&gt;Stand back and the mechanics are almost anticlimactic: a keypair, a JSON file at a well-known URL, and two request headers. The hard part was never the code. It was getting the entire web to agree on the &lt;em&gt;same&lt;/em&gt; keypair format, the &lt;em&gt;same&lt;/em&gt; JSON document, the &lt;em&gt;same&lt;/em&gt; two headers -- so that an origin and a bot that have never met can still understand each other. That agreement is the thirty-year story you just read.&lt;/p&gt;
&lt;h2&gt;11. Frequently Asked Questions&lt;/h2&gt;


No. Mutual TLS authenticates at the transport layer with a per-connection X.509 certificate, and it struggles on the open web: a client without a certificate gets an inscrutable browser error, provisioning and rotation are heavy, and the identity does not survive a reverse proxy that terminates TLS. Web Bot Auth signs at the HTTP layer, per request, so it rides through content delivery networks and can be verified at the edge [@cf-blog-webbotauth].


No. A signature proves who signed and what was covered. &quot;Allowed&quot; is a separate policy decision -- identity is not authorization. The web drew this exact line for `robots.txt`, whose rules &quot;are not a form of access authorization&quot; [@rfc9309]. A signature makes the sender *nameable*, not *trustworthy* -- it hands you someone to hold responsible, not a reason to say yes.


No. RFC 9421 is algorithm-agnostic, and its IANA registry also profiles RSA-PSS, ECDSA over P-256 and P-384, and HMAC [@iana-httpsig-registry]. Ed25519 is the deployed default for interoperability today, not a mandate.


No, and this is the point most coverage gets wrong. AWS Signature v4, `draft-cavage` HTTP Signatures across the Fediverse, Stripe webhook signatures, and mTLS all predate it [@cf-blog-webbotauth] [@draft-cavage]. The novelty is a standardized, discoverable, open-web identity -- not signing itself.


No. `robots.txt` says *what* an origin permits; Web Bot Auth establishes *who* is asking. Identity makes etiquette enforceable against a named party for the first time, so the two are complementary rather than competing [@rfc9309].


Partly. RFC 9421 is a published standard, but the Web Bot Auth profile is a set of Individual Internet-Drafts, not RFCs, and Cloudflare&apos;s edge validation is ramping from beta into its Verified Bots Program rather than being generally available [@cf-blog-verifiedbots]. OpenAI&apos;s Operator, however, signs in production today [@cf-blog-webbotauth].


A short `created`/`expires` window plus an optional server-tracked 64-byte nonce [@draft-webbotauth-arch]. But stateless anti-replay is impossible: you trade a smaller window against the verifier having to remember nonces. The window shrinks the opportunity, it never eliminates it.

&lt;h3&gt;After Thirty Years, an Identity&lt;/h3&gt;
&lt;p&gt;Return to the two requests from the opening -- the agent and the scraper, identical down to the byte. The web spent its first three decades unable to separate them, not for lack of trying but because every tool it reached for answered the wrong question. &lt;code&gt;robots.txt&lt;/code&gt; asked bots to behave. &lt;code&gt;User-Agent&lt;/code&gt; let them name themselves. Reverse DNS pinned them to a network. Bearer tokens and per-vendor signatures added real cryptography but locked it inside islands no stranger could reach. Each was a better answer to &quot;which bot is this?&quot; and each broke in a way that named its successor.&lt;/p&gt;
&lt;p&gt;RFC 9421 and Web Bot Auth close the loop with two acts of standardization the previous generations never managed: a signature construction any verifier can parse, and a public directory any origin can look up. That is the honest shape of the claim. Not the first cryptography for machines -- the first cryptographic identity for machines that is standardized, discoverable, and open to the whole web. A bot generates one keypair, publishes one signed JSON document at one well-known URL, and any origin on earth can verify it on first contact, with no prior arrangement.&lt;/p&gt;

After decades of hardening how it authenticates humans, the web has finally given agents a verifiable cryptographic identity -- finally standardized and discoverable, not first-ever. And it proves exactly one thing: who signed, never whether they are allowed.
&lt;p&gt;That last clause is the discipline the whole article kept. A signature makes a bot accountable, not good. It relocates trust, it does not manufacture it. Everything above the identity floor -- who deserves trust, how you revoke it, what &quot;allowed&quot; even means for an agent acting for a thousand different users -- is governance, and governance is unfinished. The door now knows who is knocking. Deciding whether to open it is a different, still-unwritten story, and it begins exactly where this one ends.&lt;/p&gt;
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;signed-agents-web-bot-auth&quot; keyTerms={[
  { term: &quot;Web Bot Auth&quot;, definition: &quot;Cloudflare and Google IETF drafts that profile RFC 9421 for bot identity and add a discoverable public key directory.&quot; },
  { term: &quot;Signature Base&quot;, definition: &quot;The canonical string built from covered components plus the @signature-params line; the exact input a private key signs.&quot; },
  { term: &quot;Derived Component&quot;, definition: &quot;A signable pseudo-field computed from the message, such as @method, @authority, or @path.&quot; },
  { term: &quot;JWK Thumbprint (RFC 7638)&quot;, definition: &quot;A base64url SHA-256 hash over a JWK&apos;s canonical members; what Web Bot Auth&apos;s keyid carries.&quot; },
  { term: &quot;Signature-Agent header&quot;, definition: &quot;An in-band, covered request header pointing to the signer&apos;s key directory.&quot; },
  { term: &quot;Forward-confirmed reverse DNS&quot;, definition: &quot;Reverse-then-forward DNS resolution that proves network control, not identity.&quot; },
  { term: &quot;Identity versus authorization&quot;, definition: &quot;A signature proves who signed and what was covered, never whether the sender is allowed.&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>web-bot-auth</category><category>http-message-signatures</category><category>rfc-9421</category><category>bot-identity</category><category>ai-agents</category><category>cryptographic-identity</category><category>ed25519</category><category>web-security</category><author>noreply@paragmali.com (Parag Mali)</author></item><item><title>A Perfect Signature for a Certificate That Should Never Have Existed: A Field Guide to X.509 and PKI</title><link>https://paragmali.com/blog/a-perfect-signature-for-a-certificate-that-should-never-have/</link><guid isPermaLink="true">https://paragmali.com/blog/a-perfect-signature-for-a-certificate-that-should-never-have/</guid><description>How the Web PKI turns a certificate into a trust decision -- path validation, revocation, and Certificate Transparency -- and every famous way that trust broke.</description><pubDate>Wed, 08 Jul 2026 12:12:39 GMT</pubDate><content:encoded>
A certificate is a signed statement, and &quot;verifying&quot; it is a one-line signature check -- which is exactly why almost every famous PKI disaster left the cryptography flawless. DigiNotar&apos;s rogue `*.google.com` certificate, Flame&apos;s forged Microsoft signature, and Debian&apos;s guessable-but-valid keys were failures of *delegation*, not math: whom a trusted authority vouched for, what it was allowed to say, and whether anyone could take that trust back. This field guide hands you one lens -- *whom am I trusting, to say what, and how would I know if they lied?* -- and uses it to walk chain building versus path validation, the authorization-extension bug family, revocation and why it stayed broken, and Certificate Transparency&apos;s pivot from *preventing* mis-issuance to merely *detecting* it. It closes with the 2024-2026 answer -- short-lived and 47-day certificates, CRLite, multi-perspective issuance, tiled logs, and post-quantum certificates -- and a design-review checklist for using X.509 correctly in 2026.
&lt;h2&gt;1. The Certificate That Should Not Have Existed&lt;/h2&gt;
&lt;p&gt;In the late summer of 2011, hundreds of thousands of people in Iran opened Gmail over what looked like a perfectly secure connection. The padlock was closed. The certificate for &lt;code&gt;*.google.com&lt;/code&gt; chained to a trusted root. Every signature in that chain verified flawlessly. It was also completely fake: minted by a breached Dutch certificate authority called DigiNotar, it let someone read all of that traffic, and nothing in any browser&apos;s validation logic so much as blinked [@foxit-diginotar]. The RSA math was perfect. The trust was the bug.&lt;/p&gt;
&lt;p&gt;Sit with the paradox, because the rest of this article lives inside it. A certificate that is &lt;em&gt;cryptographically perfect&lt;/em&gt; and a &lt;em&gt;total compromise&lt;/em&gt; at the same time is not a contradiction the mathematics can resolve. Modular exponentiation did what it promised; SHA hashed what it was given; the signature over the certificate was valid in the strict, checkable sense that it matched the issuer&apos;s public key. And a browser trusting that certificate handed an eavesdropper a live feed of people&apos;s email.&lt;/p&gt;
&lt;p&gt;Here is the reframing that dissolves it. A certificate is not identity, and it is not magic. It is a &lt;em&gt;signed statement&lt;/em&gt;: one party vouches, by signature, that a particular public key belongs to a particular name. &quot;Verifying&quot; a certificate means checking that signature -- the easy, automatable part. Deciding whether to &lt;em&gt;believe the statement it signs&lt;/em&gt; is the hard part, and no signature check can do it for you.&lt;/p&gt;
&lt;p&gt;At DigiNotar, the statement was a lie, signed by an authority the whole Web had agreed to trust. The variable was never the cryptography. It was &lt;strong&gt;delegation&lt;/strong&gt; -- whom a trusted authority chose to vouch for.&lt;/p&gt;
&lt;p&gt;That gives us the single diagnostic tool this field guide is built around, a sentence to ask of any trust system you will ever design or debug:&lt;/p&gt;

Whom am I trusting, to say what, and how would I know if they lied?
&lt;p&gt;By the end of this article you will drop any PKI disaster into one of those buckets on sight. DigiNotar is a &lt;em&gt;whom did I trust&lt;/em&gt; failure -- the answer turned out to be an attacker. Flame, a year later, forged a Microsoft code-signing certificate with a novel hash collision, a &lt;em&gt;what were they allowed to say&lt;/em&gt; failure reaching into a corner of Microsoft&apos;s own PKI [@flame-counter-crypto-2013]. Debian&apos;s OpenSSL, in 2008, generated certificates that were structurally flawless and yet trivially guessable [@cve-2008-0166]. None of them broke a cipher. Every one of them broke a delegation.&lt;/p&gt;
&lt;p&gt;This is Part 4 of a field guide for protocol designers, and it assumes the earlier parts&apos; habits of mind. DigiNotar is, in the language of &lt;a href=&quot;https://paragmali.com/blog/secure-against-whom-the-security-definitions-every-protocol-&quot; rel=&quot;noopener&quot;&gt;Part 1&lt;/a&gt;, a &lt;em&gt;wrong-adversary-model&lt;/em&gt; story: path validation was secure against exactly the attacker it was never going to face. Debian belongs to &lt;a href=&quot;https://paragmali.com/blog/predictable-or-repeated-the-only-two-ways-cryptographic-rand&quot; rel=&quot;noopener&quot;&gt;Part 2&lt;/a&gt;&apos;s account of what a weak key is. And the parsing bugs we will meet in the enforcement code are &lt;a href=&quot;https://paragmali.com/blog/the-signature-was-valid-the-message-was-forged-a-field-guide&quot; rel=&quot;noopener&quot;&gt;Part 3&lt;/a&gt;&apos;s serialization hazards wearing certificates.&lt;/p&gt;
&lt;p&gt;If neither RSA&apos;s modular exponentiation nor SHA&apos;s collision resistance was broken at DigiNotar, then what, exactly, failed? To answer that with any precision, you first have to know what a certificate actually is -- and what a signature over one does, and does not, promise.&lt;/p&gt;
&lt;h2&gt;2. What an X.509 Certificate Actually Is&lt;/h2&gt;
&lt;p&gt;Strip away the mystique and an X.509 certificate is a small, rigidly-encoded data structure in which one key vouches, by signature, for a binding between &lt;em&gt;another&lt;/em&gt; key and a &lt;em&gt;name&lt;/em&gt;, plus a set of machine-readable limits on what that key may do.&lt;/p&gt;

A signed data structure that binds a public key to a name (and to a set of usage constraints), so that anyone holding the issuer&apos;s public key can verify the binding offline. Its format is defined by ITU-T Recommendation X.509 and profiled for the Internet by RFC 5280.
&lt;p&gt;The signed part of the certificate is called the &lt;code&gt;tbsCertificate&lt;/code&gt; -- &quot;to be signed.&quot; RFC 5280 fixes its fields: a version number, a serial number, the issuer&apos;s name, a validity window (&lt;code&gt;notBefore&lt;/code&gt; and &lt;code&gt;notAfter&lt;/code&gt;), the subject&apos;s name, the subject&apos;s public key (&lt;code&gt;subjectPublicKeyInfo&lt;/code&gt;), and, since version 3, a list of extensions [@rfc5280]. The issuer then hashes that whole body and signs the hash. The certificate you receive on the wire is the body plus that one signature.&lt;/p&gt;

flowchart TD
    subgraph TBS[&quot;tbsCertificate (the signed body)&quot;]
      V[&quot;version, serial number&quot;]
      I[&quot;issuer name&quot;]
      VAL[&quot;validity: notBefore, notAfter&quot;]
      SUB[&quot;subject name&quot;]
      SPKI[&quot;subjectPublicKeyInfo: the bound key&quot;]
      EXT[&quot;extensions: v3 authorization scope&quot;]
    end
    TBS --&amp;gt; H[&quot;hash the entire body&quot;]
    H --&amp;gt; SIG[&quot;issuer signs the hash with its private key&quot;]
    SIG --&amp;gt; CERT[&quot;certificate: body plus one signature&quot;]
&lt;p&gt;What does verifying that signature actually establish? Almost exactly one thing. Let $H$ be the hash function and $\sigma$ the signature; a relying party recomputes $H(\text{tbsCertificate})$, runs the signature-verification algorithm against the issuer&apos;s public key $pk$, and confirms the clock is inside the window:&lt;/p&gt;
&lt;p&gt;$$\text{Verify}(pk,, H(\text{tbsCertificate}),, \sigma) = 1 \quad\text{and}\quad t_{\text{notBefore}} \le t_{\text{now}} \le t_{\text{notAfter}}.$$&lt;/p&gt;
&lt;p&gt;That is an $O(1)$ operation over a single certificate -- one hash, one signature check, two date comparisons. A student can implement it in an afternoon.The cost of verifying one certificate does not grow with anything: it is a fixed number of operations regardless of how many certificates exist in the world. The hard part of PKI is never this check. It is deciding whether to &lt;em&gt;believe&lt;/em&gt; the statement the check confirms was signed.&lt;/p&gt;
&lt;p&gt;This is the load-bearing observation of the whole field, worth stating flatly: &lt;strong&gt;the signature check is the easy, automatable part, and deciding whether to believe the signed statement is the hard, unautomatable part.&lt;/strong&gt; A valid signature tells you that &lt;em&gt;whoever holds the issuer&apos;s private key&lt;/em&gt; asserted this binding. It tells you nothing about whether that assertion is &lt;em&gt;true&lt;/em&gt;, or whether that issuer should have been trusted to make it. DigiNotar&apos;s forged certificate passed the check perfectly.&lt;/p&gt;
&lt;p&gt;The bytes themselves are encoded with ASN.1 using the Distinguished Encoding Rules.&lt;/p&gt;

ASN.1 is a language for describing structured data; DER is a canonical binary encoding of it in which every value has exactly one valid byte representation. X.509 certificates are DER-encoded so that the signature covers a single, unambiguous sequence of bytes.
&lt;p&gt;That canonical-encoding requirement is not pedantry. If two different byte sequences could parse to the &quot;same&quot; certificate, an attacker could sign one and present another, and the whole scheme unravels -- which is why certificate parsers are a recurring attack surface, a hazard &lt;a href=&quot;https://paragmali.com/blog/the-signature-was-valid-the-message-was-forged-a-field-guide&quot; rel=&quot;noopener&quot;&gt;Part 3&lt;/a&gt; treats in depth [@itu-x690].&lt;/p&gt;
&lt;p&gt;The format grew in three steps. Version 1 (1988) carried only the basic fields; version 2 added issuer and subject unique identifiers; version 3 (1996) added the &lt;em&gt;extension mechanism&lt;/em&gt; -- a list of &lt;code&gt;{OID, critical flag, value}&lt;/code&gt; entries that finally let a certificate carry its own authorization scope [@rfc5280]. That extension seam is where the &quot;trusted to say what&quot; question gets its answer, and where a two-decade family of enforcement bugs would later live.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This series calls each installment a field guide to &quot;a primitive,&quot; but X.509 and PKI are not a cryptographic primitive in the way a hash or a signature scheme is. They are trust &lt;em&gt;infrastructure composed of&lt;/em&gt; primitives -- signatures, hashes, and encodings wired together into a system for delegating and checking claims. Every failure in this article lives in that wiring, never in the primitives underneath.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So a certificate is just a signed claim, verifiable offline by anyone holding the issuer&apos;s key. That idea raises the question the next forty years of engineering tried to answer: where did it come from, and why did it take so long to make it work on the open Internet?&lt;/p&gt;
&lt;h2&gt;3. Where the Idea Came From: The Trust Gap in Public-Key Cryptography&lt;/h2&gt;
&lt;p&gt;Public-key cryptography did not create trust. It created a &lt;em&gt;gap&lt;/em&gt;. When Whitfield Diffie and Martin Hellman published &quot;New Directions in Cryptography&quot; in 1976, they removed the need for two parties to share a secret before they could communicate securely. But their own paper leaned on a trustworthy &quot;Public File&quot; -- a directory you would query to learn someone&apos;s public key [@diffie-hellman-1976]. Knowing that a key really belongs to the party you think it does was, and remains, a trust problem, not a math problem.&lt;/p&gt;
&lt;p&gt;Querying a live directory for every correspondent was a bottleneck and a hazard: the directory was online, central, and had to be trusted in real time. Two years later, an MIT undergraduate named Loren Kohnfelder proposed the fix in his 1978 bachelor&apos;s thesis. Replace the live lookup with a &lt;em&gt;signed object&lt;/em&gt; -- a &lt;strong&gt;certificate&lt;/strong&gt; -- that anyone can verify offline by checking one signature [@kohnfelder-1978]. That single move, from an online lookup to an offline signed token, is the germ of every certificate authority that has existed since.&lt;/p&gt;

timeline
    title From the public file to the modern trust window
    1976 : Diffie-Hellman assume a trustworthy public file of keys
    1978 : Kohnfelder replaces the lookup with an offline certificate
    1988 : X.509 v1 standardizes the format under the X.500 directory dream
    1993 to 1996 : PEM single-root hierarchy fails : X.509 v3 adds extensions
    1994 to 1999 : Netscape SSL brings many roots and the browser root store : PKIX profiles X.509
    2011 to 2012 : DigiNotar and Flame break the many-root trust model
    2013 to 2018 : Certificate Transparency pivots from prevention to detection
    2024 to 2026 : short lifetimes, multi-perspective issuance, tiled logs, PQ certificates
&lt;p&gt;The concept needed a standard, and in 1988 the CCITT (renamed the ITU-T in 1993) published X.509 as the authentication layer of the &lt;strong&gt;X.500&lt;/strong&gt; directory series. That parentage is the format&apos;s original sin. X.500 envisioned one global directory with one global namespace, and X.509 inherited both assumptions [@itu-x509]. The global directory was never built. The format outlived it, carrying forward a picture of the world -- one namespace, one hierarchy -- that the messy, multi-vendor Internet would spend decades constraining.&lt;/p&gt;
&lt;p&gt;The edition years (v1 in 1988, v3 in 1996) are corroborated here through the normative references of RFC 5280 rather than read from the ITU text directly -- and it is RFC 5280 that carries the profile browsers actually implement [@rfc5280].&lt;/p&gt;
&lt;p&gt;The 1990s taught two hard lessons about &lt;em&gt;how many&lt;/em&gt; authorities the Web should trust. The first attempt, Privacy-Enhanced Mail, specified a single global certification hierarchy: one root, the Internet Policy Registration Authority, atop everything [@rfc1422]. It was undeployable. No one could agree on who should hold the one root key, and the model died. The opposite approach won by shipping. When Netscape put X.509 certificates under its new Secure Sockets Layer protocol, it bundled &lt;em&gt;many&lt;/em&gt; independent root certificates into the browser itself, and the commercial certificate authority industry -- and the browser &lt;strong&gt;root store&lt;/strong&gt; -- was born [@rfc6101].Taher Elgamal, Netscape&apos;s chief scientist, is often called the &quot;father of SSL,&quot; and he did lead the effort. But SSL 3.0, the version that survived, was substantially redesigned with Paul Kocher and Phil Karlton, and SSL 1.0 never shipped publicly [@rfc6101]. Treat the epithet as an honorific, not a claim of sole authorship.&lt;/p&gt;
&lt;p&gt;That left a standards problem: raw X.509 was too permissive for the open Web. The IETF&apos;s PKIX working group produced a constrained Internet profile, first as RFC 2459 in 1999 and then as the RFC 5280 that still governs Web certificates today [@rfc2459]. PKIX also did something subtle the next section depends on: it separated the messy job of &lt;em&gt;finding&lt;/em&gt; a chain of certificates from the precise job of &lt;em&gt;checking&lt;/em&gt; one.&lt;/p&gt;
&lt;p&gt;Other trust models were proposed and lost. PGP&apos;s web of trust asked users to vouch for each other&apos;s keys directly, but it foundered on usability [@whitten-tygar-1999, @wikipedia-web-of-trust]. SPKI/SDSI tried authorization without global names and never saw adoption [@rfc2693]. DANE later offered to anchor certificate trust in DNSSEC instead, a parallel path browsers never shipped [@rfc6698].&lt;/p&gt;
&lt;p&gt;By the 2000s the machinery worked at Internet scale -- hundreds of roots, automatic chaining, a padlock in every browser. And then, one break at a time, the Web discovered that a certificate whose signature &lt;em&gt;verifies&lt;/em&gt; is not the same as a certificate you should &lt;em&gt;trust&lt;/em&gt;. To see why every one of those breaks is the same kind of break, you need a single lens.&lt;/p&gt;
&lt;h2&gt;4. The Three Questions of Delegated Trust&lt;/h2&gt;
&lt;p&gt;Here is the entire article in one sentence, and the tool you will apply to every mechanism below and to tomorrow&apos;s certificate CVE: &lt;em&gt;whom am I trusting, to say what, and how would I know if they lied?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Those clauses are not rhetorical. Each names a question that any public-key infrastructure has to answer, each maps to a specific mechanism in X.509, and each has its own catalog of real-world breaks. The sentence names three questions outright, and answering the last one honestly forces a fourth into the open.&lt;/p&gt;

flowchart TD
    Q[&quot;Whom am I trusting, to say what, and how would I know if they lied?&quot;]
    Q --&amp;gt; B[&quot;1. Binding: is this key really that name?&quot;]
    Q --&amp;gt; A[&quot;2. Authorization: trusted to say what?&quot;]
    Q --&amp;gt; W[&quot;3. Withdrawal: how do I take the trust back?&quot;]
    Q --&amp;gt; D[&quot;4. Detection: how would I even know they lied?&quot;]
&lt;p&gt;&lt;strong&gt;Binding&lt;/strong&gt; is the &quot;whom am I trusting&quot; question: is this key really that name? It lives in issuance, in chain building, and in path validation, and it fails when a certificate is forged, collided, guessable, or signed by an authority that should not have signed it. &lt;strong&gt;Authorization&lt;/strong&gt; is the &quot;to say what&quot; question: even granting that this key is that name, was that name trusted to &lt;em&gt;make this particular claim&lt;/em&gt;? It lives in the version-3 extensions and fails when a relying party skips or mis-parses them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Withdrawal&lt;/strong&gt; is the question everyone assumes is solved: once you know a certificate is bad, how do you stop trusting it? It lives in revocation, and it has almost never worked. &lt;strong&gt;Detection&lt;/strong&gt;, the fourth question, is the one the field arrived at last and reluctantly: if you cannot prevent a trusted authority from lying, how would you even find out that it did?&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;What it asks&lt;/th&gt;
&lt;th&gt;The X.509 mechanism&lt;/th&gt;
&lt;th&gt;Named breaks&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Binding&lt;/td&gt;
&lt;td&gt;Is this key really that name?&lt;/td&gt;
&lt;td&gt;Issuance, path building, path validation&lt;/td&gt;
&lt;td&gt;DigiNotar, Flame, Debian, CurveBall&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authorization&lt;/td&gt;
&lt;td&gt;Trusted to say &lt;em&gt;what&lt;/em&gt;?&lt;/td&gt;
&lt;td&gt;Basic Constraints, Key Usage, EKU, Name Constraints&lt;/td&gt;
&lt;td&gt;MS02-050, CVE-2021-3450, Spooky SSL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Withdrawal&lt;/td&gt;
&lt;td&gt;How do I stop trusting them?&lt;/td&gt;
&lt;td&gt;CRL, OCSP, stapling, Must-Staple&lt;/td&gt;
&lt;td&gt;soft-fail, Heartbleed CRL costs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Detection&lt;/td&gt;
&lt;td&gt;How would I know if they lied?&lt;/td&gt;
&lt;td&gt;Certificate Transparency logs and SCTs&lt;/td&gt;
&lt;td&gt;the answer, not a break&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; Every X.509 and PKI failure lands in one of these four questions, and never in the signature or hash math underneath. When you meet a new certificate incident, do not ask &quot;which algorithm broke?&quot; Ask: whom did someone trust, to say what, could anyone take it back, and would anyone have known? The answer is always in the delegation, not the arithmetic.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Start with the first question -- is this key really that name? -- because before a relying party can enforce anything at all, it has to turn a pile of certificates into a single chain it is willing to trust. That turns out to be two very different jobs, and the industry keeps confusing them for one.&lt;/p&gt;
&lt;h2&gt;5. Binding, Part One: Chain Building Is Not Path Validation&lt;/h2&gt;
&lt;p&gt;A browser is handed a leaf certificate and a bag of intermediates. Getting from there to &quot;trusted&quot; is not one algorithm but two, and treating them as one is a source of bugs.&lt;/p&gt;

A trust anchor is a certificate authority public key that a relying party trusts a priori -- there is no certificate above it to check. The root store is the curated set of trust anchors that a browser or operating-system root program ships and maintains.
&lt;p&gt;The first job is discovery. Because certificate authorities cross-certify each other, reissue intermediates, and publish &quot;Authority Information Access&quot; pointers to their issuers, the certificates in the wild form a &lt;em&gt;graph&lt;/em&gt; with multiple edges and even cycles. Finding a route from your leaf to some trust anchor is a search over that graph.&lt;/p&gt;

The graph search that discovers a candidate chain from a leaf certificate up to some trust anchor, navigating cross-certificates, reissued intermediates, and Authority Information Access pointers. On an adversarial cross-certified graph it is worst-case exponential, which is why RFC 4158 is essentially a catalog of pruning heuristics.
&lt;p&gt;Path building is a depth-first search with loop detection [@rfc4158]. Its output is not a verdict -- it is a &lt;em&gt;candidate&lt;/em&gt;: one chain that plausibly reaches a root. The second job is to decide whether that specific chain is any good.&lt;/p&gt;

The linear state machine that checks one chosen chain: it verifies each signature, checks each validity window, and enforces the authorization extensions (Basic Constraints, Key Usage, Extended Key Usage, Name Constraints, and policy) down the path. A critical extension the verifier does not understand must cause rejection.
&lt;p&gt;Validation is a single pass, so its cost is $O(n)$ in the chain length $n$: for each certificate, check the signature against the next issuer up, confirm the dates, and update the running authorization state from the extensions [@rfc5280]. The rule that trips up naive implementations is the last one in the definition: if a certificate marks an extension &lt;em&gt;critical&lt;/em&gt; and the verifier does not recognize it, the verifier must reject rather than ignore it. Silence is not consent.&lt;/p&gt;

flowchart TD
    Leaf[&quot;Leaf certificate plus a bag of intermediates&quot;]
    Leaf --&amp;gt; Build[&quot;Path BUILDING (RFC 4158): graph search over cross-certs and AIA pointers to find one chain to a trust anchor&quot;]
    Build --&amp;gt; Chain[&quot;One candidate chain: leaf to intermediate to root&quot;]
    Chain --&amp;gt; Val[&quot;Path VALIDATION (RFC 5280 Section 6): linear state machine over the chosen chain&quot;]
    Val --&amp;gt; C1[&quot;verify each signature and validity window&quot;]
    Val --&amp;gt; C2[&quot;enforce Basic Constraints, Key Usage, EKU&quot;]
    Val --&amp;gt; C3[&quot;intersect Name Constraints down the path&quot;]
    Val --&amp;gt; Verdict[&quot;well-formed and authorized? accept or reject&quot;]
&lt;p&gt;Keeping the two apart matters because they fail differently. Building can blow up on a pathological cross-certified graph -- worst-case exponential search -- but a slow build is a denial-of-service risk, not a trust breach. Validation is where trust is decided, and its limits are the subject of the rest of this article.&lt;/p&gt;
&lt;p&gt;Here is the first and deepest of them, the observation that seeds every failure to come: &lt;strong&gt;path validation can prove a chain is well-formed; it can never prove the issuing authority was honest.&lt;/strong&gt; Everything RFC 5280 Section 6 checks is a property of the certificates. None of it is a property of the &lt;em&gt;decision to sign them&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;To make the difference between &quot;verifies&quot; and &quot;authorizes&quot; concrete, here is a certificate as a decoded object, with a few lines that print what it actually permits. Notice that nothing here checks a signature -- it reads the &lt;em&gt;claims&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;{`
// A decoded X.509 certificate as a plain object. No crypto library needed:
// verifying the signature is easy. Reading what it CLAIMS is the point.
var cert = {
  subject: &quot;CN=example.com&quot;,
  issuer: &quot;CN=Example Root CA&quot;,
  notBefore: &quot;2026-01-01&quot;,
  notAfter: &quot;2026-01-07&quot;,
  basicConstraints: { cA: false },
  extendedKeyUsage: [&quot;serverAuth&quot;],
  subjectAltName: [&quot;example.com&quot;, &quot;&lt;a href=&quot;http://www.example.com%22%5D&quot; rel=&quot;noopener&quot;&gt;www.example.com&quot;]&lt;/a&gt;
};&lt;/p&gt;
&lt;p&gt;function describeAuthorization(c) {
  console.log(&quot;Subject: &quot; + c.subject);
  console.log(&quot;Issued by: &quot; + c.issuer);
  console.log(&quot;Valid: &quot; + c.notBefore + &quot; through &quot; + c.notAfter);
  console.log(c.basicConstraints.cA
    ? &quot;CA flag TRUE: this key MAY sign other certificates.&quot;
    : &quot;CA flag false: this key may NOT sign other certificates.&quot;);
  var purposes = {
    serverAuth: &quot;authenticate a TLS server&quot;,
    clientAuth: &quot;authenticate a TLS client&quot;,
    codeSigning: &quot;sign executable code&quot;,
    emailProtection: &quot;sign or encrypt email&quot;
  };
  var allowed = (c.extendedKeyUsage || []).map(function (k) {
    return purposes[k] || k;
  });
  console.log(&quot;This certificate authorizes: &quot; + allowed.join(&quot;, &quot;));
  console.log(&quot;For the names: &quot; + c.subjectAltName.join(&quot;, &quot;));
}&lt;/p&gt;
&lt;p&gt;describeAuthorization(cert);
`}&lt;/p&gt;
&lt;p&gt;The binding model got to this point in six recognizable steps, each answering a limitation of the one before:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Year&lt;/th&gt;
&lt;th&gt;Key idea&lt;/th&gt;
&lt;th&gt;Strength&lt;/th&gt;
&lt;th&gt;Why it was superseded or limited&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Online public-key directory&lt;/td&gt;
&lt;td&gt;1976&lt;/td&gt;
&lt;td&gt;Live name-to-key lookup&lt;/td&gt;
&lt;td&gt;Distributes public keys at all&lt;/td&gt;
&lt;td&gt;Online single point of failure; no offline authenticity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;X.509 v1/v2 certificate&lt;/td&gt;
&lt;td&gt;1988&lt;/td&gt;
&lt;td&gt;Signed offline name-to-key token&lt;/td&gt;
&lt;td&gt;Portable, verifiable offline&lt;/td&gt;
&lt;td&gt;No authorization scope; X.500 global namespace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;X.509 v3 certificate&lt;/td&gt;
&lt;td&gt;1996&lt;/td&gt;
&lt;td&gt;Extensions carry authorization scope&lt;/td&gt;
&lt;td&gt;Still the live format&lt;/td&gt;
&lt;td&gt;Only as good as the verifier&apos;s enforcement&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Path building&lt;/td&gt;
&lt;td&gt;2005&lt;/td&gt;
&lt;td&gt;Graph search from leaf to a root&lt;/td&gt;
&lt;td&gt;Finds a path amid cross-certs and AIA&lt;/td&gt;
&lt;td&gt;Ambiguity is a bug class; worst-case exponential&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Path validation&lt;/td&gt;
&lt;td&gt;2008 (RFC 5280; algorithm since RFC 2459, 1999)&lt;/td&gt;
&lt;td&gt;Section 6 state machine over one chain&lt;/td&gt;
&lt;td&gt;Deterministic, fully specified checks&lt;/td&gt;
&lt;td&gt;Cannot detect a dishonest CA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Web PKI (many roots)&lt;/td&gt;
&lt;td&gt;1994&lt;/td&gt;
&lt;td&gt;Any-root chaining, browser root store&lt;/td&gt;
&lt;td&gt;Deployed at Internet scale&lt;/td&gt;
&lt;td&gt;Trust equals the trust of the weakest CA&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Validation answers &quot;is this chain well-formed?&quot; But well-formed &lt;em&gt;for what&lt;/em&gt;? A chain can be flawless and still end in a leaf certificate quietly claiming the right to sign for the entire Internet. That is the second question -- trusted to say what? -- and it has its own two-decade catalog of enforcement bugs.&lt;/p&gt;
&lt;h2&gt;6. Binding, Part Two: Trusted to Say What?&lt;/h2&gt;
&lt;p&gt;Version 3&apos;s extensions finally let a certificate carry its own limits. Four of them do the heavy lifting for authorization, and each is a field that path validation is supposed to enforce as it walks the chain [@rfc5280].&lt;/p&gt;

The extension that says whether a certificate&apos;s key may act as a certificate authority (`cA` = TRUE or FALSE) and, if so, how many sub-CAs may appear below it (`pathLenConstraint`). A leaf certificate has `cA` = FALSE and must not be able to sign other certificates.

The machine-readable list of purposes a key is permitted to serve -- TLS server authentication, TLS client authentication, code signing, email protection, and so on. A certificate valid for `serverAuth` is not thereby valid for `codeSigning`.

The extension that limits a sub-CA to issuing only within permitted name subtrees (and outside excluded ones). During validation the permitted and excluded sets are intersected down the path, so a constrained sub-CA cannot widen its own authority.
&lt;p&gt;Key Usage is the fourth: a lower-level bitfield (digital signature, key encipherment, certificate signing) that further narrows what the key may do. Together these fields answer the &quot;trusted to say what&quot; question in a form a machine can check. And here is the catch, the sentence that generates a two-decade bug family: &lt;strong&gt;a declarative authorization field is worthless if the relying party&apos;s code skips, misorders, or mis-parses the check.&lt;/strong&gt; The certificate can be flawless. If the verifier does not enforce the limit, the limit does not exist.&lt;/p&gt;
&lt;p&gt;The archetype is &lt;strong&gt;MS02-050&lt;/strong&gt;, disclosed in 2002. Microsoft&apos;s CryptoAPI simply did not check the &lt;code&gt;cA&lt;/code&gt; flag in Basic Constraints, so &lt;em&gt;any&lt;/em&gt; valid leaf certificate could sign certificates for other names, and every Microsoft application that used CryptoAPI would accept them [@cve-2002-0862]. Moxie Marlinspike weaponized it in a tool called sslsniff: obtain an ordinary certificate for a domain you control, then use it to mint a certificate for any other domain, and Internet Explorer would trust it [@sslsniff-moxie]. The certificate authority had done nothing wrong. The &lt;em&gt;verifier&lt;/em&gt; had.&lt;/p&gt;
&lt;p&gt;You might expect that lesson to stay learned. It did not. In March 2021, &lt;strong&gt;OpenSSL CVE-2021-3450&lt;/strong&gt; landed: a regression in strict mode meant the library would accept a non-CA certificate as a CA -- the exact same bug class, nineteen years later, in the most widely used TLS library on Earth [@openssl-secadv-20210325].&lt;/p&gt;
&lt;p&gt;The failure family is structural, not incidental. Enforcement code is written by humans, and the check that a certificate is &lt;em&gt;not&lt;/em&gt; allowed to do something is precisely the check that is easy to forget. Apple&apos;s 2014 &quot;goto fail&quot; bug made the same point in a single line -- a duplicated &lt;code&gt;goto&lt;/code&gt; statement that skipped the signature check altogether [@cve-2014-1266].CurveBall (CVE-2020-0601, 2020) is a cousin worth knowing: Windows CryptoAPI accepted attacker-supplied &lt;em&gt;explicit&lt;/em&gt; elliptic-curve parameters, so a signature could verify against an attacker-chosen curve rather than the standard one. The signature was &quot;valid,&quot; but the validity was meaningless -- a binding-adjacent verification bug in the same spirit as skipping Basic Constraints [@cve-2020-0601].&lt;/p&gt;
&lt;p&gt;The machinery meant to enforce these limits can itself become the attack surface. In 2022, &lt;strong&gt;&quot;Spooky SSL&quot;&lt;/strong&gt; (CVE-2022-3602 and CVE-2022-3786) was a stack buffer overflow reached &lt;em&gt;during Name-Constraint checking&lt;/em&gt; of punycode-encoded email addresses in OpenSSL 3.0 [@cve-2022-3602]. The code enforcing an authorization limit had a memory-safety bug in its parser -- exactly the serialization hazard &lt;a href=&quot;https://paragmali.com/blog/the-signature-was-valid-the-message-was-forged-a-field-guide&quot; rel=&quot;noopener&quot;&gt;Part 3&lt;/a&gt; warns about, now living inside the authorization check itself [@openssl-secadv-20221101].&lt;/p&gt;
&lt;p&gt;To feel how thin the line is, here are two validators over the same forged chain. They differ by one &lt;code&gt;if&lt;/code&gt; statement.&lt;/p&gt;
&lt;p&gt;{`
// A leaf certificate (cA:false) is trying to sign a certificate for a bank.
var attackerLeaf = { subject: &quot;CN=mallory.example&quot;, basicConstraints: { cA: false } };
var forged = { subject: &quot;CN=login.yourbank.com&quot;, issuedBy: attackerLeaf };&lt;/p&gt;
&lt;p&gt;function accepts(issuer, opts) {
  var signatureMatches = true; // assume the crypto checks out -- it usually does
  if (opts.checkBasicConstraints &amp;amp;&amp;amp; issuer.basicConstraints.cA !== true) {
    return false; // issuer is not a CA, so reject. This is the whole defense.
  }
  return signatureMatches;
}&lt;/p&gt;
&lt;p&gt;console.log(&quot;Strict validator (checks cA): &quot; +
  (accepts(forged.issuedBy, { checkBasicConstraints: true }) ? &quot;ACCEPT&quot; : &quot;REJECT&quot;));
console.log(&quot;Lax validator (skips cA):     &quot; +
  (accepts(forged.issuedBy, { checkBasicConstraints: false }) ? &quot;ACCEPT&quot; : &quot;REJECT&quot;));
console.log(&quot;The lax path just let a leaf sign for &quot; + forged.subject + &quot;.&quot;);
`}&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Use a maintained validation library and turn on strict path validation. Do not skip Basic Constraints, Key Usage, EKU, or Name Constraints, and never treat a &lt;em&gt;critical&lt;/em&gt; extension the verifier does not understand as benign -- RFC 5280 requires rejection. Most of the failure catalog in this article is not broken cryptography; it is a verifier that let something through.&lt;/p&gt;
&lt;/blockquote&gt;

The enforcement that fails so often in public code is a gift when you run it deliberately. If you operate an internal certificate authority, issue its root with Name Constraints so it can *only* sign for your own namespace -- `*.corp.example.com` and nothing else. Even if that root key leaks, it cannot mint a certificate for `google.com`, because conforming validators intersect the constraint down every path. You trade public trust for tight, auditable scope, which is exactly the trade Active Directory Certificate Services operators wrestle with in [this companion post](/blog/certified-pre-owned-ad-cs-and-active-directorys-second-trust).
&lt;p&gt;Every one of these breaks had a fix and a lesson, and the Web learned them. But they share a comforting property: the certificate authority was honest, and the &lt;em&gt;code&lt;/em&gt; was wrong. What happens when the code is right, the signature verifies, and the authority itself is the adversary?&lt;/p&gt;
&lt;h2&gt;7. The Binding Failure Catalog: When the Authority Vouched for the Wrong Key&lt;/h2&gt;
&lt;p&gt;There are three ways a certificate can be perfectly valid and perfectly compromised, and none of them is a bug in the mathematics. Each is a failure of &lt;em&gt;binding&lt;/em&gt; -- the &quot;is this key really that name?&quot; question -- and each taught the Web something it did not want to learn.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;DigiNotar (2011)&lt;/strong&gt; is the pure case. Attackers who had breached the Dutch certificate authority used its fully trusted key to issue a valid &lt;code&gt;*.google.com&lt;/code&gt; certificate, along with more than 500 others, and someone used the Google certificate to intercept the traffic of roughly 300,000 users in Iran [@foxit-diginotar]. Every signature verified. Path validation flagged nothing, because a &lt;em&gt;trusted&lt;/em&gt; authority had chosen to sign. The forensic report, aptly titled &quot;Black Tulip,&quot; traced the intrusion; the company, owned by VASCO, was distrusted by every major browser and declared bankruptcy in September 2011 [@wikipedia-diginotar].&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; DigiNotar has no cryptographic tell. There was nothing in the certificate, the chain, or the signature for path validation to catch, because the failure was not in any of them -- it was in the decision to sign. Prevention failed at the root of trust itself. When a trusted authority turns adversary, the only defense left is &lt;em&gt;detection&lt;/em&gt;: making the lie visible after the fact. Hold that thought; it is the reason Certificate Transparency exists.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Flame (2012)&lt;/strong&gt; made the same point on a different trust path. The Flame espionage platform carried a certificate that chained to Microsoft and appeared valid for &lt;em&gt;code signing&lt;/em&gt;, letting its components masquerade as legitimate Windows Update content. The certificate was forged using a &lt;em&gt;novel&lt;/em&gt; chosen-prefix MD5 collision -- a new variant, distinct from the 2008 technique -- against a corner of Microsoft&apos;s Terminal Server Licensing infrastructure still signing with MD5 [@flame-counter-crypto-2013].&lt;/p&gt;
&lt;p&gt;Marc Stevens&apos; &quot;counter-cryptanalysis&quot; later reconstructed it forensically. A hash-function weakness, reached through a forgotten sub-CA, subverted code-signing trust at nation-state scale; the trust path Flame abused is the subject of &lt;a href=&quot;https://paragmali.com/blog/authenticode-and-catalog-files-the-crypto-foundation-under-w&quot; rel=&quot;noopener&quot;&gt;this companion post&lt;/a&gt;.A chosen-prefix collision takes two &lt;em&gt;attacker-chosen&lt;/em&gt; prefixes and appends carefully computed bytes so their MD5 digests come out identical. The attacker gets a benign-looking certificate signed and transplants the signature onto a malicious one with the same digest. The underlying &quot;valid signature, forged message&quot; hazard is the theme of &lt;a href=&quot;https://paragmali.com/blog/the-signature-was-valid-the-message-was-forged-a-field-guide&quot; rel=&quot;noopener&quot;&gt;Part 3&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Debian OpenSSL (2008)&lt;/strong&gt; is the case that proves the binding failure need not involve the authority at all. A well-meaning Debian patch removed code the maintainer thought was uninitialized-memory noise, and in doing so gutted the entropy of OpenSSL&apos;s random number generator. Keys generated on affected systems came from a space of only about 32,767 possibilities [@cve-2008-0166]. The X.509 structure was flawless; the private key underneath was one you could guess.With the entropy source reduced to essentially the process ID, affected keys had roughly 15 bits of randomness -- around 32,767 candidates for a given key type and size, enumerable in an afternoon. This is a randomness failure, catalogued under weak keys in &lt;a href=&quot;https://paragmali.com/blog/predictable-or-repeated-the-only-two-ways-cryptographic-rand&quot; rel=&quot;noopener&quot;&gt;Part 2&lt;/a&gt;, not a defect in certificate logic. The certificate did its job perfectly; the job was worthless.&lt;/p&gt;
&lt;p&gt;Flame did not appear from nowhere. Its research ancestor is the &lt;strong&gt;2008 rogue CA&lt;/strong&gt; demonstration, in which Marc Stevens and six co-authors used a chosen-prefix MD5 collision -- on the order of &lt;code&gt;2^49&lt;/code&gt; MD5 compression-function evaluations -- to obtain a working intermediate CA certificate from a RapidSSL branch that still signed with MD5 [@rogue-ca-md5-2009]. That was a controlled proof of concept. Flame was the same idea, refined, in the wild.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Incident&lt;/th&gt;
&lt;th&gt;Year&lt;/th&gt;
&lt;th&gt;Root cause&lt;/th&gt;
&lt;th&gt;Question it breaks&lt;/th&gt;
&lt;th&gt;The lesson&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;DigiNotar&lt;/td&gt;
&lt;td&gt;2011&lt;/td&gt;
&lt;td&gt;Compromised trusted CA issued valid rogue certificates&lt;/td&gt;
&lt;td&gt;Binding (whom)&lt;/td&gt;
&lt;td&gt;You cannot prevent a trusted CA from lying -- only detect it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flame&lt;/td&gt;
&lt;td&gt;2012&lt;/td&gt;
&lt;td&gt;Novel MD5 chosen-prefix collision forged a code-signing cert&lt;/td&gt;
&lt;td&gt;Binding (whom)&lt;/td&gt;
&lt;td&gt;A hash break subverts code signing; retire weak hashes in CA signing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Debian OpenSSL&lt;/td&gt;
&lt;td&gt;2008&lt;/td&gt;
&lt;td&gt;PRNG entropy gutted, leaving ~15-bit guessable keys&lt;/td&gt;
&lt;td&gt;Binding (weak key)&lt;/td&gt;
&lt;td&gt;A perfect certificate over a guessable key is worthless&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2008 rogue CA (RapidSSL)&lt;/td&gt;
&lt;td&gt;2008&lt;/td&gt;
&lt;td&gt;Chosen-prefix MD5 collision, about &lt;code&gt;2^49&lt;/code&gt; compressions&lt;/td&gt;
&lt;td&gt;Binding (whom)&lt;/td&gt;
&lt;td&gt;MD5 in CA signing is fatal; the research ancestor of Flame&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;

DigiNotar was the sharpest instance of a recurring pattern, not a one-off. Across the 2010s, other trusted authorities were caught mis-issuing certificates or were distrusted for related failures -- names like Comodo [@comodo-fraud-2011], TURKTRUST [@turktrust-2013], Symantec [@symantec-distrust-2017], and WoSign [@wosign-distrust-2016] recur in any serious history of the Web PKI. The details differ; the shape does not. Each time, a certificate that path validation would happily accept turned out to rest on a delegation that should never have been made. And the modern analog is a *stolen signing key* rather than a rogue authority, as in the [Storm-0558 incident](/blog/forged-from-2016-how-storm-0558-turned-one-stolen-signing-ke), where one compromised key forged tokens at cloud scale.
&lt;p&gt;DigiNotar proved you cannot &lt;em&gt;prevent&lt;/em&gt; a trusted authority from lying. That leaves two questions the field is still wrestling with: once a certificate is known to be bad, how do you take the trust back -- and if you cannot, how would you even find out? Take withdrawal first, because it is the one everyone assumes is solved and almost nobody has working.&lt;/p&gt;
&lt;h2&gt;8. Withdrawal: How Do I Stop Trusting Them, and Why That Is So Hard&lt;/h2&gt;
&lt;p&gt;Ask an engineer how certificate revocation works and you will get a clear description of a mechanism. Ask whether it &lt;em&gt;works&lt;/em&gt; and the honest answer is: for most of the Web, for most of its history, it did not. Revocation is where the &quot;how do I take the trust back?&quot; question goes to fail, and it fails in a genealogy of increasingly clever attempts.&lt;/p&gt;

Any connection-time revocation mechanism wants three things at once: *freshness* (the answer reflects reality now), *privacy* (checking does not tell anyone what you are browsing), and *scalability* (it works at Web scale without huge cost). No single online mechanism achieved all three. CRLs sacrificed freshness and size; OCSP sacrificed privacy; stapling sacrificed enforceability. The field eventually stopped trying to satisfy the trilemma and started dissolving the need for it.
&lt;p&gt;The original mechanism is the &lt;strong&gt;Certificate Revocation List&lt;/strong&gt;, a periodically published, CA-signed list of revoked serial numbers, in X.509 since 1988 and profiled by RFC 5280 Section 5 [@rfc5280]. The problem is size. A CRL grows with the number of revocations, routinely reaching megabytes, and downloading a multi-megabyte list to visit one site is untenable, so browsers cached them (reintroducing staleness) or stopped fetching them entirely [@mozilla-crlite-2020].&lt;/p&gt;
&lt;p&gt;The cost became concrete after Heartbleed triggered mass revocation in 2014. Serving its CA partner GlobalSign&apos;s suddenly-4.7 MB CRL, Cloudflare estimated, would have added about $400,000 a month in bandwidth -- roughly $953,000 at AWS pricing -- a load Cloudflare largely absorbed rather than a bill it paid [@cloudflare-heartbleed]. Online revocation was still being reaffirmed as ineffective years afterward [@langley-revocationagain-2014].&lt;/p&gt;
&lt;p&gt;The fix seemed obvious: instead of downloading the whole list, ask about the one certificate you care about. The &lt;strong&gt;Online Certificate Status Protocol&lt;/strong&gt; does exactly that -- a per-certificate query to a CA responder that returns a signed good, revoked, or unknown [@rfc6960]. It has three fatal properties.&lt;/p&gt;
&lt;p&gt;First, &lt;strong&gt;soft-fail&lt;/strong&gt;: because the client cannot always reach the responder -- captive portals, responder downtime, or an attacker who simply blocks the request -- a failed check is &lt;em&gt;ignored&lt;/em&gt;. That single design choice makes it useless against the attacker it targets, because a man-in-the-middle who can intercept your HTTPS can also block your OCSP query [@langley-crlsets-2012, @langley-revocation-2011].&lt;/p&gt;

&quot;So soft-fail revocation checks are like a seat-belt that snaps when you crash. Even though it works 99% of the time, it&apos;s worthless because it only works when you don&apos;t need it.&quot; -- Adam Langley
&lt;p&gt;Second, OCSP is a &lt;strong&gt;privacy leak&lt;/strong&gt;: every check tells the certificate authority that a particular user, at a particular IP, is visiting a particular site, in real time -- reviving the online-directory hazard that certificates were invented to escape [@langley-crlsets-2012]. Third, it adds &lt;strong&gt;latency&lt;/strong&gt;: a median around 300 milliseconds on every first connection [@langley-crlsets-2012], with more than 7% of checks timing out entirely [@mozilla-crlite-2020], which is why Chrome disabled online revocation checking by default.Soft-fail is not a careless default; it is a forced one. If you make OCSP hard-fail instead -- block the connection whenever the check does not complete -- then any responder outage becomes an outage for every site that depends on it. So you either honor a blocked check (worthless against a MITM) or you refuse it (a self-inflicted denial of service). There is no third option at connection time.&lt;/p&gt;
&lt;p&gt;Two fixes branched from OCSP. &lt;strong&gt;OCSP stapling&lt;/strong&gt; moves the query off the client: the server fetches a fresh signed response and staples it into the TLS handshake, so the CA never sees the visitor and there is no per-connection round-trip [@rfc6066]. But stapling is optional, so it inherits soft-fail -- an attacker just presents a certificate with no staple, and the client falls back to accepting it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;OCSP Must-Staple&lt;/strong&gt; tried to close that gap with a flag in the certificate demanding a valid staple, turning a missing staple into a hard failure [@rfc7633]. Hard-fail, though, means any hiccup in obtaining a staple takes the site down, so adoption stayed near zero and the feature became a footgun.&lt;/p&gt;

flowchart TD
    CRL[&quot;CRL 1988: periodic signed lists&quot;]
    OCSP[&quot;OCSP 1999: per-cert online query&quot;]
    STAP[&quot;OCSP stapling 2003: server fetches the response&quot;]
    MUST[&quot;Must-Staple 2015: certificate demands a staple&quot;]
    PUSH[&quot;Pushed and dissolved 2012 to 2026: CRLite plus short-lived certs&quot;]
    CRL --&amp;gt;|&quot;lists too large, went stale&quot;| OCSP
    OCSP --&amp;gt;|&quot;soft-fail, privacy leak, latency&quot;| STAP
    STAP --&amp;gt;|&quot;optional, so still soft-fails&quot;| MUST
    MUST --&amp;gt;|&quot;hard-fail self-DoS, near-zero adoption&quot;| PUSH
&lt;p&gt;Then the whole branch was retired. Let&apos;s Encrypt stopped issuing Must-Staple certificates on 30 January 2025, dropped OCSP URLs from its certificates around 7 May 2025, and turned its OCSP responders &lt;strong&gt;off on 6 August 2025&lt;/strong&gt; [@le-ending-ocsp-2024] -- responders that at peak had served roughly 340 billion requests per month [@le-ocsp-eol-2025]. The CA/Browser Forum made OCSP optional [@cabforum-br-documents]. The stated rationale was user privacy and reliability, not mere cleanup [@abetterinternet-ending-ocsp]. This is not a mechanism you tune; it is one the industry decommissioned.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mechanism&lt;/th&gt;
&lt;th&gt;Client cost&lt;/th&gt;
&lt;th&gt;Freshness&lt;/th&gt;
&lt;th&gt;Privacy&lt;/th&gt;
&lt;th&gt;Fail mode versus MITM&lt;/th&gt;
&lt;th&gt;Status in 2026&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;CRL&lt;/td&gt;
&lt;td&gt;Download the whole list (MBs)&lt;/td&gt;
&lt;td&gt;Publication interval&lt;/td&gt;
&lt;td&gt;Good (offline)&lt;/td&gt;
&lt;td&gt;Stale or skipped&lt;/td&gt;
&lt;td&gt;Fallback substrate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OCSP&lt;/td&gt;
&lt;td&gt;About 300 ms per check&lt;/td&gt;
&lt;td&gt;Real-time&lt;/td&gt;
&lt;td&gt;Leaks browsing to the CA&lt;/td&gt;
&lt;td&gt;Soft-fail is worthless&lt;/td&gt;
&lt;td&gt;Off (2025-08-06)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stapling / Must-Staple&lt;/td&gt;
&lt;td&gt;Zero client round-trips&lt;/td&gt;
&lt;td&gt;Staple window&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Missing staple soft-fails, or hard-fail self-DoS&lt;/td&gt;
&lt;td&gt;Legacy, effectively dead&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CRLite&lt;/td&gt;
&lt;td&gt;O(1) local lookup&lt;/td&gt;
&lt;td&gt;Push interval (4x/day)&lt;/td&gt;
&lt;td&gt;Good (offline)&lt;/td&gt;
&lt;td&gt;Cannot be suppressed&lt;/td&gt;
&lt;td&gt;Active (Firefox)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Short-lived certs&lt;/td&gt;
&lt;td&gt;Zero (no revocation data)&lt;/td&gt;
&lt;td&gt;Certificate expires in ~6 days&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Nothing to check&lt;/td&gt;
&lt;td&gt;Active (GA Jan 2026)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; OCSP, OCSP stapling, and OCSP Must-Staple are legacy. The responders are off, Must-Staple is a self-inflicted denial-of-service risk, and the CA/Browser Forum has made OCSP optional. Design revocation around CRLite and CRLs as the offline fallback, plus short-lived certificates -- never around OCSP.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Every mechanism that tried to let you &lt;em&gt;withdraw&lt;/em&gt; trust at connection time failed on soft-fail, privacy, latency, or brittleness. So the Web made a different move -- one that does not fix revocation at all. But before we get there, it had to answer DigiNotar&apos;s unanswered question: if you cannot prevent a rogue certificate, how would you even know one exists?&lt;/p&gt;
&lt;h2&gt;9. Detection: How Would I Know If They Lied? Certificate Transparency&lt;/h2&gt;
&lt;p&gt;The most important idea in modern PKI is an admission of defeat. You cannot stop a trusted authority from lying, DigiNotar proved that, so stop trying to prevent it and make lying &lt;em&gt;impossible to hide&lt;/em&gt; instead.&lt;/p&gt;

A system of public, append-only Merkle-tree logs that record every certificate a trusted authority issues. Browsers require proof that a certificate was logged before they will trust it, so mis-issuance leaves a permanent, public record that anyone can monitor.
&lt;p&gt;Certificate Transparency, introduced by Ben Laurie, Adam Langley, and Emilia Kasper of Google in RFC 6962 in 2013, works like this: before a certificate can be trusted, it must be submitted to public logs, and each log returns a small signed receipt [@rfc6962].&lt;/p&gt;

A log&apos;s signed promise to include a certificate in its append-only tree within a bounded time. Browsers require one or more SCTs, from independent logs, as a precondition for trusting a certificate.
&lt;p&gt;A CT log is an append-only Merkle tree, and its power is that two things about it are cheaply &lt;em&gt;provable&lt;/em&gt;. An &lt;strong&gt;inclusion proof&lt;/strong&gt; (or audit proof) shows that a specific certificate is in the tree, using only about $\log_2 n$ hashes for a log of $n$ entries. A &lt;strong&gt;consistency proof&lt;/strong&gt; shows that an earlier version of the tree is a prefix of a later one -- that the log only appended and never rewrote history -- also in $O(\log n)$ hashes [@rfc6962]. Those logarithmic proofs are what make a log at Internet scale auditable by a small client [@rfc9162].&lt;/p&gt;

sequenceDiagram
    participant CA as Certificate Authority
    participant Log as CT Log
    participant Server as TLS Server
    participant Browser as Browser
    participant Monitor as Monitor
    CA-&amp;gt;&amp;gt;Log: submit newly issued certificate
    Log--&amp;gt;&amp;gt;CA: return a Signed Certificate Timestamp
    CA--&amp;gt;&amp;gt;Server: certificate with embedded SCT
    Server-&amp;gt;&amp;gt;Browser: TLS handshake with certificate and SCT
    Browser-&amp;gt;&amp;gt;Browser: require a valid SCT before trusting
    Monitor-&amp;gt;&amp;gt;Log: read the append-only log
    Monitor--&amp;gt;&amp;gt;Monitor: alert the domain owner on mis-issuance
&lt;p&gt;The SCT reaches the browser by one of three paths: embedded in the certificate itself, delivered in a TLS handshake extension, or -- historically -- stapled into an OCSP response.The OCSP-stapled delivery path for SCTs is dying along with OCSP itself, which leaves the embedded and TLS-extension paths as the durable ones. The most common in practice is embedding, because it requires no server configuration. Whichever path carries it, the browser refuses to trust a publicly-trusted certificate that cannot prove it was logged. And because the logs are public, independent &lt;strong&gt;monitors&lt;/strong&gt; read them continuously, so a domain owner can &lt;em&gt;discover&lt;/em&gt; a mis-issued certificate for their own name, often within minutes, even though nothing stopped it from being issued [@wikipedia-ct].&lt;/p&gt;
&lt;p&gt;Here is the mechanism under an SCT: recomputing a Merkle root from a leaf and a short audit path, the check a browser or auditor performs.&lt;/p&gt;
&lt;p&gt;{`
// Recompute a Merkle root from one leaf plus an audit path of sibling hashes.
// Toy hash shows the SHAPE; a real CT log uses SHA-256 over the same structure.
function h(x) { return &quot;H(&quot; + x + &quot;)&quot;; }&lt;/p&gt;
&lt;p&gt;var leaf = h(&quot;cert:example.com&quot;);           // the certificate we want to prove
var auditPath = [                            // one sibling hash per tree level
  { side: &quot;right&quot;, hash: &quot;H(cert:sibling)&quot; },
  { side: &quot;right&quot;, hash: &quot;H(subtree-2)&quot; },
  { side: &quot;left&quot;,  hash: &quot;H(subtree-3)&quot; }
];&lt;/p&gt;
&lt;p&gt;function recomputeRoot(node, path) {
  for (var i = 0; i &amp;lt; path.length; i++) {
    var s = path[i];
    node = (s.side === &quot;left&quot;) ? h(s.hash + &quot;|&quot; + node) : h(node + &quot;|&quot; + s.hash);
  }
  return node;
}&lt;/p&gt;
&lt;p&gt;var treeSize = 8; // eight certificates in this toy log
console.log(&quot;Log holds &quot; + treeSize + &quot; certificates.&quot;);
console.log(&quot;Proof length: &quot; + auditPath.length + &quot; hashes, about log2 of &quot; + treeSize + &quot;.&quot;);
console.log(&quot;Recomputed root: &quot; + recomputeRoot(leaf, auditPath));
console.log(&quot;If it equals the log&apos;s signed root, inclusion is proven, in O(log n).&quot;);
`}&lt;/p&gt;
&lt;p&gt;CT became mandatory for public trust when Chrome began requiring SCTs for certificates issued after 30 April 2018 [@chromium-ct-policy], and Apple followed for Safari on 15 October 2018 [@apple-ct-requirements]. Browsers accept SCTs only from logs admitted to their transparency programs [@google-ct-requirements]. The design goal was stated plainly in the RFC:&lt;/p&gt;

Publicly logging certificates &quot;allows anyone to audit certificate authority activity and notice the issuance of suspect certificates as well as to audit the certificate logs themselves.&quot; -- RFC 6962
&lt;p&gt;But read the verbs carefully. &lt;em&gt;Audit. Notice.&lt;/em&gt; Not &lt;em&gt;prevent&lt;/em&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Certificate Transparency does not stop a rogue certificate from being issued or trusted. It makes the certificate leave a public, permanent record. A logged rogue certificate is still valid and trusted until a human notices it and gets it distrusted. CT shrinks the &lt;em&gt;undetected&lt;/em&gt; window, not the &lt;em&gt;exploitable&lt;/em&gt; one -- it is the honest ceiling of what an open, many-root system can do.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;CT closes the loop DigiNotar opened, but only as far as &lt;em&gt;detection&lt;/em&gt;. A detected rogue certificate is still a valid, trusted certificate until it is revoked, and we have just seen that revocation is broken. That unresolved tension -- detected but still valid, and hard to take back -- is exactly what the 2024-2026 frontier attacks, and it does so with one surprisingly simple move.&lt;/p&gt;
&lt;h2&gt;10. State of the Art, 2024 to 2026&lt;/h2&gt;
&lt;p&gt;The whole modern frontier is one strategic move wearing five hats: shrink the trust window until withdrawal barely matters, corroborate issuance from many vantage points, keep every act of delegation public, and make the whole apparatus quantum-durable. Here are the layers, each sitting on the settled PKIX substrate.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Short-lived certificates&lt;/strong&gt; are the dissolving move. Let&apos;s Encrypt&apos;s roughly six-day profile, which reached general availability in January 2026, issues certificates that carry &lt;em&gt;no OCSP or CRL URLs at all&lt;/em&gt; -- a certificate that mis-issues simply expires before revocation would have mattered [@le-6day-2025]. Revocation is not fixed; it is designed out. The price is a renewal cadence that only automation can sustain.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The 47-day validity schedule&lt;/strong&gt; turns that from a boutique option into Web-wide policy. CA/Browser Forum Ballot SC-081v3, proposed by Apple (endorsed by Sectigo, Google, and Mozilla) and approved on 11 April 2025, steps the maximum TLS certificate validity down in stages: 398 days now, 200 days from 15 March 2026, 100 days from 15 March 2027, and 47 days from 15 March 2029, with domain-validation data reuse falling to just 10 days [@cabforum-sc081-47day].&lt;/p&gt;
&lt;p&gt;All four major browser vendors voted yes, and the schedule binds publicly trusted authorities through the CA/Browser Forum Baseline Requirements [@cabforum-br-documents]. Its real function is a forcing function: certificates this short cannot be managed by hand, so the schedule mandates ACME-style automation across the entire Web [@digicert-47day].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CRLite&lt;/strong&gt; handles the certificates that still live long enough to need revoking. It ingests every certificate from CT plus Internet scans, sorts them into revoked and valid, and encodes the answer as a cascade of Bloom filters, compressing roughly 300 MB of revocation data into about 1 MB that is refreshed four times a day and checked locally, offline, with zero false negatives [@mozilla-crlite-2020, @crlite-pdf]. Lookups leak nothing and cannot be suppressed by a man-in-the-middle, which is the property OCSP never had [@crlite-ieee-2017]. After OCSP&apos;s retirement, plain CRLs are again the universal fallback beneath it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Multi-Perspective Issuance Corroboration (MPIC)&lt;/strong&gt; hardens the &lt;em&gt;binding&lt;/em&gt; step at issuance time. Under CA/Browser Forum Ballot SC-067v3, a certificate authority must perform domain-control and CAA checks from several network vantage points, and the remote perspectives must corroborate the primary result or issuance is blocked -- which defeats localized BGP hijacks that could fool validation from a single location [@cabforum-sc067-mpic].MPIC revives the &quot;check from many vantage points&quot; idea of the Perspectives and Convergence notary projects from around 2008 to 2011, which failed as user-facing products. The revival works because it runs inside the certificate authority at &lt;em&gt;issuance&lt;/em&gt; time, not on the user&apos;s machine at &lt;em&gt;connection&lt;/em&gt; time.&lt;/p&gt;
&lt;p&gt;The requirement was adopted in 2024; enforcement began with two corroborating perspectives on 15 September 2025, ramping to three by 15 March 2026, four by 15 June 2026, and five by 15 December 2026, with perspectives required to sit at least 500 km apart [@sectigo-mpic-faq].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Static, tile-based CT&lt;/strong&gt; makes universal logging affordable. The Sunlight design, built by Let&apos;s Encrypt and Filippo Valsorda with the interoperable Static CT API, publishes the Merkle tree as immutable 256-element tiles served as plain static files from object storage, with no per-request database. The scale motivation is concrete: Let&apos;s Encrypt issues over four million certificates per day, and its Oak log passed 700 million entries, sizes that buckled the older relational design [@le-sunlight-2024]. Tiling turns every proof read into a cache hit [@static-ct-api].&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Merkle Tree Certificates and post-quantum certificates&lt;/strong&gt; are the frontier. NIST&apos;s ML-DSA (FIPS 204) and SLH-DSA (FIPS 205) are the standardized post-quantum signatures a certificate will eventually use [@nist-fips-204], and they hit a wall made of bytes.&lt;/p&gt;

At the AES-128 security level, post-quantum signatures dwarf today&apos;s Ed25519 (32-byte key, 64-byte signature). ML-DSA-44 is 1,312 bytes of key and 2,420 of signature; SLH-DSA-128s is a 7,856-byte signature [@cloudflare-pq-2024]. A single TLS handshake carries several signatures -- the leaf, the CA chain, the SCTs -- so post-quantum multiplies the on-the-wire cost and can burst a handshake past what ossified middleboxes tolerate. Merkle Tree Certificates dodge this by batching many certificates under *one* post-quantum signature over a Merkle root and shipping each subscriber a small inclusion proof instead of a full per-certificate signature [@mtc-draft]. It is Certificate Transparency&apos;s logarithmic proof structure, reused as the delivery format. For the migration itself, see the post-quantum posts [here](/blog/post-quantum-cryptography-on-windows-the-thirty-year-migrati) and [here](/blog/the-thirty-year-migration-ships-in-a-pip-install-how-post-qu).
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scheme&lt;/th&gt;
&lt;th&gt;Post-quantum&lt;/th&gt;
&lt;th&gt;Public key (bytes)&lt;/th&gt;
&lt;th&gt;Signature (bytes)&lt;/th&gt;
&lt;th&gt;Note&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Ed25519&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;td&gt;today&apos;s default, the bar to beat&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RSA-2048&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;256&lt;/td&gt;
&lt;td&gt;256&lt;/td&gt;
&lt;td&gt;still common&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ML-DSA-44&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;1,312&lt;/td&gt;
&lt;td&gt;2,420&lt;/td&gt;
&lt;td&gt;the default post-quantum pick (FIPS 204)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FN-DSA-512&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;897&lt;/td&gt;
&lt;td&gt;666&lt;/td&gt;
&lt;td&gt;smallest, but floating-point-dangerous to sign safely&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SLH-DSA-128s&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;td&gt;7,856&lt;/td&gt;
&lt;td&gt;most conservative security, worst size and speed (FIPS 205)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Post-quantum &lt;em&gt;key agreement&lt;/em&gt; is already live -- around 1.8% of Cloudflare&apos;s TLS 1.3 connections in early 2024 -- but post-quantum &lt;em&gt;certificates&lt;/em&gt; are not, and no certificate authority was expected to issue one before 2026 [@cloudflare-pq-2024]. Put the layers side by side and the strategy is legible at a glance.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;What it buys&lt;/th&gt;
&lt;th&gt;Status in 2026&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Short-lived certificates&lt;/td&gt;
&lt;td&gt;Certificates expire in days&lt;/td&gt;
&lt;td&gt;Makes revocation barely matter&lt;/td&gt;
&lt;td&gt;GA January 2026 (Let&apos;s Encrypt)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;47-day validity schedule&lt;/td&gt;
&lt;td&gt;Steps max validity toward 47 days by 2029&lt;/td&gt;
&lt;td&gt;Forces automation Web-wide&lt;/td&gt;
&lt;td&gt;Approved April 2025 (CA/B Forum)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CRLite&lt;/td&gt;
&lt;td&gt;Pushes all revocations, compressed&lt;/td&gt;
&lt;td&gt;Fresh, private, offline withdrawal&lt;/td&gt;
&lt;td&gt;Active (Firefox)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MPIC&lt;/td&gt;
&lt;td&gt;Corroborates issuance from many vantage points&lt;/td&gt;
&lt;td&gt;Defeats BGP-hijack mis-issuance&lt;/td&gt;
&lt;td&gt;Enforcing since 2025-09-15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tiled / Static CT&lt;/td&gt;
&lt;td&gt;Serves the Merkle tree as static tiles&lt;/td&gt;
&lt;td&gt;Cheap, scalable transparency&lt;/td&gt;
&lt;td&gt;Active (Chrome accepts)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Merkle Tree Certificates&lt;/td&gt;
&lt;td&gt;Batches certs under one PQ signature&lt;/td&gt;
&lt;td&gt;Dodges the post-quantum size wall&lt;/td&gt;
&lt;td&gt;Draft (IETF PLANTS WG)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key idea:&lt;/strong&gt; The 2024-2026 frontier is a single move seen from five angles: shrink the trust window so withdrawal barely matters, corroborate issuance so mis-binding is harder, keep every delegation public so lies are visible, and make it all quantum-durable. Notice what is absent from that list -- a better cipher. The math was never the variable. Delegation always was.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Automate issuance with ACME. Adopt short-lived or at most 47-day certificates. Rely on Certificate Transparency plus CRLite and CRLs for withdrawal. Monitor CT for your own domains. Publish CAA records. Do not configure OCSP or Must-Staple. Deploy post-quantum key agreement now and wait on post-quantum certificates.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is the closest reachable approximation to a perfect trust layer. &quot;Closest reachable&quot; carries weight, because some of what stands between here and &lt;em&gt;perfect&lt;/em&gt; is not an engineering backlog. It is a wall.&lt;/p&gt;
&lt;h2&gt;11. The Walls: Theoretical Limits and Open Problems&lt;/h2&gt;
&lt;p&gt;Some of this you cannot out-engineer. Underneath the 2024-2026 machinery are four structural limits, and no ballot or algorithm removes them.&lt;/p&gt;
&lt;p&gt;The first is the deepest. &lt;strong&gt;You cannot prevent a rogue trusted authority; you can only detect one.&lt;/strong&gt; In a system with hundreds of equally powerful roots, the trust of the whole equals the trust of its &lt;em&gt;weakest&lt;/em&gt; member, and nothing in path validation flags a certificate that a trusted authority chose to sign. Certificate Transparency is the field&apos;s formal admission of this ceiling: it does not optimize prevention, because prevention is unavailable under an open many-root model. It optimizes &lt;em&gt;detection latency&lt;/em&gt; [@rfc6962].&lt;/p&gt;
&lt;p&gt;That is the reader&apos;s final turn: after CT and CRLite and short-lived certs, it is tempting to conclude the problem is solved. It is not. The exploit window is shrunk, not closed, and the reason it can never reach zero is structural, not a missing feature.&lt;/p&gt;
&lt;p&gt;The second is that &lt;strong&gt;the soft-fail/hard-fail fork of Section 8 is a structural wall, not an implementation gap.&lt;/strong&gt; No connection-time status check escapes it: an attacker who can block the check defeats soft-fail, and hard-fail turns any responder outage into a self-inflicted denial of service for every dependent site. That is why the OCSP branch was retired rather than repaired [@langley-crlsets-2012].&lt;/p&gt;
&lt;p&gt;The third is the &lt;strong&gt;revocation trilemma&lt;/strong&gt; -- freshness, privacy, and scalability cannot be maximized together by any single online mechanism. This one is &lt;em&gt;practically&lt;/em&gt; resolved by pre-distribution plus short lifetimes, but that is an escape from the trilemma, not a refutation of it; taken together, the historical record and the CRLite result suggest the trilemma is real and the field simply stopped paying its toll [@mozilla-crlite-2020].&lt;/p&gt;
&lt;p&gt;The fourth is that &lt;strong&gt;post-quantum signatures have no free lunch.&lt;/strong&gt; No standardized post-quantum scheme matches Ed25519 on size &lt;em&gt;and&lt;/em&gt; speed &lt;em&gt;and&lt;/em&gt; conservative security at once; the three NIST standards each win a different corner, and the gap to an ideal is large enough that NIST opened a signatures onramp to search for better ones [@cloudflare-pq-2024].An &quot;onramp&quot; is NIST&apos;s term for a supplementary call for new candidate schemes after the main standardization round -- an admission that the standardized set does not yet contain a signature good enough for the certificate use case.&lt;/p&gt;
&lt;p&gt;Around those walls sit the genuinely open problems. Preventing, rather than merely detecting, mis-issuance remains unsolved for the open Web; MPIC hardens one attack path, but the DigiNotar ceiling stands. Certificate Transparency has its own unfinished business: a &lt;em&gt;misbehaving&lt;/em&gt; log can in principle show different trees to different clients -- a split view -- unless clients gossip their observations, and a complete, deployed gossip protocol that defeats split views without leaking privacy is not finished [@rfc9162].&lt;/p&gt;
&lt;p&gt;Fresh, private, complete, &lt;em&gt;and&lt;/em&gt; cheap revocation at full Web scale has no consensus mechanism; CRLite is the best answer and still ships a bounded-staleness dataset [@mozilla-crlite-2020]. The post-quantum size wall is unsolved for the general handshake, which is what Merkle Tree Certificates exist to dodge [@mtc-draft]. And name-constraint completeness over adversarial, worst-case-exponential path-building graphs is still an open corner of the validation problem [@rfc4158].&lt;/p&gt;
&lt;p&gt;Two of these deserve special mention because they are the &lt;em&gt;new&lt;/em&gt; risks the SOTA created. The first is operational: short lifetimes convert &quot;revocation is broken&quot; into &quot;your renewal automation must never fail,&quot; making the ACME pipeline a fresh single point of failure. No named large-scale post-mortem documents this yet; it is a structural consequence of the six-day and 47-day moves, flagged here rather than sourced to an incident [@le-6day-2025].&lt;/p&gt;
&lt;p&gt;The second is not technical at all, and it is the one the whole subject circles back to. I will state it as a judgment rather than a theorem: the hardest limit in PKI is &lt;em&gt;who gets to be a trust anchor and who decides when to remove one&lt;/em&gt;. Root programs at Mozilla, Google, Apple, and Microsoft are the de-facto governors, and there is no technical mechanism that substitutes for that human judgment. The deepest limit is not mathematical. It is organizational, and consolidation makes it more fragile, not less.&lt;/p&gt;
&lt;p&gt;Which means the practical question is never &quot;is PKI secure?&quot; but &quot;am I using it in a way that respects what it can and cannot promise?&quot; Here is how to do exactly that in 2026.&lt;/p&gt;
&lt;h2&gt;12. Using X.509 Correctly in 2026&lt;/h2&gt;
&lt;p&gt;Everything above collapses into one decision procedure and one misuse checklist you can run in a design review. Walk the tree in order.&lt;/p&gt;

flowchart TD
    Start[&quot;Designing certificate handling in 2026&quot;]
    Start --&amp;gt; Q1{&quot;Am I validating certificates myself?&quot;}
    Q1 --&amp;gt;|&quot;yes&quot;| R1[&quot;Use a maintained library, enable strict path validation&quot;]
    Q1 --&amp;gt;|&quot;no, I operate servers&quot;| Q2{&quot;Public or private trust?&quot;}
    Q2 --&amp;gt;|&quot;public&quot;| R2[&quot;Automate with ACME, short-lived or 47-day certs, match SAN not CN&quot;]
    Q2 --&amp;gt;|&quot;private&quot;| R3[&quot;Name-constrained root, enforce EKU, keep off the public path&quot;]
    R2 --&amp;gt; Q3{&quot;Withdrawal or detection?&quot;}
    Q3 --&amp;gt;|&quot;withdrawal&quot;| R4[&quot;CRLite plus CRLs, never OCSP or Must-Staple&quot;]
    Q3 --&amp;gt;|&quot;detection&quot;| R5[&quot;Monitor CT logs, publish CAA records&quot;]
&lt;p&gt;Now the rules behind the tree, with the parameters that matter.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Never hand-roll validation.&lt;/strong&gt; Most of this article&apos;s failure catalog is verifier bugs, and the classic study of non-browser software found certificate validation &quot;completely broken&quot; across a wide range of applications that rolled their own [@georgiev-2012]. Use a maintained library, enable strict path validation, and do not skip Basic Constraints, Key Usage, EKU, or Name Constraints. Treat a critical extension you do not understand as fatal [@rfc5280].&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Match the SAN, not the CN.&lt;/strong&gt; Validate hostnames against &lt;code&gt;subjectAltName&lt;/code&gt;. The legacy &lt;code&gt;commonName&lt;/code&gt;-as-hostname path is deprecated and is the root of classic misuse -- null-prefix names and wildcard abuse [@rfc5280].&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Make revocation a non-problem.&lt;/strong&gt; Automate issuance with ACME and adopt short-lived (six-day) or 47-day-or-shorter certificates so a bad certificate expires on its own [@le-6day-2025]. For the certificates that still live long, rely on CRLite and CRLs [@mozilla-crlite-2020]. Do not deploy OCSP or Must-Staple as new work; the responders are off [@le-ocsp-eol-2025].&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;If you run a private authority, constrain it.&lt;/strong&gt; Issue a name-constrained root so it can &lt;em&gt;only&lt;/em&gt; sign your namespace, enforce EKU, and keep it out of the public trust path [@rfc5280]. This is the deliberate use of the enforcement that public code so often skips.ACME clients -- Certbot, lego, acme.sh, and Caddy&apos;s automatic HTTPS -- are the renewal substrate the 47-day schedule quietly assumes. Choosing and monitoring one is now part of certificate design, not an afterthought.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Authorize issuance with CAA.&lt;/strong&gt; Publish DNS Certification Authority Authorization records naming which authorities may issue for your domain [@rfc8659]. Under MPIC, those records are now checked from multiple network perspectives, so they are harder to spoof [@cabforum-sc067-mpic].&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Detect mis-issuance for your own names.&lt;/strong&gt; Run or subscribe to a Certificate Transparency monitor, reading the logs through the Static CT tiles [@static-ct-api]. This is how you personally exercise the detection that CT makes possible [@chromium-ct-policy].&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Plan for post-quantum without over-rotating.&lt;/strong&gt; Deploy post-quantum key agreement (an ML-KEM hybrid) now, but wait on post-quantum certificates and track the Merkle Tree Certificate and ML-DSA rollout instead [@cloudflare-pq-2024]. There is no reason to ship kilobyte-scale certificate signatures before the format that tames them lands [@mtc-draft].&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Do not pin keys.&lt;/strong&gt; Prefer CT monitoring plus CAA over HPKP-style public-key pinning, which browsers deprecated in 2018 after it proved to be a self-inflicted denial-of-service and ransom risk [@rfc7469].&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Each misuse below maps one-to-one to a named break earlier in this article and to its fix. If you recognize your own system in the left column, you already know which section to reread.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Common misuse&lt;/th&gt;
&lt;th&gt;Maps to which named break&lt;/th&gt;
&lt;th&gt;The fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;Skipping Basic Constraints, accepting a leaf as a CA&lt;/td&gt;
&lt;td&gt;MS02-050, CVE-2021-3450&lt;/td&gt;
&lt;td&gt;Enable strict path validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Matching the CN instead of the SAN&lt;/td&gt;
&lt;td&gt;null-prefix and wildcard abuse&lt;/td&gt;
&lt;td&gt;Match &lt;code&gt;subjectAltName&lt;/code&gt; only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deploying OCSP or Must-Staple as new work&lt;/td&gt;
&lt;td&gt;soft-fail, Must-Staple self-DoS&lt;/td&gt;
&lt;td&gt;Short-lived certs plus CRLite and CRLs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Treating a public CA-signed cert as inherently &quot;safe&quot;&lt;/td&gt;
&lt;td&gt;DigiNotar&lt;/td&gt;
&lt;td&gt;Monitor CT; question the delegation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Long-lived certificates and manual renewal&lt;/td&gt;
&lt;td&gt;Heartbleed mass-revocation cost&lt;/td&gt;
&lt;td&gt;Automate with ACME, 47 days or shorter&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Explicit ECC parameters, hand-rolled crypto&lt;/td&gt;
&lt;td&gt;CurveBall (CVE-2020-0601)&lt;/td&gt;
&lt;td&gt;Maintained libraries, standard curves&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pinning keys with HPKP&lt;/td&gt;
&lt;td&gt;HPKP self-DoS and ransom pinning&lt;/td&gt;
&lt;td&gt;CT monitoring plus CAA&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;Notice that every rule is the same rule wearing different clothes: know whom you are trusting, to say what, and how you would know if they lied. The folklore that gets this wrong is dense, and it causes real incidents. Here are the misconceptions worth dismantling.&lt;/p&gt;
&lt;h2&gt;13. Seven Half-Truths That Cause Real Incidents&lt;/h2&gt;


Historically, mostly not. Online checks soft-fail, which makes them worthless against the man-in-the-middle they target, and they leak your browsing to the certificate authority while adding latency [@langley-crlsets-2012]. The 2026 answer is not better revocation but *short-lived certificates* that dissolve the need [@le-6day-2025] plus *CRLite and CRLs* that push the remaining data offline [@mozilla-crlite-2020]. Do not build new systems on OCSP -- its responders are off [@le-ocsp-eol-2025].


No. The padlock means the connection is encrypted and authenticated to *a* name that passed domain-control validation. It does not mean the operator is honest, the content safe, or the business real -- DigiNotar&apos;s forged Google certificate produced a perfectly closed padlock [@foxit-diginotar]. Whom you are trusting, and to say what, is a separate question the padlock cannot answer.


No. A CA-signed certificate is only as trustworthy as the authority that signed it and the validation that checked it, and DigiNotar&apos;s rogue certificates were fully CA-signed [@wikipedia-diginotar]. For internal use, a *name-constrained* private root can be safer than a public one, because it physically cannot issue outside your namespace [@rfc5280]. &quot;Signed by a CA&quot; is not a synonym for &quot;safe.&quot;


No. Modern validation matches the `subjectAltName` extension, not the Common Name, and CN-as-hostname is deprecated precisely because it was the root of null-prefix and wildcard abuse [@rfc5280]. If your tooling still relies on the CN field for hostname matching, it is running a validation path the standards left behind.


No -- it is a legacy footgun on top of a retired substrate. Must-Staple hard-fails, so any hiccup in obtaining a staple takes your site down [@rfc7633], and it sits on OCSP responders that Let&apos;s Encrypt turned off in August 2025 [@le-ocsp-eol-2025]. Present it historically. Design revocation around short lifetimes and CRLite instead.


No -- it *detects* it. CT makes every issued certificate leave a public, permanent record so a rogue certificate can be *noticed*, but it does not stop the certificate from being issued or trusted [@rfc6962]. Detection, not prevention, is the achievable ceiling for an open, many-root system. A logged rogue certificate is valid until a human gets it distrusted.


Not in the way people assume. This is my read of the evidence rather than a theorem, but Extended Validation buys little practical protection once its distinct browser indicator is gone, and extra intermediate certificates add attack surface rather than safety. Chain *length* is not chain *trustworthiness* -- what matters is whom the chain delegates to, not how many hops it takes.

&lt;p&gt;Every one of these dissolves into the same sentence -- which is the conclusion&apos;s job to deliver, back where the article began.&lt;/p&gt;
&lt;h2&gt;The Trust Was the Bug&lt;/h2&gt;
&lt;p&gt;Go back to that DigiNotar certificate one last time. It was cryptographically perfect. It was trusted by every browser on Earth. And it was a total compromise -- because the question that mattered was never &quot;does this verify?&quot; [@foxit-diginotar]&lt;/p&gt;
&lt;p&gt;Look at the whole catalog with that in mind. DigiNotar, Flame, the 2008 rogue CA, Debian&apos;s guessable keys, MS02-050, CurveBall, OpenSSL&apos;s strict-mode regression, Spooky SSL -- in every single case, the RSA, ECDSA, and SHA math did exactly what it promised [@flame-counter-crypto-2013]. The signatures verified. The hashes hashed. The variable was always &lt;em&gt;delegation&lt;/em&gt;: whom a trusted party vouched for [@cve-2008-0166], what it was allowed to say [@cve-2002-0862], and whether anyone could take the trust back. Not once was the primitive the weak link.&lt;/p&gt;
&lt;p&gt;And read the 2024-2026 answer the same way, because it is one move, not five. The Web could not make certificate authorities honest, so it made them &lt;strong&gt;public&lt;/strong&gt; with Certificate Transparency [@rfc6962], &lt;strong&gt;corroborated&lt;/strong&gt; with multi-perspective issuance [@cabforum-sc067-mpic], and &lt;strong&gt;short-lived&lt;/strong&gt; with the 47-day schedule [@cabforum-sc081-47day] -- shrinking the trust window until a lie expires before it can spread. The engineering did not find a better cipher. It changed what it demanded of delegation.&lt;/p&gt;
&lt;p&gt;That is the same payoff the rest of this series keeps arriving at from different directions. The RSA math was always fine, as Part 1 argued about adversary models. The primitive was never the variable, as Part 3 showed about serialization. Here it is again, in the machinery of trust itself: the cryptography was innocent, and the delegation was guilty.&lt;/p&gt;
&lt;p&gt;So take the one tool this article was built to give you. When the next certificate incident crosses your feed -- and there will be a next one -- do not reach for the algorithm. Reach for the sentence: &lt;em&gt;whom am I trusting, to say what, and how would I know if they lied?&lt;/em&gt; It will sort the break into binding, authorization, withdrawal, or detection before you have finished reading the headline.&lt;/p&gt;
&lt;p&gt;That lens outlives every algorithm transition and every CA/Browser Forum ballot. RSA will yield to ML-DSA; OCSP already yielded to CRLite; 398-day certificates are yielding to 47-day ones. The three questions do not change. Which is exactly why this is Part 4 of a field guide to &lt;em&gt;protocol design&lt;/em&gt;, and not a chapter on parsing certificates.&lt;/p&gt;
&lt;p&gt;&amp;lt;StudyGuide slug=&quot;a-perfect-signature-for-a-certificate-that-should-never-have&quot; keyTerms={[
  { term: &quot;X.509 certificate&quot;, definition: &quot;A signed data structure that binds a public key to a name and usage constraints, verifiable offline by anyone holding the issuer&apos;s key.&quot; },
  { term: &quot;ASN.1 and DER&quot;, definition: &quot;The description language and canonical binary encoding X.509 uses, so a signature covers one unambiguous sequence of bytes.&quot; },
  { term: &quot;Trust anchor and root store&quot;, definition: &quot;A certificate authority key trusted a priori, and the curated set of such keys a browser or OS root program ships.&quot; },
  { term: &quot;Path building (RFC 4158)&quot;, definition: &quot;The graph search that discovers a candidate chain from a leaf certificate up to some trust anchor; worst-case exponential.&quot; },
  { term: &quot;Path validation (RFC 5280 Section 6)&quot;, definition: &quot;The linear state machine that checks one chosen chain: signatures, validity dates, and the authorization extensions.&quot; },
  { term: &quot;Basic Constraints&quot;, definition: &quot;The extension stating whether a key may act as a CA and how many sub-CAs may appear below it.&quot; },
  { term: &quot;Extended Key Usage (EKU)&quot;, definition: &quot;The machine-readable list of purposes a key may serve, such as TLS server authentication or code signing.&quot; },
  { term: &quot;Name Constraints&quot;, definition: &quot;The name subtrees a sub-CA may issue within, intersected down the certification path.&quot; },
  { term: &quot;Certificate Transparency (CT)&quot;, definition: &quot;Public, append-only Merkle-tree logs of every issued certificate; browsers require proof of logging before trusting.&quot; },
  { term: &quot;Signed Certificate Timestamp (SCT)&quot;, definition: &quot;A log&apos;s signed promise to include a certificate, required by browsers as a precondition for trust.&quot; }
]} /&amp;gt;&lt;/p&gt;
</content:encoded><category>x509</category><category>pki</category><category>certificate-transparency</category><category>revocation</category><category>tls</category><category>web-security</category><category>certificate-authorities</category><category>cryptography</category><author>noreply@paragmali.com (Parag Mali)</author></item></channel></rss>