<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: German</title>
    <description>The latest articles on DEV Community by German (@pqbuilder).</description>
    <link>https://dev.to/pqbuilder</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3946071%2F91040bd0-0bb6-434d-961d-890fdc9e109d.jpg</url>
      <title>DEV Community: German</title>
      <link>https://dev.to/pqbuilder</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pqbuilder"/>
    <language>en</language>
    <item>
      <title>Your IoT Devices Will Outlive Your Cryptography</title>
      <dc:creator>German</dc:creator>
      <pubDate>Tue, 09 Jun 2026 13:13:25 +0000</pubDate>
      <link>https://dev.to/pqbuilder/your-iot-devices-will-outlive-your-cryptography-1dnb</link>
      <guid>https://dev.to/pqbuilder/your-iot-devices-will-outlive-your-cryptography-1dnb</guid>
      <description>&lt;p&gt;A smart meter installed today has a 15-year service life. A medical device implanted this year may still be transmitting data in 2040. An industrial sensor bolted into a factory floor will be there long after the engineer who commissioned it has moved on.&lt;/p&gt;

&lt;p&gt;The certificates you issue to those devices today are signed with ECDSA or RSA. Those algorithms are secure against classical computers. They are not secure against a sufficiently large quantum computer running Shor's algorithm. NIST's own timeline puts cryptographically relevant quantum computers within a 10 to 15 year window — which lands squarely inside the service life of the devices you're deploying right now.&lt;/p&gt;

&lt;p&gt;This is not a theoretical concern. It's a lifecycle mismatch.&lt;/p&gt;




&lt;h2&gt;
  
  
  The attack you're not thinking about
&lt;/h2&gt;

&lt;p&gt;The threat model for most IoT security discussions is an attacker who breaks in today. Patch fast, rotate keys, monitor traffic. Classical defenses.&lt;/p&gt;

&lt;p&gt;The threat model that matters for long-lived devices is different: an attacker who captures encrypted traffic today and decrypts it later, once quantum hardware is available. This is called harvest-now-decrypt-later, and it's already happening at the nation-state level. Governments and well-resourced adversaries are archiving encrypted data now, waiting.&lt;/p&gt;

&lt;p&gt;For a medical device transmitting patient vitals, for a smart meter reporting energy consumption, for an industrial sensor logging process data — the data being transmitted today has value in 2035. The signature on the certificate authenticating that device has to be trustworthy in 2035.&lt;/p&gt;

&lt;p&gt;ECDSA won't be.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the migration looks like if you wait
&lt;/h2&gt;

&lt;p&gt;The case for doing this now rather than later is not about fear. It's about cost.&lt;/p&gt;

&lt;p&gt;Every IoT integration you build today on classical cryptography is technical debt with a known expiration date. The migration cost compounds with every device you deploy, every certificate you issue, every integration you build on top of RSA or ECDSA. Migrating a fleet of 10,000 devices in the field is a different problem than migrating a fleet of 100. Migrating when you're under regulatory pressure is a different problem than migrating on your own timeline.&lt;/p&gt;

&lt;p&gt;NIST finalized ML-DSA-65 (FIPS 204) in August 2024. The standard exists. The implementations exist. The only missing piece is production integrations — systems where post-quantum signing is actually running, not in a proof of concept but in a real device fleet with real data.&lt;/p&gt;




&lt;h2&gt;
  
  
  What post-quantum device identity looks like in practice
&lt;/h2&gt;

&lt;p&gt;The pattern for device identity in IoT is well-established: each device gets a certificate issued by a CA the organization controls. The certificate encodes the device's public key, its identity, and its authorized scope. When the device communicates, the certificate proves it belongs to your fleet. When a device is compromised or decommissioned, you revoke its certificate and the revocation propagates.&lt;/p&gt;

&lt;p&gt;This pattern doesn't change with post-quantum cryptography. What changes is the algorithm underneath — ML-DSA-65 instead of ECDSA, with key sizes and signature sizes that reflect the difference in security model.&lt;/p&gt;

&lt;p&gt;ML-DSA-65 public keys are 1952 bytes. Signatures are 3309 bytes. X.509 certificates come out around 5.5KB DER. These are larger than classical equivalents — worth accounting for in constrained environments, but not prohibitive for most IoT backends and gateways.&lt;/p&gt;




&lt;h2&gt;
  
  
  A concrete integration with FIPSign
&lt;/h2&gt;

&lt;p&gt;FIPSign is a post-quantum signing API built on ML-DSA-65 (NIST FIPS 204). It runs a private CA per project — you issue certificates to your devices, verify them, and revoke them via REST API or SDK. No infrastructure to manage.&lt;/p&gt;

&lt;p&gt;There are two distinct guarantees you can establish for an IoT device:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CA certificates&lt;/strong&gt; answer the question &lt;em&gt;"does this device belong to my fleet?"&lt;/em&gt; — identity at the device level, established once at provisioning time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signed tokens&lt;/strong&gt; answer the question &lt;em&gt;"was this specific reading transmitted by this device, at this exact time, unaltered?"&lt;/em&gt; — data integrity at the transmission level, established on every message.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For many systems, the certificate alone is sufficient. For systems where individual data points have legal, billing, or compliance consequences — smart meters, medical devices, industrial sensors — you want both. The certificate proves the device is legitimate. The signature on each transmission creates a tamper-proof audit trail of every data point it ever sent.&lt;/p&gt;

&lt;p&gt;Here's what that looks like in practice:&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1 — Generate a key pair at provisioning time
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PQAuth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;generateKeyPair&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fipsign-sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fipsign&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PQAuth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FIPSIGN_API_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Generate ML-DSA-65 key pair&lt;/span&gt;
&lt;span class="c1"&gt;// The secret key stays on the device — never sent to the server&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;publicKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secretKey&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generateKeyPair&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2 — Issue a certificate from your CA
&lt;/h3&gt;

&lt;p&gt;Your provisioning backend receives the device's public key and issues a certificate that encodes its identity and authorized scope:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Provisioning backend — not the device itself&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;certificate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;meta&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fipsign&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;device-serial-00123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;publicKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="nx"&gt;devicePublicKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;expiresInSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;365&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// 5 years&lt;/span&gt;
  &lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;deviceType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;smart-meter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;firmwareVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2.1.4&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;grid-zone-7&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// Store meta.certId — needed for revocation&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The certificate is signed by your CA with ML-DSA-65. Any backend in your infrastructure can verify it offline against the CA root — no network call required.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3 — The device signs each transmission
&lt;/h3&gt;

&lt;p&gt;When the device sends a reading, it signs the payload. Each signature is cryptographic proof that this specific value was transmitted by this specific device at this specific time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// On the device, at transmission time&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fipsign&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;device-serial-00123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;reading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;47.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;kWh&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;firmwareHash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;8f4a...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// token travels with the transmission&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where the two mechanisms work together. The certificate establishes that device-serial-00123 belongs to your fleet. The signed token establishes that this 47.3 kWh reading came from that device unaltered. If there is ever a billing dispute or a regulatory audit, you can produce both: proof of device identity and proof of data integrity, cryptographically bound, resistant to tampering even with quantum hardware.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4 — Your backend verifies the transmission
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Data ingestion pipeline&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fipsign&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Reject — signature failed or token was tampered&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// payload.sub is the device ID&lt;/span&gt;
&lt;span class="c1"&gt;// payload contains the original reading, unmodified&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 5 — Revoke when needed
&lt;/h3&gt;

&lt;p&gt;A device is stolen, decommissioned, or shows anomalous behavior:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fipsign&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;revokeCert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;certId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;device-reported-stolen&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From this point the device's certificate is invalid. Its identity is revoked across your entire fleet. Any future verification against that certificate returns revoked status immediately.&lt;/p&gt;

&lt;p&gt;Note that if you only need data integrity without fleet identity — simpler deployments, internal systems, early-stage integrations — &lt;code&gt;sign()&lt;/code&gt; and &lt;code&gt;verify()&lt;/code&gt; alone are sufficient. The CA is the layer you add when you need a verifiable chain of custody from device identity down to individual data points.&lt;/p&gt;




&lt;h2&gt;
  
  
  The CA feature and X.509 compatibility
&lt;/h2&gt;

&lt;p&gt;FIPSign's private CA supports two certificate formats:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PQCert&lt;/strong&gt; — a JSON certificate format native to FIPSign. Compact, easy to parse, embeds arbitrary metadata fields. Good for systems you control end-to-end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;X.509&lt;/strong&gt; — standard DER/PEM format with ML-DSA-65 as the signature algorithm. Compatible with existing PKI tooling that can handle the algorithm. Good for systems that need to interoperate with broader infrastructure.&lt;/p&gt;

&lt;p&gt;Both formats use ML-DSA-65. The choice depends on whether you need X.509 interoperability or prefer the simplicity of JSON.&lt;/p&gt;




&lt;h2&gt;
  
  
  The lifecycle question
&lt;/h2&gt;

&lt;p&gt;The hardest part of post-quantum IoT is not the cryptography — it's the lifecycle math.&lt;/p&gt;

&lt;p&gt;A certificate issued today with a 5-year expiry will still be in use in 2031. The devices holding that certificate may still be in the field in 2040. The data those devices signed in 2026 may be relevant in an audit in 2035.&lt;/p&gt;

&lt;p&gt;Every one of those dates falls inside the window where classical cryptography becomes a liability. ML-DSA-65 certificates issued today are designed to remain trustworthy across that window.&lt;/p&gt;

&lt;p&gt;The migration cost of getting this right now is an afternoon of integration work. The migration cost of getting it wrong later is measured in device fleets, field operations, and regulatory timelines.&lt;/p&gt;




&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;FIPSign has a free tier — 10,000 tokens per month, no credit card. The private CA is available on all plans. The JS/TS and Python SDKs are on npm and PyPI. There's also an MCP server if you want to prototype the integration directly from Claude.&lt;/p&gt;

&lt;p&gt;Start at &lt;a href="https://fipsign.dev" rel="noopener noreferrer"&gt;fipsign.dev&lt;/a&gt;. The guide walks through the CA setup and the first certificate issuance end-to-end.&lt;/p&gt;

&lt;p&gt;The devices you deploy this year will still be running your certificates in a decade. Make sure those certificates are still trustworthy when they get there.&lt;/p&gt;

</description>
      <category>security</category>
      <category>typescript</category>
      <category>javascript</category>
      <category>iot</category>
    </item>
    <item>
      <title>Your AI agent's audit trail is not evidence. Here's what makes it one.</title>
      <dc:creator>German</dc:creator>
      <pubDate>Sun, 07 Jun 2026 17:56:49 +0000</pubDate>
      <link>https://dev.to/pqbuilder/your-ai-agents-audit-trail-is-not-evidence-heres-what-makes-it-one-32f7</link>
      <guid>https://dev.to/pqbuilder/your-ai-agents-audit-trail-is-not-evidence-heres-what-makes-it-one-32f7</guid>
      <description>&lt;p&gt;&lt;em&gt;This post builds on a conversation that started in &lt;a href="https://dev.to/tacoda/the-harness-stack-4a7d"&gt;Ian Johnson's harness stack thread&lt;/a&gt; and the research being published by &lt;a href="https://dev.to/zep1997"&gt;@zep1997&lt;/a&gt; in the Self-Correcting Systems series. The problem is real and I've been building in this space.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;CLAIM-24 tested whether an agent checks a grant against current source conditions, not just the clock.&lt;/p&gt;

&lt;p&gt;CLAIM-25 tested whether a signed response is both authentic and fresh — signed-AND-fresh, not just signed.&lt;/p&gt;

&lt;p&gt;CLAIM-26 tests what happens after the action is taken: can an auditor reconstruct exactly what authority justified it?&lt;/p&gt;

&lt;p&gt;The finding: a log that says &lt;code&gt;ALLOW&lt;/code&gt; is not evidence. An authority event written after the action, even with matching hashes, is reconstruction — not prior authorization. The gap between a &lt;code&gt;SeparateWriteGate&lt;/code&gt; (5/7) and a &lt;code&gt;PairedAuthorityActionGate&lt;/code&gt; (7/7) is two failure modes that look clean but aren't: &lt;code&gt;post_hoc&lt;/code&gt; and &lt;code&gt;audit_gap&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That's the structural boundary. But there's a layer below it that the CLAIM-26 findings leave open:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What makes the authority event itself tamper-proof? &lt;code&gt;is_immutable: true&lt;/code&gt; in a JSON field is self-assertion.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The missing piece: cryptographic weight
&lt;/h2&gt;

&lt;p&gt;The minimum audit-safe shape from CLAIM-26 looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"authority_event_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"auth-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"grant_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"grant-abc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"decision"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ALLOW"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"snapshot_hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sha256:policy_v21_sequence_42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source_sequence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"is_immutable"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"written_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-06-06T12:00:01Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;is_immutable: true&lt;/code&gt; field tells you nothing. Any system can write that field. Any system can modify the record and rewrite it. The audit trail is internally consistent — and externally unverifiable.&lt;/p&gt;

&lt;p&gt;What closes this: a cryptographic signature over the authority event, issued by an external authority the agent cannot write to.&lt;/p&gt;

&lt;p&gt;Not just any signature. For audit trails that need to survive regulatory scrutiny years from now — or more concretely, the harvest-now-decrypt-later threat where adversaries are recording signed traffic today — ECDSA and RSA signatures have a known expiry date. Shor's algorithm breaks both. The NIST finalized the replacements in August 2024: ML-DSA-65, NIST FIPS 204.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this looks like in practice
&lt;/h2&gt;

&lt;p&gt;The conversation in Ian's thread landed on a three-layer architecture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;L1&lt;/strong&gt;: Memory carries authority metadata — &lt;code&gt;governs.action_types&lt;/code&gt;, &lt;code&gt;verification_required&lt;/code&gt;, &lt;code&gt;resource_sensitivity&lt;/code&gt;. This is the schema zep1997 is building: structured claims about what a memory is authorized to govern.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;L2&lt;/strong&gt;: Actions are signed against a certificate. Each agent action produces a signed token — &lt;code&gt;sub: "agent_id"&lt;/code&gt;, scoped to the specific operation, short TTL, revocable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;L3&lt;/strong&gt;: A CA issues certificates per agent, fleet-wide. Not just a token for the action, but a certificate that encodes what action classes this agent is authorized to perform.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The certificate doesn't replace the L1 schema. It becomes the issuing authority for it. &lt;code&gt;governs.action_types: ["execute", "write"]&lt;/code&gt; is a claim. The certificate makes that claim verifiable and gives it a revocation path.&lt;/p&gt;

&lt;p&gt;Here's what that looks like with actual code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PQAuth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;generateKeyPair&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fipsign-sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fipsign&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PQAuth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pqa_your_api_key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// At agent provisioning time: issue a certificate that encodes&lt;/span&gt;
&lt;span class="c1"&gt;// what action classes this agent is authorized to perform&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;publicKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secretKey&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generateKeyPair&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;certificate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;meta&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fipsign&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;agent_summarizer_v2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;publicKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;expiresInSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// 30 days&lt;/span&gt;
  &lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;action_types&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;read&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;summarize&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;resource_sensitivity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;low&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;verification_required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// Store certificate and secretKey on the agent&lt;/span&gt;
&lt;span class="c1"&gt;// meta.certId is what you use for revocation&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;certId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// cert_...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At runtime, when the agent takes an action, it signs the action event:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Agent signs the authority event before taking the action&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fipsign&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;              &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;agent_summarizer_v2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;document:summarize&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;authority_cert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;certId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;snapshot_hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256:policy_v21_sequence_42&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;source_sequence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;grant_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;grant-abc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;expiresInSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// short TTL — this is evidence of this specific action&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// token is the authority_event with a quantum-resistant signature&lt;/span&gt;
&lt;span class="c1"&gt;// Store it alongside the action record — this is what CLAIM-26 calls for&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The authority event is now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"authority_event_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"auth-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"grant_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"grant-abc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"decision"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ALLOW"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"snapshot_hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sha256:policy_v21_sequence_42"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source_sequence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"payload"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"eyJzdWIiOiJhZ2..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"signature"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"oi5UKsTn..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"algorithm"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ML-DSA-65"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"issuedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1749254401&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;is_immutable: true&lt;/code&gt; is gone. The signature is the immutability guarantee. Any modification to the payload — snapshot_hash, source_sequence, grant_id — invalidates the signature. An auditor verifying this event doesn't need to trust the storage system. They verify the signature against the public key, which they can fetch independently.&lt;/p&gt;




&lt;h2&gt;
  
  
  The revocation piece
&lt;/h2&gt;

&lt;p&gt;CLAIM-26 notes that write order matters: authority written before or atomically with the action, not after.&lt;/p&gt;

&lt;p&gt;The signing model gives you something stronger: the token has a &lt;code&gt;issuedAt&lt;/code&gt; timestamp that's part of the signed payload. You cannot backdate a signature. If the authority event was signed at &lt;code&gt;12:00:01&lt;/code&gt; and the action record shows &lt;code&gt;12:00:02&lt;/code&gt;, you have cryptographic proof of the ordering — not just a timestamp field that any process could have written.&lt;/p&gt;

&lt;p&gt;But what if the agent is compromised mid-session? zep1997 and I worked through the lazy/eager revocation split in the thread:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lazy revocation&lt;/strong&gt; — the certificate remains valid but the gate checks it against the CRL before authorizing each action. If the certificate has been revoked, the action is blocked. No out-of-band channel needed — the next gate check catches it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before any action: check the CRL&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;crl&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fipsign&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCrl&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fipsign&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isCertRevoked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;certId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;crl&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Agent certificate has been revoked — action blocked&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Then sign the action&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fipsign&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;agent_summarizer_v2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;document:summarize&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Eager revocation&lt;/strong&gt; — required when the agent has long sessions and the revoked scope covers high-sensitivity action classes. The window between "certificate revoked" and "next gate check" is too large to accept. In this case, the agent controller needs to be notified out-of-band and flush the agent context immediately. This requires a separate notification mechanism in your own infrastructure — a message queue, an internal event bus, or a polling loop against &lt;code&gt;ca.getCert()&lt;/code&gt; for real-time status.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Real-time status check for a specific certificate (free, no token cost)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;fipsign&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;certId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;revoked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Flush agent context immediately — eager path&lt;/span&gt;
  &lt;span class="nf"&gt;flushAgentContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;agentId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lazy is sufficient for most cases. Eager becomes necessary when session length and action sensitivity combine to create a window too large to accept.&lt;/p&gt;




&lt;h2&gt;
  
  
  The governs field maps to certificate scope
&lt;/h2&gt;

&lt;p&gt;The insight from the thread: &lt;code&gt;governs.action_types&lt;/code&gt; in the memory schema is exactly the field a certificate would sign over. The certificate doesn't replace that schema — it verifies it.&lt;/p&gt;

&lt;p&gt;A memory that self-asserts &lt;code&gt;action_types: ["execute", "write"]&lt;/code&gt; without external verification is what CLAIM-22 flagged: 3/3 false-certainty errors on mislabeled scenarios. The metadata is the authority and the assertion simultaneously.&lt;/p&gt;

&lt;p&gt;Certificate-scoped retrieval changes the architecture: before ranking by relevance, the retriever filters by certificate scope. The question becomes not "which memory is most relevant to this query" but "which memories are authorized for this action class, and of those, which is most relevant." Relevance becomes a tiebreaker within an authorized set.&lt;/p&gt;

&lt;p&gt;The missing verification layer — what moves from self-asserted to externally verifiable — is the certificate. And for that certificate to mean anything a decade from now, the signing algorithm needs to be post-quantum.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this is and isn't
&lt;/h2&gt;

&lt;p&gt;This is not a production trust model. It's the layer that closes the gap between "the log says ALLOW" and "the log proves it was authorized before it happened, by an agent that was authorized to perform that action class, with a signature that cannot be forged."&lt;/p&gt;

&lt;p&gt;The open questions from CLAIM-26 remain open at this layer too:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What storage substrate holds the high-water mark for replay protection?&lt;/li&gt;
&lt;li&gt;What canonicalization scheme covers the full authority event?&lt;/li&gt;
&lt;li&gt;How does multi-source authority work when the action depends on multiple policy registries?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are next layers. The claim here is narrower: &lt;code&gt;is_immutable: true&lt;/code&gt; is self-description. A quantum-resistant signature over the authority event is evidence.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The CA and signing API used in this post is &lt;a href="https://fipsign.dev" rel="noopener noreferrer"&gt;FIPSign&lt;/a&gt; — ML-DSA-65, NIST FIPS 204, free tier. The JS/TS SDK (&lt;code&gt;fipsign-sdk&lt;/code&gt; on npm) and Python SDK (&lt;code&gt;fipsign-sdk&lt;/code&gt; on PyPI) implement the full lifecycle shown above. If you're building in this space and want to run the packet against a real external source — which is what CLAIM-24 was waiting on — drop a note here or open an issue on the &lt;a href="https://github.com/tacoda/keystone/issues/3" rel="noopener noreferrer"&gt;Keystone repo&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>security</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>Your AI agents are authorized by vibes. Here's how to fix that.</title>
      <dc:creator>German</dc:creator>
      <pubDate>Tue, 02 Jun 2026 18:58:26 +0000</pubDate>
      <link>https://dev.to/pqbuilder/your-ai-agents-are-authorized-by-vibes-heres-how-to-fix-that-1l41</link>
      <guid>https://dev.to/pqbuilder/your-ai-agents-are-authorized-by-vibes-heres-how-to-fix-that-1l41</guid>
      <description>&lt;p&gt;The AI agent security community has been converging on a problem. A researcher recently ran an experiment — feeding a memory-retrieval framework 10 scenarios involving certificate operations: signing, issuing, revoking, delegating. The system retrieved the right memory 8 out of 10 times. It matched the external authorization gate 7 out of 10.&lt;/p&gt;

&lt;p&gt;The conclusion: metadata per item isn't enough. You need a separate authorization gate over the proposed operation.&lt;/p&gt;

&lt;p&gt;That conclusion is correct. But I want to show what that gate actually looks like when you build it — because the primitive already exists, and it's older than LLMs.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem is authorization, not retrieval
&lt;/h2&gt;

&lt;p&gt;Most agent frameworks today invest in memory and observability. The agent can recall what it did before. You can see what tools it called. Logs, traces, dashboards.&lt;/p&gt;

&lt;p&gt;What they don't have is a cryptographically enforced answer to the question: &lt;strong&gt;was this agent authorized to do this, before it did it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Those are different problems. Retrieval tells you what the agent remembers about its permissions. Authorization tells you what it was actually granted — signed, tamper-proof, at dispatch time.&lt;/p&gt;

&lt;p&gt;An agent that retrieves "I have revocation permissions" from memory and then revokes a certificate it shouldn't touch is not an authorization failure at the retrieval layer. It's an authorization failure at the gate layer — because there was no gate.&lt;/p&gt;




&lt;h2&gt;
  
  
  Certificates are that gate
&lt;/h2&gt;

&lt;p&gt;A certificate is a signed declaration of what an entity is authorized to do. Issued once, verifiable offline in ~1ms, revocable instantly. We've used them for TLS, for IoT devices, for code signing. The same primitive works for agents.&lt;/p&gt;

&lt;p&gt;The model is simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Orchestrator issues a certificate at dispatch time&lt;/li&gt;
&lt;li&gt;The certificate carries the agent's identity and its exact scope in &lt;code&gt;meta&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Every tool call goes through a gate that verifies the certificate offline&lt;/li&gt;
&lt;li&gt;On completion — or abort, or timeout — the orchestrator revokes it
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Orchestrator — dispatch&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;certificate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;meta&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="s2"&gt;`agent_payment_processor_run_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;runId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;publicKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="nx"&gt;agentPublicKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;expiresInSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;action_types&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;execute&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;write&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;resource_scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;payments:process&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;resource_sensitivity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;verification_required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;max_amount_usd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The certificate is signed with ML-DSA-65. Any attempt to modify &lt;code&gt;meta&lt;/code&gt; after issuance invalidates the signature. The agent cannot promote its own scope.&lt;/p&gt;




&lt;h2&gt;
  
  
  The gate — offline, zero tokens, ~1ms
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifyAgentScope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;certificate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requiredScope&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Signature + expiry — offline, no API call&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;cert&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verifyCert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;certificate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rootCert&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Gate: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;meta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

  &lt;span class="c1"&gt;// Action type check&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action_types&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requiredScope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`Gate: action "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;requiredScope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" not in scope [&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action_types&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;]`&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Sensitivity ceiling&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;LEVELS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;low&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;medium&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;high&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;critical&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;LEVELS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;requiredScope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sensitivity&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;LEVELS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resource_sensitivity&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Gate: sensitivity ceiling exceeded`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;meta&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every tool call, before execution:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;processPendingPayments&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;certificate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;verifyAgentScope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;certificate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;execute&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;sensitivity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;high&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// Gate passed — proceed&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;paymentService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;processNext&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the certificate doesn't authorize this action, the gate throws before any external call is made. Not after. Not in the logs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Revocation closes the window
&lt;/h2&gt;

&lt;p&gt;Short-lived certificates handle the happy path. Eager revocation handles everything else.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;runAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;certificate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;certId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;dispatchAgent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;certificate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Always — success, exception, or timeout&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;revokeCert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;certId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;run complete&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;finally&lt;/code&gt; block is not optional. If the agent is compromised mid-run, the orchestrator can call &lt;code&gt;revokeCert()&lt;/code&gt; immediately — the next gate check on the compromised agent fails instantly.&lt;/p&gt;

&lt;p&gt;This is what the memory-retrieval approach can't do. You can't retroactively un-retrieve a memory. You can revoke a certificate.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this solves from the packet experiment
&lt;/h2&gt;

&lt;p&gt;Going back to the experiment that opened this post — the 4 cases that didn't match the external gate:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Excessive delegation&lt;/strong&gt; — a &lt;code&gt;delegate_depth&lt;/code&gt; field in &lt;code&gt;meta&lt;/code&gt;, decremented on each sub-agent issue. When it reaches 0, the gate blocks delegation. The agent cannot grant itself deeper delegation than it was given.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Issue without verification&lt;/strong&gt; — &lt;code&gt;verification_required: true&lt;/code&gt; in &lt;code&gt;meta&lt;/code&gt;. The gate blocks execution until a human approves. The approval request carries the certificate ID, the scope, and the expiry — everything needed to make an informed decision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ambiguous batch&lt;/strong&gt; — the gate runs once per operation, not once per batch. Each item in the batch hits the same gate. No ambiguity about whether the scope covers "some items" or "all items."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Revocation by project&lt;/strong&gt; — &lt;code&gt;resource_scope: "certs:own"&lt;/code&gt; vs &lt;code&gt;"certs:*"&lt;/code&gt;. The gate checks the scope string. An agent with &lt;code&gt;certs:own&lt;/code&gt; cannot revoke a certificate it didn't issue, regardless of what it retrieves from memory.&lt;/p&gt;

&lt;p&gt;In every case, the fix is the same: enforce at the gate, not at the retrieval layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  The post-quantum angle
&lt;/h2&gt;

&lt;p&gt;The signing infrastructure matters here. If you build this on RSA or ECDSA, you're building on algorithms that quantum computers will break. NIST finalized ML-DSA-65 (FIPS 204) in August 2024 — the certificate signatures in the examples above use that standard.&lt;/p&gt;

&lt;p&gt;Building the authorization layer now means building it on algorithms that hold when the threat materializes. The migration cost later is significantly higher than doing it right today.&lt;/p&gt;




&lt;p&gt;FIPSign is the API I built for this — post-quantum certificates, signing, and revocation as a service. The Private CA feature is what powers the agent authorization model above. JS/TS and Python SDKs available. The gate itself costs zero tokens — only issuance and revocation are charged.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The authorization gap in agent systems is a solvable problem with existing primitives. If you're working on agent security and want to discuss the architecture — especially the delegation chain problem — drop a comment.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>webdev</category>
      <category>agents</category>
    </item>
    <item>
      <title>The Missing Layer in AI Agent Security: Auditability Is Not Observability</title>
      <dc:creator>German</dc:creator>
      <pubDate>Tue, 02 Jun 2026 12:47:06 +0000</pubDate>
      <link>https://dev.to/pqbuilder/the-missing-layer-in-ai-agent-security-auditability-is-not-observability-5e5p</link>
      <guid>https://dev.to/pqbuilder/the-missing-layer-in-ai-agent-security-auditability-is-not-observability-5e5p</guid>
      <description>&lt;p&gt;The other day someone proposed a taxonomy for AI agent configuration — five layers, from the model itself to fleet orchestration. It was the cleanest framing I'd seen of how agents are structured. But something was missing from every layer.&lt;/p&gt;

&lt;p&gt;Nobody had named it yet.&lt;/p&gt;




&lt;h2&gt;
  
  
  Observability tells you what happened. Auditability tells you it's true.
&lt;/h2&gt;

&lt;p&gt;Most agent frameworks today have observability. You can see what the agent did, what tools it called, what outputs it produced. Logs, traces, dashboards.&lt;/p&gt;

&lt;p&gt;What they don't have is auditability — a cryptographic guarantee that the record is authentic and was not altered after the fact.&lt;/p&gt;

&lt;p&gt;These are not the same thing. And the difference matters more than most teams realize.&lt;/p&gt;




&lt;h2&gt;
  
  
  The scenario that makes it concrete
&lt;/h2&gt;

&lt;p&gt;Imagine an agent that handles financial operations. It reads account data, evaluates conditions, and approves or rejects transfers automatically.&lt;/p&gt;

&lt;p&gt;Your observability stack tells you the agent approved a $47,000 transfer at 3:42 AM. The log is right there.&lt;/p&gt;

&lt;p&gt;But can you prove it?&lt;/p&gt;

&lt;p&gt;Can you prove that log wasn't written after the fact? Can you prove the agent that approved it was the agent you deployed — and not a compromised version running with altered instructions? Can you prove the action wasn't injected by a malicious prompt that slipped through your guardrails?&lt;/p&gt;

&lt;p&gt;Observability says "this happened." Auditability says "this happened, here is the cryptographic proof, and it cannot be disputed."&lt;/p&gt;

&lt;p&gt;In financial systems, healthcare, legal workflows, or anywhere agent outputs have real consequences — the difference between those two statements is the difference between a log and evidence.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this gap exists
&lt;/h2&gt;

&lt;p&gt;Agent frameworks are built around capability and reliability. Can the agent do the task? Does it do it consistently? Those are the questions that dominate the design space right now.&lt;/p&gt;

&lt;p&gt;Security is treated as a layer you add on top. And auditability — the ability to prove what happened — isn't even in the conversation yet.&lt;/p&gt;

&lt;p&gt;Researchers working on agent memory authorization are running into the same root problem from a different angle. Their question is whether the memory the agent retrieved was authorized to govern the action it took. My question is whether the action the agent took can be proven authentic after the fact. Both gaps share the same absence: no authority chain attached to the action.&lt;/p&gt;

&lt;p&gt;The agent acted. But nothing signed for it.&lt;/p&gt;




&lt;h2&gt;
  
  
  What auditability actually looks like
&lt;/h2&gt;

&lt;p&gt;The model is straightforward. Every agent action produces a signed token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;agent_payment_processor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;approve_transfer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="mi"&gt;47000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;acct_8821&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;expiresInSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The token is cryptographic proof that this specific agent performed this specific action at this specific time. If the token verifies, the action is authentic. If it doesn't verify, something was tampered with or the agent was compromised.&lt;/p&gt;

&lt;p&gt;Revocation is the other half. If the agent is compromised, you revoke its credentials instantly — not wait for tokens to expire. Every subsequent verify call rejects immediately.&lt;/p&gt;

&lt;p&gt;This gives you two things observability cannot:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tamper-evident records&lt;/strong&gt; — the action log cannot be altered after the fact without breaking the signatures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Instant credential revocation&lt;/strong&gt; — when something looks wrong, you pull the agent's credentials and it stops being trusted immediately, not in five minutes when the token expires.&lt;/p&gt;




&lt;h2&gt;
  
  
  The post-quantum angle
&lt;/h2&gt;

&lt;p&gt;The signing infrastructure matters. If you build agent auditability on RSA or ECDSA today, you're building on algorithms that quantum computers will break within this decade. NIST finalized post-quantum standards in August 2024 — ML-DSA-65 (FIPS 204) is the signing standard.&lt;/p&gt;

&lt;p&gt;Building auditability now means building it on algorithms that will still hold when the threat materializes. The migration cost later is much higher than doing it right today.&lt;/p&gt;

&lt;p&gt;FIPSign is the API I built for exactly this — post-quantum signing, verification, and revocation as a service. An agent is just another &lt;code&gt;sub&lt;/code&gt;. No changes to your existing architecture. The auditability layer sits on top of whatever you're already running. JS/TS and Python SDKs available.&lt;/p&gt;




&lt;h2&gt;
  
  
  The question worth asking
&lt;/h2&gt;

&lt;p&gt;Most teams deploying agents in production today can answer "what did the agent do?"&lt;/p&gt;

&lt;p&gt;Fewer can answer "can you prove it?"&lt;/p&gt;

&lt;p&gt;If your agents are making decisions with real consequences — financial, legal, medical, operational — that second question is coming. The teams that answer it now will have the compliance story when the audit arrives. The teams that don't will be rebuilding their auditability layer under pressure.&lt;/p&gt;

&lt;p&gt;The infrastructure exists today. The threat model is real. The migration is cheap now and expensive later.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you're thinking about agent auditability in your stack, I'd genuinely like to hear how you're approaching it. Drop a comment — especially if you've run into the detection latency problem that makes revocation timing tricky.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>agentaichallenge</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why token revocation matters — and why JWT can't do it</title>
      <dc:creator>German</dc:creator>
      <pubDate>Sat, 30 May 2026 13:50:13 +0000</pubDate>
      <link>https://dev.to/pqbuilder/why-token-revocation-matters-and-why-jwt-cant-do-it-jib</link>
      <guid>https://dev.to/pqbuilder/why-token-revocation-matters-and-why-jwt-cant-do-it-jib</guid>
      <description>&lt;p&gt;JWT has a design problem that most developers don't think about until it bites them.&lt;/p&gt;

&lt;p&gt;Once you issue a JWT, you can't take it back.&lt;/p&gt;

&lt;p&gt;The token is valid until it expires. There's no built-in mechanism to say "this token is no longer valid, reject it." The signature is cryptographically correct, the expiry hasn't passed — and yet you need it to stop working right now.&lt;/p&gt;

&lt;p&gt;This isn't a bug. It's a deliberate tradeoff in JWT's stateless design. But it's a tradeoff with real consequences.&lt;/p&gt;




&lt;h2&gt;
  
  
  The scenarios where this actually hurts
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;User logs out.&lt;/strong&gt; The client discards the token, but the token is still cryptographically valid. If someone intercepted it — through a compromised device, a browser extension, a network log — they can still use it until it expires. You have no way to stop them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Order cancelled.&lt;/strong&gt; You signed a payment intent with a 5-minute expiry. The user cancels before paying, but the token is still valid for the next 4 minutes and 50 seconds. Anyone holding that token can still attempt to complete the transaction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Employee offboarded.&lt;/strong&gt; You revoke their account in your database, but they still have a valid JWT in their API client. Depending on your expiry policy, they might have hours or days of continued access.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Account compromised.&lt;/strong&gt; You detect suspicious activity and want to lock down the account immediately. You can update a flag in your database, but if you're doing stateless JWT verification, you're not hitting the database on every request — that's the whole point of stateless tokens.&lt;/p&gt;




&lt;h2&gt;
  
  
  The workarounds developers use (and their problems)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Short-lived tokens with refresh tokens.&lt;/strong&gt; Set expiry to 15 minutes. Issue a refresh token to get a new access token. This limits the blast radius — a stolen token is only valid for 15 minutes. But it doesn't solve the problem, it just shrinks the window. And it adds complexity: you now have two token types, a refresh endpoint, rotation logic, and a client that needs to handle token refresh transparently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Token blacklist in Redis or a database.&lt;/strong&gt; On every verify, check if the token's JTI (JWT ID) is in a blocklist. This works, but you've now reintroduced the stateful system you were trying to avoid. Every verify call hits Redis. You need to manage Redis availability, replication, and eviction policies. You've essentially built your own revocation system on top of a stateless protocol.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Very short expiry without refresh.&lt;/strong&gt; Set expiry to 60 seconds. Forces frequent re-authentication. Users hate it. The UX is terrible. Only viable for specific use cases like single-use links.&lt;/p&gt;

&lt;p&gt;None of these are wrong — they're widely used in production. But they're all workarounds for a missing primitive.&lt;/p&gt;




&lt;h2&gt;
  
  
  What proper revocation looks like
&lt;/h2&gt;

&lt;p&gt;Revocation should be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Immediate&lt;/strong&gt; — once revoked, the token is rejected on the next verify call, not eventually&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cryptographically tied&lt;/strong&gt; — the revocation entry must be tied to the specific token, not just the user or a JTI that can be forged&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automatic cleanup&lt;/strong&gt; — revocation entries should expire when the original token would have expired, with no manual cleanup needed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Idempotent&lt;/strong&gt; — revoking a token twice should return success without double-charging or erroring&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rejection of expired tokens&lt;/strong&gt; — a token that's already expired shouldn't be revokable — that would be revoking something that's already invalid&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How FIPSign handles revocation
&lt;/h2&gt;

&lt;p&gt;FIPSign is built on ML-DSA-65 (NIST FIPS 204) — the post-quantum digital signature standard. The token structure includes a &lt;code&gt;payload&lt;/code&gt; (base64 JSON), a &lt;code&gt;signature&lt;/code&gt; (ML-DSA-65 over the payload), and metadata.&lt;/p&gt;

&lt;p&gt;When you revoke a token, the server:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Verifies the ML-DSA-65 signature — confirms the token is genuine and hasn't expired&lt;/li&gt;
&lt;li&gt;Computes SHA-256 of the full signature and stores it in a revocation table&lt;/li&gt;
&lt;li&gt;Returns the revocation timestamp, the token's &lt;code&gt;sub&lt;/code&gt;, and the original expiry
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;revoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user logged out&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// result.success   → true&lt;/span&gt;
&lt;span class="c1"&gt;// result.revokedAt → Unix timestamp&lt;/span&gt;
&lt;span class="c1"&gt;// result.sub       → "user_123"&lt;/span&gt;
&lt;span class="c1"&gt;// result.expiresAt → original token expiry&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On every subsequent &lt;code&gt;verify()&lt;/code&gt; call, the server checks the revocation table before returning a result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// valid: false&lt;/span&gt;
&lt;span class="c1"&gt;// error: "Token has been revoked"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few properties worth noting:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cryptographically bound.&lt;/strong&gt; The revocation entry is keyed on a SHA-256 hash of the ML-DSA-65 signature — not a JTI that could be replicated, not the user ID. Two tokens for the same user with the same payload will have different signatures and different revocation entries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Automatic expiry.&lt;/strong&gt; Revocation entries are stored with a TTL equal to the remaining lifetime of the original token. When the token would have expired anyway, the revocation entry is cleaned up. No manual maintenance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idempotent.&lt;/strong&gt; Revoking an already-revoked token returns &lt;code&gt;{ success: true, message: "Token was already revoked" }&lt;/code&gt; without consuming an extra token or throwing an error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expired tokens can't be revoked.&lt;/strong&gt; If a token has already expired, &lt;code&gt;revoke()&lt;/code&gt; returns a 400 error. There's no point in revoking something that's already invalid — and accepting expired tokens for revocation would open a vector for abuse.&lt;/p&gt;




&lt;h2&gt;
  
  
  The cost model
&lt;/h2&gt;

&lt;p&gt;Signing costs 1 token. Verifying costs 1 token. Revoking costs 1 token. There's no separate revocation infrastructure to run — it's the same API.&lt;/p&gt;

&lt;p&gt;With 10,000 free tokens per month, a typical session lifecycle — sign on login, verify on each protected request, revoke on logout — is well within the free tier for most applications.&lt;/p&gt;




&lt;h2&gt;
  
  
  Revocation isn't optional
&lt;/h2&gt;

&lt;p&gt;The pattern of "issue a JWT, let it expire naturally" works in many cases. But any application that handles sessions, financial operations, access control, or sensitive data needs a way to say "this token is no longer valid" — and mean it immediately.&lt;/p&gt;

&lt;p&gt;JWT's stateless design makes that genuinely hard. Every workaround has tradeoffs.&lt;/p&gt;

&lt;p&gt;Revocation built into the signing primitive — tied to the cryptographic signature itself, with automatic cleanup and no separate infrastructure — is a better model.&lt;/p&gt;

&lt;p&gt;That's what we built into FIPSign from day one, not as an add-on, but as a first-class operation alongside sign and verify.&lt;/p&gt;

&lt;p&gt;If you want to try it: &lt;a href="https://fipsign.dev" rel="noopener noreferrer"&gt;fipsign.dev&lt;/a&gt; — 10,000 free tokens per month, no credit card. SDK for JS/TS and Python, REST API for everything else.&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>typescript</category>
    </item>
    <item>
      <title>How to secure AI agents with post-quantum signatures</title>
      <dc:creator>German</dc:creator>
      <pubDate>Wed, 27 May 2026 12:45:49 +0000</pubDate>
      <link>https://dev.to/pqbuilder/how-to-secure-ai-agents-with-post-quantum-signatures-13cd</link>
      <guid>https://dev.to/pqbuilder/how-to-secure-ai-agents-with-post-quantum-signatures-13cd</guid>
      <description>&lt;p&gt;Everyone is building AI agents. Most are not thinking about how to secure them.&lt;/p&gt;

&lt;p&gt;An agent that sends emails, executes trades, controls infrastructure, or manages files is not just a chatbot. It's a system that takes real actions with real consequences. And when something goes wrong — a compromised agent, a replayed action, a forged instruction — the question isn't just "what happened?" It's "can you prove it, and can you stop it?"&lt;/p&gt;

&lt;p&gt;This post covers three practical problems in agent security and how to solve them with ML-DSA-65 (NIST FIPS 204) signatures.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem with agent actions
&lt;/h2&gt;

&lt;p&gt;When an agent executes an action, you need to answer three questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Was this action actually authorized?&lt;/strong&gt; Not just "did the agent do it" but "was the agent authorized to do it at this specific moment?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Can you invalidate it?&lt;/strong&gt; If the agent is compromised mid-session, can you stop future actions without waiting for a token to expire?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;How do you verify identity at scale?&lt;/strong&gt; When you have dozens of agents communicating with each other, how does Agent B know it's actually talking to Agent A and not an impersonator?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;JWT with RS256 answers none of these well. It has no revocation. Its signatures are vulnerable to Shor's algorithm. And there's no concept of agent identity — a token proves a session, not an entity.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 1 — Sign every action
&lt;/h2&gt;

&lt;p&gt;The simplest and most useful pattern: every action an agent takes gets a signed token before execution.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PQAuth&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fipsign-sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PQAuth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FIPSIGN_API_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Agent is about to send an email&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;              &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;agent_email_sender&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;send_email&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;               &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Your weekly report&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;authorizedBy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;workflow_xyz&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;expiresInSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// this action is valid for 5 minutes&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// Execute the action — pass the token along&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;emailService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;emailData&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// On the receiving side — verify before executing&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unauthorized agent action&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;        &lt;span class="c1"&gt;// 'agent_email_sender'&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;// 'send_email'&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorizedBy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// 'workflow_xyz'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same pattern in Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fipsign&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PQAuth&lt;/span&gt;

&lt;span class="n"&gt;pq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PQAuth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FIPSIGN_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;agent_email_sender&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;send_email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;authorized_by&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;workflow_xyz&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;expires_in_seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;

&lt;span class="c1"&gt;# Verify before executing
&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;PermissionError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unauthorized agent action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What you gain: every action has a cryptographic proof of authorization. The signature is ML-DSA-65 — no known quantum attack. The payload is tamper-proof. If someone intercepts and modifies &lt;code&gt;action: "delete_files"&lt;/code&gt; from &lt;code&gt;action: "send_email"&lt;/code&gt;, verification fails.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2 — Revoke immediately when an agent is compromised
&lt;/h2&gt;

&lt;p&gt;This is where JWT breaks down completely.&lt;/p&gt;

&lt;p&gt;With JWT, if an agent is compromised, you can't invalidate its tokens. You wait for expiry. For a token with a 1-hour TTL, that's up to 60 minutes of a compromised agent still passing authentication.&lt;/p&gt;

&lt;p&gt;With FIPSign, revocation is immediate and permanent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Agent_X is compromised at 14:32&lt;/span&gt;
&lt;span class="c1"&gt;// Revoke its active token immediately&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;revoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;agentToken&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;agent compromised — security incident&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Any subsequent verify() call returns valid: false&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;agentToken&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// false&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// 'Token has been revoked'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The revocation is backed by a blacklist in Cloudflare D1. Every remote &lt;code&gt;verify()&lt;/code&gt; call checks it before returning a result. There's no TTL to wait out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The failure mode &lt;a class="mentioned-user" href="https://dev.to/valentin_monteiro"&gt;@valentin_monteiro&lt;/a&gt; mentioned&lt;/strong&gt; — agents failing in production — almost always involves a window of time where a compromised agent keeps operating because nothing invalidated its credentials. Revocation closes that window to zero.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2.5 — Closing the detection gap
&lt;/h2&gt;

&lt;p&gt;Revocation is immediate — but only from the moment you call &lt;code&gt;pq.revoke()&lt;/code&gt;. The real problem is the window between when an agent is compromised and when someone detects it and triggers the revoke. That gap can be minutes, hours, or days depending on your setup.&lt;/p&gt;

&lt;p&gt;Three mechanisms to close it:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Short TTL as a damage ceiling
&lt;/h3&gt;

&lt;p&gt;TTL isn't a substitute for revocation — it's a limit on how much damage can happen if detection is slow. A compromised agent signing actions with a 5-minute TTL has at most 5 minutes of undetected exposure per token. Keep high-risk actions under 5 minutes. Keep low-risk actions under 60 minutes. Never use TTLs over a few hours for agent tokens.&lt;/p&gt;

&lt;p&gt;This is already in the checklist above — but the reasoning matters: short TTL is your last line of defense when everything else fails.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Wire &lt;code&gt;token.rejected&lt;/code&gt; to automatic revocation
&lt;/h3&gt;

&lt;p&gt;FIPSign emits a &lt;code&gt;token.rejected&lt;/code&gt; webhook every time a verification fails. A compromised agent that starts replaying tokens, sending malformed payloads, or operating outside its expected scope will generate rejections. You can wire that signal directly to an auto-revoke:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Your webhook handler&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/fipsign-webhook&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;verifyWebhookSignature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;token.rejected&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;projectId&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;

    &lt;span class="c1"&gt;// A project generating repeated rejections is a signal worth acting on&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;activeTokensByProject&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;revoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;activeTokensByProject&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;auto-revoked: anomaly detected&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;alertSecurityTeam&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;projectId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reason&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This closes the detection gap to seconds — no human in the loop required. FIPSign provides the signal. Your system decides what to do with it.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Use &lt;code&gt;limit.warning&lt;/code&gt; as an indirect signal
&lt;/h3&gt;

&lt;p&gt;A compromised agent that starts spamming actions will consume tokens faster than expected. FIPSign emits a &lt;code&gt;limit.warning&lt;/code&gt; webhook when usage reaches 80% of your monthly limit. It's not a security signal by design — but if you're nowhere near your normal consumption and &lt;code&gt;limit.warning&lt;/code&gt; fires unexpectedly, something is wrong.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;limit.warning&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Unexpected spike — investigate&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;alertSecurityTeam&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unusual token consumption spike&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;freeRemaining&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;freeRemaining&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;month&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;month&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not a substitute for proper anomaly detection — but it's a signal you already have for free.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;The model:&lt;/strong&gt; FIPSign provides the cryptographic primitives and the signals. Your system provides the context and the response logic. Neither layer can do the other's job — and they shouldn't try.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 3 — Agent identity with Private CA
&lt;/h2&gt;

&lt;p&gt;For simple agents, signed action tokens are enough. But when you have persistent agents — a fleet of IoT devices, a set of microservices, a group of agents that communicate with each other — you need something more durable than a session token.&lt;/p&gt;

&lt;p&gt;That's where a Private CA comes in.&lt;/p&gt;

&lt;p&gt;The model: each agent gets an ML-DSA-65 certificate issued by your project's CA. The certificate contains the agent's public key, its identity, and its expiry. It's signed by the CA's private key — which never leaves the server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PQAuth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;generateKeyPair&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fipsign-sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PQAuth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FIPSIGN_API_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// At agent provisioning time: generate a key pair for the agent&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;publicKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;secretKey&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;generateKeyPair&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;// secretKey stays on the agent — never transmitted&lt;/span&gt;

&lt;span class="c1"&gt;// Issue a certificate for this agent&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;certificate&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;issue&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;agent-data-processor-001&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;publicKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;expiresInSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// 30 days&lt;/span&gt;
  &lt;span class="na"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data_processor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;team&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;analytics&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2.1.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// Store certificate on the agent&lt;/span&gt;
&lt;span class="c1"&gt;// certificate.id, certificate.publicKey, certificate.signature, certificate.expiresAt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now when Agent B receives a message from Agent A, it can verify Agent A's identity offline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;rootCert&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./root-cert.json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="nx"&gt;assert&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// No API call — purely local ML-DSA-65 verification&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verifyCert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;agentACertificate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rootCert&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// 'Invalid certificate signature', 'CERT_EXPIRED', etc.&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Agent not authorized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// 'agent-data-processor-001'&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expiresAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// Unix timestamp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And if Agent A is decommissioned or compromised:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Revoke the certificate immediately&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;revokeCert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;certificate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;agent decommissioned&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// Check revocation before trusting&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;crl&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getCrl&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ca&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isCertRevoked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;agentACertificate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;crl&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Agent certificate revoked&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The full security model for agents
&lt;/h2&gt;

&lt;p&gt;Putting it together:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;Solution&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Was this action authorized?&lt;/td&gt;
&lt;td&gt;Sign every action with &lt;code&gt;/sign&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Can I stop a compromised agent?&lt;/td&gt;
&lt;td&gt;Revoke with &lt;code&gt;/revoke&lt;/code&gt; — immediate, permanent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How do agents identify themselves?&lt;/td&gt;
&lt;td&gt;Private CA — one certificate per agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How do agents verify each other?&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;verifyCert()&lt;/code&gt; offline — no API call needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Is the agent's identity still valid?&lt;/td&gt;
&lt;td&gt;Check CRL with &lt;code&gt;getCrl()&lt;/code&gt; + &lt;code&gt;isCertRevoked()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Are signatures quantum-resistant?&lt;/td&gt;
&lt;td&gt;ML-DSA-65 — NIST FIPS 204&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Security checklist for agents
&lt;/h2&gt;

&lt;p&gt;Before deploying an agent that takes real actions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Every action produces a signed token before execution&lt;/li&gt;
&lt;li&gt;[ ] Token expiry is short — 5 minutes maximum for high-risk actions&lt;/li&gt;
&lt;li&gt;[ ] Revocation is wired in — if the agent is compromised, you can stop it in seconds&lt;/li&gt;
&lt;li&gt;[ ] Persistent agents have certificates, not just session tokens&lt;/li&gt;
&lt;li&gt;[ ] Agents verify each other's certificates before trusting inter-agent messages&lt;/li&gt;
&lt;li&gt;[ ] CRL is checked periodically — don't rely on certificate expiry alone&lt;/li&gt;
&lt;li&gt;[ ] Signatures use ML-DSA-65 — not RS256, not ES256&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why post-quantum matters for agents specifically
&lt;/h2&gt;

&lt;p&gt;Most of the "harvest now, decrypt later" discussion focuses on data encryption. But signatures are equally at risk.&lt;/p&gt;

&lt;p&gt;An attacker recording your agents' signed actions today can — once quantum computers are available — forge signatures for actions that never happened. That means fabricated audit trails, forged authorizations, and retroactive tampering with your agent's history.&lt;/p&gt;

&lt;p&gt;ML-DSA-65 has no known quantum attack. Migrating now, while your agent system is being built, costs one install command. Migrating later, after you've deployed thousands of agents with RS256-signed credentials, costs months.&lt;/p&gt;




&lt;h2&gt;
  
  
  Get started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;fipsign-sdk
&lt;span class="c"&gt;# or&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;fipsign-sdk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Free tier: 10,000 tokens/month, no credit card. Private CA included — create one from the dashboard in seconds.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://fipsign.dev" rel="noopener noreferrer"&gt;fipsign.dev&lt;/a&gt; · &lt;a href="https://fipsign.dev/guide" rel="noopener noreferrer"&gt;Developer guide&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;If you're building agents and hitting security questions I didn't cover — drop them in the comments. Happy to go deeper on specific failure modes.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>security</category>
      <category>javascript</category>
      <category>python</category>
    </item>
    <item>
      <title>How to migrate from JWT to post-quantum tokens without breaking your API</title>
      <dc:creator>German</dc:creator>
      <pubDate>Tue, 26 May 2026 11:22:45 +0000</pubDate>
      <link>https://dev.to/pqbuilder/how-to-migrate-from-jwt-to-post-quantum-tokens-without-breaking-your-api-2kpc</link>
      <guid>https://dev.to/pqbuilder/how-to-migrate-from-jwt-to-post-quantum-tokens-without-breaking-your-api-2kpc</guid>
      <description>&lt;p&gt;You've read about ML-DSA-65. You understand why RS256 and ES256 are on borrowed time. Now the question is practical: how do you actually migrate a production API without taking everything down or breaking clients that already have tokens in the wild?&lt;/p&gt;

&lt;p&gt;This is a step-by-step guide. By the end you'll have a working migration — login, logout, protected routes, and a transition strategy that lets old JWT tokens and new post-quantum tokens coexist during the rollout.&lt;/p&gt;




&lt;h2&gt;
  
  
  What we're starting from
&lt;/h2&gt;

&lt;p&gt;A typical Express API with JWT looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jsonwebtoken&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="c1"&gt;// Login — sign a JWT&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;checkPassword&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;passwordHash&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid credentials&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;JWT_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;algorithm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RS256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;expiresIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1h&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// Middleware — verify JWT on every protected route&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;requireAuth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bearer &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unauthorized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;JWT_PUBLIC_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/profile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requireAuth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works. But the signature is RS256 — vulnerable to Shor's algorithm. The migration replaces that signature scheme with ML-DSA-65, which has no known quantum attack.&lt;/p&gt;




&lt;h2&gt;
  
  
  The transition strategy
&lt;/h2&gt;

&lt;p&gt;The hardest part of any cryptographic migration isn't the new code — it's the existing tokens in the wild. Users that logged in yesterday have a JWT in their browser or mobile app. You can't invalidate all of them at once without forcing everyone to log out.&lt;/p&gt;

&lt;p&gt;The approach that works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Deploy the new signing scheme&lt;/strong&gt; — new logins get ML-DSA-65 tokens&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the old verification&lt;/strong&gt; — during the transition period, verify both JWT and ML-DSA-65 tokens&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set a deprecation date&lt;/strong&gt; — after N days (typically the max token lifetime), all old JWTs will have expired naturally&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Remove the old path&lt;/strong&gt; — once the deprecation date passes, drop JWT verification entirely&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This means zero forced logouts and zero downtime. Clients migrate automatically as their old tokens expire.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1 — Install the SDK
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;fipsign-sdk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Get a free API key at &lt;a href="https://app.fipsign.dev" rel="noopener noreferrer"&gt;app.fipsign.dev&lt;/a&gt; — no credit card required. Create a project, create an API key inside that project. Store it as an environment variable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;FIPSIGN_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;pqa_your_api_key
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 2 — Replace login
&lt;/h2&gt;

&lt;p&gt;The new login signs the same payload with ML-DSA-65 instead of RS256. The token object returned by &lt;code&gt;sign()&lt;/code&gt; is a JSON object — encode it to base64 before sending it to the client so it fits cleanly in an Authorization header.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PQAuth&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fipsign-sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PQAuth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FIPSIGN_API_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/login&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findByEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;checkPassword&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;passwordHash&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid credentials&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;              &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;            &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;             &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;expiresInSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="c1"&gt;// Encode to base64 — this is what the client sends in Authorization: Bearer &amp;lt;token&amp;gt;&lt;/span&gt;
  &lt;span class="c1"&gt;// (JSON objects with special characters can break header parsing)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;encoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;encoded&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The payload structure is identical to what you had in JWT — &lt;code&gt;sub&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;role&lt;/code&gt;. Clients don't need to change anything about how they store or send the token.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3 — Add logout with revocation
&lt;/h2&gt;

&lt;p&gt;This is where ML-DSA-65 tokens improve on JWT. With JWT, you can't revoke a token — once issued, it's valid until expiry. With FIPSign, &lt;code&gt;revoke()&lt;/code&gt; immediately invalidates the token. Any future &lt;code&gt;verify()&lt;/code&gt; call rejects it even if the signature is valid and the token hasn't expired.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/logout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bearer &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;revoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user logged out&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* ignore malformed token */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;success&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 4 — Replace the middleware (with transition support)
&lt;/h2&gt;

&lt;p&gt;During the transition period, the middleware needs to handle both token formats. The simplest way is to try ML-DSA-65 first, and fall back to JWT verification for old tokens.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PQAuthError&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fipsign-sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;requireAuth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;authorization&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bearer &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Unauthorized&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// Try ML-DSA-65 first&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;payload&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Not a base64 JSON token — try JWT fallback&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// JWT fallback — remove this block after your deprecation date&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;JWT_PUBLIC_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Invalid token&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/profile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;requireAuth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once your JWT deprecation date passes and all old tokens have expired, remove the JWT fallback block. The middleware becomes purely ML-DSA-65.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5 — Use the built-in middleware (optional)
&lt;/h2&gt;

&lt;p&gt;If you don't need the transition period, FIPSign ships an Express middleware that handles everything automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Protect all routes under /api&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="c1"&gt;// req.user is the verified payload — sub, email, role, exp, iat&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/profile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The middleware reads &lt;code&gt;Authorization: Bearer &amp;lt;base64(token)&amp;gt;&lt;/code&gt;, verifies it, and attaches the decoded payload to &lt;code&gt;req.user&lt;/code&gt;. Returns 401 automatically on invalid tokens.&lt;/p&gt;




&lt;h2&gt;
  
  
  Python — FastAPI example
&lt;/h2&gt;

&lt;p&gt;If you're running a Python backend, the migration looks the same. Install the SDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;fipsign-sdk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Depends&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fipsign&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;PQAuth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fastapi_middleware&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;          &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;pq&lt;/span&gt;           &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PQAuth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pqa_your_api_key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;require_auth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fastapi_middleware&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pq&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/login&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;LoginRequest&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_by_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;check_password&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;password_hash&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;401&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invalid credentials&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;result&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expires_in_seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;encoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__dict__&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;token&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;encoded&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/logout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;logout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PQToken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;:]).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
            &lt;span class="n"&gt;pq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;revoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user logged out&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;pass&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@app.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/api/profile&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;Depends&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;require_auth&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Flask works the same way with &lt;code&gt;flask_middleware&lt;/code&gt; — the &lt;a href="https://fipsign.dev/guide" rel="noopener noreferrer"&gt;full guide&lt;/a&gt; covers both.&lt;/p&gt;




&lt;h2&gt;
  
  
  Pre-deprecation checklist
&lt;/h2&gt;

&lt;p&gt;Before you remove the JWT fallback:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Confirm your max JWT lifetime has passed (e.g. if JWTs expire in 24h, wait at least 24h after switching new logins to ML-DSA-65)&lt;/li&gt;
&lt;li&gt;[ ] Check your logs — are there still requests hitting the JWT verification path?&lt;/li&gt;
&lt;li&gt;[ ] Verify that mobile clients have updated — old app versions might still be sending JWTs&lt;/li&gt;
&lt;li&gt;[ ] Revoke any long-lived JWTs that were issued manually (service accounts, API clients)&lt;/li&gt;
&lt;li&gt;[ ] Remove &lt;code&gt;JWT_SECRET&lt;/code&gt; and &lt;code&gt;JWT_PUBLIC_KEY&lt;/code&gt; from your environment variables&lt;/li&gt;
&lt;li&gt;[ ] Remove the &lt;code&gt;jsonwebtoken&lt;/code&gt; dependency&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What you gained
&lt;/h2&gt;

&lt;p&gt;After the migration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Quantum-resistant signatures&lt;/strong&gt; — ML-DSA-65, NIST FIPS 204&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Revocation&lt;/strong&gt; — instantly invalidate any token, no blacklist to build yourself&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No key management&lt;/strong&gt; — FIPSign handles key generation, storage, and rotation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Same payload structure&lt;/strong&gt; — &lt;code&gt;sub&lt;/code&gt;, custom fields, expiry — nothing changes for your application logic&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The migration is mostly mechanical. The cryptography underneath is fundamentally different.&lt;/p&gt;




&lt;p&gt;If you have questions about specific edge cases — long-lived tokens, service-to-service auth, mobile clients — drop them in the comments.&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>python</category>
    </item>
    <item>
      <title>NIST published post-quantum standards in August 2024. Most developers haven't noticed.</title>
      <dc:creator>German</dc:creator>
      <pubDate>Sun, 24 May 2026 21:13:46 +0000</pubDate>
      <link>https://dev.to/pqbuilder/-nist-published-post-quantum-standards-in-august-2024-most-developers-havent-noticed-4b5d</link>
      <guid>https://dev.to/pqbuilder/-nist-published-post-quantum-standards-in-august-2024-most-developers-havent-noticed-4b5d</guid>
      <description>&lt;p&gt;In August 2024, the National Institute of Standards and Technology finalized the first post-quantum cryptography standards in history. It was the result of an eight-year selection process involving cryptographers from around the world.&lt;/p&gt;

&lt;p&gt;Most developers missed it entirely.&lt;/p&gt;

&lt;p&gt;That's not a criticism — it happened quietly, buried under the usual noise of framework releases and AI announcements. But the implications for the code you're shipping today are significant, and the window to act is shorter than most people think.&lt;/p&gt;




&lt;h2&gt;
  
  
  What NIST actually published
&lt;/h2&gt;

&lt;p&gt;Three standards. The one that matters most for developers building APIs and authentication systems is &lt;strong&gt;FIPS 204&lt;/strong&gt;, which specifies &lt;strong&gt;ML-DSA&lt;/strong&gt; — Module Lattice-based Digital Signature Algorithm.&lt;/p&gt;

&lt;p&gt;ML-DSA comes in three variants: ML-DSA-44, ML-DSA-65, and ML-DSA-87. The numbers roughly correspond to security levels. ML-DSA-65 is the sweet spot for most production use cases — NIST security level 3, reasonable signature size, fast enough for high-throughput APIs.&lt;/p&gt;

&lt;p&gt;The other two standards cover key encapsulation (ML-KEM, formerly Kyber) and a hash-based signature scheme (SLH-DSA). Important, but less immediately relevant if your primary concern is signing tokens and verifying identity.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why your current signatures are vulnerable
&lt;/h2&gt;

&lt;p&gt;If you're using JWT today — and most APIs are — you're almost certainly signing with RS256 (RSA) or ES256 (ECDSA). Both of these rely on mathematical problems that are hard for classical computers to solve: integer factorization for RSA, discrete logarithm for ECDSA.&lt;/p&gt;

&lt;p&gt;Peter Shor published an algorithm in 1994 that solves both of these problems efficiently on a quantum computer. A sufficiently powerful quantum computer running Shor's algorithm can break RS256 and ES256. Not slowly — efficiently.&lt;/p&gt;

&lt;p&gt;The natural response is: "quantum computers aren't powerful enough yet, so why does this matter now?"&lt;/p&gt;

&lt;p&gt;Two reasons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Harvest now, decrypt later.&lt;/strong&gt; Nation-state actors are already recording encrypted traffic and signed tokens today, storing them for future decryption once quantum hardware is available. If you're signing anything with a long-term security requirement — contracts, compliance records, device certificates, financial transactions — those signatures are already being collected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Migration takes years.&lt;/strong&gt; Updating cryptographic primitives across a production system isn't a one-afternoon job. It touches every service that signs or verifies tokens, every client that stores them, every integration that depends on the format. The teams that start now will be ready when quantum computers arrive. The teams that wait will be scrambling.&lt;/p&gt;

&lt;p&gt;Estimates for cryptographically relevant quantum computers range from 10 to 20 years. That sounds comfortable until you factor in that large-scale migrations typically take 3 to 7 years.&lt;/p&gt;




&lt;h2&gt;
  
  
  What "post-quantum resistant" actually means
&lt;/h2&gt;

&lt;p&gt;ML-DSA is based on the hardness of lattice problems — specifically Module Learning With Errors (Module-LWE) and Module Short Integer Solution (Module-SIS). Unlike RSA and ECDSA, no known quantum algorithm provides a significant speedup against these problems.&lt;/p&gt;

&lt;p&gt;This doesn't mean ML-DSA is unbreakable. It means that the best known attacks — classical or quantum — require computational effort that scales in a way that makes them infeasible for any realistic adversary. That's the same bar RSA and ECDSA were held to before Shor's algorithm changed the picture.&lt;/p&gt;

&lt;p&gt;The signatures are larger than ECDSA signatures — an ML-DSA-65 signature is around 3.3 KB compared to 64 bytes for ES256. For API tokens and document signing, this is a manageable tradeoff. For TLS handshakes at massive scale, it requires more careful consideration.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this means for your code today
&lt;/h2&gt;

&lt;p&gt;If you're building a new system, there's no good reason to start with RS256 or ES256. The post-quantum alternatives are standardized, implemented in audited libraries, and ready for production.&lt;/p&gt;

&lt;p&gt;If you have an existing system, the migration path depends on your architecture. The core change is replacing the signing and verification step — the token format, the payload structure, and everything else can stay the same. The hard part is coordinating the rollout across services that need to verify tokens issued under the old scheme during the transition period.&lt;/p&gt;

&lt;p&gt;The practical checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Audit where you're generating signatures today (JWT issuance, document signing, webhook signatures, device authentication)&lt;/li&gt;
&lt;li&gt;Identify which of those have long-term security requirements vs. short-lived session tokens&lt;/li&gt;
&lt;li&gt;Prioritize migration for anything with long-term requirements&lt;/li&gt;
&lt;li&gt;For new projects, start with ML-DSA-65&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The tooling gap
&lt;/h2&gt;

&lt;p&gt;Here's the honest state of things: the tooling for post-quantum signing is still catching up.&lt;/p&gt;

&lt;p&gt;Most JWT libraries don't support ML-DSA yet. HSMs are adding support gradually — the standard only finalized in August 2024, so hardware vendors are still shipping firmware updates. Key management services from the major cloud providers are on their roadmaps but not universally available.&lt;/p&gt;

&lt;p&gt;The clearest path for most developers right now is using an audited implementation of ML-DSA-65 directly — the &lt;a href="https://github.com/paulmillr/noble-post-quantum" rel="noopener noreferrer"&gt;@noble/post-quantum&lt;/a&gt; library is a well-regarded option — or using a managed signing service that handles the key management and infrastructure.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;After going through this migration problem myself, I built &lt;a href="https://fipsign.dev" rel="noopener noreferrer"&gt;FIPSign&lt;/a&gt; — a signing API that lets you sign any payload with ML-DSA-65 without managing keys, running infrastructure, or calling sales.&lt;/p&gt;

&lt;p&gt;The API is simple: you pass a payload with a &lt;code&gt;sub&lt;/code&gt; field identifying the entity, get back a quantum-resistant signed token. Verification checks the ML-DSA-65 signature, token expiry, and a revocation list. Revocation is built in — something JWT alone doesn't give you.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;PQAuth&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fipsign-sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PQAuth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pqa_your_api_key&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user_123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;expiresInSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's not the only path to ML-DSA-65 in production — but it's the fastest one I know of if you want to start signing with a post-quantum algorithm today without building key management from scratch.&lt;/p&gt;

&lt;p&gt;10,000 free tokens per month, no credit card. SDK for JS/TS and Python, REST API for everything else. Full guide at &lt;a href="https://fipsign.dev/guide" rel="noopener noreferrer"&gt;fipsign.dev/guide&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;The August 2024 publication was a quiet moment that most developers scrolled past. The cryptographic community has been working toward this for eight years. The standards are finalized, the libraries exist, and the threat — while not immediate — is real enough that waiting is a decision with consequences.&lt;/p&gt;

&lt;p&gt;The best time to start was August 2024. The second best time is now.&lt;/p&gt;

</description>
      <category>api</category>
      <category>security</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Why I built a post-quantum signing API (and why JWT is on borrowed time)</title>
      <dc:creator>German</dc:creator>
      <pubDate>Fri, 22 May 2026 12:36:31 +0000</pubDate>
      <link>https://dev.to/pqbuilder/why-i-built-a-post-quantum-signing-api-and-why-jwt-is-on-borrowed-time-1d65</link>
      <guid>https://dev.to/pqbuilder/why-i-built-a-post-quantum-signing-api-and-why-jwt-is-on-borrowed-time-1d65</guid>
      <description>&lt;p&gt;JWT with RS256/ES256 is everywhere. It's also going to be broken.&lt;/p&gt;

&lt;p&gt;Not today. But the clock is ticking — and the timeline is shorter than most developers think.&lt;/p&gt;

&lt;p&gt;NIST published the post-quantum signature standards in August 2024. AWS KMS added ML-DSA support in June 2025. Microsoft shipped it natively to Windows 11 and .NET in November 2025. The migration window is open — and most backend developers haven't started yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem with classical signatures
&lt;/h2&gt;

&lt;p&gt;Every JWT signed with RS256 or ES256 relies on RSA or ECDSA. These algorithms are vulnerable to Shor's algorithm running on a sufficiently powerful quantum computer. NIST finalized the post-quantum replacements in August 2024 — but most developers haven't started migrating yet.&lt;/p&gt;

&lt;p&gt;The threat isn't just future decryption. Nation-state actors are already harvesting signed traffic today to decrypt and forge once quantum computers arrive. "Harvest now, decrypt later" is a real attack pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;I built &lt;a href="https://fipsign.dev" rel="noopener noreferrer"&gt;FIPSign&lt;/a&gt; — a signing API built on ML-DSA-65 (NIST FIPS 204). The idea is simple: you call &lt;code&gt;/sign&lt;/code&gt; with any payload, get back a quantum-resistant token. No infrastructure to manage, no keys to generate or rotate, no DevOps.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user_123&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;expiresInSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pq&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Not just for auth
&lt;/h2&gt;

&lt;p&gt;Most post-quantum signing tools focus only on user authentication. FIPSign lets you sign anything — the only required field is &lt;code&gt;sub&lt;/code&gt;, any string identifying the entity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sub: "user_123"&lt;/code&gt; — user sessions&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sub: "order_456"&lt;/code&gt; — payment intents&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sub: "doc_789"&lt;/code&gt; — document certification&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sub: "device_iot_001"&lt;/code&gt; — IoT firmware&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How revocation works
&lt;/h2&gt;

&lt;p&gt;Revocation is built in. When you revoke a token, we store a SHA-256 hash of the ML-DSA-65 signature as a blacklist entry in Cloudflare D1. Every remote &lt;code&gt;/verify&lt;/code&gt; call checks that blacklist. Entries expire automatically when the original token would have expired.&lt;/p&gt;

&lt;p&gt;Local verification (&lt;code&gt;localVerify: true&lt;/code&gt;) runs entirely in memory at ~1ms with no API call — but doesn't check revocation. Use remote verification for payments and admin actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why ML-DSA-65 specifically?
&lt;/h2&gt;

&lt;p&gt;ML-DSA-65 is NIST security level 3 — the right balance between security and signature size for API use cases. ML-DSA-44 is faster but lower security. ML-DSA-87 produces larger signatures with no practical benefit for most applications.&lt;/p&gt;

&lt;p&gt;The implementation uses &lt;a href="https://github.com/paulmillr/noble-post-quantum" rel="noopener noreferrer"&gt;@noble/post-quantum&lt;/a&gt; — a widely audited library you can verify independently.&lt;/p&gt;

&lt;h2&gt;
  
  
  One more thing — no subscription
&lt;/h2&gt;

&lt;p&gt;FIPSign doesn't have monthly plans. You get 10,000 free tokens per month automatically. If you need more, you buy a token pack — they never expire and accumulate across purchases.&lt;/p&gt;

&lt;p&gt;One account gives you unlimited projects with unlimited API keys per project, each with its own usage stats. Useful if you're managing multiple clients, environments, or microservices from the same account — no enterprise plan required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;fipsign-sdk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Full guide at &lt;a href="https://fipsign.dev/guide" rel="noopener noreferrer"&gt;fipsign.dev/guide&lt;/a&gt;. Free account at &lt;a href="https://app.fipsign.dev" rel="noopener noreferrer"&gt;app.fipsign.dev&lt;/a&gt; — no credit card.&lt;/p&gt;

&lt;p&gt;Happy to answer any questions about the cryptography, the architecture, or the migration path from JWT.&lt;/p&gt;

</description>
      <category>security</category>
      <category>webdev</category>
      <category>api</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
