<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Symbolic Software</title>
    <link>https://symbolic.software/</link>
    <description>Applied cryptography consultancy. Design-level security audits — protocol architecture, cryptographic design, threat modeling, formal verification. Since 2017.</description>
    <language>en-us</language>
    <managingEditor>business@symbolic.software (Symbolic Software)</managingEditor>
    <lastBuildDate>Sun, 16 Aug 2026 17:42:01 &#43;0000</lastBuildDate>
    
    <atom:link href="https://symbolic.software/index.xml" rel="self" type="application/rss+xml" />
    
    <image>
      <url>https://symbolic.software/images/icon.png</url>
      <title>Symbolic Software</title>
      <link>https://symbolic.software/</link>
    </image>
    
    <item>
      <title>Verifpal Reaches 1.0: A Toy Becomes an Instrument</title>
      <link>https://symbolic.software/blog/2026-08-15-verifpal-1-0/</link>
      <pubDate>Sat, 15 Aug 2026 00:00:00 &#43;0000</pubDate>
      <guid>https://symbolic.software/blog/2026-08-15-verifpal-1-0/</guid>
      <description>Verifpal 1.0 removes the beta warning the tool has printed with every analysis since 2020, on the strength of a new paper. The language now covers post-quantum key exchange, declared weakening assumptions, witness-based unlinkability, and two concurrent sessions of every principal; the engine behind it comes with a soundness theorem that holds however the solver behaves, unconditional termination, and attack traces measured for readability against ProVerif and Tamarin.</description>
      <content:encoded>&lt;p&gt;&lt;a href=&#34;https://verifpal.com&#34;&gt;Verifpal&lt;/a&gt; 1.0 is out. Since May 2020, Verifpal has printed the same warning at the top of its analyses: &lt;code&gt;Verifpal is Beta software.&lt;/code&gt; Version 1.0 deletes that line. What justifies the deletion is a new paper, &lt;a href=&#34;https://eprint.iacr.org/2026/1654&#34;&gt;&lt;em&gt;Verifpal Seven Years Later: Can a Toy Become an Instrument?&lt;/em&gt;&lt;/a&gt;, the successor to &lt;a href=&#34;https://eprint.iacr.org/2019/971&#34;&gt;the 2019 paper&lt;/a&gt; that introduced the tool. Two things have changed since then, and the paper accounts for both: what the modeling language can express, rebuilt across last week&amp;rsquo;s &lt;a href=&#34;https://symbolic.software/blog/2026-08-05-verifpal-0-70/&#34;&gt;0.70.0&lt;/a&gt; and &lt;a href=&#34;https://symbolic.software/blog/2026-08-08-verifpal-0-80/&#34;&gt;0.80.0&lt;/a&gt; releases, and whether the engine&amp;rsquo;s answers deserve belief. This post is the whole argument in one place.&lt;/p&gt;
&lt;p&gt;The 2019 paper was frank about what it was presenting. It described its own soundness argument as &amp;ldquo;an incomplete, semi-formal, in-progress set of results,&amp;rdquo; called its unlinkability queries &amp;ldquo;especially experimental,&amp;rdquo; and conceded that its completeness argument rested on &amp;ldquo;the problematically subjective definition of what constitutes a &amp;lsquo;real-world protocol&amp;rsquo;.&amp;rdquo; The reasonable conclusion at the time was that &lt;a href=&#34;https://proverif.inria.fr&#34;&gt;ProVerif&lt;/a&gt; and &lt;a href=&#34;https://tamarin-prover.github.io&#34;&gt;Tamarin&lt;/a&gt; were verification tools, and that Verifpal was a teaching aid that resembled one. The blunter version, which the new paper&amp;rsquo;s title takes up rather than disputes, was that Verifpal was a toy.&lt;/p&gt;
&lt;p&gt;Seven years later, the paper re-asks the question, and it tries to answer it the way a skeptic would want it answered: by proving what can be proved, measuring what can be measured, and conceding in print what can be neither.&lt;/p&gt;
&lt;h2 id=&#34;what-the-language-can-now-say&#34;&gt;What the language can now say&lt;/h2&gt;
&lt;p&gt;A tool that cannot express the protocol in front of you is of no use whatever its theory, so the paper&amp;rsquo;s first answer to its own question is a list of what the language now covers. The items landed across &lt;a href=&#34;https://symbolic.software/blog/2026-08-05-verifpal-0-70/&#34;&gt;0.70.0&lt;/a&gt; and &lt;a href=&#34;https://symbolic.software/blog/2026-08-08-verifpal-0-80/&#34;&gt;0.80.0&lt;/a&gt;, each of which has its own detailed post; here is the short version.&lt;/p&gt;
&lt;h3 id=&#34;diffie-hellman-stopped-being-special&#34;&gt;Diffie-Hellman stopped being special&lt;/h3&gt;
&lt;p&gt;The 2019 language had a third kind of value alongside constants and primitives: equations, written &lt;code&gt;G^a&lt;/code&gt; and &lt;code&gt;G^a^b&lt;/code&gt;, with a reserved generator, a limit of two exponents, and their own special case in the parser, the printer, the equational theory, the unifier and the search. They modeled Diffie-Hellman and nothing else. That kind of value no longer exists. Key exchange is written with two ordinary primitives, &lt;code&gt;PUBKEY(a)&lt;/code&gt; and &lt;code&gt;DH_KEX(ga, b)&lt;/code&gt;, and commutativity is a declared field on the primitive&amp;rsquo;s specification, interpreted by the same machinery as every other rule. The equivalence relation is arranged so that &lt;code&gt;DH_KEX(PUBKEY(a), PUBKEY(b))&lt;/code&gt;, the term an attacker can build from two public keys off the wire, is equivalent to no shared secret: the computational Diffie-Hellman assumption, made structural in the term algebra instead of handled as a special case in the engine.&lt;/p&gt;
&lt;h3 id=&#34;post-quantum-and-hybrid-key-exchange&#34;&gt;Post-quantum and hybrid key exchange&lt;/h3&gt;
&lt;p&gt;Removing equations was the precondition for this. A KEM has no exponent to commute, so it could not be expressed through the equation interface at all. &lt;code&gt;KEM_ENCAP&lt;/code&gt; and &lt;code&gt;KEM_DECAP&lt;/code&gt; are thirty-eight lines of declarative data, absorbed by the rest of the engine without modification, and they make the question we hear most often since &lt;a href=&#34;https://symbolic.software/blog/2026-04-02-pq-native/&#34;&gt;recommending post-quantum native design&lt;/a&gt; something a model can settle: what does a hybrid actually buy you? Three shipped models are the same protocol, an X25519 leg and a KEM leg fed into one key schedule, differing only in what phase 1 leaks:&lt;/p&gt;
&lt;table class=&#34;vp7-matrix&#34;&gt;
&lt;thead&gt;&lt;tr&gt;&lt;th&gt;model&lt;/th&gt;&lt;th&gt;what leaks in phase 1&lt;/th&gt;&lt;th&gt;confidentiality? m&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;kem_hybrid_classical_broken.vp&lt;/td&gt;&lt;td&gt;both Diffie-Hellman private values&lt;/td&gt;&lt;td class=&#34;y&#34;&gt;holds&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;kem_hybrid_pq_broken.vp&lt;/td&gt;&lt;td&gt;the decapsulation key&lt;/td&gt;&lt;td class=&#34;y&#34;&gt;holds&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;kem_hybrid_both_broken.vp&lt;/td&gt;&lt;td&gt;both, together&lt;/td&gt;&lt;td class=&#34;n&#34;&gt;contradicted&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;It is the &lt;a href=&#34;https://symbolic.software/blog/2026-04-13-hybrid-constructions/&#34;&gt;hybrid construction argument&lt;/a&gt; as three files you can run in a design review.&lt;/p&gt;
&lt;h3 id=&#34;assumptions-you-can-declare-verdicts-that-carry-them&#34;&gt;Assumptions you can declare, verdicts that carry them&lt;/h3&gt;
&lt;p&gt;From 0.80.0, a call site can state a weakening assumption directly instead of approximating it with a hand-maintained list of leaked values: &lt;code&gt;SIGN[forgeable](sk, m)&lt;/code&gt; costs authenticity, &lt;code&gt;AEAD_ENC[weak](k, m, ad)&lt;/code&gt; costs confidentiality, and &lt;code&gt;from phase 2&lt;/code&gt; schedules the break, which is exactly what a harvest-now-decrypt-later adversary is. Scheduling comes with an obligation the paper formalizes: knowledge acquired in a later phase must never justify an action taken in an earlier one, or any key ever leaked would forge any signature ever checked. The annotations feed the ordinary deduction rules, and the design shows in what we did not have to write: once &lt;code&gt;PUBKEY[weak]&lt;/code&gt; hands the attacker a private value, the existing reconstruction machinery recomputes every shared secret that value was ever used in, with no Diffie-Hellman-specific rule anywhere.&lt;/p&gt;
&lt;p&gt;Two decisions govern how the feature behaves at its edges. An annotation a primitive does not declare is a hard error at load time with an explanation, because an annotation that reads like a check but does nothing is more dangerous than no annotation; &lt;code&gt;malleable&lt;/code&gt; parses and is rejected on those grounds, held back until the solver can actually find the two-substitution attacks it would advertise. And every verdict obtained under assumptions is printed beneath the assumptions in force, which travel through the JSON interface too, so a &lt;code&gt;Pass&lt;/code&gt; under &lt;code&gt;PUBKEY[weak from phase 1]&lt;/code&gt; cannot be quoted as a &lt;code&gt;Pass&lt;/code&gt; outright:&lt;/p&gt;
&lt;div class=&#34;vp7-term&#34;&gt;
&lt;div class=&#34;vp7-term-bar&#34;&gt;
&lt;div class=&#34;vp7-term-left&#34;&gt;&lt;div class=&#34;vp7-traffic&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;span class=&#34;vp7-fname&#34;&gt;verifpal verify &lt;b&gt;pqxdh-weak.vp&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;span class=&#34;vp7-verdict&#34;&gt;2 of 2 verified · 0.03s&lt;/span&gt;
&lt;/div&gt;
&lt;pre&gt;&lt;span class=&#34;k&#34;&gt;  Warning ▲ Analysis performed under 5 declared weakening assumptions:&lt;/span&gt;
&lt;span class=&#34;k&#34;&gt;  Warning ▲ PUBKEY[weak from phase 1](alongterm)&lt;/span&gt;
&lt;span class=&#34;k&#34;&gt;  Warning ▲ PUBKEY[weak from phase 1](blongterm)&lt;/span&gt;
&lt;span class=&#34;k&#34;&gt;  Warning ▲ PUBKEY[weak from phase 1](bs)&lt;/span&gt;
&lt;span class=&#34;k&#34;&gt;  Warning ▲ PUBKEY[weak from phase 1](bo)&lt;/span&gt;
&lt;span class=&#34;k&#34;&gt;  Warning ▲ PUBKEY[weak from phase 1](ae1)&lt;/span&gt;
&lt;span class=&#34;d&#34;&gt;&amp;nbsp;&lt;/span&gt;
&lt;span class=&#34;ok&#34;&gt;     Pass ✓&lt;/span&gt; confidentiality? m1
&lt;span class=&#34;ok&#34;&gt;     Pass ✓&lt;/span&gt; authentication? Alice -&amp;gt; Bob: e1
&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;The model behind that output is PQXDH, Signal&amp;rsquo;s current session setup, with every Diffie-Hellman public key annotated &lt;code&gt;weak from phase 1&lt;/code&gt; and nothing leaking at all. The one line to look at is the KEM encapsulation key, which carries no annotation. That omission is the hybrid argument in its entirety: discrete logarithm falls, the KEM does not, and the message survives on exactly that difference.&lt;/p&gt;
&lt;h3 id=&#34;unlinkability-decided-by-finding-a-witness&#34;&gt;Unlinkability, decided by finding a witness&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;unlinkability?&lt;/code&gt; used to be decided by structural tests that never consulted what the attacker had observed. It is now decided the way the manual always defined it: by searching for a concrete link witness, a value in the attacker&amp;rsquo;s hands that ties both queried values to one participant, which the trace then reports as the explanation. In the DP-3T contact-tracing model, a leaked day-zero seed links every identifier that phone will ever broadcast, and the trace names the seed, so the fix is visible in the result. The specification table records which argument positions identify a participant, which is how the engine knows that two signatures verifying under one key are linkable while two ring signatures over the same ring are not: a successful ring verification names a set of possible signers and never a member of it, and that declared emptiness is what stands between this query and a false attack on the one primitive whose entire purpose is unlinkability.&lt;/p&gt;
&lt;h2 id=&#34;soundness-assuming-the-solver-is-broken&#34;&gt;Soundness, assuming the solver is broken&lt;/h2&gt;
&lt;p&gt;The active-attacker search is the part of a symbolic verifier where the difficulty lives, and therefore the part where the bugs live. The 2019 engine searched forwards, enumerating combinations of wire values to tamper with under four tuned parameters, and those parameters, rather than the protocol under analysis, decided which attacks were reachable. The &lt;a href=&#34;https://symbolic.software/blog/2026-08-05-verifpal-0-70/&#34;&gt;0.70.0 post&lt;/a&gt; describes its replacement: a backward search that starts from each unresolved query and works out what the attacker would need to contradict it.&lt;/p&gt;
&lt;p&gt;The paper&amp;rsquo;s job is different: to say why anyone should believe what that search reports. Its answer is not a correctness proof for the solver. &amp;ldquo;The theorem we want,&amp;rdquo; it says, &amp;ldquo;is not that the solver is correct, but something stronger and easier to check: that the solver&amp;rsquo;s correctness is irrelevant to soundness.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;The theorem treats the solver as an arbitrary function, an adversarially buggy one included, and shows that a reported attack is real anyway. The structure that makes this provable is the one 0.70.0 shipped: the solver can only propose. Before anything reaches you, a much smaller component checks that the attacker controls every wire slot the proposal writes to, re-derives every term it installs from the attacker&amp;rsquo;s own knowledge, re-executes the protocol with the substitution in place, and re-tests the query against what the attacker then holds. Recording a query result requires a token whose constructor is private to one module, so &amp;ldquo;the search cannot record anything&amp;rdquo; is a fact the Rust compiler enforces rather than a discipline we promise to maintain.&lt;/p&gt;
&lt;p&gt;The paper then does the accounting that a claim like this owes its reader.&lt;/p&gt;
&lt;figure class=&#34;vp7fig reveal&#34;&gt;
&lt;div class=&#34;vp7-scroll&#34;&gt;
&lt;svg viewBox=&#34;0 0 880 252&#34; role=&#34;img&#34; aria-label=&#34;A bar representing 12,004 lines of Rust divided to scale: a trusted region of 2,751 lines at 22.9 percent, the untrusted solver at 1,650 lines, and 7,603 lines of parser, printer and interface code with no write path to a query result.&#34;&gt;
  &lt;text class=&#34;vp7-lab&#34; x=&#34;20&#34; y=&#34;16&#34;&gt;The 1.0 engine, to scale · 12,004 lines of Rust&lt;/text&gt;
  &lt;rect class=&#34;vp7-box hot&#34; x=&#34;20&#34; y=&#34;34&#34; width=&#34;193&#34; height=&#34;46&#34; rx=&#34;2&#34;/&gt;
  &lt;rect class=&#34;vp7-box solve&#34; x=&#34;213&#34; y=&#34;34&#34; width=&#34;115&#34; height=&#34;46&#34; rx=&#34;2&#34;/&gt;
  &lt;rect class=&#34;vp7-box soft&#34; x=&#34;328&#34; y=&#34;34&#34; width=&#34;532&#34; height=&#34;46&#34; rx=&#34;2&#34;/&gt;
  &lt;text class=&#34;vp7-t hot b&#34; x=&#34;116&#34; y=&#34;62&#34; text-anchor=&#34;middle&#34;&gt;22.9%&lt;/text&gt;
  &lt;text class=&#34;vp7-t solve b&#34; x=&#34;270&#34; y=&#34;62&#34; text-anchor=&#34;middle&#34;&gt;13.7%&lt;/text&gt;
  &lt;text class=&#34;vp7-t&#34; x=&#34;594&#34; y=&#34;62&#34; text-anchor=&#34;middle&#34;&gt;63.3%&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm hot&#34; x=&#34;20&#34; y=&#34;100&#34;&gt;trusted · 2,751 lines&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm solve&#34; x=&#34;213&#34; y=&#34;100&#34;&gt;solver · 1,650&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;328&#34; y=&#34;100&#34;&gt;parser · printer · JSON · CLI · 7,603&lt;/text&gt;
  &lt;rect class=&#34;vp7-box hot&#34; x=&#34;20&#34; y=&#34;126&#34; width=&#34;12&#34; height=&#34;12&#34;/&gt;
  &lt;text class=&#34;vp7-t&#34; x=&#34;42&#34; y=&#34;137&#34;&gt;Must be right for a reported attack to be real and mean what it appears to mean:&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;42&#34; y=&#34;154&#34;&gt;the five violation predicates (995), the knowledge closure with equivalence, hashing and&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;42&#34; y=&#34;170&#34;&gt;the capability indexes (1,377), and the session expansion (379).&lt;/text&gt;
  &lt;rect class=&#34;vp7-box solve&#34; x=&#34;20&#34; y=&#34;186&#34; width=&#34;12&#34; height=&#34;12&#34;/&gt;
  &lt;text class=&#34;vp7-t&#34; x=&#34;42&#34; y=&#34;197&#34;&gt;May be wrong in any way at all: symbolic execution, the goal calculus, unification.&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;42&#34; y=&#34;214&#34;&gt;The soundness theorem quantifies over this code, so a bug here can hide an attack but not invent one.&lt;/text&gt;
  &lt;rect class=&#34;vp7-box soft&#34; x=&#34;20&#34; y=&#34;230&#34; width=&#34;12&#34; height=&#34;12&#34;/&gt;
  &lt;text class=&#34;vp7-t&#34; x=&#34;42&#34; y=&#34;241&#34;&gt;Everything else, with no write path to a query result.&lt;/text&gt;
&lt;/svg&gt;
&lt;/div&gt;
&lt;figcaption&gt;&lt;b&gt;What a reported attack depends on.&lt;/b&gt; A query result can only be recorded through one call site, gated by a token whose constructor is private; the call-site counts are enforced by tests over the source, and &lt;code&gt;unsafe&lt;/code&gt; is forbidden crate-wide. The theorem is a reduction: it shrinks what has to be trusted from the whole engine to under a quarter of it, and names the lines that remain. It is not mechanized.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Writing the deduction rules down as an inference system, which the paper required, also found a bug. The closure&amp;rsquo;s decomposition rule demands that the attacker actually hold a term before opening it, and holding &lt;code&gt;k&lt;/code&gt; makes &lt;code&gt;ENC(k, m)&lt;/code&gt; openable without making it obtainable: producing it still requires &lt;code&gt;m&lt;/code&gt;. A helper in the engine had conflated the two, and on a forty-line model the tool reported the attacker decrypting under a key that the same run separately certified it could not obtain. A false attack, in the passive analysis, with no solver anywhere near it. The repair is one premise in one rule, &lt;code&gt;decompose_is_not_construct.vp&lt;/code&gt; now pins it in the test suite, and the episode is in the paper because it is the best evidence we have for doing this kind of writing-down at all: the formalization caught what the test suite had not.&lt;/p&gt;
&lt;h2 id=&#34;the-model-that-would-not-finish&#34;&gt;The model that would not finish&lt;/h2&gt;
&lt;p&gt;A verifier that does not terminate is worse than useless, because you cannot tell a hard protocol from a broken tool. Symmetric-key Needham-Schroeder ships in Verifpal&amp;rsquo;s examples directory, and for five years, across two implementations of the tool, it did not finish: the analysis ran for minutes without resolving a single query.&lt;/p&gt;
&lt;p&gt;The cause turns out to be a clean pattern the paper calls a replay pump. The protocol decrypts a value, hashes it, and encrypts the result under the same key, so its own output is a well-typed input to itself, one level deeper. An attacker that replays the output into the input obtains a term it did not previously hold; the analysis counts new knowledge as progress and goes around again; the next round produces a term nested one level deeper still. On Needham-Schroeder the engine climbed to 213 nested hash applications and roughly 132,000 proposals in its first 45 seconds, with no end in prospect.&lt;/p&gt;
&lt;p&gt;What stops the pump in 1.0 is a bound that is read off the model rather than chosen: the attacker may not install a term that nests deeper than anything the protocol itself computes. A protocol that builds deeper values gets a deeper bound (across the shipped corpus it ranges from 4 to 20), and there is nothing to tune. Symmetric-key Needham-Schroeder now completes in about a tenth of a second at one session and 2.4 seconds at the default two, reporting the textbook Denning-Sacco replay, and the paper proves that the analysis as a whole terminates, unconditionally.&lt;/p&gt;
&lt;p&gt;The bound excludes a class of attacks: any whose injected term must nest deeper than the protocol builds. So the tool says so, at the exact place the exclusion occurred, without being asked:&lt;/p&gt;
&lt;div class=&#34;vp7-term&#34;&gt;
&lt;div class=&#34;vp7-term-bar&#34;&gt;
&lt;div class=&#34;vp7-term-left&#34;&gt;&lt;div class=&#34;vp7-traffic&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;span class=&#34;vp7-fname&#34;&gt;verifpal verify &lt;b&gt;replay_pump_reflection.vp&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;span class=&#34;vp7-verdict&#34;&gt;one line of the analysis output&lt;/span&gt;
&lt;/div&gt;
&lt;pre&gt;&lt;span class=&#34;k&#34;&gt;     Info ●&lt;/span&gt; Search declined AEAD_ENC(k, HASH(HASH(HASH(HASH(m)))), nil) at
            Bob&#39;s e1: it nests deeper than the 4 levels this protocol itself
            computes. Attacks needing a term deeper than the protocol builds
            are out of reach.
&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;A &lt;code&gt;Pass&lt;/code&gt; gives you no way of knowing which branches a search declined to walk down, which is what makes undisclosed incompleteness corrosive. This disclosure names the principal, the slot, the term, and the depth the protocol itself reaches, so a reader who worries about deep-construction attacks against this protocol knows exactly where to direct the worry.&lt;/p&gt;
&lt;h2 id=&#34;two-sessions-of-every-principal&#34;&gt;Two sessions of every principal&lt;/h2&gt;
&lt;p&gt;Protocols are deployed concurrently, and some attacks need two instances of one role to exist at the same time. The standard specimen is Millen&amp;rsquo;s f&lt;sup&gt;n&lt;/sup&gt;g&lt;sup&gt;n&lt;/sup&gt; protocol, engineered so that its secrecy break requires two parallel sessions of the responder; an analysis holding one session of each principal cannot even state the attack. This class of protocol was the sharpest objection to Verifpal&amp;rsquo;s standing as a verifier, and until 0.80.1, released alongside this post, the objection was correct.&lt;/p&gt;
&lt;p&gt;As of 0.80.1, every principal enters the analysis as two concurrent sessions of itself, so a two-party model runs as four principal states: Alice and Alice#2, Bob and Bob#2. Each session draws its own fresh values while long-term material stays shared, and the attacker is free to route any session&amp;rsquo;s traffic into any other. No model needs editing and no syntax was added, because the language has carried the necessary distinction since 2019 without using it: &lt;code&gt;generates&lt;/code&gt; was always &amp;ldquo;fresh in each run,&amp;rdquo; and &lt;code&gt;knows&lt;/code&gt; was always &amp;ldquo;long-term.&amp;rdquo; Raising the count is a matter of &lt;code&gt;--sessions&lt;/code&gt;, up to 16 sessions per principal.&lt;/p&gt;
&lt;p&gt;Here is what that buys on FFGG, Millen&amp;rsquo;s protocol at n = 2 and the very model the 2019 paper used for its own trace comparison:&lt;/p&gt;
&lt;div class=&#34;vp7-term&#34;&gt;
&lt;div class=&#34;vp7-term-bar&#34;&gt;
&lt;div class=&#34;vp7-term-left&#34;&gt;&lt;div class=&#34;vp7-traffic&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;span class=&#34;vp7-fname&#34;&gt;verifpal verify &lt;b&gt;ffgg.vp&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;span class=&#34;vp7-verdict fail&#34;&gt;1 of 1 failed · 0.02s&lt;/span&gt;
&lt;/div&gt;
&lt;pre&gt;&lt;span class=&#34;no&#34;&gt;     Fail ✗&lt;/span&gt; confidentiality? m
            ╭─ Attack trace:
            │ 1. &lt;span class=&#34;k&#34;&gt;In an earlier session in which the attacker replaced n2#2&lt;/span&gt;
            │    &lt;span class=&#34;k&#34;&gt;with n1, msg2#2 resolved to PKE_ENC(pkb, CONCAT(n1, m, n1#2)).&lt;/span&gt;
            │ 2. &lt;span class=&#34;m&#34;&gt;Attacker replaces msg (sent by Alice to Bob) with&lt;/span&gt;
            │    &lt;span class=&#34;m&#34;&gt;PKE_ENC(pkb, CONCAT(n1, m, n1#2)).&lt;/span&gt; &lt;span class=&#34;d&#34;&gt;(msg was&lt;/span&gt;
            │    &lt;span class=&#34;d&#34;&gt;PKE_ENC(pkb, CONCAT(n1, n2, m)))&lt;/span&gt;
            │ 3. Bob&#39;s SPLIT(PKE_DEC(skb, msg))? passes — its inputs are
            │    attacker-controlled.
            │ 4. Bob&#39;s ASSERT(SPLIT(PKE_DEC(skb, msg))?, n1)? passes — its
            │    inputs are attacker-controlled.
            │ 5. Attacker observes m on the wire.
            ╰▸ m (m) is obtained by Attacker.
&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;The two nonces in that trace are different objects: &lt;code&gt;n1&lt;/code&gt; belongs to one responder session and &lt;code&gt;n1#2&lt;/code&gt; to the other. The attacker uses the second session as an oracle, feeding it the first session&amp;rsquo;s nonce so that its reply comes back with the tuple rotated, then presents that reply to the first session, whose own checks pass and whose response moves the secret into a position sent in the clear. This is the shape Tamarin&amp;rsquo;s counterexample for the same protocol takes, and Verifpal finds and narrates it in the names the model declares.&lt;/p&gt;
&lt;p&gt;Bounded sessions are still not unbounded replication, and the paper is specific about the two gaps that remain. First, a passing query at &lt;em&gt;k&lt;/em&gt; sessions means that no attack was found within &lt;em&gt;k&lt;/em&gt; sessions, never that none exists. Second, the repeated runs of any one session share that session&amp;rsquo;s fresh values, which grants the attacker an equality that true replication would withhold. The engine cannot close the second gap, so it discloses each instance of it: after minimizing a witness, Verifpal re-runs the attack with the target&amp;rsquo;s own fresh values renamed to the copies a different session would have held. A witness that survives the renaming did not depend on shared freshness. A witness that dies is still reported, with a label. Run the same FFGG model at &lt;code&gt;--sessions 1&lt;/code&gt; and the only route to the secret is exactly such a witness:&lt;/p&gt;
&lt;div class=&#34;vp7-term&#34;&gt;
&lt;div class=&#34;vp7-term-bar&#34;&gt;
&lt;div class=&#34;vp7-term-left&#34;&gt;&lt;div class=&#34;vp7-traffic&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;span class=&#34;vp7-fname&#34;&gt;verifpal verify &lt;b&gt;ffgg.vp&lt;/b&gt; --sessions 1&lt;/span&gt;&lt;/div&gt;
&lt;span class=&#34;vp7-verdict fail&#34;&gt;the same query, one session&lt;/span&gt;
&lt;/div&gt;
&lt;pre&gt;&lt;span class=&#34;no&#34;&gt;     Fail ✗&lt;/span&gt; confidentiality? m
            ╭─ Attack trace:
            │ 1. In an earlier session in which the attacker replaced n2
            │    with n1, msg2 resolved to PKE_ENC(pkb, CONCAT(n1, m, n1)).
            │ 2. Attacker replaces msg (sent by Alice to Bob) with
            │    PKE_ENC(pkb, CONCAT(n1, m, n1)). (msg was
            │    PKE_ENC(pkb, CONCAT(n1, n2, m)))
            │ 3. Bob&#39;s SPLIT(PKE_DEC(skb, msg))? passes — its inputs are
            │    attacker-controlled.
            │ 4. Bob&#39;s ASSERT(SPLIT(PKE_DEC(skb, msg))?, n1)? passes — its
            │    inputs are attacker-controlled.
            │ 5. Attacker observes m on the wire.
            │ &lt;span class=&#34;k&#34;&gt;Note: this trace reproduces only because sessions of Bob share&lt;/span&gt;
            │ &lt;span class=&#34;k&#34;&gt;n1. Under per-session freshness this witness does not reproduce;&lt;/span&gt;
            │ &lt;span class=&#34;k&#34;&gt;an attack may still exist by another route.&lt;/span&gt;
            ╰▸ m (m) is obtained by Attacker.
&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;Across the corpus, that label survives on 13 of the 153 narrated traces that involve a substitution, Scuttlebutt and TLS 1.3 among them. It asks the reader the one question the engine cannot answer for itself: can the attacker obtain this principal&amp;rsquo;s fresh value inside the very session it is attacking? For Needham-Schroeder public key the answer is yes, which is why Lowe&amp;rsquo;s attack is real even though its Verifpal witness carries the label. The label is a request for review, not a verdict.&lt;/p&gt;
&lt;p&gt;Concurrency has a price: two sessions cost roughly four times one, because the model handed to the analysis is twice the size. Most models still finish in fractions of a second; a TLS 1.3 handshake takes 7.9 seconds; the slowest analysis in the corpus is the twelve-message Signal model at 307 seconds, which &lt;code&gt;--sessions 1&lt;/code&gt; brings down to 42.&lt;/p&gt;
&lt;h2 id=&#34;measuring-whose-names-are-in-the-trace&#34;&gt;Measuring whose names are in the trace&lt;/h2&gt;
&lt;p&gt;When a query falls, the thing you are shown was built in two stages. The state that falsifies one query usually carries substitutions that other queries needed, so the witness is first minimized: substitutions are dropped one at a time for as long as the violation keeps reproducing, with every probe run against a scratch context that is not permitted to record results. What survives is narrated as numbered steps of exactly three kinds: a substitution, with the displaced honest values shown in parentheses; a gate, a checked primitive that passed on attacker-controlled input, which is the step a skeptical reader most wants to interrogate and so gets a line of its own; and a derivation, read off the record the deduction rules kept as they ran rather than reconstructed afterwards by a pretty-printer guessing at what must have happened.&lt;/p&gt;
&lt;p&gt;The other half of readability is vocabulary. The narrator keeps a table mapping terms back to the names your model gave them, which is why one step of a Signal trace reads &lt;code&gt;Attacker opens e1 with akenc1, obtaining m1.&lt;/code&gt; while the engine&amp;rsquo;s internal record of the same fact runs to seven lines of nested &lt;code&gt;HKDF&lt;/code&gt;, &lt;code&gt;MAC&lt;/code&gt;, &lt;code&gt;HASH&lt;/code&gt; and &lt;code&gt;DH_KEX&lt;/code&gt; applications.&lt;/p&gt;
&lt;p&gt;How readable is the result, next to what the peers produce? The 2019 paper printed Verifpal&amp;rsquo;s FFGG attack trace beside ProVerif&amp;rsquo;s and argued that the former was easier to read. The argument was impressionistic, and the trace it was defending was a state dump. The new paper re-runs that comparison seven years later, against ProVerif 2.05 and Tamarin 1.12.0, and replaces the impression with a measurement: of the identifiers in the region where each tool expresses its counterexample, once each tool&amp;rsquo;s fixed boilerplate is discarded, what fraction occur in the model the user wrote? The strict reading requires a verbatim match; the lenient reading forgives each tool its decorations, so that Verifpal&amp;rsquo;s &lt;code&gt;n1#2&lt;/code&gt; and ProVerif&amp;rsquo;s &lt;code&gt;~M_14&lt;/code&gt; are treated alike.&lt;/p&gt;
&lt;table class=&#34;vp7-matrix&#34;&gt;
&lt;thead&gt;&lt;tr&gt;&lt;th&gt;protocol&lt;/th&gt;&lt;th&gt;Verifpal&lt;/th&gt;&lt;th&gt;ProVerif&lt;/th&gt;&lt;th&gt;Tamarin&#39;s trace graph&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;FFGG&lt;/td&gt;&lt;td&gt;88.6% strict · 100% lenient&lt;/td&gt;&lt;td&gt;36.9% · 95.1%&lt;/td&gt;&lt;td&gt;67.9% · 76.9%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Unauthenticated DH&lt;/td&gt;&lt;td&gt;86.5% · 86.5%&lt;/td&gt;&lt;td&gt;81.8% · 100%&lt;/td&gt;&lt;td&gt;87.5% · 87.5%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;Needham-Schroeder&lt;/td&gt;&lt;td&gt;93.4% · 100%&lt;/td&gt;&lt;td&gt;51.1% · 75.5%&lt;/td&gt;&lt;td&gt;66.0% · 70.0%&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Read the caveats before the numbers, because the paper insists on them. The table is not a scoreboard: under a coarser reading that counts every token, including all of Verifpal&amp;rsquo;s English, Verifpal falls to between 41.9% and 62.9% and Tamarin&amp;rsquo;s graph beats it on all three protocols, so no absolute percentage is stable, and what survives every reading is only where each tool&amp;rsquo;s renaming falls. Tamarin is measured on the trace graph it can serialize rather than on its default output, because its default output under &lt;code&gt;--prove&lt;/code&gt; is a proof script, an artifact that is not a counterexample at all; which artifact a tool hands you by default is itself a finding. And the sharpest caveat cuts against Verifpal: the reason it can keep your names is the same reason its session count is bounded. Analyzing finitely many sessions lets it privilege the first as the one you wrote and suffix the rest, while a tool that quantifies over unboundedly many sessions has no first session to privilege and must rename every instance. Verifpal&amp;rsquo;s readability score and its central limitation have the same cause, and the paper says so in as many words.&lt;/p&gt;
&lt;p&gt;The comparison would be worthless if the tools disagreed about the verdicts underneath, so the paper first checks agreement across fourteen properties. Twelve are unanimous among the tools that ran, and the two exceptions are examined rather than smoothed over: one is ProVerif returning &lt;code&gt;cannot be proved&lt;/code&gt; where the other two exhibit a falsification, the stated price of the coarse abstraction that gives ProVerif its generality, and the other is a Needham-Schroeder-Lowe query on which Verifpal&amp;rsquo;s use-based authentication predicate and ProVerif&amp;rsquo;s injective agreement ask different questions and get different answers, with neither tool wrong about its own question.&lt;/p&gt;
&lt;p&gt;One more property rides along with the measurement. Every trace Verifpal shows you was re-executed, from a pristine state, to the reported violation before being rendered, and that is a theorem in the paper rather than a habit of the implementation. In the 9 cases out of 153 where no minimized witness could be confirmed to re-execute, the trace says that too, on the trace itself.&lt;/p&gt;
&lt;p&gt;This line of work has a specific origin: the paper&amp;rsquo;s acknowledgments credit the applied cryptography students at the American University of Beirut, &amp;ldquo;who are the readers this tool is actually for,&amp;rdquo; and whose difficulty reading attack traces from every tool, ours included, is what turned trace readability from a feature request into a research question.&lt;/p&gt;
&lt;h2 id=&#34;what-it-still-cannot-do&#34;&gt;What it still cannot do&lt;/h2&gt;
&lt;p&gt;The paper gives its limitations a full section, and none of them is small.&lt;/p&gt;
&lt;p&gt;There is no observational equivalence. If the property you care about is that an adversary cannot tell two worlds apart, which is the standard formulation of vote privacy, anonymity and strong secrecy, Verifpal cannot answer it, and no combination of its queries adds up to an answer.&lt;/p&gt;
&lt;p&gt;There are no user-defined equational theories. A protocol whose security depends on algebraic structure the built-in table does not model (XOR-based constructions, pairings, small-subgroup and invalid-curve behavior) cannot be modeled faithfully at all, and may be modeled unfaithfully without any complaint from the tool. The fixed table is deliberate, and it is the one design principle unchanged since 2019: a user-supplied theory can fail to terminate or quietly grant the attacker an equality the actual primitive does not have, no tool can tell a deliberate abstraction from a broken one, and both peers constrain their theories too, so the question was never whether the theory is constrained but who constrains it. We stand by that defense, and it does not shrink the gap. No new table row gives Verifpal XOR.&lt;/p&gt;
&lt;p&gt;And sessions are bounded. The paper calls this the best case against everything else in it: without unbounded replication, a passing query can be weaker than it looks. We accept the case. It bears on what a passing query means, not on the attacks the tool reports, which are sound however the solver behaves, reproduce as printed, and are the attacks its peers report.&lt;/p&gt;
&lt;h2 id=&#34;why-instrument&#34;&gt;Why &amp;ldquo;instrument&amp;rdquo;&lt;/h2&gt;
&lt;p&gt;The paper chose its final word for three properties a measuring instrument has, and it checks Verifpal against each. An instrument&amp;rsquo;s readings are characterized, so that you know in which direction to trust them: Verifpal&amp;rsquo;s are characterized in one direction, since a reported attack is real whatever the solver does, and silence never means security. An instrument&amp;rsquo;s operating range is documented: the section above is the range, and the paper states it at greater length. And an instrument indicates when it is being read outside its range, which is what the labels in this post are: a declined proposal named at its slot, a shared-freshness note printed on the trace it qualifies, a verdict obtained under a declared weakening assumption printed beneath the assumptions in force.&lt;/p&gt;
&lt;p&gt;The paper also names the one place the analogy fails, rather than leaving the reader to find it. The backward search draws whole terms only from a finite basis of terms the protocol computes, and a goal that fails for want of a term outside that basis looks exactly like a goal with no solution, so this incompleteness has no particular place to point to. An instrument&amp;rsquo;s casing states its range completely; Verifpal&amp;rsquo;s cannot. What it does instead is print the fact itself at the top of every active analysis: &amp;ldquo;Where the search needs a whole term at once, it draws only from terms this protocol computes; an attack needing one outside that set is out of reach. Unlike the other limits, this one cannot report where it applied.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;None of this makes Verifpal a substitute for ProVerif or Tamarin, and the paper does not argue that it should be one. The three tools distribute trust differently. Tamarin returns a proof object, the strongest artifact any of them produces, and a reader who can read one should prefer it; reading one, and often completing one, is expert work. ProVerif buys automation and unbounded sessions with an abstraction deliberately coarser than the concrete semantics, and tells you when that abstraction cannot decide your query. Verifpal moves the solver out of the trusted region and hands you an attack you can re-execute by hand without trusting the tool that found it. Its positive claims are the most bounded of the three; its negative claims come with the most checkable object. &amp;ldquo;A tool can be worth using without being worth using alone,&amp;rdquo; as the paper puts it; &amp;ldquo;nobody proposes retiring ProVerif on the grounds that Tamarin proves more.&amp;rdquo;&lt;/p&gt;
&lt;h2 id=&#34;getting-it-and-what-10-commits-to&#34;&gt;Getting it, and what 1.0 commits to&lt;/h2&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;brew tap verifpal.com/source https://github.com/symbolicsoft/verifpal
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;brew install verifpal
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On Windows, &lt;code&gt;scoop bucket add verifpal https://github.com/symbolicsoft/verifpal.git&lt;/code&gt; followed by &lt;code&gt;scoop install verifpal&lt;/code&gt;. The &lt;a href=&#34;https://verifpal.com/workbench/&#34;&gt;Workbench&lt;/a&gt; runs the release in the browser with nothing to install, and &lt;a href=&#34;https://eprint.iacr.org/2026/1654&#34;&gt;the paper&lt;/a&gt; is on the IACR ePrint Archive; its artifact re-runs all three tools on every model and regenerates every number quoted above, the provenance table included.&lt;/p&gt;
&lt;p&gt;Verifpal 1.0 adds no features over 0.80.1. It removes a sentence: the beta warning, in place since May 2020, whose job was to say that the tool&amp;rsquo;s claims about itself had not been written down. They now are, in one paper: the semantics, the deduction rules, the search, the soundness theorem and its 2,751-line residue, the termination bound, the session model, and the limits. The abstract answers the title&amp;rsquo;s question in one line: Verifpal &amp;ldquo;has become a different instrument, not a smaller one,&amp;rdquo; and it &amp;ldquo;belongs alongside its two peers instead of in their place.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;If you model something and a result looks wrong to you, please &lt;a href=&#34;https://github.com/symbolicsoft/verifpal/issues&#34;&gt;open an issue&lt;/a&gt;. The paper&amp;rsquo;s acknowledgments thank the Verifpal users who reported results that looked wrong, because the unlinkability rework began exactly that way. What 1.0 changes is that there is now much more for a report like that to be checked against.&lt;/p&gt;
</content:encoded>
      <category>Software</category>
      <category>Research</category>
      <category>Verifpal</category>
      <category>Formal Verification</category>
      
    </item>
    
    <item>
      <title>Verifpal 0.80.0: Nuancing Weak Cryptography Modeling and Witness-Based Unlinkability</title>
      <link>https://symbolic.software/blog/2026-08-08-verifpal-0-80/</link>
      <pubDate>Sat, 08 Aug 2026 00:00:00 &#43;0000</pubDate>
      <guid>https://symbolic.software/blog/2026-08-08-verifpal-0-80/</guid>
      <description>Verifpal 0.80.0 lets a model declare that a particular piece of cryptography is broken (weak, forgeable, and from a chosen phase onward) instead of approximating the break with a list of leaked values, and decides unlinkability queries by searching for a concrete link witness that it then reports back to you.</description>
      <content:encoded>&lt;p&gt;&lt;a href=&#34;https://verifpal.com&#34;&gt;Verifpal&lt;/a&gt; 0.80.0 is out, a few days behind &lt;a href=&#34;https://symbolic.software/blog/2026-08-05-verifpal-0-70/&#34;&gt;0.70.0&lt;/a&gt;. The two releases do quite different kinds of work, and it is worth separating them at the outset: 0.70.0 changed how you write models, while 0.80.0 changes what a model is able to say.&lt;/p&gt;
&lt;p&gt;There are two additions. The first is that a primitive can now be declared broken, using &lt;code&gt;weak&lt;/code&gt;, &lt;code&gt;forgeable&lt;/code&gt;, and optionally only from a chosen phase onward, so that an assumption which previously had to be approximated with a list of leaked values, or which could not be written down at all, becomes part of the model itself. The second is that unlinkability queries are now decided by searching for a concrete &lt;em&gt;link witness&lt;/em&gt;: a value in the attacker&amp;rsquo;s hands that ties two identities to the same participant. When such a witness exists, Verifpal reports it to you as the explanation.&lt;/p&gt;
&lt;p&gt;Unlike 0.70.0, this release asks you to edit nothing. A model with no annotations behaves exactly as it did before, and there is a test in the suite whose only job is to keep that true. The one thing worth re-checking is your unlinkability verdicts, which can legitimately move in either direction; we will come back to why near the end.&lt;/p&gt;
&lt;h2 id=&#34;why-a-model-might-want-imperfect-cryptography&#34;&gt;Why a model might want imperfect cryptography&lt;/h2&gt;
&lt;p&gt;In a symbolic model, cryptographic primitives are perfect by construction. A hash is never invertible, a signature is never forgeable, an encryption never gives up its plaintext. This is not a simplification we tolerate; it is the entire point of the approach. Because the primitives are ideal, every attack the tool finds has to come from the &lt;em&gt;structure&lt;/em&gt; of the protocol, from a missing check or a reused key or a value that arrives unauthenticated, rather than from a weakness in a cipher. That is what makes symbolic analysis fast, automatic, and worth doing.&lt;/p&gt;
&lt;p&gt;But some of the most important questions we ask about protocols are precisely questions about what happens when a primitive stops being perfect. What survives if this cipher is broken in ten years? What does an implementation flaw in one signature scheme cost the rest of the handshake? These questions are about protocol structure too, since they ask how well the design contains a failure, and until now Verifpal could only approximate them.&lt;/p&gt;
&lt;p&gt;The approximation was &lt;code&gt;leaks&lt;/code&gt;. If you wanted to model &amp;ldquo;the attacker can now break this Diffie-Hellman&amp;rdquo;, you handed the attacker the private values with a &lt;code&gt;leaks&lt;/code&gt; declaration and let the ordinary deduction rules do the rest. The 0.70.0 post leaned on exactly this technique: the hybrid key-exchange argument was made with three files whose only difference was what phase 1 leaked.&lt;/p&gt;
&lt;p&gt;This works well, and it is still the right tool for many jobs. Its limitation is that a &lt;code&gt;leaks&lt;/code&gt; declaration enumerates &lt;em&gt;values&lt;/em&gt;, while the assumption you are actually making is about a &lt;em&gt;capability&lt;/em&gt;. When those two line up exactly, and the list is short, nothing is lost. As a model grows, keeping them lined up becomes your job rather than the tool&amp;rsquo;s. If you later add a sixth Diffie-Hellman exchange and forget to extend the list, the model quietly proves something weaker than you believe it proves, and nothing in the output tells you so.&lt;/p&gt;
&lt;p&gt;From 0.80.0, you can state the capability directly. A primitive call site may carry a bracketed list of declared weakening assumptions, written between the primitive&amp;rsquo;s name and its arguments:&lt;/p&gt;
&lt;div class=&#34;highlight&#34; data-lang=&#34;verifpal&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-verifpal&#34; data-lang=&#34;verifpal&#34;&gt;ga = &lt;span class=&#34;vp-prim&#34;&gt;PUBKEY&lt;/span&gt;[weak](a)                    &lt;span class=&#34;vp-cmt&#34;&gt;// discrete logarithm: holding ga yields a&lt;/span&gt;
s  = &lt;span class=&#34;vp-prim&#34;&gt;SIGN&lt;/span&gt;[forgeable](sk, m)             &lt;span class=&#34;vp-cmt&#34;&gt;// signatures forgeable without sk&lt;/span&gt;
e  = &lt;span class=&#34;vp-prim&#34;&gt;AEAD_ENC&lt;/span&gt;[weak, forgeable from &lt;span class=&#34;vp-kw&#34;&gt;phase&lt;/span&gt; &lt;span class=&#34;vp-lit&#34;&gt;2&lt;/span&gt;](k, m, ad)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;weak&lt;/code&gt; means that confidentiality is lost, so that holding the term yields whatever the term was protecting. &lt;code&gt;forgeable&lt;/code&gt; means that authenticity is lost, so that the term becomes constructible by someone who does not hold its secret. &lt;code&gt;from phase N&lt;/code&gt; means the assumption comes into force from phase N onward, and it binds to the capability it follows; without it, the assumption is in force from the beginning.&lt;/p&gt;
&lt;p&gt;Which primitives accept which assumption, and what precisely falls out of each, is declarative data on the primitive specification, the same table that already defines every rewrite and decomposition rule in the equational theory:&lt;/p&gt;
&lt;table class=&#34;vp7-matrix&#34;&gt;
&lt;thead&gt;&lt;tr&gt;&lt;th&gt;assumption&lt;/th&gt;&lt;th&gt;declared by&lt;/th&gt;&lt;th&gt;holding the term yields&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;weak&lt;/code&gt;&lt;/td&gt;&lt;td&gt;HASH, PW_HASH&lt;/td&gt;&lt;td&gt;a preimage — every argument&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;weak&lt;/code&gt;&lt;/td&gt;&lt;td&gt;ENC, AEAD_ENC, PKE_ENC&lt;/td&gt;&lt;td&gt;the plaintext&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;weak&lt;/code&gt;&lt;/td&gt;&lt;td&gt;KEM_ENCAP&lt;/td&gt;&lt;td&gt;the shared secret&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;weak&lt;/code&gt;&lt;/td&gt;&lt;td&gt;PUBKEY&lt;/td&gt;&lt;td&gt;the private key&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;forgeable&lt;/code&gt;&lt;/td&gt;&lt;td&gt;SIGN, MAC, RINGSIGN, AEAD_ENC&lt;/td&gt;&lt;td&gt;the term itself, forged without its secret&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The last row of the &lt;code&gt;weak&lt;/code&gt; group is worth pausing on, because of how much it gives us for how little. &lt;code&gt;PUBKEY[weak]&lt;/code&gt; says that a public key yields its private half, which is to say that discrete logarithm has been solved. Notice what this implies for Diffie-Hellman: if the attacker takes a public key off the wire and recovers the private value behind it, then it can recompute every shared secret that value was ever used in. That cascade is not something we had to teach the engine. Recall from 0.70.0 that Diffie-Hellman in Verifpal is written with two ordinary primitives, &lt;code&gt;PUBKEY&lt;/code&gt; and &lt;code&gt;DH_KEX&lt;/code&gt;, with commutativity declared as a field on the specification. Because of that, once &lt;code&gt;PUBKEY[weak]&lt;/code&gt; hands the attacker a private value, the existing reconstruction machinery, the same rules that let an attacker rebuild any primitive whose arguments it knows, carries the consequences forward on its own. There is no Diffie-Hellman-specific rule involved anywhere. The model &lt;code&gt;cap_weak_pubkey_dh.vp&lt;/code&gt; exists to pin that down, and its header comment says so explicitly, so that a future change which quietly introduces a special case will be caught.&lt;/p&gt;
&lt;h2 id=&#34;a-worked-example-the-hybrid-argument-written-as-an-assumption&#34;&gt;A worked example: the hybrid argument, written as an assumption&lt;/h2&gt;
&lt;p&gt;Two new models in &lt;code&gt;examples/messaging/&lt;/code&gt; describe PQXDH, &lt;a href=&#34;https://signal.org/docs/specifications/pqxdh/&#34;&gt;Signal&amp;rsquo;s current session setup&lt;/a&gt;, which is X3DH with a signed KEM prekey added and the KEM shared secret mixed into the master secret alongside the four Diffie-Hellman values.&lt;/p&gt;
&lt;p&gt;The first model, &lt;code&gt;pqxdh.vp&lt;/code&gt;, asks the harvest-now-decrypt-later question in the older style. The attacker records the handshake as it goes past, and then in phase 1 every Diffie-Hellman private value leaks, five of them, named one by one. The message has to survive on the KEM leg alone, and it does.&lt;/p&gt;
&lt;p&gt;The second model, &lt;code&gt;pqxdh-weak.vp&lt;/code&gt;, is the same protocol with the same adversary stated as an assumption instead. Every Diffie-Hellman public key is annotated, and nothing leaks at all:&lt;/p&gt;
&lt;div class=&#34;highlight&#34; data-lang=&#34;verifpal&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-verifpal&#34; data-lang=&#34;verifpal&#34;&gt;&lt;span class=&#34;vp-kw&#34;&gt;principal&lt;/span&gt; &lt;span class=&#34;vp-prin&#34;&gt;Bob&lt;/span&gt;[
	&lt;span class=&#34;vp-kw&#34;&gt;knows&lt;/span&gt; &lt;span class=&#34;vp-kw&#34;&gt;private&lt;/span&gt; blongterm, bs
	&lt;span class=&#34;vp-kw&#34;&gt;generates&lt;/span&gt; bo, dkb
	gblongterm = &lt;span class=&#34;vp-prim&#34;&gt;PUBKEY&lt;/span&gt;[weak from &lt;span class=&#34;vp-kw&#34;&gt;phase&lt;/span&gt; &lt;span class=&#34;vp-lit&#34;&gt;1&lt;/span&gt;](blongterm)
	gbs = &lt;span class=&#34;vp-prim&#34;&gt;PUBKEY&lt;/span&gt;[weak from &lt;span class=&#34;vp-kw&#34;&gt;phase&lt;/span&gt; &lt;span class=&#34;vp-lit&#34;&gt;1&lt;/span&gt;](bs)
	gbo = &lt;span class=&#34;vp-prim&#34;&gt;PUBKEY&lt;/span&gt;[weak from &lt;span class=&#34;vp-kw&#34;&gt;phase&lt;/span&gt; &lt;span class=&#34;vp-lit&#34;&gt;1&lt;/span&gt;](bo)
	ekb = &lt;span class=&#34;vp-prim&#34;&gt;PUBKEY&lt;/span&gt;(dkb)
	gbssig = &lt;span class=&#34;vp-prim&#34;&gt;SIGN&lt;/span&gt;(blongterm, &lt;span class=&#34;vp-prim&#34;&gt;HASH&lt;/span&gt;(c2, gbs))
	ekbsig = &lt;span class=&#34;vp-prim&#34;&gt;SIGN&lt;/span&gt;(blongterm, &lt;span class=&#34;vp-prim&#34;&gt;HASH&lt;/span&gt;(c3, ekb))
]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The one line to look at closely is &lt;code&gt;ekb&lt;/code&gt;, the KEM encapsulation key, which carries no annotation. That omission is the hybrid argument in its entirety: discrete logarithm falls, the KEM does not, and the model claims the message survives on exactly that difference. The &lt;code&gt;phase[1]&lt;/code&gt; block in this model is empty, because the annotation is carrying the whole adversary by itself.&lt;/p&gt;
&lt;div class=&#34;vp7-term&#34;&gt;
&lt;div class=&#34;vp7-term-bar&#34;&gt;
&lt;div class=&#34;vp7-term-left&#34;&gt;&lt;div class=&#34;vp7-traffic&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;span class=&#34;vp7-fname&#34;&gt;verifpal verify &lt;b&gt;pqxdh-weak.vp&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;span class=&#34;vp7-verdict&#34;&gt;2 of 2 verified · 0.03s&lt;/span&gt;
&lt;/div&gt;
&lt;pre&gt;&lt;span class=&#34;k&#34;&gt;  Warning ▲ Analysis performed under 5 declared weakening assumptions:&lt;/span&gt;
&lt;span class=&#34;k&#34;&gt;  Warning ▲ PUBKEY[weak from phase 1](alongterm)&lt;/span&gt;
&lt;span class=&#34;k&#34;&gt;  Warning ▲ PUBKEY[weak from phase 1](blongterm)&lt;/span&gt;
&lt;span class=&#34;k&#34;&gt;  Warning ▲ PUBKEY[weak from phase 1](bs)&lt;/span&gt;
&lt;span class=&#34;k&#34;&gt;  Warning ▲ PUBKEY[weak from phase 1](bo)&lt;/span&gt;
&lt;span class=&#34;k&#34;&gt;  Warning ▲ PUBKEY[weak from phase 1](ae1)&lt;/span&gt;
&lt;span class=&#34;d&#34;&gt;&amp;nbsp;&lt;/span&gt;
&lt;span class=&#34;ok&#34;&gt;     Pass ✓&lt;/span&gt; confidentiality? m1
&lt;span class=&#34;ok&#34;&gt;     Pass ✓&lt;/span&gt; authentication? Alice -&amp;gt; Bob: e1
&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;The warning block above the results is deliberate, and we would like to explain the reasoning behind it. A passing result obtained under a weakening assumption is a different claim from a passing result obtained without one, and results travel: they get pasted into review documents, quoted in issues, and screenshotted into slides. So a verification performed under declared assumptions now says so every time, listing each annotated term directly above the verdicts. The &lt;code&gt;internal-json&lt;/code&gt; interface carries the same list on every result it emits, and the &lt;a href=&#34;https://verifpal.com/workbench/&#34;&gt;Workbench&lt;/a&gt; displays it, so the caveat stays attached to the verdict wherever the verdict goes.&lt;/p&gt;
&lt;table class=&#34;vp7-matrix&#34;&gt;
&lt;thead&gt;&lt;tr&gt;&lt;th&gt;model&lt;/th&gt;&lt;th&gt;the phase-1 adversary is written as&lt;/th&gt;&lt;th&gt;confidentiality? m1&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;pqxdh.vp&lt;/td&gt;&lt;td&gt;&lt;code&gt;leaks alongterm, ae1&lt;/code&gt; and &lt;code&gt;leaks blongterm, bs, bo&lt;/code&gt;&lt;/td&gt;&lt;td class=&#34;y&#34;&gt;holds&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;pqxdh-weak.vp&lt;/td&gt;&lt;td&gt;&lt;code&gt;PUBKEY[weak from phase 1]&lt;/code&gt; on every DH key; nothing leaks&lt;/td&gt;&lt;td class=&#34;y&#34;&gt;holds&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Both models report the same result code, which is a good sign: the new mechanism reproduces what the old technique already established. What differs is what each model claims, and how each behaves as it grows. The annotated version covers any key derived through an annotated &lt;code&gt;PUBKEY&lt;/code&gt;, whether or not the author remembered that particular key was there.&lt;/p&gt;
&lt;figure class=&#34;vp7fig reveal&#34;&gt;
&lt;div class=&#34;vp7-scroll&#34;&gt;
&lt;svg viewBox=&#34;0 0 880 350&#34; role=&#34;img&#34; aria-label=&#34;Left: at phase 0, the five inputs to the PQXDH master secret are all locked and the message is sealed. Right: at phase 1, discrete logarithm falls and the four Diffie-Hellman legs open, but the KEM leg, never annotated, stays locked and the message stays sealed.&#34;&gt;
  &lt;text class=&#34;vp7-lab&#34; x=&#34;14&#34; y=&#34;16&#34;&gt;Phase 0 — every leg locked, the wire recorded&lt;/text&gt;
  &lt;line class=&#34;vp7-line soft&#34; x1=&#34;14&#34; y1=&#34;24&#34; x2=&#34;406&#34; y2=&#34;24&#34;/&gt;
  &lt;rect class=&#34;vp7-box&#34; x=&#34;14&#34; y=&#34;36&#34; width=&#34;236&#34; height=&#34;26&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t&#34; x=&#34;24&#34; y=&#34;54&#34;&gt;DH_KEX(gbs, alongterm)&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;258&#34; y=&#34;54&#34;&gt;locked&lt;/text&gt;
  &lt;rect class=&#34;vp7-box&#34; x=&#34;14&#34; y=&#34;70&#34; width=&#34;236&#34; height=&#34;26&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t&#34; x=&#34;24&#34; y=&#34;88&#34;&gt;DH_KEX(gblongterm, ae1)&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;258&#34; y=&#34;88&#34;&gt;locked&lt;/text&gt;
  &lt;rect class=&#34;vp7-box&#34; x=&#34;14&#34; y=&#34;104&#34; width=&#34;236&#34; height=&#34;26&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t&#34; x=&#34;24&#34; y=&#34;122&#34;&gt;DH_KEX(gbs, ae1)&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;258&#34; y=&#34;122&#34;&gt;locked&lt;/text&gt;
  &lt;rect class=&#34;vp7-box&#34; x=&#34;14&#34; y=&#34;138&#34; width=&#34;236&#34; height=&#34;26&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t&#34; x=&#34;24&#34; y=&#34;156&#34;&gt;DH_KEX(gbo, ae1)&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;258&#34; y=&#34;156&#34;&gt;locked&lt;/text&gt;
  &lt;rect class=&#34;vp7-box good&#34; x=&#34;14&#34; y=&#34;172&#34; width=&#34;236&#34; height=&#34;26&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t good&#34; x=&#34;24&#34; y=&#34;190&#34;&gt;KEM_ENCAP(ekb, r)&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;258&#34; y=&#34;190&#34;&gt;locked&lt;/text&gt;
  &lt;line class=&#34;vp7-line&#34; x1=&#34;132&#34; y1=&#34;198&#34; x2=&#34;132&#34; y2=&#34;214&#34;/&gt;
  &lt;polygon points=&#34;128,214 136,214 132,222&#34; fill=&#34;#1a1a1a&#34;/&gt;
  &lt;rect class=&#34;vp7-box good&#34; x=&#34;14&#34; y=&#34;222&#34; width=&#34;236&#34; height=&#34;30&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t good b&#34; x=&#34;26&#34; y=&#34;242&#34;&gt;m1 sealed under akenc&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;14&#34; y=&#34;292&#34;&gt;The attacker keeps everything it sees:&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;14&#34; y=&#34;310&#34;&gt;gbs, gbo, ekb, ct, e1.&lt;/text&gt;
  &lt;line class=&#34;vp7-region&#34; x1=&#34;430&#34; y1=&#34;8&#34; x2=&#34;430&#34; y2=&#34;332&#34;/&gt;
  &lt;text class=&#34;vp7-lab&#34; x=&#34;452&#34; y=&#34;16&#34;&gt;Phase 1 — discrete logarithm falls&lt;/text&gt;
  &lt;line class=&#34;vp7-line soft&#34; x1=&#34;452&#34; y1=&#34;24&#34; x2=&#34;866&#34; y2=&#34;24&#34;/&gt;
  &lt;rect class=&#34;vp7-box hot&#34; x=&#34;452&#34; y=&#34;36&#34; width=&#34;236&#34; height=&#34;26&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t&#34; x=&#34;462&#34; y=&#34;54&#34;&gt;DH_KEX(gbs, alongterm)&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm hot&#34; x=&#34;696&#34; y=&#34;54&#34;&gt;open · alongterm recovered&lt;/text&gt;
  &lt;rect class=&#34;vp7-box hot&#34; x=&#34;452&#34; y=&#34;70&#34; width=&#34;236&#34; height=&#34;26&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t&#34; x=&#34;462&#34; y=&#34;88&#34;&gt;DH_KEX(gblongterm, ae1)&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm hot&#34; x=&#34;696&#34; y=&#34;88&#34;&gt;open · blongterm, ae1&lt;/text&gt;
  &lt;rect class=&#34;vp7-box hot&#34; x=&#34;452&#34; y=&#34;104&#34; width=&#34;236&#34; height=&#34;26&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t&#34; x=&#34;462&#34; y=&#34;122&#34;&gt;DH_KEX(gbs, ae1)&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm hot&#34; x=&#34;696&#34; y=&#34;122&#34;&gt;open · bs, ae1&lt;/text&gt;
  &lt;rect class=&#34;vp7-box hot&#34; x=&#34;452&#34; y=&#34;138&#34; width=&#34;236&#34; height=&#34;26&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t&#34; x=&#34;462&#34; y=&#34;156&#34;&gt;DH_KEX(gbo, ae1)&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm hot&#34; x=&#34;696&#34; y=&#34;156&#34;&gt;open · bo, ae1&lt;/text&gt;
  &lt;rect class=&#34;vp7-box good&#34; x=&#34;452&#34; y=&#34;172&#34; width=&#34;236&#34; height=&#34;26&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t good&#34; x=&#34;462&#34; y=&#34;190&#34;&gt;KEM_ENCAP(ekb, r)&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm good&#34; x=&#34;696&#34; y=&#34;190&#34;&gt;locked · never annotated&lt;/text&gt;
  &lt;line class=&#34;vp7-line&#34; x1=&#34;570&#34; y1=&#34;198&#34; x2=&#34;570&#34; y2=&#34;214&#34;/&gt;
  &lt;polygon points=&#34;566,214 574,214 570,222&#34; fill=&#34;#1a1a1a&#34;/&gt;
  &lt;rect class=&#34;vp7-box good&#34; x=&#34;452&#34; y=&#34;222&#34; width=&#34;340&#34; height=&#34;46&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t good b&#34; x=&#34;464&#34; y=&#34;242&#34;&gt;confidentiality? m1 holds&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;464&#34; y=&#34;258&#34;&gt;carried by the KEM leg alone&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;452&#34; y=&#34;292&#34;&gt;pqxdh.vp writes this adversary as five leaked values;&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;452&#34; y=&#34;310&#34;&gt;pqxdh-weak.vp writes it as one assumption.&lt;/text&gt;
&lt;/svg&gt;
&lt;/div&gt;
&lt;figcaption&gt;&lt;b&gt;What the annotation does to the master secret.&lt;/b&gt; All five inputs to PQXDH&#39;s master secret are unavailable to the attacker during phase 0. When discrete logarithm falls in phase 1, the four Diffie-Hellman legs open together, including any key that a hand-maintained list of leaks might have missed, while the KEM leg, which was never annotated, stays closed and keeps &lt;code&gt;m1&lt;/code&gt; sealed. This is the hybrid argument, and the model now states it in those terms.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id=&#34;assumptions-that-arrive-later&#34;&gt;Assumptions that arrive later&lt;/h2&gt;
&lt;p&gt;There is a class of assumption that &lt;code&gt;leaks&lt;/code&gt; approximates awkwardly, and one that it cannot express at all. The second case is easiest to see in a small model, &lt;code&gt;cap_weak_phase_delayed.vp&lt;/code&gt;, which is about twenty lines long:&lt;/p&gt;
&lt;div class=&#34;highlight&#34; data-lang=&#34;verifpal&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-verifpal&#34; data-lang=&#34;verifpal&#34;&gt;&lt;span class=&#34;vp-kw&#34;&gt;principal&lt;/span&gt; &lt;span class=&#34;vp-prin&#34;&gt;Alice&lt;/span&gt;[
	&lt;span class=&#34;vp-kw&#34;&gt;knows&lt;/span&gt; &lt;span class=&#34;vp-kw&#34;&gt;private&lt;/span&gt; k
	&lt;span class=&#34;vp-kw&#34;&gt;knows&lt;/span&gt; &lt;span class=&#34;vp-kw&#34;&gt;private&lt;/span&gt; m_now
	&lt;span class=&#34;vp-kw&#34;&gt;knows&lt;/span&gt; &lt;span class=&#34;vp-kw&#34;&gt;private&lt;/span&gt; m_later
	e_now = &lt;span class=&#34;vp-prim&#34;&gt;AEAD_ENC&lt;/span&gt;(k, m_now, &lt;span class=&#34;vp-lit&#34;&gt;nil&lt;/span&gt;)
	e_later = &lt;span class=&#34;vp-prim&#34;&gt;AEAD_ENC&lt;/span&gt;[weak from &lt;span class=&#34;vp-kw&#34;&gt;phase&lt;/span&gt; &lt;span class=&#34;vp-lit&#34;&gt;2&lt;/span&gt;](k, m_later, &lt;span class=&#34;vp-lit&#34;&gt;nil&lt;/span&gt;)
]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Read the two encryptions carefully: they use the &lt;em&gt;same key&lt;/em&gt;. The result code is &lt;code&gt;c0c1&lt;/code&gt;, meaning that &lt;code&gt;m_now&lt;/code&gt; stays confidential for the whole run, while &lt;code&gt;m_later&lt;/code&gt;, recorded off the wire back in phase 0, opens once phase 2 arrives.&lt;/p&gt;
&lt;p&gt;It is worth trying to write this with &lt;code&gt;leaks&lt;/code&gt; before reading on, because the exercise is short and the failure is instructive. Leaking &lt;code&gt;k&lt;/code&gt; breaks both messages, since both were encrypted under it. Leaking &lt;code&gt;m_later&lt;/code&gt; assumes the very thing we wanted the tool to derive. There is simply no value whose disclosure expresses &amp;ldquo;the attacker becomes able to invert this particular encryption, at this particular time&amp;rdquo;. The assumption is about an ability rather than about a secret, and it is scheduled rather than immediate.&lt;/p&gt;
&lt;p&gt;This is the distinction the two mechanisms now divide between them. &lt;code&gt;leaks&lt;/code&gt; hands the attacker values. A capability grants the attacker abilities, optionally on a timetable. Harvest-now-decrypt-later scenarios are the obvious beneficiaries, since their entire premise is that data collected today becomes readable under a capability the adversary does not yet have.&lt;/p&gt;
&lt;h2 id=&#34;why-weak-and-forgeable-are-two-separate-assumptions&#34;&gt;Why &lt;code&gt;weak&lt;/code&gt; and &lt;code&gt;forgeable&lt;/code&gt; are two separate assumptions&lt;/h2&gt;
&lt;p&gt;The 0.70.0 post made a point of keeping properties apart: reading a message and forging one are different powers, and Verifpal reports them as different queries. When we designed capabilities, the same separation had to hold for assumptions, which is why there are two words here rather than one general notion of &amp;ldquo;broken&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;The case that settles the argument is &lt;code&gt;AEAD_ENC[forgeable]&lt;/code&gt;: a ciphertext that an attacker can produce but still cannot read. That combination sounds unusual, but it is exactly what an authenticity failure without a confidentiality failure looks like, and a single combined assumption could not express it. The model &lt;code&gt;cap_forgeable_aead.vp&lt;/code&gt; keeps the two from collapsing back together:&lt;/p&gt;
&lt;div class=&#34;vp7-term&#34;&gt;
&lt;div class=&#34;vp7-term-bar&#34;&gt;
&lt;div class=&#34;vp7-term-left&#34;&gt;&lt;div class=&#34;vp7-traffic&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;span class=&#34;vp7-fname&#34;&gt;verifpal verify &lt;b&gt;cap_forgeable_aead.vp&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;span class=&#34;vp7-verdict fail&#34;&gt;1 of 2 failed&lt;/span&gt;
&lt;/div&gt;
&lt;pre&gt;&lt;span class=&#34;k&#34;&gt;  Warning ▲ Analysis performed under 1 declared weakening assumption:&lt;/span&gt;
&lt;span class=&#34;k&#34;&gt;  Warning ▲ AEAD_ENC[forgeable](k, m, ad)&lt;/span&gt;
&lt;span class=&#34;d&#34;&gt;&amp;nbsp;&lt;/span&gt;
&lt;span class=&#34;ok&#34;&gt;     Pass ✓&lt;/span&gt; confidentiality? m
&lt;span class=&#34;no&#34;&gt;     Fail ✗&lt;/span&gt; authentication? Alice -&amp;gt; Bob: e
            ╭─ Attack trace:
            │ 1. &lt;span class=&#34;m&#34;&gt;Attacker replaces e (sent by Alice to Bob) with&lt;/span&gt;
            │    &lt;span class=&#34;m&#34;&gt;AEAD_ENC(k, nil, ad).&lt;/span&gt; &lt;span class=&#34;d&#34;&gt;(e was AEAD_ENC(k, m, ad))&lt;/span&gt;
            │ 2. Bob&#39;s AEAD_DEC(k, e, ad)? passes — its inputs are
            │    attacker-controlled.
            ╰▸ e (e), sent by Attacker and not by Alice, is successfully
               used in AEAD_DEC(k, e, ad)? within Bob&#39;s state.
&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;Step 2 of that trace is the one to examine. Bob&amp;rsquo;s decryption is &lt;em&gt;checked&lt;/em&gt;, marked with &lt;code&gt;?&lt;/code&gt;, which normally means that a failure halts him. Here it passes, on a ciphertext Alice never produced, because the assumption says such ciphertexts can be manufactured. The narrator prints that step on a line of its own rather than leaving it implicit, since it is the step a careful reader will want to see justified. Meanwhile &lt;code&gt;confidentiality? m&lt;/code&gt; still passes, because forgeability says nothing whatsoever about reading. The model&amp;rsquo;s header comment records what it is guarding: if that confidentiality query ever starts failing, &lt;code&gt;weak&lt;/code&gt; and &lt;code&gt;forgeable&lt;/code&gt; have been merged somewhere in the engine and the distinction has stopped doing its job.&lt;/p&gt;
&lt;h2 id=&#34;what-happens-when-an-assumption-does-not-apply&#34;&gt;What happens when an assumption does not apply&lt;/h2&gt;
&lt;p&gt;A declared assumption reads, to anyone reviewing the model, like a check that was carried out. That creates a responsibility for the tool. If &lt;code&gt;SIGN[weak]&lt;/code&gt; were to parse and then quietly do nothing, every reader of that model would come away believing something about it that is not true, and they would have no way of noticing. An annotation that silently does nothing is therefore more dangerous than no annotation at all.&lt;/p&gt;
&lt;p&gt;For that reason, every capability is validated against the primitive specification when the model is loaded, and the diagnostics try to name the assumption you probably intended:&lt;/p&gt;
&lt;div class=&#34;vp7-term&#34;&gt;
&lt;div class=&#34;vp7-term-bar&#34;&gt;
&lt;div class=&#34;vp7-term-left&#34;&gt;&lt;div class=&#34;vp7-traffic&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;span class=&#34;vp7-fname&#34;&gt;three annotations that do not get to run&lt;/span&gt;&lt;/div&gt;
&lt;span class=&#34;vp7-verdict fail&#34;&gt;refused at load time&lt;/span&gt;
&lt;/div&gt;
&lt;pre&gt;&lt;span class=&#34;no&#34;&gt;Error:&lt;/span&gt; cap_err_sign_weak.vp:11:2: sanity error: SIGN provides authenticity,
not confidentiality; did you mean `SIGN[forgeable]`?
   s = SIGN[weak](sk, m)
   &lt;span class=&#34;d&#34;&gt;^^^^^^^^^^^^^^^^^^^^^&lt;/span&gt;
&lt;span class=&#34;d&#34;&gt;&amp;nbsp;&lt;/span&gt;
&lt;span class=&#34;no&#34;&gt;Error:&lt;/span&gt; cap_err_hash_forgeable.vp:10:2: sanity error: HASH has no secret
argument; anyone who knows its inputs can compute it
   h = HASH[forgeable](m)
   &lt;span class=&#34;d&#34;&gt;^^^^^^^^^^^^^^^^^^^^^^&lt;/span&gt;
&lt;span class=&#34;d&#34;&gt;&amp;nbsp;&lt;/span&gt;
&lt;span class=&#34;no&#34;&gt;Error:&lt;/span&gt; cap_err_aead_malleable.vp:12:2: sanity error: malleability of an
authenticated primitive is an authenticity break; did you mean
`AEAD_ENC[forgeable]`?
   e = AEAD_ENC[malleable](k, m, ad)
   &lt;span class=&#34;d&#34;&gt;^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^&lt;/span&gt;
&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;Each message tries to teach something rather than merely refuse. &lt;code&gt;SIGN[weak]&lt;/code&gt; is rejected with a reminder of what a signature is for. &lt;code&gt;HASH[forgeable]&lt;/code&gt; is rejected because a hash has no secret argument, so there is nothing for forgeability to bypass: anyone who knows the inputs can already compute the output. The same principle extends to time: &lt;code&gt;weak from phase 5&lt;/code&gt; in a model that never reaches phase 5 is a hard error, not an assumption that quietly never fires.&lt;/p&gt;
&lt;p&gt;The third message deserves a fuller explanation, since &lt;code&gt;malleable&lt;/code&gt; is in the vocabulary and parses correctly, yet no primitive declares it, so every use of it is rejected. It was designed and partially built during this release cycle and then deliberately held back. The attacks that malleability enables generally require two coordinated substitutions on the wire, and the solver&amp;rsquo;s goal-directed search cannot presently derive the ciphertext goal that would force them. Shipping the annotation anyway would have meant advertising a class of attack that the engine could not actually find, and a model author would read a passing result as evidence of resistance to malleability when it was really evidence of a search limitation. Rejecting the word with an explanation seemed much better than that, so it stays reserved until the solver can honor it.&lt;/p&gt;
&lt;p&gt;Two further guarantees are worth stating plainly, because they are what let you adopt this feature without auditing your existing work. First, &lt;code&gt;weak&lt;/code&gt;, &lt;code&gt;forgeable&lt;/code&gt;, &lt;code&gt;malleable&lt;/code&gt; and &lt;code&gt;from&lt;/code&gt; are contextual keywords: they are recognised only inside the parameter brackets, where no constant can appear. A model that already uses &lt;code&gt;weak&lt;/code&gt; or &lt;code&gt;from&lt;/code&gt; as a constant name is unaffected. Second, a model with no annotations produces byte-identical behaviour to before, and &lt;code&gt;cap_noop_annotated.vp&lt;/code&gt; holds an annotated model to the exact verdicts of its unannotated twin.&lt;/p&gt;
&lt;p&gt;That second guarantee rests on a design decision that we think is interesting in its own right. Capabilities are deliberately invisible to term hashing and to equivalence, so an annotated term and its unannotated twin count as the same cryptographic object throughout the engine. The assumptions live instead in a model-level index that the engine consults separately. The reason is that attacker knowledge deduplicates values by equivalence: if the engine read capabilities off whichever copy of a term it happened to be holding, then the outcome could depend on whether the annotated or the unannotated copy of a value reached the attacker first, which in turn depends on the order in which principals were walked. Keeping the annotation out of the term and in an index removes that possibility entirely. And because capabilities feed the ordinary deduction pipeline, the soundness argument from 0.70.0 carries over untouched: the solver still only proposes, and validation still re-executes every proposal against real attacker knowledge before any result is recorded.&lt;/p&gt;
&lt;h2 id=&#34;unlinkability-decided-by-finding-a-witness&#34;&gt;Unlinkability, decided by finding a witness&lt;/h2&gt;
&lt;p&gt;The other half of this release concerns a query we had been deciding badly. &lt;code&gt;unlinkability?&lt;/code&gt; asks whether an attacker can tell that two values belong to the same party, which is the property that pseudonymous identifiers, rotating beacons and anonymous credentials are built to provide. Verifpal decided it with two structural tests, and we now think both were the wrong shape.&lt;/p&gt;
&lt;p&gt;The first test failed the query whenever a queried value contained no fresh constant. That reports a property of the model as though it were an attack, and worse, it disagreed with Verifpal&amp;rsquo;s own &lt;code&gt;freshness?&lt;/code&gt; query, which asks about the very same thing and asks it correctly. The second test failed the query when two queried values were outputs of a single primitive application that the attacker could rebuild. Because it could only ever compare siblings from one application, linkability &lt;em&gt;across&lt;/em&gt; sessions or epochs, which is the kind privacy protocols actually worry about, was invisible to it by construction. Neither test consulted what the attacker had observed, which is a strange thing for a decision about an attacker&amp;rsquo;s knowledge.&lt;/p&gt;
&lt;p&gt;Verifpal now decides unlinkability the way the User Manual has always defined it, by searching for a concrete link witness: a value the attacker holds which ties both queried values to one participant. There are three kinds of witness, tried in order of cost:&lt;/p&gt;
&lt;table class=&#34;vp7-matrix&#34;&gt;
&lt;thead&gt;&lt;tr&gt;&lt;th&gt;witness&lt;/th&gt;&lt;th&gt;the attacker exhibits&lt;/th&gt;&lt;th&gt;the trace reads&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;observed equality&lt;/td&gt;&lt;td&gt;both values in hand, and they are the same secret-dependent value&lt;/td&gt;&lt;td&gt;&#34;…because both are the same value (tok1)&#34;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;identifying check&lt;/td&gt;&lt;td&gt;a checked primitive that succeeds over both values under one participant&#39;s key&lt;/td&gt;&lt;td&gt;&#34;…because SIGNVERIF succeeds for both under ga&#34;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;common secret origin&lt;/td&gt;&lt;td&gt;one held secret from which it can recompute both values&lt;/td&gt;&lt;td&gt;&#34;…via sk0a&#34;&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The third kind is the one the old procedure could not see at all, and a contact-tracing model shows why it matters. In &lt;code&gt;dp3t_root_leaked.vp&lt;/code&gt;, each phone derives its broadcast identifiers from a daily seed, and each day&amp;rsquo;s seed is the hash of the previous one:&lt;/p&gt;
&lt;div class=&#34;highlight&#34; data-lang=&#34;verifpal&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-verifpal&#34; data-lang=&#34;verifpal&#34;&gt;EphID00A, EphID01A, EphID02A = &lt;span class=&#34;vp-prim&#34;&gt;HKDF&lt;/span&gt;(&lt;span class=&#34;vp-lit&#34;&gt;nil&lt;/span&gt;, SK0A, BroadcastKey)
&lt;span class=&#34;vp-cmt&#34;&gt;// the next day:&lt;/span&gt;
SK1A = &lt;span class=&#34;vp-prim&#34;&gt;HASH&lt;/span&gt;(SK0A)
EphID10A, EphID11A, EphID12A = &lt;span class=&#34;vp-prim&#34;&gt;HKDF&lt;/span&gt;(&lt;span class=&#34;vp-lit&#34;&gt;nil&lt;/span&gt;, SK1A, BroadcastKey)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;EphID00A&lt;/code&gt; and &lt;code&gt;EphID10A&lt;/code&gt; come from different days, and from different HKDF applications, so the old sibling test had no way to relate them. Leak the day-zero seed, and the new procedure finds the connection immediately:&lt;/p&gt;
&lt;div class=&#34;vp7-term&#34;&gt;
&lt;div class=&#34;vp7-term-bar&#34;&gt;
&lt;div class=&#34;vp7-term-left&#34;&gt;&lt;div class=&#34;vp7-traffic&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;span class=&#34;vp7-fname&#34;&gt;verifpal verify &lt;b&gt;dp3t_root_leaked.vp&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;span class=&#34;vp7-verdict fail&#34;&gt;one query of three&lt;/span&gt;
&lt;/div&gt;
&lt;pre&gt;&lt;span class=&#34;no&#34;&gt;     Fail ✗&lt;/span&gt; unlinkability? ephid00a, ephid10a
            ╭─ Attack trace:
            │ 1. &lt;span class=&#34;k&#34;&gt;Attacker is handed sk0a by a leaks declaration.&lt;/span&gt;
            ╰▸ &lt;span class=&#34;no&#34;&gt;Attacker links ephid00a and ephid10a via sk0a.&lt;/span&gt;
&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;A single leaked seed links every identifier that phone will ever broadcast, in every epoch, because &lt;code&gt;HASH(SK0A)&lt;/code&gt; and each &lt;code&gt;HKDF&lt;/code&gt; above it can be recomputed from the root. This is seed-compromise linkability, and being able to name the seed as the witness is what makes the result useful: the fix is visible in the trace itself.&lt;/p&gt;
&lt;h2 id=&#34;two-conditions-that-keep-a-witness-honest&#34;&gt;Two conditions that keep a witness honest&lt;/h2&gt;
&lt;p&gt;A search that reports links is only useful if the links are real, so two conditions gate every witness. They are worth understanding if you write privacy models, because they explain results that might otherwise look conservative.&lt;/p&gt;
&lt;p&gt;The first is observability. Both queried values must have travelled on the wire or been leaked. A value the attacker cannot reach cannot be linked, no matter what it is derived from, since linkage is a statement about what an observer can conclude. Related to this, when the search tries to reconstruct values from a candidate secret, it first withholds the queried values themselves from the attacker&amp;rsquo;s knowledge. The reason is worth stating: merely receiving a value tells you nothing about whose it is. Only being able to &lt;em&gt;recompute&lt;/em&gt; it from something that identifies a participant does. Reconstructing a value the attacker was simply handed would be circular, and the search declines to do it.&lt;/p&gt;
&lt;p&gt;The second condition is that neither queried value may be attacker-authored. An active attacker that overwrites two slots with one value of its own has manufactured the very equality it would then be reporting, which is not an observation about the protocol at all. &lt;code&gt;unlink_injected_equality.vp&lt;/code&gt; pins that this is never counted as a witness.&lt;/p&gt;
&lt;p&gt;That restriction applies to the linked values themselves, not to the attacker&amp;rsquo;s activity in general. Attacker actions upstream still count, and they should. In &lt;code&gt;unlink_active_links.vp&lt;/code&gt;, the attacker substitutes its own Diffie-Hellman key into the handshake, and the session key that results becomes the common origin for two tokens that the victim went on to compute honestly:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;│ 1. Attacker replaces ga (sent by Alice to Bob) with PUBKEY(nil). (ga was PUBKEY(a))
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;│ 2. Attacker observes gb on the wire.
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;│ 3. Attacker constructs k from gb, nil.
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;╰▸ Attacker links tok1 and tok2 via k.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Both queried tokens are Bob&amp;rsquo;s own values, computed by Bob, so the witness describes something real about the protocol&amp;rsquo;s structure, not an artifact of the query. The boundary sits precisely at authorship of the linked values, and nowhere earlier.&lt;/p&gt;
&lt;p&gt;There is one more restriction, and it is our favourite illustration of how much care the identifying-check witness needs. Consider two models with the same structure and opposite verdicts:&lt;/p&gt;
&lt;table class=&#34;vp7-matrix&#34;&gt;
&lt;thead&gt;&lt;tr&gt;&lt;th&gt;model&lt;/th&gt;&lt;th&gt;Bob verifies with&lt;/th&gt;&lt;th&gt;unlinkability?&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;unlink_signature_links.vp&lt;/td&gt;&lt;td&gt;&lt;code&gt;SIGNVERIF(ga, m, s)?&lt;/code&gt;&lt;/td&gt;&lt;td class=&#34;n&#34;&gt;contradicted — SIGNVERIF succeeds for both under ga&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;unlink_ringsign.vp&lt;/td&gt;&lt;td&gt;&lt;code&gt;RINGSIGNVERIF(ga, gb, gc, m, r)?&lt;/code&gt;&lt;/td&gt;&lt;td class=&#34;y&#34;&gt;holds&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Two ordinary signatures verifying under one key are linkable, and the reasoning is exactly what a student would give: the verification key names the signer, so a successful &lt;code&gt;SIGNVERIF&lt;/code&gt; over both signatures identifies them as coming from the same party. Two ring signatures over the same ring are &lt;em&gt;not&lt;/em&gt; linkable, even though &lt;code&gt;RINGSIGNVERIF&lt;/code&gt; also succeeds over both, because a successful ring verification names a set of possible signers and never a member of it. The engine knows the difference because the specification table records which argument positions identify a participant: &lt;code&gt;SIGNVERIF&lt;/code&gt; declares its key argument as identifying, and &lt;code&gt;RINGSIGNVERIF&lt;/code&gt; deliberately declares none. That declared emptiness is the only thing standing between this query and a false attack against the one primitive whose entire purpose is unlinkability, which is why the two models sit next to each other in the test suite.&lt;/p&gt;
&lt;figure class=&#34;vp7fig reveal&#34;&gt;
&lt;div class=&#34;vp7-scroll&#34;&gt;
&lt;svg viewBox=&#34;0 0 900 400&#34; role=&#34;img&#34; aria-label=&#34;Left: the old unlinkability procedure, two structural tests that never consult attacker observation. Right: the new procedure, which gates on observability and attacker authorship, then searches for one of three link witnesses, reporting a found witness as the attack trace.&#34;&gt;
  &lt;text class=&#34;vp7-lab&#34; x=&#34;8&#34; y=&#34;16&#34;&gt;Before · two structural tests&lt;/text&gt;
  &lt;line class=&#34;vp7-line soft&#34; x1=&#34;8&#34; y1=&#34;24&#34; x2=&#34;404&#34; y2=&#34;24&#34;/&gt;
  &lt;rect class=&#34;vp7-box soft&#34; x=&#34;8&#34; y=&#34;36&#34; width=&#34;396&#34; height=&#34;28&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t&#34; x=&#34;20&#34; y=&#34;55&#34;&gt;unlinkability? h1, h2&lt;/text&gt;
  &lt;line class=&#34;vp7-line&#34; x1=&#34;206&#34; y1=&#34;64&#34; x2=&#34;206&#34; y2=&#34;78&#34;/&gt;
  &lt;polygon points=&#34;202,78 210,78 206,86&#34; fill=&#34;#1a1a1a&#34;/&gt;
  &lt;rect class=&#34;vp7-box gone&#34; x=&#34;8&#34; y=&#34;86&#34; width=&#34;396&#34; height=&#34;46&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t b&#34; x=&#34;20&#34; y=&#34;105&#34;&gt;does either value lack a fresh constant?&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;20&#34; y=&#34;121&#34;&gt;if so, report an attack&lt;/text&gt;
  &lt;rect class=&#34;vp7-box gone&#34; x=&#34;8&#34; y=&#34;144&#34; width=&#34;396&#34; height=&#34;46&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t b&#34; x=&#34;20&#34; y=&#34;163&#34;&gt;are both outputs of one primitive application&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;20&#34; y=&#34;179&#34;&gt;that the attacker can rebuild?&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm hot&#34; x=&#34;8&#34; y=&#34;216&#34;&gt;The first reports a property of the model as an attack,&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm hot&#34; x=&#34;8&#34; y=&#34;234&#34;&gt;and disagrees with Verifpal&#39;s own freshness query.&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm hot&#34; x=&#34;8&#34; y=&#34;252&#34;&gt;The second cannot see across sessions or epochs.&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;8&#34; y=&#34;288&#34;&gt;Neither consults what the attacker actually observed.&lt;/text&gt;
  &lt;line class=&#34;vp7-region&#34; x1=&#34;428&#34; y1=&#34;8&#34; x2=&#34;428&#34; y2=&#34;392&#34;/&gt;
  &lt;text class=&#34;vp7-lab&#34; x=&#34;452&#34; y=&#34;16&#34;&gt;Now · find a link witness&lt;/text&gt;
  &lt;line class=&#34;vp7-line soft&#34; x1=&#34;452&#34; y1=&#34;24&#34; x2=&#34;884&#34; y2=&#34;24&#34;/&gt;
  &lt;rect class=&#34;vp7-box solve&#34; x=&#34;452&#34; y=&#34;36&#34; width=&#34;432&#34; height=&#34;28&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t solve b&#34; x=&#34;464&#34; y=&#34;55&#34;&gt;what ties ephid00a and ephid10a to one phone?&lt;/text&gt;
  &lt;line class=&#34;vp7-line solve&#34; x1=&#34;668&#34; y1=&#34;64&#34; x2=&#34;668&#34; y2=&#34;78&#34;/&gt;
  &lt;polygon points=&#34;664,78 672,78 668,86&#34; fill=&#34;#6e5883&#34;/&gt;
  &lt;rect class=&#34;vp7-box soft&#34; x=&#34;452&#34; y=&#34;86&#34; width=&#34;208&#34; height=&#34;44&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t b&#34; x=&#34;464&#34; y=&#34;105&#34;&gt;observable&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;464&#34; y=&#34;121&#34;&gt;on the wire, or leaked&lt;/text&gt;
  &lt;rect class=&#34;vp7-box soft&#34; x=&#34;676&#34; y=&#34;86&#34; width=&#34;208&#34; height=&#34;44&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t b&#34; x=&#34;688&#34; y=&#34;105&#34;&gt;not attacker-authored&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;688&#34; y=&#34;121&#34;&gt;no manufactured equality&lt;/text&gt;
  &lt;line class=&#34;vp7-line solve&#34; x1=&#34;668&#34; y1=&#34;130&#34; x2=&#34;668&#34; y2=&#34;144&#34;/&gt;
  &lt;polygon points=&#34;664,144 672,144 668,152&#34; fill=&#34;#6e5883&#34;/&gt;
  &lt;rect class=&#34;vp7-box&#34; x=&#34;452&#34; y=&#34;152&#34; width=&#34;432&#34; height=&#34;40&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t b&#34; x=&#34;464&#34; y=&#34;169&#34;&gt;observed equality&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;464&#34; y=&#34;185&#34;&gt;the attacker holds both, and they are the same value&lt;/text&gt;
  &lt;rect class=&#34;vp7-box&#34; x=&#34;452&#34; y=&#34;200&#34; width=&#34;432&#34; height=&#34;40&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t b&#34; x=&#34;464&#34; y=&#34;217&#34;&gt;identifying check&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;464&#34; y=&#34;233&#34;&gt;one checked primitive succeeds over both, under one key&lt;/text&gt;
  &lt;rect class=&#34;vp7-box&#34; x=&#34;452&#34; y=&#34;248&#34; width=&#34;432&#34; height=&#34;40&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t b&#34; x=&#34;464&#34; y=&#34;265&#34;&gt;common secret origin&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;464&#34; y=&#34;281&#34;&gt;one held secret recomputes both values&lt;/text&gt;
  &lt;line class=&#34;vp7-line hot&#34; x1=&#34;556&#34; y1=&#34;288&#34; x2=&#34;556&#34; y2=&#34;308&#34;/&gt;
  &lt;polygon points=&#34;552,308 560,308 556,316&#34; fill=&#34;#a35c3a&#34;/&gt;
  &lt;line class=&#34;vp7-line&#34; x1=&#34;780&#34; y1=&#34;288&#34; x2=&#34;780&#34; y2=&#34;308&#34;/&gt;
  &lt;polygon points=&#34;776,308 784,308 780,316&#34; fill=&#34;#1a1a1a&#34;/&gt;
  &lt;rect class=&#34;vp7-box hot&#34; x=&#34;452&#34; y=&#34;316&#34; width=&#34;208&#34; height=&#34;44&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t hot b&#34; x=&#34;464&#34; y=&#34;335&#34;&gt;witness found&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;464&#34; y=&#34;351&#34;&gt;it becomes the attack trace&lt;/text&gt;
  &lt;rect class=&#34;vp7-box good&#34; x=&#34;676&#34; y=&#34;316&#34; width=&#34;208&#34; height=&#34;44&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t good b&#34; x=&#34;688&#34; y=&#34;335&#34;&gt;no witness&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;688&#34; y=&#34;351&#34;&gt;the query holds&lt;/text&gt;
&lt;/svg&gt;
&lt;/div&gt;
&lt;figcaption&gt;&lt;b&gt;From structural tests to a witness the attacker has to exhibit.&lt;/b&gt; The old procedure answered questions about the model&#39;s syntax. The new one asks for a value in the attacker&#39;s hands that ties both queried values to one participant, and prints whichever witness it finds as the trace. Like the rest of the tool, it is sound for attacks and incomplete: a reported link is real, and a passing result means that this search found no witness.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;If you have unlinkability queries in existing models, please re-run them, because verdicts can move in both directions and both movements are corrections. Models that used to fail on the freshness clause will now pass, since a value the attacker can neither reach nor observe cannot be linked. Models with a compromised seed high in a derivation chain may now fail where they previously passed, which is the case the old procedure was structurally unable to detect. In both directions, a failing query now hands you a witness you can check by hand rather than a condition you have to take on faith.&lt;/p&gt;
&lt;h2 id=&#34;getting-it-and-what-to-re-check&#34;&gt;Getting it, and what to re-check&lt;/h2&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;brew tap verifpal.com/source https://github.com/symbolicsoft/verifpal
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;brew install verifpal
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On Windows, &lt;code&gt;scoop bucket add verifpal https://github.com/symbolicsoft/verifpal.git&lt;/code&gt; followed by &lt;code&gt;scoop install verifpal&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;There is no migration this time. Annotations are entirely opt-in, an unannotated model behaves as it did under 0.70.0, and the result code format is unchanged. Two things are worth your attention. Re-run any model containing an &lt;code&gt;unlinkability?&lt;/code&gt; query, and read the witness if the verdict has changed. And look at any &lt;code&gt;leaks&lt;/code&gt; declaration that was standing in for &amp;ldquo;this primitive is broken&amp;rdquo;, since it may now be expressible directly, with the added benefit that anyone reading the output will see the assumption stated alongside the result.&lt;/p&gt;
&lt;p&gt;The rest of the toolchain is current. The &lt;a href=&#34;https://verifpal.com/workbench/&#34;&gt;Workbench&lt;/a&gt; runs 0.80.0 and displays declared weakening assumptions alongside results, &lt;code&gt;internal-json&lt;/code&gt; attaches the assumption list to every result it emits, the &lt;a href=&#34;https://static.verifpal.com/manual.pdf&#34;&gt;Verifpal User Manual&lt;/a&gt; documents both additions, and the &lt;a href=&#34;https://github.com/symbolicsoft/verifpal-vscode&#34;&gt;VS Code extension&lt;/a&gt; (1.0.12) and &lt;a href=&#34;https://github.com/symbolicsoft/verifpal-nvim&#34;&gt;Neovim extension&lt;/a&gt; highlight the new syntax.&lt;/p&gt;
&lt;p&gt;If you model something and a result looks wrong to you, please &lt;a href=&#34;https://github.com/symbolicsoft/verifpal/issues&#34;&gt;open an issue&lt;/a&gt;. The unlinkability rework in this release began with noticing that the old procedure disagreed with the tool&amp;rsquo;s own manual and its own freshness query, and that sort of noticing is how the tool improves.&lt;/p&gt;
</content:encoded>
      <category>Software</category>
      <category>Research</category>
      <category>Verifpal</category>
      <category>Formal Verification</category>
      <category>Post-Quantum</category>
      
    </item>
    
    <item>
      <title>Verifpal 0.70.0: Post-Quantum Key Exchange, and an Attacker That Works Backwards</title>
      <link>https://symbolic.software/blog/2026-08-05-verifpal-0-70/</link>
      <pubDate>Wed, 05 Aug 2026 00:00:00 &#43;0000</pubDate>
      <guid>https://symbolic.software/blog/2026-08-05-verifpal-0-70/</guid>
      <description>Verifpal 0.70.0 removes Diffie-Hellman equations from the modelling language, adds a generic KEM for post-quantum and hybrid key exchange, rebuilds the active attacker around a search that works backwards from each query, and narrates every attack it finds as numbered causal steps.</description>
      <content:encoded>&lt;p&gt;&lt;a href=&#34;https://verifpal.com&#34;&gt;Verifpal&lt;/a&gt; 0.70.0 is out. It is the biggest change to the Verifpal language since the project started, and the first release in years that asks you to edit your existing models, so this post goes through what changed and why.&lt;/p&gt;
&lt;p&gt;Three things landed. Diffie-Hellman equations are gone: &lt;code&gt;G^a&lt;/code&gt; is now &lt;code&gt;PUBKEY(a)&lt;/code&gt;, &lt;code&gt;ga^b&lt;/code&gt; is now &lt;code&gt;DH_KEX(ga, b)&lt;/code&gt;, and the &lt;code&gt;^&lt;/code&gt; operator no longer exists. There is a new key encapsulation mechanism, &lt;code&gt;KEM_ENCAP&lt;/code&gt; and &lt;code&gt;KEM_DECAP&lt;/code&gt;, for modelling post-quantum and hybrid key exchange. And the active attacker has been rebuilt around a different way of searching for attacks, and around explaining them, because an attack Verifpal finds is now narrated to you as a numbered sequence of things the attacker does, rather than dumped as a pile of state for you to decode.&lt;/p&gt;
&lt;p&gt;If you have not used Verifpal before: you describe a protocol in a small language (who knows what, who generates fresh values, who sends what to whom, what gets hashed, signed and encrypted) and then you write down the security properties you want, as queries. Verifpal plays an attacker on the network, reading everything and, if you ask for an active attacker, tampering with anything you have not explicitly protected. It then tries to break those properties. If it cannot, your protocol holds against every attack Verifpal knows how to look for, under the model you wrote. If it can, it shows you how, step by step.&lt;/p&gt;
&lt;h2 id=&#34;an-example-one-missing-character&#34;&gt;An example: one missing character&lt;/h2&gt;
&lt;p&gt;Signal&amp;rsquo;s session setup was, for a decade, X3DH. Since 2023 it has been PQXDH, the same construction with a KEM encapsulation mixed in, a shape the primitives introduced later in this post can now express. This example models the classical X3DH. Bob publishes a bundle of public keys in advance, including a &lt;em&gt;signed prekey&lt;/em&gt;: a medium-term key carrying a signature from his long-term identity key. Alice fetches the bundle, checks that signature, and mixes four Diffie-Hellman values into a master secret (three, when Bob has run out of one-time prekeys; this model includes one). The Double Ratchet takes over from there and gives every message its own key.&lt;/p&gt;
&lt;p&gt;In Verifpal, Alice&amp;rsquo;s signature check looks like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34; data-lang=&#34;verifpal&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-verifpal&#34; data-lang=&#34;verifpal&#34;&gt;&lt;span class=&#34;vp-lit&#34;&gt;_&lt;/span&gt; = &lt;span class=&#34;vp-prim&#34;&gt;SIGNVERIF&lt;/span&gt;(gblongterm, gbs, gbssig)&lt;span class=&#34;vp-op&#34;&gt;?&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code&gt;?&lt;/code&gt; is what makes it a check. It tells Verifpal that Alice stops if verification fails. Without it, Alice computes the verification, discards the result and carries on. That is a realistic bug: the line is present, it calls the right function with the right arguments, and only the return value is missing.&lt;/p&gt;
&lt;p&gt;Below is Verifpal 0.70.0 on a model of X3DH plus three Double Ratchet messages, with that &lt;code&gt;?&lt;/code&gt; removed. This model and the correct one both end with &lt;code&gt;phase[1]&lt;/code&gt;, in which Alice and Bob leak their long-term identity keys. That is how you ask Verifpal about forward secrecy: when these keys are compromised later, do the earlier messages stay secret?&lt;/p&gt;
&lt;div class=&#34;vp7-term&#34;&gt;
&lt;div class=&#34;vp7-term-bar&#34;&gt;
&lt;div class=&#34;vp7-term-left&#34;&gt;&lt;div class=&#34;vp7-traffic&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;span class=&#34;vp7-fname&#34;&gt;verifpal verify &lt;b&gt;signal_unchecked_signature_forward_secrecy.vp&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;span class=&#34;vp7-verdict fail&#34;&gt;1 of 6 failed · 0.28s&lt;/span&gt;
&lt;/div&gt;
&lt;pre&gt;&lt;span class=&#34;no&#34;&gt;     Fail ✗&lt;/span&gt; confidentiality? m1
            ╭─ Attack trace:
            │ 1. &lt;span class=&#34;m&#34;&gt;Attacker replaces gbs, gbo (sent by Bob to Alice) with&lt;/span&gt;
            │    &lt;span class=&#34;m&#34;&gt;PUBKEY(nil), PUBKEY(nil).&lt;/span&gt; &lt;span class=&#34;d&#34;&gt;(gbs was PUBKEY(bs); gbo was PUBKEY(bo))&lt;/span&gt;
            │ 2. Attacker observes e1 on the wire.
            │ 3. Attacker observes gae2 on the wire.
            │ 4. Attacker constructs akshared1 from gae2, nil.
            │ 5. Attacker constructs gbs from nil.
            │ 6. &lt;span class=&#34;k&#34;&gt;Attacker is handed alongterm by a leaks declaration.&lt;/span&gt;
            │ 7. Attacker constructs DH_KEX(gbs, alongterm) from gbs, alongterm.
            │ 8. Attacker observes gae1 on the wire.
            │ 9. &lt;span class=&#34;k&#34;&gt;Attacker is handed blongterm by a leaks declaration.&lt;/span&gt;
            │ 10. Attacker constructs DH_KEX(gblongterm, ae1) from gae1, blongterm.
            │ 11. Attacker constructs DH_KEX(gbs, ae1) from gae1, nil.
            │ 12. Attacker constructs amaster from DH_KEX(gbs, alongterm),
            │     DH_KEX(gblongterm, ae1), DH_KEX(gbs, ae1), DH_KEX(gbs, ae1).
            │ 13. Attacker constructs arkba1 from amaster, nil, nil.
            │ 14. Attacker constructs ackab1 from akshared1, arkba1, nil.
            │ 15. Attacker constructs MAC(ackab1, nil) from ackab1, nil.
            │ 16. Attacker constructs akenc1 from MAC(ackab1, nil), nil, nil.
            │ 17. &lt;span class=&#34;no&#34;&gt;Attacker opens e1 with akenc1, obtaining m1.&lt;/span&gt;
            ╰▸ m1 (m1) is obtained by Attacker.
&lt;span class=&#34;ok&#34;&gt;     Pass ✓&lt;/span&gt; authentication? Alice -&amp;gt; Bob: e1
&lt;span class=&#34;ok&#34;&gt;     Pass ✓&lt;/span&gt; confidentiality? m2
&lt;span class=&#34;ok&#34;&gt;     Pass ✓&lt;/span&gt; authentication? Bob -&amp;gt; Alice: e2
&lt;span class=&#34;ok&#34;&gt;     Pass ✓&lt;/span&gt; confidentiality? m3
&lt;span class=&#34;ok&#34;&gt;     Pass ✓&lt;/span&gt; authentication? Alice -&amp;gt; Bob: e3
&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;The order of the steps matters here, so it is worth walking through them.&lt;/p&gt;
&lt;p&gt;In step 1, before anything has been encrypted, the attacker swaps two of Bob&amp;rsquo;s public values for its own. That works because Alice&amp;rsquo;s signature check is not a check. At that point the attacker still cannot read anything: X3DH mixes four Diffie-Hellman values into the master secret, and two of them involve keys it does not have.&lt;/p&gt;
&lt;p&gt;Then phase 1 arrives and the long-term keys leak. Two of the four Diffie-Hellman values now come from the leak, and the other two come from the substitution made much earlier, because &lt;code&gt;PUBKEY(nil)&lt;/code&gt; is a public key whose private half the attacker knows. The master secret completes, the key schedule runs forward, and &lt;code&gt;m1&lt;/code&gt; opens.&lt;/p&gt;
&lt;figure class=&#34;vp7fig reveal&#34;&gt;
&lt;div class=&#34;vp7-scroll&#34;&gt;
&lt;svg viewBox=&#34;0 0 860 430&#34; role=&#34;img&#34; aria-label=&#34;Timeline showing an attacker substitution made in phase 0 lying dormant until long-term keys leak in phase 1, after which the first message opens while later ratchet messages remain secret.&#34;&gt;
  &lt;text class=&#34;vp7-lab&#34; x=&#34;14&#34; y=&#34;18&#34;&gt;Phase 0 — the handshake&lt;/text&gt;
  &lt;line class=&#34;vp7-line soft&#34; x1=&#34;14&#34; y1=&#34;26&#34; x2=&#34;846&#34; y2=&#34;26&#34;/&gt;
  &lt;rect class=&#34;vp7-box&#34; x=&#34;14&#34; y=&#34;44&#34; width=&#34;164&#34; height=&#34;46&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t&#34; x=&#34;26&#34; y=&#34;64&#34;&gt;Bob → Alice:&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;26&#34; y=&#34;80&#34;&gt;gblongterm, gbs, gbo&lt;/text&gt;
  &lt;line class=&#34;vp7-line hot&#34; x1=&#34;178&#34; y1=&#34;67&#34; x2=&#34;216&#34; y2=&#34;67&#34;/&gt;
  &lt;polygon points=&#34;216,63 226,67 216,71&#34; fill=&#34;#a35c3a&#34;/&gt;
  &lt;rect class=&#34;vp7-box hot&#34; x=&#34;228&#34; y=&#34;44&#34; width=&#34;176&#34; height=&#34;46&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t hot b&#34; x=&#34;240&#34; y=&#34;64&#34;&gt;attacker substitutes&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;240&#34; y=&#34;80&#34;&gt;gbs, gbo := PUBKEY(nil)&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;14&#34; y=&#34;116&#34;&gt;Alice verifies the prekey signature without ?, so she does not stop:&lt;/text&gt;
  &lt;rect class=&#34;vp7-box soft&#34; x=&#34;14&#34; y=&#34;128&#34; width=&#34;390&#34; height=&#34;26&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t&#34; x=&#34;26&#34; y=&#34;146&#34;&gt;_ = SIGNVERIF(gblongterm, gbs, gbssig)&lt;/text&gt;
  &lt;text class=&#34;vp7-t hot&#34; x=&#34;330&#34; y=&#34;146&#34;&gt;← no ?&lt;/text&gt;
  &lt;text class=&#34;vp7-lab&#34; x=&#34;440&#34; y=&#34;40&#34;&gt;amaster mixes four DH values&lt;/text&gt;
  &lt;rect class=&#34;vp7-box&#34; x=&#34;440&#34; y=&#34;52&#34; width=&#34;238&#34; height=&#34;26&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t&#34; x=&#34;450&#34; y=&#34;70&#34;&gt;DH_KEX(gbs, alongterm)&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;688&#34; y=&#34;70&#34;&gt;locked · needs alongterm&lt;/text&gt;
  &lt;rect class=&#34;vp7-box&#34; x=&#34;440&#34; y=&#34;86&#34; width=&#34;238&#34; height=&#34;26&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t&#34; x=&#34;450&#34; y=&#34;104&#34;&gt;DH_KEX(gblongterm, ae1)&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;688&#34; y=&#34;104&#34;&gt;locked · needs blongterm&lt;/text&gt;
  &lt;rect class=&#34;vp7-box hot&#34; x=&#34;440&#34; y=&#34;120&#34; width=&#34;238&#34; height=&#34;26&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t&#34; x=&#34;450&#34; y=&#34;138&#34;&gt;DH_KEX(gbs, ae1)&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm hot&#34; x=&#34;688&#34; y=&#34;138&#34;&gt;open · attacker&#39;s key&lt;/text&gt;
  &lt;rect class=&#34;vp7-box hot&#34; x=&#34;440&#34; y=&#34;154&#34; width=&#34;238&#34; height=&#34;26&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t&#34; x=&#34;450&#34; y=&#34;172&#34;&gt;DH_KEX(gbo, ae1)&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm hot&#34; x=&#34;688&#34; y=&#34;172&#34;&gt;open · attacker&#39;s key&lt;/text&gt;
  &lt;rect class=&#34;vp7-box good&#34; x=&#34;440&#34; y=&#34;196&#34; width=&#34;238&#34; height=&#34;30&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t good b&#34; x=&#34;452&#34; y=&#34;216&#34;&gt;m1 not yet recoverable&lt;/text&gt;
  &lt;line class=&#34;vp7-line soft dash&#34; x1=&#34;14&#34; y1=&#34;252&#34; x2=&#34;846&#34; y2=&#34;252&#34;/&gt;
  &lt;text class=&#34;vp7-lab&#34; x=&#34;14&#34; y=&#34;272&#34;&gt;Phase 1 — the long-term keys leak&lt;/text&gt;
  &lt;rect class=&#34;vp7-box&#34; x=&#34;14&#34; y=&#34;286&#34; width=&#34;164&#34; height=&#34;66&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t&#34; x=&#34;26&#34; y=&#34;306&#34;&gt;phase[1]&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;26&#34; y=&#34;324&#34;&gt;Alice leaks alongterm&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;26&#34; y=&#34;342&#34;&gt;Bob leaks blongterm&lt;/text&gt;
  &lt;line class=&#34;vp7-line hot&#34; x1=&#34;178&#34; y1=&#34;318&#34; x2=&#34;216&#34; y2=&#34;318&#34;/&gt;
  &lt;polygon points=&#34;216,314 226,318 216,322&#34; fill=&#34;#a35c3a&#34;/&gt;
  &lt;rect class=&#34;vp7-box hot&#34; x=&#34;228&#34; y=&#34;286&#34; width=&#34;200&#34; height=&#34;66&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t hot b&#34; x=&#34;240&#34; y=&#34;306&#34;&gt;all four now available&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;240&#34; y=&#34;324&#34;&gt;2 DH values from the leak&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;240&#34; y=&#34;342&#34;&gt;2 from the phase-0 swap&lt;/text&gt;
  &lt;line class=&#34;vp7-line hot&#34; x1=&#34;428&#34; y1=&#34;318&#34; x2=&#34;466&#34; y2=&#34;318&#34;/&gt;
  &lt;polygon points=&#34;466,314 476,318 466,322&#34; fill=&#34;#a35c3a&#34;/&gt;
  &lt;rect class=&#34;vp7-box hot&#34; x=&#34;478&#34; y=&#34;286&#34; width=&#34;200&#34; height=&#34;66&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t hot b&#34; x=&#34;490&#34; y=&#34;306&#34;&gt;amaster → akenc1 → m1&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;490&#34; y=&#34;324&#34;&gt;confidentiality? m1&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm hot&#34; x=&#34;490&#34; y=&#34;342&#34;&gt;contradicted&lt;/text&gt;
  &lt;rect class=&#34;vp7-box good&#34; x=&#34;690&#34; y=&#34;286&#34; width=&#34;156&#34; height=&#34;66&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t good b&#34; x=&#34;702&#34; y=&#34;306&#34;&gt;m2, m3 hold&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;702&#34; y=&#34;324&#34;&gt;the ratchet moved on&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;702&#34; y=&#34;342&#34;&gt;to ae2, ae3, be&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;14&#34; y=&#34;392&#34;&gt;The swap and the leak are reported as a single causal chain, in the order the attacker would&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;14&#34; y=&#34;410&#34;&gt;have to carry them out: the swap first, the payoff eight steps and one phase later.&lt;/text&gt;
&lt;/svg&gt;
&lt;/div&gt;
&lt;figcaption&gt;&lt;b&gt;The substitution has to be planned before it can be used.&lt;/b&gt; Swapping Bob&#39;s public values in phase 0 gains the attacker nothing at the time; it only pays off once the long-term keys leak in phase 1. A search that judges each substitution by what it immediately reveals will not find this.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The queries that passed are worth reading too, because they tell you where the damage stops.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;m2&lt;/code&gt; and &lt;code&gt;m3&lt;/code&gt; survive because the Double Ratchet heals. By the second message the key schedule has moved on to ephemerals the attacker never touched, and touching them now would fail a checked decryption on the far side and halt the protocol. Verifpal is reporting the boundary of the break as well as the break itself, which is usually the more useful half for a protocol designer.&lt;/p&gt;
&lt;p&gt;All three authentication queries also pass. The attacker reads &lt;code&gt;m1&lt;/code&gt;, but it never gets Alice or Bob to accept a message the other one did not send. Reading and forging are separate properties, and the tool keeps them separate.&lt;/p&gt;
&lt;p&gt;The whole analysis takes 0.28 seconds.&lt;/p&gt;
&lt;h2 id=&#34;how-the-search-finds-it&#34;&gt;How the search finds it&lt;/h2&gt;
&lt;p&gt;Seventeen steps, two simultaneous substitutions on one message, and a payoff a full phase later. That combination is why the search engine was replaced.&lt;/p&gt;
&lt;p&gt;The old active attacker searched forwards: pick some combination of wire values to replace, try it, see what the attacker learns. The number of combinations grows quickly, since six controllable values already give you six single substitutions, fifteen pairs and twenty triples, so the search needed a depth limit and a per-principal budget. Once those exist, they decide which attacks are reachable, rather than the protocol deciding.&lt;/p&gt;
&lt;p&gt;Signal is the clearest example. The classic man-in-the-middle against X3DH needs three public keys replaced at the same time, and no affordable budget reliably got there. Whether Verifpal found it came down to a constant that could not be justified by looking at the model.&lt;/p&gt;
&lt;p&gt;The new engine starts from the query instead. Given an unresolved query, what would the attacker need to contradict it? That requirement is broken into subgoals. Each subgoal is discharged against something the attacker already holds, or against the shape of what a principal is about to compute. A subgoal that can only be discharged under a particular substitution &lt;em&gt;forces&lt;/em&gt; that substitution as a binding.&lt;/p&gt;
&lt;p&gt;An attack requiring four simultaneous substitutions is then not a four-element combination that has to be affordable. It is four bindings, each forced independently, merged at the end.&lt;/p&gt;
&lt;figure class=&#34;vp7fig reveal&#34;&gt;
&lt;div class=&#34;vp7-scroll&#34;&gt;
&lt;svg viewBox=&#34;0 0 900 400&#34; role=&#34;img&#34; aria-label=&#34;Left: forward enumeration of substitution combinations, cut off by a budget line above the level where a three-key attack lives. Right: backwards solving, where a query decomposes into three subgoals that each force a single binding, and the bindings merge into one substitution.&#34;&gt;
  &lt;text class=&#34;vp7-lab&#34; x=&#34;8&#34; y=&#34;16&#34;&gt;Before · guess forwards&lt;/text&gt;
  &lt;line class=&#34;vp7-line soft&#34; x1=&#34;8&#34; y1=&#34;24&#34; x2=&#34;392&#34; y2=&#34;24&#34;/&gt;
  &lt;rect class=&#34;vp7-box soft&#34; x=&#34;8&#34; y=&#34;36&#34; width=&#34;384&#34; height=&#34;26&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t&#34; x=&#34;20&#34; y=&#34;54&#34;&gt;6 attacker-controllable wire values&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;8&#34; y=&#34;94&#34;&gt;1 at a time&lt;/text&gt;
  &lt;circle class=&#34;vp7-box&#34; cx=&#34;176&#34; cy=&#34;90&#34; r=&#34;5&#34;/&gt;
  &lt;circle class=&#34;vp7-box&#34; cx=&#34;196&#34; cy=&#34;90&#34; r=&#34;5&#34;/&gt;
  &lt;circle class=&#34;vp7-box&#34; cx=&#34;216&#34; cy=&#34;90&#34; r=&#34;5&#34;/&gt;
  &lt;circle class=&#34;vp7-box&#34; cx=&#34;236&#34; cy=&#34;90&#34; r=&#34;5&#34;/&gt;
  &lt;circle class=&#34;vp7-box&#34; cx=&#34;256&#34; cy=&#34;90&#34; r=&#34;5&#34;/&gt;
  &lt;circle class=&#34;vp7-box&#34; cx=&#34;276&#34; cy=&#34;90&#34; r=&#34;5&#34;/&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;356&#34; y=&#34;94&#34;&gt;6&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;8&#34; y=&#34;130&#34;&gt;2 at a time&lt;/text&gt;
  &lt;circle class=&#34;vp7-box&#34; cx=&#34;136&#34; cy=&#34;126&#34; r=&#34;5&#34;/&gt;
  &lt;circle class=&#34;vp7-box&#34; cx=&#34;156&#34; cy=&#34;126&#34; r=&#34;5&#34;/&gt;
  &lt;circle class=&#34;vp7-box&#34; cx=&#34;176&#34; cy=&#34;126&#34; r=&#34;5&#34;/&gt;
  &lt;circle class=&#34;vp7-box&#34; cx=&#34;196&#34; cy=&#34;126&#34; r=&#34;5&#34;/&gt;
  &lt;circle class=&#34;vp7-box&#34; cx=&#34;216&#34; cy=&#34;126&#34; r=&#34;5&#34;/&gt;
  &lt;circle class=&#34;vp7-box&#34; cx=&#34;236&#34; cy=&#34;126&#34; r=&#34;5&#34;/&gt;
  &lt;circle class=&#34;vp7-box&#34; cx=&#34;256&#34; cy=&#34;126&#34; r=&#34;5&#34;/&gt;
  &lt;circle class=&#34;vp7-box&#34; cx=&#34;276&#34; cy=&#34;126&#34; r=&#34;5&#34;/&gt;
  &lt;circle class=&#34;vp7-box&#34; cx=&#34;296&#34; cy=&#34;126&#34; r=&#34;5&#34;/&gt;
  &lt;circle class=&#34;vp7-box&#34; cx=&#34;316&#34; cy=&#34;126&#34; r=&#34;5&#34;/&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;356&#34; y=&#34;130&#34;&gt;15&lt;/text&gt;
  &lt;line class=&#34;vp7-line hot dash&#34; x1=&#34;8&#34; y1=&#34;152&#34; x2=&#34;392&#34; y2=&#34;152&#34;/&gt;
  &lt;text class=&#34;vp7-t sm hot&#34; x=&#34;8&#34; y=&#34;168&#34;&gt;depth cap · per-principal budget&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;8&#34; y=&#34;206&#34;&gt;3 at a time&lt;/text&gt;
  &lt;circle class=&#34;vp7-box gone&#34; cx=&#34;136&#34; cy=&#34;202&#34; r=&#34;5&#34;/&gt;
  &lt;circle class=&#34;vp7-box gone&#34; cx=&#34;156&#34; cy=&#34;202&#34; r=&#34;5&#34;/&gt;
  &lt;circle class=&#34;vp7-box gone&#34; cx=&#34;176&#34; cy=&#34;202&#34; r=&#34;5&#34;/&gt;
  &lt;circle class=&#34;vp7-box gone&#34; cx=&#34;196&#34; cy=&#34;202&#34; r=&#34;5&#34;/&gt;
  &lt;circle cx=&#34;216&#34; cy=&#34;202&#34; r=&#34;6&#34; fill=&#34;#a35c3a&#34;/&gt;
  &lt;circle class=&#34;vp7-box gone&#34; cx=&#34;236&#34; cy=&#34;202&#34; r=&#34;5&#34;/&gt;
  &lt;circle class=&#34;vp7-box gone&#34; cx=&#34;256&#34; cy=&#34;202&#34; r=&#34;5&#34;/&gt;
  &lt;circle class=&#34;vp7-box gone&#34; cx=&#34;276&#34; cy=&#34;202&#34; r=&#34;5&#34;/&gt;
  &lt;circle class=&#34;vp7-box gone&#34; cx=&#34;296&#34; cy=&#34;202&#34; r=&#34;5&#34;/&gt;
  &lt;circle class=&#34;vp7-box gone&#34; cx=&#34;316&#34; cy=&#34;202&#34; r=&#34;5&#34;/&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;356&#34; y=&#34;206&#34;&gt;20&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;8&#34; y=&#34;242&#34;&gt;4 at a time&lt;/text&gt;
  &lt;circle class=&#34;vp7-box gone&#34; cx=&#34;136&#34; cy=&#34;238&#34; r=&#34;5&#34;/&gt;
  &lt;circle class=&#34;vp7-box gone&#34; cx=&#34;156&#34; cy=&#34;238&#34; r=&#34;5&#34;/&gt;
  &lt;circle class=&#34;vp7-box gone&#34; cx=&#34;176&#34; cy=&#34;238&#34; r=&#34;5&#34;/&gt;
  &lt;circle class=&#34;vp7-box gone&#34; cx=&#34;196&#34; cy=&#34;238&#34; r=&#34;5&#34;/&gt;
  &lt;circle class=&#34;vp7-box gone&#34; cx=&#34;216&#34; cy=&#34;238&#34; r=&#34;5&#34;/&gt;
  &lt;circle class=&#34;vp7-box gone&#34; cx=&#34;236&#34; cy=&#34;238&#34; r=&#34;5&#34;/&gt;
  &lt;circle class=&#34;vp7-box gone&#34; cx=&#34;256&#34; cy=&#34;238&#34; r=&#34;5&#34;/&gt;
  &lt;circle class=&#34;vp7-box gone&#34; cx=&#34;276&#34; cy=&#34;238&#34; r=&#34;5&#34;/&gt;
  &lt;circle class=&#34;vp7-box gone&#34; cx=&#34;296&#34; cy=&#34;238&#34; r=&#34;5&#34;/&gt;
  &lt;circle class=&#34;vp7-box gone&#34; cx=&#34;316&#34; cy=&#34;238&#34; r=&#34;5&#34;/&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;356&#34; y=&#34;242&#34;&gt;15&lt;/text&gt;
  &lt;line class=&#34;vp7-line hot&#34; x1=&#34;220&#34; y1=&#34;209&#34; x2=&#34;248&#34; y2=&#34;284&#34;/&gt;
  &lt;rect class=&#34;vp7-box hot&#34; x=&#34;120&#34; y=&#34;288&#34; width=&#34;272&#34; height=&#34;46&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t hot b&#34; x=&#34;132&#34; y=&#34;308&#34;&gt;the X3DH man-in-the-middle&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;132&#34; y=&#34;325&#34;&gt;three public keys, replaced at once&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;8&#34; y=&#34;366&#34;&gt;Reaching it depended on the budget, not on&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;8&#34; y=&#34;384&#34;&gt;anything about the protocol.&lt;/text&gt;
  &lt;line class=&#34;vp7-region&#34; x1=&#34;418&#34; y1=&#34;8&#34; x2=&#34;418&#34; y2=&#34;392&#34;/&gt;
  &lt;text class=&#34;vp7-lab&#34; x=&#34;440&#34; y=&#34;16&#34;&gt;Now · work backwards&lt;/text&gt;
  &lt;line class=&#34;vp7-line soft&#34; x1=&#34;440&#34; y1=&#34;24&#34; x2=&#34;884&#34; y2=&#34;24&#34;/&gt;
  &lt;rect class=&#34;vp7-box solve&#34; x=&#34;440&#34; y=&#34;36&#34; width=&#34;444&#34; height=&#34;28&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t solve b&#34; x=&#34;452&#34; y=&#34;55&#34;&gt;confidentiality? m1 — what would it take?&lt;/text&gt;
  &lt;line class=&#34;vp7-line solve&#34; x1=&#34;662&#34; y1=&#34;64&#34; x2=&#34;662&#34; y2=&#34;80&#34;/&gt;
  &lt;polygon points=&#34;658,80 666,80 662,88&#34; fill=&#34;#6e5883&#34;/&gt;
  &lt;rect class=&#34;vp7-box solve&#34; x=&#34;440&#34; y=&#34;88&#34; width=&#34;444&#34; height=&#34;28&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t&#34; x=&#34;452&#34; y=&#34;107&#34;&gt;the attacker needs akenc1, and so amaster&lt;/text&gt;
  &lt;line class=&#34;vp7-line solve&#34; x1=&#34;662&#34; y1=&#34;116&#34; x2=&#34;662&#34; y2=&#34;132&#34;/&gt;
  &lt;polygon points=&#34;658,132 666,132 662,140&#34; fill=&#34;#6e5883&#34;/&gt;
  &lt;text class=&#34;vp7-lab&#34; x=&#34;440&#34; y=&#34;153&#34;&gt;amaster needs all three of these&lt;/text&gt;
  &lt;rect class=&#34;vp7-box&#34; x=&#34;440&#34; y=&#34;160&#34; width=&#34;250&#34; height=&#34;28&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t&#34; x=&#34;452&#34; y=&#34;179&#34;&gt;DH_KEX(gbs, alongterm)&lt;/text&gt;
  &lt;rect class=&#34;vp7-box&#34; x=&#34;440&#34; y=&#34;196&#34; width=&#34;250&#34; height=&#34;28&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t&#34; x=&#34;452&#34; y=&#34;215&#34;&gt;DH_KEX(gbs, ae1)&lt;/text&gt;
  &lt;rect class=&#34;vp7-box&#34; x=&#34;440&#34; y=&#34;232&#34; width=&#34;250&#34; height=&#34;28&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t&#34; x=&#34;452&#34; y=&#34;251&#34;&gt;DH_KEX(gbo, ae1)&lt;/text&gt;
  &lt;text class=&#34;vp7-lab&#34; x=&#34;712&#34; y=&#34;153&#34;&gt;each forces one binding&lt;/text&gt;
  &lt;polyline class=&#34;vp7-line solve&#34; points=&#34;690,174 701,174 701,192 712,192&#34;/&gt;
  &lt;polygon points=&#34;712,188 720,192 712,196&#34; fill=&#34;#6e5883&#34;/&gt;
  &lt;polyline class=&#34;vp7-line solve&#34; points=&#34;690,210 701,210 701,192&#34;/&gt;
  &lt;polyline class=&#34;vp7-line solve&#34; points=&#34;690,246 712,246&#34;/&gt;
  &lt;polygon points=&#34;712,242 720,246 712,250&#34; fill=&#34;#6e5883&#34;/&gt;
  &lt;rect class=&#34;vp7-box hot&#34; x=&#34;720&#34; y=&#34;178&#34; width=&#34;164&#34; height=&#34;28&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t&#34; x=&#34;732&#34; y=&#34;197&#34;&gt;gbs := PUBKEY(nil)&lt;/text&gt;
  &lt;rect class=&#34;vp7-box hot&#34; x=&#34;720&#34; y=&#34;232&#34; width=&#34;164&#34; height=&#34;28&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t&#34; x=&#34;732&#34; y=&#34;251&#34;&gt;gbo := PUBKEY(nil)&lt;/text&gt;
  &lt;polyline class=&#34;vp7-line solve&#34; points=&#34;884,192 892,192 892,296 802,296&#34;/&gt;
  &lt;polyline class=&#34;vp7-line solve&#34; points=&#34;802,260 802,296&#34;/&gt;
  &lt;polyline class=&#34;vp7-line solve&#34; points=&#34;802,296 802,312 662,312 662,326&#34;/&gt;
  &lt;polygon points=&#34;658,326 666,326 662,334&#34; fill=&#34;#6e5883&#34;/&gt;
  &lt;rect class=&#34;vp7-box solve&#34; x=&#34;440&#34; y=&#34;334&#34; width=&#34;444&#34; height=&#34;30&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t solve b&#34; x=&#34;452&#34; y=&#34;354&#34;&gt;merge the bindings — that is the substitution&lt;/text&gt;
&lt;/svg&gt;
&lt;/div&gt;
&lt;figcaption&gt;&lt;b&gt;The difference is the direction the search runs.&lt;/b&gt; Forwards, the number of combinations is a cost that has to be paid, and a cap ends up deciding what is reachable. Backwards, nothing is enumerated: each subgoal forces at most one binding, and a simultaneous substitution is what you get when several independent requirements agree.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Termination now comes from the structure of the search rather than a cap on it. Goals are memoised, so the same question is never answered twice. A goal that ends up depending on itself is cut as a cycle. The two rules that could otherwise generate unboundedly many new terms are restricted to a finite basis drawn from the protocol. Each of those is a property of the term algebra that you can check by reading it.&lt;/p&gt;
&lt;p&gt;The old engine is gone: &lt;code&gt;verifyactive.rs&lt;/code&gt;, &lt;code&gt;inject.rs&lt;/code&gt; and &lt;code&gt;mutationmap.rs&lt;/code&gt;, 1,624 lines between them, along with the &lt;code&gt;rayon&lt;/code&gt; dependency that parallelised the sweep. There is no &lt;code&gt;SearchConfig&lt;/code&gt; in the new code, no depth cap, and nothing to tune.&lt;/p&gt;
&lt;h2 id=&#34;why-a-reported-attack-can-be-trusted&#34;&gt;Why a reported attack can be trusted&lt;/h2&gt;
&lt;p&gt;Replacing the search engine in a verification tool is risky in one specific direction. Verifpal is sound but incomplete by design: the search may miss an attack, but any attack it reports has to be real. A missed attack is a disappointment. A false attack costs someone a day of work and their trust in the tool.&lt;/p&gt;
&lt;p&gt;So soundness is enforced by the structure of the code rather than argued for in a comment. No module under &lt;code&gt;src/solve/&lt;/code&gt; can record a query result. The solver only proposes a substitution. Every proposal is materialised into a real principal state, re-executed through the ordinary analysis pipeline, and re-checked against actual attacker knowledge before anything is allowed to resolve. A bug in the solver costs a missed attack and cannot produce a false one.&lt;/p&gt;
&lt;p&gt;The validator also owns one more check that does a lot of work: a substituted term that reduces back to the honest value is a replay, not a forgery. A replay can still be an attack, which is what &lt;code&gt;freshness?&lt;/code&gt; queries exist to catch, but it never makes a principal accept a message its partner never sent, and conflating replay with forgery is one of the easiest ways to produce a false authentication result. Four models in the test suite exist to hold that line.&lt;/p&gt;
&lt;figure class=&#34;vp7fig reveal&#34;&gt;
&lt;div class=&#34;vp7-scroll&#34;&gt;
&lt;svg viewBox=&#34;0 0 1000 520&#34; role=&#34;img&#34; aria-label=&#34;Verifpal&#39;s analysis architecture: parsing and construction on the left, a per-phase pipeline of attacker state, trace generation, knowledge closure and query evaluation in the middle, the active attacker&#39;s symbolic execution, backwards solving, proposals and validation on the right, and witness minimization and narration along the bottom.&#34;&gt;
  &lt;rect class=&#34;vp7-box&#34; x=&#34;20&#34; y=&#34;34&#34; width=&#34;170&#34; height=&#34;38&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t b&#34; x=&#34;105&#34; y=&#34;52&#34; text-anchor=&#34;middle&#34;&gt;Verifpal model&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;105&#34; y=&#34;66&#34; text-anchor=&#34;middle&#34;&gt;.vp&lt;/text&gt;
  &lt;line class=&#34;vp7-line&#34; x1=&#34;105&#34; y1=&#34;72&#34; x2=&#34;105&#34; y2=&#34;90&#34;/&gt;
  &lt;polygon points=&#34;101,90 109,90 105,98&#34; fill=&#34;#1a1a1a&#34;/&gt;
  &lt;rect class=&#34;vp7-box&#34; x=&#34;20&#34; y=&#34;98&#34; width=&#34;170&#34; height=&#34;44&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t b&#34; x=&#34;105&#34; y=&#34;118&#34; text-anchor=&#34;middle&#34;&gt;Parse&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;105&#34; y=&#34;133&#34; text-anchor=&#34;middle&#34;&gt;recursive descent&lt;/text&gt;
  &lt;line class=&#34;vp7-line&#34; x1=&#34;105&#34; y1=&#34;142&#34; x2=&#34;105&#34; y2=&#34;154&#34;/&gt;
  &lt;polygon points=&#34;101,154 109,154 105,162&#34; fill=&#34;#1a1a1a&#34;/&gt;
  &lt;rect class=&#34;vp7-box&#34; x=&#34;20&#34; y=&#34;162&#34; width=&#34;170&#34; height=&#34;44&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t b&#34; x=&#34;105&#34; y=&#34;182&#34; text-anchor=&#34;middle&#34;&gt;Sanity check&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;105&#34; y=&#34;197&#34; text-anchor=&#34;middle&#34;&gt;arities, phases, arguments&lt;/text&gt;
  &lt;line class=&#34;vp7-line&#34; x1=&#34;105&#34; y1=&#34;206&#34; x2=&#34;105&#34; y2=&#34;224&#34;/&gt;
  &lt;polygon points=&#34;101,224 109,224 105,232&#34; fill=&#34;#1a1a1a&#34;/&gt;
  &lt;text class=&#34;vp7-lab&#34; x=&#34;113&#34; y=&#34;220&#34;&gt;construct&lt;/text&gt;
  &lt;rect class=&#34;vp7-box soft&#34; x=&#34;20&#34; y=&#34;232&#34; width=&#34;170&#34; height=&#34;40&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t&#34; x=&#34;105&#34; y=&#34;250&#34; text-anchor=&#34;middle&#34;&gt;ProtocolTrace&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;105&#34; y=&#34;264&#34; text-anchor=&#34;middle&#34;&gt;global, immutable&lt;/text&gt;
  &lt;rect class=&#34;vp7-box soft&#34; x=&#34;20&#34; y=&#34;288&#34; width=&#34;170&#34; height=&#34;40&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t&#34; x=&#34;105&#34; y=&#34;306&#34; text-anchor=&#34;middle&#34;&gt;PrincipalState × n&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;105&#34; y=&#34;320&#34; text-anchor=&#34;middle&#34;&gt;one per principal&lt;/text&gt;
  &lt;polyline class=&#34;vp7-line&#34; points=&#34;190,252 214,252 214,63 244,63&#34;/&gt;
  &lt;polygon points=&#34;244,59 252,63 244,67&#34; fill=&#34;#1a1a1a&#34;/&gt;
  &lt;polyline class=&#34;vp7-line&#34; points=&#34;190,308 226,308 226,129 244,129&#34;/&gt;
  &lt;polygon points=&#34;244,125 252,129 244,133&#34; fill=&#34;#1a1a1a&#34;/&gt;
  &lt;rect class=&#34;vp7-region&#34; x=&#34;236&#34; y=&#34;20&#34; width=&#34;244&#34; height=&#34;330&#34; rx=&#34;4&#34;/&gt;
  &lt;text class=&#34;vp7-lab&#34; x=&#34;248&#34; y=&#34;16&#34;&gt;for each phase&lt;/text&gt;
  &lt;rect class=&#34;vp7-box&#34; x=&#34;252&#34; y=&#34;40&#34; width=&#34;212&#34; height=&#34;46&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t b&#34; x=&#34;358&#34; y=&#34;59&#34; text-anchor=&#34;middle&#34;&gt;AttackerState&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;358&#34; y=&#34;74&#34; text-anchor=&#34;middle&#34;&gt;public values, wire, leaks&lt;/text&gt;
  &lt;line class=&#34;vp7-line&#34; x1=&#34;358&#34; y1=&#34;86&#34; x2=&#34;358&#34; y2=&#34;98&#34;/&gt;
  &lt;polygon points=&#34;354,98 362,98 358,106&#34; fill=&#34;#1a1a1a&#34;/&gt;
  &lt;rect class=&#34;vp7-box&#34; x=&#34;252&#34; y=&#34;106&#34; width=&#34;212&#34; height=&#34;46&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t b&#34; x=&#34;358&#34; y=&#34;125&#34; text-anchor=&#34;middle&#34;&gt;1. Trace generation&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;358&#34; y=&#34;140&#34; text-anchor=&#34;middle&#34;&gt;resolve → diff → rewrite&lt;/text&gt;
  &lt;line class=&#34;vp7-line&#34; x1=&#34;358&#34; y1=&#34;152&#34; x2=&#34;358&#34; y2=&#34;164&#34;/&gt;
  &lt;polygon points=&#34;354,164 362,164 358,172&#34; fill=&#34;#1a1a1a&#34;/&gt;
  &lt;rect class=&#34;vp7-box&#34; x=&#34;252&#34; y=&#34;172&#34; width=&#34;212&#34; height=&#34;92&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t b&#34; x=&#34;358&#34; y=&#34;191&#34; text-anchor=&#34;middle&#34;&gt;2. Knowledge closure&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;358&#34; y=&#34;206&#34; text-anchor=&#34;middle&#34;&gt;monotone fixed point&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;264&#34; y=&#34;224&#34;&gt;i. decompose&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;264&#34; y=&#34;240&#34;&gt;ii. reconstruct, recompose&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;264&#34; y=&#34;256&#34;&gt;iii. equivalize, extract&lt;/text&gt;
  &lt;line class=&#34;vp7-line&#34; x1=&#34;358&#34; y1=&#34;264&#34; x2=&#34;358&#34; y2=&#34;276&#34;/&gt;
  &lt;polygon points=&#34;354,276 362,276 358,284&#34; fill=&#34;#1a1a1a&#34;/&gt;
  &lt;rect class=&#34;vp7-box&#34; x=&#34;252&#34; y=&#34;284&#34; width=&#34;212&#34; height=&#34;46&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t b&#34; x=&#34;358&#34; y=&#34;303&#34; text-anchor=&#34;middle&#34;&gt;3. Query evaluation&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;358&#34; y=&#34;318&#34; text-anchor=&#34;middle&#34;&gt;never inside the closure&lt;/text&gt;
  &lt;rect class=&#34;vp7-region&#34; x=&#34;560&#34; y=&#34;20&#34; width=&#34;258&#34; height=&#34;330&#34; rx=&#34;4&#34;/&gt;
  &lt;text class=&#34;vp7-lab&#34; x=&#34;572&#34; y=&#34;16&#34;&gt;active attacker only&lt;/text&gt;
  &lt;rect class=&#34;vp7-box solve&#34; x=&#34;574&#34; y=&#34;40&#34; width=&#34;230&#34; height=&#34;46&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t solve b&#34; x=&#34;689&#34; y=&#34;59&#34; text-anchor=&#34;middle&#34;&gt;Symbolic execution&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;689&#34; y=&#34;74&#34; text-anchor=&#34;middle&#34;&gt;controllable values → variables&lt;/text&gt;
  &lt;line class=&#34;vp7-line solve&#34; x1=&#34;689&#34; y1=&#34;86&#34; x2=&#34;689&#34; y2=&#34;98&#34;/&gt;
  &lt;polygon points=&#34;685,98 693,98 689,106&#34; fill=&#34;#6e5883&#34;/&gt;
  &lt;rect class=&#34;vp7-box solve&#34; x=&#34;574&#34; y=&#34;106&#34; width=&#34;230&#34; height=&#34;46&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t solve b&#34; x=&#34;689&#34; y=&#34;125&#34; text-anchor=&#34;middle&#34;&gt;Backwards solving&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;689&#34; y=&#34;140&#34; text-anchor=&#34;middle&#34;&gt;what would the attacker need?&lt;/text&gt;
  &lt;line class=&#34;vp7-line solve&#34; x1=&#34;689&#34; y1=&#34;152&#34; x2=&#34;689&#34; y2=&#34;164&#34;/&gt;
  &lt;polygon points=&#34;685,164 693,164 689,172&#34; fill=&#34;#6e5883&#34;/&gt;
  &lt;rect class=&#34;vp7-box solve&#34; x=&#34;574&#34; y=&#34;172&#34; width=&#34;230&#34; height=&#34;46&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t solve b&#34; x=&#34;689&#34; y=&#34;191&#34; text-anchor=&#34;middle&#34;&gt;Proposals&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;689&#34; y=&#34;206&#34; text-anchor=&#34;middle&#34;&gt;goals · checks · blanket · slots&lt;/text&gt;
  &lt;line class=&#34;vp7-line solve&#34; x1=&#34;689&#34; y1=&#34;218&#34; x2=&#34;689&#34; y2=&#34;230&#34;/&gt;
  &lt;polygon points=&#34;685,230 693,230 689,238&#34; fill=&#34;#6e5883&#34;/&gt;
  &lt;text class=&#34;vp7-lab&#34; x=&#34;697&#34; y=&#34;228&#34;&gt;propose&lt;/text&gt;
  &lt;rect class=&#34;vp7-box hot&#34; x=&#34;574&#34; y=&#34;238&#34; width=&#34;230&#34; height=&#34;56&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t hot b&#34; x=&#34;689&#34; y=&#34;257&#34; text-anchor=&#34;middle&#34;&gt;Validation&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;689&#34; y=&#34;272&#34; text-anchor=&#34;middle&#34;&gt;materialise → re-execute →&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;689&#34; y=&#34;286&#34; text-anchor=&#34;middle&#34;&gt;re-check against real knowledge&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm hot&#34; x=&#34;574&#34; y=&#34;316&#34;&gt;the only step allowed to record a result, which is why&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm hot&#34; x=&#34;574&#34; y=&#34;332&#34;&gt;a solver bug costs a missed attack and never a false one&lt;/text&gt;
  &lt;polyline class=&#34;vp7-line&#34; points=&#34;464,307 502,307 502,129 566,129&#34;/&gt;
  &lt;polygon points=&#34;566,125 574,129 566,133&#34; fill=&#34;#1a1a1a&#34;/&gt;
  &lt;text class=&#34;vp7-lab&#34; x=&#34;486&#34; y=&#34;300&#34;&gt;unresolved&lt;/text&gt;
  &lt;polyline class=&#34;vp7-line hot dash&#34; points=&#34;574,266 530,266 530,215 472,215&#34;/&gt;
  &lt;polygon points=&#34;472,211 464,215 472,219&#34; fill=&#34;#a35c3a&#34;/&gt;
  &lt;polyline class=&#34;vp7-line hot&#34; points=&#34;358,330 358,404 340,404 340,418&#34;/&gt;
  &lt;polygon points=&#34;336,418 344,418 340,426&#34; fill=&#34;#a35c3a&#34;/&gt;
  &lt;text class=&#34;vp7-lab&#34; x=&#34;366&#34; y=&#34;376&#34;&gt;resolved&lt;/text&gt;
  &lt;rect class=&#34;vp7-box hot&#34; x=&#34;240&#34; y=&#34;426&#34; width=&#34;200&#34; height=&#34;52&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t b&#34; x=&#34;340&#34; y=&#34;446&#34; text-anchor=&#34;middle&#34;&gt;Minimize witness&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;340&#34; y=&#34;462&#34; text-anchor=&#34;middle&#34;&gt;drop needless substitutions&lt;/text&gt;
  &lt;line class=&#34;vp7-line hot&#34; x1=&#34;440&#34; y1=&#34;452&#34; x2=&#34;466&#34; y2=&#34;452&#34;/&gt;
  &lt;polygon points=&#34;466,448 474,452 466,456&#34; fill=&#34;#a35c3a&#34;/&gt;
  &lt;rect class=&#34;vp7-box hot&#34; x=&#34;476&#34; y=&#34;426&#34; width=&#34;180&#34; height=&#34;52&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t b&#34; x=&#34;566&#34; y=&#34;446&#34; text-anchor=&#34;middle&#34;&gt;Narrate&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;566&#34; y=&#34;462&#34; text-anchor=&#34;middle&#34;&gt;swaps · gates · derivations&lt;/text&gt;
  &lt;line class=&#34;vp7-line hot&#34; x1=&#34;656&#34; y1=&#34;452&#34; x2=&#34;682&#34; y2=&#34;452&#34;/&gt;
  &lt;polygon points=&#34;682,448 690,452 682,456&#34; fill=&#34;#a35c3a&#34;/&gt;
  &lt;rect class=&#34;vp7-box hot&#34; x=&#34;692&#34; y=&#34;426&#34; width=&#34;160&#34; height=&#34;52&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t b&#34; x=&#34;772&#34; y=&#34;456&#34; text-anchor=&#34;middle&#34;&gt;Attack trace&lt;/text&gt;
  &lt;line class=&#34;vp7-line&#34; x1=&#34;20&#34; y1=&#34;446&#34; x2=&#34;52&#34; y2=&#34;446&#34;/&gt;
  &lt;polygon points=&#34;52,442 60,446 52,450&#34; fill=&#34;#1a1a1a&#34;/&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;66&#34; y=&#34;450&#34;&gt;data flow&lt;/text&gt;
  &lt;line class=&#34;vp7-line hot dash&#34; x1=&#34;20&#34; y1=&#34;470&#34; x2=&#34;52&#34; y2=&#34;470&#34;/&gt;
  &lt;polygon points=&#34;52,466 60,470 52,474&#34; fill=&#34;#a35c3a&#34;/&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;66&#34; y=&#34;474&#34;&gt;feedback · new knowledge&lt;/text&gt;
&lt;/svg&gt;
&lt;/div&gt;
&lt;figcaption&gt;&lt;b&gt;How the analysis fits together.&lt;/b&gt; A model is parsed and checked once. The analysis then runs once per phase: attacker knowledge is seeded from what the wire and the &lt;code&gt;leaks&lt;/code&gt; declarations make available in that phase, values are resolved and rewritten, and the deduction rules run until nothing new can be learned. Query evaluation is a separate step that never interleaves with that loop. Under an active attacker, unresolved queries are solved backwards, and anything validation uncovers feeds the next round of deduction.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id=&#34;making-the-attack-readable&#34;&gt;Making the attack readable&lt;/h2&gt;
&lt;p&gt;A verifier that prints &lt;code&gt;confidentiality? m1&lt;/code&gt; and stops has done the easy half of the job. Knowing that something is broken is useful. Knowing &lt;em&gt;what the attacker does&lt;/em&gt; is what lets you fix it, review it, argue about it, or teach it. Verifpal used to print the attacker&amp;rsquo;s knowledge set and the mutated state, and leave you to reconstruct the story from that yourself, which is a difficult reading exercise on anything the size of Signal.&lt;/p&gt;
&lt;p&gt;So the engine does that work now. When a query resolves, two things happen before you see anything: the witness is minimized, and then it is narrated.&lt;/p&gt;
&lt;h3 id=&#34;minimizing-dropping-everything-the-attack-did-not-need&#34;&gt;Minimizing: dropping everything the attack did not need&lt;/h3&gt;
&lt;p&gt;The solver pursues goals for every unresolved query at once and installs the union of the bindings it collects. That is the efficient thing to do, but it means the state that finally answers &lt;em&gt;your&lt;/em&gt; query usually carries substitutions that some &lt;em&gt;other&lt;/em&gt; query needed.&lt;/p&gt;
&lt;p&gt;The forward-secrecy model from the top of this post is a good illustration. Analysing it, Verifpal made 1,805 proposals, and the one that broke &lt;code&gt;confidentiality? m1&lt;/code&gt; replaced five values on the wire simultaneously. You can watch this yourself by setting &lt;code&gt;VERIFPAL_SOLVE_DEBUG=1&lt;/code&gt;, which logs every proposal the solver makes:&lt;/p&gt;
&lt;div class=&#34;vp7-term&#34;&gt;
&lt;div class=&#34;vp7-term-bar&#34;&gt;
&lt;div class=&#34;vp7-term-left&#34;&gt;&lt;div class=&#34;vp7-traffic&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;span class=&#34;vp7-fname&#34;&gt;VERIFPAL_SOLVE_DEBUG=1 verifpal verify &lt;b&gt;signal_unchecked_signature_forward_secrecy.vp&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;span class=&#34;vp7-verdict&#34;&gt;1805 proposals&lt;/span&gt;
&lt;/div&gt;
&lt;pre&gt;&lt;span class=&#34;d&#34;&gt;[solve] Alice ran=true [&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;gbs=PUBKEY(nil) gbo=PUBKEY(nil)&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;gbssig=nil gbe=PUBKEY(nil) e2=nil&lt;/span&gt;&lt;span class=&#34;d&#34;&gt;]&lt;/span&gt;
&lt;span class=&#34;d&#34;&gt;                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                        the two the attack needs         the three the minimizer drops&lt;/span&gt;
&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;The trace you actually saw named two of them. The other three went away one at a time, each dropped because the query kept resolving without it. &lt;code&gt;gbssig&lt;/code&gt; is Bob&amp;rsquo;s prekey signature, and the attacker never needs to touch it precisely because Alice is not checking it. &lt;code&gt;gbe&lt;/code&gt; and &lt;code&gt;e2&lt;/code&gt; belong to the second message, which has nothing to do with &lt;code&gt;m1&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Minimization is deliberately paranoid, because a minimizer that cheats produces a trace that does not reproduce. Every probe re-runs the attack through the same re-execution path the search itself uses, in a scratch analysis context that is not permitted to record results. Each probe also re-seeds attacker knowledge from the passive baseline rather than from everything the attacker knew at the end of the search: knowledge only ever grows, so probing against the final state would make every substitution look droppable. And if nothing replays at all, Verifpal says so and reports only the derivation, instead of naming substitutions that may not be the ones that mattered.&lt;/p&gt;
&lt;h3 id=&#34;narrating-three-kinds-of-step&#34;&gt;Narrating: three kinds of step&lt;/h3&gt;
&lt;p&gt;What survives minimization gets rendered as numbered steps, of which there are exactly three kinds.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A substitution&lt;/strong&gt;, grouped per wire message rather than one line per value, with the honest values in parentheses so you can see what was displaced:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;1. Attacker replaces gbs, gbo (sent by Bob to Alice) with PUBKEY(nil),
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;   PUBKEY(nil). (gbs was PUBKEY(bs); gbo was PUBKEY(bo))
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;A gate:&lt;/strong&gt; a checked primitive that passed even though its inputs were attacker-controlled. This is the step a reader is most likely to disbelieve, so it gets a line of its own rather than being left implicit.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A derivation:&lt;/strong&gt; one line for each step of the knowledge derivation, ingredients before target. These are not reconstructed after the fact by a pretty-printer guessing at what must have happened. Every rule that adds something to attacker knowledge is required to record &lt;em&gt;how&lt;/em&gt; it got there, so the narration is read straight off the derivation the engine performed. A step the attacker carried out inside a different principal&amp;rsquo;s session is labelled with that session, on the line itself.&lt;/p&gt;
&lt;h3 id=&#34;names-not-terms&#34;&gt;Names, not terms&lt;/h3&gt;
&lt;p&gt;The other half of readability is vocabulary. Step 17 of that trace read:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;17. Attacker opens e1 with akenc1, obtaining m1.
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Here is the engine&amp;rsquo;s internal record of the same step, with the terms written out in full:&lt;/p&gt;
&lt;div class=&#34;vp7-term&#34;&gt;
&lt;div class=&#34;vp7-term-bar&#34;&gt;
&lt;div class=&#34;vp7-term-left&#34;&gt;&lt;div class=&#34;vp7-traffic&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;span class=&#34;vp7-fname&#34;&gt;the same fact, before name compression&lt;/span&gt;&lt;/div&gt;
&lt;span class=&#34;vp7-verdict fail&#34;&gt;one step&lt;/span&gt;
&lt;/div&gt;
&lt;pre&gt;&lt;span class=&#34;d&#34;&gt;Deduction › m1 obtained by decomposing AEAD_ENC(HKDF(MAC(HKDF(DH_KEX(PUBKEY(nil),
ae2), HKDF(HASH(DH_KEX(PUBKEY(nil), alongterm), DH_KEX(PUBKEY(blongterm), ae1),
DH_KEX(PUBKEY(nil), ae1), DH_KEX(PUBKEY(blongterm), ae1)), nil, nil), nil), nil),
nil, nil), m1, HASH(PUBKEY(alongterm), PUBKEY(blongterm), PUBKEY(ae2))) with
HKDF(MAC(HKDF(DH_KEX(PUBKEY(nil), ae2), HKDF(HASH(DH_KEX(PUBKEY(nil), alongterm),
DH_KEX(PUBKEY(blongterm), ae1), DH_KEX(PUBKEY(nil), ae1),
DH_KEX(PUBKEY(blongterm), ae1)), nil, nil), nil), nil), nil, nil).&lt;/span&gt;
&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;Both say the same thing. The difference is that the narrator keeps a table mapping terms back to the names the model already gave them, so a value Alice calls &lt;code&gt;akenc1&lt;/code&gt; is called &lt;code&gt;akenc1&lt;/code&gt; in the trace, and only the parts with no name of their own get spelled out. The table also excludes the slots a step is itself touching, which is what stops a substitution line from reading &amp;ldquo;replaces &lt;code&gt;gb&lt;/code&gt; with &lt;code&gt;gb&lt;/code&gt;&amp;rdquo;, and the same table feeds the summary line so it quotes the same vocabulary as the steps above it.&lt;/p&gt;
&lt;p&gt;None of this changes a single verdict. It changes whether the verdict is something you can hand to a colleague who has never opened Verifpal, walk through line by line, and check by hand. For the people who use Verifpal to teach protocol analysis, it also decides whether an attack is a thing students can &lt;em&gt;read&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&#34;phases-and-when-the-attacker-knows-things&#34;&gt;Phases, and when the attacker knows things&lt;/h2&gt;
&lt;p&gt;Phases are how Verifpal models time. &lt;code&gt;phase[1]&lt;/code&gt; means later, and a &lt;code&gt;leaks&lt;/code&gt; declaration inside it is a compromise that happens after the earlier traffic has already gone by. That is what makes forward secrecy, key compromise impersonation and post-compromise security expressible.&lt;/p&gt;
&lt;p&gt;It also creates a trap. If the attacker&amp;rsquo;s phase-1 knowledge is allowed to justify an action it took in phase 0, then any key that ever leaks retroactively forges every signature that was ever checked, and forward secrecy queries stop meaning anything.&lt;/p&gt;
&lt;p&gt;In 0.70.0, a proposed substitution is judged against the attacker&amp;rsquo;s knowledge as of the phase of the value it touches, not against everything accumulated by the end of the run. Attacker knowledge is snapshotted at the end of each phase, and a substitution that reaches back into an earlier phase is re-executed against that earlier snapshot.&lt;/p&gt;
&lt;p&gt;Two models one line apart show the difference:&lt;/p&gt;
&lt;table class=&#34;vp7-matrix&#34;&gt;
&lt;thead&gt;&lt;tr&gt;&lt;th&gt;model&lt;/th&gt;&lt;th&gt;the prekey is…&lt;/th&gt;&lt;th&gt;phase 1&lt;/th&gt;&lt;th&gt;confidentiality? m&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;phase_signed_prekey.vp&lt;/td&gt;&lt;td&gt;signed, and checked with &lt;code&gt;?&lt;/code&gt;&lt;/td&gt;&lt;td&gt;both identity keys leak&lt;/td&gt;&lt;td class=&#34;y&#34;&gt;holds&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;phase_unsigned_prekey.vp&lt;/td&gt;&lt;td&gt;not signed at all&lt;/td&gt;&lt;td&gt;both identity keys leak&lt;/td&gt;&lt;td class=&#34;n&#34;&gt;contradicted&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;In the signed model, the attacker would have to swap the prekey during phase 0, and in phase 0 it does not have the identity key it would need to forge the accompanying signature. The leak arrives too late to help. In the unsigned model there is no signature to forge, the swap succeeds when it is made, and the phase-1 leak completes it. It is the same shape as the Signal example above, in about twenty lines, which makes it a good teaching model for why signed prekeys exist.&lt;/p&gt;
&lt;p&gt;Both directions matter. Too permissive and the tool invents retroactive forgeries; too strict and it misses the store-now-decrypt-later attacks that phases exist to express. Seven models in &lt;code&gt;examples/test/&lt;/code&gt; pin the boundary from both sides, including a three-phase one where the tampering, the compromise and the payoff happen at three different times.&lt;/p&gt;
&lt;h2 id=&#34;relaying-is-not-forging&#34;&gt;Relaying is not forging&lt;/h2&gt;
&lt;p&gt;This distinction comes up as soon as a protocol has more than two parties.&lt;/p&gt;
&lt;p&gt;Take a value that Alice signs and encrypts to Carol; Carol opens it and re-envelopes it to Bob; Bob verifies the signature and MACs it onward to Damian. The message Bob receives was sent by Carol, and it contains Alice&amp;rsquo;s content unchanged. Should &lt;code&gt;authentication? Alice -&amp;gt; Carol: envelope&lt;/code&gt; fail?&lt;/p&gt;
&lt;p&gt;It should not, because being known to the attacker is not the same as being forgeable by the attacker. A value the attacker can watch go past, relay unchanged, or rebuild identically out of honest pieces is not a value it can substitute for something else. An engine that blurs those two things reports man-in-the-middle attacks against protocols that do not have any.&lt;/p&gt;
&lt;div class=&#34;vp7-term&#34;&gt;
&lt;div class=&#34;vp7-term-bar&#34;&gt;
&lt;div class=&#34;vp7-term-left&#34;&gt;&lt;div class=&#34;vp7-traffic&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;span class=&#34;vp7-fname&#34;&gt;verifpal verify &lt;b&gt;four_party.vp&lt;/b&gt;&lt;/span&gt;&lt;/div&gt;
&lt;span class=&#34;vp7-verdict fail&#34;&gt;1 of 4 failed&lt;/span&gt;
&lt;/div&gt;
&lt;pre&gt;&lt;span class=&#34;no&#34;&gt;     Fail ✗&lt;/span&gt; confidentiality? msg
            ╭─ Attack trace:
            │ 1. Attacker observes msg on the wire.
            ╰▸ msg (msg) is obtained by Attacker.
&lt;span class=&#34;ok&#34;&gt;     Pass ✓&lt;/span&gt; authentication? Alice -&amp;gt; Carol: envelope
&lt;span class=&#34;ok&#34;&gt;     Pass ✓&lt;/span&gt; authentication? Carol -&amp;gt; Bob: re_envelope
&lt;span class=&#34;ok&#34;&gt;     Pass ✓&lt;/span&gt; authentication? Bob -&amp;gt; Damian: audit_mac
&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;Confidentiality fails because the final hop sends the plaintext in the clear, which is what this model does deliberately, and the trace is one line long because that is the whole attack. The three authentication queries hold across the entire relay chain, through a re-envelope, a signature check and a MAC. &lt;code&gt;relay_not_forgery.vp&lt;/code&gt; and &lt;code&gt;key_ratchet.vp&lt;/code&gt; pin the same property in smaller models.&lt;/p&gt;
&lt;h2 id=&#34;removing-equations&#34;&gt;Removing equations&lt;/h2&gt;
&lt;p&gt;Verifpal has always had a kind of value that existed for exactly one purpose. Alongside constants and primitives there were &lt;em&gt;equations&lt;/em&gt;, written &lt;code&gt;G^a&lt;/code&gt; and &lt;code&gt;G^a^b&lt;/code&gt;, with &lt;code&gt;G&lt;/code&gt; as a reserved generator, a limit of two exponents, and their own handling in the parser, the pretty printer, the equational theory, the unifier and the attacker search. They modelled Diffie-Hellman and nothing else, and all five of those places had to be kept consistent by hand.&lt;/p&gt;
&lt;p&gt;From 0.70.0, Diffie-Hellman is written with two ordinary primitives: &lt;code&gt;PUBKEY(a)&lt;/code&gt; for a public key derived from a private value, and &lt;code&gt;DH_KEX(gb, a)&lt;/code&gt; for a shared secret.&lt;/p&gt;
&lt;figure class=&#34;vp7fig reveal&#34;&gt;
&lt;div class=&#34;vp7-plain&#34;&gt;
&lt;svg viewBox=&#34;0 0 780 350&#34; role=&#34;img&#34; aria-label=&#34;Left: the old equation value kind, with special cases in the parser, printer, theory, unifier, search and resolution. Right: the same key exchange written as ordinary nested primitives, with commutativity declared as two fields in the primitive spec.&#34;&gt;
  &lt;text class=&#34;vp7-lab&#34; x=&#34;14&#34; y=&#34;16&#34;&gt;Before · Verifpal 0.53&lt;/text&gt;
  &lt;line class=&#34;vp7-line soft&#34; x1=&#34;14&#34; y1=&#34;24&#34; x2=&#34;366&#34; y2=&#34;24&#34;/&gt;
  &lt;rect class=&#34;vp7-box soft&#34; x=&#34;14&#34; y=&#34;36&#34; width=&#34;352&#34; height=&#34;52&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t&#34; x=&#34;28&#34; y=&#34;57&#34;&gt;ga  = G^a&lt;/text&gt;
  &lt;text class=&#34;vp7-t&#34; x=&#34;28&#34; y=&#34;77&#34;&gt;gab = ga^b&lt;/text&gt;
  &lt;line class=&#34;vp7-line&#34; x1=&#34;190&#34; y1=&#34;88&#34; x2=&#34;190&#34; y2=&#34;102&#34;/&gt;
  &lt;polygon points=&#34;186,102 194,102 190,110&#34; fill=&#34;#1a1a1a&#34;/&gt;
  &lt;rect class=&#34;vp7-box gone&#34; x=&#34;90&#34; y=&#34;110&#34; width=&#34;200&#34; height=&#34;32&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t b&#34; x=&#34;190&#34; y=&#34;131&#34; text-anchor=&#34;middle&#34;&gt;Value::Equation&lt;/text&gt;
  &lt;line class=&#34;vp7-line&#34; x1=&#34;190&#34; y1=&#34;142&#34; x2=&#34;190&#34; y2=&#34;158&#34;/&gt;
  &lt;polygon points=&#34;186,158 194,158 190,166&#34; fill=&#34;#1a1a1a&#34;/&gt;
  &lt;text class=&#34;vp7-lab&#34; x=&#34;14&#34; y=&#34;181&#34;&gt;its own special case in all of:&lt;/text&gt;
  &lt;rect class=&#34;vp7-box gone&#34; x=&#34;14&#34; y=&#34;188&#34; width=&#34;108&#34; height=&#34;26&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;68&#34; y=&#34;205&#34; text-anchor=&#34;middle&#34;&gt;parser&lt;/text&gt;
  &lt;rect class=&#34;vp7-box gone&#34; x=&#34;132&#34; y=&#34;188&#34; width=&#34;108&#34; height=&#34;26&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;186&#34; y=&#34;205&#34; text-anchor=&#34;middle&#34;&gt;pretty printer&lt;/text&gt;
  &lt;rect class=&#34;vp7-box gone&#34; x=&#34;250&#34; y=&#34;188&#34; width=&#34;116&#34; height=&#34;26&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;308&#34; y=&#34;205&#34; text-anchor=&#34;middle&#34;&gt;equational theory&lt;/text&gt;
  &lt;rect class=&#34;vp7-box gone&#34; x=&#34;14&#34; y=&#34;222&#34; width=&#34;108&#34; height=&#34;26&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;68&#34; y=&#34;239&#34; text-anchor=&#34;middle&#34;&gt;unifier&lt;/text&gt;
  &lt;rect class=&#34;vp7-box gone&#34; x=&#34;132&#34; y=&#34;222&#34; width=&#34;108&#34; height=&#34;26&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;186&#34; y=&#34;239&#34; text-anchor=&#34;middle&#34;&gt;attacker search&lt;/text&gt;
  &lt;rect class=&#34;vp7-box gone&#34; x=&#34;250&#34; y=&#34;222&#34; width=&#34;116&#34; height=&#34;26&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;308&#34; y=&#34;239&#34; text-anchor=&#34;middle&#34;&gt;resolution&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;14&#34; y=&#34;288&#34;&gt;Two exponents maximum. One reserved generator.&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;14&#34; y=&#34;308&#34;&gt;No way to express a primitive with no exponent at all.&lt;/text&gt;
  &lt;line class=&#34;vp7-region&#34; x1=&#34;390&#34; y1=&#34;8&#34; x2=&#34;390&#34; y2=&#34;342&#34;/&gt;
  &lt;text class=&#34;vp7-lab&#34; x=&#34;414&#34; y=&#34;16&#34;&gt;Now · Verifpal 0.70&lt;/text&gt;
  &lt;line class=&#34;vp7-line soft&#34; x1=&#34;414&#34; y1=&#34;24&#34; x2=&#34;766&#34; y2=&#34;24&#34;/&gt;
  &lt;rect class=&#34;vp7-box soft&#34; x=&#34;414&#34; y=&#34;36&#34; width=&#34;352&#34; height=&#34;52&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t&#34; x=&#34;428&#34; y=&#34;57&#34;&gt;ga  = PUBKEY(a)&lt;/text&gt;
  &lt;text class=&#34;vp7-t&#34; x=&#34;428&#34; y=&#34;77&#34;&gt;gab = DH_KEX(ga, b)&lt;/text&gt;
  &lt;line class=&#34;vp7-line&#34; x1=&#34;590&#34; y1=&#34;88&#34; x2=&#34;590&#34; y2=&#34;102&#34;/&gt;
  &lt;polygon points=&#34;586,102 594,102 590,110&#34; fill=&#34;#1a1a1a&#34;/&gt;
  &lt;rect class=&#34;vp7-box&#34; x=&#34;530&#34; y=&#34;110&#34; width=&#34;120&#34; height=&#34;28&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t b&#34; x=&#34;590&#34; y=&#34;129&#34; text-anchor=&#34;middle&#34;&gt;DH_KEX&lt;/text&gt;
  &lt;line class=&#34;vp7-line&#34; x1=&#34;570&#34; y1=&#34;138&#34; x2=&#34;512&#34; y2=&#34;164&#34;/&gt;
  &lt;line class=&#34;vp7-line&#34; x1=&#34;610&#34; y1=&#34;138&#34; x2=&#34;668&#34; y2=&#34;164&#34;/&gt;
  &lt;rect class=&#34;vp7-box&#34; x=&#34;452&#34; y=&#34;164&#34; width=&#34;120&#34; height=&#34;28&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t&#34; x=&#34;512&#34; y=&#34;183&#34; text-anchor=&#34;middle&#34;&gt;PUBKEY&lt;/text&gt;
  &lt;rect class=&#34;vp7-box&#34; x=&#34;638&#34; y=&#34;164&#34; width=&#34;60&#34; height=&#34;28&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t&#34; x=&#34;668&#34; y=&#34;183&#34; text-anchor=&#34;middle&#34;&gt;b&lt;/text&gt;
  &lt;line class=&#34;vp7-line&#34; x1=&#34;512&#34; y1=&#34;192&#34; x2=&#34;512&#34; y2=&#34;212&#34;/&gt;
  &lt;rect class=&#34;vp7-box&#34; x=&#34;482&#34; y=&#34;212&#34; width=&#34;60&#34; height=&#34;28&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t&#34; x=&#34;512&#34; y=&#34;231&#34; text-anchor=&#34;middle&#34;&gt;a&lt;/text&gt;
  &lt;rect class=&#34;vp7-box good&#34; x=&#34;414&#34; y=&#34;266&#34; width=&#34;352&#34; height=&#34;54&#34; rx=&#34;3&#34;/&gt;
  &lt;text class=&#34;vp7-t good b&#34; x=&#34;428&#34; y=&#34;288&#34;&gt;primitive/spec.rs — two declarative fields&lt;/text&gt;
  &lt;text class=&#34;vp7-t sm&#34; x=&#34;428&#34; y=&#34;307&#34;&gt;commutativity · argument_restrictions&lt;/text&gt;
&lt;/svg&gt;
&lt;/div&gt;
&lt;figcaption&gt;&lt;b&gt;The same key exchange, with one kind of value fewer.&lt;/b&gt; None of the security semantics changed. &lt;code&gt;DH_KEX(PUBKEY(a), b)&lt;/code&gt; and &lt;code&gt;DH_KEX(PUBKEY(b), a)&lt;/code&gt; are still the same shared secret, and the attacker still cannot compute either one from two public keys. What changed is where those facts live: commutativity, and the argument restrictions that make computational Diffie-Hellman structural, are now two fields on a primitive spec instead of properties baked into the value representation.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Migration is mechanical, with five patterns to look for:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;0.53&lt;/th&gt;
          &lt;th&gt;0.70&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;ga = G^a&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;ga = PUBKEY(a)&lt;/code&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;gab = ga^b&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;gab = DH_KEX(ga, b)&lt;/code&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;SIGNVERIF(G^sk, m, sig)&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;SIGNVERIF(PUBKEY(sk), m, sig)&lt;/code&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;PKE_ENC(G^sk, m)&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;PKE_ENC(PUBKEY(sk), m)&lt;/code&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;the attacker&amp;rsquo;s own key, &lt;code&gt;G^nil&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;the attacker&amp;rsquo;s own key, &lt;code&gt;PUBKEY(nil)&lt;/code&gt;&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;Value&lt;/code&gt; now has two variants instead of three. Every function in the engine that matched on a value used to carry a third arm whose behaviour had to be kept consistent with the other two by hand. Resolution, equivalence, matching and the solver each lost one.&lt;/p&gt;
&lt;h2 id=&#34;kem_encap-and-kem_decap&#34;&gt;KEM_ENCAP and KEM_DECAP&lt;/h2&gt;
&lt;p&gt;Removing equations was the prerequisite for this part, rather than the goal in itself.&lt;/p&gt;
&lt;div class=&#34;highlight&#34; data-lang=&#34;verifpal&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-verifpal&#34; data-lang=&#34;verifpal&#34;&gt;&lt;span class=&#34;vp-kw&#34;&gt;principal&lt;/span&gt; &lt;span class=&#34;vp-prin&#34;&gt;Alice&lt;/span&gt;[
	&lt;span class=&#34;vp-kw&#34;&gt;generates&lt;/span&gt; r
	ss, ct = &lt;span class=&#34;vp-prim&#34;&gt;KEM_ENCAP&lt;/span&gt;(ekb, r)      &lt;span class=&#34;vp-cmt&#34;&gt;// a shared secret, and a ciphertext&lt;/span&gt;
	e = &lt;span class=&#34;vp-prim&#34;&gt;AEAD_ENC&lt;/span&gt;(ss, m, &lt;span class=&#34;vp-lit&#34;&gt;nil&lt;/span&gt;)
]

&lt;span class=&#34;vp-prin&#34;&gt;Alice&lt;/span&gt; &lt;span class=&#34;vp-arrow&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;vp-prin&#34;&gt;Bob&lt;/span&gt;: ct, e

&lt;span class=&#34;vp-kw&#34;&gt;principal&lt;/span&gt; &lt;span class=&#34;vp-prin&#34;&gt;Bob&lt;/span&gt;[
	ssb = &lt;span class=&#34;vp-prim&#34;&gt;KEM_DECAP&lt;/span&gt;(dkb, ct)        &lt;span class=&#34;vp-cmt&#34;&gt;// where ekb = PUBKEY(dkb)&lt;/span&gt;
	d = &lt;span class=&#34;vp-prim&#34;&gt;AEAD_DEC&lt;/span&gt;(ssb, e, &lt;span class=&#34;vp-lit&#34;&gt;nil&lt;/span&gt;)&lt;span class=&#34;vp-op&#34;&gt;?&lt;/span&gt;
]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;A key encapsulation mechanism has no exponent to commute and no generator to sit in base position, so there was no way to express one through the equation interface. That was not for want of a feature: the interface was shaped around one algebraic structure that a KEM does not have. As two ordinary primitives, &lt;code&gt;KEM_ENCAP&lt;/code&gt; and &lt;code&gt;KEM_DECAP&lt;/code&gt; are about thirty-five lines of declarative data in &lt;code&gt;primitive/spec.rs&lt;/code&gt;, and the rest of the engine picked them up without being told they exist.&lt;/p&gt;
&lt;p&gt;They are generic rather than named after a specific algorithm. ML-KEM, HQC and Classic McEliece are indistinguishable in the symbolic model: what a symbolic attacker can do with a ciphertext does not depend on whether the underlying hardness assumption is module-lattice or code-based. Naming the primitive &lt;code&gt;MLKEM_ENCAP&lt;/code&gt; would imply a precision the model does not have.&lt;/p&gt;
&lt;p&gt;Since we started recommending &lt;a href=&#34;https://symbolic.software/blog/2026-04-02-pq-native/&#34;&gt;post-quantum native design&lt;/a&gt;, the question we get most often is what a hybrid actually buys you. Three models answer it:&lt;/p&gt;
&lt;table class=&#34;vp7-matrix&#34;&gt;
&lt;thead&gt;&lt;tr&gt;&lt;th&gt;model&lt;/th&gt;&lt;th&gt;what leaks in phase 1&lt;/th&gt;&lt;th&gt;confidentiality? m&lt;/th&gt;&lt;/tr&gt;&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;kem_hybrid_classical_broken.vp&lt;/td&gt;&lt;td&gt;both Diffie-Hellman private values&lt;/td&gt;&lt;td class=&#34;y&#34;&gt;holds&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;kem_hybrid_pq_broken.vp&lt;/td&gt;&lt;td&gt;the decapsulation key&lt;/td&gt;&lt;td class=&#34;y&#34;&gt;holds&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;kem_hybrid_both_broken.vp&lt;/td&gt;&lt;td&gt;both, together&lt;/td&gt;&lt;td class=&#34;n&#34;&gt;contradicted&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;All three are the same protocol: an X25519 shared secret and a KEM shared secret, both fed into &lt;code&gt;HKDF&lt;/code&gt;, with the resulting session key protecting one message. The only difference is what phase 1 leaks. It is the &lt;a href=&#34;https://symbolic.software/blog/2026-04-13-hybrid-constructions/&#34;&gt;hybrid construction argument&lt;/a&gt; as three files you can run in a design review.&lt;/p&gt;
&lt;p&gt;One detail in the KEM rules is easy to get wrong. An attacker holding the decapsulation key can open an encapsulation even if no principal in the model ever calls &lt;code&gt;KEM_DECAP&lt;/code&gt;, because the decomposition rule yields one of the primitive&amp;rsquo;s outputs rather than one of its arguments. Holding the ciphertext and a decapsulation key of its own gets the attacker nowhere: &lt;code&gt;kem_secret_not_forgeable.vp&lt;/code&gt; gives the attacker its own key pair, leaks it outright, hands it the ciphertext from the wire, and both confidentiality queries still hold.&lt;/p&gt;
&lt;p&gt;Two more shapes follow from encapsulation being a function of its arguments. Encapsulate twice to the same key with the same randomness and the shared secret is identical, so &lt;code&gt;equivalence? ss1, ss2&lt;/code&gt; holds, which is how the hazard becomes visible, since two secrets from two separate encapsulations should not be interchangeable. With fresh randomness the query is contradicted. Freshness works the same way: a shared secret is only as fresh as the randomness behind it, so &lt;code&gt;freshness? ssfresh&lt;/code&gt; passes and &lt;code&gt;freshness? ssstatic&lt;/code&gt; does not.&lt;/p&gt;
&lt;h2 id=&#34;twelve-messages-of-signal&#34;&gt;Twelve messages of Signal&lt;/h2&gt;
&lt;p&gt;In February, &lt;a href=&#34;https://symbolic.software/blog/2026-02-10-verifpal-signal/&#34;&gt;Verifpal verified X3DH and the Double Ratchet across three messages&lt;/a&gt; under an active attacker, which was then the deepest result the tool had produced. &lt;code&gt;examples/messaging/signal_twelve.vp&lt;/code&gt; extends the same model to twelve messages. Each one generates a fresh ephemeral, mixes it with the peer&amp;rsquo;s most recent ephemeral public value, and chains the resulting root key onto the previous one: twelve rounds of nested key derivation, and twenty-four queries covering every message in both directions.&lt;/p&gt;
&lt;div class=&#34;vp7-term&#34;&gt;
&lt;div class=&#34;vp7-term-bar&#34;&gt;
&lt;div class=&#34;vp7-term-left&#34;&gt;&lt;div class=&#34;vp7-traffic&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;span class=&#34;vp7-fname&#34;&gt;verifpal verify &lt;b&gt;signal_twelve.vp&lt;/b&gt; --result-code&lt;/span&gt;&lt;/div&gt;
&lt;span class=&#34;vp7-verdict&#34;&gt;24 of 24 verified&lt;/span&gt;
&lt;/div&gt;
&lt;pre&gt;&lt;span class=&#34;d&#34;&gt;attacker[active] · 2 principals · 12 messages · 24 queries&lt;/span&gt;
&lt;span class=&#34;d&#34;&gt;&amp;nbsp;&lt;/span&gt;
&lt;span class=&#34;ok&#34;&gt;c0a0c0a0c0a0c0a0c0a0c0a0c0a0c0a0c0a0c0a0c0a0c0a0&lt;/span&gt;
&lt;span class=&#34;d&#34;&gt;&amp;nbsp;&lt;/span&gt;
&lt;span class=&#34;d&#34;&gt;confidentiality and authentication, for every message, all holding.
real 37.19s&lt;/span&gt;
&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;Thirty-seven seconds is slower than we would like, and we intend to bring it down. But it finishes, and it finishes with the right answer, which the old search could not do at this depth.&lt;/p&gt;
&lt;h2 id=&#34;getting-it-and-moving-your-models-over&#34;&gt;Getting it, and moving your models over&lt;/h2&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;brew tap verifpal.com/source https://github.com/symbolicsoft/verifpal
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;brew install verifpal
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On Windows, &lt;code&gt;scoop bucket add verifpal https://github.com/symbolicsoft/verifpal.git&lt;/code&gt; followed by &lt;code&gt;scoop install verifpal&lt;/code&gt;. Either way you get automatic updates.&lt;/p&gt;
&lt;p&gt;There is no automatic migration tool, deliberately. A rewriter would have to guess which side of a &lt;code&gt;^&lt;/code&gt; was meant to be the exponent in the cases where a model was doing something unusual, and a wrong guess gives you a model that parses cleanly and means something else. The parser points at every line that needs attention instead:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;Error: mymodel.vp:5:8: parse error: cannot use reserved keyword in name: g
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;   ga = G^a
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;         ^
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Work through the five patterns above and most models take a few minutes. The rest of the toolchain is already updated: the browser-based &lt;a href=&#34;https://verifpal.com/workbench/&#34;&gt;Workbench&lt;/a&gt; runs 0.70.0 with nothing to install, &lt;a href=&#34;https://static.verifpal.com/manual.pdf&#34;&gt;Print 20&lt;/a&gt; of the Verifpal Manual documents the new primitives and the backwards search, and both the &lt;a href=&#34;https://github.com/symbolicsoft/verifpal-vscode&#34;&gt;VS Code extension&lt;/a&gt; (1.0.11) and the &lt;a href=&#34;https://github.com/symbolicsoft/verifpal-nvim&#34;&gt;Neovim extension&lt;/a&gt; highlight and document &lt;code&gt;PUBKEY&lt;/code&gt;, &lt;code&gt;DH_KEX&lt;/code&gt;, &lt;code&gt;KEM_ENCAP&lt;/code&gt; and &lt;code&gt;KEM_DECAP&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If you model something and a result looks wrong to you, please &lt;a href=&#34;https://github.com/symbolicsoft/verifpal/issues&#34;&gt;open an issue&lt;/a&gt;. Several of the changes in this release started with exactly that.&lt;/p&gt;
</content:encoded>
      <category>Software</category>
      <category>Research</category>
      <category>Verifpal</category>
      <category>Formal Verification</category>
      <category>Post-Quantum</category>
      
    </item>
    
    <item>
      <title>Meet the Symbolic Software Summer 2026 Team</title>
      <link>https://symbolic.software/blog/2026-07-29-summer-team/</link>
      <pubDate>Wed, 29 Jul 2026 00:00:00 &#43;0000</pubDate>
      <guid>https://symbolic.software/blog/2026-07-29-summer-team/</guid>
      <description>Five people joined us this summer: two research interns formalizing zkVM soundness bounds in Lean, and three teaching assistants running the Applied Cryptography course. Here they are, in their own words.</description>
      <content:encoded>&lt;p&gt;Symbolic Software is a bigger team than usual this summer, and we&amp;rsquo;d like you to meet everyone. Five people joined us: two research interns, Lorenzo and Hossein, who came on board through our &lt;a href=&#34;https://symbolic.software/blog/2026-03-19-internship/&#34;&gt;Summer 2026 research internship&lt;/a&gt;, and three teaching assistants, Faysal, Abd El Kader and Rabab, who are helping run the &lt;a href=&#34;https://appliedcryptography.page&#34;&gt;Applied Cryptography&lt;/a&gt; online course for the &lt;a href=&#34;https://symbolic.software/blog/2026-05-12-course-summer-2026/&#34;&gt;Summer 2026 cohort&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The interns have been working on &lt;a href=&#34;https://github.com/symbolicsoft/soundcalc-lean&#34;&gt;soundcalc-lean&lt;/a&gt;, a project proposed and backed by the Ethereum Foundation: a Lean 4 restatement of &lt;a href=&#34;https://github.com/ethereum/soundcalc&#34;&gt;soundcalc&lt;/a&gt;, the calculator that evaluates the concrete bit security of hash-based zkVMs under different parameter choices. In soundcalc-lean, every cell of a soundness report is re-derived as a machine-checked theorem over exact rationals rather than computed with floats. The teaching assistants, meanwhile, have been grading, proctoring, answering questions and handling logistics for the fifty Lebanese students taking the course this summer.&lt;/p&gt;
&lt;p&gt;Rather than describe everyone ourselves, we asked each of them to introduce themselves and their work. Here they are, in their own words.&lt;/p&gt;
&lt;h2 id=&#34;the-research-interns&#34;&gt;The research interns&lt;/h2&gt;
&lt;h3 id=&#34;hossein-hafezi&#34;&gt;Hossein Hafezi&lt;/h3&gt;
&lt;p&gt;I&amp;rsquo;m Hossein Hafezi, a PhD student at the University of Cambridge (previously at NYU), where I work on applied cryptography and systems security. You can find out more about me at &lt;a href=&#34;https://hosseinhafezi.com&#34;&gt;hosseinhafezi.com&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;At Symbolic Software I worked on rewriting soundcalc in Lean, with added semantics. The project was proposed and backed by the Ethereum Foundation, and it computes the bit security of different hash-based zkVMs under different parameter choices — an important effort given how widely these zkVMs are used, especially in zk-rollups, and the role they are likely to play in post-quantum blockchains. I started this project with zero knowledge of Lean and formal methods, and came away having learned a great deal. We hope the Ethereum and ArkLib communities will keep building on top of our library. One small note: working with Nadim is fun, because he&amp;rsquo;s very hands-on.&lt;/p&gt;
&lt;h3 id=&#34;lorenzo-magliocco&#34;&gt;Lorenzo Magliocco&lt;/h3&gt;
&lt;p&gt;I am Lorenzo Magliocco, a PhD graduate in Cybersecurity from Sapienza University of Rome and LUISS. My research efforts focused on foundational theoretical cryptography, particularly in the design of multi-party protocols and theoretical models where truly honest parties do not exist.&lt;/p&gt;
&lt;p&gt;Coming from a background in engineering and cybersecurity, I always had an inkling for exploring more applied avenues, and that&amp;rsquo;s what brought me to Symbolic Software! From no prior experience in formal verification and open-source development, I was able to contribute towards writing a Lean port of soundcalc: a Python tool that enables zkVM developers to evaluate the overall soundness and proof size of different zkVM configurations. Early on, I gave an introductory bullet talk on our efforts to the broader Ethereum formal verification community (Ethproofs #9). Ever since, I have been working mostly on the engineering side of the project, while diving into Lean subtleties and proofs as needed. I am already happy with the results we achieved so far, and I hope the broader zkVM ecosystem will be able to benefit from our efforts!&lt;/p&gt;
&lt;h2 id=&#34;the-teaching-assistants&#34;&gt;The teaching assistants&lt;/h2&gt;
&lt;h3 id=&#34;faysal-el-estwani&#34;&gt;Faysal El Estwani&lt;/h3&gt;
&lt;p&gt;Hi, my name is Faysal El Estwani! I&amp;rsquo;m currently pursuing a Master&amp;rsquo;s degree in Computer Science at the American University of Beirut, where my thesis focuses on cybersecurity, specifically malware analysis. I&amp;rsquo;m passionate about software engineering and enjoy tackling programming problems. Outside of cybersecurity, I have a strong interest in game development and love exploring how software can be used to create interactive experiences. In my free time, I&amp;rsquo;m also an avid music enthusiast and enjoy learning about and playing musical instruments.&lt;/p&gt;
&lt;p&gt;This summer, I&amp;rsquo;ve been working as a Teaching Assistant for the Applied Cryptography course. I spend most of my time grading assignments and exams, answering students&amp;rsquo; questions, and helping explain concepts that they find challenging. It&amp;rsquo;s been a great opportunity to strengthen my own understanding while helping others learn. It&amp;rsquo;s so rewarding explaining something to a student and then seeing them apply it correctly on an exam or exercise!&lt;/p&gt;
&lt;h3 id=&#34;abd-el-kader-kahil&#34;&gt;Abd El Kader Kahil&lt;/h3&gt;
&lt;p&gt;Hello, my name is Abd El Kader Kahil. I am a graduate student, finishing up my Master&amp;rsquo;s thesis in Lebanon at the American University of Beirut. My technical interests include programming language design and implementation, cryptography, graphics/game programming and algorithm optimizations. Here&amp;rsquo;s &lt;a href=&#34;https://aboudekahil.github.io/&#34;&gt;my website&lt;/a&gt; with links to my socials.&lt;/p&gt;
&lt;p&gt;At Symbolic Software I worked as a Teaching Assistant with Professor Nadim Kobeissi at his independent &lt;a href=&#34;https://appliedcryptography.page&#34;&gt;summer course&lt;/a&gt;. During this period I corrected assignments, answered questions, held exams and handled logistics. My goal is to minimize the friction between Lebanese students and learning cryptography and make this subject more widely studied and researched in the country. This opportunity also allowed me to connect with promising students in Lebanon and help shape their knowledge base and future.&lt;/p&gt;
&lt;h3 id=&#34;rabab-salim&#34;&gt;Rabab Salim&lt;/h3&gt;
&lt;p&gt;Hello, I&amp;rsquo;m Rabab Salim, a Master&amp;rsquo;s student at the American University of Beirut where I&amp;rsquo;m currently finishing up my thesis on topics related to cybersecurity and machine learning. I have also TA&amp;rsquo;ed many courses as part of my Graduate Assistantship here, which has ignited my passion towards teaching.&lt;/p&gt;
&lt;p&gt;Working with Professor Nadim under Symbolic Software as part of the Applied Cryptography course has been a wonderful experience. Being a teaching assistant here isn&amp;rsquo;t constricted to grading assignments and proctoring exams, but is part of a bigger goal of making cryptography easy and accessible to Lebanese students, and this is what I hope to accomplish by the end of the course. Meeting and engaging with students has been insightful and inspiring in many different ways. I aspire to make the most out of it while pushing towards many more meaningful goals like this one!&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Our thanks to all five. The summer&amp;rsquo;s research output and the course both rest on their work. We&amp;rsquo;ll have more to say about soundcalc-lean as the project matures, and the Summer 2026 cohort runs through October 10.&lt;/p&gt;
</content:encoded>
      <category>Announcement</category>
      <category>Internship</category>
      <category>Course</category>
      <category>Applied Cryptography</category>
      
    </item>
    
    <item>
      <title>Why I Teach Cryptography in Lebanon</title>
      <link>https://symbolic.software/blog/2026-07-22-why-i-teach-cryptography-in-lebanon/</link>
      <pubDate>Wed, 22 Jul 2026 00:00:00 &#43;0000</pubDate>
      <guid>https://symbolic.software/blog/2026-07-22-why-i-teach-cryptography-in-lebanon/</guid>
      <description>Cedarcrypt&#39;s first edition has concluded. On what a year of teaching cryptography in and for Lebanon has meant, why the conference met in Cyprus rather than Beirut, and the decades of work ahead.</description>
      <content:encoded>&lt;p&gt;&lt;a href=&#34;https://cedarcrypt.org&#34;&gt;Cedarcrypt&lt;/a&gt;, an applied cryptography summer school and conference, concluded on July 16 after four days at the American University of Beirut&amp;rsquo;s campus in Paphos, Cyprus, about two hundred kilometers of sea from Lebanon. All year, colleagues and friends have asked me some version of the same question, and I have asked it of myself: why spend a year of eighty-hour weeks building a cryptography course, then a scholarship program, then a conference, for a region the field has never counted? This is the mission of my lifetime.&lt;/p&gt;
&lt;h2 id=&#34;communication-in-the-presence-of-adversaries&#34;&gt;Communication in the presence of adversaries&lt;/h2&gt;
&lt;p&gt;Ron Rivest defined cryptography as communication in the presence of adversaries. It remains the best definition available, and it is a strange one: a science whose object is defined by hostility, by the standing assumption that someone is working against you. Number theory assumes no enemy. Cryptography assumes little else.&lt;/p&gt;
&lt;p&gt;I was born in Beirut. In the country I come from, Rivest&amp;rsquo;s phrase is not a technical definition; it is a description of conditions. Since 2019, Lebanon&amp;rsquo;s currency has lost roughly ninety-eight percent of its value, and depositors have watched their savings sealed inside a banking system that had quietly become insolvent. The state provides a few hours of electricity a day; diesel generators cover the rest. In 2020, a warehouse of ammonium nitrate leveled the port of Beirut in one of the largest non-nuclear explosions ever recorded. In 2024, a war killed thousands and displaced more than a million people in a country of five million. The World Bank ranks the compound collapse among the most severe crises anywhere since the middle of the nineteenth century. Ordinary life in Lebanon has become an exercise in operating in the presence of adversaries: institutional, economic, military, and increasingly digital.&lt;/p&gt;
&lt;p&gt;So when I began teaching applied cryptography to fifty-five students at the American University of Beirut in the fall of 2025, certain things did not need to be taught. I have watched seasoned engineers in peaceful countries struggle to take metadata seriously as a threat. My students had relatives who were tracked through their phones during the war. Nobody asked why forward secrecy matters, or why a protocol should assume the server is hostile, or why anyone would bother hiding who talks to whom. Their threat model predated my whiteboard: it was the family group chat.&lt;/p&gt;
&lt;p&gt;The geography of cryptographic expertise is upside down. The field concentrates its people, funding, conferences, and prestige in a handful of prosperous capitals, places where cryptography is a profession. It is thinnest where cryptography is a lifeline: post-conflict states, blackout zones, societies under active surveillance. I claim no position outside this pattern; I built my career and my firm in Paris and have spent years enjoying the field&amp;rsquo;s comforts. But a discipline that theorizes adversarial conditions while absenting itself from nearly every place that suffers them has an incomplete claim to its own name. Bringing the expertise to where the adversaries actually operate is what the phrase &lt;em&gt;real-world cryptography&lt;/em&gt; ought, at minimum, to mean.&lt;/p&gt;
&lt;h2 id=&#34;fifty-five-students&#34;&gt;Fifty-five students&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://appliedcryptography.page&#34;&gt;The course was built from scratch&lt;/a&gt;: about 1,500 slides, eight problem sets, eight implementation projects in Go and Rust, a syllabus that starts at the one-time pad and ends at zero-knowledge proofs and post-quantum systems. Most of the fifty-five had never seen a security definition. The conditions were what they were. Israeli drones passed low over campus most days, often during lecture. Power cuts were routine, and students debugged their Double Ratchet implementations on laptop batteries. I expected all of this to degrade the course, and it did the opposite. Simone Weil called attention the rarest and purest form of generosity; whatever that is as metaphysics, as a description of a classroom in a collapsing country it is exact. Attention there is expensive, and people who choose to spend it on you have made an actual decision.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://symbolic.software/images/blog/aub-class-fall-2025.webp&#34; alt=&#34;Group photo of the fifty-five students of the Applied Cryptography class at the American University of Beirut&#34; width=&#34;1800&#34; height=&#34;1350&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;
&lt;figcaption&gt;The Applied Cryptography class at the American University of Beirut, fall 2025.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Teaching there showed me something that I suspect is invisible from inside the field&amp;rsquo;s usual venues: most of the difficulty of cryptography is not difficulty. It is intimidation. The notation is more frightening than the ideas it encodes. When I presented Diffie–Hellman in full formal dress, safe primes and cyclic subgroups and a decisional assumption quantified over probabilistic polynomial-time adversaries, I watched faces close. When I presented &lt;a href=&#34;https://appliedcryptography.page/gadgets/clocks/&#34;&gt;the same idea as clock arithmetic&lt;/a&gt;, the same students started asking exactly the right questions, and weeks later were reading the formal treatment on their own, because by then they knew the notation was shorthand for something they already understood. The obstacle was never the mathematics but an acquired belief, installed by years of small signals, that this field belongs to other people. We treated that belief as a technical problem and worked on it deliberately: optional student presentations that filled the room anyway, a poster session, a weekly coffee-shop-style, donut-laden gathering called The Key Exchange where the conversation ran from lattice reductions to how one writes to a professor one has never met. The field helped more than I had any right to expect. Jean-Philippe Aumasson flew to Beirut to lecture in person; Kenny Paterson, Rolfe Schmidt, Nigel Smart, Vitalik Buterin and others gave their time in a semester when engaging with Lebanon was not a light decision.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://symbolic.software/images/blog/aub-student-presentations.webp&#34; alt=&#34;Students giving presentations on the AUB campus&#34; width=&#34;1500&#34; height=&#34;1125&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;
&lt;figcaption&gt;Student presentation sessions on the AUB campus.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Students who found the first problem set bewildering earned near-perfect scores on the fourth. The &lt;a href=&#34;https://appliedcryptography.page/final-exam/&#34;&gt;final exam, which I published openly&lt;/a&gt; so that colleagues could judge its difficulty for themselves, averaged 82 out of 100. By semester&amp;rsquo;s end, students who had begun with nothing had implemented Signal&amp;rsquo;s Double Ratchet, built hybrid post-quantum systems on ML-KEM and ML-DSA, written zero-knowledge applications, and formally verified protocols. One of them, Silina Elfil, produced state-separable proofs cleaner and shorter than the published ones in the textbook she was learning from. Strong departments in Europe and North America now write to ask me for these students. A door was opened, and the people on the other side of it turned out to be as capable as anyone should have assumed all along.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://symbolic.software/images/blog/aub-zk-presentation.webp&#34; alt=&#34;Suleiman Jaber presenting on zero-knowledge proofs&#34; width=&#34;1398&#34; height=&#34;1423&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;
&lt;figcaption&gt;Suleiman Jaber presenting on zero-knowledge proofs.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;That semester took five months of eighty-hour weeks, and I corrected more than 2,300 pages of assignments and exams by hand, alone. In the accounting my field uses to allocate careers, all of it counts for less than a single workshop paper. I knew this going in; colleagues confirmed it, kindly, as a warning. We have built an economy of recognition in which the work that most changes lives is the work that does not count, and professors at every university you know spend entire careers on the wrong side of that ledger. &lt;a href=&#34;https://www.youtube.com/watch?v=z_Hx5xgD0tc&#34;&gt;I said a version of this from the stage at Real World Crypto&lt;/a&gt; earlier this year: we hand one another recognitions we will not care about at the end of our lives while withholding them from the work we will. Two of my students were supposed to be at that conference with me. Their flights out of Beirut were canceled at the last minute because of the war, and I opened the talk beneath their photographs. The field&amp;rsquo;s incentive structures, unlike the war, are ours: we built them, and we can build different ones.&lt;/p&gt;
&lt;h2 id=&#34;precedents&#34;&gt;Precedents&lt;/h2&gt;
&lt;p&gt;Cedarcrypt&amp;rsquo;s logo is a broken Phoenician tablet mended back together with gold, in the manner of Japanese kintsugi, the craft of repairing fractured pottery with lacquer dusted or mixed with gold so that the seams are not hidden but illuminated. I chose it because the break is not disguised. The mend is worth more than the unbroken original precisely because it makes the fracture part of the object&amp;rsquo;s history. The Phoenician tablet stands for the region that gave the alphabet to the world; the gold seams stand for what can be made of a thing after it has been shattered.&lt;/p&gt;
&lt;p&gt;The university stands for the same thing. AUB was founded in 1866 and has held classes through famine, world wars, fifteen years of civil war, and the current collapse. In 1948, Constantine Zurayk, a historian there, wrote a short, severe book called &lt;a href=&#34;https://archive.org/details/zurayk-nakba&#34;&gt;&lt;em&gt;The Meaning of the Disaster&lt;/em&gt;&lt;/a&gt;, arguing that the answer to regional catastrophe was neither slogan nor lament but the patient, generational mastery of modern science. I make no grand claims for one cryptography course, but I know whose program I am following. And when Bana Bashour opened Cedarcrypt with a talk on higher education that turned on Plato&amp;rsquo;s cave, her point being that the ascent toward the light is only half of Plato&amp;rsquo;s argument and that the one who ascends is obligated to go back down, she was making a general case about universities. For me it doubled as a precise account of why I went back to Beirut to teach.&lt;/p&gt;
&lt;h2 id=&#34;four-days-in-paphos&#34;&gt;Four days in Paphos&lt;/h2&gt;
&lt;p&gt;Cedarcrypt was hosted in Paphos instead of Beirut because I could not yet responsibly ask the international community to convene inside Lebanon, and I could not hang two dozen student scholarships on travel logistics that one bad week could unmake. Cyprus is the standing answer to this problem. During the civil war, when Beirut&amp;rsquo;s port and airport closed, the ferry from Jounieh to Larnaca became Lebanon&amp;rsquo;s route to the world, and a good part of Lebanese commercial life ran from the island until home reopened. AUB now keeps a campus in Paphos, AUB Mediterraneo, our host and co-organizer, and its teaching therefore no longer depends entirely on conditions at home. Convening there was a compromise. I want a future edition of Cedarcrypt to meet in Beirut, the moment it can be done responsibly.&lt;/p&gt;
&lt;p&gt;The program itself: Bart Preneel, who heads the COSIC group at KU Leuven, traced fifty years of the Crypto Wars, from the Clipper Chip to client-side scanning, before an audience for whom the question of who may read one&amp;rsquo;s messages has never been academic. Meryem Cherkaoui Semmouni, of Sidi Mohamed Ben Abdellah University in Morocco, spent the first afternoon leading students through lattice attacks on RSA with the LLL algorithm. Engineers and researchers from Mozilla, The Guardian, Nokia Bell Labs, and the threshold-cryptography world taught constant-time programming, web transparency logs, homomorphic encryption in production, and group messaging for whistleblowing systems. Students implemented components of hash-based SNARKs in a three-hour workshop. A panel put a former IACR president and the co-founder of Ethereum in front of students who twelve months earlier had no path into the field. There were lightning talks; the best won a hardcover of Mike Rosulek&amp;rsquo;s excellent &lt;a href=&#34;https://joyofcryptography.com&#34;&gt;&lt;em&gt;The Joy of Cryptography&lt;/em&gt;&lt;/a&gt;. There was a dinner on a mountaintop above Paphos, and a day trip along the coast before the program began, to the sanctuary of Aphrodite at Palaepaphos and the theatre at Kourion, because communities form on buses and over long lunches at least as reliably as in lecture halls.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://symbolic.software/images/blog/cedarcrypt-panel.webp&#34; alt=&#34;Panel discussion on stage at Cedarcrypt with Vitalik Buterin, Lucas Meier, Bart Preneel, Giacomo Fenzi, and Nadim Kobeissi&#34; width=&#34;1600&#34; height=&#34;1148&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;
&lt;figcaption&gt;The panel on the future of applied cryptography: Vitalik Buterin, Lucas Meier, Bart Preneel, and Giacomo Fenzi, with me moderating.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;On the morning of July 11, most of our scholarship students boarded the same Middle East Airlines flight out of Beirut, and a chartered bus met them at Larnaca airport to take them to the student dorms. More than two dozen attendees came on scholarship support, the majority from Lebanon, joined by two more who flew in from Cairo and Zurich. The funding came from Cure53, Zama, PQShield, Electi, and Passbolt, and from anonymous donors, one of whom, a private individual, gave more than any company; the event ran in collaboration with the IACR. Among the students on that flight were some of those whose flights to Real World Crypto the war had canceled in the spring. This time the plane took off.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://symbolic.software/images/blog/cedarcrypt-outing.webp&#34; alt=&#34;Group photo of Cedarcrypt attendees on the coastal outing in Cyprus&#34; width=&#34;1800&#34; height=&#34;1200&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;
&lt;figcaption&gt;The pre-conference outing along the Cyprus coast.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&#34;https://www.flickr.com/photos/nadimkobeissi/albums/72177720334794381/&#34;&gt;The full Cedarcrypt 2026 photo album is available on Flickr.&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This conference was organized in substantial part by my former students. The workshop chair, the web secretary, the sponsorship secretary, the logistics secretary, the ethics officer: a year ago, every one of them was sitting in a lecture hall in Beirut learning what a pseudorandom function is. This summer they negotiated with sponsors, ran international logistics, and staffed the registration desk of an institution that had not existed a year earlier. The distance from student to builder of institutions was twelve months.&lt;/p&gt;
&lt;p&gt;On the first day, three of these organizers walked on stage and gave Bart Preneel a painting: a woman in the embroidered dress of the old Levant, carrying a water jar up through a hillside village.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://symbolic.software/images/blog/cedarcrypt-painting.webp&#34; alt=&#34;Three students presenting Bart Preneel with a painting of a woman carrying a water jar&#34; width=&#34;1800&#34; height=&#34;1174&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;
&lt;figcaption&gt;Faysal Elestwani, Abd El Kader Kahil, and Rabab Salim presenting Bart Preneel with the painting.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The attendees are writing their own accounts now. &lt;a href=&#34;https://www.linkedin.com/feed/update/urn:li:activity:7484998386404995072/&#34;&gt;One, a master&amp;rsquo;s student at AUB, wrote that this was their first conference&lt;/a&gt; and that a highlight was discussing their own research with Vitalik Buterin. Another, &lt;a href=&#34;https://www.linkedin.com/feed/update/urn:li:activity:7485128931575021568/&#34;&gt;a graduate student at Beirut Arab University who came on scholarship&lt;/a&gt;, wrote of leaving with more questions and more motivation than they had arrived with, and that this felt like the whole point. I could not have put the success criterion better, and I did not have to. A student wrote it!&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://symbolic.software/images/blog/cedarcrypt-auditorium.webp&#34; alt=&#34;Students in the AUB Mediterraneo auditorium following a workshop&#34; width=&#34;1800&#34; height=&#34;1200&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;
&lt;figcaption&gt;A workshop session in the AUB Mediterraneo auditorium.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Meanwhile the teaching continues. &lt;a href=&#34;https://symbolic.software/blog/2026-05-12-course-summer-2026/&#34;&gt;This summer&amp;rsquo;s free online cohort&lt;/a&gt;, which initially accepted fifty students from nine institutions with the Lebanese University foremost among them, is mid-semester right now, meeting Tuesdays and Thursdays at eleven in the morning, Beirut time. The materials are open under Creative Commons, permanently. And the course written for Beirut is now the foundation of applied cryptography teaching at Ruhr University Bochum, Technische Hochschule Lübeck, the University of Vienna, and the University of Edinburgh. The expertise was supposed to flow in the other direction.&lt;/p&gt;
&lt;figure&gt;
&lt;img src=&#34;https://symbolic.software/images/blog/summer-2026-midterm.webp&#34; alt=&#34;Students seated at U-shaped conference tables writing their midterm exams in a hotel conference room&#34; width=&#34;1800&#34; height=&#34;1350&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34;&gt;
&lt;figcaption&gt;Students of the Summer 2026 online cohort sitting their midterm exam, in a hotel conference room rented for the occasion.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id=&#34;the-decades-ahead&#34;&gt;The decades ahead&lt;/h2&gt;
&lt;p&gt;The strongest of my students receive offers from abroad; I write their recommendation letters gladly, and every letter costs me something. A region does not keep its field by barring the door. It keeps its field by building reasons to stay and bridges for those who leave to remain part of the work. The old cities of this coast never held an empire; they held a network, and the network outlasted the empires that sacked them. That is the working model: a community whose members may sit in Waterloo or Zurich or Hamra, with its center of gravity in the Levant all the same.&lt;/p&gt;
&lt;p&gt;Cedarcrypt returns in 2027. The summer program will run again, free, for as long as I can fund and staff it. The course materials remain open, and I will help any university anywhere that wants to teach from them, as four already do. I will keep making the argument about incentives in every venue that will have me: a field that cannot count the work of building people has resigned itself to remaining a guild of the fortunate. And the long-term goal, at the ten- and twenty-year mark, is for this mission to become unnecessary. Cryptography in the Levant will have succeeded when it is unremarkable, when a student in Tripoli or Saida can find a course, an advisor, a community, and a career without any of it depending on one exhausted man and his friends.&lt;/p&gt;
&lt;p&gt;None of this belongs to me alone. Wassim El Hajj, AUB Mediterraneo&amp;rsquo;s rector, Malek Tabbal, my co-chair, and the leadership of AUB and AUB Mediterraneo, who said yes when yes was expensive. Jean-Philippe Aumasson and Lucas Meier, who helped me build the program. Ghady Youssef, Abd El Kader Kahil, Rabab Salim, Nai Hamdan, and Faysal Elestwani, the students who became organizers. The AUB faculty who carried the course with me through the fall. The sponsors, and the donors who asked for nothing, not even their names. The speakers who crossed the world for a first-edition conference on the strength of an idea. My wife, Arij, who kept believing in this throughout. And my students, who are the entire point.&lt;/p&gt;
&lt;p&gt;The painting the students chose is an image of provision as work: unglamorous, scheduled, indispensable. That is what teaching is, and it is what building a field will be: water carried uphill, on a schedule, for years. Two hundred kilometers east of that stage is the city where this started and where I intend for it to continue. In the meantime there is a cohort mid-semester, a 2027 edition to plan, and water to carry.&lt;/p&gt;
</content:encoded>
      <category>Essay</category>
      <category>Cedarcrypt</category>
      <category>Applied Cryptography</category>
      <category>Course</category>
      
    </item>
    
    <item>
      <title>How Jevil works</title>
      <link>https://symbolic.software/blog/2026-05-31-jevil/</link>
      <pubDate>Sun, 31 May 2026 00:00:00 &#43;0000</pubDate>
      <guid>https://symbolic.software/blog/2026-05-31-jevil/</guid>
      <description>A playground-first, interactive explainer for Jevil, a post-quantum, transparent few-time signature scheme whose key-recovery threshold is a single sharp cliff rather than a slow slope.</description>
      <content:encoded>&lt;p&gt;Most signature schemes are designed so that nothing bad ever happens. Jevil is designed so that something catastrophic happens &lt;em&gt;exactly once&lt;/em&gt;, at a moment you choose in advance.&lt;/p&gt;
&lt;p&gt;It is a &lt;em&gt;few-time&lt;/em&gt; signature scheme. You pick a budget (say, a thousand signatures) at key generation. The first thousand signatures are ordinary, unforgeable, post-quantum signatures. The thousand-and-first publishes your secret key to the entire world. Not &amp;ldquo;weakens.&amp;rdquo; Not &amp;ldquo;degrades.&amp;rdquo; Publishes. Anyone watching can sit down with a piece of paper and reconstruct it.&lt;/p&gt;
&lt;p&gt;That is the whole idea, and the rest of this post is about why anyone would want such a thing, and how the machinery underneath actually works. The best way to start is to break it yourself. Go ahead: sign a few messages, then push it over the edge.&lt;/p&gt;




&lt;figure class=&#34;jvfig&#34; data-fig=&#34;sandbox&#34;&gt;
  &lt;div class=&#34;jvfig-fallback&#34;&gt;
    &lt;span class=&#34;jvfig-fallback-tag&#34;&gt;interactive figure&lt;/span&gt;
    &lt;p&gt;Enable JavaScript to explore this figure.&lt;/p&gt;
  &lt;/div&gt;
&lt;/figure&gt;

&lt;p&gt;If you cranked the budget up and signed past it, you saw the fog of possibilities collapse onto a single curve. Hold that picture; everything below explains it.&lt;/p&gt;
&lt;h2 id=&#34;why-would-you-want-a-cliff&#34;&gt;Why would you &lt;em&gt;want&lt;/em&gt; a cliff?&lt;/h2&gt;
&lt;p&gt;A few-time signature caps how many signatures you can safely issue. That sounds like a limitation, and it is, but plenty of things in the real world are intrinsically finite and would rather &lt;em&gt;be&lt;/em&gt; finite: a firmware vendor that will sign a bounded number of releases and then go quiet for a decade; an operator binding themselves to a fixed per-tenure attestation budget; an audit-budgeted credential that issues its allowance and then has no further business existing. In all of these, &amp;ldquo;you may sign at most $n^\star$ times&amp;rdquo; is not a nuisance. It is the actual security goal.&lt;/p&gt;
&lt;p&gt;The obvious way to enforce a count is with &lt;em&gt;state&lt;/em&gt;: a counter, a synced database, a hardware register that ticks up and refuses to go past the budget. This is how stateful hash-based schemes like XMSS and LMS work, and it is notoriously fragile in deployment. Restore a backup, clone a VM, fork a container, and the counter silently rolls back, so the signer happily re-uses key material it already spent and security quietly collapses with no alarm. That fragility is exactly what motivated the move to &lt;em&gt;stateless&lt;/em&gt; schemes in the first place.&lt;/p&gt;
&lt;p&gt;Stateless schemes route around the counter by making the cap a property of the cryptographic object itself. The dominant family, HORS and its descendants (which sit at the leaves of SPHINCS⁺ and the standardized SLH-DSA), does this, but its failure is &lt;em&gt;soft&lt;/em&gt;. After $n$ signatures, a forger&amp;rsquo;s success probability rises gradually, roughly as $\left(\tfrac{nK}{T}\right)^{K}$. There is no single moment where anything breaks; &amp;ldquo;how many signatures is safe&amp;rdquo; is a slider on a continuum, and you pick a point on the slope and hope.&lt;/p&gt;
&lt;p&gt;Jevil&amp;rsquo;s degradation is the opposite of soft. It is a wall.&lt;/p&gt;




&lt;figure class=&#34;jvfig&#34; data-fig=&#34;slope-cliff&#34;&gt;
  &lt;div class=&#34;jvfig-fallback&#34;&gt;
    &lt;span class=&#34;jvfig-fallback-tag&#34;&gt;interactive figure&lt;/span&gt;
    &lt;p&gt;Enable JavaScript to explore this figure.&lt;/p&gt;
  &lt;/div&gt;
&lt;/figure&gt;

&lt;p&gt;To the best of my knowledge, Jevil is the first few-time signature scheme that is simultaneously &lt;strong&gt;post-quantum&lt;/strong&gt;, &lt;strong&gt;transparent&lt;/strong&gt; (no trusted setup), and &lt;strong&gt;cliff-shaped&lt;/strong&gt;: its key-recovery threshold is a single sharp edge rather than a probabilistic ramp. Cliff-like behavior has shown up before, but always in a corner that fails one of those three tests. Nonce reuse in Schnorr or ECDSA recovers the key from two signatures, but it is classical and an &lt;em&gt;accident&lt;/em&gt;, not a designed feature. Chaum-style offline e-cash extracts a double-spender&amp;rsquo;s identity, but it is classical and interactive. Polynomial-witness constructions over KZG or IPA commitments have exactly this geometry, but KZG needs a trusted setup and both are broken by Shor. Jevil&amp;rsquo;s one technical move is to bring that geometry into the post-quantum, transparent world. We will get to how.&lt;/p&gt;
&lt;h2 id=&#34;the-secret-is-a-polynomial&#34;&gt;The secret is a polynomial&lt;/h2&gt;
&lt;p&gt;Here is the entire trick, with no cryptography in it yet, just a fact about polynomials you already half-remember.&lt;/p&gt;
&lt;p&gt;Two points determine a line. Three points determine a parabola. In general, $D+1$ distinct points determine a unique polynomial of degree $D$, and &lt;em&gt;fewer than that determine nothing&lt;/em&gt;: through any $D$ points there run infinitely many degree-$D$ curves. This is the seesaw the whole scheme balances on. Give an observer $D$ evaluations of a secret degree-$D$ polynomial and they have learned, in a precise sense, nothing about which polynomial it is. Give them one more, the $(D{+}1)$-th, and it is pinned forever; they can recover every coefficient in $O(D^2)$ arithmetic by Lagrange interpolation.&lt;/p&gt;
&lt;p&gt;That is the snap you saw in the sandbox. Below the threshold, a fog of curves all fit the revealed points equally well. At the threshold, the fog has exactly one member.&lt;/p&gt;
&lt;p&gt;So here is the plan. &lt;strong&gt;The secret key is the coefficient vector of a degree-$D$ polynomial $f$.&lt;/strong&gt; Signing a message will reveal a few evaluations of $f$. If we arrange for each signature to reveal exactly enough, and for the budget $n^\star$ to be the number of signatures that &lt;em&gt;just barely&lt;/em&gt; keeps the total below $D+1$, then signatures $1$ through $n^\star$ leave $f$ undetermined, and signature $n^\star{+}1$ tips it over. The cliff is not bolted on. It is the same Lagrange fact that powers Shamir secret sharing and threshold signatures, simply &lt;em&gt;inverted&lt;/em&gt;: where those schemes hand out evaluations on purpose so that enough parties can reconstruct, Jevil accumulates them as the penalty for over-use.&lt;/p&gt;
&lt;p&gt;There is one detail worth flagging now because it reappears everywhere: the public key carries a single free evaluation of $f$, a point $(z, w=f(z))$ I will call the &lt;strong&gt;out-of-domain&lt;/strong&gt; point. Think of it as one of the $D+1$ pins handed out for free at key generation. It is the purple point in the sandbox, and it earns its keep later.&lt;/p&gt;
&lt;h2 id=&#34;a-polynomial-becomes-a-signature&#34;&gt;A polynomial becomes a signature&lt;/h2&gt;
&lt;p&gt;If the secret is a polynomial, signing a message has to mean &lt;em&gt;revealing some evaluations of it&lt;/em&gt;, and the choice of &lt;em&gt;where&lt;/em&gt; to evaluate has to come from the message, deterministically, so that the verifier can re-derive the same points.&lt;/p&gt;
&lt;p&gt;That is exactly what happens. The message is hashed to produce $K$ positions (Jevil fixes $K=16$), and the signature reveals $f$ at those $K$ points. &amp;ldquo;Hashed&amp;rdquo; means the positions are unpredictable and effectively random, but &lt;em&gt;not&lt;/em&gt; fresh on every call: the same message always yields the same positions, so re-signing a message you already signed is free and tells the world nothing new. Type the same string into the figure below twice and watch the second one change nothing.&lt;/p&gt;




&lt;figure class=&#34;jvfig&#34; data-fig=&#34;positions-ring&#34;&gt;
  &lt;div class=&#34;jvfig-fallback&#34;&gt;
    &lt;span class=&#34;jvfig-fallback-tag&#34;&gt;interactive figure&lt;/span&gt;
    &lt;p&gt;Enable JavaScript to explore this figure.&lt;/p&gt;
  &lt;/div&gt;
&lt;/figure&gt;

&lt;p&gt;Now, &amp;ldquo;reveal $f$ at a point&amp;rdquo; needs to be something a verifier can &lt;em&gt;check&lt;/em&gt; against the public key without holding $f$, and ideally something expressible in the narrow language that our commitment scheme will speak. The bridge is almost embarrassingly simple. Write $f(X)=\sum_{k} c_k X^k$ with coefficient vector $c$. Then evaluating $f$ at a point $x$ is just a dot product of $c$ against the powers of $x$:&lt;/p&gt;
$$ f(x) \;=\; \langle\, c,\; (1, x, x^2, \dots, x^{M-1}) \,\rangle. $$&lt;p&gt;This is the &lt;em&gt;lift&lt;/em&gt;: it turns &amp;ldquo;evaluate the secret polynomial&amp;rdquo; into &amp;ldquo;take a linear form of the secret vector.&amp;rdquo; Drag $x$ below and watch the powers vector fill in and the dot product compute to $f(x)$.&lt;/p&gt;




&lt;figure class=&#34;jvfig&#34; data-fig=&#34;lift-dotprod&#34;&gt;
  &lt;div class=&#34;jvfig-fallback&#34;&gt;
    &lt;span class=&#34;jvfig-fallback-tag&#34;&gt;interactive figure&lt;/span&gt;
    &lt;p&gt;Enable JavaScript to explore this figure.&lt;/p&gt;
  &lt;/div&gt;
&lt;/figure&gt;

&lt;p&gt;Two things fall out of this little identity, and both matter later. First, whatever vector the signer is committed to lives in $M$ coordinates, so $\langle c, (1,x,\dots)\rangle$ is &lt;em&gt;always&lt;/em&gt; a polynomial of degree at most $D=M-1$. There is no room to cheat the degree. Second, the claim &amp;ldquo;this is $f(x)$&amp;rdquo; is purely a statement about $c$, which is what lets us hide everything &lt;em&gt;except&lt;/em&gt; the evaluations we meant to reveal.&lt;/p&gt;
&lt;h2 id=&#34;the-catch-and-the-fix&#34;&gt;The catch, and the fix&lt;/h2&gt;
&lt;p&gt;If signing just meant publishing the pairs $(x_t, f(x_t))$ in the clear, we would be done, and also broken in three different ways.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A dishonest signer could &lt;strong&gt;lie&lt;/strong&gt;, publishing a $y_t$ that is not actually $f(x_t)$, to dodge the consequences of an honest reveal.&lt;/li&gt;
&lt;li&gt;A dishonest signer could &lt;strong&gt;cheat the degree&lt;/strong&gt;: secretly use a higher-degree polynomial so that the &amp;ldquo;$D+1$ points pin it&amp;rdquo; accounting never catches up, and keep signing forever past the budget.&lt;/li&gt;
&lt;li&gt;And even an honest reveal might &lt;strong&gt;leak more than the $K$ points&lt;/strong&gt; if the verification machinery exposed side information about $f$.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The fix for all three is a &lt;strong&gt;polynomial commitment&lt;/strong&gt;. At key generation the signer commits to the coefficient vector $c$, producing a short public digest, a 32-byte &lt;code&gt;root&lt;/code&gt;. From then on, the signer can &lt;em&gt;open&lt;/em&gt; evaluations: prove &amp;ldquo;the polynomial behind &lt;code&gt;root&lt;/code&gt; takes value $y$ at point $x$&amp;rdquo; without revealing anything else about it. Jevil uses &lt;strong&gt;zk-WHIR&lt;/strong&gt;, a hash-based commitment I will treat here as a sealed box with three buttons.&lt;/p&gt;




&lt;figure class=&#34;jvfig&#34; data-fig=&#34;commit-box&#34;&gt;
  &lt;div class=&#34;jvfig-fallback&#34;&gt;
    &lt;span class=&#34;jvfig-fallback-tag&#34;&gt;interactive figure&lt;/span&gt;
    &lt;p&gt;Enable JavaScript to explore this figure.&lt;/p&gt;
  &lt;/div&gt;
&lt;/figure&gt;

&lt;p&gt;The box gives us exactly the two guarantees we need. It is &lt;strong&gt;hiding&lt;/strong&gt;: an opening reveals the asked-for evaluation and nothing more. Formally, it is zero-knowledge, so the proof could have been simulated from the evaluation alone. And it is &lt;strong&gt;binding to the degree&lt;/strong&gt;: the thing inside is a vector of $M$ coordinates, so every value it ever opens is consistent with one fixed polynomial of degree at most $D$. The signer cannot lie (an inconsistent opening fails to verify) and cannot smuggle in a higher-degree secret. The &amp;ldquo;lie&amp;rdquo; attack and the &amp;ldquo;leak&amp;rdquo; attack both die here.&lt;/p&gt;
&lt;p&gt;The &amp;ldquo;cheat the degree&amp;rdquo; attack is worth lingering on, because it is what makes the cliff &lt;em&gt;unavoidable&lt;/em&gt; rather than merely &lt;em&gt;default&lt;/em&gt;. You might worry that a malicious signer could pick a weird public key, not the honest output of key generation, engineered so the cliff never fires. It cannot. zk-WHIR&amp;rsquo;s knowledge soundness means that behind &lt;em&gt;any&lt;/em&gt; &lt;code&gt;root&lt;/code&gt; the verifier ever accepts, there provably exists a degree-$\le D$ polynomial that every accepted opening agrees with. The degree cap is a property of the public key itself, not of honest behavior. So the cliff binds even an adversary who chose their key in bad faith.&lt;/p&gt;




&lt;figure class=&#34;jvfig&#34; data-fig=&#34;cap-binding&#34;&gt;
  &lt;div class=&#34;jvfig-fallback&#34;&gt;
    &lt;span class=&#34;jvfig-fallback-tag&#34;&gt;interactive figure&lt;/span&gt;
    &lt;p&gt;Enable JavaScript to explore this figure.&lt;/p&gt;
  &lt;/div&gt;
&lt;/figure&gt;

&lt;p&gt;Why zk-WHIR specifically? Because it is the commitment that satisfies all of Jevil&amp;rsquo;s constraints at once. It is &lt;strong&gt;post-quantum&lt;/strong&gt; (its security rests on hash functions and Reed–Solomon proximity, not on discrete logs, so Shor does not touch it), it is &lt;strong&gt;transparent&lt;/strong&gt; (no trusted setup, so nobody has to generate and then destroy a secret), it supports the &lt;strong&gt;linear-form openings&lt;/strong&gt; our lift produces, and it is &lt;strong&gt;zero-knowledge&lt;/strong&gt;. It sits at the end of a lineage of Reed–Solomon proximity tests (FRI, then DEEP-FRI, then STIR, then WHIR) with the zero-knowledge layer added on top. For this post it stays a black box; if you want the inside of the box, the &lt;a href=&#34;https://eprint.iacr.org/2026/1103&#34;&gt;paper&lt;/a&gt; and the zk-WHIR paper it builds on are the places to go.&lt;/p&gt;
&lt;h2 id=&#34;one-proof-not-seventeen&#34;&gt;One proof, not seventeen&lt;/h2&gt;
&lt;p&gt;Each signature reveals $K=16$ evaluations, plus we want to fold in that free out-of-domain point, so naively that is $K+1 = 17$ openings per signature, each with its own proof. That would be bulky. Instead Jevil batches them into a &lt;em&gt;single&lt;/em&gt; opening.&lt;/p&gt;
&lt;p&gt;The trick is a random linear combination. Squeeze $K+1$ random coefficients $\beta_t$ out of the transcript (Fiat–Shamir: hash everything so far), and combine the $K+1$ separate claims &amp;ldquo;$f(x_t)=y_t$&amp;rdquo; into one claim about a single linear form. Because the lift already turned each evaluation into a linear form of $c$, a combination of them is again a linear form of $c$, so the whole bundle becomes &lt;em&gt;one&lt;/em&gt; zk-WHIR opening.&lt;/p&gt;




&lt;figure class=&#34;jvfig&#34; data-fig=&#34;batching&#34;&gt;
  &lt;div class=&#34;jvfig-fallback&#34;&gt;
    &lt;span class=&#34;jvfig-fallback-tag&#34;&gt;interactive figure&lt;/span&gt;
    &lt;p&gt;Enable JavaScript to explore this figure.&lt;/p&gt;
  &lt;/div&gt;
&lt;/figure&gt;

&lt;p&gt;What stops a signer from sneaking one false value into the bundle, hoping it averages out? Schwartz–Zippel. If even a single one of the $K+1$ claimed values is wrong, the combined claim is wrong for all but a $1/|\mathbb{F}|$ fraction of the random $\beta$&amp;rsquo;s, and $|\mathbb{F}|\approx 2^{256}$, so the signer is caught with overwhelming probability. Lie once, get caught. One proof now stands in for seventeen, at no security cost.&lt;/p&gt;
&lt;h2 id=&#34;why-you-cant-forge-below-the-cliff&#34;&gt;Why you can&amp;rsquo;t forge below the cliff&lt;/h2&gt;
&lt;p&gt;The cliff is the dramatic part, but a few-time signature is worthless if the signatures &lt;em&gt;before&lt;/em&gt; the cliff are forgeable. They are not. Below the budget, Jevil is unforgeable at roughly $124$-bit classical security against an adversary making up to $2^{128}$ hash queries.&lt;/p&gt;
&lt;p&gt;A forger trying to sign a fresh message has only three moves, and each is a wall. They can lie about an evaluation, but we just closed that door with the commitment and Schwartz–Zippel. They can try to &lt;em&gt;guess&lt;/em&gt; the value of $f$ at a position nobody has seen, but below the cliff $f$ is information-theoretically undetermined, so any unseen evaluation is uniform over a $2^{256}$-element field, and guessing it is hopeless. Or they can hope that the fresh message&amp;rsquo;s positions happen to land &lt;em&gt;entirely&lt;/em&gt; inside the set of positions already revealed by past signatures, the only case where they could quote honest values without ever signing.&lt;/p&gt;
&lt;p&gt;That last move is the classic HORS &lt;em&gt;coverage&lt;/em&gt; attack, and it is the one the parameters are tuned against. The &amp;ldquo;seen pool&amp;rdquo; grows by $K$ positions per signature; a fresh message&amp;rsquo;s $K$ positions are an independent random subset of the whole space $\{0,\dots,T-1\}$; the forger wins only if all $K$ land in the pool, which happens with probability at most $\left(\tfrac{n^\star K}{T}\right)^{K}$ per attempt. Make the position space $T$ big enough and that probability stays negligible even after grinding $2^{128}$ messages. Throw some forgeries below and watch the odds.&lt;/p&gt;




&lt;figure class=&#34;jvfig&#34; data-fig=&#34;coverage&#34;&gt;
  &lt;div class=&#34;jvfig-fallback&#34;&gt;
    &lt;span class=&#34;jvfig-fallback-tag&#34;&gt;interactive figure&lt;/span&gt;
    &lt;p&gt;Enable JavaScript to explore this figure.&lt;/p&gt;
  &lt;/div&gt;
&lt;/figure&gt;

&lt;p&gt;Add the three bounds together (the commitment&amp;rsquo;s soundness, the Schwartz–Zippel term, and the coverage term) and the total forgery advantage comes in under $2^{-124}$. Signatures below the budget are real signatures.&lt;/p&gt;
&lt;h2 id=&#34;tuning-it-and-when-to-reach-for-it&#34;&gt;Tuning it, and when to reach for it&lt;/h2&gt;
&lt;p&gt;Everything is driven by one knob: the budget $n^\star$, chosen from $\{1, 3, 7, 15, \dots, 2^{14}-1\}$, the values where $n^\star+1$ is a power of two, so the cliff lands &lt;em&gt;exactly&lt;/em&gt; at signature $n^\star+1$. From $n^\star$ and the fixed $K=16$, the rest falls out: the polynomial has $M=(n^\star{+}1)K$ coefficients, degree $D=M-1$, and the cliff sits at $n^\star+1$. Slide the budget below and watch the sizes move.&lt;/p&gt;




&lt;figure class=&#34;jvfig&#34; data-fig=&#34;params&#34;&gt;
  &lt;div class=&#34;jvfig-fallback&#34;&gt;
    &lt;span class=&#34;jvfig-fallback-tag&#34;&gt;interactive figure&lt;/span&gt;
    &lt;p&gt;Enable JavaScript to explore this figure.&lt;/p&gt;
  &lt;/div&gt;
&lt;/figure&gt;

&lt;p&gt;The numbers are friendly. Public keys are &lt;strong&gt;68 bytes&lt;/strong&gt; flat: a 32-byte commitment root, the 32-byte out-of-domain value, and 4 bytes naming the budget. Secret keys are &lt;strong&gt;32 bytes&lt;/strong&gt;, a single seed everything else is derived from. Signatures run from about &lt;strong&gt;40 KB&lt;/strong&gt; at the smallest budget to a few hundred KB at the large end, growing only logarithmically in the polynomial size, so a thousand-fold increase in budget costs only a handful of times more bytes. Key generation, signing, and verification are all comfortably sub-second across the deployable range. The ceiling on the budget, $n^\star = 2^{14}-1$, comes from an arithmetic property of the working field: a degree-4 extension of the 64-bit Goldilocks prime, with about $2^{256}$ elements and just enough power-of-two structure to index the positions.&lt;/p&gt;
&lt;p&gt;So when is the cliff the right tool? When over-signing should be &lt;em&gt;self-punishing&lt;/em&gt; rather than merely &lt;em&gt;forbidden&lt;/em&gt;. A stateful scheme says &amp;ldquo;don&amp;rsquo;t sign past the budget&amp;rdquo; and trusts your storage to enforce it; lose that bet to a rolled-back counter and you are silently insecure. Jevil moves the cap into the math: oversign and the polynomial becomes public, in $O(D^2)$, for everyone, immediately. There is no silent failure mode, because the failure is the loudest event the scheme can produce. That is a strictly stronger promise than discipline, and it does not depend on durable state, on operator vigilance, or on anyone trusting anything operational.&lt;/p&gt;
&lt;h2 id=&#34;the-paper-and-the-code&#34;&gt;The paper and the code&lt;/h2&gt;
&lt;p&gt;If you want the formal version, with the security proofs, the exact parameters, and the cap-binding theorem stated properly, it is all in the paper. The reference implementation in Rust tracks the construction module for module.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Paper:&lt;/strong&gt; &lt;a href=&#34;https://eprint.iacr.org/2026/1103&#34;&gt;eprint 2026/1103&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reference implementation:&lt;/strong&gt; &lt;a href=&#34;https://github.com/symbolicsoft/jevil&#34;&gt;github.com/symbolicsoft/jevil&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded>
      <category>Research</category>
      <category>Software</category>
      <category>Post-Quantum</category>
      <category>Signatures</category>
      <category>Few-Time Signatures</category>
      <category>Polynomial Commitments</category>
      
    </item>
    
    <item>
      <title>On Our Telegram MTProto Review</title>
      <link>https://symbolic.software/blog/2026-05-21-telegram-mtproto-review/</link>
      <pubDate>Thu, 21 May 2026 00:00:00 &#43;0000</pubDate>
      <guid>https://symbolic.software/blog/2026-05-21-telegram-mtproto-review/</guid>
      <description>A note on Symbolic Software&#39;s technical review of Telegram&#39;s MTProto protocol, made public in 2026 through litigation. Covers the auth_key_id tracking vulnerability, the empirical case against Telegram&#39;s &#39;changes regularly&#39; rebuttal, the editorial independence terms of the engagement, and how the document entered the public record.</description>
      <content:encoded>&lt;p&gt;On May 18, 2026, &lt;a href=&#34;https://istories.media/en/stories/2026/05/18/independent-review-confirms-critical-telegram-vulnerability/&#34;&gt;iStories&lt;/a&gt;, part of the OCCRP investigative network, published a piece describing a Symbolic Software technical review of Telegram&amp;rsquo;s MTProto protocol. The review, titled &lt;em&gt;Telegram&amp;rsquo;s MTProto: Assessing Deanonymization Potential for a Network Attacker&lt;/em&gt; and identified as &lt;strong&gt;GNMX-01&lt;/strong&gt;, was commissioned in 2025 by &lt;strong&gt;Global Network Solutions, Inc. (&amp;ldquo;GNM&amp;rdquo;)&lt;/strong&gt;, the Telegram infrastructure provider that figures centrally in OCCRP&amp;rsquo;s &lt;a href=&#34;https://www.occrp.org/en/investigation/telegram-the-fsb-and-the-man-in-the-middle&#34;&gt;2025 investigation&lt;/a&gt; &lt;em&gt;Telegram, the FSB, and the Man in the Middle&lt;/em&gt;. We did not publish the report. It entered the public record because GNM attached it to a civil filing in Switzerland, and iStories obtained it from the materials of that case.&lt;/p&gt;
&lt;p&gt;We are publishing this post because the report is now being characterised through third-party paraphrase in coverage of a contested matter, and we would prefer the substance to stand without that mediation. The &lt;a href=&#34;https://symbolic.software/pdf/gnmx-01.pdf&#34;&gt;full report&lt;/a&gt; is available for download.&lt;/p&gt;
&lt;h2 id=&#34;the-conditions-of-the-engagement&#34;&gt;The conditions of the engagement&lt;/h2&gt;
&lt;p&gt;The report could not have been written without the following conditions, which are reproduced verbatim in Section 2.4:&lt;/p&gt;
&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Symbolic Software retains full editorial control over all findings, conclusions, and recommendations presented in this report;&lt;/li&gt;
&lt;li&gt;Symbolic Software has the unconditional right to reach any conclusion deemed technically appropriate based on the evidence, regardless of whether such conclusions align with the expectations or preferences of any party;&lt;/li&gt;
&lt;li&gt;No party may modify, censor, or otherwise interfere with the technical content or conclusions of this report;&lt;/li&gt;
&lt;li&gt;Symbolic Software maintains sole authority over the methodology, scope, and presentation of the technical analysis;&lt;/li&gt;
&lt;li&gt;The integrity of this report and its findings shall be preserved in any publication or distribution.&lt;/li&gt;
&lt;/ul&gt;&lt;/blockquote&gt;
&lt;p&gt;These were non-negotiable prerequisites of the engagement, and the commissioner agreed to them. Their relevance to the present situation is clear. The final clause in particular continues to bind the commissioner, even though the route by which the report became public was the commissioner&amp;rsquo;s own decision.&lt;/p&gt;
&lt;p&gt;A second methodological note deserves quoting alongside that disclaimer. The OCCRP investigation&amp;rsquo;s principal technical source is Michał &amp;ldquo;rysiek&amp;rdquo; Woźniak, who formerly served as OCCRP&amp;rsquo;s Head of Infrastructure and Information Security. OCCRP &lt;a href=&#34;https://www.occrp.org/en/investigation/telegram-the-fsb-and-the-man-in-the-middle&#34;&gt;disclosed this conflict of interest&lt;/a&gt; in the original reporting. The report addresses it directly in a footnote:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Mr. Woźniak&amp;rsquo;s technical claims were independently verified to be correct regardless of this potential conflict of interest. The political claims and personal opinions made in Mr. Woźniak&amp;rsquo;s technical analysis were disregarded during our independent verification.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;The same standard runs through the report in both directions. Testable technical claims are evaluated against the evidence. Characterisations that blend technical fact with geopolitical framing are noted as falling outside the scope of technical validation, regardless of which party offers them.&lt;/p&gt;
&lt;h2 id=&#34;the-core-technical-finding&#34;&gt;The core technical finding&lt;/h2&gt;
&lt;p&gt;Telegram&amp;rsquo;s MTProto protocol prepends a 64-bit &lt;code&gt;auth_key_id&lt;/code&gt; to the external header of every encrypted message. The identifier is the 64 lower-order bits of a SHA-1 hash of the 2048-bit authorisation key established at registration. MTProto 2.0 continues to use SHA-1 here even though it migrated to SHA-256 elsewhere, because the identifier must outlive any single protocol version. The underlying authorisation key is long-lived by design: it is generated on the client device, never transmitted over the network, and tied to the device&amp;rsquo;s persistent state.&lt;/p&gt;
&lt;p&gt;The question the report examined was whether this identifier is exposed to passive network observers in deployed clients. On every platform tested, it is.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Telegram for Android.&lt;/strong&gt; We replicated &lt;a href=&#34;https://rys.io/en/179.html&#34;&gt;Michał Woźniak&amp;rsquo;s prior analysis&lt;/a&gt; of Telegram for Android v11.9.2 (5901). MTProto runs over plain TCP under a trivial obfuscation layer. Telegram&amp;rsquo;s own documentation describes that layer as designed only to defeat naïve protocol detection, not to provide cryptographic protection. The obfuscation is an XOR with a pseudo-random stream derived from a public, documented initialisation procedure, so deobfuscation is computationally trivial. After deobfuscation, the &lt;code&gt;auth_key_id&lt;/code&gt; appears in cleartext in the external header of every MTProto message.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Telegram Desktop on macOS&lt;/strong&gt; (version 11.15.275369 at the time of testing). The desktop client connects to Telegram&amp;rsquo;s servers on port 443, the canonical HTTPS port, but the traffic is not HTTPS. We verified this with four independent techniques:
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;TLS fingerprinting&lt;/strong&gt; of the initial packets of each connection. No TLS ClientHello was ever observed, and the connections began immediately with MTProto exchanges.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Certificate validation.&lt;/strong&gt; No TLS certificates were exchanged at any point.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Packet structure analysis.&lt;/strong&gt; The MTProto external header, including the 64-bit &lt;code&gt;auth_key_id&lt;/code&gt; and the 128-bit message key, begins immediately after the TCP header, with no intervening TLS record layer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Selective traffic blocking.&lt;/strong&gt; We configured local firewall rules to block outbound HTTPS to Telegram&amp;rsquo;s IP ranges while permitting plain TCP on port 443. Telegram continued to function normally, confirming that it does not depend on TLS at all.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The use of port 443 appears designed to evade simple firewall rules and censorship classifiers that block non-standard ports while allowing HTTPS. It does not deliver transport-layer encryption. The Telegram Desktop codebase is &lt;a href=&#34;https://github.com/telegramdesktop/tdesktop&#34;&gt;shared with the Windows and Linux builds&lt;/a&gt;, so the finding almost certainly generalises.&lt;/p&gt;
&lt;p&gt;To characterise persistence, we observed the &lt;code&gt;auth_key_id&lt;/code&gt; across a range of conditions that would normally defeat or interrupt other forms of network-layer tracking. The result was uniform across all of them.&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Condition tested&lt;/th&gt;
          &lt;th&gt;What this would normally defeat&lt;/th&gt;
          &lt;th&gt;Effect on &lt;code&gt;auth_key_id&lt;/code&gt;&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Application restart&lt;/td&gt;
          &lt;td&gt;Session-scoped identifiers, in-memory state&lt;/td&gt;
          &lt;td&gt;Unchanged&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;IP address change (DHCP renewal)&lt;/td&gt;
          &lt;td&gt;IP-based tracking&lt;/td&gt;
          &lt;td&gt;Unchanged&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Switching between WiFi networks&lt;/td&gt;
          &lt;td&gt;Network-scoped fingerprints, IP attribution&lt;/td&gt;
          &lt;td&gt;Unchanged&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Switching between WiFi and cellular&lt;/td&gt;
          &lt;td&gt;IP attribution and per-medium fingerprints&lt;/td&gt;
          &lt;td&gt;Unchanged&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;VPN activation&lt;/td&gt;
          &lt;td&gt;IP-based tracking, geolocation, network attribution&lt;/td&gt;
          &lt;td&gt;Unchanged&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Routing through Tor&lt;/td&gt;
          &lt;td&gt;Origin IP attribution, network-path observation&lt;/td&gt;
          &lt;td&gt;Unchanged&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Connecting to a different Telegram server IP within the same datacenter&lt;/td&gt;
          &lt;td&gt;Server-endpoint-pinned identifiers&lt;/td&gt;
          &lt;td&gt;Unchanged&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Continuous observation over days and weeks&lt;/td&gt;
          &lt;td&gt;Short-lived or session-rotated identifiers&lt;/td&gt;
          &lt;td&gt;Unchanged&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;With Perfect Forward Secrecy enabled, the visible identifier corresponds to a temporary key rather than the permanent one. The report walks through in some detail why this does not address the tracking problem. The temporary keys remain valid and unchanged for their full lifetime, which is typically 24 hours, and during that lifetime the temporary &lt;code&gt;auth_key_id&lt;/code&gt; is just as observable and just as persistent as a permanent one would be. The binding step that ties a new temporary key to the permanent key is itself a network-observable event from the same client, typically from the same IP address. The old temporary &lt;code&gt;auth_key_id&lt;/code&gt; is used to negotiate and authorise the new one, and both appear in succession in the traffic. A passive observer maintaining a record of these binding events can follow a device across arbitrary numbers of rotations. In the report&amp;rsquo;s words, PFS &amp;ldquo;is designed to protect against &lt;em&gt;post factum&lt;/em&gt; decryption of message contents if keys are later compromised — it is not designed to prevent real-time traffic analysis or device tracking.&amp;rdquo; It does what it was specified to do. Transport-layer tracking is a separate problem.&lt;/p&gt;
&lt;p&gt;The capability this creates is straightforward, and it does not require an active attacker. The report enumerates the in-scope adversary classes: ISPs along the network path, mobile carriers&amp;rsquo; deep packet inspection systems, corporate and institutional network administrators, public WiFi operators, parties operating at internet exchange points or in transit, malicious hotspot operators, state surveillance programs, and any passive eavesdropper with physical or wireless access to the medium. None of these requires a man-in-the-middle attack, a certificate compromise, or active protocol manipulation. Simple packet capture and the publicly documented deobfuscation step suffice.&lt;/p&gt;
&lt;h2 id=&#34;on-auth_key_id-changes-regularly&#34;&gt;On &amp;ldquo;auth_key_id changes regularly&amp;rdquo;&lt;/h2&gt;
&lt;p&gt;A &lt;a href=&#34;https://istories.media/en/stories/2026/05/18/independent-review-confirms-critical-telegram-vulnerability/&#34;&gt;statement attributed to Telegram&lt;/a&gt; in iStories&amp;rsquo; coverage holds that the &lt;code&gt;auth_key_id&lt;/code&gt; &amp;ldquo;changes regularly and does not reveal user information.&amp;rdquo; The empirical part of that claim is testable, and the report tests it. From the executive summary:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Our empirical testing demonstrated that &lt;code&gt;auth_key_id&lt;/code&gt; rotation, while theoretically possible, occurs infrequently in practice — we observed no rotation across application restarts, network changes, or extended time periods in our tests. This persistence contradicts claims of frequent rotation and significantly amplifies the privacy implications of the exposure.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;For rotation to defeat the tracking model, &amp;ldquo;regularly&amp;rdquo; would have to mean two things at once: rotation at a cadence shorter than an adversary&amp;rsquo;s correlation window, and rotation that is &lt;em&gt;not itself observable&lt;/em&gt; from the network. The deployed protocol satisfies neither condition.&lt;/p&gt;
&lt;p&gt;The long-term &lt;code&gt;auth_key_id&lt;/code&gt; did not rotate at all across the conditions enumerated in the previous section. When PFS was enabled, the temporary &lt;code&gt;auth_key_id&lt;/code&gt; persisted for its full 24-hour validity period, and the transition from one temporary key to the next was trivially linkable. The binding event between successive temporary keys appears in the same flow, from the same client, and typically from the same IP address. The report quotes Woźniak&amp;rsquo;s prior analysis on this point: &amp;ldquo;It is extremely unlikely for the IP address of the client and the temporary &lt;code&gt;auth_key_id&lt;/code&gt; to both change at the exact same time.&amp;rdquo; The transition is therefore not a privacy boundary. It is a network-visible event that links the old identifier to the new.&lt;/p&gt;
&lt;p&gt;If Telegram has empirical evidence to the contrary, that evidence has not been made public, and our testing did not observe its effects.&lt;/p&gt;
&lt;h2 id=&#34;on-what-the-report-concludes-about-each-claim&#34;&gt;On what the report concludes about each claim&lt;/h2&gt;
&lt;p&gt;The report evaluates five claims drawn from OCCRP&amp;rsquo;s investigation, and it does not endorse them in identical terms.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Claims 1, 3, and 5: fully supported.&lt;/strong&gt; The first claim is that the &lt;code&gt;auth_key_id&lt;/code&gt; enables user tracking. The third is that Secret Chats&amp;rsquo; end-to-end encryption does not prevent that tracking, because the identifier sits in the MTProto external header &lt;em&gt;beneath&lt;/em&gt; the Secret Chat encryption layer. The fifth is that infrastructure with router-level visibility can read those identifiers. The report describes each of these as technically accurate without overstatement, and the empirical evidence in our analysis substantiates them in full.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Claim 2: substantially accurate but with elements of overstatement, and in places actively misleading.&lt;/strong&gt; Phrases like &lt;em&gt;&amp;ldquo;all possible information&amp;rdquo;&lt;/em&gt; overstate what passive network observation strictly provides. The assertion that observers can determine &lt;em&gt;&amp;ldquo;who exchanges data packets with whom&amp;rdquo;&lt;/em&gt; overstates the visibility that client-to-server observation provides in a centralised architecture. Network observers see traffic flowing to and from Telegram&amp;rsquo;s servers, but they cannot directly reconstruct the server-side communication graph from that traffic alone. Beyond these overstatements, there is a more substantive problem. The metadata surveillance capabilities described in Claim 2 are not unique to Telegram. Signal, WhatsApp, iMessage, and every other major messaging platform expose comparable IP-layer metadata to network observers. Singling out Telegram on this point, without acknowledging that Signal and WhatsApp face identical metadata exposure, misrepresents the scope of the issue. The MTProto-specific problem is the &lt;code&gt;auth_key_id&lt;/code&gt; exposure of Claim 1. The broader metadata picture of Claim 2 is a property of unencrypted internet traffic at large.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Claim 4: technically sound core, geopolitical attribution speculative.&lt;/strong&gt; Infrastructure-level access combined with &lt;code&gt;auth_key_id&lt;/code&gt; visibility can enable extensive device tracking. That much is sound. The problem is the inferential chain OCCRP uses to attribute that capability to Russian intelligence services specifically. Applied symmetrically, the same chain would implicate Western messaging platforms and their infrastructure providers just as readily. Signal runs on AWS, which holds &lt;a href=&#34;https://www.theatlantic.com/technology/archive/2014/07/the-details-about-the-cias-deal-with-amazon/374632/&#34;&gt;a $600M CIA contract&lt;/a&gt; and subsequent NSA contracts. WhatsApp is owned by Meta, which discloses &lt;a href=&#34;https://transparency.meta.com/integrity-reports-q1-2025/&#34;&gt;data in response to thousands of government requests annually&lt;/a&gt;. iMessage runs on Apple&amp;rsquo;s contracted data centres. Microsoft Teams runs on Azure. The report puts the asymmetry plainly:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The OCCRP investigation appears to apply a standard of suspicion to Russian-connected infrastructure that is not symmetrically applied to U.S.- or European-connected infrastructure, despite comparable (and often more direct) relationships between Western technology providers and Western intelligence services. This asymmetry suggests a politically motivated framing rather than a neutral security analysis.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;The technical vulnerability is real. The specific adversary attribution is investigative work, not technical analysis.&lt;/p&gt;
&lt;p&gt;None of these qualifications reduces the technical vulnerability. They sharpen the claim being made about it, and we hope they are preserved in future coverage.&lt;/p&gt;
&lt;h2 id=&#34;what-the-tracking-looks-like-in-practice&#34;&gt;What the tracking looks like in practice&lt;/h2&gt;
&lt;p&gt;Chapter 5 of the report walks through a hypothetical but technically faithful deanonymisation timeline for a journalist using Telegram to communicate with sources. The point of the case study is that the tracking does not require future technology or nation-state infrastructure. It uses standard ISP logging already deployed for troubleshooting and billing, mobile carrier deep packet inspection commercially available from vendors like Sandvine and Procera, captive portal authentication logs standard in enterprise and public WiFi, and database queries to correlate identifiers across datasets.&lt;/p&gt;
&lt;p&gt;The chain runs in three steps. The first step is passive observation of an &lt;code&gt;auth_key_id&lt;/code&gt; across several networks. The second step is the establishment of a single identity anchor. In the case study, a press-conference WiFi registration ties the &lt;code&gt;auth_key_id&lt;/code&gt; to a name and outlet. The third step is retroactive search of all logged traffic for that identifier across home ISP, mobile carrier, airport WiFi, and even a VPN session. The VPN successfully masks the IP from Telegram&amp;rsquo;s perspective, but it does not protect the &lt;code&gt;auth_key_id&lt;/code&gt; from observation by the VPN provider or by any infrastructure between the VPN exit and the Telegram server. The case study takes twenty days from initial observation to retroactive analysis, and it uses only logs the providers were already maintaining for unrelated reasons.&lt;/p&gt;
&lt;p&gt;The report is careful to keep this on the right side of the line. Two legal and ethical disclaimers appear in the body of the report (in Chapters 1 and 5), and Appendix A summarises the legal framework governing interception in the EU, US, and Germany. The appendix concludes:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Where the report states that &lt;code&gt;auth_key_id&lt;/code&gt; is technically observable, that should be read as a statement of technical possibility. Any operational collection or retention of such identifiers requires legal authority and appropriate procedural safeguards; otherwise it risks criminal and civil liability.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;The vulnerability described in the report is a latent capability under the protocol design. It is for legal frameworks to constrain how that capability may lawfully be exercised.&lt;/p&gt;
&lt;h2 id=&#34;on-editorial-independence&#34;&gt;On editorial independence&lt;/h2&gt;
&lt;p&gt;Two clarifications are worth offering.&lt;/p&gt;
&lt;p&gt;The first concerns the engagement itself. We accepted it on the conditions quoted above, and we delivered on them. The report concluded against the commissioning party&amp;rsquo;s preferred outcome on the central technical questions. The &lt;code&gt;auth_key_id&lt;/code&gt; exposure is real. The deployed protocol does not rotate at a cadence that defeats tracking. PFS does not address the tracking problem. The desktop client&amp;rsquo;s use of port 443 does not constitute transport encryption. The architectural responsibility for fixing this lies with Telegram, not with its infrastructure providers. This is the firm&amp;rsquo;s standing practice, and the engagement letter was structured the way it was precisely so that the practice could hold under pressure.&lt;/p&gt;
&lt;p&gt;The second concerns the report&amp;rsquo;s qualifications of OCCRP&amp;rsquo;s framing. Those qualifications, on Claim 2 and on Claim 4, are not concessions to the commissioner. They are the same editorial standard applied in the other direction. The methodological footnote on Woźniak, quoted earlier, is the cleanest statement of that standard. Technical claims are evaluated on the evidence. Political characterisations are noted as falling outside the technical scope, regardless of which party offers them. We applied the same rule to OCCRP&amp;rsquo;s framing that we applied to GNM&amp;rsquo;s preferences.&lt;/p&gt;
&lt;h2 id=&#34;on-the-publication-route&#34;&gt;On the publication route&lt;/h2&gt;
&lt;p&gt;The report became public because Global Network Solutions, Inc. attached it to a civil filing in Switzerland, where the matter is currently in litigation. We did not publish it. iStories obtained the document from the materials of that case.&lt;/p&gt;
&lt;p&gt;We were not contacted by iStories prior to their publication. We note this without grievance, but for the record. The standard practice when attributing technical conclusions to a named expert firm is to confirm with that firm both that the document is genuine and that the proposed framing of its conclusions is accurate. The document is genuine. The framing is substantially accurate, but compressed in places where the report itself is more careful, particularly on Claims 2 and 4, where the report&amp;rsquo;s qualifications are the kind of detail a one-paragraph paraphrase tends to elide. We mention this because publications of iStories&amp;rsquo; standing are normally diligent on the verification step. The situation it could have produced, in which a forged or altered document was attributed to us in a politically sensitive context, is the kind of failure that next-time diligence is designed to prevent.&lt;/p&gt;
&lt;h2 id=&#34;what-remains&#34;&gt;What remains&lt;/h2&gt;
&lt;p&gt;The remedy is not novel and does not require theoretical advances. WhatsApp wraps its Noise transport in TLS. Signal uses TLS, and has historically used that TLS layer for &lt;a href=&#34;https://signal.org/blog/looking-back-on-the-front/&#34;&gt;further useful purposes&lt;/a&gt; such as censorship-circumvention domain fronting. iMessage and every other major messenger of comparable scale uses TLS or an equivalent transport-encrypted layer. The cost of TLS termination is well understood, and modern implementations are mature on every platform Telegram supports. The only remaining barrier to closing the &lt;code&gt;auth_key_id&lt;/code&gt; exposure is an organisational decision on Telegram&amp;rsquo;s part to make transport-layer encryption mandatory for MTProto and to remove the unencrypted fallback paths.&lt;/p&gt;
&lt;p&gt;Until that decision is taken, the privacy properties of Telegram against passive network observers are what the report describes. The report&amp;rsquo;s executive summary puts the conclusion more plainly than we will here: the present design represents &lt;em&gt;&amp;ldquo;not merely a technical failing, but a fundamental abdication of Telegram&amp;rsquo;s responsibility to protect user privacy through appropriate cryptographic measures.&amp;rdquo;&lt;/em&gt; Users in high-risk categories, including journalists, activists, dissidents, and anyone whose communications may attract the interest of state or near-state actors, should incorporate that exposure into their threat models. The standard countermeasures, such as VPNs, Tor, regular network switching, and SIM swaps, do not address the underlying vulnerability, because the vulnerability is in the protocol rather than in the user&amp;rsquo;s network.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&#34;https://symbolic.software/pdf/gnmx-01.pdf&#34;&gt;full report&lt;/a&gt; is linked above. It contains the formal evaluation of each OCCRP claim, the illustrative deanonymisation case study, the appendix on legal context, and the engagement-letter clauses quoted in this post.&lt;/p&gt;
</content:encoded>
      <category>Security</category>
      <category>Research</category>
      <category>Telegram</category>
      <category>MTProto</category>
      <category>Transport Security</category>
      
    </item>
    
    <item>
      <title>Applied Cryptography Course: Welcome, Class of Summer 2026!</title>
      <link>https://symbolic.software/blog/2026-05-12-course-summer-2026/</link>
      <pubDate>Tue, 12 May 2026 00:00:00 &#43;0000</pubDate>
      <guid>https://symbolic.software/blog/2026-05-12-course-summer-2026/</guid>
      <description>Fifty students from nine institutions have been selected for the Summer 2026 Applied Cryptography online program, our free intensive course bringing modern cryptography to Lebanese university students. What the course covers, and who is joining us this June.</description>
      <content:encoded>&lt;p&gt;The Summer 2026 cohort for our &lt;a href=&#34;https://symbolic.software/blog/2026-03-23-applied-cryptography-summer/&#34;&gt;Applied Cryptography online program&lt;/a&gt; has been selected, and we are delighted with who is coming. Fifty students from nine institutions across Lebanon will join us online from June 9 through October 10 for a full semester of modern cryptography, taught the way we teach it at AUB.&lt;/p&gt;
&lt;p&gt;This is a full-featured university course delivered completely free to Lebanese students, and reading the applications we could already tell this was going to be a good cohort to teach.&lt;/p&gt;
&lt;div class=&#34;cohort-hero reveal&#34;&gt;
&lt;div class=&#34;cohort-panel-bar&#34;&gt;
&lt;div class=&#34;left&#34;&gt;&lt;div class=&#34;traffic&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;span class=&#34;filename&#34;&gt;cohort · &lt;span class=&#34;stem&#34;&gt;summer 2026&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;span class=&#34;status&#34;&gt;&lt;span class=&#34;pulse&#34;&gt;&lt;/span&gt;50 admitted&lt;/span&gt;
&lt;/div&gt;
&lt;div class=&#34;cohort-hero-grid&#34;&gt;
&lt;div class=&#34;counter&#34;&gt;&lt;div class=&#34;big&#34;&gt;&lt;span data-counter-target=&#34;50&#34; data-counter-from=&#34;0&#34;&gt;50&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;label&#34;&gt;students admitted&lt;/div&gt;&lt;/div&gt;
&lt;div class=&#34;counter&#34;&gt;&lt;div class=&#34;big&#34;&gt;&lt;span data-counter-target=&#34;9&#34; data-counter-from=&#34;0&#34;&gt;9&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;label&#34;&gt;institutions&lt;/div&gt;&lt;/div&gt;
&lt;div class=&#34;counter&#34;&gt;&lt;div class=&#34;big&#34;&gt;&lt;span data-counter-target=&#34;18&#34; data-counter-from=&#34;0&#34;&gt;18&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;label&#34;&gt;course topics&lt;/div&gt;&lt;/div&gt;
&lt;div class=&#34;counter&#34;&gt;&lt;div class=&#34;big&#34;&gt;&lt;span data-counter-target=&#34;8&#34; data-counter-from=&#34;0&#34;&gt;8&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;label&#34;&gt;hands-on projects&lt;/div&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&#34;what-the-course-covers&#34;&gt;What the course covers&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://appliedcryptography.page&#34;&gt;Applied Cryptography&lt;/a&gt; is a two-part course. Part 1 builds the theoretical foundations of modern cryptography from first principles: pseudorandomness, chosen-plaintext and chosen-ciphertext security, collision-resistant hash functions, hard problems and Diffie-Hellman, elliptic curves, and digital signatures, all developed inside a rigorous provable-security framework. Part 2 then applies that foundation to the systems that actually run the modern internet: TLS, the cautionary tale of RC4, secure messaging protocols like Signal, end-to-end encrypted cloud storage, high-assurance cryptographic implementations, post-quantum cryptography, cryptocurrency cryptography, zero-knowledge proofs, secure multiparty computation, and fully homomorphic encryption. Eighteen topics in total, paced over a full semester.&lt;/p&gt;
&lt;p&gt;Around the lectures sits a serious problem-set program (eight problem sets, paced with the topics) and a hands-on project track. Students choose one or two projects from a menu of eight: build a password manager, design a secure messenger, formally verify a TLS-like protocol in ProVerif, implement a zero-knowledge Battleship game, migrate a system to post-quantum cryptography, build private set intersection for contact discovery, design a privacy-preserving age verification system, or construct a time-locked message capsule. The implementation languages are Go and Rust, and the course ships &lt;a href=&#34;https://appliedcryptography.page#resources&#34;&gt;Go and Rust starter kits&lt;/a&gt; so students can get writing code from day one.&lt;/p&gt;
&lt;p&gt;There is also a four-week rotating schedule running alongside the technical content. Week 1 is a paper reading, where the cohort tears apart a seminal or recent cryptography paper together. Week 2 is student presentations, with 15-minute talks and lightning talks. Week 3 is a writing workshop on technical and academic writing, including LaTeX, audit reports, and security disclosures. Week 4 is a career café with guest speakers from industry and academia. The goal is to send students out of the semester with cryptographic knowledge, plus the surrounding skills the field actually needs.&lt;/p&gt;
&lt;p&gt;The full setup, including the two required textbooks (Mike Rosulek&amp;rsquo;s &lt;em&gt;The Joy of Cryptography&lt;/em&gt; and Jean-Philippe Aumasson&amp;rsquo;s &lt;em&gt;Serious Cryptography, 2nd Edition&lt;/em&gt;), the extensive reading list of around ninety research papers and technical resources, problem-set specifications, and project briefs, is all available on the &lt;a href=&#34;https://appliedcryptography.page&#34;&gt;course website&lt;/a&gt; under a Creative Commons license. Everyone is welcome to use it, cohort or not.&lt;/p&gt;
&lt;h2 id=&#34;what-an-opportunity-this-is&#34;&gt;What an opportunity this is&lt;/h2&gt;
&lt;p&gt;Lebanon has a remarkable density of talented computer-science students. Dedicated cryptography coursework is not always easy to come by, and the gap between a standard undergraduate curriculum and what is needed to do serious work in the field can be a wide one to cross alone. Applied Cryptography 2026 is built to help close that gap, no matter which Lebanese university you happen to be enrolled at, and at no cost.&lt;/p&gt;
&lt;h2 id=&#34;a-few-fun-snapshots-of-the-cohort&#34;&gt;A few fun snapshots of the cohort&lt;/h2&gt;
&lt;p&gt;We thought it would be fun to share a quick demographic look at the group of fifty before the course kicks off. Nothing scientific, just some incidental snapshots.&lt;/p&gt;
&lt;h3 id=&#34;where-they-study&#34;&gt;Where they study&lt;/h3&gt;
&lt;div class=&#34;cohort-block reveal&#34;&gt;
&lt;div class=&#34;cohort-panel-bar&#34;&gt;
&lt;div class=&#34;left&#34;&gt;&lt;div class=&#34;traffic&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;span class=&#34;filename&#34;&gt;distribution · &lt;span class=&#34;stem&#34;&gt;institution&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;span class=&#34;status&#34;&gt;&lt;span class=&#34;pulse&#34;&gt;&lt;/span&gt;n=50&lt;/span&gt;
&lt;/div&gt;
&lt;div class=&#34;cohort-donut-row&#34;&gt;
&lt;svg class=&#34;cohort-donut&#34; viewBox=&#34;0 0 120 120&#34; role=&#34;img&#34; aria-label=&#34;Cohort by university: Lebanese University 23, AUB 17, Non-Lebanese 5, USJ 2, LIU 1, IUL 1, Balamand 1&#34;&gt;
&lt;circle class=&#34;donut-track&#34; cx=&#34;60&#34; cy=&#34;60&#34; r=&#34;50&#34;/&gt;
&lt;circle class=&#34;donut-seg&#34; cx=&#34;60&#34; cy=&#34;60&#34; r=&#34;50&#34; style=&#34;--rot:0;--len:144.5;stroke:var(--cohort-lu)&#34;/&gt;
&lt;circle class=&#34;donut-seg&#34; cx=&#34;60&#34; cy=&#34;60&#34; r=&#34;50&#34; style=&#34;--rot:165.6;--len:106.8;stroke:var(--cohort-aub)&#34;/&gt;
&lt;circle class=&#34;donut-seg&#34; cx=&#34;60&#34; cy=&#34;60&#34; r=&#34;50&#34; style=&#34;--rot:288.0;--len:31.4;stroke:var(--cohort-ext)&#34;/&gt;
&lt;circle class=&#34;donut-seg&#34; cx=&#34;60&#34; cy=&#34;60&#34; r=&#34;50&#34; style=&#34;--rot:324.0;--len:12.6;stroke:var(--cohort-usj)&#34;/&gt;
&lt;circle class=&#34;donut-seg&#34; cx=&#34;60&#34; cy=&#34;60&#34; r=&#34;50&#34; style=&#34;--rot:338.4;--len:6.3;stroke:var(--cohort-liu)&#34;/&gt;
&lt;circle class=&#34;donut-seg&#34; cx=&#34;60&#34; cy=&#34;60&#34; r=&#34;50&#34; style=&#34;--rot:345.6;--len:6.3;stroke:var(--cohort-iul)&#34;/&gt;
&lt;circle class=&#34;donut-seg&#34; cx=&#34;60&#34; cy=&#34;60&#34; r=&#34;50&#34; style=&#34;--rot:352.8;--len:6.3;stroke:var(--cohort-bal)&#34;/&gt;
&lt;text class=&#34;donut-mid&#34; x=&#34;60&#34; y=&#34;58&#34; text-anchor=&#34;middle&#34;&gt;50&lt;/text&gt;
&lt;text class=&#34;donut-sub&#34; x=&#34;60&#34; y=&#34;71&#34; text-anchor=&#34;middle&#34;&gt;students&lt;/text&gt;
&lt;/svg&gt;
&lt;ul class=&#34;cohort-legend&#34;&gt;
&lt;li&gt;&lt;span class=&#34;sw&#34; style=&#34;background:var(--cohort-lu)&#34;&gt;&lt;/span&gt;&lt;span class=&#34;name&#34;&gt;Lebanese University&lt;/span&gt;&lt;span class=&#34;val&#34;&gt;23 · 46%&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span class=&#34;sw&#34; style=&#34;background:var(--cohort-aub)&#34;&gt;&lt;/span&gt;&lt;span class=&#34;name&#34;&gt;American University of Beirut&lt;/span&gt;&lt;span class=&#34;val&#34;&gt;17 · 34%&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span class=&#34;sw&#34; style=&#34;background:var(--cohort-ext)&#34;&gt;&lt;/span&gt;&lt;span class=&#34;name&#34;&gt;Non-Lebanese&lt;/span&gt;&lt;span class=&#34;val&#34;&gt;5 · 10%&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span class=&#34;sw&#34; style=&#34;background:var(--cohort-usj)&#34;&gt;&lt;/span&gt;&lt;span class=&#34;name&#34;&gt;Saint-Joseph University&lt;/span&gt;&lt;span class=&#34;val&#34;&gt;2 · 4%&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span class=&#34;sw&#34; style=&#34;background:var(--cohort-liu)&#34;&gt;&lt;/span&gt;&lt;span class=&#34;name&#34;&gt;Lebanese International U.&lt;/span&gt;&lt;span class=&#34;val&#34;&gt;1 · 2%&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span class=&#34;sw&#34; style=&#34;background:var(--cohort-iul)&#34;&gt;&lt;/span&gt;&lt;span class=&#34;name&#34;&gt;Islamic University of Lebanon&lt;/span&gt;&lt;span class=&#34;val&#34;&gt;1 · 2%&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span class=&#34;sw&#34; style=&#34;background:var(--cohort-bal)&#34;&gt;&lt;/span&gt;&lt;span class=&#34;name&#34;&gt;University of Balamand&lt;/span&gt;&lt;span class=&#34;val&#34;&gt;1 · 2%&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;A nice spread across Lebanese universities, with the Lebanese University and AUB contributing the most students. The five non-Lebanese admits joined us through instructor discretion as exceptional cases, and most of them are from the Middle East and North Africa region: a Tunisian graduate student at the EPFL/ETH joint program in Zurich, a Syrian formal-verification specialist, and a Lebanese national pursuing a PhD in Luxembourg. The other two come from further afield: a graduate student from Latin America and a career applied-cryptography engineer based in the UK.&lt;/p&gt;
&lt;h3 id=&#34;gender&#34;&gt;Gender&lt;/h3&gt;
&lt;div class=&#34;cohort-block reveal&#34;&gt;
&lt;div class=&#34;cohort-panel-bar&#34;&gt;
&lt;div class=&#34;left&#34;&gt;&lt;div class=&#34;traffic&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;span class=&#34;filename&#34;&gt;composition · &lt;span class=&#34;stem&#34;&gt;gender&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;span class=&#34;status&#34;&gt;&lt;span class=&#34;pulse&#34;&gt;&lt;/span&gt;n=50&lt;/span&gt;
&lt;/div&gt;
&lt;div class=&#34;cohort-donut-row&#34;&gt;
&lt;svg class=&#34;cohort-donut&#34; viewBox=&#34;0 0 120 120&#34; role=&#34;img&#34; aria-label=&#34;Gender: Male 34, Female 16&#34;&gt;
&lt;circle class=&#34;donut-track&#34; cx=&#34;60&#34; cy=&#34;60&#34; r=&#34;50&#34;/&gt;
&lt;circle class=&#34;donut-seg&#34; cx=&#34;60&#34; cy=&#34;60&#34; r=&#34;50&#34; style=&#34;--rot:0;--len:213.6;stroke:var(--cohort-male)&#34;/&gt;
&lt;circle class=&#34;donut-seg&#34; cx=&#34;60&#34; cy=&#34;60&#34; r=&#34;50&#34; style=&#34;--rot:244.8;--len:100.5;stroke:var(--cohort-female)&#34;/&gt;
&lt;text class=&#34;donut-mid&#34; x=&#34;60&#34; y=&#34;58&#34; text-anchor=&#34;middle&#34;&gt;32%&lt;/text&gt;
&lt;text class=&#34;donut-sub&#34; x=&#34;60&#34; y=&#34;71&#34; text-anchor=&#34;middle&#34;&gt;women&lt;/text&gt;
&lt;/svg&gt;
&lt;ul class=&#34;cohort-legend&#34;&gt;
&lt;li&gt;&lt;span class=&#34;sw&#34; style=&#34;background:var(--cohort-male)&#34;&gt;&lt;/span&gt;&lt;span class=&#34;name&#34;&gt;Male&lt;/span&gt;&lt;span class=&#34;val&#34;&gt;34 · 68%&lt;/span&gt;&lt;/li&gt;
&lt;li&gt;&lt;span class=&#34;sw&#34; style=&#34;background:var(--cohort-female)&#34;&gt;&lt;/span&gt;&lt;span class=&#34;name&#34;&gt;Female&lt;/span&gt;&lt;span class=&#34;val&#34;&gt;16 · 32%&lt;/span&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Roughly a third of the cohort is women, which sits a touch above the typical Lebanese-engineering-school baseline, and we are happy to see that and committed to pushing it higher in future editions.&lt;/p&gt;
&lt;h3 id=&#34;what-the-cohort-brings-to-the-room&#34;&gt;What the cohort brings to the room&lt;/h3&gt;
&lt;div class=&#34;cohort-block reveal&#34;&gt;
&lt;div class=&#34;cohort-panel-bar&#34;&gt;
&lt;div class=&#34;left&#34;&gt;&lt;div class=&#34;traffic&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;span class=&#34;filename&#34;&gt;accomplishments · &lt;span class=&#34;stem&#34;&gt;incidental highlights&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;span class=&#34;status&#34;&gt;&lt;span class=&#34;pulse&#34;&gt;&lt;/span&gt;aggregate&lt;/span&gt;
&lt;/div&gt;
&lt;div class=&#34;cohort-hbars compact&#34;&gt;
&lt;div class=&#34;hbar&#34;&gt;&lt;div class=&#34;lbl&#34;&gt;Master&#39;s or PhD student&lt;/div&gt;&lt;div class=&#34;bar-wrap&#34;&gt;&lt;div class=&#34;bar bar-soft&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;12&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;
&lt;div class=&#34;hbar&#34;&gt;&lt;div class=&#34;lbl&#34;&gt;Applied-crypto / E2EE build&lt;/div&gt;&lt;div class=&#34;bar-wrap&#34;&gt;&lt;div class=&#34;bar bar-soft&#34; style=&#34;--w:75%&#34;&gt;&lt;span class=&#34;num&#34;&gt;9&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;
&lt;div class=&#34;hbar&#34;&gt;&lt;div class=&#34;lbl&#34;&gt;CTF or pen-test competition&lt;/div&gt;&lt;div class=&#34;bar-wrap&#34;&gt;&lt;div class=&#34;bar bar-soft&#34; style=&#34;--w:58%&#34;&gt;&lt;span class=&#34;num&#34;&gt;7&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;
&lt;div class=&#34;hbar&#34;&gt;&lt;div class=&#34;lbl&#34;&gt;Top GPA / national-ranking top-10&lt;/div&gt;&lt;div class=&#34;bar-wrap&#34;&gt;&lt;div class=&#34;bar bar-soft&#34; style=&#34;--w:50%&#34;&gt;&lt;span class=&#34;num&#34;&gt;6&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;
&lt;div class=&#34;hbar&#34;&gt;&lt;div class=&#34;lbl&#34;&gt;Peer-reviewed publication&lt;/div&gt;&lt;div class=&#34;bar-wrap&#34;&gt;&lt;div class=&#34;bar bar-soft&#34; style=&#34;--w:42%&#34;&gt;&lt;span class=&#34;num&#34;&gt;5&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;
&lt;div class=&#34;hbar&#34;&gt;&lt;div class=&#34;lbl&#34;&gt;Founded startup or initiative&lt;/div&gt;&lt;div class=&#34;bar-wrap&#34;&gt;&lt;div class=&#34;bar bar-soft&#34; style=&#34;--w:33%&#34;&gt;&lt;span class=&#34;num&#34;&gt;4&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;
&lt;div class=&#34;hbar&#34;&gt;&lt;div class=&#34;lbl&#34;&gt;Formal verification (Lean / Dafny / Certora)&lt;/div&gt;&lt;div class=&#34;bar-wrap&#34;&gt;&lt;div class=&#34;bar bar-soft&#34; style=&#34;--w:25%&#34;&gt;&lt;span class=&#34;num&#34;&gt;3&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;cohort-hbars-axis&#34;&gt;admitted students with this on their CV (out of 50)&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;The range of what this cohort has already done is wild. Graduate-level coursework, peer-reviewed publications, formal-verification contributions to widely deployed protocols, CTF placements, applied-cryptography builds already in users&amp;rsquo; hands, and several founded companies and initiatives. We cannot wait to see what this group does together over the summer.&lt;/p&gt;
&lt;h2 id=&#34;see-you-on-june-9&#34;&gt;See you on June 9&lt;/h2&gt;
&lt;p&gt;Lectures run Tuesdays and Thursdays, 11:00 to 12:30 Beirut time. To the admitted students: congratulations, welcome aboard, and we will see you in three weeks for what is going to be a really fun semester.&lt;/p&gt;
&lt;p&gt;To everyone else: every piece of course material is publicly available on the &lt;a href=&#34;https://appliedcryptography.page&#34;&gt;course website&lt;/a&gt; under Creative Commons, so you can follow along at your own pace. And we plan to run the program again. Come back next year!&lt;/p&gt;
</content:encoded>
      <category>Announcement</category>
      <category>Course</category>
      <category>Applied Cryptography</category>
      
    </item>
    
    <item>
      <title>hpke-ng: Faster, Smaller, Harder HPKE for Rust</title>
      <link>https://symbolic.software/blog/2026-05-08-hpke-ng/</link>
      <pubDate>Fri, 08 May 2026 00:00:00 &#43;0000</pubDate>
      <guid>https://symbolic.software/blog/2026-05-08-hpke-ng/</guid>
      <description>126 head-to-head benchmarks against hpke-rs and rust-hpke. hpke-ng wins 103, ties 19, loses 4. ML-KEM-1024 decap −55%, X25519 decap −41%, export −72% to −76%, end-to-end roundtrip −30%, plus a type system that catches four classes of bug at compile time.</description>
      <content:encoded>&lt;p&gt;Today we&amp;rsquo;re releasing &lt;a href=&#34;https://github.com/symbolicsoft/hpke-ng&#34;&gt;hpke-ng&lt;/a&gt;, a clean-slate Rust implementation of &lt;a href=&#34;https://www.rfc-editor.org/rfc/rfc9180.html&#34;&gt;HPKE (RFC 9180)&lt;/a&gt;. It is published under Apache-2.0 OR MIT, and you can install it now with &lt;code&gt;cargo add hpke-ng&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;We benchmark hpke-ng against the two major Rust HPKE libraries: &lt;a href=&#34;https://github.com/celabshq/hpke-rs&#34;&gt;hpke-rs&lt;/a&gt;, CE Labs&amp;rsquo; library and currently the most widely deployed, and &lt;a href=&#34;https://github.com/rozbb/rust-hpke&#34;&gt;rust-hpke&lt;/a&gt;, the older type-state-based implementation by Michael Rosenberg. Across &lt;strong&gt;126 head-to-head measurements&lt;/strong&gt;, 76 against hpke-rs and 50 against rust-hpke (which doesn&amp;rsquo;t support every ciphersuite hpke-ng or hpke-rs do), hpke-ng wins &lt;strong&gt;103&lt;/strong&gt;, ties &lt;strong&gt;19&lt;/strong&gt;, and loses &lt;strong&gt;4&lt;/strong&gt;. The wins concentrate in the &lt;strong&gt;post-quantum decap path&lt;/strong&gt; (−54% to −55% on ML-KEM, the largest deltas in the dataset; rust-hpke has no post-quantum support), the &lt;strong&gt;classical decap path&lt;/strong&gt; (−41% on X25519 vs hpke-rs, −31% vs rust-hpke), the &lt;strong&gt;single-shot open path&lt;/strong&gt; (every payload size a clean win against both libraries), the &lt;strong&gt;post-quantum and ChaCha20 setup paths&lt;/strong&gt;, and the &lt;strong&gt;export path&lt;/strong&gt; (a new datapoint added in this round: −72% to −76% across all five output lengths vs hpke-rs). End-to-end roundtrip, one full seal-and-open against the same recipient, comes in at −30% vs both libraries on a 1 KiB message. The ties cluster on rows where the underlying primitive crate dominates wall time and the framing overhead is too small to matter.&lt;/p&gt;
&lt;div class=&#34;hpke-score hpke-score-multi reveal&#34;&gt;
&lt;div class=&#34;hpke-panel-bar&#34;&gt;
&lt;div class=&#34;left&#34;&gt;&lt;div class=&#34;traffic&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;span class=&#34;filename&#34;&gt;cargo bench · &lt;span class=&#34;stem&#34;&gt;comparative&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;span class=&#34;status&#34;&gt;&lt;span class=&#34;pulse&#34;&gt;&lt;/span&gt;126 head-to-head benchmarks · 2 libraries&lt;/span&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-score-strips&#34;&gt;
&lt;div class=&#34;hpke-score-strip&#34;&gt;
&lt;div class=&#34;strip-label&#34;&gt;&lt;strong&gt;vs hpke-rs&lt;/strong&gt;76 benchmarks&lt;/div&gt;
&lt;div class=&#34;strip-counters&#34;&gt;
&lt;div class=&#34;c c-win&#34;&gt;&lt;span class=&#34;n&#34;&gt;62&lt;/span&gt;&lt;span class=&#34;lab&#34;&gt;wins&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;c c-tie&#34;&gt;&lt;span class=&#34;n&#34;&gt;10&lt;/span&gt;&lt;span class=&#34;lab&#34;&gt;tied&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;c c-loss&#34;&gt;&lt;span class=&#34;n&#34;&gt;4&lt;/span&gt;&lt;span class=&#34;lab&#34;&gt;losses&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;strip-dots&#34; data-wins=&#34;62&#34; data-ties=&#34;10&#34; data-losses=&#34;4&#34;&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-score-strip&#34;&gt;
&lt;div class=&#34;strip-label&#34;&gt;&lt;strong&gt;vs rust-hpke&lt;/strong&gt;50 benchmarks&lt;/div&gt;
&lt;div class=&#34;strip-counters&#34;&gt;
&lt;div class=&#34;c c-win&#34;&gt;&lt;span class=&#34;n&#34;&gt;41&lt;/span&gt;&lt;span class=&#34;lab&#34;&gt;wins&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;c c-tie&#34;&gt;&lt;span class=&#34;n&#34;&gt;9&lt;/span&gt;&lt;span class=&#34;lab&#34;&gt;tied&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;c c-loss&#34;&gt;&lt;span class=&#34;n&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;lab&#34;&gt;losses&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;strip-dots&#34; data-wins=&#34;41&#34; data-ties=&#34;9&#34; data-losses=&#34;0&#34;&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-score-strip&#34;&gt;
&lt;div class=&#34;strip-label&#34;&gt;&lt;strong&gt;combined&lt;/strong&gt;126 benchmarks&lt;/div&gt;
&lt;div class=&#34;strip-counters&#34;&gt;
&lt;div class=&#34;c c-win&#34;&gt;&lt;span class=&#34;n&#34;&gt;103&lt;/span&gt;&lt;span class=&#34;lab&#34;&gt;wins&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;c c-tie&#34;&gt;&lt;span class=&#34;n&#34;&gt;19&lt;/span&gt;&lt;span class=&#34;lab&#34;&gt;tied&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;c c-loss&#34;&gt;&lt;span class=&#34;n&#34;&gt;4&lt;/span&gt;&lt;span class=&#34;lab&#34;&gt;losses&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;strip-dots&#34; data-wins=&#34;103&#34; data-ties=&#34;19&#34; data-losses=&#34;4&#34;&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-score-legend&#34;&gt;
&lt;span&gt;&lt;span class=&#34;legend-dot win&#34;&gt;&lt;/span&gt;hpke-ng faster (&amp;gt;3% delta)&lt;/span&gt;
&lt;span&gt;&lt;span class=&#34;legend-dot tie&#34;&gt;&lt;/span&gt;within criterion noise band (±3%)&lt;/span&gt;
&lt;span&gt;&lt;span class=&#34;legend-dot loss&#34;&gt;&lt;/span&gt;competitor faster&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;All three libraries lean on the RustCrypto family of primitive crates, with one exception we&amp;rsquo;ll flag below: rust-hpke pulls a different P-256 implementation, and you&amp;rsquo;ll see the gap show up at the primitive layer when we get there. On the rows where the underlying primitive crate matches, the cryptography itself is identical: all three pass the full RFC 9180 known-answer-test set on the vectors we hand-checked, and all three produce byte-identical wire output to each other on every ciphersuite we differentially tested. The wins are gains in framing, monomorphization, allocation behaviour, and dispatch, not in the underlying crypto math. That is the point: the math is a solved problem, and the surrounding library is where the engineering still has slack.&lt;/p&gt;
&lt;h2 id=&#34;why-we-built-another-hpke-library&#34;&gt;Why we built another HPKE library&lt;/h2&gt;
&lt;p&gt;Earlier this year we found and reported two security bugs in hpke-rs. The first was a &lt;a href=&#34;https://github.com/celabshq/hpke-rs/pull/117&#34;&gt;missing RFC 9180 §7.1.4 zero-shared-secret check&lt;/a&gt;: a low-order or identity public key forces the X25519 shared secret to all zeros, after which the rest of the key schedule becomes deterministic and predictable to anyone who knows the static recipient key. The fix is a single comparison against zero, which RFC 9180 explicitly requires; it was missing. The second was a &lt;a href=&#34;https://github.com/celabshq/hpke-rs/pull/118&#34;&gt;&lt;code&gt;u32&lt;/code&gt; sequence-counter that silently wrapped in release builds&lt;/a&gt;, reusing nonces after 2³² messages. Nonce reuse in AEAD is catastrophic: for AES-GCM it leaks the authentication key, and for ChaCha20-Poly1305 it leaks plaintext via XOR. The wrap was happening below the type system, in a release build, with no diagnostic. Both are now fixed. We documented the broader pattern of bugs we kept finding in libraries marketed under &amp;ldquo;high assurance&amp;rdquo; branding &lt;a href=&#34;https://symbolic.software/blog/2026-02-05-ce-labs/&#34;&gt;in February&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;rust-hpke, by contrast, is not a library we have audit findings against. It is older (its 0.x line predates hpke-rs by years), uses a type-state design similar to the one we settled on for hpke-ng, and has the careful, hand-tuned feel of a personal project that has aged well. We added it to this round of benchmarks for exactly that reason: comparing only against hpke-rs invites the objection that the bar is low. rust-hpke raises it. The deltas you&amp;rsquo;ll see below are against both libraries; where rust-hpke supports a ciphersuite, we show its row alongside hpke-rs&amp;rsquo;s. It doesn&amp;rsquo;t support post-quantum KEMs, X-Wing, or secp256k1, so those rows stay two-way.&lt;/p&gt;
&lt;p&gt;That experience, together with the day-to-day frictions of integrating HPKE through libraries that weren&amp;rsquo;t built to make those bug classes structurally impossible, is what got us thinking about a rewrite. Three frictions in particular kept showing up in hpke-rs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The provider abstraction.&lt;/strong&gt; hpke-rs is structured as a generic library over an &lt;code&gt;HpkeCrypto&lt;/code&gt; trait, with two backend implementations, RustCrypto and libcrux, shipped as separate crates. The abstraction is real engineering and serves a real purpose: it lets a deployment swap the underlying crypto stack without touching call sites. The cost is that every primitive call goes through a trait dispatch, every &lt;code&gt;Hpke&lt;/code&gt; value carries a 344-byte instance struct (most of it a 256-byte ChaCha20 PRNG state), and the workspace is four crates instead of one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The struct-owned PRNG.&lt;/strong&gt; &lt;code&gt;Hpke&amp;lt;Crypto&amp;gt;::new&lt;/code&gt; constructs and stores a PRNG. That PRNG is reused across operations, which is fine functionally but creates a subtle aliasing hazard: cloning an &lt;code&gt;Hpke&lt;/code&gt; does not clone the PRNG state, per the rustdoc, so a careless clone can reset randomness in ways that aren&amp;rsquo;t visible at the call site. Nothing in the type system stops it, and nothing in a test suite is likely to catch it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;Option&amp;lt;&amp;amp;[u8]&amp;gt;&lt;/code&gt; for required-by-mode arguments.&lt;/strong&gt; &lt;code&gt;hpke.seal(&amp;amp;pk, info, aad, pt, None, None, None)&lt;/code&gt; is the canonical Base-mode call. The three &lt;code&gt;None&lt;/code&gt;s are the PSK, PSK ID, and sender static key, all of which are required in the Auth or AuthPsk modes. The single &lt;code&gt;seal&lt;/code&gt; method accepts every mode by making mode-specific arguments optional, which means the type system can&amp;rsquo;t tell you that you&amp;rsquo;ve built a Base-mode call with a PSK supplied.&lt;/p&gt;
&lt;p&gt;These are not catastrophic problems. They&amp;rsquo;re the kind of small persistent costs you stop noticing until you build the alternative.&lt;/p&gt;
&lt;h2 id=&#34;the-shape-change-enum-dispatch-becomes-type-state&#34;&gt;The shape change: enum dispatch becomes type-state&lt;/h2&gt;
&lt;p&gt;The single biggest design difference between hpke-ng and hpke-rs is what carries the ciphersuite. In hpke-rs, the ciphersuite is four runtime enums (&lt;code&gt;Mode&lt;/code&gt;, &lt;code&gt;KemAlgorithm&lt;/code&gt;, &lt;code&gt;KdfAlgorithm&lt;/code&gt;, &lt;code&gt;AeadAlgorithm&lt;/code&gt;) constructed at &lt;code&gt;Hpke::new&lt;/code&gt; time. In hpke-ng, the ciphersuite is the type itself, as in &lt;code&gt;Hpke&amp;lt;DhKemX25519HkdfSha256, HkdfSha256, ChaCha20Poly1305&amp;gt;&lt;/code&gt;, and the struct body is &lt;code&gt;PhantomData&amp;lt;(K, F, A)&amp;gt;&lt;/code&gt;. Zero bytes at runtime; everything resolved at the call site by the compiler.&lt;/p&gt;
&lt;div class=&#34;hpke-api reveal&#34;&gt;
&lt;div class=&#34;hpke-panel-bar&#34;&gt;
&lt;div class=&#34;left&#34;&gt;&lt;div class=&#34;traffic&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;span class=&#34;filename&#34;&gt;canonical seal · &lt;span class=&#34;stem&#34;&gt;&#34;encrypt one message&#34;&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-api-grid&#34;&gt;
&lt;div class=&#34;hpke-api-col&#34;&gt;
&lt;div class=&#34;hpke-api-head&#34;&gt;&lt;span class=&#34;lib lib-rs&#34;&gt;hpke-rs&lt;/span&gt;&lt;span class=&#34;meta&#34;&gt;7 args · 3× &lt;code&gt;Option&amp;lt;&amp;amp;[u8]&amp;gt;&lt;/code&gt;&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&#34;hpke-api-code&#34;&gt;&lt;span class=&#34;ln&#34;&gt;&lt;span class=&#34;kw&#34;&gt;let mut&lt;/span&gt; hpke = Hpke::&amp;lt;HpkeRustCrypto&amp;gt;::&lt;span class=&#34;fn&#34;&gt;new&lt;/span&gt;(&lt;/span&gt;
&lt;span class=&#34;ln&#34;&gt;    Mode::Base,&lt;/span&gt;
&lt;span class=&#34;ln&#34;&gt;    KemAlgorithm::DhKem25519,&lt;/span&gt;
&lt;span class=&#34;ln&#34;&gt;    KdfAlgorithm::HkdfSha256,&lt;/span&gt;
&lt;span class=&#34;ln&#34;&gt;    AeadAlgorithm::ChaCha20Poly1305,&lt;/span&gt;
&lt;span class=&#34;ln&#34;&gt;);&lt;/span&gt;
&lt;span class=&#34;ln&#34;&gt;&lt;span class=&#34;kw&#34;&gt;let&lt;/span&gt; kp = hpke.&lt;span class=&#34;fn&#34;&gt;generate_key_pair&lt;/span&gt;()?;&lt;/span&gt;
&lt;span class=&#34;ln&#34;&gt;&lt;span class=&#34;kw&#34;&gt;let&lt;/span&gt; (sk, pk) = kp.&lt;span class=&#34;fn&#34;&gt;into_keys&lt;/span&gt;();&lt;/span&gt;
&lt;span class=&#34;ln&#34;&gt;&lt;span class=&#34;kw&#34;&gt;let&lt;/span&gt; (enc, ct) = hpke.&lt;span class=&#34;fn&#34;&gt;seal&lt;/span&gt;(&lt;/span&gt;
&lt;span class=&#34;ln&#34;&gt;    &amp;amp;pk, info, aad, pt,&lt;/span&gt;
&lt;span class=&#34;ln&#34;&gt;    &lt;span class=&#34;lit&#34;&gt;None&lt;/span&gt;, &lt;span class=&#34;lit&#34;&gt;None&lt;/span&gt;, &lt;span class=&#34;lit&#34;&gt;None&lt;/span&gt;, &lt;span class=&#34;com&#34;&gt;// psk, psk_id, sk_s&lt;/span&gt;&lt;/span&gt;
&lt;span class=&#34;ln&#34;&gt;)?;&lt;/span&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-api-col&#34;&gt;
&lt;div class=&#34;hpke-api-head&#34;&gt;&lt;span class=&#34;lib lib-ng&#34;&gt;hpke-ng&lt;/span&gt;&lt;span class=&#34;meta&#34;&gt;5 args · zero placeholders&lt;/span&gt;&lt;/div&gt;
&lt;pre class=&#34;hpke-api-code&#34;&gt;&lt;span class=&#34;ln&#34;&gt;&lt;span class=&#34;kw&#34;&gt;type&lt;/span&gt; Suite = Hpke&amp;lt;&lt;/span&gt;
&lt;span class=&#34;ln&#34;&gt;    DhKemX25519HkdfSha256,&lt;/span&gt;
&lt;span class=&#34;ln&#34;&gt;    HkdfSha256,&lt;/span&gt;
&lt;span class=&#34;ln&#34;&gt;    ChaCha20Poly1305,&lt;/span&gt;
&lt;span class=&#34;ln&#34;&gt;&amp;gt;;&lt;/span&gt;
&lt;span class=&#34;ln&#34;&gt;&lt;span class=&#34;kw&#34;&gt;let mut&lt;/span&gt; os = OsRng;&lt;/span&gt;
&lt;span class=&#34;ln&#34;&gt;&lt;span class=&#34;kw&#34;&gt;let mut&lt;/span&gt; rng = os.&lt;span class=&#34;fn&#34;&gt;unwrap_mut&lt;/span&gt;();&lt;/span&gt;
&lt;span class=&#34;ln&#34;&gt;&lt;span class=&#34;kw&#34;&gt;let&lt;/span&gt; (sk, pk) = DhKemX25519HkdfSha256::&lt;span class=&#34;fn&#34;&gt;generate&lt;/span&gt;(&amp;amp;&lt;span class=&#34;kw&#34;&gt;mut&lt;/span&gt; rng)?;&lt;/span&gt;
&lt;span class=&#34;ln&#34;&gt;&lt;span class=&#34;kw&#34;&gt;let&lt;/span&gt; (enc, ct) = Suite::&lt;span class=&#34;fn&#34;&gt;seal_base&lt;/span&gt;(&lt;/span&gt;
&lt;span class=&#34;ln&#34;&gt;    &amp;amp;&lt;span class=&#34;kw&#34;&gt;mut&lt;/span&gt; rng, &amp;amp;pk, info, aad, pt,&lt;/span&gt;
&lt;span class=&#34;ln&#34;&gt;)?;&lt;/span&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;rust-hpke&amp;rsquo;s API is closer to hpke-ng&amp;rsquo;s: it also encodes KEM / KDF / AEAD as type parameters, and exposes &lt;code&gt;setup_sender::&amp;lt;Aead, Kdf, Kem, _&amp;gt;(&amp;amp;OpModeS::Base, &amp;amp;pk, info, &amp;amp;mut rng)&lt;/code&gt; as the canonical entry point. The visible consequence is the same as in hpke-ng: no &lt;code&gt;Option&lt;/code&gt; placeholders for unused fields. The wider difference between rust-hpke and hpke-ng is in scope rather than shape. rust-hpke ships fewer ciphersuites (no secp256k1, no post-quantum KEMs), bundles the AEAD into the type chain rather than letting &lt;code&gt;Context&lt;/code&gt; specialize on it, and doesn&amp;rsquo;t expose raw &lt;code&gt;encap&lt;/code&gt;/&lt;code&gt;decap&lt;/code&gt; separately from setup. We&amp;rsquo;ll come back to that last point in the methodology note.&lt;/p&gt;
&lt;p&gt;The visible consequence of hpke-ng&amp;rsquo;s type-state choice is the call site. The invisible consequence is what the compiler can rule out:&lt;/p&gt;
&lt;div class=&#34;hpke-reject reveal-stagger&#34;&gt;
&lt;div class=&#34;hpke-reject-head&#34;&gt;
&lt;span&gt;Operation&lt;/span&gt;&lt;span&gt;hpke-rs&lt;/span&gt;&lt;span&gt;hpke-ng&lt;/span&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-reject-row&#34;&gt;
&lt;div class=&#34;op&#34;&gt;&lt;code&gt;Hpke::&amp;lt;XWingDraft06, _, _&amp;gt;::seal_auth(...)&lt;/code&gt;&lt;/div&gt;
&lt;div class=&#34;rs&#34;&gt;&lt;span class=&#34;badge badge-runtime&#34;&gt;runtime&lt;/span&gt; &lt;code&gt;Error::UnsupportedKemOperation&lt;/code&gt;&lt;/div&gt;
&lt;div class=&#34;ng&#34;&gt;&lt;span class=&#34;badge badge-compile&#34;&gt;compile&lt;/span&gt; trait bound &lt;code&gt;K: AuthKem&lt;/code&gt; not satisfied&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-reject-row&#34;&gt;
&lt;div class=&#34;op&#34;&gt;&lt;code&gt;Hpke::&amp;lt;_, _, ExportOnly&amp;gt;::seal_base(...)&lt;/code&gt;&lt;/div&gt;
&lt;div class=&#34;rs&#34;&gt;&lt;span class=&#34;badge badge-runtime&#34;&gt;runtime&lt;/span&gt; &lt;code&gt;HpkeError::InvalidConfig&lt;/code&gt;&lt;/div&gt;
&lt;div class=&#34;ng&#34;&gt;&lt;span class=&#34;badge badge-compile&#34;&gt;compile&lt;/span&gt; no &lt;code&gt;seal_base&lt;/code&gt; on &lt;code&gt;ExportOnly&lt;/code&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-reject-row&#34;&gt;
&lt;div class=&#34;op&#34;&gt;Wrong &lt;code&gt;KemAlgorithm&lt;/code&gt; for a private key&lt;/div&gt;
&lt;div class=&#34;rs&#34;&gt;&lt;span class=&#34;badge badge-runtime&#34;&gt;runtime&lt;/span&gt; mismatch error at &lt;code&gt;setup_*&lt;/code&gt;&lt;/div&gt;
&lt;div class=&#34;ng&#34;&gt;&lt;span class=&#34;badge badge-compile&#34;&gt;compile&lt;/span&gt; key types are KEM-tagged&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-reject-row&#34;&gt;
&lt;div class=&#34;op&#34;&gt;Base-mode call with &lt;code&gt;Some(psk)&lt;/code&gt; argument&lt;/div&gt;
&lt;div class=&#34;rs&#34;&gt;&lt;span class=&#34;badge badge-runtime&#34;&gt;runtime&lt;/span&gt; &lt;code&gt;HpkeError::UnnecessaryPsk&lt;/code&gt;&lt;/div&gt;
&lt;div class=&#34;ng&#34;&gt;&lt;span class=&#34;badge badge-compile&#34;&gt;compile&lt;/span&gt; &lt;code&gt;seal_base&lt;/code&gt; has no PSK parameter&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Each row is a runtime error in hpke-rs and a compiler diagnostic in hpke-ng. rust-hpke catches most of the same shapes at compile time too, but hpke-ng goes further on the &lt;code&gt;Auth&lt;/code&gt;/&lt;code&gt;Base&lt;/code&gt; and &lt;code&gt;ExportOnly&lt;/code&gt; rows, because the method-set split there relies on per-KEM trait bounds (&lt;code&gt;K: AuthKem&lt;/code&gt;) and per-AEAD method availability that rust-hpke doesn&amp;rsquo;t model.&lt;/p&gt;
&lt;p&gt;This isn&amp;rsquo;t theoretical. We&amp;rsquo;ve seen each of those four shapes in production code reviews, usually as a stale &lt;code&gt;match&lt;/code&gt; arm catching the runtime error and turning it into a generic 500. Surfacing them as compile errors deletes a class of code path entirely.&lt;/p&gt;
&lt;h2 id=&#34;feature-parity&#34;&gt;Feature parity&lt;/h2&gt;
&lt;p&gt;Before going further: the obvious skeptical question is &lt;em&gt;what got cut?&lt;/em&gt; The answer is one thing, deliberately, and it&amp;rsquo;s the thing that buys the wins everywhere else.&lt;/p&gt;
&lt;div class=&#34;hpke-parity reveal&#34;&gt;
&lt;div class=&#34;hpke-parity-head&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;span class=&#34;col-h&#34;&gt;hpke-rs&lt;/span&gt;&lt;span class=&#34;col-h&#34;&gt;rust-hpke&lt;/span&gt;&lt;span class=&#34;col-h&#34;&gt;hpke-ng&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-parity-row&#34;&gt;&lt;span class=&#34;cap&#34;&gt;DH KEMs (X25519, X448, P-256, P-384, P-521, secp256k1)&lt;/span&gt;&lt;span class=&#34;cell yes&#34;&gt;✓ all 6&lt;/span&gt;&lt;span class=&#34;cell no&#34;&gt;4 (no X448, no secp256k1)&lt;/span&gt;&lt;span class=&#34;cell yes&#34;&gt;✓ all 6&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-parity-row&#34;&gt;&lt;span class=&#34;cap&#34;&gt;Post-quantum KEMs (X-Wing draft-06, ML-KEM-768, ML-KEM-1024)&lt;/span&gt;&lt;span class=&#34;cell yes&#34;&gt;✓ all 3 (&lt;code&gt;experimental&lt;/code&gt;, upstream-flagged unstable)&lt;/span&gt;&lt;span class=&#34;cell no&#34;&gt;none&lt;/span&gt;&lt;span class=&#34;cell yes&#34;&gt;✓ all 3 (&lt;code&gt;pq&lt;/code&gt; feature)&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-parity-row&#34;&gt;&lt;span class=&#34;cap&#34;&gt;KDFs (HKDF-SHA-256 / -384 / -512)&lt;/span&gt;&lt;span class=&#34;cell yes&#34;&gt;✓ all 3&lt;/span&gt;&lt;span class=&#34;cell yes&#34;&gt;✓ all 3&lt;/span&gt;&lt;span class=&#34;cell yes&#34;&gt;✓ all 3&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-parity-row&#34;&gt;&lt;span class=&#34;cap&#34;&gt;AEADs (AES-128-GCM, AES-256-GCM, ChaCha20-Poly1305, ExportOnly)&lt;/span&gt;&lt;span class=&#34;cell yes&#34;&gt;✓ all 4&lt;/span&gt;&lt;span class=&#34;cell yes&#34;&gt;✓ all 4&lt;/span&gt;&lt;span class=&#34;cell yes&#34;&gt;✓ all 4&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-parity-row&#34;&gt;&lt;span class=&#34;cap&#34;&gt;Modes (Base, Psk, Auth, AuthPsk)&lt;/span&gt;&lt;span class=&#34;cell yes&#34;&gt;✓ all 4&lt;/span&gt;&lt;span class=&#34;cell yes&#34;&gt;✓ all 4&lt;/span&gt;&lt;span class=&#34;cell yes&#34;&gt;✓ all 4&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-parity-row&#34;&gt;&lt;span class=&#34;cap&#34;&gt;RFC 9180 KAT conformance&lt;/span&gt;&lt;span class=&#34;cell yes&#34;&gt;✓ pass&lt;/span&gt;&lt;span class=&#34;cell yes&#34;&gt;✓ pass&lt;/span&gt;&lt;span class=&#34;cell yes&#34;&gt;✓ pass&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-parity-row&#34;&gt;&lt;span class=&#34;cap&#34;&gt;Type-state ciphersuite selection&lt;/span&gt;&lt;span class=&#34;cell no&#34;&gt;runtime enums&lt;/span&gt;&lt;span class=&#34;cell yes&#34;&gt;✓ type-params&lt;/span&gt;&lt;span class=&#34;cell yes&#34;&gt;✓ type-params + per-mode method-set&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-parity-row&#34;&gt;&lt;span class=&#34;cap&#34;&gt;Compile-time-rejected operation classes&lt;/span&gt;&lt;span class=&#34;cell num&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;cell num&#34;&gt;~2&lt;/span&gt;&lt;span class=&#34;cell num plus&#34;&gt;4&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-parity-row&#34;&gt;&lt;span class=&#34;cap&#34;&gt;Raw &lt;code&gt;encap&lt;/code&gt; / &lt;code&gt;decap&lt;/code&gt; exposed (separable from setup)&lt;/span&gt;&lt;span class=&#34;cell no&#34;&gt;no (only via &lt;code&gt;setup_*&lt;/code&gt;)&lt;/span&gt;&lt;span class=&#34;cell no&#34;&gt;no (only via &lt;code&gt;setup_*&lt;/code&gt;)&lt;/span&gt;&lt;span class=&#34;cell yes&#34;&gt;✓ yes&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-parity-row hpke-parity-flip&#34;&gt;&lt;span class=&#34;cap&#34;&gt;Pluggable crypto provider (multiple backends)&lt;/span&gt;&lt;span class=&#34;cell yes&#34;&gt;✓ RustCrypto + libcrux&lt;/span&gt;&lt;span class=&#34;cell yes&#34;&gt;✓ generic over &lt;code&gt;CryptoRng&lt;/code&gt;&lt;/span&gt;&lt;span class=&#34;cell no&#34;&gt;— RustCrypto only&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;hpke-ng supports every ciphersuite hpke-rs&amp;rsquo;s RustCrypto provider supports (it&amp;rsquo;s a superset of rust-hpke), passes the RFC 9180 KAT vector set, and adds two structural capabilities neither alternative has: a deeper method-set split that catches more configuration errors at compile time, and standalone &lt;code&gt;encap&lt;/code&gt;/&lt;code&gt;decap&lt;/code&gt; exposed alongside the high-level setup paths.&lt;/p&gt;
&lt;p&gt;The one thing hpke-rs has that hpke-ng deliberately doesn&amp;rsquo;t: a pluggable crypto provider. hpke-rs ships an &lt;code&gt;HpkeCrypto&lt;/code&gt; trait with two backend implementations, RustCrypto and libcrux; a deployment can swap one for the other at compile time. hpke-ng ships one provider, RustCrypto, and removes the abstraction. Everything else follows from that tradeoff. It&amp;rsquo;s why &lt;code&gt;Hpke&amp;lt;...&amp;gt;&lt;/code&gt; is zero-sized, why &lt;code&gt;Context::seal&lt;/code&gt; is monomorphized rather than dispatched, why the workspace is one crate, and why the canonical call site is five arguments instead of seven.&lt;/p&gt;
&lt;p&gt;The libcrux half of that tradeoff is, in our view, not a feature worth chasing. Our &lt;a href=&#34;https://symbolic.software/blog/2026-02-05-ce-labs/&#34;&gt;February audit&lt;/a&gt; of CE Labs&amp;rsquo; &amp;ldquo;formally verified&amp;rdquo; libraries found undisclosed silent cryptographic failures in libcrux-ml-dsa (platform-dependent SHA-3 corruption that was patched without a public advisory), entropy-reducing pre-hash clamping in Ed25519, and a denial-of-service panic in libcrux-psq&amp;rsquo;s AES-GCM decryption path. CE Labs&amp;rsquo; &lt;a href=&#34;https://symbolic.software/blog/2026-02-12-ce-labs-response/&#34;&gt;public response&lt;/a&gt; acknowledged the findings without retracting the &amp;ldquo;highest level of assurance&amp;rdquo; marketing language attached to the library; our &lt;a href=&#34;https://symbolic.software/blog/2026-02-17-ce-labs-mldsa/&#34;&gt;follow-up analysis&lt;/a&gt; and an &lt;a href=&#34;https://eprint.iacr.org/2026/192&#34;&gt;independent paper&lt;/a&gt; have since surfaced further specification deviations in libcrux-ml-dsa. The &amp;ldquo;formally verified&amp;rdquo; framing that draws users to libcrux does not, on the evidence we have assembled, describe the engineering you are getting. If you&amp;rsquo;re using RustCrypto anyway, which is what hpke-ng does by default and what most hpke-rs deployments do in practice, the provider abstraction is paying rent for a backend you would be wise to avoid.&lt;/p&gt;
&lt;h2 id=&#34;speed&#34;&gt;Speed&lt;/h2&gt;
&lt;p&gt;The full benchmark protocol is in &lt;code&gt;cargo bench --features comparative&lt;/code&gt; in the hpke-ng repository: criterion harness, sample sizes 40–60 per group depending on per-iteration cost, 2–3 second measurement windows, &lt;code&gt;RUSTFLAGS=&amp;quot;-C target-cpu=native&amp;quot;&lt;/code&gt;, &lt;code&gt;lto = &amp;quot;thin&amp;quot;&lt;/code&gt;, &lt;code&gt;codegen-units = 1&lt;/code&gt;. Apple Silicon M-series, macOS. The numbers below are the median across two independent bench runs. The post-quantum rows enable &lt;code&gt;hpke-rs-rust-crypto&lt;/code&gt;&amp;rsquo;s &lt;code&gt;experimental&lt;/code&gt; feature flag. Without it, hpke-rs&amp;rsquo;s PQ KEMs return &lt;code&gt;UnsupportedKemOperation&lt;/code&gt; at runtime; the upstream comment on the gate is &amp;ldquo;broken and pre-releases. Disabling them until they are stable.&amp;rdquo; rust-hpke doesn&amp;rsquo;t ship post-quantum at all, so the PQ and X-Wing rows are two-way (hpke-ng vs hpke-rs only).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Methodology note: the &lt;code&gt;_via_setup_*&lt;/code&gt; suffix.&lt;/strong&gt; hpke-ng exposes raw &lt;code&gt;encap&lt;/code&gt; and &lt;code&gt;decap&lt;/code&gt; as separable KEM operations. hpke-rs and rust-hpke do not. Their nearest public API is &lt;code&gt;setup_sender&lt;/code&gt; / &lt;code&gt;setup_receiver&lt;/code&gt;, which performs the KEM operation &lt;strong&gt;plus&lt;/strong&gt; the HPKE key schedule &lt;strong&gt;plus&lt;/strong&gt; Context construction. Where the benchmark name carries a &lt;code&gt;_via_setup_*&lt;/code&gt; suffix, it is measuring that fuller path because there is no narrower one available; rows so labeled are explicitly &lt;strong&gt;not&lt;/strong&gt; apples-to-apples with hpke-ng&amp;rsquo;s bare-operation rows, and that is the honest framing. We still include them, because the gap reflects an API granularity difference that is itself meaningful to anyone trying to batch KEM operations or pre-compute encap material. We also seed hpke-rs&amp;rsquo;s deterministic PRNG &lt;strong&gt;once&lt;/strong&gt; per benchmark group, in setup, never inside a timed &lt;code&gt;b.iter()&lt;/code&gt; closure; the bare &lt;code&gt;seed()&lt;/code&gt; cost (~58 ns) is deliberately hoisted out so it does not bias any hpke-rs measurement.&lt;/p&gt;
&lt;p&gt;The wins concentrate in five places: the &lt;strong&gt;post-quantum KEM path&lt;/strong&gt; (the largest deltas in the entire dataset, peaking at −55% on ML-KEM-1024 decap), the &lt;strong&gt;classical KEM &lt;code&gt;decap&lt;/code&gt; path&lt;/strong&gt;, the &lt;strong&gt;single-shot open path&lt;/strong&gt; (every payload size, both AEADs, against both competitors), the &lt;strong&gt;post-quantum and ChaCha20 setup paths&lt;/strong&gt;, and the brand-new &lt;strong&gt;export path&lt;/strong&gt; (−72% to −76% across all five output lengths). Start with the KEM operations on X25519:&lt;/p&gt;
&lt;div class=&#34;hpke-bench reveal&#34;&gt;
&lt;div class=&#34;hpke-bench-head&#34;&gt;
&lt;h4&gt;KEM operations · X25519&lt;/h4&gt;
&lt;div class=&#34;hpke-bench-key&#34;&gt;&lt;span class=&#34;key key-rs&#34;&gt;&lt;span class=&#34;d&#34;&gt;&lt;/span&gt;hpke-rs&lt;/span&gt;&lt;span class=&#34;key key-rh&#34;&gt;&lt;span class=&#34;d&#34;&gt;&lt;/span&gt;rust-hpke&lt;/span&gt;&lt;span class=&#34;key key-ng&#34;&gt;&lt;span class=&#34;d&#34;&gt;&lt;/span&gt;hpke-ng&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-bench-body&#34;&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;generate&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:86.7%&#34;&gt;&lt;span class=&#34;num&#34;&gt;8.00 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;9.23 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:89.6%&#34;&gt;&lt;span class=&#34;num&#34;&gt;8.27 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-tie&#34;&gt;+3% rs · −10% rh&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;derive_key_pair&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:98.4%&#34;&gt;&lt;span class=&#34;num&#34;&gt;8.80 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;8.94 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:88.7%&#34;&gt;&lt;span class=&#34;num&#34;&gt;7.93 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−10% rs · −11% rh&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;encap&lt;span class=&#34;dim&#34;&gt;†&lt;/span&gt;&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:93.9%&#34;&gt;&lt;span class=&#34;num&#34;&gt;37.97 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;40.45 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:75.2%&#34;&gt;&lt;span class=&#34;num&#34;&gt;30.41 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−20% rs · −25% rh&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;decap&lt;span class=&#34;dim&#34;&gt;†&lt;/span&gt;&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;37.03 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:86.4%&#34;&gt;&lt;span class=&#34;num&#34;&gt;32.00 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:59.3%&#34;&gt;&lt;span class=&#34;num&#34;&gt;21.96 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−41% rs · −31% rh&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-bench-foot&#34;&gt;Lower is faster. Bars normalized per-row to &lt;code&gt;max(rs, rh, ng)&lt;/code&gt;. &lt;span class=&#34;dim&#34;&gt;†&lt;/span&gt; on &lt;code&gt;encap&lt;/code&gt;/&lt;code&gt;decap&lt;/code&gt;: hpke-ng measures the raw KEM op; hpke-rs and rust-hpke have no such API, so the bar shows their &lt;code&gt;setup_sender&lt;/code&gt;/&lt;code&gt;setup_receiver&lt;/code&gt; (KEM op + key schedule + Context construction). See the methodology note.&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;The decap row is where hpke-ng&amp;rsquo;s design pays off most: hpke-ng caches the recipient&amp;rsquo;s serialized public-key bytes alongside the secret, so &lt;code&gt;decap&lt;/code&gt; no longer pays a base-point scalar multiplication just to rebuild the recipient-PK piece of &lt;code&gt;kem_context&lt;/code&gt; on every call. The classical libraries each run that scalar mult per setup_receiver. The encap row is the methodology note in action: hpke-ng can isolate encap from the surrounding key schedule, the others can&amp;rsquo;t, and a use case that wants to precompute encap material or batch ephemeral generation can exploit that gap directly. The &lt;code&gt;generate&lt;/code&gt; and &lt;code&gt;derive_key_pair&lt;/code&gt; rows are fully comparable across all three libraries; hpke-ng wins by 10% on &lt;code&gt;derive_key_pair&lt;/code&gt; (cached-PK monomorphization) and is tied with hpke-rs on &lt;code&gt;generate&lt;/code&gt; (the bare X25519 keypair generation, where everyone reaches the same primitive ceiling).&lt;/p&gt;
&lt;p&gt;Setup is the combined fixed cost paid every time a sender or receiver context is constructed: KEM op + key schedule + Context allocation. The wins are consistent across ChaCha20, AES-128, AES-256, all three modes:&lt;/p&gt;
&lt;div class=&#34;hpke-bench reveal&#34;&gt;
&lt;div class=&#34;hpke-bench-head&#34;&gt;
&lt;h4&gt;Setup paths · sender / receiver / PSK · X25519 and P-256&lt;/h4&gt;
&lt;div class=&#34;hpke-bench-key&#34;&gt;&lt;span class=&#34;key key-rs&#34;&gt;&lt;span class=&#34;d&#34;&gt;&lt;/span&gt;hpke-rs&lt;/span&gt;&lt;span class=&#34;key key-rh&#34;&gt;&lt;span class=&#34;d&#34;&gt;&lt;/span&gt;rust-hpke&lt;/span&gt;&lt;span class=&#34;key key-ng&#34;&gt;&lt;span class=&#34;d&#34;&gt;&lt;/span&gt;hpke-ng&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-bench-body&#34;&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;X25519+ChaCha20&lt;br&gt;sender (Base)&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:89.8%&#34;&gt;&lt;span class=&#34;num&#34;&gt;38.69 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;43.07 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:71.1%&#34;&gt;&lt;span class=&#34;num&#34;&gt;30.60 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−21% rs · −29% rh&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;X25519+ChaCha20&lt;br&gt;receiver (Base)&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;38.31 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:91.0%&#34;&gt;&lt;span class=&#34;num&#34;&gt;34.86 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:57.3%&#34;&gt;&lt;span class=&#34;num&#34;&gt;21.95 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−43% rs · −37% rh&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;X25519+ChaCha20&lt;br&gt;sender (PSK)&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:98.4%&#34;&gt;&lt;span class=&#34;num&#34;&gt;39.87 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;40.53 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:72.9%&#34;&gt;&lt;span class=&#34;num&#34;&gt;29.56 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−26% rs · −27% rh&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;X25519+AES-128&lt;br&gt;sender (Base)&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:98.9%&#34;&gt;&lt;span class=&#34;num&#34;&gt;40.60 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;41.07 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:77.7%&#34;&gt;&lt;span class=&#34;num&#34;&gt;31.91 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−21% rs · −22% rh&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;X25519+AES-256&lt;br&gt;sender (Base)&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:91.9%&#34;&gt;&lt;span class=&#34;num&#34;&gt;38.16 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;41.51 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:74.1%&#34;&gt;&lt;span class=&#34;num&#34;&gt;30.75 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−19% rs · −26% rh&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;P-256+AES-128&lt;br&gt;sender (Base)&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:68.0%&#34;&gt;&lt;span class=&#34;num&#34;&gt;150.16 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;220.93 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:63.1%&#34;&gt;&lt;span class=&#34;num&#34;&gt;139.35 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−7% rs · −37% rh&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;P-256+AES-256&lt;br&gt;sender (Base)&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:68.3%&#34;&gt;&lt;span class=&#34;num&#34;&gt;150.66 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;220.46 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:63.3%&#34;&gt;&lt;span class=&#34;num&#34;&gt;139.53 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−7% rs · −37% rh&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;secp256k1+ChaCha20&lt;br&gt;sender (Base)&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;59.05 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:79.8%&#34;&gt;&lt;span class=&#34;num&#34;&gt;47.12 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−20% rs · — &lt;span class=&#34;dim&#34;&gt;rh unsupported&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-bench-foot&#34;&gt;Receiver-side wins are larger than sender-side because both &lt;code&gt;decap&lt;/code&gt; and the post-decap key schedule benefit from the cached public-key bytes. rust-hpke&#39;s P-256 path is markedly slower than hpke-rs&#39;s, and the gap there is at the primitive layer (rust-hpke uses a different P-256 crate); hpke-ng is faster than both regardless.&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;The post-quantum KEMs add a structural dimension to the comparison that the classical KEMs don&amp;rsquo;t. hpke-ng stores &lt;code&gt;MlKem*&lt;/code&gt; private keys as the 64-byte &lt;code&gt;(d, z)&lt;/code&gt; seed &lt;em&gt;plus&lt;/em&gt; the materialized FIPS 203 expanded decapsulation key, built once at construction, kept in the &lt;code&gt;PrivateKey&lt;/code&gt; wrapper, reused on every decap. hpke-rs stores only the seed and rebuilds the expanded key on every &lt;code&gt;setup_receiver&lt;/code&gt; call by re-running FIPS 203 KeyGen_internal. That single design choice is why ML-KEM decap shows up at −54% to −55%, the largest single deltas in the dataset. rust-hpke doesn&amp;rsquo;t ship post-quantum at all, so these rows are two-way. ML-KEM-768 first:&lt;/p&gt;
&lt;div class=&#34;hpke-bench reveal&#34;&gt;
&lt;div class=&#34;hpke-bench-head&#34;&gt;
&lt;h4&gt;KEM operations · ML-KEM-768 &lt;span class=&#34;dim&#34;&gt;(rust-hpke unsupported)&lt;/span&gt;&lt;/h4&gt;
&lt;div class=&#34;hpke-bench-key&#34;&gt;&lt;span class=&#34;key key-rs&#34;&gt;&lt;span class=&#34;d&#34;&gt;&lt;/span&gt;hpke-rs&lt;/span&gt;&lt;span class=&#34;key key-ng&#34;&gt;&lt;span class=&#34;d&#34;&gt;&lt;/span&gt;hpke-ng&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-bench-body&#34;&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;generate&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;18.85 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:93.7%&#34;&gt;&lt;span class=&#34;num&#34;&gt;17.66 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−6% rs&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;derive_key_pair&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;14.54 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:98.1%&#34;&gt;&lt;span class=&#34;num&#34;&gt;14.26 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-tie&#34;&gt;−2% rs&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;encap&lt;span class=&#34;dim&#34;&gt;†&lt;/span&gt;&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;24.62 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:59.7%&#34;&gt;&lt;span class=&#34;num&#34;&gt;14.70 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−40% rs&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;decap&lt;span class=&#34;dim&#34;&gt;†&lt;/span&gt;&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;40.86 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:45.5%&#34;&gt;&lt;span class=&#34;num&#34;&gt;18.58 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−55% rs&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-bench-foot&#34;&gt;decap −55%, encap −40%. hpke-ng caches the expanded decapsulation key in the private key and the parsed encapsulation key in the public key; hpke-rs rebuilds both from raw bytes on every call.&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;ML-KEM-1024, the higher-security parameter set, shows the same shape with larger absolute numbers and the same architectural delta in the same place:&lt;/p&gt;
&lt;div class=&#34;hpke-bench reveal&#34;&gt;
&lt;div class=&#34;hpke-bench-head&#34;&gt;
&lt;h4&gt;KEM operations · ML-KEM-1024 &lt;span class=&#34;dim&#34;&gt;(rust-hpke unsupported)&lt;/span&gt;&lt;/h4&gt;
&lt;div class=&#34;hpke-bench-key&#34;&gt;&lt;span class=&#34;key key-rs&#34;&gt;&lt;span class=&#34;d&#34;&gt;&lt;/span&gt;hpke-rs&lt;/span&gt;&lt;span class=&#34;key key-ng&#34;&gt;&lt;span class=&#34;d&#34;&gt;&lt;/span&gt;hpke-ng&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-bench-body&#34;&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;generate&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:96.9%&#34;&gt;&lt;span class=&#34;num&#34;&gt;27.55 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;28.43 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-flip&#34;&gt;+3% rs&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;derive_key_pair&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:93.8%&#34;&gt;&lt;span class=&#34;num&#34;&gt;23.07 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;24.59 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-flip&#34;&gt;+7% rs&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;encap&lt;span class=&#34;dim&#34;&gt;†&lt;/span&gt;&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;36.97 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:62.2%&#34;&gt;&lt;span class=&#34;num&#34;&gt;22.99 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−38% rs&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;decap&lt;span class=&#34;dim&#34;&gt;†&lt;/span&gt;&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;61.03 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:44.6%&#34;&gt;&lt;span class=&#34;num&#34;&gt;27.24 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−55% rs&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-bench-foot&#34;&gt;Same pattern as ML-KEM-768 at higher security level: encap −38%, decap −55%. The +7% on &lt;code&gt;derive_key_pair&lt;/code&gt; is the cost of cloning the larger ML-KEM-1024 expanded encapsulation key into the public-key wrapper at construction, paid once and recovered on every subsequent decap.&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;X-Wing, the X25519 + ML-KEM-768 hybrid, picks up the same &lt;code&gt;DecapsulationKey&lt;/code&gt;-caching trick we applied to ML-KEM, plus the parsed &lt;code&gt;EncapsulationKey&lt;/code&gt; cache on the encap side. The decap delta lands close to the ML-KEM rows:&lt;/p&gt;
&lt;div class=&#34;hpke-bench reveal&#34;&gt;
&lt;div class=&#34;hpke-bench-head&#34;&gt;
&lt;h4&gt;KEM operations · X-Wing draft-06 &lt;span class=&#34;dim&#34;&gt;(rust-hpke unsupported)&lt;/span&gt;&lt;/h4&gt;
&lt;div class=&#34;hpke-bench-key&#34;&gt;&lt;span class=&#34;key key-rs&#34;&gt;&lt;span class=&#34;d&#34;&gt;&lt;/span&gt;hpke-rs&lt;/span&gt;&lt;span class=&#34;key key-ng&#34;&gt;&lt;span class=&#34;d&#34;&gt;&lt;/span&gt;hpke-ng&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-bench-body&#34;&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;generate&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;40.42 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:94.4%&#34;&gt;&lt;span class=&#34;num&#34;&gt;38.15 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−6% rs&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;derive_key_pair&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;38.77 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:97.1%&#34;&gt;&lt;span class=&#34;num&#34;&gt;37.63 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-tie&#34;&gt;−3% rs&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;encap&lt;span class=&#34;dim&#34;&gt;†&lt;/span&gt;&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;66.01 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:83.7%&#34;&gt;&lt;span class=&#34;num&#34;&gt;55.26 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−16% rs&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;decap&lt;span class=&#34;dim&#34;&gt;†&lt;/span&gt;&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;115.75 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:62.8%&#34;&gt;&lt;span class=&#34;num&#34;&gt;72.64 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−37% rs&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-bench-foot&#34;&gt;decap −37%: the construction-side &lt;code&gt;expand_key&lt;/code&gt; that &lt;code&gt;DecapsulationKey::from(seed)&lt;/code&gt; runs (SHAKE-256 + ML-KEM-768 keygen) is now amortized to once per private key.&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;The same pattern propagates to setup paths. &lt;code&gt;setup_sender&lt;/code&gt; is dominated by encap; &lt;code&gt;setup_receiver&lt;/code&gt; is dominated by decap; the ML-KEM rows widen accordingly. rust-hpke is unsupported across all of these:&lt;/p&gt;
&lt;div class=&#34;hpke-bench reveal&#34;&gt;
&lt;div class=&#34;hpke-bench-head&#34;&gt;
&lt;h4&gt;Setup paths · post-quantum (HKDF-SHA-256 + ChaCha20-Poly1305) &lt;span class=&#34;dim&#34;&gt;(rust-hpke unsupported)&lt;/span&gt;&lt;/h4&gt;
&lt;div class=&#34;hpke-bench-key&#34;&gt;&lt;span class=&#34;key key-rs&#34;&gt;&lt;span class=&#34;d&#34;&gt;&lt;/span&gt;hpke-rs&lt;/span&gt;&lt;span class=&#34;key key-ng&#34;&gt;&lt;span class=&#34;d&#34;&gt;&lt;/span&gt;hpke-ng&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-bench-body&#34;&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;X-Wing&lt;br&gt;sender (Base)&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;67.10 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:87.0%&#34;&gt;&lt;span class=&#34;num&#34;&gt;58.38 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−13% rs&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;X-Wing&lt;br&gt;receiver (Base)&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;125.95 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:59.4%&#34;&gt;&lt;span class=&#34;num&#34;&gt;74.83 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−41% rs&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;ML-KEM-768&lt;br&gt;sender (Base)&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;23.90 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:68.6%&#34;&gt;&lt;span class=&#34;num&#34;&gt;16.39 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−31% rs&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;ML-KEM-768&lt;br&gt;receiver (Base)&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;39.74 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:48.2%&#34;&gt;&lt;span class=&#34;num&#34;&gt;19.16 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−52% rs&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;ML-KEM-1024&lt;br&gt;sender (Base)&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;34.19 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:69.5%&#34;&gt;&lt;span class=&#34;num&#34;&gt;23.77 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−31% rs&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;ML-KEM-1024&lt;br&gt;receiver (Base)&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;60.28 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:46.4%&#34;&gt;&lt;span class=&#34;num&#34;&gt;27.99 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−54% rs&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-bench-foot&#34;&gt;All six post-quantum setup rows: 6 wins, 0 ties, 0 losses.&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;The single-shot open path, meaning &lt;code&gt;setup_receiver&lt;/code&gt; plus &lt;code&gt;Context::open&lt;/code&gt; for one message, is where hpke-ng wins most consistently across payload size. Six rows across two orders of magnitude, against both competitors, every row is hpke-ng:&lt;/p&gt;
&lt;div class=&#34;hpke-bench reveal&#34;&gt;
&lt;div class=&#34;hpke-bench-head&#34;&gt;
&lt;h4&gt;Single-shot open · X25519 + HKDF-SHA-256 + ChaCha20-Poly1305&lt;/h4&gt;
&lt;div class=&#34;hpke-bench-key&#34;&gt;&lt;span class=&#34;key key-rs&#34;&gt;&lt;span class=&#34;d&#34;&gt;&lt;/span&gt;hpke-rs&lt;/span&gt;&lt;span class=&#34;key key-rh&#34;&gt;&lt;span class=&#34;d&#34;&gt;&lt;/span&gt;rust-hpke&lt;/span&gt;&lt;span class=&#34;key key-ng&#34;&gt;&lt;span class=&#34;d&#34;&gt;&lt;/span&gt;hpke-ng&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-bench-body&#34;&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;64 B&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;38.19 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:93.0%&#34;&gt;&lt;span class=&#34;num&#34;&gt;35.52 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:60.8%&#34;&gt;&lt;span class=&#34;num&#34;&gt;23.21 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−39% rs · −35% rh&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;256 B&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;39.09 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:86.4%&#34;&gt;&lt;span class=&#34;num&#34;&gt;33.79 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:58.5%&#34;&gt;&lt;span class=&#34;num&#34;&gt;22.85 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−42% rs · −32% rh&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;1 KiB&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;38.57 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:88.4%&#34;&gt;&lt;span class=&#34;num&#34;&gt;34.08 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:60.4%&#34;&gt;&lt;span class=&#34;num&#34;&gt;23.30 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−40% rs · −32% rh&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;4 KiB&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;42.89 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:89.0%&#34;&gt;&lt;span class=&#34;num&#34;&gt;38.18 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:64.6%&#34;&gt;&lt;span class=&#34;num&#34;&gt;27.69 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−35% rs · −28% rh&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;16 KiB&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;61.20 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:95.0%&#34;&gt;&lt;span class=&#34;num&#34;&gt;58.12 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:74.0%&#34;&gt;&lt;span class=&#34;num&#34;&gt;45.32 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−26% rs · −22% rh&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;64 KiB&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;131.77 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:97.6%&#34;&gt;&lt;span class=&#34;num&#34;&gt;128.58 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:88.2%&#34;&gt;&lt;span class=&#34;num&#34;&gt;116.16 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−12% rs · −10% rh&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-bench-foot&#34;&gt;Lower is faster. Open inherits the full setup-receiver win, including the cached &lt;code&gt;pk_bytes&lt;/code&gt; shaving a scalar mult off every &lt;code&gt;decap&lt;/code&gt;, so the small-payload regime where setup dominates picks up the largest deltas. All 12 cells against both libraries are hpke-ng wins.&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;The AES-128-GCM seal sweep shows hpke-ng 6–22% ahead from 64 B through 64 KiB against both competitors. The cached AES cipher state (round keys plus the GHash precomputed table, built once at key-schedule time) eliminates the per-call expansion that hpke-rs runs on every seal:&lt;/p&gt;
&lt;div class=&#34;hpke-bench reveal&#34;&gt;
&lt;div class=&#34;hpke-bench-head&#34;&gt;
&lt;h4&gt;Single-shot seal · X25519 + HKDF-SHA-256 + AES-128-GCM&lt;/h4&gt;
&lt;div class=&#34;hpke-bench-key&#34;&gt;&lt;span class=&#34;key key-rs&#34;&gt;&lt;span class=&#34;d&#34;&gt;&lt;/span&gt;hpke-rs&lt;/span&gt;&lt;span class=&#34;key key-rh&#34;&gt;&lt;span class=&#34;d&#34;&gt;&lt;/span&gt;rust-hpke&lt;/span&gt;&lt;span class=&#34;key key-ng&#34;&gt;&lt;span class=&#34;d&#34;&gt;&lt;/span&gt;hpke-ng&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-bench-body&#34;&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;64 B&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:94.5%&#34;&gt;&lt;span class=&#34;num&#34;&gt;39.16 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;41.46 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:76.9%&#34;&gt;&lt;span class=&#34;num&#34;&gt;31.87 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−19% rs · −23% rh&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;256 B&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:94.0%&#34;&gt;&lt;span class=&#34;num&#34;&gt;39.67 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;42.18 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:73.3%&#34;&gt;&lt;span class=&#34;num&#34;&gt;30.91 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−22% rs · −27% rh&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;1 KiB&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:94.9%&#34;&gt;&lt;span class=&#34;num&#34;&gt;42.41 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;44.71 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:75.2%&#34;&gt;&lt;span class=&#34;num&#34;&gt;33.60 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−21% rs · −25% rh&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;4 KiB&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:97.7%&#34;&gt;&lt;span class=&#34;num&#34;&gt;55.20 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;56.50 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:78.3%&#34;&gt;&lt;span class=&#34;num&#34;&gt;44.25 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−20% rs · −22% rh&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;16 KiB&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:99.2%&#34;&gt;&lt;span class=&#34;num&#34;&gt;99.38 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;100.21 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:92.8%&#34;&gt;&lt;span class=&#34;num&#34;&gt;93.00 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−6% rs · −7% rh&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;64 KiB&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;286.56 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:98.5%&#34;&gt;&lt;span class=&#34;num&#34;&gt;282.39 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:90.8%&#34;&gt;&lt;span class=&#34;num&#34;&gt;260.29 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−9% rs · −8% rh&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-bench-foot&#34;&gt;At 64 KiB the AES-NI bulk encryption cost dominates and the framing delta is the residual, still a clean ~8% win against both competitors.&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;The ChaCha20 seal sweep mirrors the AES one but reaches further into the large-payload regime (up to 256 KiB) where the AEAD primitive dominates and all three libraries converge to the same ceiling. The interesting story is what happens &lt;em&gt;before&lt;/em&gt; convergence:&lt;/p&gt;
&lt;div class=&#34;hpke-bench reveal&#34;&gt;
&lt;div class=&#34;hpke-bench-head&#34;&gt;
&lt;h4&gt;Single-shot seal · X25519 + HKDF-SHA-256 + ChaCha20-Poly1305&lt;/h4&gt;
&lt;div class=&#34;hpke-bench-key&#34;&gt;&lt;span class=&#34;key key-rs&#34;&gt;&lt;span class=&#34;d&#34;&gt;&lt;/span&gt;hpke-rs&lt;/span&gt;&lt;span class=&#34;key key-rh&#34;&gt;&lt;span class=&#34;d&#34;&gt;&lt;/span&gt;rust-hpke&lt;/span&gt;&lt;span class=&#34;key key-ng&#34;&gt;&lt;span class=&#34;d&#34;&gt;&lt;/span&gt;hpke-ng&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-bench-body&#34;&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;16 B&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:99.6%&#34;&gt;&lt;span class=&#34;num&#34;&gt;44.40 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;44.59 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:68.3%&#34;&gt;&lt;span class=&#34;num&#34;&gt;30.46 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−31% rs · −32% rh&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;64 B&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:96.4%&#34;&gt;&lt;span class=&#34;num&#34;&gt;40.46 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;41.98 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:75.8%&#34;&gt;&lt;span class=&#34;num&#34;&gt;31.84 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−21% rs · −24% rh&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;256 B&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:94.1%&#34;&gt;&lt;span class=&#34;num&#34;&gt;38.95 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;41.38 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:72.6%&#34;&gt;&lt;span class=&#34;num&#34;&gt;30.03 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−23% rs · −27% rh&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;1 KiB&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:94.4%&#34;&gt;&lt;span class=&#34;num&#34;&gt;40.11 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;42.50 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:74.8%&#34;&gt;&lt;span class=&#34;num&#34;&gt;31.81 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−21% rs · −25% rh&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;4 KiB&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:95.0%&#34;&gt;&lt;span class=&#34;num&#34;&gt;44.70 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;47.03 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:76.2%&#34;&gt;&lt;span class=&#34;num&#34;&gt;35.85 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−20% rs · −24% rh&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;16 KiB&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:96.4%&#34;&gt;&lt;span class=&#34;num&#34;&gt;62.65 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;64.97 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:82.6%&#34;&gt;&lt;span class=&#34;num&#34;&gt;53.70 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−14% rs · −17% rh&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;64 KiB&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:97.3%&#34;&gt;&lt;span class=&#34;num&#34;&gt;135.19 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;138.98 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:90.8%&#34;&gt;&lt;span class=&#34;num&#34;&gt;126.17 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−7% rs · −9% rh&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;256 KiB&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;423.89 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:99.3%&#34;&gt;&lt;span class=&#34;num&#34;&gt;420.97 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:96.5%&#34;&gt;&lt;span class=&#34;num&#34;&gt;409.19 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−3% rs · tie rh&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-bench-foot&#34;&gt;At 256 KiB the ChaCha20-Poly1305 primitive both libraries call identically dominates the wall time and the deltas vanish into noise. At the primitive&#39;s ceiling, the framing has nothing left to remove.&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Once setup is paid, the post-setup hot path is just &lt;code&gt;Context::seal&lt;/code&gt; and &lt;code&gt;Context::open&lt;/code&gt;: nonce computation + AEAD primitive call, no KEM, no key schedule. We benchmark these in isolation by running sender and receiver contexts in lockstep outside the timed loop, then measuring seal-then-open on every iteration. This is where the framing-vs-primitive divide is sharpest:&lt;/p&gt;
&lt;div class=&#34;hpke-bench reveal&#34;&gt;
&lt;div class=&#34;hpke-bench-head&#34;&gt;
&lt;h4&gt;Post-setup hot path · &lt;code&gt;Context::seal&lt;/code&gt; · X25519+ChaCha20&lt;/h4&gt;
&lt;div class=&#34;hpke-bench-key&#34;&gt;&lt;span class=&#34;key key-rs&#34;&gt;&lt;span class=&#34;d&#34;&gt;&lt;/span&gt;hpke-rs&lt;/span&gt;&lt;span class=&#34;key key-rh&#34;&gt;&lt;span class=&#34;d&#34;&gt;&lt;/span&gt;rust-hpke&lt;/span&gt;&lt;span class=&#34;key key-ng&#34;&gt;&lt;span class=&#34;d&#34;&gt;&lt;/span&gt;hpke-ng&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-bench-body&#34;&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;64 B&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;252.51 ns&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:88.2%&#34;&gt;&lt;span class=&#34;num&#34;&gt;222.61 ns&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:86.2%&#34;&gt;&lt;span class=&#34;num&#34;&gt;217.77 ns&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−14% rs · tie rh&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;1 KiB&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;1.63 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:98.1%&#34;&gt;&lt;span class=&#34;num&#34;&gt;1.60 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:97.2%&#34;&gt;&lt;span class=&#34;num&#34;&gt;1.58 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-tie&#34;&gt;tie rs · tie rh&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;16 KiB&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:93.0%&#34;&gt;&lt;span class=&#34;num&#34;&gt;23.46 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;25.24 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:93.0%&#34;&gt;&lt;span class=&#34;num&#34;&gt;23.48 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-tie&#34;&gt;tie rs · −7% rh&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;64 KiB&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:95.5%&#34;&gt;&lt;span class=&#34;num&#34;&gt;93.48 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:99.8%&#34;&gt;&lt;span class=&#34;num&#34;&gt;97.70 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;97.94 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-flip&#34;&gt;+5% rs · tie rh&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-bench-foot&#34;&gt;At 64 B framing dominates and hpke-ng&#39;s stack-array nonce + monomorphized AEAD call wins 14% vs hpke-rs; at 64 KiB the AEAD primitive owns the wall time and hpke-rs&#39;s slightly tighter inner loop edges ahead by 5%, the only post-setup loss in the entire suite.&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-bench reveal&#34;&gt;
&lt;div class=&#34;hpke-bench-head&#34;&gt;
&lt;h4&gt;Post-setup hot path · &lt;code&gt;Context::open&lt;/code&gt; · X25519+ChaCha20&lt;/h4&gt;
&lt;div class=&#34;hpke-bench-key&#34;&gt;&lt;span class=&#34;key key-rs&#34;&gt;&lt;span class=&#34;d&#34;&gt;&lt;/span&gt;hpke-rs&lt;/span&gt;&lt;span class=&#34;key key-rh&#34;&gt;&lt;span class=&#34;d&#34;&gt;&lt;/span&gt;rust-hpke&lt;/span&gt;&lt;span class=&#34;key key-ng&#34;&gt;&lt;span class=&#34;d&#34;&gt;&lt;/span&gt;hpke-ng&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-bench-body&#34;&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;64 B&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;507.59 ns&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:90.7%&#34;&gt;&lt;span class=&#34;num&#34;&gt;460.40 ns&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:86.5%&#34;&gt;&lt;span class=&#34;num&#34;&gt;439.10 ns&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−14% rs · −5% rh&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;1 KiB&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;3.28 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:97.9%&#34;&gt;&lt;span class=&#34;num&#34;&gt;3.21 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:99.7%&#34;&gt;&lt;span class=&#34;num&#34;&gt;3.27 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-tie&#34;&gt;tie rs · tie rh&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;16 KiB&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;46.99 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:99.5%&#34;&gt;&lt;span class=&#34;num&#34;&gt;46.74 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:99.8%&#34;&gt;&lt;span class=&#34;num&#34;&gt;46.88 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-tie&#34;&gt;tie rs · tie rh&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;64 KiB&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:99.98%&#34;&gt;&lt;span class=&#34;num&#34;&gt;187.32 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:99.96%&#34;&gt;&lt;span class=&#34;num&#34;&gt;187.28 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;187.35 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-tie&#34;&gt;tie rs · tie rh&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-bench-foot&#34;&gt;Open&#39;s pattern mirrors seal: framing-dominant at 64 B (hpke-ng −14% vs hpke-rs), primitive-bound everywhere else (all three libraries hit the same ceiling within 0.04%).&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;The convergence at 64 KiB on both seal and open is the most diagnostic single result in the suite: all three libraries reach the same wall-clock cost of the underlying ChaCha20-Poly1305 primitive, because all three call the same code. hpke-ng matches that ceiling without overhead.&lt;/p&gt;
&lt;h3 id=&#34;export&#34;&gt;Export&lt;/h3&gt;
&lt;p&gt;The HPKE secret-export interface, &lt;code&gt;Context::export(ctx, len)&lt;/code&gt;, is a new addition to this round of benchmarks. It is also the largest single delta in the dataset by a wide margin. Five output lengths, ranging from 16 B (typical for keying a downstream symmetric cipher) to 256 B (room for several derived secrets), against hpke-rs only. rust-hpke&amp;rsquo;s &lt;code&gt;export()&lt;/code&gt; writes into a caller-supplied &lt;code&gt;&amp;amp;mut [u8]&lt;/code&gt; rather than returning a &lt;code&gt;Vec&lt;/code&gt;, so benchmarking it at these lengths would include a &lt;code&gt;Vec&lt;/code&gt; allocation in the timed path and bias the comparison; we omit it rather than ship a misleading number.&lt;/p&gt;
&lt;div class=&#34;hpke-bench reveal&#34;&gt;
&lt;div class=&#34;hpke-bench-head&#34;&gt;
&lt;h4&gt;Export · X25519 + HKDF-SHA-256 + ChaCha20-Poly1305 &lt;span class=&#34;dim&#34;&gt;(rust-hpke API-incompatible)&lt;/span&gt;&lt;/h4&gt;
&lt;div class=&#34;hpke-bench-key&#34;&gt;&lt;span class=&#34;key key-rs&#34;&gt;&lt;span class=&#34;d&#34;&gt;&lt;/span&gt;hpke-rs&lt;/span&gt;&lt;span class=&#34;key key-ng&#34;&gt;&lt;span class=&#34;d&#34;&gt;&lt;/span&gt;hpke-ng&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-bench-body&#34;&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;16 B&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;497.37 ns&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:23.5%&#34;&gt;&lt;span class=&#34;num&#34;&gt;117.09 ns&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−76% rs&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;32 B&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;500.27 ns&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:26.2%&#34;&gt;&lt;span class=&#34;num&#34;&gt;131.15 ns&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−74% rs&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;64 B&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;842.48 ns&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:26.6%&#34;&gt;&lt;span class=&#34;num&#34;&gt;223.93 ns&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−73% rs&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;128 B&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;1.52 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:26.8%&#34;&gt;&lt;span class=&#34;num&#34;&gt;406.16 ns&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−73% rs&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;256 B&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;2.87 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:28.1%&#34;&gt;&lt;span class=&#34;num&#34;&gt;806.43 ns&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−72% rs&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-bench-foot&#34;&gt;All five rows wins of &lt;strong&gt;72–76%&lt;/strong&gt;, the largest sustained delta in the suite. Export is on the small enough path that the per-call cost is dominated by what the library does *around* the HKDF call.&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;The −72% to −76% range is consistent across all five sizes, which tells you the win is structural, not size-dependent. hpke-ng&amp;rsquo;s &lt;code&gt;export&lt;/code&gt; is a labeled-HKDF call with a stack-allocated label and a single allocation for the output &lt;code&gt;Vec&lt;/code&gt;; hpke-rs&amp;rsquo;s path goes through the &lt;code&gt;HpkeCrypto&lt;/code&gt; trait, allocates intermediate buffers for the &lt;code&gt;LabeledExtract&lt;/code&gt; and &lt;code&gt;LabeledExpand&lt;/code&gt; arguments, and routes the HKDF call itself through the provider abstraction. That&amp;rsquo;s overhead that doesn&amp;rsquo;t appear in hpke-ng because the type system already knows the KDF at the call site.&lt;/p&gt;
&lt;h3 id=&#34;end-to-end-roundtrip&#34;&gt;End-to-end roundtrip&lt;/h3&gt;
&lt;p&gt;A single number for the eyes-glaze-over question of &amp;ldquo;how fast is the whole thing, end to end&amp;rdquo;: one 1 KiB seal-and-open against the same recipient keypair, full setup on each side every iteration, three libraries:&lt;/p&gt;
&lt;div class=&#34;hpke-bench reveal&#34;&gt;
&lt;div class=&#34;hpke-bench-head&#34;&gt;
&lt;h4&gt;End-to-end roundtrip · 1 KiB · X25519+ChaCha20&lt;/h4&gt;
&lt;div class=&#34;hpke-bench-key&#34;&gt;&lt;span class=&#34;key key-rs&#34;&gt;&lt;span class=&#34;d&#34;&gt;&lt;/span&gt;hpke-rs&lt;/span&gt;&lt;span class=&#34;key key-rh&#34;&gt;&lt;span class=&#34;d&#34;&gt;&lt;/span&gt;rust-hpke&lt;/span&gt;&lt;span class=&#34;key key-ng&#34;&gt;&lt;span class=&#34;d&#34;&gt;&lt;/span&gt;hpke-ng&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-bench-body&#34;&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;seal + open&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;77.31 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-rh&#34; style=&#34;--w:99.7%&#34;&gt;&lt;span class=&#34;num&#34;&gt;77.11 µs&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:69.7%&#34;&gt;&lt;span class=&#34;num&#34;&gt;53.90 µs&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−30% rs · −30% rh&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-bench-foot&#34;&gt;hpke-rs and rust-hpke land within 0.3% of each other, so the framing differences between them roughly cancel at this granularity. hpke-ng is a clean 30% faster than both.&lt;/div&gt;
&lt;/div&gt;
&lt;h2 id=&#34;memory&#34;&gt;Memory&lt;/h2&gt;
&lt;p&gt;Memory has two halves. The configuration and per-context state, where hpke-ng is uniformly smaller, and the per-key footprint, where the post-quantum KEMs introduce a deliberate tradeoff in the other direction. The memory comparison below is against hpke-rs only; rust-hpke&amp;rsquo;s &lt;code&gt;Context&lt;/code&gt; shape varies with its type parameters in ways that aren&amp;rsquo;t directly comparable to hpke-rs&amp;rsquo;s enum-keyed instance struct.&lt;/p&gt;
&lt;div class=&#34;hpke-mem reveal-stagger&#34;&gt;
&lt;div class=&#34;hpke-mem-row&#34;&gt;
&lt;div class=&#34;hpke-mem-head&#34;&gt;&lt;span class=&#34;type&#34;&gt;&lt;code&gt;sizeof(Hpke&amp;lt;K, F, A&amp;gt;)&lt;/code&gt;&lt;/span&gt;&lt;span class=&#34;delta&#34;&gt;−344 B (zero-sized)&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;rectpair&#34;&gt;&lt;span class=&#34;lib&#34;&gt;hpke-rs&lt;/span&gt;&lt;div class=&#34;rect rect-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;bytes&#34;&gt;344 B&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
&lt;div class=&#34;rectpair&#34;&gt;&lt;span class=&#34;lib&#34;&gt;hpke-ng&lt;/span&gt;&lt;div class=&#34;rect rect-ng rect-zero&#34;&gt;&lt;span class=&#34;bytes&#34;&gt;PhantomData · 0 B&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-mem-row&#34;&gt;
&lt;div class=&#34;hpke-mem-head&#34;&gt;&lt;span class=&#34;type&#34;&gt;&lt;code&gt;sizeof(Context&amp;lt;_, _, ChaCha20Poly1305&amp;gt;)&lt;/code&gt;&lt;/span&gt;&lt;span class=&#34;delta&#34;&gt;−312 B (−78%)&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;rectpair&#34;&gt;&lt;span class=&#34;lib&#34;&gt;hpke-rs&lt;/span&gt;&lt;div class=&#34;rect rect-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;bytes&#34;&gt;400 B&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
&lt;div class=&#34;rectpair&#34;&gt;&lt;span class=&#34;lib&#34;&gt;hpke-ng&lt;/span&gt;&lt;div class=&#34;rect rect-ng&#34; style=&#34;--w:22%&#34;&gt;&lt;span class=&#34;bytes&#34;&gt;88 B&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-mem-row&#34;&gt;
&lt;div class=&#34;hpke-mem-head&#34;&gt;&lt;span class=&#34;type&#34;&gt;&lt;code&gt;sizeof(Context&amp;lt;_, _, Aes128Gcm&amp;gt;)&lt;/code&gt;&lt;/span&gt;&lt;span class=&#34;delta&#34;&gt;+392 B (cached AES round keys + GHash)&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;rectpair&#34;&gt;&lt;span class=&#34;lib&#34;&gt;hpke-rs&lt;/span&gt;&lt;div class=&#34;rect rect-rs&#34; style=&#34;--w:50.5%&#34;&gt;&lt;span class=&#34;bytes&#34;&gt;400 B&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
&lt;div class=&#34;rectpair&#34;&gt;&lt;span class=&#34;lib&#34;&gt;hpke-ng&lt;/span&gt;&lt;div class=&#34;rect rect-ng&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;bytes&#34;&gt;792 B&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-mem-row&#34;&gt;
&lt;div class=&#34;hpke-mem-head&#34;&gt;&lt;span class=&#34;type&#34;&gt;&lt;code&gt;sizeof(Context&amp;lt;_, _, Aes256Gcm&amp;gt;)&lt;/code&gt;&lt;/span&gt;&lt;span class=&#34;delta&#34;&gt;+648 B (cached AES round keys + GHash)&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;rectpair&#34;&gt;&lt;span class=&#34;lib&#34;&gt;hpke-rs&lt;/span&gt;&lt;div class=&#34;rect rect-rs&#34; style=&#34;--w:38.2%&#34;&gt;&lt;span class=&#34;bytes&#34;&gt;400 B&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
&lt;div class=&#34;rectpair&#34;&gt;&lt;span class=&#34;lib&#34;&gt;hpke-ng&lt;/span&gt;&lt;div class=&#34;rect rect-ng&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;bytes&#34;&gt;1,048 B&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;code&gt;hpke-ng::Hpke&amp;lt;K, F, A&amp;gt;&lt;/code&gt; is &lt;code&gt;PhantomData&amp;lt;(K, F, A)&amp;gt;&lt;/code&gt;. There is no runtime presence; it costs zero bytes and &lt;code&gt;cargo expand&lt;/code&gt; confirms the compiler optimizes it out completely. &lt;code&gt;hpke-rs::Hpke&amp;lt;Crypto&amp;gt;&lt;/code&gt; carries a 256-byte ChaCha20 PRNG plus four enum discriminants and padding (344 bytes measured at the time of writing).&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Context&lt;/code&gt; is where hpke-ng&amp;rsquo;s per-AEAD specialization shows up. With &lt;code&gt;ChaCha20Poly1305&lt;/code&gt; the cipher state is just the 32-byte key, so &lt;code&gt;Context&lt;/code&gt; is 88 bytes, under a quarter of hpke-rs&amp;rsquo;s 400, since hpke-rs&amp;rsquo;s &lt;code&gt;Context&lt;/code&gt; carries a per-instance PRNG plus trait-object overhead that hpke-ng&amp;rsquo;s monomorphized design doesn&amp;rsquo;t need. With AES-GCM the trade goes the other way: hpke-ng caches the expanded round keys plus the precomputed GHash table inline so that &lt;code&gt;Context::seal&lt;/code&gt; doesn&amp;rsquo;t pay key-schedule expansion on every call. That is why AES-128 single-shot seal is 6–22% faster across the sweep. Streaming AES applications get the throughput; ChaCha20 deployments stay at the small footprint.&lt;/p&gt;
&lt;p&gt;Practical impact: an application that holds a thousand long-lived ChaCha20 contexts, such as a server with persistent client sessions, a relay, or MLS group state, saves ~310 KB of resident memory over hpke-rs. An AES-128-GCM deployment with the same shape pays ~390 KB of additional context state for the per-call seal-side speedup. Whether that&amp;rsquo;s a good trade is application-specific, and it&amp;rsquo;s now an explicit choice the type system makes visible.&lt;/p&gt;
&lt;h3 id=&#34;the-post-quantum-and-pk-cache-tradeoff-hpke-ng-private-keys-are-larger-across-the-board&#34;&gt;The post-quantum and PK-cache tradeoff: hpke-ng private keys are larger across the board&lt;/h3&gt;
&lt;p&gt;Per-key footprint is where the speed/memory trade lands explicitly. Every KEM private key in hpke-ng now caches material that hpke-rs reconstructs from raw bytes on demand: the recipient&amp;rsquo;s serialized public key for the DH-KEMs (so &lt;code&gt;decap&lt;/code&gt; doesn&amp;rsquo;t recompute it via base-point scalar multiplication), the expanded &lt;code&gt;x_wing::DecapsulationKey&lt;/code&gt; for X-Wing (so &lt;code&gt;decap&lt;/code&gt; doesn&amp;rsquo;t re-run SHAKE-256 + ML-KEM-768 keygen), and the materialized FIPS 203 decapsulation key for ML-KEM (same trick at the parameter-set boundary). The size impact is uniform and easy to reason about:&lt;/p&gt;
&lt;div class=&#34;hpke-bench reveal&#34;&gt;
&lt;div class=&#34;hpke-bench-head&#34;&gt;
&lt;h4&gt;KEM private key footprint · stack + heap, in bytes&lt;/h4&gt;
&lt;div class=&#34;hpke-bench-key&#34;&gt;&lt;span class=&#34;key key-rs&#34;&gt;&lt;span class=&#34;d&#34;&gt;&lt;/span&gt;hpke-rs&lt;/span&gt;&lt;span class=&#34;key key-ng&#34;&gt;&lt;span class=&#34;d&#34;&gt;&lt;/span&gt;hpke-ng&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-bench-body&#34;&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;X25519&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:63.6%&#34;&gt;&lt;span class=&#34;num&#34;&gt;56 B&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;88 B&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-flip&#34;&gt;+32 B&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;P-256&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:46.3%&#34;&gt;&lt;span class=&#34;num&#34;&gt;56 B&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;121 B&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-flip&#34;&gt;+65 B&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;X-Wing&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:3.3%&#34;&gt;&lt;span class=&#34;num&#34;&gt;56 B&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;1,698 B&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-flip&#34;&gt;+1,642 B&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;ML-KEM-768&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:2.7%&#34;&gt;&lt;span class=&#34;num&#34;&gt;88 B&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;3,266 B&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-flip&#34;&gt;+3,178 B&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-bench-row&#34;&gt;&lt;span class=&#34;payload&#34;&gt;ML-KEM-1024&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:2.1%&#34;&gt;&lt;span class=&#34;num&#34;&gt;88 B&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;4,290 B&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-flip&#34;&gt;+4,202 B&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-bench-foot&#34;&gt;Every row spends extra memory at private-key construction in exchange for not paying the same work on every subsequent &lt;code&gt;decap&lt;/code&gt;. That is the explicit memory cost of the −37% to −55% decap deltas.&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;So the trade is concrete: an extra 32–65 B per DH private key, ~1.7 KB extra per X-Wing private key, ~3.2 KB extra per ML-KEM-768 private key, and ~4.2 KB extra per ML-KEM-1024 private key. In exchange the recipient skips a base-point scalar mult per DH &lt;code&gt;decap&lt;/code&gt;, a SHAKE-256 + ML-KEM-768 keygen per X-Wing &lt;code&gt;decap&lt;/code&gt;, and a FIPS 203 KeyGen_internal per ML-KEM &lt;code&gt;decap&lt;/code&gt;. For a server pinning a few thousand long-lived receiver keys this is good arithmetic in essentially every setting we can think of, a possibly-bad trade on a microcontroller pinning very few keys, and we&amp;rsquo;d rather you have the numbers than guess.&lt;/p&gt;
&lt;p&gt;Public keys grow in the same direction on the post-quantum side, because hpke-ng now caches the parsed &lt;code&gt;EncapsulationKey&lt;/code&gt; alongside the wire bytes so &lt;code&gt;encap&lt;/code&gt; doesn&amp;rsquo;t re-decode the 1,184/1,568-byte payload on every call. X-Wing public keys are 1,656 B, ML-KEM-768 public keys are 1,624 B, ML-KEM-1024 public keys are 2,136 B (vs hpke-rs&amp;rsquo;s &lt;code&gt;Vec&amp;lt;u8&amp;gt;&lt;/code&gt; of just the wire bytes). DH public keys are unchanged: 32 B for X25519, 96 B for P-256 (uncompressed encoded point form).&lt;/p&gt;
&lt;h2 id=&#34;smaller&#34;&gt;Smaller&lt;/h2&gt;
&lt;div class=&#34;hpke-shrink reveal&#34;&gt;
&lt;div class=&#34;hpke-shrink-head&#34;&gt;&lt;h4&gt;Project surface area&lt;/h4&gt;&lt;div class=&#34;hpke-bench-key&#34;&gt;&lt;span class=&#34;key key-rs&#34;&gt;&lt;span class=&#34;d&#34;&gt;&lt;/span&gt;hpke-rs&lt;/span&gt;&lt;span class=&#34;key key-ng&#34;&gt;&lt;span class=&#34;d&#34;&gt;&lt;/span&gt;hpke-ng&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-shrink-body&#34;&gt;
&lt;div class=&#34;hpke-shrink-row&#34;&gt;&lt;span class=&#34;metric&#34;&gt;End-user binary (stripped, release)&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;561 KB&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:69.8%&#34;&gt;&lt;span class=&#34;num&#34;&gt;392 KB&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−30%&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-shrink-row&#34;&gt;&lt;span class=&#34;metric&#34;&gt;Total project code (cloc)&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;5,631&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:85.5%&#34;&gt;&lt;span class=&#34;num&#34;&gt;4,817&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−14%&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-shrink-row&#34;&gt;&lt;span class=&#34;metric&#34;&gt;Library source (cloc)&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;2,623&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:92.5%&#34;&gt;&lt;span class=&#34;num&#34;&gt;2,426&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−8%&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-shrink-row&#34;&gt;&lt;span class=&#34;metric&#34;&gt;Test code (cloc)&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;2,230&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:50.4%&#34;&gt;&lt;span class=&#34;num&#34;&gt;1,124&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−50%&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-shrink-row&#34;&gt;&lt;span class=&#34;metric&#34;&gt;Bench code (cloc)&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:44.6%&#34;&gt;&lt;span class=&#34;num&#34;&gt;759&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;~1,700&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-flip&#34;&gt;+124%&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-shrink-row&#34;&gt;&lt;span class=&#34;metric&#34;&gt;Crates in workspace&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;4&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:25%&#34;&gt;&lt;span class=&#34;num&#34;&gt;1&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−75%&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-shrink-row&#34;&gt;&lt;span class=&#34;metric&#34;&gt;User-facing Cargo features&lt;/span&gt;&lt;div class=&#34;bars&#34;&gt;&lt;div class=&#34;bar bar-rs&#34; style=&#34;--w:100%&#34;&gt;&lt;span class=&#34;num&#34;&gt;11&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;bar bar-ng&#34; style=&#34;--w:63.6%&#34;&gt;&lt;span class=&#34;num&#34;&gt;7&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;&lt;span class=&#34;delta delta-win&#34;&gt;−36%&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Expanding on the above:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;End-user binary, −30%.&lt;/strong&gt; A minimal application (generate a key, seal a message, open it back, ten lines of Rust) compiled with &lt;code&gt;RUSTFLAGS=&amp;quot;-C target-cpu=native&amp;quot;&lt;/code&gt;, &lt;code&gt;lto=&amp;quot;thin&amp;quot;&lt;/code&gt;, &lt;code&gt;codegen-units=1&lt;/code&gt;, &lt;code&gt;strip=&amp;quot;symbols&amp;quot;&lt;/code&gt;. hpke-ng comes in at 392 KB; hpke-rs at 561 KB. 169 KB is not nothing on embedded targets, in WASM bundles, or in CDN-served binaries.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Library code, −8%.&lt;/strong&gt; hpke-ng is 197 lines smaller than hpke-rs at the library level (2,426 vs 2,623), while implementing strictly more: the full HPKE surface plus the optional post-quantum suite (X-Wing draft-06, ML-KEM-768, ML-KEM-1024) that hpke-rs reaches only via experimental feature flags. The type-state design earns its keep here: ciphersuite selection lives entirely in the type system, so there is no provider trait, no per-primitive enum dispatch, and no glue between the two. Inline &lt;code&gt;#[cfg(test)]&lt;/code&gt; modules are kept tight: anything covered by &lt;code&gt;tests/roundtrip.rs&lt;/code&gt;&amp;rsquo;s 59-cell macro matrix is deleted from &lt;code&gt;src/&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Test code, −50%.&lt;/strong&gt; hpke-rs&amp;rsquo;s test suite is 167 tests in 2,230 lines; hpke-ng&amp;rsquo;s is 128 tests in 1,124 lines, with deeper coverage on roundtrips (59 macro-generated &lt;code&gt;(mode, KEM, KDF, AEAD)&lt;/code&gt; combinations vs hpke-rs&amp;rsquo;s 17 hand-written cases). The reduction is structural. The type system carries information that would otherwise be repeated test setup, and the &lt;code&gt;roundtrip!&lt;/code&gt; macro generates one test per supported configuration from a single declaration.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Bench code, +124%.&lt;/strong&gt; This row runs against the section&amp;rsquo;s grain, and it&amp;rsquo;s deliberate. hpke-ng&amp;rsquo;s bench harness has more than doubled, all of it concentrated in a new &lt;code&gt;comparative.rs&lt;/code&gt; that loads hpke-rs &lt;em&gt;and&lt;/em&gt; rust-hpke as dev-dependencies and benchmarks every supported ciphersuite against both, including the post-quantum suite that hpke-rs ships only behind an experimental feature flag and that rust-hpke doesn&amp;rsquo;t ship at all. That single-source-of-truth harness is what made the 126-row tally at the top of this post possible. Coverage, not waste.&lt;/p&gt;
&lt;p&gt;The one place this chart doesn&amp;rsquo;t reach is the fuzz harnesses. hpke-ng spends substantially &lt;em&gt;more&lt;/em&gt; code on cargo-fuzz targets than hpke-rs does, deliberately so. That&amp;rsquo;s the next section.&lt;/p&gt;
&lt;h2 id=&#34;harder&#34;&gt;Harder&lt;/h2&gt;
&lt;div class=&#34;hpke-harden reveal&#34;&gt;
&lt;div class=&#34;hpke-panel-bar&#34;&gt;
&lt;div class=&#34;left&#34;&gt;&lt;div class=&#34;traffic&#34;&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;span&gt;&lt;/span&gt;&lt;/div&gt;&lt;span class=&#34;filename&#34;&gt;cargo test · &lt;span class=&#34;stem&#34;&gt;--features pq,kat-internals,differential&lt;/span&gt;&lt;/span&gt;&lt;/div&gt;
&lt;span class=&#34;status&#34;&gt;&lt;span class=&#34;pulse&#34;&gt;&lt;/span&gt;128 / 128 passing&lt;/span&gt;
&lt;/div&gt;
&lt;div class=&#34;hpke-harden-grid&#34;&gt;
&lt;div class=&#34;harden-stats&#34;&gt;
&lt;div class=&#34;stat&#34;&gt;&lt;div class=&#34;big&#34;&gt;&lt;span data-counter-target=&#34;128&#34; data-counter-from=&#34;0&#34;&gt;128&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;label&#34;&gt;tests passing&lt;/div&gt;&lt;/div&gt;
&lt;div class=&#34;stat&#34;&gt;&lt;div class=&#34;big&#34;&gt;1.9&lt;span class=&#34;unit&#34;&gt;s&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;label&#34;&gt;total wall time&lt;/div&gt;&lt;/div&gt;
&lt;div class=&#34;stat&#34;&gt;&lt;div class=&#34;big&#34;&gt;&lt;span data-counter-target=&#34;37&#34; data-counter-from=&#34;0&#34;&gt;37&lt;/span&gt;&lt;span class=&#34;unit&#34;&gt;×&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;label&#34;&gt;faster than hpke-rs&#39;s KAT runner&lt;/div&gt;&lt;/div&gt;
&lt;div class=&#34;stat&#34;&gt;&lt;div class=&#34;big&#34;&gt;4 &lt;span class=&#34;of&#34;&gt;/ 1&lt;/span&gt;&lt;/div&gt;&lt;div class=&#34;label&#34;&gt;cargo-fuzz targets &lt;span class=&#34;dim&#34;&gt;(hpke-ng / hpke-rs)&lt;/span&gt;&lt;/div&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;harden-dots&#34; data-total=&#34;128&#34;&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&#34;harden-categories&#34;&gt;
&lt;div class=&#34;cat-line&#34;&gt;&lt;span class=&#34;name&#34;&gt;unit (lib)&lt;/span&gt;&lt;span class=&#34;v&#34;&gt;46/46&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;cat-line&#34;&gt;&lt;span class=&#34;name&#34;&gt;RFC 9180 KAT&lt;/span&gt;&lt;span class=&#34;v&#34;&gt;13/13&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;cat-line&#34;&gt;&lt;span class=&#34;name&#34;&gt;roundtrip matrix&lt;/span&gt;&lt;span class=&#34;v&#34;&gt;59/59&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;cat-line&#34;&gt;&lt;span class=&#34;name&#34;&gt;differential vs hpke-rs&lt;/span&gt;&lt;span class=&#34;v&#34;&gt;8/8&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;cat-line&#34;&gt;&lt;span class=&#34;name&#34;&gt;doctests&lt;/span&gt;&lt;span class=&#34;v&#34;&gt;2/2&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;cat-line&#34;&gt;&lt;span class=&#34;name&#34;&gt;cargo-fuzz targets&lt;/span&gt;&lt;span class=&#34;v&#34;&gt;4 / 4 clean&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;The 1.9-second figure is the headline number. The full hpke-ng test matrix runs in about 1.9 seconds: 128 tests across library unit tests, RFC 9180 KAT, generative roundtrips over every ciphersuite × mode combination, and byte-by-byte differential vs hpke-rs. The roundtrip layer alone is 59 macro-generated tests covering every supported (mode, KEM, KDF, AEAD) combination including all four post-quantum and X-Wing/ML-KEM rows. hpke-rs&amp;rsquo;s KAT runner is structured as a single test that iterates 144 vectors sequentially and takes about 70 seconds. Same vectors, same coverage, structured to take advantage of &lt;code&gt;cargo test&lt;/code&gt;&amp;rsquo;s thread pool.&lt;/p&gt;
&lt;p&gt;For day-to-day development the headline number understates the impact. A 70-second feedback loop is one you avoid running until you&amp;rsquo;re &amp;ldquo;done&amp;rdquo;; a 1.9-second feedback loop is one you run after every save.&lt;/p&gt;
&lt;p&gt;The fuzz layer is where hpke-ng makes its biggest investment in lines of code. hpke-rs ships one cargo-fuzz target, a seal/open harness for one ciphersuite. hpke-ng ships four:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;pk_from_bytes&lt;/code&gt;&lt;/strong&gt; — fuzzes public-key parsing for &lt;strong&gt;all 9 KEMs&lt;/strong&gt; (X25519, X448, P-256, P-384, P-521, secp256k1, X-Wing, ML-KEM-768, ML-KEM-1024).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;enc_from_bytes&lt;/code&gt;&lt;/strong&gt; — fuzzes encapsulated-key parsing for all 9 KEMs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;key_schedule&lt;/code&gt;&lt;/strong&gt; — fuzzes the internal key schedule with arbitrary mode bytes (including invalid &lt;code&gt;0x04..=0xFF&lt;/code&gt; mode values), arbitrary PSK / PSK-ID combinations, and arbitrary shared secrets.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;open&lt;/code&gt;&lt;/strong&gt; — fuzzes &lt;code&gt;Hpke::open_base&lt;/code&gt; with arbitrary &lt;code&gt;[encap || ciphertext]&lt;/code&gt; byte splits against a fixed receiver keypair.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The shared invariant across all four is that &lt;em&gt;panics are bugs&lt;/em&gt;. Authentication failures, decode errors and length mismatches are all expected outcomes that the harness considers a successful run. A panic, a misaligned-pointer fault, or a debug-assertion failure under cargo-fuzz&amp;rsquo;s instrumentation is a finding. As of release, all four targets run clean.&lt;/p&gt;
&lt;p&gt;There are also several structural footgun-prevention details that don&amp;rsquo;t show up in the fuzz output but are worth listing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;Context&lt;/code&gt; is not &lt;code&gt;Clone&lt;/code&gt;.&lt;/strong&gt; Cloning an HPKE context lets two callers reuse the same &lt;code&gt;(key, base_nonce, seq)&lt;/code&gt; triple, which is a nonce-reuse bug that produces correct-looking output right up until an attacker XORs two ciphertexts together. hpke-ng&amp;rsquo;s &lt;code&gt;Context&lt;/code&gt; deliberately doesn&amp;rsquo;t implement &lt;code&gt;Clone&lt;/code&gt;; cloning is a compile error.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;Context::seal&lt;/code&gt; refuses to encrypt at &lt;code&gt;seq == u64::MAX&lt;/code&gt;.&lt;/strong&gt; A pre-check, before nonce computation. This makes nonce-reuse via counter wraparound structurally impossible regardless of how the caller handles a &lt;code&gt;MessageLimitReached&lt;/code&gt; error.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;All-zero shared-secret rejection (RFC 9180 §7.1.4)&lt;/strong&gt; uses &lt;code&gt;subtle::ConstantTimeEq&lt;/code&gt; for X25519 and X448. An attacker who supplies a small-order point and watches for timing variance is one of the more subtle attacks on DHKEM; the constant-time comparison closes it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AEAD nonce length is enforced at compile time.&lt;/strong&gt; &lt;code&gt;Context::compute_nonce&lt;/code&gt; uses a const assertion that the AEAD&amp;rsquo;s &lt;code&gt;NONCE_LEN&lt;/code&gt; is between 8 and 12 bytes. Any AEAD that violates this is a compile error at the call site that uses it.&lt;/p&gt;
&lt;h2 id=&#34;interop&#34;&gt;Interop&lt;/h2&gt;
&lt;p&gt;We tested interop two ways. The first is RFC 9180 known-answer tests: both libraries are run against the same vendored test vector JSON (8 MB, derived from RFC 9180&amp;rsquo;s own test vector tooling) and required to produce byte-equal &lt;code&gt;key&lt;/code&gt;, &lt;code&gt;base_nonce&lt;/code&gt;, &lt;code&gt;exporter_secret&lt;/code&gt;, decrypted ciphertexts, and exported values for every vector. The second is byte-by-byte differential testing: a deterministic &lt;code&gt;ChaCha20Rng&lt;/code&gt; feeds identical inputs to both libraries; hpke-ng plays sender, hpke-rs plays receiver, and every byte that crosses the wire is asserted equal. Roughly 600 byte-equality assertions per CI run, all passing.&lt;/p&gt;
&lt;div class=&#34;hpke-interop reveal&#34;&gt;
&lt;div class=&#34;hpke-interop-head&#34;&gt;&lt;span class=&#34;suite-h&#34;&gt;Ciphersuite&lt;/span&gt;&lt;span class=&#34;cell-h&#34;&gt;RFC 9180 KAT&lt;/span&gt;&lt;span class=&#34;cell-h&#34;&gt;Differential vs hpke-rs&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-interop-row&#34;&gt;&lt;span class=&#34;suite&#34;&gt;DHKEM(X25519, SHA-256) × ChaCha20-Poly1305&lt;/span&gt;&lt;span class=&#34;cell pass&#34;&gt;✓ Base / Psk / Auth / AuthPsk&lt;/span&gt;&lt;span class=&#34;cell pass&#34;&gt;✓ Base + Psk&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-interop-row&#34;&gt;&lt;span class=&#34;suite&#34;&gt;DHKEM(X25519, SHA-256) × AES-128-GCM&lt;/span&gt;&lt;span class=&#34;cell pass&#34;&gt;✓ Base / Psk&lt;/span&gt;&lt;span class=&#34;cell pass&#34;&gt;✓ Base&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-interop-row&#34;&gt;&lt;span class=&#34;suite&#34;&gt;DHKEM(X25519, SHA-256) × AES-256-GCM&lt;/span&gt;&lt;span class=&#34;cell pass&#34;&gt;✓ Base / Psk&lt;/span&gt;&lt;span class=&#34;cell pass&#34;&gt;✓ Base&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-interop-row&#34;&gt;&lt;span class=&#34;suite&#34;&gt;DHKEM(X25519, SHA-256) × ExportOnly&lt;/span&gt;&lt;span class=&#34;cell pass&#34;&gt;✓ Base / Psk&lt;/span&gt;&lt;span class=&#34;cell na&#34;&gt;— no AEAD&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-interop-row&#34;&gt;&lt;span class=&#34;suite&#34;&gt;DHKEM(P-256, SHA-256) × ChaCha20-Poly1305&lt;/span&gt;&lt;span class=&#34;cell pass&#34;&gt;✓ Base / Psk&lt;/span&gt;&lt;span class=&#34;cell pass&#34;&gt;✓ via KAT&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-interop-row&#34;&gt;&lt;span class=&#34;suite&#34;&gt;DHKEM(P-256, SHA-256) × AES-128-GCM&lt;/span&gt;&lt;span class=&#34;cell pass&#34;&gt;✓ Base / Psk / Auth / AuthPsk&lt;/span&gt;&lt;span class=&#34;cell pass&#34;&gt;✓ Base + Psk&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-interop-row&#34;&gt;&lt;span class=&#34;suite&#34;&gt;DHKEM(P-521, SHA-512) × AES-256-GCM&lt;/span&gt;&lt;span class=&#34;cell pass&#34;&gt;✓ Base / Psk / Auth / AuthPsk&lt;/span&gt;&lt;span class=&#34;cell na&#34;&gt;— hpke-rs/RustCrypto unsupported&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-interop-row&#34;&gt;&lt;span class=&#34;suite&#34;&gt;DHKEM(secp256k1, SHA-256) × ChaCha20-Poly1305&lt;/span&gt;&lt;span class=&#34;cell pass&#34;&gt;✓ Base / Psk&lt;/span&gt;&lt;span class=&#34;cell pass&#34;&gt;✓ via KAT&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-interop-row&#34;&gt;&lt;span class=&#34;suite&#34;&gt;DHKEM(X448, SHA-512) × ChaCha20-Poly1305&lt;/span&gt;&lt;span class=&#34;cell pass&#34;&gt;✓ Base / Psk&lt;/span&gt;&lt;span class=&#34;cell na&#34;&gt;— hpke-rs/RustCrypto unsupported&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-interop-row&#34;&gt;&lt;span class=&#34;suite&#34;&gt;ML-KEM-768 × ChaCha20-Poly1305&lt;/span&gt;&lt;span class=&#34;cell na&#34;&gt;— no RFC vectors&lt;/span&gt;&lt;span class=&#34;cell na&#34;&gt;— seed-derivation differs by design&lt;/span&gt;&lt;/div&gt;
&lt;div class=&#34;hpke-interop-row&#34;&gt;&lt;span class=&#34;suite&#34;&gt;X-Wing draft-06 × ChaCha20-Poly1305&lt;/span&gt;&lt;span class=&#34;cell na&#34;&gt;— no RFC vectors&lt;/span&gt;&lt;span class=&#34;cell na&#34;&gt;— seed-derivation differs by design&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Some gaps worth covering, for full disclosure:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Auth and AuthPsk differential.&lt;/strong&gt; hpke-rs&amp;rsquo;s &lt;code&gt;seed()&lt;/code&gt; injects raw bytes for the base ephemeral; for Auth modes there is also a sender static keypair derived earlier, before any seed-injection happens. Aligning the two libraries&amp;rsquo; state for byte-by-byte Auth-mode differential testing would require a deeper hpke-rs API hook than &lt;code&gt;hpke-test-prng&lt;/code&gt; exposes. Auth/AuthPsk-mode interop is verified at the KAT layer instead. Both libraries pass the X25519+ChaCha20 Auth/AuthPsk vectors, the P-256+AES-128 vectors, and the P-521+AES-256 vectors.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Post-quantum differential.&lt;/strong&gt; hpke-rs&amp;rsquo;s X-Wing and ML-KEM implementations use different SHAKE-256 seeding from hpke-ng&amp;rsquo;s RFC 9180 §7.1.3-compliant &lt;code&gt;derive_key_pair&lt;/code&gt; construction, so the libraries produce different ephemeral keys from the same IKM bytes. The encap wire format itself is determined by the underlying KEM crate (which both use), so they should agree at the wire level. We haven&amp;rsquo;t tested it in this repository.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;rust-hpke differential.&lt;/strong&gt; Not currently wired up. rust-hpke&amp;rsquo;s PRNG is a generic &lt;code&gt;CryptoRng + RngCore&lt;/code&gt; parameter and is straightforward to align with a deterministic ChaCha20Rng, so adding a third differential leg is on the to-do list. As of this release, interop with rust-hpke is verified only at the speed-bench layer (each library opens what hpke-ng sealed, ad hoc, in unit tests).&lt;/p&gt;
&lt;h2 id=&#34;migrate-today&#34;&gt;Migrate today&lt;/h2&gt;
&lt;p&gt;All three libraries pass the same RFC 9180 KATs against the same primitive crates. If your code uses HPKE through a small wrapper, which most production HPKE code does, switching is mechanical:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// hpke-rs
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;mut&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;hpke&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Hpke&lt;/span&gt;::&lt;span class=&#34;o&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;HpkeRustCrypto&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;gt;&lt;/span&gt;::&lt;span class=&#34;n&#34;&gt;new&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Mode&lt;/span&gt;::&lt;span class=&#34;n&#34;&gt;Base&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;KemAlgorithm&lt;/span&gt;::&lt;span class=&#34;n&#34;&gt;DhKem25519&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;KdfAlgorithm&lt;/span&gt;::&lt;span class=&#34;n&#34;&gt;HkdfSha256&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;AeadAlgorithm&lt;/span&gt;::&lt;span class=&#34;n&#34;&gt;ChaCha20Poly1305&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;kp&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;hpke&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;generate_key_pair&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;?&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;sk&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pk&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;kp&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;into_keys&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;();&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;enc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ct&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;hpke&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;seal&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pk&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;info&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;aad&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;None&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;None&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;None&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;?&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// rust-hpke
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;sk&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pk&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;X25519HkdfSha256&lt;/span&gt;::&lt;span class=&#34;n&#34;&gt;gen_keypair&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;mut&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;rng&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;enc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;mut&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ctx&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;hpke&lt;/span&gt;::&lt;span class=&#34;n&#34;&gt;setup_sender&lt;/span&gt;::&lt;span class=&#34;o&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ChaCha20Poly1305&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;HkdfSha256&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;X25519HkdfSha256&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;_&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;OpModeS&lt;/span&gt;::&lt;span class=&#34;n&#34;&gt;Base&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pk&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;info&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;mut&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;rng&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;?&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ct&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ctx&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;seal&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;aad&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;?&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// hpke-ng
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;type&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;Suite&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Hpke&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;DhKemX25519HkdfSha256&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;HkdfSha256&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ChaCha20Poly1305&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;mut&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;os&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;OsRng&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;mut&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;rng&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;os&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;unwrap_mut&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;();&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;sk&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pk&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;DhKemX25519HkdfSha256&lt;/span&gt;::&lt;span class=&#34;n&#34;&gt;generate&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;mut&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;rng&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;?&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;enc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ct&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Suite&lt;/span&gt;::&lt;span class=&#34;n&#34;&gt;seal_base&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;mut&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;rng&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pk&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;info&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;aad&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;?&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The most common migration shape: define a &lt;code&gt;type Suite = Hpke&amp;lt;…, …, …&amp;gt;&lt;/code&gt; alias once, change &lt;code&gt;hpke.seal&lt;/code&gt; calls to &lt;code&gt;Suite::seal_base&lt;/code&gt; (or &lt;code&gt;seal_psk&lt;/code&gt; / &lt;code&gt;seal_auth&lt;/code&gt; / &lt;code&gt;seal_auth_psk&lt;/code&gt; per mode), thread an &lt;code&gt;&amp;amp;mut rng&lt;/code&gt; through the call sites that need encap entropy, drop the &lt;code&gt;Option&lt;/code&gt; placeholders. The rust-hpke → hpke-ng shape is even closer: keep the type-state ciphersuite alias, swap module paths, and replace the per-iteration &lt;code&gt;setup_sender&lt;/code&gt; with the appropriate &lt;code&gt;Suite::seal_*&lt;/code&gt; if you only ever encrypt one message per recipient.&lt;/p&gt;
&lt;h2 id=&#34;get-it&#34;&gt;Get it&lt;/h2&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-toml&#34; data-lang=&#34;toml&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;dependencies&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;hpke-ng&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;0.1.0-rc.3&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href=&#34;https://github.com/symbolicsoft/hpke-ng&#34;&gt;github.com/symbolicsoft/hpke-ng&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;docs.rs:&lt;/strong&gt; &lt;a href=&#34;https://docs.rs/hpke-ng&#34;&gt;docs.rs/hpke-ng&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;License:&lt;/strong&gt; Apache-2.0 OR MIT&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MSRV:&lt;/strong&gt; Rust 1.95 (edition 2024)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you find a row in our benchmark suite that&amp;rsquo;s wrong, an interop gap we haven&amp;rsquo;t documented, or a footgun we missed, file an issue. We&amp;rsquo;d rather know.&lt;/p&gt;
&lt;script&gt;
(function () {
  var grids = document.querySelectorAll(&#39;.harden-dots&#39;);
  grids.forEach(function (grid) {
    var total = Number(grid.dataset.total) || 91;
    for (var i = 0; i &lt; total; i++) {
      var d = document.createElement(&#39;span&#39;);
      d.className = &#39;hpke-harden-dot&#39;;
      d.style.animationDelay = (i * 14 / 1000).toFixed(3) + &#39;s&#39;;
      grid.appendChild(d);
    }
  });
  document.querySelectorAll(&#39;.strip-dots&#39;).forEach(function (strip) {
    var w = +strip.dataset.wins || 0;
    var t = +strip.dataset.ties || 0;
    var l = +strip.dataset.losses || 0;
    var add = function (cls, idx) {
      var d = document.createElement(&#39;span&#39;);
      d.className = &#39;hpke-score-dot &#39; + cls;
      d.style.animationDelay = (idx * 12 / 1000).toFixed(3) + &#39;s&#39;;
      strip.appendChild(d);
    };
    for (var i = 0; i &lt; w; i++) add(&#39;win&#39;, i);
    for (var i = 0; i &lt; t; i++) add(&#39;tie&#39;, w + i);
    for (var i = 0; i &lt; l; i++) add(&#39;loss&#39;, w + t + i);
  });
})();
&lt;/script&gt;
</content:encoded>
      <category>Software</category>
      <category>Research</category>
      <category>HPKE</category>
      <category>Rust</category>
      <category>Post-Quantum</category>
      
    </item>
    
    <item>
      <title>Announcing the Post-Quantum Migration Playbook</title>
      <link>https://symbolic.software/blog/2026-05-06-pq-migration-playbook/</link>
      <pubDate>Wed, 06 May 2026 00:00:00 &#43;0000</pubDate>
      <guid>https://symbolic.software/blog/2026-05-06-pq-migration-playbook/</guid>
      <description>A 52-page practitioner guide for engineers and architects working on post-quantum migration, alongside an interactive scorecard and TLS scanner at pq-migration.symbolic.software.</description>
      <content:encoded>&lt;p&gt;We get the same questions in almost every engagement now. &lt;em&gt;Which post-quantum primitive should we pick? When do we hybridize, and when do we stop? How do we migrate TLS without breaking production? Which library can we trust? What should we test for?&lt;/em&gt; The answers have stabilized enough that writing them down once feels more useful than reciting them in another kickoff call.&lt;/p&gt;
&lt;p&gt;So we did. The &lt;strong&gt;&lt;a href=&#34;https://pq-migration.symbolic.software/static/pdf/playbook.pdf&#34;&gt;Post-Quantum Migration Playbook&lt;/a&gt;&lt;/strong&gt; is a 52-page guide for the engineer or architect who has been told their system needs to be &amp;ldquo;post-quantum ready&amp;rdquo; and is now trying to figure out what that actually means in practice. It is organized around decisions rather than theory. Each chapter is short, scoped to a single migration concern, and ends with our recommendation and the contingency it depends on. Skim the &lt;strong&gt;TL;DR&lt;/strong&gt; boxes if you only have ten minutes; the &lt;strong&gt;Pitfall&lt;/strong&gt; boxes for mistakes we watch teams stumble into; the &lt;strong&gt;From the audit floor&lt;/strong&gt; boxes for the bug classes we have actually found in production code, anonymized but real.&lt;/p&gt;
&lt;p&gt;The topics are what you would expect: choosing primitives, when hybrid constructions are worth their cost (KEMs yes, signatures usually no), TLS and PKI migration, secure messaging, which libraries are worth using, conformance testing with &lt;a href=&#34;https://symbolic.software/blog/2026-03-23-crucible/&#34;&gt;Crucible&lt;/a&gt;, rollout strategy, and a closing gallery of bug classes. None of it is new research. Most of it is the stuff our clients keep wishing someone had handed them at the start.&lt;/p&gt;
&lt;h2 id=&#34;the-companion-scorecard&#34;&gt;The Companion Scorecard&lt;/h2&gt;
&lt;p&gt;We also built a small site to go with it: &lt;strong&gt;&lt;a href=&#34;https://pq-migration.symbolic.software&#34;&gt;pq-migration.symbolic.software&lt;/a&gt;&lt;/strong&gt;. Twelve questions, three minutes, no signup, and you get a verdict across six dimensions of post-quantum readiness. There is also a one-host TLS scanner that checks what your endpoint &lt;em&gt;actually&lt;/em&gt; negotiates, in case it doesn&amp;rsquo;t match what your team thinks.&lt;/p&gt;
&lt;p&gt;If you read the playbook and want to talk about your specific system, &lt;a href=&#34;https://symbolic.software/about&#34;&gt;get in touch&lt;/a&gt;. The whole reason this is a guide and not a checklist is that the right answers depend on context.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&#34;https://pq-migration.symbolic.software/static/pdf/playbook.pdf&#34;&gt;Download the playbook (PDF, 52 pages)&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&#34;https://pq-migration.symbolic.software&#34;&gt;Try the scorecard&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded>
      <category>Research</category>
      <category>Post-Quantum</category>
      
    </item>
    
    <item>
      <title>Verifpal 0.51.0: Sharper Semantics for Passwords, Assertions, and AEAD</title>
      <link>https://symbolic.software/blog/2026-04-20-verifpal-0-51/</link>
      <pubDate>Mon, 20 Apr 2026 00:00:00 &#43;0000</pubDate>
      <guid>https://symbolic.software/blog/2026-04-20-verifpal-0-51/</guid>
      <description>Verifpal 0.51.0 tightens the analysis engine&#39;s treatment of password-qualified values, checked ASSERT? assertions, and AEAD associated data, all on the back of excellent bug reports from the community.</description>
      <content:encoded>&lt;p&gt;Verifpal 0.51.0 is out. Where &lt;a href=&#34;https://symbolic.software/blog/2026-03-01-verifpal-engine-redesign/&#34;&gt;0.50.0&lt;/a&gt; was a deep architectural redesign of the analysis engine, 0.51.0 is a short, correctness-focused release: three semantic fixes, one UI fix, and a few new models, all driven by bug reports filed against the 0.50.0 series. None of the changes affect the Verifpal modeling language. Every existing model still parses, still runs, and produces results that are at worst the same and in several cases strictly more accurate.&lt;/p&gt;
&lt;p&gt;Thanks to &lt;strong&gt;Cédric Picard (&lt;a href=&#34;https://github.com/cym13&#34;&gt;@cym13&lt;/a&gt;)&lt;/strong&gt; for issues &lt;a href=&#34;https://github.com/symbolicsoft/verifpal/issues/11&#34;&gt;#11&lt;/a&gt;, &lt;a href=&#34;https://github.com/symbolicsoft/verifpal/issues/12&#34;&gt;#12&lt;/a&gt;, and &lt;a href=&#34;https://github.com/symbolicsoft/verifpal/issues/13&#34;&gt;#13&lt;/a&gt;, and to &lt;strong&gt;&lt;a href=&#34;https://github.com/EFCCWEB3&#34;&gt;@EFCCWEB3&lt;/a&gt;&lt;/strong&gt; for issue &lt;a href=&#34;https://github.com/symbolicsoft/verifpal/issues/10&#34;&gt;#10&lt;/a&gt;. This release exists because of them.&lt;/p&gt;
&lt;h2 id=&#34;passwords-from-static-tables-to-verifiable-guesses&#34;&gt;Passwords: From Static Tables to Verifiable Guesses&lt;/h2&gt;
&lt;p&gt;The largest change in 0.51.0 is a rewrite of how Verifpal reasons about &lt;code&gt;password&lt;/code&gt;-qualified values. Passwords occupy a strange place in the Verifpal model: they are secrets, but they are &lt;em&gt;low-entropy&lt;/em&gt; secrets, so the attacker is assumed to be able to brute-force them &lt;em&gt;if and only if&lt;/em&gt; the attacker has a way to verify guesses. Getting that &amp;ldquo;if and only if&amp;rdquo; right turns out to be subtle.&lt;/p&gt;
&lt;h3 id=&#34;what-was-wrong&#34;&gt;What Was Wrong&lt;/h3&gt;
&lt;p&gt;Before 0.51.0, password protection was defined by a static &lt;code&gt;password_hashing&lt;/code&gt; field on each primitive&amp;rsquo;s spec, a list of argument positions at which passwords were considered cryptographically protected. For &lt;code&gt;ENC&lt;/code&gt;, that list was &lt;code&gt;vec![1]&lt;/code&gt;: the plaintext is protected, the key is not.&lt;/p&gt;
&lt;p&gt;This captures the common case, where encrypting a password with a known key lets the attacker brute-force it, but it fails on compositions. Consider:&lt;/p&gt;
&lt;div class=&#34;highlight&#34; data-lang=&#34;verifpal&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-verifpal&#34; data-lang=&#34;verifpal&#34;&gt;&lt;span class=&#34;vp-kw&#34;&gt;principal&lt;/span&gt; &lt;span class=&#34;vp-prin&#34;&gt;Alice&lt;/span&gt;[
    &lt;span class=&#34;vp-kw&#34;&gt;knows&lt;/span&gt; &lt;span class=&#34;vp-kw&#34;&gt;private&lt;/span&gt; key
    &lt;span class=&#34;vp-kw&#34;&gt;knows&lt;/span&gt; &lt;span class=&#34;vp-kw&#34;&gt;public&lt;/span&gt;  data
    &lt;span class=&#34;vp-kw&#34;&gt;knows&lt;/span&gt; &lt;span class=&#34;vp-kw&#34;&gt;password&lt;/span&gt; pwd_2

    msg2 = &lt;span class=&#34;vp-prim&#34;&gt;ENC&lt;/span&gt;(key, &lt;span class=&#34;vp-prim&#34;&gt;CONCAT&lt;/span&gt;(pwd_2, data))
]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;In 0.50.0, a passive confidentiality query on &lt;code&gt;pwd_2&lt;/code&gt; would &lt;strong&gt;fail&lt;/strong&gt;. The password sits inside &lt;code&gt;CONCAT&lt;/code&gt;, which has no &lt;code&gt;password_hashing&lt;/code&gt; list of its own, so the engine treated the password as &amp;ldquo;unprotected&amp;rdquo; and assumed the attacker could brute-force it, even though the attacker does not know &lt;code&gt;key&lt;/code&gt; and therefore has no way to verify a guess. This was Cédric&amp;rsquo;s &lt;a href=&#34;https://github.com/symbolicsoft/verifpal/issues/11&#34;&gt;issue #11&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The same shape arose elsewhere: &lt;code&gt;ENC(secret, pwd)&lt;/code&gt;, &lt;code&gt;MAC(secret, pwd)&lt;/code&gt;, &lt;code&gt;AEAD_ENC(pwd, secret, pubdata)&lt;/code&gt;, &lt;code&gt;BLIND(secret, pwd)&lt;/code&gt;, &lt;code&gt;SIGN(pwd, secret)&lt;/code&gt;. Each one has a different &amp;ldquo;right answer&amp;rdquo; depending on whether the attacker actually has enough of the surrounding context to mount an offline dictionary attack. A flat static table cannot express that.&lt;/p&gt;
&lt;h3 id=&#34;the-new-model&#34;&gt;The New Model&lt;/h3&gt;
&lt;p&gt;0.51.0 replaces the table with a dynamic, position-sensitive check. A &lt;code&gt;password&lt;/code&gt;-qualified value at position &lt;em&gt;i&lt;/em&gt; of primitive &lt;code&gt;P(a₀, …, aₙ)&lt;/code&gt; is obtainable by the attacker only when &lt;strong&gt;both&lt;/strong&gt; of the following hold at every primitive level in the nesting chain:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;No inherent protection.&lt;/strong&gt; Position &lt;em&gt;i&lt;/em&gt; is not listed in &lt;code&gt;P.password_hashing&lt;/code&gt;. This field is now reserved exclusively for primitives that resist brute-force by construction (only &lt;code&gt;PW_HASH&lt;/code&gt; qualifies, since it is expensive by design). Every other primitive loses its static entry.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Verifiable guess.&lt;/strong&gt; The attacker knows every sibling argument &lt;code&gt;aⱼ&lt;/code&gt; (j ≠ i), which is what it takes to reconstruct &lt;code&gt;P(…, guess, …)&lt;/code&gt; and compare against the observed output. If any sibling is unknown, the guess cannot be verified and the password is safe.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Protection, check (1), propagates to descendants: once you are inside a &lt;code&gt;PW_HASH&lt;/code&gt; argument, everything below is protected. Verifiability, check (2), propagates only as long as every ancestor has known siblings; the first ancestor with an unknown sibling blocks the check all the way down.&lt;/p&gt;
&lt;p&gt;This gives much more accurate results across the matrix of primitives. For instance, in the canonical &lt;code&gt;ENC(key, pwd)&lt;/code&gt; case where &lt;code&gt;key&lt;/code&gt; is private, the attacker does not know &lt;code&gt;key&lt;/code&gt;, the only sibling of &lt;code&gt;pwd&lt;/code&gt;, so the password is safe. For &lt;code&gt;ENC(pwd, pubdata)&lt;/code&gt; where &lt;code&gt;pubdata&lt;/code&gt; is public, the attacker knows every sibling of &lt;code&gt;pwd&lt;/code&gt;, so the password is guessable. Both outcomes fall out of the same rule, with no per-primitive configuration.&lt;/p&gt;
&lt;p&gt;The new &lt;code&gt;examples/test/password_underspec.vp&lt;/code&gt; file walks through eleven queries across &lt;code&gt;ENC&lt;/code&gt;, &lt;code&gt;MAC&lt;/code&gt;, &lt;code&gt;AEAD_ENC&lt;/code&gt;, &lt;code&gt;SIGN&lt;/code&gt;, &lt;code&gt;BLIND&lt;/code&gt;, and &lt;code&gt;SHAMIR_SPLIT&lt;/code&gt;, with an inline comment on each one explaining what the attacker can and cannot do and why. It is probably the fastest way to get intuition for the new semantics.&lt;/p&gt;
&lt;p&gt;This fix addresses issue #11 in full and substantially refactors the machinery that &lt;a href=&#34;https://github.com/symbolicsoft/verifpal/issues/12&#34;&gt;issue #12&lt;/a&gt; was asking to clarify. #12 remains open for further discussion of edge cases, but the core formalism is now in place.&lt;/p&gt;
&lt;h2 id=&#34;checked-assert-now-actually-halts-the-principal&#34;&gt;Checked &lt;code&gt;ASSERT?&lt;/code&gt; Now Actually Halts the Principal&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/symbolicsoft/verifpal/issues/13&#34;&gt;Issue #13&lt;/a&gt;, also from Cédric, was a pleasing little bug. Consider a protocol where Alice checks a tag before leaking a derived value:&lt;/p&gt;
&lt;div class=&#34;highlight&#34; data-lang=&#34;verifpal&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-verifpal&#34; data-lang=&#34;verifpal&#34;&gt;&lt;span class=&#34;vp-kw&#34;&gt;principal&lt;/span&gt; &lt;span class=&#34;vp-prin&#34;&gt;Alice&lt;/span&gt;[
    tag_ = &lt;span class=&#34;vp-prim&#34;&gt;DEC&lt;/span&gt;(Ka, enctag)
    Kab_ = &lt;span class=&#34;vp-prim&#34;&gt;DEC&lt;/span&gt;(Ka, Kab_alice)
    Kab_bob_ = &lt;span class=&#34;vp-prim&#34;&gt;DEC&lt;/span&gt;(Ka, Kab_bob_alice)

    &lt;span class=&#34;vp-lit&#34;&gt;_&lt;/span&gt; = &lt;span class=&#34;vp-prim&#34;&gt;ASSERT&lt;/span&gt;(tag_, &lt;span class=&#34;vp-prim&#34;&gt;HASH&lt;/span&gt;(Kab_, Kab_bob_))&lt;span class=&#34;vp-op&#34;&gt;?&lt;/span&gt;
    &lt;span class=&#34;vp-kw&#34;&gt;leaks&lt;/span&gt; Kab_bob_
]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code&gt;?&lt;/code&gt; on the &lt;code&gt;ASSERT&lt;/code&gt; makes it a &lt;em&gt;checked&lt;/em&gt; assertion: if the two sides are not equivalent, the principal halts, and the &lt;code&gt;leaks&lt;/code&gt; statement that follows does not execute. Any downstream reasoning about what the attacker can learn must respect that halt.&lt;/p&gt;
&lt;p&gt;In 0.50.0, Verifpal reported a false-positive attack against this protocol. An active attacker could swap two ciphertexts on the wire, making the assertion symbolically fail. The trace literally read &lt;code&gt;ASSERT(HASH(kab, ENC(kb, kab)), HASH(kab, kab))?&lt;/code&gt;, which is not equivalent under any substitution, and yet the engine still treated &lt;code&gt;leaks Kab_bob_&lt;/code&gt; as firing in that branch and harvested values from it. The attack trace was nonsensical: the assertion it relied on was clearly false, but Verifpal was claiming the attacker had won anyway.&lt;/p&gt;
&lt;p&gt;The underlying issue was that &lt;code&gt;leaks&lt;/code&gt; was represented as a single &lt;code&gt;leaked: bool&lt;/code&gt; flag on the target constant, with no record of which principal had declared the leak or where in that principal&amp;rsquo;s execution it sat. Active-attacker analysis can truncate a principal&amp;rsquo;s state after a failed checked primitive, but a constant declared &lt;em&gt;before&lt;/em&gt; the failed assertion is still in the truncated state, and the attacker was using the leaked-constant token to re-resolve that constant in the mutated state and pick up a value the principal never actually leaked.&lt;/p&gt;
&lt;p&gt;0.51.0 fixes this by adding an explicit &lt;code&gt;LeakEvent&lt;/code&gt; record for every &lt;code&gt;leaks&lt;/code&gt; declaration:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;LeakEvent&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;constant_id&lt;/span&gt;: &lt;span class=&#34;nc&#34;&gt;ValueId&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;principal_id&lt;/span&gt;: &lt;span class=&#34;nc&#34;&gt;PrincipalId&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;declared_at&lt;/span&gt;: &lt;span class=&#34;kt&#34;&gt;i32&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;phase&lt;/span&gt;: &lt;span class=&#34;kt&#34;&gt;i32&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Each &lt;code&gt;PrincipalState&lt;/code&gt; now carries an &lt;code&gt;Option&amp;lt;halted_at&amp;gt;&lt;/code&gt; stamp set whenever a checked primitive fails during mutation analysis. The &lt;code&gt;rule_equivalize&lt;/code&gt; deduction rule, which lets the attacker re-resolve a known constant in the current state, consults both: if a leak event belongs to this principal with &lt;code&gt;declared_at &amp;gt; halted_at&lt;/code&gt;, the principal never reached it, and the equivalize step is suppressed.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;examples/test/assert_junglegym.vp&lt;/code&gt; model is the reduced test case from Cédric&amp;rsquo;s report, now enforced as a regression test. Behavior with unchecked &lt;code&gt;ASSERT&lt;/code&gt; (no &lt;code&gt;?&lt;/code&gt;) is deliberately unchanged: unchecked assertions are informative only, and the attacker is free to walk past them.&lt;/p&gt;
&lt;h2 id=&#34;aead_enc-no-longer-leaks-associated-data&#34;&gt;AEAD_ENC No Longer Leaks Associated Data&lt;/h2&gt;
&lt;p&gt;Up until 0.50.0, &lt;code&gt;AEAD_ENC(key, plaintext, ad)&lt;/code&gt; was declared with &lt;code&gt;passive_reveal: vec![2]&lt;/code&gt;, meaning a passive attacker watching the ciphertext on the wire was automatically given the associated data. This was wrong in two ways.&lt;/p&gt;
&lt;p&gt;First, the AD is an &lt;em&gt;input&lt;/em&gt; to AEAD encryption, not a part of the ciphertext output. It is bound into the authentication tag, but the tag commits to AD without necessarily revealing it. Whether AD is public depends on how the protocol transmits it, not on the encryption primitive itself.&lt;/p&gt;
&lt;p&gt;Second, the blanket passive reveal was blocking legitimate queries where the AD was a secret (for example, a password used as AD), because Verifpal was automatically handing that secret to the attacker.&lt;/p&gt;
&lt;p&gt;0.51.0 removes the &lt;code&gt;passive_reveal: vec![2]&lt;/code&gt; entry. Associated data is now treated like any other argument: if the protocol sends it in the clear, the attacker sees it; if not, the attacker does not. The &lt;code&gt;aead_leak.vp&lt;/code&gt; test has been updated to reflect the corrected semantics, and a new &lt;code&gt;password_aead.vp&lt;/code&gt; test demonstrates that a password used as AD is safe when the other inputs are secret.&lt;/p&gt;
&lt;h2 id=&#34;tui-multi-output-primitive-labels&#34;&gt;TUI: Multi-Output Primitive Labels&lt;/h2&gt;
&lt;p&gt;Issue #10, from @EFCCWEB3, was a small but annoying display bug in the terminal UI. The compact deduction ticker strips a &lt;code&gt;&amp;quot;Output of &amp;quot;&lt;/code&gt; prefix off each deduction message to show a short label like &lt;code&gt;HASH via reconstruct&lt;/code&gt;. For the &lt;em&gt;multi-output&lt;/em&gt; primitives (&lt;code&gt;HKDF&lt;/code&gt;, &lt;code&gt;SPLIT&lt;/code&gt;, &lt;code&gt;SHAMIR_SPLIT&lt;/code&gt;), the underlying message uses indexed forms like &lt;code&gt;&amp;quot;First output of HKDF(…)&amp;quot;&lt;/code&gt; or &lt;code&gt;&amp;quot;Second output of SPLIT(…)&amp;quot;&lt;/code&gt;. The literal-prefix match missed those, and the ticker fell back to the first whitespace-separated word, which was &lt;code&gt;&amp;quot;First&amp;quot;&lt;/code&gt; or &lt;code&gt;&amp;quot;Second&amp;quot;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The fix is one line: look for the shared &lt;code&gt;&amp;quot; of &amp;quot;&lt;/code&gt; delimiter instead of the literal prefix. Multi-output primitives now show up correctly in the ticker.&lt;/p&gt;
&lt;h2 id=&#34;new-models&#34;&gt;New Models&lt;/h2&gt;
&lt;p&gt;Two models of note ship with the release:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;examples/transport-layer/tls13.vp&lt;/code&gt;&lt;/strong&gt; — a 326-line TLS 1.3 model, included explicitly as a case study in Verifpal&amp;rsquo;s current limitations on large multi-phase protocols with many key-schedule stages. It is useful both as a reference and as motivation for future engine work.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;examples/messaging/scuttlebutt.vp&lt;/code&gt;&lt;/strong&gt; — the existing Scuttlebutt model has been updated to guard &lt;code&gt;longTermBPub&lt;/code&gt; (&lt;code&gt;Bob -&amp;gt; Alice: [longTermBPub]&lt;/code&gt;), which exercises more interesting branches of the analysis than the unguarded form.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The password, assertion, and AEAD fixes each ship with at least one new dedicated test model in &lt;code&gt;examples/test/&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&#34;getting-verifpal-0510&#34;&gt;Getting Verifpal 0.51.0&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Scoop (Windows):&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;scoop update verifpal
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Homebrew (macOS, Linux):&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;brew upgrade verifpal
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;From source:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;cargo install --path . --features cli
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Binaries for Windows, Linux, macOS, and FreeBSD are attached to the &lt;a href=&#34;https://github.com/symbolicsoft/verifpal/releases/tag/v0.51.0&#34;&gt;0.51.0 GitHub release&lt;/a&gt;. As always, issues and contributions are welcome at &lt;a href=&#34;https://github.com/symbolicsoft/verifpal&#34;&gt;github.com/symbolicsoft/verifpal&lt;/a&gt;.&lt;/p&gt;
</content:encoded>
      <category>Software</category>
      <category>Research</category>
      <category>Verifpal</category>
      <category>Formal Verification</category>
      
    </item>
    
    <item>
      <title>Hybrid Constructions Are a Safety Blanket, and That&#39;s Fine</title>
      <link>https://symbolic.software/blog/2026-04-13-hybrid-constructions/</link>
      <pubDate>Mon, 13 Apr 2026 00:00:00 &#43;0000</pubDate>
      <guid>https://symbolic.software/blog/2026-04-13-hybrid-constructions/</guid>
      <description>Why Symbolic Software agrees with Soatok&#39;s position on hybrid post-quantum constructions: hybrids are compelling for KEMs, far less necessary for signatures, and the real risk is migration friction.</description>
      <content:encoded>&lt;p&gt;Soatok published a &lt;a href=&#34;https://soatok.blog/2026/04/13/hybrid-constructions-the-post-quantum-safety-blanket/&#34;&gt;clear-eyed piece today&lt;/a&gt; on hybrid post-quantum constructions, meaning the practice of combining a classical algorithm (X25519, ECDSA) with a post-quantum one (ML-KEM, ML-DSA) so that the system remains secure even if one of the two is broken. The post argues that hybrid KEMs are a reasonable safety blanket while hybrid signatures have a weaker justification, and that the industry should stop letting the perfect be the enemy of the deployed. We broadly agree, and this post explains why.&lt;/p&gt;
&lt;h2 id=&#34;the-asymmetry-between-kems-and-signatures&#34;&gt;The Asymmetry Between KEMs and Signatures&lt;/h2&gt;
&lt;p&gt;Soatok&amp;rsquo;s central observation is one that deserves wider uptake: the harvest-now-decrypt-later (HNDL) threat that motivates hybrid KEMs has no analogue for signatures.&lt;/p&gt;
&lt;p&gt;When an adversary captures ciphertext today, they can store it indefinitely and decrypt it once a cryptographically relevant quantum computer (CRQC) arrives. The data that was confidential in 2026 is still confidential in 2036. A hybrid KEM like &lt;a href=&#34;https://www.ietf.org/archive/id/draft-connolly-cfrg-xwing-kem-06.html&#34;&gt;X-Wing&lt;/a&gt;, which combines ML-KEM-768 with X25519, hedges against this threat by ensuring that an attacker must break &lt;em&gt;both&lt;/em&gt; the lattice problem and the elliptic curve discrete logarithm to recover the plaintext. If ML-KEM turns out to be flawed, X25519 is still there. If X25519 falls to a quantum adversary, ML-KEM is still there. The cost is modest: a slightly larger key exchange, a second KDF call, and some additional bandwidth. The insurance is real.&lt;/p&gt;
&lt;p&gt;Signatures are different. A signature is verified at the time it is received. A quantum adversary who arrives in 2036 cannot retroactively forge a signature that was verified and acted upon in 2026. There is no stockpile of signatures waiting to be broken. The threat model that justifies hybrid KEMs, store now and exploit later, does not apply in the same way.&lt;/p&gt;
&lt;p&gt;That said, hybrid signatures are not without merit. Long-lived signatures on software packages, certificates, or firmware could in principle be forged by a future quantum adversary, and there is a reasonable belt-and-suspenders argument for hedging against a lattice break even in the authentication context. The case is simply less urgent than for KEMs, where the consequences of a wrong bet are retroactive and irrecoverable.&lt;/p&gt;
&lt;p&gt;We have been &lt;a href=&#34;https://symbolic.software/blog/2026-04-02-pq-native/&#34;&gt;recommending post-quantum native design&lt;/a&gt; for all new systems since April 2, with hybrid constructions where backward compatibility requires them. That recommendation was deliberately agnostic about signatures versus KEMs. We are now refining it: &lt;strong&gt;hybrid KEMs are a compelling transitional measure; hybrid signatures are far less necessary, and should be evaluated against the specific threat model rather than adopted as a default.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&#34;lattice-confidence-is-earned-not-assumed&#34;&gt;Lattice Confidence Is Earned, Not Assumed&lt;/h2&gt;
&lt;p&gt;A recurring objection to pure post-quantum deployment is that lattice-based cryptography is &amp;ldquo;too new&amp;rdquo; to trust without a classical fallback. Soatok addresses this directly, and the numbers bear repeating: NTRU was patented in 1997. The learning-with-errors problem was introduced by Regev in 2005, the same year Curve25519 was published. ML-KEM and ML-DSA survived nearly a decade of the most intensive public cryptanalysis effort the field has ever organized, the NIST Post-Quantum Cryptography Standardization Process, in which hundreds of researchers from dozens of countries actively tried to break every candidate.&lt;/p&gt;
&lt;p&gt;The process worked. SIKE, the supersingular isogeny-based scheme, was broken during the competition by a catastrophic attack that the evaluation process was designed to surface. The lattice candidates survived. This is not absence of scrutiny; it is scrutiny that produced a result.&lt;/p&gt;
&lt;p&gt;We have additional evidence from our own work. When we built &lt;a href=&#34;https://symbolic.software/blog/2026-03-23-crucible/&#34;&gt;Crucible&lt;/a&gt; and tested 15 ML-KEM and ML-DSA implementations across five languages, we found two minor conformance gaps and zero security vulnerabilities. The implementations that ship inside AWS-LC, Cloudflare CIRCL, the Go standard library, and wolfSSL&amp;rsquo;s FIPS module all passed every test. The post-quantum ecosystem is not speculative anymore. It is production-grade software that has been tested, audited, and deployed at scale.&lt;/p&gt;
&lt;h2 id=&#34;the-real-risk-is-complexity&#34;&gt;The Real Risk Is Complexity&lt;/h2&gt;
&lt;p&gt;Our audit practice keeps landing on the same lesson. The most common source of cryptographic failure is the complexity surrounding the primitive rather than the primitive itself: the state machine that manages keys, the serialization layer that encodes messages, the fallback logic that negotiates which algorithm to use.&lt;/p&gt;
&lt;p&gt;Every hybrid construction doubles the number of moving parts in exactly these layers. A hybrid KEM requires two key generations, two encapsulations, two decapsulations, and a combiner that must be correctly implemented to preserve the security properties of both components. A hybrid signature requires two signing operations, two verification operations, and a composition that must not introduce verification oracle attacks. Each additional algorithm is additional attack surface, not because the algorithm itself is weak, but because the &lt;em&gt;integration&lt;/em&gt; is where bugs live.&lt;/p&gt;
&lt;p&gt;We have spent the past two months &lt;a href=&#34;https://symbolic.software/blog/2026-02-05-ce-labs/&#34;&gt;documenting&lt;/a&gt; what happens when cryptographic implementation complexity outpaces engineering discipline. The bugs we found were not in the cryptographic primitives. They were in the wrappers, the serialization, the specification-to-code translation, the build system defaults. Adding a second algorithm to every cryptographic operation in a system multiplies exactly these surfaces.&lt;/p&gt;
&lt;p&gt;For KEMs, the HNDL threat clearly justifies this cost. The insurance against a catastrophic retroactive loss of confidentiality is worth the additional integration complexity. For signatures, the calculus is less clear-cut. Hybrid signatures carry the same complexity costs, but the threat they hedge against, a lattice break that enables forgery, is one where the damage is prospective rather than retroactive. That does not make hybrid signatures pointless; it makes them a less urgent priority than hybrid KEMs, and one where the complexity tradeoff deserves more scrutiny.&lt;/p&gt;
&lt;h2 id=&#34;migration-friction-kills&#34;&gt;Migration Friction Kills&lt;/h2&gt;
&lt;p&gt;Soatok makes a pragmatic point that matches everything we see in practice: the biggest risk to the post-quantum transition is not that the algorithms are wrong. It is that the transition takes too long because the industry overcomplicates it.&lt;/p&gt;
&lt;p&gt;Every additional requirement adds friction to migration: hybrid signatures on top of hybrid KEMs, dual certificate chains, negotiation for four algorithms instead of two. Friction means slower adoption. Slower adoption means more years of classical-only systems exposed to the HNDL threat that the entire transition is supposed to address. The irony is acute: treating hybrid signatures as a prerequisite for deployment may extend the window during which systems are vulnerable to exactly the quantum threat they are trying to mitigate.&lt;/p&gt;
&lt;p&gt;Our &lt;a href=&#34;https://symbolic.software/blog/2026-04-02-pq-native/&#34;&gt;recommendation&lt;/a&gt; stands: design new systems as post-quantum native. Use ML-KEM for key encapsulation. Use ML-DSA for digital signatures. Use hybrid KEMs (X-Wing or equivalent) where backward compatibility with classical systems is required during transition. Hybrid signatures may be appropriate for specific use cases, such as long-lived code signing, certificate authorities, and scenarios where signature validity must span decades. But they should not be the default, and they should not gate the broader migration.&lt;/p&gt;
&lt;h2 id=&#34;where-that-leaves-us&#34;&gt;Where That Leaves Us&lt;/h2&gt;
&lt;p&gt;Soatok&amp;rsquo;s post captures something the applied cryptography community has been circling around without stating plainly: hybrid constructions are a safety blanket, and for KEMs, that blanket is clearly worth the cost. For signatures, the case is weaker. The distinction turns on whether the urgency of the threat model justifies the additional complexity, not on how much anyone trusts lattices.&lt;/p&gt;
&lt;p&gt;We have audited a lot of post-quantum implementations. We built the &lt;a href=&#34;https://symbolic.software/blog/2026-03-23-crucible/&#34;&gt;conformance testing framework&lt;/a&gt; that the ecosystem uses to validate them, and we &lt;a href=&#34;https://symbolic.software/blog/2026-04-07-cryspen-hax/&#34;&gt;documented&lt;/a&gt; what happens when verification complexity outpaces engineering reality. From that vantage point: the lattice primitives are sound, the implementations are maturing, and the greatest risk to the transition is making it harder than it needs to be.&lt;/p&gt;
&lt;p&gt;Deploy post-quantum cryptography. Use hybrid KEMs where they make sense. Consider hybrid signatures where the use case warrants them, but do not treat them as a prerequisite. And above all, do not let the pursuit of a theoretically perfect transition delay the practically necessary one.&lt;/p&gt;
&lt;p&gt;The full version of this argument, alongside the rest of our migration recommendations, lives in our &lt;a href=&#34;https://pq-migration.symbolic.software/static/pdf/playbook.pdf&#34;&gt;Post-Quantum Migration Playbook&lt;/a&gt;, a free practitioner guide covering primitives, hybrid constructions, TLS migration, libraries, conformance testing, rollout strategy, and the bug classes we keep finding in audits. It is the companion to our live &lt;a href=&#34;https://pq-migration.symbolic.software&#34;&gt;PQ Migration Readiness scorecard and scanner&lt;/a&gt;.&lt;/p&gt;
</content:encoded>
      <category>Research</category>
      <category>Post-Quantum</category>
      
    </item>
    
    <item>
      <title>The Verification Facade: Structural Gaps in Cryspen&#39;s Hax Pipeline</title>
      <link>https://symbolic.software/blog/2026-04-07-cryspen-hax/</link>
      <pubDate>Tue, 07 Apr 2026 00:00:00 &#43;0000</pubDate>
      <guid>https://symbolic.software/blog/2026-04-07-cryspen-hax/</guid>
      <description>Five proof-of-concept exploits against ML-DSA, ML-KEM, Ed25519, and ChaCha20 demonstrate three classes of semantic gap in hax&#39;s Rust-to-F* extraction pipeline, where verified models diverge from deployed code.</description>
      <content:encoded>




&lt;nav class=&#34;series-nav&#34; aria-label=&#34;CE Labs series (top)&#34;&gt;
  &lt;header class=&#34;series-nav-header&#34;&gt;
    &lt;span class=&#34;series-nav-label&#34;&gt;▶ CE Labs Series&lt;/span&gt;
    &lt;span class=&#34;series-nav-count&#34;&gt;5 posts · 2 papers · 1 talk&lt;/span&gt;
  &lt;/header&gt;
  &lt;ol class=&#34;series-nav-list&#34;&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-02-05-ce-labs/&#34;&gt;On the Promises of &amp;#39;High-Assurance&amp;#39; Cryptography&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.02.05&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-02-12-ce-labs-response/&#34;&gt;We Found Bugs in CE Labs&amp;#39; Verified Code&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.02.12&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-02-17-ce-labs-mldsa/&#34;&gt;Even More Bugs in CE Labs&amp;#39; libcrux: ML-DSA&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.02.17&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-03-07-ce-labs-tls/&#34;&gt;CE Labs&amp;#39; Approach to TLS: A Critical Analysis&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.03.07&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li class=&#34;current&#34; aria-current=&#34;page&#34;&gt;
      
      &lt;span class=&#34;title&#34;&gt;The Verification Facade: Structural Gaps in Cryspen&amp;#39;s Hax Pipeline&lt;/span&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.04.07&lt;/span&gt;
    &lt;/li&gt;
    
  &lt;/ol&gt;
  &lt;div class=&#34;series-nav-footer&#34;&gt;
    &lt;div class=&#34;series-nav-row&#34;&gt;
      &lt;span class=&#34;series-nav-rowlabel&#34;&gt;Papers&lt;/span&gt;
      &lt;a href=&#34;https://eprint.iacr.org/2026/192&#34;&gt;&lt;em&gt;Verification Theatre&lt;/em&gt;&lt;/a&gt;
      &lt;span class=&#34;series-nav-sep&#34;&gt;·&lt;/span&gt;
      &lt;a href=&#34;https://eprint.iacr.org/2026/670&#34;&gt;&lt;em&gt;Verification Facade&lt;/em&gt;&lt;/a&gt;
    &lt;/div&gt;
    &lt;div class=&#34;series-nav-row&#34;&gt;
      &lt;span class=&#34;series-nav-rowlabel&#34;&gt;Talk&lt;/span&gt;
      &lt;a href=&#34;https://www.youtube.com/watch?v=TdOXza1-M_4&#34;&gt;&lt;em&gt;High Assurance Cryptography and the Ethics of Disclosure (OSTIF)&lt;/em&gt;&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/nav&gt;

&lt;p&gt;Over the past two months, we have documented &lt;a href=&#34;https://symbolic.software/blog/2026-03-07-ce-labs-tls/&#34;&gt;sixteen vulnerabilities&lt;/a&gt; across six CE Labs projects: bugs in unverified wrappers, wrong formal specifications, false proofs, and unsound axioms. Those findings concerned the &lt;em&gt;product&lt;/em&gt;. We now turn to the &lt;em&gt;toolchain&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Today, we&amp;rsquo;re publishing our latest paper, &lt;a href=&#34;https://eprint.iacr.org/2026/670&#34;&gt;&lt;em&gt;Verification Facade: Masquerading Insecure Cryptographic Implementations as Verified Code&lt;/em&gt;&lt;/a&gt;, which examines &lt;a href=&#34;https://github.com/cryspen/hax&#34;&gt;Hax&lt;/a&gt;, Cryspen&amp;rsquo;s verification pipeline and the toolchain that underlies CE Labs&amp;rsquo; &amp;ldquo;formally verified&amp;rdquo; claims.&lt;/p&gt;
&lt;p&gt;Hax translates a subset of Rust into F*, enabling machine-checked proofs of panic freedom and functional correctness. ML-KEM and ML-DSA implementations verified via hax are being developed in partnership with Google and tested in Signal&amp;rsquo;s PQXDH protocol. If hax&amp;rsquo;s translation is faithful, the F* proofs transfer to the Rust code. If it is not, the proofs verify a model that diverges from what actually executes.&lt;/p&gt;
&lt;p&gt;We find that it is not, in three distinct ways. We identify three classes of semantic gap in hax&amp;rsquo;s pipeline and demonstrate each through proof-of-concept exploits against ML-DSA, ML-KEM, Ed25519, and ChaCha20. Every exploit must pass all four of our inclusion gates:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Correct Rust&lt;/strong&gt;: the code compiles as valid Rust.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Successful extraction&lt;/strong&gt;: hax extracts it to F* without errors or warnings.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security-relevant model divergence&lt;/strong&gt;: the verified F* model diverges from the deployed Rust code in a way that compromises a cryptographic security property.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Invisible to testing&lt;/strong&gt;: the divergence escapes detection by functional tests (sign-then-verify, encrypt-then-decrypt, test vectors all pass).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;All source code and pre-extracted F* output are &lt;a href=&#34;https://github.com/symbolicsoft/verification-facade&#34;&gt;available on GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;We call the resulting phenomenon a &lt;em&gt;verification facade&lt;/em&gt;: verification that is performed but covers less than it appears to cover. In &lt;a href=&#34;https://eprint.iacr.org/2026/192&#34;&gt;verification theatre&lt;/a&gt;, verification is claimed but not performed. In a facade it &lt;em&gt;is&lt;/em&gt; performed, but the model either diverges from the deployed code, depends on unenforceable assumptions, or cannot express a security-critical property.&lt;/p&gt;
&lt;h2 id=&#34;how-hax-works&#34;&gt;How Hax Works&lt;/h2&gt;
&lt;p&gt;Hax translates Rust to F* through three stages. A &lt;strong&gt;frontend&lt;/strong&gt; hooks into &lt;code&gt;rustc&lt;/code&gt; and extracts the Typed High-Level Intermediate Representation (THIR), a representation where references are still present, borrow-checker constraints are implicit, and &lt;code&gt;Drop&lt;/code&gt; glue has not been elaborated. A &lt;strong&gt;transformation engine&lt;/strong&gt; then applies 35 sequential phases, each eliminating a language feature: by the time the AST reaches the backend, mutable variables, references, raw pointers, lifetimes, all loop forms, and the &lt;code&gt;?&lt;/code&gt; operator have been eliminated, leaving a purely functional representation. Finally, the &lt;strong&gt;F* backend&lt;/strong&gt; translates this to F* surface syntax. Integer types become refinement-typed wrappers (e.g., &lt;code&gt;u8&lt;/code&gt; becomes &lt;code&gt;int_t U8&lt;/code&gt; with range $[0,255]$). Rust&amp;rsquo;s &lt;code&gt;+&lt;/code&gt; becomes &lt;code&gt;+!&lt;/code&gt; (strict, with overflow proof obligation); &lt;code&gt;wrapping_add&lt;/code&gt; becomes &lt;code&gt;+.&lt;/code&gt; (modular).&lt;/p&gt;
&lt;p&gt;The pipeline&amp;rsquo;s feature-witness system ensures &lt;em&gt;structural&lt;/em&gt; correctness (the output has the right shape) but not &lt;em&gt;semantic&lt;/em&gt; preservation (the output has the same meaning). For hax&amp;rsquo;s verification claims to hold, three unverified components must be correct: the 35 OCaml transformation phases, the F* proof libraries (which axiomatize 113 operations via &lt;code&gt;assume val&lt;/code&gt;, roughly 19% of the integer model), and user annotations (&lt;code&gt;#[opaque]&lt;/code&gt;, &lt;code&gt;assume!&lt;/code&gt;, &lt;code&gt;fstar::replace&lt;/code&gt;, &lt;code&gt;verification_status(lax)&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;For code that fits its extraction model, meaning deterministic &lt;code&gt;for&lt;/code&gt; loops over bounded ranges, pure arithmetic with explicit bounds, and array transformations with statically known indices, hax provides genuine verification. Every &lt;code&gt;+!&lt;/code&gt; and &lt;code&gt;*!&lt;/code&gt; generates a real overflow proof obligation. Bounded &lt;code&gt;for&lt;/code&gt; loops extract to a defined (not axiomatized) &lt;code&gt;fold_range&lt;/code&gt; combinator with real loop invariants. And &lt;code&gt;ensures&lt;/code&gt; clauses are proof obligations, not axioms: if the function body violates the postcondition, F* rejects the code. We confirmed this experimentally.&lt;/p&gt;
&lt;p&gt;The gaps arise elsewhere.&lt;/p&gt;
&lt;h2 id=&#34;the-gaps-and-their-exploits&#34;&gt;The Gaps and Their Exploits&lt;/h2&gt;
&lt;p&gt;We identify three classes of semantic gap and demonstrate each with proof-of-concept exploits. We distinguish three gradations: &lt;em&gt;facade gaps&lt;/em&gt; (E1, E3, E4), where the F* model actively diverges from Rust; a &lt;em&gt;conditional gap&lt;/em&gt; (E2), where the divergence depends on the compilation mode; and a &lt;em&gt;scope gap&lt;/em&gt; (E5), where the model is faithful but cannot cover a critical property.&lt;/p&gt;
&lt;h3 id=&#34;e1-ml-dsa-rejection-sampling-facade-gap&#34;&gt;E1: ML-DSA Rejection Sampling (Facade Gap)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;The gap: while-loop ghosting.&lt;/strong&gt; When a &lt;code&gt;while&lt;/code&gt; loop lacks a &lt;code&gt;loop_decreases!&lt;/code&gt; annotation, hax generates a fuel function returning constant 0. The F* &lt;code&gt;while_loop&lt;/code&gt; combinator requires fuel to strictly decrease on each iteration (&lt;code&gt;fuel(next) &amp;lt; fuel(current)&lt;/code&gt;). With constant fuel 0, that obligation becomes $0 &amp;lt; 0$, which is $\mathsf{False}$. The loop body is &lt;em&gt;proof-inert&lt;/em&gt;: it exists syntactically but cannot support any verification claim.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The exploit.&lt;/strong&gt; ML-DSA signing (FIPS 204) uses rejection sampling: the signer computes $\mathbf{z} = \mathbf{y} + c\mathbf{s}_1$ and retries until $\|\mathbf{z}\|_\infty &amp;lt; \gamma_1 - \beta$. This loop is what makes ML-DSA signatures zero-knowledge. Our implementation is straightforward:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;fn&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;sign_message&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;s1&lt;/span&gt;: &lt;span class=&#34;kp&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;nc&#34;&gt;SecretS1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;seed&lt;/span&gt;: &lt;span class=&#34;kt&#34;&gt;u64&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;-&amp;gt; &lt;span class=&#34;nc&#34;&gt;SignatureZ&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;mut&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;nonce&lt;/span&gt;: &lt;span class=&#34;kt&#34;&gt;u32&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;mut&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;z&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;generate_mask&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;seed&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;nonce&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;z&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;compute_z&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;z&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;s1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;while&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;!&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;check_norm_bound&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;z&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// rejection sampling
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;nonce&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;nonce&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;wrapping_add&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;z&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;generate_mask&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;seed&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;nonce&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;z&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;compute_z&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;z&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;s1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;z&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The code compiles, the rejection loop runs correctly, and sign-then-verify tests pass. But the extracted F* tells a different story:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-gdscript3&#34; data-lang=&#34;gdscript3&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;sign_message&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;s1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;t_SecretS1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;seed&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;u64&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt; &lt;span class=&#34;n&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;z&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;compute_z&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;generate_mask&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;seed&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;s1&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt; &lt;span class=&#34;n&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;nonce&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;z&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;n&#34;&gt;Rust_primitives&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Hax&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;while_loop&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;   &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;fun&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;_&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;bp&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;                        &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;invariant&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;   &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;fun&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;_&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;z&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;~.&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;check_norm_bound&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;z&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;condition&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;   &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;fun&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;_&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Int&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;from_machine&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;mk_u32&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;fuel&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;   &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;z&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;                                 &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;accumulator&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;   &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;fun&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;nonce&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;z&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;                     &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;body&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;nonce&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;wrapping_add&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;nonce&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;let&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;z&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;compute_z&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;generate_mask&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;seed&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;nonce&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;s1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;ow&#34;&gt;in&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;nonce&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;z&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt; &lt;span class=&#34;ow&#34;&gt;in&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;z&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Line 7 is the problem. The fuel function returns constant 0, so the body&amp;rsquo;s refinement type requires $0 &amp;lt; 0$&amp;mdash;impossible. Z3 cannot use the body to prove anything about how the accumulator changes. The Rust code correctly rejects candidates, but F* has no way to verify this.&lt;/p&gt;
&lt;p&gt;ML-DSA&amp;rsquo;s security relies on the Fiat-Shamir with Aborts paradigm: rejection sampling ensures that $\mathbf{z}$&amp;rsquo;s distribution is statistically independent of $\mathbf{s}_1$. Without this property, lattice techniques can recover the secret key from approximately 1000 signatures. The verification is supposed to cover exactly this, and it cannot.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A stronger variant.&lt;/strong&gt; Because the &lt;code&gt;while&lt;/code&gt; loop is proof-inert, F* cannot distinguish a correct rejection loop from a &lt;em&gt;broken&lt;/em&gt; one. An adversarial developer could invert the loop condition, writing &lt;code&gt;while check_norm_bound(&amp;amp;z)&lt;/code&gt; instead of &lt;code&gt;while !check_norm_bound(&amp;amp;z)&lt;/code&gt;, causing the function to return the first candidate that &lt;em&gt;fails&lt;/em&gt; the norm bound, directly leaking $\mathbf{s}_1$. The extracted F* would be structurally identical: the same &lt;code&gt;while_loop&lt;/code&gt; call with the same fuel=0, and the same inability to prove anything about the output. A conforming verifier would eventually reject some of the resulting signatures (since FIPS 204 Algorithm 8 checks the same bound), so functional testing would catch it, but F* provides no earlier warning. The verification facade is the same whether the underlying code is correct or broken.&lt;/p&gt;
&lt;h3 id=&#34;e2-ml-kem-barrett-overflow-conditional-gap&#34;&gt;E2: ML-KEM Barrett Overflow (Conditional Gap)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;The gap: deployment model divergence.&lt;/strong&gt; Hax translates Rust&amp;rsquo;s &lt;code&gt;+&lt;/code&gt; to F*&amp;rsquo;s &lt;code&gt;+!&lt;/code&gt; (strict, panics on overflow), matching debug-mode semantics. In release mode, overflow wraps silently instead.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The exploit.&lt;/strong&gt; Our proof-of-concept implements ML-KEM Barrett reduction with correct preconditions. The code compiles, passes debug-mode tests, and extracts to F* where arithmetic becomes strict. The F* proof establishes: &amp;ldquo;if the precondition holds, the result is correct; if not, the program panics.&amp;rdquo; But release mode introduces a third possibility that the F* model does not represent: precondition violated, no panic, silently wrong result. Incorrect Barrett reductions corrupt NTT polynomial arithmetic, breaking ML-KEM&amp;rsquo;s IND-CCA security.&lt;/p&gt;
&lt;p&gt;This is a &lt;em&gt;conditional&lt;/em&gt; gap: a correct implementation that always reduces after each multiplication stays within bounds. The divergence requires a caller that skips intermediate reduction, and such callers exist in the wild.&lt;/p&gt;
&lt;h3 id=&#34;e3-ed25519-clamping-facade-gap&#34;&gt;E3: Ed25519 Clamping (Facade Gap)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;The gap: assumption laundering.&lt;/strong&gt; &lt;code&gt;assume!(P)&lt;/code&gt; compiles to &lt;code&gt;()&lt;/code&gt; in Rust but introduces $P$ as an axiom in F*. A false assumption introduces $\mathsf{False}$ into the proof context, making every subsequent proof trivially satisfiable via &lt;em&gt;ex falso quodlibet&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The exploit.&lt;/strong&gt; Ed25519 clamps the secret scalar: clear bits 0&amp;ndash;2, clear bit 255, set bit 254. Our proof-of-concept introduces a subtle bug: &lt;code&gt;|&lt;/code&gt; (OR) instead of &lt;code&gt;&amp;amp;&lt;/code&gt; (AND), &lt;em&gt;setting&lt;/em&gt; bit 255 instead of clearing it. An &lt;code&gt;assume!&lt;/code&gt; call immediately after asserts the bit is clear:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;fn&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;clamp_scalar&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;secret&lt;/span&gt;: &lt;span class=&#34;kp&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;nc&#34;&gt;Scalar&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;-&amp;gt; &lt;span class=&#34;nc&#34;&gt;Scalar&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;mut&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;s&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;secret&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;s&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;s&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mh&#34;&gt;0xF0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;hax_lib&lt;/span&gt;::&lt;span class=&#34;fm&#34;&gt;assume!&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;((&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;s&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mh&#34;&gt;0x07&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;==&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// BUG: | 0x80 SETS bit 255 (should be &amp;amp; 0x7F)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;s&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;31&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;s&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;31&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;|&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mh&#34;&gt;0x80&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// assume! LIES: claims bit 255 is clear
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;hax_lib&lt;/span&gt;::&lt;span class=&#34;fm&#34;&gt;assume!&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;((&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;s&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;31&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mh&#34;&gt;0x80&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;==&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;s&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;31&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;s&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;31&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;|&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mh&#34;&gt;0x40&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;hax_lib&lt;/span&gt;::&lt;span class=&#34;fm&#34;&gt;assume!&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;((&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;s&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;31&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mh&#34;&gt;0x40&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;!=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;s&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In the extracted F*, line 2 computes &lt;code&gt;s[31] | 0x80&lt;/code&gt; (sets bit 255), while line 4 assumes &lt;code&gt;(s[31] &amp;amp; 0x80) = 0&lt;/code&gt; (bit 255 is clear). The contradiction introduces $\mathsf{False}$, and from that point forward every postcondition in the function &amp;ldquo;verifies&amp;rdquo; vacuously. The developer sees green checks; the verification is meaningless.&lt;/p&gt;
&lt;p&gt;The Rust code still produces valid Ed25519 signatures, because the curve math works for any scalar, so sign-then-verify tests pass. Only tests that check the clamped scalar directly would catch this. With bit 255 set, the scalar may exceed the group order $L$, and the effective signing key becomes $s \bmod L$, a different key than intended, enabling key-substitution attacks.&lt;/p&gt;
&lt;h3 id=&#34;e4-ml-kem-samplentt-facade-gap&#34;&gt;E4: ML-KEM SampleNTT (Facade Gap)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;The gap: while-loop ghosting, compounded.&lt;/strong&gt; This exploit combines the fuel=0 bug from E1 with a second problem: early &lt;code&gt;return&lt;/code&gt; inside the loop body forces hax to use &lt;code&gt;while_loop_return&lt;/code&gt;, an &lt;code&gt;assume val&lt;/code&gt; combinator with &lt;em&gt;no implementation at all&lt;/em&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;assume val while_loop_return #acc_t #ret_t
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  (inv: acc_t -&amp;gt; Type0)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  (condition: (c:acc_t {inv c}) -&amp;gt; bool)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  (fuel: (a:acc_t -&amp;gt; nat))
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  (init: acc_t)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  (f: (acc_t -&amp;gt; ControlFlow ... acc_t))
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  : ControlFlow ret_t acc_t
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;The exploit.&lt;/strong&gt; ML-KEM&amp;rsquo;s SampleNTT is a rejection sampling loop that accepts coefficients only if they are less than $q = 3329$. Our implementation includes an early &lt;code&gt;return None&lt;/code&gt; when randomness is exhausted. The extracted F* is doubly proof-inert: the combinator is an axiom Z3 cannot reason about, and the fuel is constant 0.&lt;/p&gt;
&lt;p&gt;The property &amp;ldquo;all sampled coefficients are less than $q$&amp;rdquo; is essential, because coefficients $\geq q$ break the ring structure on which ML-KEM&amp;rsquo;s security rests. F* cannot verify this property. Even a correct &lt;code&gt;loop_decreases!&lt;/code&gt; annotation would not help, because &lt;code&gt;while_loop_return&lt;/code&gt; has no body for Z3 to unfold.&lt;/p&gt;
&lt;h3 id=&#34;e5-chacha20-rotations-scope-gap&#34;&gt;E5: ChaCha20 Rotations (Scope Gap)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;The gap: proof library axioms.&lt;/strong&gt; The proof library axiomatizes operations that &lt;em&gt;could&lt;/em&gt; be defined&amp;mdash;including bit rotations&amp;mdash;via &lt;code&gt;assume val&lt;/code&gt; with no postconditions. F* knows only the return type, not what the function computes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The exploit.&lt;/strong&gt; ChaCha20 quarter rounds use left rotation by 16, 12, 8, and 7 bits. Our implementation is &lt;em&gt;correct&lt;/em&gt;, and it passes standard test vectors. The extracted F* shows the rotation amounts explicitly:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;impl_u32__rotate_left (state.[ d ]) (mk_u32 16)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;impl_u32__rotate_left (state.[ b ]) (mk_u32 12)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;impl_u32__rotate_left (state.[ d ]) (mk_u32 8)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;impl_u32__rotate_left (state.[ b ]) (mk_u32 7)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;But &lt;code&gt;rotate_left&lt;/code&gt; is declared as &lt;code&gt;assume val impl_u32__rotate_left&#39;: x: u32 -&amp;gt; n: u32 -&amp;gt; u32&lt;/code&gt;&amp;mdash;no postcondition. Z3 cannot distinguish &lt;code&gt;rotate_left x 16&lt;/code&gt; from &lt;code&gt;rotate_left x 15&lt;/code&gt;. An implementation with amounts 15, 11, 7, 6, each off by one and producing a weak cipher, would &amp;ldquo;verify&amp;rdquo; identically.&lt;/p&gt;
&lt;p&gt;This is a &lt;em&gt;scope gap&lt;/em&gt;, not a facade gap: the model is faithful, but the verification cannot cover the core cryptographic property. E1 and E4 show models that are &lt;em&gt;wrong&lt;/em&gt;; E5 shows a model that is &lt;em&gt;right but incomplete&lt;/em&gt;. The axiomatization affects every primitive that uses rotations: SHA-256, ChaCha20, AES. HACL*&amp;rsquo;s &lt;code&gt;Lib.IntTypes&lt;/code&gt; demonstrates that defining rotations via shift-and-or is tractable. The current library chose axiomatization for simplicity.&lt;/p&gt;
&lt;h2 id=&#34;classification-and-verification-results&#34;&gt;Classification and Verification Results&lt;/h2&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Gap&lt;/th&gt;
          &lt;th&gt;Class&lt;/th&gt;
          &lt;th&gt;Type&lt;/th&gt;
          &lt;th&gt;Exploits&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;While-loop fuel=0&lt;/td&gt;
          &lt;td&gt;I-A&lt;/td&gt;
          &lt;td&gt;Bug&lt;/td&gt;
          &lt;td&gt;E1, E4&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Reference erasure&lt;/td&gt;
          &lt;td&gt;I-B&lt;/td&gt;
          &lt;td&gt;Fundamental&lt;/td&gt;
          &lt;td&gt;&amp;mdash;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Debug/release divergence&lt;/td&gt;
          &lt;td&gt;I-C&lt;/td&gt;
          &lt;td&gt;Engineering&lt;/td&gt;
          &lt;td&gt;E2&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Opaque function trust&lt;/td&gt;
          &lt;td&gt;II-A&lt;/td&gt;
          &lt;td&gt;Feature&lt;/td&gt;
          &lt;td&gt;&amp;mdash;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Proof library axioms&lt;/td&gt;
          &lt;td&gt;II-B&lt;/td&gt;
          &lt;td&gt;Engineering&lt;/td&gt;
          &lt;td&gt;E5&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Unmodeled Drop/RAII&lt;/td&gt;
          &lt;td&gt;II-C&lt;/td&gt;
          &lt;td&gt;Fundamental&lt;/td&gt;
          &lt;td&gt;&amp;mdash;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;assume!&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;III-A&lt;/td&gt;
          &lt;td&gt;Feature&lt;/td&gt;
          &lt;td&gt;E3&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;We ran the F* typechecker (v2025.03.25) on all five extracted files. In full verification mode, three pass completely: E2, E3, and E5 all report &amp;ldquo;All verification conditions discharged successfully.&amp;rdquo; These are the strongest findings: F* &lt;em&gt;fully verifies&lt;/em&gt; the code, yet the security gaps are real and undetected. E1 and E4 fail full verification due to an F* solver limitation with inner &lt;code&gt;let rec&lt;/code&gt; definitions, a failure unrelated to our exploits. In practice, projects using hax handle &lt;code&gt;while&lt;/code&gt;-loop modules via &lt;code&gt;ADMIT_MODULES&lt;/code&gt;, accepting them without full verification.&lt;/p&gt;
&lt;p&gt;The while-loop fuel=0 default is a one-line fix; we notified the hax maintainers prior to publication. The proof library axioms could be replaced with definitions. Reference erasure and unmodeled Drop are fundamental, and fixing them would require extraction at the MIR level, as Aeneas does. Beyond these, the backend should offer a release-mode arithmetic option (&lt;code&gt;+.&lt;/code&gt; instead of &lt;code&gt;+!&lt;/code&gt;), and &lt;code&gt;assume!&lt;/code&gt; should check consistency against the proof context via a Z3 query before injecting axioms.&lt;/p&gt;
&lt;h2 id=&#34;the-full-picture&#34;&gt;The Full Picture&lt;/h2&gt;
&lt;p&gt;Our &lt;a href=&#34;https://eprint.iacr.org/2026/192&#34;&gt;previous work&lt;/a&gt; documented code &lt;em&gt;outside&lt;/em&gt; the verified perimeter: wrong specifications, false proofs, admitted modules. The verification facade concerns the perimeter itself. Even for code that is extracted and whose proofs are dispatched to Z3, the verified model can diverge from the deployed code: while loops are silently proof-inert, release-mode arithmetic differs from the model, &lt;code&gt;assume!&lt;/code&gt; can poison the proof context, and axiomatized operations have no semantic content for Z3 to reason about.&lt;/p&gt;
&lt;p&gt;These gaps have direct FIPS implications. ML-KEM and ML-DSA implementations verified via hax are deployed where FIPS compliance is relevant. Rejection sampling in both algorithms uses &lt;code&gt;while&lt;/code&gt; loops. Production modules compile in release mode. The verification provides less assurance than it appears to.&lt;/p&gt;
&lt;p&gt;Together with our earlier findings, the full picture is now this. The &lt;a href=&#34;https://symbolic.software/blog/2026-02-12-ce-labs-response/&#34;&gt;specifications are wrong&lt;/a&gt;. The &lt;a href=&#34;https://symbolic.software/blog/2026-02-12-ce-labs-response/&#34;&gt;proofs are false or never checked&lt;/a&gt;. The &lt;a href=&#34;https://symbolic.software/blog/2026-02-12-ce-labs-response/&#34;&gt;build system defaults to admitting everything&lt;/a&gt;. The &lt;a href=&#34;https://symbolic.software/blog/2026-02-05-ce-labs/&#34;&gt;unverified wrappers&lt;/a&gt; contain implementation defects. The &lt;a href=&#34;https://symbolic.software/blog/2026-03-07-ce-labs-tls/&#34;&gt;TLS implementations&lt;/a&gt; reject valid signatures and accept certificates from anyone. And now: even when verification &lt;em&gt;is&lt;/em&gt; performed, the model that F* verifies can diverge from the program that Rust executes.&lt;/p&gt;
&lt;p&gt;Hax is an active research project that contributes real work to high-assurance cryptography. For pure, bounded-loop functions with precise specifications, it verifies what it claims to verify. Our critique targets structural limitations of the extraction approach, not the project&amp;rsquo;s intent. But the translation pipeline is part of the trusted computing base, and verification claims for extracted code require the same qualification and defense-in-depth practices that the formal methods community has long advocated, and that CE Labs has consistently failed to provide.&lt;/p&gt;





&lt;nav class=&#34;series-nav&#34; aria-label=&#34;CE Labs series (bottom)&#34;&gt;
  &lt;header class=&#34;series-nav-header&#34;&gt;
    &lt;span class=&#34;series-nav-label&#34;&gt;▶ CE Labs Series&lt;/span&gt;
    &lt;span class=&#34;series-nav-count&#34;&gt;5 posts · 2 papers · 1 talk&lt;/span&gt;
  &lt;/header&gt;
  &lt;ol class=&#34;series-nav-list&#34;&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-02-05-ce-labs/&#34;&gt;On the Promises of &amp;#39;High-Assurance&amp;#39; Cryptography&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.02.05&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-02-12-ce-labs-response/&#34;&gt;We Found Bugs in CE Labs&amp;#39; Verified Code&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.02.12&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-02-17-ce-labs-mldsa/&#34;&gt;Even More Bugs in CE Labs&amp;#39; libcrux: ML-DSA&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.02.17&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-03-07-ce-labs-tls/&#34;&gt;CE Labs&amp;#39; Approach to TLS: A Critical Analysis&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.03.07&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li class=&#34;current&#34; aria-current=&#34;page&#34;&gt;
      
      &lt;span class=&#34;title&#34;&gt;The Verification Facade: Structural Gaps in Cryspen&amp;#39;s Hax Pipeline&lt;/span&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.04.07&lt;/span&gt;
    &lt;/li&gt;
    
  &lt;/ol&gt;
  &lt;div class=&#34;series-nav-footer&#34;&gt;
    &lt;div class=&#34;series-nav-row&#34;&gt;
      &lt;span class=&#34;series-nav-rowlabel&#34;&gt;Papers&lt;/span&gt;
      &lt;a href=&#34;https://eprint.iacr.org/2026/192&#34;&gt;&lt;em&gt;Verification Theatre&lt;/em&gt;&lt;/a&gt;
      &lt;span class=&#34;series-nav-sep&#34;&gt;·&lt;/span&gt;
      &lt;a href=&#34;https://eprint.iacr.org/2026/670&#34;&gt;&lt;em&gt;Verification Facade&lt;/em&gt;&lt;/a&gt;
    &lt;/div&gt;
    &lt;div class=&#34;series-nav-row&#34;&gt;
      &lt;span class=&#34;series-nav-rowlabel&#34;&gt;Talk&lt;/span&gt;
      &lt;a href=&#34;https://www.youtube.com/watch?v=TdOXza1-M_4&#34;&gt;&lt;em&gt;High Assurance Cryptography and the Ethics of Disclosure (OSTIF)&lt;/em&gt;&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/nav&gt;

</content:encoded>
      <category>Research</category>
      <category>Formal Verification</category>
      
    </item>
    
    <item>
      <title>Recommending Post-Quantum Native Design Under Epistemic Duress</title>
      <link>https://symbolic.software/blog/2026-04-02-pq-native/</link>
      <pubDate>Thu, 02 Apr 2026 00:00:00 &#43;0000</pubDate>
      <guid>https://symbolic.software/blog/2026-04-02-pq-native/</guid>
      <description>Symbolic Software is recommending post-quantum native design for all new cryptographic systems. This post examines the evidence behind that recommendation, its limitations, and the epistemic questions the industry should be confronting.</description>
      <content:encoded>&lt;p&gt;Effective immediately, Symbolic Software is recommending that all clients design new cryptographic systems as post-quantum native. This post explains the reasoning behind that recommendation, subjects the evidence to the scrutiny it deserves, and raises questions about the epistemic standards the applied cryptography community should be holding itself to as the post-quantum transition accelerates.&lt;/p&gt;
&lt;p&gt;The recommendation is, on balance, probably correct. But &amp;ldquo;probably correct&amp;rdquo; is not the standard this profession is supposed to work to. The gap between the two deserves honest examination.&lt;/p&gt;
&lt;h2 id=&#34;the-google-quantum-ai-result&#34;&gt;The Google Quantum AI Result&lt;/h2&gt;
&lt;p&gt;On March 30, 2026, a team led by Google Quantum AI, with co-authors from UC Berkeley, the Ethereum Foundation, and Stanford, &lt;a href=&#34;https://research.google/blog/safeguarding-cryptocurrency-by-disclosing-quantum-vulnerabilities-responsibly/&#34;&gt;published a whitepaper&lt;/a&gt; presenting updated quantum resource estimates for solving the elliptic curve discrete logarithm problem on the secp256k1 curve (ECDLP-256), the curve underpinning the vast majority of cryptocurrency wallet signatures and a significant share of TLS deployments. The headline claim is a nearly 20-fold reduction in physical qubit requirements compared to Litinski&amp;rsquo;s 2023 estimates, which were themselves already a substantial improvement over earlier work.&lt;/p&gt;
&lt;p&gt;Two compiled circuits are presented, representing different points on the space-time tradeoff curve:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Circuit A (low-qubit variant):&lt;/strong&gt; $\leq 1{,}200$ logical qubits and $\leq 90$ million Toffoli gates.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Circuit B (low-gate variant):&lt;/strong&gt; $\leq 1{,}450$ logical qubits and $\leq 70$ million Toffoli gates.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Under assumptions about superconducting qubit hardware (physical error rates at or below $10^{-3}$, planar degree-4 connectivity), these circuits are projected to execute on fewer than 500,000 physical qubits in a matter of minutes. The resource cost curve for ECDLP-256 is now tracking the same downward trajectory that RSA-2048 followed over the preceding decade.&lt;/p&gt;
&lt;p&gt;The result is significant regardless of one&amp;rsquo;s position on post-quantum timelines. It narrows the gap between theoretical quantum advantage and engineering feasibility in a way that demands serious attention.&lt;/p&gt;
&lt;h2 id=&#34;a-novel-disclosure-model&#34;&gt;A Novel Disclosure Model&lt;/h2&gt;
&lt;p&gt;The paper introduces something new to the quantum cryptanalysis literature. Rather than publishing the full circuit constructions, the authors provide a zero-knowledge proof, specifically a Groth16 SNARK executed within the SP1 zkVM, attesting that they possess a quantum kickmix circuit of the claimed size that correctly computes secp256k1 elliptic curve point addition on 9,024 Fiat-Shamir-sampled pseudo-random inputs. (A kickmix circuit, as defined in the paper, is composed of classical reversible logic gates, measurement-based uncomputation, and diagonal phasing gates for phase correction: efficiently simulable classically, but structured for direct quantum execution.)&lt;/p&gt;
&lt;p&gt;The stated rationale is responsible disclosure. Publishing optimized quantum circuits for breaking deployed elliptic curve cryptography would hand a blueprint to any future adversary with access to a sufficiently large quantum computer. The ZK approach attempts to let the community verify the &lt;em&gt;plausibility&lt;/em&gt; of the resource estimate without providing the &lt;em&gt;means&lt;/em&gt; to execute the attack.&lt;/p&gt;
&lt;p&gt;This is an intellectually interesting approach and, in principle, a reasonable application of zero-knowledge techniques to a disclosure problem that has no good precedent. The authors deserve credit for thinking carefully about the responsible communication of quantum cryptanalytic capability, particularly in a domain like cryptocurrency, where public confidence is itself a security-relevant property.&lt;/p&gt;
&lt;h2 id=&#34;what-the-proof-covers-and-what-it-doesnt&#34;&gt;What the Proof Covers, and What It Doesn&amp;rsquo;t&lt;/h2&gt;
&lt;p&gt;The ZK proof must be evaluated precisely for what it attests to and what it leaves open.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What is proven:&lt;/strong&gt; The authors possess a quantum kickmix circuit that correctly computes secp256k1 point addition, and that circuit fits within the claimed resource counts. This is verified against 9,024 pseudo-random inputs derived via the Fiat-Shamir heuristic (SHAKE256 seeded with the circuit&amp;rsquo;s own bytes). The paper provides an explicit soundness bound: if the circuit produced incorrect outputs on more than 1% of inputs, the probability of passing all 9,024 tests is at most $(1-0.01)^{9024} \approx 2^{-130}$, yielding 128 bits of cryptographic security for the claim that the circuit is approximately correct on at least 99% of inputs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What is not proven:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The full Shor compilation is not attested.&lt;/strong&gt; The ZK proof covers the elliptic curve point addition subroutine, which the authors identify as the computational bottleneck. The reduction from point-addition cost to full end-to-end Shor execution cost relies on published windowed arithmetic techniques (double-and-add chains, interleaved modular multiplication). These techniques are well-established in the literature and the reduction is reasonable, but it remains an inferential step. The proof does not verify the complete quantum circuit that would actually break a key. An error anywhere else in the compilation, in the modular inversion or the quantum Fourier transform or the classical pre- and post-processing, would not be caught by this proof.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The soundness guarantee covers approximate correctness, not exact correctness.&lt;/strong&gt; The paper&amp;rsquo;s soundness analysis establishes that the circuit is correct on at least 99% of inputs with $2^{-130}$ failure probability, which is a strong bound. But Shor&amp;rsquo;s algorithm is tolerant of this: a superposition with 1% incorrect values causes the algorithm to fail at most 1% of the time. What is less explicitly analyzed is the end-to-end relationship between &amp;ldquo;the point addition subroutine is 99%-correct on random inputs&amp;rdquo; and &amp;ldquo;the full Shor circuit, composed of 28 windowed point additions interleaved with modular arithmetic and a quantum Fourier transform, produces correct discrete logarithms.&amp;rdquo; The subroutine correctness bound is rigorous; the compositional argument connecting it to the top-level ECDLP claim rests on standard but unstated assumptions about how errors in the subroutine propagate through the larger algorithm.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The physical qubit projection rests on hardware assumptions that do not yet hold.&lt;/strong&gt; The 500,000 physical qubit figure assumes error rates at or below $10^{-3}$ sustained across planar degree-4 connectivity at scale. Current superconducting qubit hardware has demonstrated error rates in this range on small systems, but maintaining these rates at the 500,000-qubit scale involves engineering challenges (crosstalk, wiring density, cooling) that have not been demonstrated. The projection is plausible given current trajectories, but it is a projection, not a measurement.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The circuits themselves are not available for independent analysis.&lt;/strong&gt; This is by design, since the entire point of the ZK approach is to withhold the circuits. But it means the community cannot independently verify the circuit architecture, check for optimization opportunities that might further reduce costs, or identify errors in the compilation strategy. The claim is auditable only to the extent that the ZK proof permits.&lt;/p&gt;
&lt;p&gt;None of these observations make the paper wrong. Taken together, they make the paper a strong signal that is not yet conclusive evidence. This distinction matters.&lt;/p&gt;
&lt;h2 id=&#34;the-risk-calculus-behind-the-recommendation&#34;&gt;The Risk Calculus Behind the Recommendation&lt;/h2&gt;
&lt;p&gt;Given the above, why is Symbolic Software recommending post-quantum native design?&lt;/p&gt;
&lt;p&gt;The answer is the asymmetry of consequences.&lt;/p&gt;
&lt;p&gt;Consider the two error modes. If the recommendation is premature, and the quantum timeline turns out to be significantly longer than the resource estimates suggest, the cost to clients is engineering overhead: larger key sizes, larger signatures, increased bandwidth, more complex protocol negotiations, and the implementation risks inherent in deploying newer cryptographic primitives. These costs are real and non-trivial, but they are recoverable. A system designed with ML-KEM and ML-DSA that turns out not to have needed them for another fifteen years is a system that over-invested in security. There are worse failure modes.&lt;/p&gt;
&lt;p&gt;If the recommendation is late, and the estimates are roughly correct so that classical elliptic curve cryptography becomes vulnerable within the operational lifetime of systems being designed today, the consequences are categorically different. Private keys are extractable. Signatures are forgeable. Store-now-decrypt-later attacks, in which encrypted traffic captured today is decrypted by a future quantum adversary, become retroactively devastating. These consequences are not recoverable. Data that has been exfiltrated cannot be un-exfiltrated.&lt;/p&gt;
&lt;p&gt;This asymmetry is not new. It has been the standard argument for post-quantum preparedness for years. What the Google result changes is the &lt;em&gt;urgency&lt;/em&gt; of the timeline, not the &lt;em&gt;structure&lt;/em&gt; of the argument. The resource estimates have now improved to the point where &amp;ldquo;within the operational lifetime of systems being designed today&amp;rdquo; is no longer a worst-case assumption but a median-case projection.&lt;/p&gt;
&lt;h2 id=&#34;the-harder-question-epistemic-standards-under-pressure&#34;&gt;The Harder Question: Epistemic Standards Under Pressure&lt;/h2&gt;
&lt;p&gt;The risk calculus is straightforward. The harder question is about the epistemic standards the community is applying as it navigates the post-quantum transition.&lt;/p&gt;
&lt;p&gt;Applied cryptography has historically operated under a norm of open, reproducible, and fully auditable evidence. Protocols are published. Proofs are checked. Implementations are reviewed. When a vulnerability is claimed, the expectation is that the claim can be independently verified, not as a matter of trust but as a matter of scientific practice. This norm exists for good reason: the history of the field is littered with examples of informal reasoning, indirect signals, and trusted-but-wrong claims leading to catastrophic outcomes. Snake oil gets sold on the basis of plausible-sounding arguments. Bad standards get adopted because the evidence supporting them was not subjected to sufficient scrutiny.&lt;/p&gt;
&lt;p&gt;The Google team&amp;rsquo;s ZK disclosure model represents a deliberate departure from this norm. The departure is well-motivated, and there is a legitimate argument that publishing optimized quantum attack circuits would cause more harm than benefit. But it creates a new epistemic regime in which the community is asked to make critical infrastructure decisions based on claims that are, by design, not fully auditable.&lt;/p&gt;
&lt;p&gt;Several aspects of this regime deserve scrutiny:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The precedent effect.&lt;/strong&gt; If &amp;ldquo;trust the ZK proof, don&amp;rsquo;t ask to see the circuits&amp;rdquo; becomes the accepted standard for quantum cryptanalysis disclosure, the community will be building security policy on a foundation where the underlying evidence is structurally unverifiable beyond what the prover chooses to reveal. This is an uncomfortable position regardless of the credibility of the prover, and Google Quantum AI is a very credible prover. The question is not whether &lt;em&gt;this team&lt;/em&gt; can be trusted, but whether &lt;em&gt;this disclosure model&lt;/em&gt; should become normative. Norms are not evaluated against the best-case actor; they are evaluated against the full range of actors who will invoke them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The gap between verified and verifiable.&lt;/strong&gt; A ZK proof provides cryptographic assurance that a specific computation was performed correctly. It does not provide the community with the ability to &lt;em&gt;understand&lt;/em&gt; what was computed, to identify potential improvements, or to catch errors in the broader reasoning that connects the proven subroutine to the top-level claim. In traditional disclosure, a published circuit can be independently optimized, refuted, or extended. Under ZK disclosure, the community&amp;rsquo;s role is reduced from &lt;em&gt;auditor&lt;/em&gt; to &lt;em&gt;verifier of a predefined claim&lt;/em&gt;. These are different epistemic positions, and the latter is weaker.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The relationship between conservatism and rigor.&lt;/strong&gt; The natural response to all of the above is: &amp;ldquo;Just be conservative. Assume the attack works and design against it. That&amp;rsquo;s Kerckhoffs&amp;rsquo; principle applied to threat modeling.&amp;rdquo; This response is correct as far as it goes, and it is the basis of the recommendation being made here. But there is a meaningful difference between conservatism as a &lt;em&gt;design principle&lt;/em&gt;, where uncertainty is resolved in favor of caution, and conservatism as an &lt;em&gt;epistemic shortcut&lt;/em&gt;, where uncertainty is invoked to avoid the hard work of evaluating evidence rigorously. The former strengthens the field. The latter, if left unchecked, erodes the standards that make the field&amp;rsquo;s work meaningful.&lt;/p&gt;
&lt;p&gt;The applied cryptography community has spent decades building a culture in which claims are expected to be substantiated, assumptions are expected to be explicit, and &amp;ldquo;it seems plausible&amp;rdquo; is not considered sufficient grounds for action. The post-quantum transition is now testing whether that culture holds under the pressure of urgency.&lt;/p&gt;
&lt;h2 id=&#34;what-should-happen-next&#34;&gt;What Should Happen Next&lt;/h2&gt;
&lt;p&gt;Several concrete steps would strengthen the evidentiary basis for post-quantum migration decisions:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Independent reproduction of the resource estimates.&lt;/strong&gt; The Google result is currently a single data point. A well-attested single data point, but a single data point nonetheless. Independent teams should attempt to reproduce or improve on the ECDLP-256 resource estimates using the publicly available literature on quantum arithmetic and Shor compilation. This is not an expression of doubt in the Google result. It is standard scientific practice. A global cryptographic migration should not be calibrated against a claim that only one team can verify.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;End-to-end soundness analysis of the ZK disclosure model.&lt;/strong&gt; The paper provides a clear soundness bound for the subroutine ($2^{-130}$ failure probability for 99%-correctness). What remains implicit is the compositional argument: how subroutine-level approximate correctness propagates through the full Shor circuit (28 windowed point additions, modular arithmetic, QFT) to yield a correct discrete logarithm. The authors also note a striking irony: the Groth16 SNARK itself relies on pairing-friendly elliptic curves vulnerable to the very quantum attacks the paper analyzes, which bounds the proof&amp;rsquo;s validity to the pre-CRQC era. A formal end-to-end analysis connecting the ZK-attested subroutine properties to the top-level ECDLP claim would strengthen the disclosure model considerably.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Continued hardware benchmarking against the projected requirements.&lt;/strong&gt; The 500,000 physical qubit projection is credible given current trajectories, but it depends on engineering parameters that should be tracked publicly. As superconducting qubit systems scale, the community should maintain a clear, openly accessible mapping between demonstrated hardware capabilities and the requirements of the published quantum circuits. This mapping would provide a more rigorous basis for timeline estimates than trend extrapolation alone.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;An honest conversation about epistemic norms under uncertainty.&lt;/strong&gt; The applied cryptography community needs to explicitly discuss how it will handle situations where the evidence for a threat is strong but not fully transparent. The current post-quantum transition is the first instance of this problem at scale, but it will not be the last. The field would be served well beyond the current transition by settling now on what constitutes sufficient evidence for different classes of decisions, what obligations fall on provers who withhold details, and what role independent verification should play.&lt;/p&gt;
&lt;h2 id=&#34;the-recommendation&#34;&gt;The Recommendation&lt;/h2&gt;
&lt;p&gt;Symbolic Software is advising all clients to design new cryptographic systems as post-quantum native. This means ML-KEM for key encapsulation, ML-DSA for digital signatures, and hybrid constructions where backward compatibility with classical systems is required during transition. The recommendation applies to systems currently in the design phase; guidance on migrating existing systems is available on a per-engagement basis.&lt;/p&gt;
&lt;p&gt;This recommendation is made with full awareness that the evidence base, while strong, is not conclusive in the way the profession traditionally demands. The risk asymmetry justifies action. The evidentiary gaps justify continued scrutiny. Both of these things can be true simultaneously.&lt;/p&gt;
&lt;p&gt;The post-quantum transition should be pursued with urgency. It should not be pursued with credulity. The applied cryptography community&amp;rsquo;s greatest asset is its insistence on rigor, and that insistence should not be the first casualty of the transition it is meant to protect.&lt;/p&gt;
&lt;p&gt;For the operational follow-through on this recommendation, covering primitive selection, hybrid construction, TLS migration mechanics, the state of the libraries, conformance testing, rollout strategy, and the bug classes we keep finding in audits, see our free &lt;a href=&#34;https://pq-migration.symbolic.software/static/pdf/playbook.pdf&#34;&gt;Post-Quantum Migration Playbook&lt;/a&gt;, companion to the live &lt;a href=&#34;https://pq-migration.symbolic.software&#34;&gt;PQ Migration Readiness scorecard and scanner&lt;/a&gt;.&lt;/p&gt;
</content:encoded>
      <category>Research</category>
      <category>Post-Quantum</category>
      
    </item>
    
    <item>
      <title>Applied Cryptography: Free Online Course for 50 Lebanese University Students This Summer</title>
      <link>https://symbolic.software/blog/2026-03-23-applied-cryptography-summer/</link>
      <pubDate>Mon, 23 Mar 2026 00:00:00 &#43;0000</pubDate>
      <guid>https://symbolic.software/blog/2026-03-23-applied-cryptography-summer/</guid>
      <description>We&#39;re opening 50 spots for students at Lebanese universities to take the Applied Cryptography course online, completely free of charge, starting June 2026. Applications are open now.</description>
      <content:encoded>&lt;p&gt;The &lt;a href=&#34;https://appliedcryptography.page&#34;&gt;Applied Cryptography&lt;/a&gt; course that I taught last year at the American University of Beirut is coming back this summer as a free online program, open to 50 students at Lebanese universities nationwide.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;This is a full-featured course with strong instructor-student engagement&lt;/strong&gt;. The same lectures, the same problem sets, the same projects, the same rigor, delivered online over the summer to a small, selectively admitted cohort. If you are a university student in Lebanon and you want to learn modern cryptography properly, this is your application: &lt;a href=&#34;https://register.appliedcryptography.page/&#34;&gt;&lt;strong&gt;apply here&lt;/strong&gt;&lt;/a&gt;. The deadline is &lt;strong&gt;May 10, 2026&lt;/strong&gt;. The course starts &lt;strong&gt;June 9, 2026&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id=&#34;what-the-course-covers&#34;&gt;What the Course Covers&lt;/h2&gt;
&lt;p&gt;Applied Cryptography is a two-part course. The first part builds the theoretical foundations: provable security, pseudorandomness, chosen-plaintext and chosen-ciphertext attacks, collision-resistant hash functions, the discrete logarithm problem, Diffie-Hellman, elliptic curves, and digital signatures. The second part applies those foundations to real-world systems: TLS, the story of RC4, secure messaging (Signal, OTR, MLS), end-to-end encrypted cloud storage, high-assurance implementations, post-quantum cryptography, cryptocurrency cryptography, zero-knowledge proofs, secure multiparty computation, and fully homomorphic encryption.&lt;/p&gt;
&lt;p&gt;The course has 18 topics, 8 problem sets, and 8 hands-on projects (students choose one or two). Projects range from building a password manager to designing a secure messenger, formally verifying a TLS-like protocol in ProVerif, implementing a zero-knowledge battleship game, and migrating a system to post-quantum cryptography. The programming languages are Go and Rust.&lt;/p&gt;
&lt;p&gt;The required textbooks are Mike Rosulek&amp;rsquo;s &lt;em&gt;The Joy of Cryptography&lt;/em&gt; and Jean-Philippe Aumasson&amp;rsquo;s &lt;em&gt;Serious Cryptography, 2nd Edition&lt;/em&gt;. There is also an extensive reading list of around 90 research papers and technical resources, all linked from the &lt;a href=&#34;https://appliedcryptography.page&#34;&gt;course website&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;how-it-works&#34;&gt;How It Works&lt;/h2&gt;
&lt;p&gt;The program runs entirely online:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Lectures&lt;/strong&gt; are delivered live, then published on YouTube so you can watch them on your own schedule.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discussion, Q&amp;amp;A, and community&lt;/strong&gt; happen on a dedicated Discord server.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Problem sets and projects&lt;/strong&gt; are submitted and graded through Google Classroom.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All course materials (slides, readings, problem sets, project specifications) are already publicly available on the &lt;a href=&#34;https://appliedcryptography.page&#34;&gt;course website&lt;/a&gt; under a Creative Commons license. The online program adds structured pacing, graded assignments, direct access to the instructor, and a cohort of peers to work with.&lt;/p&gt;
&lt;h2 id=&#34;who-should-apply&#34;&gt;Who Should Apply&lt;/h2&gt;
&lt;p&gt;You must be currently enrolled at a qualifying Lebanese university. Undergraduate or graduate. We will select 50 students based on:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Motivation.&lt;/strong&gt; Why do you want to learn cryptography? What do you intend to do with it?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Background.&lt;/strong&gt; Familiarity with at least one programming language and basic mathematical maturity (discrete math, probability, or any proof-based coursework).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Diversity.&lt;/strong&gt; We want representation from across Lebanese universities and academic backgrounds, not just the usual suspects.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;No prior cryptography experience is required. The course starts from first principles. If you have never seen a security reduction before, that is fine. You will by the end.&lt;/p&gt;
&lt;h2 id=&#34;certificate-and-recommendation-letters&#34;&gt;Certificate and Recommendation Letters&lt;/h2&gt;
&lt;p&gt;Students who complete the course will receive a signed certificate of completion. High-achieving students may be eligible for personalized letters of recommendation for graduate school applications.&lt;/p&gt;
&lt;h2 id=&#34;why-were-doing-this&#34;&gt;Why We&amp;rsquo;re Doing This&lt;/h2&gt;
&lt;p&gt;Lebanon has a remarkable density of talented computer science students who, for various well-known reasons, do not always have access to specialized coursework in areas like cryptography. This is not a commentary on any university&amp;rsquo;s curriculum. It is an observation that the gap between what is taught in a standard undergraduate CS program and what is needed to do serious work in cryptography, in research or industry or open source, is large, and that closing it should not depend on which university you happened to enroll in or whether you can afford a semester abroad.&lt;/p&gt;
&lt;p&gt;This course exists because I believe applied cryptography education should be accessible to any motivated student in Lebanon who wants it. The course materials are already open. The lectures will be open. The only scarce resource is structured mentorship and graded feedback, and we are making that available to 50 students this summer at no cost.&lt;/p&gt;
&lt;h2 id=&#34;looking-for-tas&#34;&gt;Looking for TAs&lt;/h2&gt;
&lt;p&gt;My students from last semester: in case you&amp;rsquo;re reading this, get in touch. There&amp;rsquo;s no way I&amp;rsquo;m grading fifty students on my lonesome again! I need your help!&lt;/p&gt;
&lt;h2 id=&#34;apply&#34;&gt;Apply&lt;/h2&gt;
&lt;p&gt;The application is short. Tell us who you are, where you study, why you want to take this course, and what background you bring. That&amp;rsquo;s it.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://register.appliedcryptography.page/&#34;&gt;&lt;strong&gt;Apply now!&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
</content:encoded>
      <category>Announcement</category>
      <category>Applied Cryptography</category>
      <category>Course</category>
      
    </item>
    
    <item>
      <title>Crucible: Forging Post-Quantum Implementations in the Fire of Real Audit Findings</title>
      <link>https://symbolic.software/blog/2026-03-23-crucible/</link>
      <pubDate>Mon, 23 Mar 2026 00:00:00 &#43;0000</pubDate>
      <guid>https://symbolic.software/blog/2026-03-23-crucible/</guid>
      <description>We tested 15 ML-KEM and ML-DSA implementations across 5 languages, and found two conformance gaps and no security vulnerabilities.</description>
      <content:encoded>&lt;p&gt;NIST&amp;rsquo;s Known Answer Tests verify final outputs. Formal verification tools verify abstract models. Neither catches the bugs that live in between: the off-by-one rounding, the bounds check compiled to dead code, the inverse NTT omitted from the decryption path.&lt;/p&gt;
&lt;p&gt;We know these bugs exist because we&amp;rsquo;ve &lt;a href=&#34;https://symbolic.software/blog/2026-02-05-ce-labs/&#34;&gt;found them&lt;/a&gt; in production cryptographic code, including code marketed as &amp;ldquo;formally verified.&amp;rdquo; So we built a tool to catch them systematically.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/symbolicsoft/crucible&#34;&gt;Crucible&lt;/a&gt; is a conformance testing framework for ML-KEM (FIPS 203) and ML-DSA (FIPS 204) implementations. It encodes the specific bug classes we&amp;rsquo;ve encountered in real audits as targeted, reusable test batteries that any implementation can be wired up to. Then we pointed it at every ML-KEM and ML-DSA implementation we could find.&lt;/p&gt;
&lt;h2 id=&#34;the-setup&#34;&gt;The Setup&lt;/h2&gt;
&lt;p&gt;Crucible communicates with implementations through a simple JSON line protocol over stdin/stdout. A thin harness shim maps Crucible&amp;rsquo;s function calls to the implementation&amp;rsquo;s API. We built harnesses for 15 implementations across Rust, Go, C, C++20, and Java:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Implementation&lt;/th&gt;
          &lt;th&gt;Language&lt;/th&gt;
          &lt;th&gt;Who uses it&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Reference (Crucible)&lt;/td&gt;
          &lt;td&gt;Rust&lt;/td&gt;
          &lt;td&gt;Our known-good baseline, built alongside the test suite&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;a href=&#34;https://github.com/celabshq/libcrux&#34;&gt;libcrux&lt;/a&gt;&lt;/td&gt;
          &lt;td&gt;Rust&lt;/td&gt;
          &lt;td&gt;CE Labs&amp;rsquo; formally verified library&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;a href=&#34;https://github.com/symbolicsoft/kyber-k2so&#34;&gt;Kyber-K2SO&lt;/a&gt;&lt;/td&gt;
          &lt;td&gt;Go&lt;/td&gt;
          &lt;td&gt;Our own implementation&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;a href=&#34;https://github.com/pq-code-package/mlkem-native&#34;&gt;mlkem-native&lt;/a&gt;&lt;/td&gt;
          &lt;td&gt;C&lt;/td&gt;
          &lt;td&gt;Ships inside AWS-LC and liboqs&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;a href=&#34;https://pkg.go.dev/crypto/mlkem&#34;&gt;Go stdlib&lt;/a&gt; &lt;code&gt;crypto/mlkem&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;Go&lt;/td&gt;
          &lt;td&gt;Every Go 1.26+ binary using TLS 1.3&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;a href=&#34;https://github.com/aws/aws-lc&#34;&gt;AWS-LC&lt;/a&gt;&lt;/td&gt;
          &lt;td&gt;C&lt;/td&gt;
          &lt;td&gt;AWS KMS, S3, CloudFront&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;a href=&#34;https://github.com/pq-crystals/kyber&#34;&gt;pq-crystals ref&lt;/a&gt;&lt;/td&gt;
          &lt;td&gt;C&lt;/td&gt;
          &lt;td&gt;The original reference from the algorithm designers&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;a href=&#34;https://github.com/cloudflare/circl&#34;&gt;CIRCL&lt;/a&gt;&lt;/td&gt;
          &lt;td&gt;Go&lt;/td&gt;
          &lt;td&gt;Cloudflare&amp;rsquo;s TLS deployment&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;a href=&#34;https://github.com/open-quantum-safe/liboqs&#34;&gt;liboqs&lt;/a&gt;&lt;/td&gt;
          &lt;td&gt;C&lt;/td&gt;
          &lt;td&gt;The most widely used PQC prototyping library&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;a href=&#34;https://github.com/bcgit/bc-java&#34;&gt;Bouncy Castle&lt;/a&gt;&lt;/td&gt;
          &lt;td&gt;Java&lt;/td&gt;
          &lt;td&gt;The dominant JVM crypto library&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;a href=&#34;https://github.com/wolfSSL/wolfssl&#34;&gt;wolfCrypt&lt;/a&gt;&lt;/td&gt;
          &lt;td&gt;C&lt;/td&gt;
          &lt;td&gt;Embedded/IoT, FIPS 140-3 validated&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;a href=&#34;https://github.com/itzmeanjan/ml-kem&#34;&gt;itzmeanjan/ml-kem&lt;/a&gt;&lt;/td&gt;
          &lt;td&gt;C++20&lt;/td&gt;
          &lt;td&gt;Header-only, fully &lt;code&gt;constexpr&lt;/code&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;a href=&#34;https://github.com/rustpq/pqcrypto&#34;&gt;pqcrypto&lt;/a&gt;&lt;/td&gt;
          &lt;td&gt;Rust&lt;/td&gt;
          &lt;td&gt;Rust FFI bindings to PQClean&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;a href=&#34;https://github.com/PQClean/PQClean&#34;&gt;PQClean&lt;/a&gt;&lt;/td&gt;
          &lt;td&gt;C&lt;/td&gt;
          &lt;td&gt;Clean portable C implementations&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;a href=&#34;https://github.com/trailofbits/ml-dsa&#34;&gt;Trail of Bits ml-dsa&lt;/a&gt;&lt;/td&gt;
          &lt;td&gt;Go&lt;/td&gt;
          &lt;td&gt;Side-channel resistant ML-DSA&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The ML-KEM battery has 78 tests across 6 categories (compression arithmetic, NTT correctness, coefficient bounds enforcement, decapsulation robustness, serialization, rejection sampling). The ML-DSA battery has 51 tests across 6 categories (norm checks, arithmetic, signing internals, verification edge cases, serialization, constant-time behavior). Every test is tagged with the bug class it targets, the FIPS spec section it verifies, and, where applicable, the audit finding that motivated it.&lt;/p&gt;
&lt;h2 id=&#34;the-results&#34;&gt;The Results&lt;/h2&gt;
&lt;p&gt;We ran 1,296 test executions. Here&amp;rsquo;s the ML-KEM scorecard:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Implementation&lt;/th&gt;
          &lt;th&gt;Pass&lt;/th&gt;
          &lt;th&gt;Fail&lt;/th&gt;
          &lt;th&gt;Skip&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Reference (Crucible)&lt;/td&gt;
          &lt;td&gt;78&lt;/td&gt;
          &lt;td&gt;0&lt;/td&gt;
          &lt;td&gt;0&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;libcrux v0.0.8&lt;/td&gt;
          &lt;td&gt;54&lt;/td&gt;
          &lt;td&gt;0&lt;/td&gt;
          &lt;td&gt;24&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Kyber-K2SO v1.0.0&lt;/td&gt;
          &lt;td&gt;54&lt;/td&gt;
          &lt;td&gt;0&lt;/td&gt;
          &lt;td&gt;24&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;mlkem-native&lt;/td&gt;
          &lt;td&gt;21&lt;/td&gt;
          &lt;td&gt;0&lt;/td&gt;
          &lt;td&gt;45&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Go stdlib&lt;/td&gt;
          &lt;td&gt;25&lt;/td&gt;
          &lt;td&gt;0&lt;/td&gt;
          &lt;td&gt;45&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;AWS-LC&lt;/td&gt;
          &lt;td&gt;33&lt;/td&gt;
          &lt;td&gt;0&lt;/td&gt;
          &lt;td&gt;45&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;CIRCL (Cloudflare)&lt;/td&gt;
          &lt;td&gt;33&lt;/td&gt;
          &lt;td&gt;0&lt;/td&gt;
          &lt;td&gt;45&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;liboqs&lt;/td&gt;
          &lt;td&gt;33&lt;/td&gt;
          &lt;td&gt;0&lt;/td&gt;
          &lt;td&gt;45&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;wolfCrypt&lt;/td&gt;
          &lt;td&gt;33&lt;/td&gt;
          &lt;td&gt;0&lt;/td&gt;
          &lt;td&gt;45&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;itzmeanjan&lt;/td&gt;
          &lt;td&gt;21&lt;/td&gt;
          &lt;td&gt;0&lt;/td&gt;
          &lt;td&gt;45&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Bouncy Castle 1.80&lt;/td&gt;
          &lt;td&gt;25&lt;/td&gt;
          &lt;td&gt;2&lt;/td&gt;
          &lt;td&gt;45&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;pq-crystals ref&lt;/td&gt;
          &lt;td&gt;19&lt;/td&gt;
          &lt;td&gt;2&lt;/td&gt;
          &lt;td&gt;45&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;PQClean&lt;/td&gt;
          &lt;td&gt;19&lt;/td&gt;
          &lt;td&gt;2&lt;/td&gt;
          &lt;td&gt;45&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;pqcrypto (rustpq)&lt;/td&gt;
          &lt;td&gt;21&lt;/td&gt;
          &lt;td&gt;6&lt;/td&gt;
          &lt;td&gt;45&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;(Skipped tests are NTT/sampling internals that implementations don&amp;rsquo;t expose through their public API. Errors from single-parameter-set harnesses are omitted for clarity.)&lt;/p&gt;
&lt;h3 id=&#34;finding-1-missing-encapsulation-key-modulus-check&#34;&gt;Finding 1: Missing Encapsulation Key Modulus Check&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Affected:&lt;/strong&gt; pq-crystals/kyber ref, PQClean, pqcrypto/rustpq
&lt;strong&gt;Severity:&lt;/strong&gt; Conformance gap
&lt;strong&gt;Spec reference:&lt;/strong&gt; FIPS 203 §7.2, Equation 7.1&lt;/p&gt;
&lt;p&gt;FIPS 203 requires that before encapsulation, the encapsulation key must pass a modulus check:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;test ← ByteEncode₁₂(ByteDecode₁₂(ek[0 : 384k])). If test ≠ ek[0 : 384k], then input checking failed.&lt;/em&gt;&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;This check ensures that no 12-bit coefficient in the serialized public key encodes a value ≥ q = 3329. All three affected implementations accept a key with coefficient value 3329 without error. None of them provide a separate key validation function.&lt;/p&gt;
&lt;p&gt;This is a spec conformance gap, not a vulnerability. The practical impact is limited to interoperability: if two implementations handle an out-of-range coefficient differently (one reduces mod q, another wraps), the same key produces different shared secrets. The pq-crystals codebase predates FIPS 203, and the modulus check was added during standardization.&lt;/p&gt;
&lt;h3 id=&#34;finding-2-bouncy-castle-accepts-wrong-length-decapsulation-keys&#34;&gt;Finding 2: Bouncy Castle Accepts Wrong-Length Decapsulation Keys&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Affected:&lt;/strong&gt; Bouncy Castle 1.80 (Java)
&lt;strong&gt;Severity:&lt;/strong&gt; Input validation gap
&lt;strong&gt;Spec reference:&lt;/strong&gt; FIPS 203 §7.3&lt;/p&gt;
&lt;p&gt;Bouncy Castle&amp;rsquo;s &lt;code&gt;MLKEMPrivateKeyParameters&lt;/code&gt; constructor accepts byte arrays of any length without validating against the expected size for the parameter set. Crucible sends a decapsulation key that is 1 byte too short (2399 bytes for ML-KEM-768, expected 2400) and Bouncy Castle proceeds with decapsulation.&lt;/p&gt;
&lt;p&gt;A truncated dk means the implicit rejection secret &lt;code&gt;z&lt;/code&gt; is partially missing, which could weaken the FO transform&amp;rsquo;s security guarantee. In practice, the dk is private and an attacker wouldn&amp;rsquo;t normally control it, but a corrupted key file should fail loudly rather than silently produce wrong results.&lt;/p&gt;
&lt;h3 id=&#34;what-about-the-other-11-implementations&#34;&gt;What About the Other 11 Implementations?&lt;/h3&gt;
&lt;p&gt;They all passed. Every test. libcrux, mlkem-native, AWS-LC, CIRCL, liboqs, wolfCrypt, itzmeanjan, the Go standard library, and Trail of Bits ml-dsa: all clean.&lt;/p&gt;
&lt;h2 id=&#34;what-this-means&#34;&gt;What This Means&lt;/h2&gt;
&lt;p&gt;We set out to find the kinds of bugs we&amp;rsquo;d found in audits: missing inverse NTTs, dead bounds checks, incorrect rounding, timing leaks. We built 129 tests specifically targeting those bug classes. We tested the 15 most deployed post-quantum implementations in the world.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;We found two conformance gaps and zero security vulnerabilities.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This is actually the good news. The implementations that matter, the ones shipping inside AWS, Cloudflare, the Go standard library, and wolfSSL&amp;rsquo;s FIPS module, are doing the right things. The FIPS 203 standardization process added the right checks. The formally verified implementations (libcrux) pass. The clean-room implementations (mlkem-native, CIRCL, Trail of Bits) pass.&lt;/p&gt;
&lt;p&gt;The bugs we designed Crucible to catch existed in the pre-FIPS era and have largely been fixed. The ecosystem learned from the CE Labs findings, from KyberSlash, from the various timing advisories. The 2026 implementations are better than the 2024 ones.&lt;/p&gt;
&lt;h2 id=&#34;crucible-is-open-source&#34;&gt;Crucible Is Open Source&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/symbolicsoft/crucible&#34;&gt;Crucible is available on GitHub.&lt;/a&gt; It ships with harness templates for Rust, Go, and C. If you maintain an ML-KEM or ML-DSA implementation, wire it up and run the battery. If you find a bug we missed, it becomes a new test. The framework grows sharper over time.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;cargo build --release
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;cargo run --bin crucible -- ./your-harness
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;cargo run --bin crucible -- ./your-harness --battery ml-dsa --json
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The test batteries will continue to expand as we encounter new bug classes in future audits. The name is a Star Wars reference: the &lt;a href=&#34;https://www.starwars.com/databank/crucible&#34;&gt;Crucible&lt;/a&gt; is the ancient Jedi vessel where younglings forge their lightsabers.&lt;/p&gt;
&lt;p&gt;For more on where Crucible fits in a production post-quantum migration, covering primitive selection, hybrid constructions, library evaluation and rollout strategy, see our &lt;a href=&#34;https://pq-migration.symbolic.software/static/pdf/playbook.pdf&#34;&gt;Post-Quantum Migration Playbook&lt;/a&gt; and the companion &lt;a href=&#34;https://pq-migration.symbolic.software&#34;&gt;PQ Migration Readiness scorecard and scanner&lt;/a&gt;.&lt;/p&gt;
</content:encoded>
      <category>Software</category>
      <category>Research</category>
      <category>Crucible</category>
      <category>Kyber-K2SO</category>
      <category>Post-Quantum</category>
      
    </item>
    
    <item>
      <title>Summer 2026 Research Internship at Symbolic Software</title>
      <link>https://symbolic.software/blog/2026-03-19-internship/</link>
      <pubDate>Thu, 19 Mar 2026 00:00:00 &#43;0000</pubDate>
      <guid>https://symbolic.software/blog/2026-03-19-internship/</guid>
      <description>We&#39;re looking for a research intern to join us this summer and contribute to new papers on real-world cryptographic constructions.</description>
      <content:encoded>&lt;p&gt;Symbolic Software is looking for a research intern to join us for the summer of 2026. This is a hands-on position: you will co-author papers with me, &lt;a href=&#34;https://nadim.computer&#34;&gt;Nadim Kobeissi&lt;/a&gt;, on real-world cryptography, with a focus on designing new secure constructions that address open problems in cryptographic applications people actually deploy.&lt;/p&gt;
&lt;p&gt;I already have specific, concrete research projects and I want you to help me turn them into solid papers and artifacts that we can publish at strong conferences.&lt;/p&gt;
&lt;h2 id=&#34;what-were-working-on&#34;&gt;What We&amp;rsquo;re Working On&lt;/h2&gt;
&lt;p&gt;The research agenda centers on the gap between cryptographic theory and deployed systems. Secure messaging, authenticated key exchange, post-quantum migration and end-to-end encrypted media all rely on constructions that were often designed under assumptions that no longer hold, or that leave meaningful security properties on the table. We want to work out what the state of the art should be now.&lt;/p&gt;
&lt;p&gt;Concretely, the internship will involve working on new papers that propose, formalize, and analyze cryptographic constructions addressing real problems in real systems. This means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Identifying concrete shortcomings&lt;/strong&gt; in cryptographic constructions used by deployed applications today.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Designing new constructions&lt;/strong&gt; that improve on the state of the art, whether that means stronger security guarantees, better efficiency, or cleaner compositional properties.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Writing security proofs&lt;/strong&gt; and building formal or semi-formal arguments for why the new constructions achieve their claimed properties.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Producing publication-ready papers&lt;/strong&gt; targeting top-tier venues in applied cryptography and security.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The specific problems we tackle will depend in part on your interests and strengths, but the common thread is practical relevance: we are not interested in constructions that exist only to be proven secure. We want constructions that people will actually deploy.&lt;/p&gt;
&lt;h2 id=&#34;who-should-apply&#34;&gt;Who Should Apply&lt;/h2&gt;
&lt;p&gt;You should be a graduate student (Master&amp;rsquo;s or PhD) in cryptography, computer science, or a related field, or an exceptional undergraduate with demonstrated research ability. What matters most:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Strong foundations in cryptography.&lt;/strong&gt; You should be comfortable with standard security definitions, reduction-based proofs, and the range of modern cryptographic primitives. If you&amp;rsquo;ve taken a graduate-level cryptography course and found it straightforward, that&amp;rsquo;s a good sign.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Interest in real-world systems.&lt;/strong&gt; The best candidates are curious about how cryptography is actually used, and misused, in practice. If you&amp;rsquo;ve ever read a protocol specification and spotted something that bothered you, we should talk.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Writing ability.&lt;/strong&gt; Research papers require clear, precise writing. You don&amp;rsquo;t need to have published before, but you should be able to communicate technical ideas effectively in English.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Self-direction.&lt;/strong&gt; This is a small team. You&amp;rsquo;ll have guidance and close collaboration, but you should be capable of reading papers independently, formulating questions, and pursuing ideas without constant oversight.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Intellectual honesty.&lt;/strong&gt; Most especially, I&amp;rsquo;m looking for candidates that are honest about their work, how it&amp;rsquo;s conducted, and who are able to examine both third-party systems and their own research output critically.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Prior publications are a plus but not required. What matters is that you can think clearly about cryptographic problems and want to produce work that changes what gets deployed.&lt;/p&gt;
&lt;h2 id=&#34;details&#34;&gt;Details&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Duration:&lt;/strong&gt; Summer 2026 (approximately 3 months, with flexibility on exact dates).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Location:&lt;/strong&gt; Remote. Candidates in or near the Paris, France time zone are preferred.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compensation:&lt;/strong&gt; Paid internship. Details discussed during the application process.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deadline:&lt;/strong&gt; April 20, 2026&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;how-to-apply&#34;&gt;How to Apply&lt;/h2&gt;
&lt;p&gt;Send an email to &lt;a href=&#34;mailto:nadim@symbolic.software&#34;&gt;nadim@symbolic.software&lt;/a&gt; with:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Your CV or resume.&lt;/li&gt;
&lt;li&gt;A brief note (a few paragraphs is fine) on why this internship interests you and what kinds of cryptographic problems you find compelling.&lt;/li&gt;
&lt;li&gt;Any relevant prior work: publications, thesis drafts, course projects, or open-source contributions.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;No formal cover letter needed. Just be direct about who you are and what you want to work on.&lt;/p&gt;
</content:encoded>
      <category>Announcement</category>
      <category>Internship</category>
      
    </item>
    
    <item>
      <title>CE Labs&#39; Approach to TLS: A Critical Analysis</title>
      <link>https://symbolic.software/blog/2026-03-07-ce-labs-tls/</link>
      <pubDate>Sat, 07 Mar 2026 00:00:00 &#43;0000</pubDate>
      <guid>https://symbolic.software/blog/2026-03-07-ce-labs-tls/</guid>
      <description>An examination of CE Labs&#39; TLS implementations reveals 75% of valid ECDSA signatures rejected, authentication tags silently dropped, no certificate validation, and remote denial-of-service vectors.</description>
      <content:encoded>




&lt;nav class=&#34;series-nav&#34; aria-label=&#34;CE Labs series (top)&#34;&gt;
  &lt;header class=&#34;series-nav-header&#34;&gt;
    &lt;span class=&#34;series-nav-label&#34;&gt;▶ CE Labs Series&lt;/span&gt;
    &lt;span class=&#34;series-nav-count&#34;&gt;5 posts · 2 papers · 1 talk&lt;/span&gt;
  &lt;/header&gt;
  &lt;ol class=&#34;series-nav-list&#34;&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-02-05-ce-labs/&#34;&gt;On the Promises of &amp;#39;High-Assurance&amp;#39; Cryptography&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.02.05&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-02-12-ce-labs-response/&#34;&gt;We Found Bugs in CE Labs&amp;#39; Verified Code&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.02.12&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-02-17-ce-labs-mldsa/&#34;&gt;Even More Bugs in CE Labs&amp;#39; libcrux: ML-DSA&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.02.17&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li class=&#34;current&#34; aria-current=&#34;page&#34;&gt;
      
      &lt;span class=&#34;title&#34;&gt;CE Labs&amp;#39; Approach to TLS: A Critical Analysis&lt;/span&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.03.07&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-04-07-cryspen-hax/&#34;&gt;The Verification Facade: Structural Gaps in Cryspen&amp;#39;s Hax Pipeline&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.04.07&lt;/span&gt;
    &lt;/li&gt;
    
  &lt;/ol&gt;
  &lt;div class=&#34;series-nav-footer&#34;&gt;
    &lt;div class=&#34;series-nav-row&#34;&gt;
      &lt;span class=&#34;series-nav-rowlabel&#34;&gt;Papers&lt;/span&gt;
      &lt;a href=&#34;https://eprint.iacr.org/2026/192&#34;&gt;&lt;em&gt;Verification Theatre&lt;/em&gt;&lt;/a&gt;
      &lt;span class=&#34;series-nav-sep&#34;&gt;·&lt;/span&gt;
      &lt;a href=&#34;https://eprint.iacr.org/2026/670&#34;&gt;&lt;em&gt;Verification Facade&lt;/em&gt;&lt;/a&gt;
    &lt;/div&gt;
    &lt;div class=&#34;series-nav-row&#34;&gt;
      &lt;span class=&#34;series-nav-rowlabel&#34;&gt;Talk&lt;/span&gt;
      &lt;a href=&#34;https://www.youtube.com/watch?v=TdOXza1-M_4&#34;&gt;&lt;em&gt;High Assurance Cryptography and the Ethics of Disclosure (OSTIF)&lt;/em&gt;&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/nav&gt;

&lt;p&gt;This is the fourth installment in our series examining CE Labs&amp;rsquo; cryptographic libraries. Over the past month, we have disclosed &lt;a href=&#34;https://symbolic.software/blog/2026-02-17-ce-labs-mldsa/&#34;&gt;eleven vulnerabilities&lt;/a&gt; spanning post-quantum key encapsulation, digital signatures, hybrid public key encryption, and the formally verified F* specifications underlying them. We now turn to CE Labs&amp;rsquo; TLS implementations: &lt;strong&gt;rustls-libcrux&lt;/strong&gt;, a cryptographic provider for rustls; &lt;strong&gt;Bertie&lt;/strong&gt;, a standalone TLS 1.3 implementation that has been the subject of formal verification research; and &lt;strong&gt;libcrux-iot&lt;/strong&gt;, an embedded-targeted fork of libcrux. Across these three projects, we found five more bugs, bringing the total to sixteen: a signature verification function that rejects 75% of valid signatures, encryption that silently drops its authentication tag, a TLS client that performs no certificate validation, remote denial-of-service vectors, and a FIPS 204 parameter that is off by a factor of eight.&lt;/p&gt;
&lt;h2 id=&#34;how-ce-labs-handled-our-disclosures&#34;&gt;How CE Labs Handled our Disclosures&lt;/h2&gt;
&lt;p&gt;Before presenting our new findings, we document how CE Labs responded to our previous vulnerability disclosures.&lt;/p&gt;
&lt;p&gt;In January 2026, we began our audit work and opened our first issue on CE Labs&amp;rsquo; GitHub (&lt;a href=&#34;https://github.com/celabshq/libcrux/issues/1299&#34;&gt;#1299&lt;/a&gt;). It was professional and submitted in good faith. When CE Labs&amp;rsquo; team responded, we acknowledged that their perspective was legitimate, closed the issue cooperatively, and moved on.&lt;/p&gt;
&lt;p&gt;We then submitted four pull requests with tested fixes for security vulnerabilities, including a critical nonce reuse bug in hpke-rs (&lt;a href=&#34;https://github.com/celabshq/hpke-rs/pull/117&#34;&gt;PR #117&lt;/a&gt;, &lt;a href=&#34;https://github.com/celabshq/hpke-rs/pull/118&#34;&gt;PR #118&lt;/a&gt;) and issues in libcrux (&lt;a href=&#34;https://github.com/celabshq/libcrux/pull/1315&#34;&gt;PR #1315&lt;/a&gt;, &lt;a href=&#34;https://github.com/celabshq/libcrux/pull/1316&#34;&gt;PR #1316&lt;/a&gt;). These were submitted in the same professional manner as contributions from other external researchers such as Filippo Valsorda and &amp;ldquo;GuuJiang,&amp;rdquo; whom CE Labs had publicly thanked for their reports.&lt;/p&gt;
&lt;p&gt;CE Labs&amp;rsquo; response to our contributions was markedly different:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;They closed all four of our pull requests.&lt;/li&gt;
&lt;li&gt;They publicly accused us of &amp;ldquo;putting the community at risk&amp;rdquo; and called our disclosures &amp;ldquo;irresponsible.&amp;rdquo;&lt;/li&gt;
&lt;li&gt;They blocked us from their GitHub organization, preventing us from submitting any further issues or pull requests.&lt;/li&gt;
&lt;li&gt;They then copied our fixes and applied them without credit, after blocking us and closing the PRs.&lt;/li&gt;
&lt;li&gt;They published security advisories that downplayed severity and obscured impact, classifying our critical nonce reuse vulnerability as &amp;ldquo;medium severity&amp;rdquo; and describing fixes with vague labels like &amp;ldquo;fix context counter&amp;rdquo; and &amp;ldquo;avoid panic on AEADError&amp;rdquo; rather than explaining the vulnerabilities and their impact.&lt;/li&gt;
&lt;li&gt;To date, no RustSec advisories exist for their most severe vulnerabilities.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The GitHub block had an immediate practical consequence: we discovered it when attempting to submit our fifth fix, a patch for an &lt;a href=&#34;https://github.com/nadimkobeissi/libcrux/tree/aesgcm-dos-fix&#34;&gt;AES-GCM denial-of-service crash in libcrux-psq&lt;/a&gt;. That fix remains available on our fork but could never be submitted upstream.&lt;/p&gt;
&lt;p&gt;We offered to continue disclosing our remaining findings, including those being revealed here today, as GitHub pull requests complete with tested fixes provided on the day of disclosure. &lt;strong&gt;CE Labs declined&lt;/strong&gt;, and then accused us of not giving them time to address vulnerabilities before public disclosure. It was CE Labs who blocked our ability to submit fixes, after we had already disclosed four bugs complete with tested patches, free of charge.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;After we submitted our initial four bug disclosures with tested fixes, we were blocked by CE Labs from submitting additional fixes, something we discovered when trying to submit our fifth fix.&lt;/strong&gt; In spite of this, CE Labs accused &lt;em&gt;us&lt;/em&gt; of not giving them time to fix the bugs we were disclosing.&lt;/p&gt;
&lt;p&gt;While CE Labs described our disclosures as &amp;ldquo;irresponsible&amp;rdquo; and &amp;ldquo;putting the community at risk&amp;rdquo;, we posit that it is their &lt;a href=&#34;https://bughunters.google.com/blog/formally-verified-post-quantum-algorithms&#34;&gt;active lobbying&lt;/a&gt; to get their cryptographic libraries merged in production into Signal, Google and OpenMLS&amp;rsquo;s stacks, despite their being aware of, and at times openly &lt;a href=&#34;https://github.com/celabshq/libcrux/issues/1335&#34;&gt;admitting their immaturity&lt;/a&gt;, that constitutes irresponsible behavior. CE Labs has also &lt;a href=&#34;https://github.com/celabshq/libcrux/issues/1275#event-21774379885&#34;&gt;often acted to downplay the severity of bugs that were found to impact applications such as Signal in production&lt;/a&gt;, and to this day has not issued complete or appropriate advisories for their most severe, critical vulnerabilities.&lt;/p&gt;
&lt;p&gt;CE Labs&amp;rsquo; own &lt;a href=&#34;https://github.com/celabshq/libcrux/blob/main/CODE_OF_CONDUCT.md&#34;&gt;Code of Conduct&lt;/a&gt; commits to &amp;ldquo;accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience&amp;rdquo; and &amp;ldquo;focusing on what is best not just for us as individuals, but for the overall community.&amp;rdquo; We leave it to the reader to judge whether CE Labs&amp;rsquo; handling of our disclosures lived up to these principles.&lt;/p&gt;
&lt;h2 id=&#34;findings&#34;&gt;Findings&lt;/h2&gt;
&lt;p&gt;Our eleven earlier findings included bugs in formally verified F* specifications, false proofs accepted by a build system that defaults to not checking them, and unproven axioms that render entire proof infrastructures unsound. The five new ones are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A signature verification function that rejects approximately 75% of valid ECDSA P-256 signatures.&lt;/li&gt;
&lt;li&gt;A TLS 1.2 encryption path that silently drops the authentication tag, eliminating all integrity protection.&lt;/li&gt;
&lt;li&gt;A TLS 1.3 client that performs no certificate chain validation or hostname verification.&lt;/li&gt;
&lt;li&gt;Remote denial-of-service vectors exploitable with a single malformed handshake message.&lt;/li&gt;
&lt;li&gt;A FIPS 204 parameter that is off by a factor of eight.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;rustls-libcrux&#34;&gt;rustls-libcrux&lt;/h2&gt;
&lt;p&gt;The rustls-libcrux project provides a &lt;code&gt;CryptoProvider&lt;/code&gt; implementation for the rustls TLS library, replacing rustls&amp;rsquo;s default cryptographic backend with CE Labs&amp;rsquo; libcrux. It implements signature verification, AEAD encryption and decryption, key exchange, and RSA signing: the cryptographic foundation that the TLS protocol depends on for authentication, confidentiality, and integrity.&lt;/p&gt;
&lt;h3 id=&#34;finding-1-ecdsa-p-256-verification-rejects-75-of-valid-signatures&#34;&gt;Finding 1: ECDSA P-256 Verification Rejects ~75% of Valid Signatures&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;File:&lt;/strong&gt; &lt;code&gt;libcrux-provider/src/verify.rs&lt;/code&gt;, lines 57&amp;ndash;70.&lt;/p&gt;
&lt;p&gt;The ECDSA P-256 signature verification code extracts the &lt;code&gt;r&lt;/code&gt; and &lt;code&gt;s&lt;/code&gt; components from a DER-encoded signature and converts them to fixed-size 32-byte arrays:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;r&lt;/span&gt;: &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;u8&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;32&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;sig&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;r&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;as_bytes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;().&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;try_into&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;().&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;map_err&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;|&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;_&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;|&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;InvalidSignature&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;?&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;s&lt;/span&gt;: &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;u8&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;32&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;sig&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;s&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;as_bytes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;().&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;try_into&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;().&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;map_err&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;|&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;_&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;|&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;InvalidSignature&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;?&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;sig.r&lt;/code&gt; and &lt;code&gt;sig.s&lt;/code&gt; values are &lt;code&gt;der::asn1::Int&lt;/code&gt; types. The &lt;code&gt;Int::as_bytes()&lt;/code&gt; method returns the raw DER value bytes, which include a leading &lt;code&gt;0x00&lt;/code&gt; sign byte when the most significant bit of the integer is set. This is how DER encodes positive integers: if the MSB would otherwise indicate a negative number, a &lt;code&gt;0x00&lt;/code&gt; byte is prepended.&lt;/p&gt;
&lt;p&gt;For ECDSA P-256, &lt;code&gt;r&lt;/code&gt; and &lt;code&gt;s&lt;/code&gt; are 32-byte unsigned scalars. When the MSB is set, which happens with roughly 50% probability for each scalar, DER encoding prepends &lt;code&gt;0x00&lt;/code&gt;, making &lt;code&gt;as_bytes()&lt;/code&gt; return 33 bytes. The &lt;code&gt;try_into::&amp;lt;[u8; 32]&amp;gt;()&lt;/code&gt; conversion then fails with a length mismatch, and the signature is rejected as invalid.&lt;/p&gt;
&lt;p&gt;The probability that at least one of &lt;code&gt;r&lt;/code&gt; or &lt;code&gt;s&lt;/code&gt; has its MSB set is $1 - 0.5 \cdot 0.5 = 75\%$. Three out of every four valid ECDSA P-256 signatures will be rejected by this code.&lt;/p&gt;
&lt;p&gt;This is the primary signature scheme for TLS 1.3 server authentication. A rustls client using the libcrux provider will fail to complete TLS handshakes with approximately 75% of servers, because the server&amp;rsquo;s valid certificate signature will be incorrectly rejected.&lt;/p&gt;
&lt;h3 id=&#34;finding-2-tls-12-encryption-drops-authentication-tag&#34;&gt;Finding 2: TLS 1.2 Encryption Drops Authentication Tag&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;File:&lt;/strong&gt; &lt;code&gt;libcrux-provider/src/aead.rs&lt;/code&gt;, lines 161&amp;ndash;163.&lt;/p&gt;
&lt;p&gt;The TLS 1.2 &lt;code&gt;MessageEncrypter&lt;/code&gt; implementation discards the AEAD authentication tag:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;libcrux&lt;/span&gt;::&lt;span class=&#34;n&#34;&gt;aead&lt;/span&gt;::&lt;span class=&#34;n&#34;&gt;encrypt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;payload&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;as_mut&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(),&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;iv&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;aad&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;map_err&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;|&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;_&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;|&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;rustls&lt;/span&gt;::&lt;span class=&#34;n&#34;&gt;Error&lt;/span&gt;::&lt;span class=&#34;n&#34;&gt;EncryptError&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;map&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;|&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;_&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;|&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;OutboundOpaqueMessage&lt;/span&gt;::&lt;span class=&#34;n&#34;&gt;new&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;m&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;typ&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;m&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;version&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;payload&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;//       ^^^ tag discarded
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The &lt;code&gt;libcrux::aead::encrypt()&lt;/code&gt; function returns the authentication tag as a separate value. The TLS 1.3 code path handles this correctly, appending the tag to the ciphertext:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;map&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;|&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;tag&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;|&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;payload&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;extend_from_slice&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;tag&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;as_ref&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;());&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;OutboundOpaqueMessage&lt;/span&gt;::&lt;span class=&#34;n&#34;&gt;new&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;m&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;typ&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;m&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;version&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;payload&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;})&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The TLS 1.2 path captures the tag as &lt;code&gt;_&lt;/code&gt; and drops it. The resulting TLS record contains only ciphertext with no integrity protection. The receiving peer will fail to decrypt because it expects a 16-byte authentication tag appended to the ciphertext, and the connection will fail.&lt;/p&gt;
&lt;p&gt;This is not a subtle vulnerability. Authenticated encryption without the authentication tag is just encryption, and the entire integrity guarantee of the TLS record layer is eliminated. The fix is one line: append the tag, exactly as the TLS 1.3 path already does.&lt;/p&gt;
&lt;h2 id=&#34;bertie&#34;&gt;Bertie&lt;/h2&gt;
&lt;p&gt;Bertie is CE Labs&amp;rsquo; standalone TLS 1.3 implementation, written in Rust. It is described as a &amp;ldquo;minimal, low-level TLS 1.3 implementation&amp;rdquo; and has been the subject of formal verification research. Unlike rustls-libcrux, which delegates protocol logic to rustls, Bertie implements the TLS 1.3 handshake and record layer from scratch.&lt;/p&gt;
&lt;h3 id=&#34;finding-3-no-certificate-chain-validation-or-hostname-verification&#34;&gt;Finding 3: No Certificate Chain Validation or Hostname Verification&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Files:&lt;/strong&gt; &lt;code&gt;src/tls13cert.rs&lt;/code&gt;, lines 356&amp;ndash;373; &lt;code&gt;src/tls13handshake.rs&lt;/code&gt;, lines 318&amp;ndash;331.&lt;/p&gt;
&lt;p&gt;Bertie&amp;rsquo;s TLS 1.3 client accepts any certificate whose public key produces a valid CertificateVerify signature over the handshake transcript. No other validation is performed:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No certificate chain validation (issuer signature verification)&lt;/li&gt;
&lt;li&gt;No hostname or SNI matching against Subject CN or Subject Alternative Name&lt;/li&gt;
&lt;li&gt;No validity period checking (expiration or not-before dates)&lt;/li&gt;
&lt;li&gt;No certificate extension validation (BasicConstraints, KeyUsage)&lt;/li&gt;
&lt;li&gt;No revocation checking (CRL or OCSP)&lt;/li&gt;
&lt;li&gt;No trust anchor verification (root CA store)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;verification_key_from_cert()&lt;/code&gt; function at &lt;code&gt;tls13cert.rs:356&lt;/code&gt; parses a certificate by skipping version, serial number, signature algorithm, issuer, validity, and subject fields, extracting only the SubjectPublicKeyInfo. The &lt;code&gt;server_name&lt;/code&gt; stored in &lt;code&gt;ServerPubInfo&lt;/code&gt; is populated during the handshake but never compared against any field in the certificate.&lt;/p&gt;
&lt;p&gt;The handshake function &lt;code&gt;put_server_signature()&lt;/code&gt; at &lt;code&gt;tls13handshake.rs:318&lt;/code&gt; extracts the public key using &lt;code&gt;verification_key_from_cert()&lt;/code&gt; and verifies the CertificateVerify signature over the transcript. If the signature is valid, the handshake proceeds. The certificate itself is never examined.&lt;/p&gt;
&lt;p&gt;An attacker can present a self-signed certificate for any domain, or a certificate with no domain at all, and Bertie will accept it, provided the CertificateVerify signature is valid with respect to the key in that certificate. This completely defeats server authentication. The attacker does not need to compromise a certificate authority, exploit a parsing vulnerability, or perform any cryptographic attack. They simply generate a key pair, create a self-signed certificate, and present it.&lt;/p&gt;
&lt;p&gt;This is not a missing feature in an early prototype. Certificate validation is the mechanism by which TLS clients distinguish legitimate servers from imposters. Without it, the encrypted channel provides confidentiality against passive observers but no authentication whatsoever. The client cannot know who it is talking to.&lt;/p&gt;
&lt;h3 id=&#34;finding-4-remote-denial-of-service-via-unwrap-in-kem-operations&#34;&gt;Finding 4: Remote Denial of Service via &lt;code&gt;.unwrap()&lt;/code&gt; in KEM Operations&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;File:&lt;/strong&gt; &lt;code&gt;src/tls13crypto.rs&lt;/code&gt;, lines 710, 750, 752.&lt;/p&gt;
&lt;p&gt;Three &lt;code&gt;.unwrap()&lt;/code&gt; calls on fallible &lt;code&gt;decode()&lt;/code&gt; results will crash the process if given malformed key material:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// Line 710 — server-side, processes client&amp;#39;s key_share
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pk&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;PublicKey&lt;/span&gt;::&lt;span class=&#34;n&#34;&gt;decode&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;alg&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;libcrux_kem_algorithm&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;?&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pk&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;declassify&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()).&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;unwrap&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;();&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// Line 750 — client-side, processes server&amp;#39;s key_share
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;sk&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;PrivateKey&lt;/span&gt;::&lt;span class=&#34;n&#34;&gt;decode&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;librux_algorithm&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;sk&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;declassify&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()).&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;unwrap&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;();&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// Line 752 — client-side
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ct&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Ct&lt;/span&gt;::&lt;span class=&#34;n&#34;&gt;decode&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;librux_algorithm&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ct&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;).&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;unwrap&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;();&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A remote attacker can crash a Bertie server by sending a ClientHello with a malformed &lt;code&gt;key_share&lt;/code&gt; extension. The malformed public key bytes reach &lt;code&gt;kem_encap()&lt;/code&gt; at &lt;code&gt;tls13handshake.rs:620&lt;/code&gt;, where &lt;code&gt;PublicKey::decode().unwrap()&lt;/code&gt; panics. Similarly, a malicious server can crash a Bertie client by sending a ServerHello with a malformed &lt;code&gt;key_share&lt;/code&gt;, reaching &lt;code&gt;kem_decap()&lt;/code&gt; at &lt;code&gt;tls13handshake.rs:243&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This is a single-packet denial of service. No handshake completion is required. No authentication is needed. One malformed message crashes the process.&lt;/p&gt;
&lt;p&gt;The fix is mechanical: replace &lt;code&gt;.unwrap()&lt;/code&gt; with &lt;code&gt;.map_err(|_| CRYPTO_ERROR)?&lt;/code&gt;. The correct error-handling pattern is used elsewhere in the same file.&lt;/p&gt;
&lt;h2 id=&#34;libcrux-iot&#34;&gt;libcrux-iot&lt;/h2&gt;
&lt;p&gt;libcrux-iot is CE Labs&amp;rsquo; embedded-targeted fork of libcrux, designed for constrained IoT devices. It implements ML-KEM (FIPS 203), ML-DSA (FIPS 204), and SHA-3 (FIPS 202), with C code extracted via the Eurydice toolchain for deployment on bare-metal platforms.&lt;/p&gt;
&lt;h3 id=&#34;finding-5-ml-dsa-pre-hash-uses-wrong-output-length&#34;&gt;Finding 5: ML-DSA Pre-Hash Uses Wrong Output Length&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Files:&lt;/strong&gt; &lt;code&gt;ml-dsa/src/pre_hash.rs&lt;/code&gt;, line 40; &lt;code&gt;ml-dsa/src/ml_dsa_44.rs&lt;/code&gt;, line 121; &lt;code&gt;ml-dsa/src/ml_dsa_65.rs&lt;/code&gt;, line 134; &lt;code&gt;ml-dsa/src/ml_dsa_87.rs&lt;/code&gt;, line 121.&lt;/p&gt;
&lt;p&gt;FIPS 204 Table 2 specifies that the SHAKE-128 pre-hash output length for HashML-DSA is &lt;strong&gt;256 bytes&lt;/strong&gt; (2048 bits). The module docstring correctly states this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;sd&#34;&gt;//! pre-hash trait for SHAKE-128, with a digest length of 256 bytes.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;But the implementation allocates a 32-byte buffer and asserts this size:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// pre_hash.rs:40
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;fm&#34;&gt;debug_assert_eq!&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;output&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;len&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(),&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;32&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;All call sites allocate 32-byte buffers:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// ml_dsa_44.rs:121, ml_dsa_65.rs:134, ml_dsa_87.rs:121
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;mut&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pre_hash_buffer&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;u8&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;classify&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;();&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;32&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;];&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is not an edge case. The pre-hash output length is a fundamental parameter of the HashML-DSA mode. SHAKE-128 at 32 bytes produces a different digest than SHAKE-128 at 256 bytes. Signatures produced by this implementation using the &lt;code&gt;sign_pre_hashed_shake128&lt;/code&gt; API will not verify against any FIPS 204-compliant implementation that uses the correct 256-byte output, and vice versa.&lt;/p&gt;
&lt;p&gt;The bug is present in the upstream &lt;code&gt;celabshq/libcrux&lt;/code&gt; as well, indicating it was inherited rather than introduced during the IoT fork. The pre-hashed NIST KAT files were generated by the same codebase, so they cannot detect the discrepancy. ACVP test vectors do not cover the HashML-DSA mode.&lt;/p&gt;
&lt;p&gt;Pure ML-DSA signing and verification, the default and most commonly used mode, is unaffected. Only the &lt;code&gt;sign_pre_hashed_shake128&lt;/code&gt; and &lt;code&gt;verify_pre_hashed_shake128&lt;/code&gt; APIs are impacted.&lt;/p&gt;
&lt;h2 id=&#34;the-scope-of-the-problem&#34;&gt;The Scope of the Problem&lt;/h2&gt;
&lt;p&gt;These five findings bring the total to sixteen bugs across CE Labs&amp;rsquo; cryptographic ecosystem in one month. The pattern we have documented extends beyond any single library.&lt;/p&gt;
&lt;p&gt;In libcrux, we found &lt;a href=&#34;https://symbolic.software/blog/2026-02-12-ce-labs-response/&#34;&gt;wrong F* specifications&lt;/a&gt;, &lt;a href=&#34;https://symbolic.software/blog/2026-02-12-ce-labs-response/&#34;&gt;false proofs&lt;/a&gt;, and &lt;a href=&#34;https://symbolic.software/blog/2026-02-17-ce-labs-mldsa/&#34;&gt;unsound axioms&lt;/a&gt;. In hpke-rs, we found &lt;a href=&#34;https://symbolic.software/blog/2026-02-05-ce-labs/&#34;&gt;nonce reuse and broken forward secrecy&lt;/a&gt;. In rustls-libcrux, we find that TLS 1.3 connections will fail 75% of the time and TLS 1.2 encryption drops its authentication tag. In Bertie, we find a TLS implementation that does not validate certificates. In libcrux-iot, we find FIPS non-compliance in a post-quantum signature parameter.&lt;/p&gt;
&lt;p&gt;The rustls-libcrux findings are particularly instructive. This is a cryptographic provider whose entire purpose is to correctly implement a small set of well-specified cryptographic operations for a well-tested TLS library. The operations are standard: ECDSA verification, AEAD encryption and decryption. The specifications are unambiguous. And yet the implementation rejects 75% of valid ECDSA signatures and drops authentication tags. These are not subtle corner cases. They are the primary code paths.&lt;/p&gt;
&lt;p&gt;Bertie&amp;rsquo;s missing certificate validation is harder to characterize charitably. Certificate chain validation is not an optional feature of TLS. It is the mechanism by which a client determines whether the server is who it claims to be. A TLS 1.3 implementation that omits this check has not implemented TLS in any meaningful sense. It has implemented an encrypted channel to an unknown party.&lt;/p&gt;
&lt;p&gt;The fact that Bertie has been the subject of formal verification research makes this omission more concerning, not less. The verification effort focused on the handshake protocol&amp;rsquo;s cryptographic properties while the most basic authentication mechanism was absent. This is verification theatre in its purest form: proving properties of a protocol that cannot authenticate its peer.&lt;/p&gt;
&lt;p&gt;Across sixteen findings in six CE Labs projects, the pattern is consistent. The verified core is surrounded by an unverified periphery where basic engineering discipline is absent. The specifications that the verified code is checked against are themselves wrong. The build system defaults to not checking proofs. And the marketing claims make no distinction between what has been verified and what has not.&lt;/p&gt;
&lt;p&gt;Formal verification is a tool. Like any tool, its value depends on how it is applied. When it is applied selectively, communicated imprecisely, and used to justify claims that the evidence does not support, it becomes something worse than useless: it becomes a source of false confidence. Users who trust &amp;ldquo;formally verified&amp;rdquo; to mean &amp;ldquo;safe to deploy&amp;rdquo; will deploy code that rejects valid signatures, drops authentication tags, and accepts certificates from anyone.&lt;/p&gt;





&lt;nav class=&#34;series-nav&#34; aria-label=&#34;CE Labs series (bottom)&#34;&gt;
  &lt;header class=&#34;series-nav-header&#34;&gt;
    &lt;span class=&#34;series-nav-label&#34;&gt;▶ CE Labs Series&lt;/span&gt;
    &lt;span class=&#34;series-nav-count&#34;&gt;5 posts · 2 papers · 1 talk&lt;/span&gt;
  &lt;/header&gt;
  &lt;ol class=&#34;series-nav-list&#34;&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-02-05-ce-labs/&#34;&gt;On the Promises of &amp;#39;High-Assurance&amp;#39; Cryptography&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.02.05&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-02-12-ce-labs-response/&#34;&gt;We Found Bugs in CE Labs&amp;#39; Verified Code&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.02.12&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-02-17-ce-labs-mldsa/&#34;&gt;Even More Bugs in CE Labs&amp;#39; libcrux: ML-DSA&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.02.17&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li class=&#34;current&#34; aria-current=&#34;page&#34;&gt;
      
      &lt;span class=&#34;title&#34;&gt;CE Labs&amp;#39; Approach to TLS: A Critical Analysis&lt;/span&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.03.07&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-04-07-cryspen-hax/&#34;&gt;The Verification Facade: Structural Gaps in Cryspen&amp;#39;s Hax Pipeline&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.04.07&lt;/span&gt;
    &lt;/li&gt;
    
  &lt;/ol&gt;
  &lt;div class=&#34;series-nav-footer&#34;&gt;
    &lt;div class=&#34;series-nav-row&#34;&gt;
      &lt;span class=&#34;series-nav-rowlabel&#34;&gt;Papers&lt;/span&gt;
      &lt;a href=&#34;https://eprint.iacr.org/2026/192&#34;&gt;&lt;em&gt;Verification Theatre&lt;/em&gt;&lt;/a&gt;
      &lt;span class=&#34;series-nav-sep&#34;&gt;·&lt;/span&gt;
      &lt;a href=&#34;https://eprint.iacr.org/2026/670&#34;&gt;&lt;em&gt;Verification Facade&lt;/em&gt;&lt;/a&gt;
    &lt;/div&gt;
    &lt;div class=&#34;series-nav-row&#34;&gt;
      &lt;span class=&#34;series-nav-rowlabel&#34;&gt;Talk&lt;/span&gt;
      &lt;a href=&#34;https://www.youtube.com/watch?v=TdOXza1-M_4&#34;&gt;&lt;em&gt;High Assurance Cryptography and the Ethics of Disclosure (OSTIF)&lt;/em&gt;&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/nav&gt;

</content:encoded>
      <category>Research</category>
      <category>Formal Verification</category>
      <category>Protocol Design</category>
      
    </item>
    
    <item>
      <title>Making Verifpal Easier to Reason About</title>
      <link>https://symbolic.software/blog/2026-03-01-verifpal-engine-redesign/</link>
      <pubDate>Sun, 01 Mar 2026 00:00:00 &#43;0000</pubDate>
      <guid>https://symbolic.software/blog/2026-03-01-verifpal-engine-redesign/</guid>
      <description>Verifpal&#39;s analysis engine has been redesigned with a unified equational theory, provenance-tagged values, a formally grounded deduction loop, and a bounded-depth search that runs 3x faster, plus updated tooling across the board.</description>
      <content:encoded>&lt;p&gt;A couple of weeks after &lt;a href=&#34;https://symbolic.software/blog/2026-02-23-verifpal-rust/&#34;&gt;releasing the Rust rewrite&lt;/a&gt;, we&amp;rsquo;ve shipped a deep architectural redesign of Verifpal&amp;rsquo;s analysis engine in Verifpal version 0.50.0. The observable behavior hasn&amp;rsquo;t changed, but the engine is now faster (3x on the full test suite), formally cleaner, and significantly easier to reason about. This post explains what changed and why, starting from the basics.&lt;/p&gt;
&lt;h2 id=&#34;what-verifpal-does-and-why-the-engine-matters&#34;&gt;What Verifpal Does, and Why the Engine Matters&lt;/h2&gt;
&lt;p&gt;Verifpal analyzes cryptographic protocols. You describe a protocol (who talks to whom, what gets encrypted, what gets signed) and Verifpal checks whether an attacker can break its security properties: steal secrets, forge messages, impersonate participants.&lt;/p&gt;
&lt;p&gt;The engine that does this analysis has three jobs:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Deduction&lt;/strong&gt;: figure out everything the attacker can learn from observing (and tampering with) the protocol.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Search&lt;/strong&gt;: systematically try different attacker strategies, such as replacing values on the wire or injecting forged messages.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Query evaluation&lt;/strong&gt;: check whether any security property has been violated.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The old engine did all three jobs correctly, but the way it organized its work made it hard to &lt;em&gt;see&lt;/em&gt; why it was correct. The redesign doesn&amp;rsquo;t change what the engine computes. It changes how the computation is structured, so that correctness becomes obvious rather than subtle.&lt;/p&gt;
&lt;h2 id=&#34;change-1-one-place-for-all-the-rules&#34;&gt;Change 1: One Place for All the Rules&lt;/h2&gt;
&lt;h3 id=&#34;the-problem&#34;&gt;The Problem&lt;/h3&gt;
&lt;p&gt;A protocol verifier is only as good as its equational theory, the formal rules that define what cryptographic operations do. In Verifpal, these rules say things like:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;DEC(k, ENC(k, m)) → m                  [given: k]
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;AEAD_DEC(k, AEAD_ENC(k, m, ad), ad) → m [given: k, ad]
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;SIGNVERIF(G^sk, msg, SIGN(sk, msg)) → nil [verification check]
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;G^a^b = G^b^a                           [DH commutativity]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The first rule says: if you have the key &lt;code&gt;k&lt;/code&gt; and you see &lt;code&gt;ENC(k, m)&lt;/code&gt;, a value encrypted with that key, you can recover the plaintext &lt;code&gt;m&lt;/code&gt;. The Diffie-Hellman commutativity rule says: &lt;code&gt;G^a^b&lt;/code&gt; and &lt;code&gt;G^b^a&lt;/code&gt; are the same value, which is the mathematical property that makes key agreement work.&lt;/p&gt;
&lt;p&gt;Before this redesign, these rules were &lt;em&gt;scattered across five files&lt;/em&gt;. The declarative specifications lived in &lt;code&gt;primitive/spec.rs&lt;/code&gt;. The code that interpreted those specs and checked &amp;ldquo;can the attacker actually do this?&amp;rdquo; lived in &lt;code&gt;possible.rs&lt;/code&gt;. The code that applied the rules during protocol execution lived in &lt;code&gt;rewrite.rs&lt;/code&gt;. DH commutativity was handled in &lt;code&gt;equivalence.rs&lt;/code&gt;. And the filtering of constructible values for the attacker lived in &lt;code&gt;inject.rs&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This meant that the answer to &amp;ldquo;what can the attacker do?&amp;rdquo; didn&amp;rsquo;t have a single location in the code. You had to hold all five files in your head simultaneously.&lt;/p&gt;
&lt;h3 id=&#34;the-solution&#34;&gt;The Solution&lt;/h3&gt;
&lt;p&gt;A new module, &lt;code&gt;theory.rs&lt;/code&gt;, now centralizes every derivation operation. The complete equational theory, all 14 rewrite rules for 21 cryptographic primitives, is listed in a single doc comment at the top of the file:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;DEC(k, ENC(k, m)) → m                                    [given: k]
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;AEAD_DEC(k, AEAD_ENC(k, m, ad), ad) → m                  [given: k, ad]
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;AEAD_ENC(k, m, ad) → ad                                   [passive]
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;ENC(k, m) → m                                             [given: k]
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;BLIND(factor, m) → m                                      [given: factor]
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;PKE_DEC(sk, PKE_ENC(G^sk, m)) → m                         [given: sk]
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;PKE_ENC(G^sk, m) → m                                      [given: sk (via G^sk)]
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;SIGNVERIF(G^sk, msg, SIGN(sk, msg)) → nil                 [rewrite: def check]
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;RINGSIGNVERIF(G^sk, ..., RINGSIGN(sk, ...)) → nil         [rewrite: def check]
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;UNBLIND(factor, SIGN(sk, BLIND(factor, m)), m) → SIGN(sk, m) [rewrite]
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;SHAMIR_SPLIT(s)[i] + SHAMIR_SPLIT(s)[j] → s               [recompose: 2-of-3]
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;SHAMIR_JOIN(SHAMIR_SPLIT(s)[i], SHAMIR_SPLIT(s)[j]) → s   [rebuild]
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;CONCAT(a, b, ...) → a, b, ...                             [passive: reveals args]
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;G^a^b = G^b^a                                              [DH commutativity]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;All eight operations the attacker can perform are implemented in this one file: decompose, passive-decompose, recompose, reconstruct (primitive), reconstruct (equation), rewrite, rebuild, and password extraction. The old &lt;code&gt;possible.rs&lt;/code&gt; became a one-line re-export shim. No callers needed to change their imports.&lt;/p&gt;
&lt;p&gt;The practical benefit: to understand what the attacker can do, read one file. To add a new cryptographic primitive, edit one file (&lt;code&gt;spec.rs&lt;/code&gt;). The engine picks up the new rules automatically.&lt;/p&gt;
&lt;h2 id=&#34;change-2-the-three-phase-pipeline&#34;&gt;Change 2: The Three-Phase Pipeline&lt;/h2&gt;
&lt;h3 id=&#34;the-problem-1&#34;&gt;The Problem&lt;/h3&gt;
&lt;p&gt;When analyzing a protocol, the engine needs to do three things for each principal (participant): resolve all symbolic values, figure out what the attacker learns, and check security queries. The old engine interleaved these steps:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;for each principal:
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    resolve values
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    loop:
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        check queries        ← interleaved!
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        try one deduction step
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        if no progress: stop
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    check queries again
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The query check inside the deduction loop meant that the engine could exit early: if all queries happened to resolve mid-deduction, why keep going? This was a performance optimization, and it worked. But it had a subtle cost. The deduction loop was no longer a clean mathematical object. Its behavior depended on &lt;em&gt;when&lt;/em&gt; queries resolved, which depended on &lt;em&gt;what order&lt;/em&gt; values were processed, which was an implementation detail rather than a formal property.&lt;/p&gt;
&lt;h3 id=&#34;the-solution-separation-of-concerns&#34;&gt;The Solution: Separation of Concerns&lt;/h3&gt;
&lt;p&gt;The pipeline is now three explicit phases:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;fn&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;verify_standard_run&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ctx&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;km&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;principal_states&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;stage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ps&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;in&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;principal_states&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// Phase 1: Trace generation
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ps_resolved&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;generate_trace&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ctx&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;km&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ps&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;attacker&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;?&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// Phase 2: Knowledge closure (monotone fixed-point)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;compute_knowledge_closure&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ctx&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;km&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ps_resolved&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;stage&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;?&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// Phase 3: Query evaluation
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;verify_resolve_queries&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ctx&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;km&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ps_resolved&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;?&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Phase 1&lt;/strong&gt; (&lt;code&gt;generate_trace&lt;/code&gt;) does all the mechanical work: resolve symbolic constant references to concrete values, record which slots differ from the original protocol trace, inject structural templates into the attacker&amp;rsquo;s knowledge, apply cryptographic rewrite rules (like &lt;code&gt;DEC(k, ENC(k, m)) → m&lt;/code&gt;), and check that nothing went wrong.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Phase 2&lt;/strong&gt; (&lt;code&gt;compute_knowledge_closure&lt;/code&gt;) is a pure deduction loop. It runs the attacker&amp;rsquo;s derivation rules until nothing new can be learned: no query checks, no early exits, no side effects beyond expanding the attacker&amp;rsquo;s knowledge set. This is now a textbook &lt;em&gt;monotone fixed-point computation&lt;/em&gt;, which gives us a clean correctness argument.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Phase 3&lt;/strong&gt; (&lt;code&gt;verify_resolve_queries&lt;/code&gt;) checks all security queries against the final, complete attacker knowledge.&lt;/p&gt;
&lt;h3 id=&#34;why-fixed-points-matter&#34;&gt;Why Fixed Points Matter&lt;/h3&gt;
&lt;p&gt;This might sound like an academic distinction, but it has consequences. A &lt;strong&gt;monotone fixed point&lt;/strong&gt; is a concept from order theory. Informally: if you have a function F that only ever &lt;em&gt;adds&lt;/em&gt; to a set (never removes), and the set has a maximum size, then repeatedly applying F will eventually reach a stable state where applying F doesn&amp;rsquo;t change anything. That stable state is the &lt;em&gt;least fixed point&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The attacker&amp;rsquo;s deduction rules are monotone: the attacker only ever learns new values, never forgets them. The set of derivable values is finite (bounded by the protocol model). So the deduction loop is computing a least fixed point, and the &lt;strong&gt;Knaster-Tarski theorem&lt;/strong&gt; guarantees it converges. Every value in the result is derivable. No value is missed. These properties hold regardless of evaluation order.&lt;/p&gt;
&lt;p&gt;The old interleaved loop &lt;em&gt;approximated&lt;/em&gt; this, but the early exits and query checks made the argument indirect. Now it&amp;rsquo;s immediate: &lt;code&gt;compute_knowledge_closure&lt;/code&gt; is a Kleene iteration over a monotone function, and the Knaster-Tarski theorem does the rest.&lt;/p&gt;
&lt;h2 id=&#34;change-3-provenance-tagged-values&#34;&gt;Change 3: Provenance-Tagged Values&lt;/h2&gt;
&lt;h3 id=&#34;the-problem-2&#34;&gt;The Problem&lt;/h3&gt;
&lt;p&gt;Verifpal&amp;rsquo;s analysis is fundamentally about &lt;em&gt;perspective&lt;/em&gt;. When an attacker intercepts and modifies a message on the wire, the sender and receiver see different things. Alice computes &lt;code&gt;k = HASH(secret)&lt;/code&gt; and sends it to Bob. The attacker replaces &lt;code&gt;k&lt;/code&gt; with &lt;code&gt;HASH(attacker_value)&lt;/code&gt;. Alice still &amp;ldquo;sees&amp;rdquo; her original &lt;code&gt;k&lt;/code&gt;. Bob sees the attacker&amp;rsquo;s version. If the engine gets this wrong, so that Alice somehow &amp;ldquo;sees&amp;rdquo; the tampered value in her own subsequent computations, authentication queries produce false positives.&lt;/p&gt;
&lt;p&gt;The old engine tracked this with three separate copies of every value:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// Old design
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;SlotValues&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;assigned&lt;/span&gt;: &lt;span class=&#34;nc&#34;&gt;Value&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// current value (after mutation + rewrite)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;before_rewrite&lt;/span&gt;: &lt;span class=&#34;nc&#34;&gt;Value&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// after mutation, before rewrite
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;before_mutate&lt;/span&gt;: &lt;span class=&#34;nc&#34;&gt;Value&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;   &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// original (before attacker tampering)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;mutated&lt;/span&gt;: &lt;span class=&#34;kt&#34;&gt;bool&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;creator&lt;/span&gt;: &lt;span class=&#34;nc&#34;&gt;PrincipalId&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;sender&lt;/span&gt;: &lt;span class=&#34;nc&#34;&gt;PrincipalId&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;rewritten&lt;/span&gt;: &lt;span class=&#34;kt&#34;&gt;bool&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The decision about which copy a principal &amp;ldquo;sees&amp;rdquo; was made by a function called &lt;code&gt;should_use_before_mutate&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// Old logic
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;fn&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;should_use_before_mutate&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;i&lt;/span&gt;: &lt;span class=&#34;kt&#34;&gt;usize&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;-&amp;gt; &lt;span class=&#34;kt&#34;&gt;bool&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;values&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;].&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;creator&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;==&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;||&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;!&lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;meta&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;].&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;known&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;||&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;!&lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;meta&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;].&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;wire&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;contains&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;||&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;!&lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;values&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;].&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;mutated&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This worked, but the &lt;em&gt;meaning&lt;/em&gt; was indirect. You had to know that &lt;code&gt;creator == self.id&lt;/code&gt; means &amp;ldquo;I made this myself, so I see my version.&amp;rdquo; You had to know that &lt;code&gt;!mutated&lt;/code&gt; means &amp;ldquo;the attacker didn&amp;rsquo;t touch it, so there&amp;rsquo;s only one version.&amp;rdquo; The fields &lt;code&gt;creator&lt;/code&gt;, &lt;code&gt;sender&lt;/code&gt;, and &lt;code&gt;mutated&lt;/code&gt; were scattered alongside the value copies, with no explicit connection between them.&lt;/p&gt;
&lt;h3 id=&#34;the-solution-provenance&#34;&gt;The Solution: Provenance&lt;/h3&gt;
&lt;p&gt;The redesign introduces an explicit &lt;code&gt;Provenance&lt;/code&gt; record:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;Provenance&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;creator&lt;/span&gt;: &lt;span class=&#34;nc&#34;&gt;PrincipalId&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;sender&lt;/span&gt;: &lt;span class=&#34;nc&#34;&gt;PrincipalId&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;attacker_tainted&lt;/span&gt;: &lt;span class=&#34;kt&#34;&gt;bool&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;SlotValues&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;value&lt;/span&gt;: &lt;span class=&#34;nc&#34;&gt;Value&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;           &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// current canonical value
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;original&lt;/span&gt;: &lt;span class=&#34;nc&#34;&gt;Value&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// the principal&amp;#39;s own computation
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pre_rewrite&lt;/span&gt;: &lt;span class=&#34;nc&#34;&gt;Value&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;     &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// forensic snapshot (for error messages)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;rewritten&lt;/span&gt;: &lt;span class=&#34;kt&#34;&gt;bool&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;provenance&lt;/span&gt;: &lt;span class=&#34;nc&#34;&gt;Provenance&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The visibility decision is now a direct provenance check:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;fn&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;should_use_original&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;i&lt;/span&gt;: &lt;span class=&#34;kt&#34;&gt;usize&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;-&amp;gt; &lt;span class=&#34;kt&#34;&gt;bool&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;!&lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;values&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;].&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;provenance&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;attacker_tainted&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;||&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;values&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;].&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;provenance&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;creator&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;==&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;||&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;!&lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;meta&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;].&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;known&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;||&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;!&lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;meta&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;].&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;wire&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;contains&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The semantic difference is subtle but important. The old code asked: &amp;ldquo;was this value mutated, and who created it?&amp;rdquo; The new code asks: &amp;ldquo;is this value tainted by the attacker?&amp;rdquo; The answer is the same, since the conditions haven&amp;rsquo;t changed, but the question now asks &lt;em&gt;what happened to the value&lt;/em&gt; rather than &lt;em&gt;what fields happen to be set&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;This also cleaned up the naming throughout the codebase: &lt;code&gt;assigned&lt;/code&gt; → &lt;code&gt;value&lt;/code&gt;, &lt;code&gt;before_mutate&lt;/code&gt; → &lt;code&gt;original&lt;/code&gt;, &lt;code&gt;before_rewrite&lt;/code&gt; → &lt;code&gt;pre_rewrite&lt;/code&gt;, &lt;code&gt;mutated&lt;/code&gt; → &lt;code&gt;provenance.attacker_tainted&lt;/code&gt;, &lt;code&gt;set_assigned()&lt;/code&gt; → &lt;code&gt;set_value()&lt;/code&gt;. These renamings touched every module, but each change made the code read more naturally.&lt;/p&gt;
&lt;h2 id=&#34;change-4-bounded-depth-search&#34;&gt;Change 4: Bounded-Depth Search&lt;/h2&gt;
&lt;h3 id=&#34;the-problem-3&#34;&gt;The Problem&lt;/h3&gt;
&lt;p&gt;Verifpal&amp;rsquo;s active attacker analysis works by trying mutations: replacing wire values with attacker-controlled alternatives, then checking whether the attacker can derive new secrets. The old engine used a &lt;strong&gt;ten-stage search&lt;/strong&gt; controlled by seven empirical constants:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// Old design
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;const&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;no&#34;&gt;BUDGET&lt;/span&gt;: &lt;span class=&#34;nc&#34;&gt;StageBudget&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;StageBudget&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;exhaustion_threshold&lt;/span&gt;: &lt;span class=&#34;mi&#34;&gt;6&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;max_stage&lt;/span&gt;: &lt;span class=&#34;mi&#34;&gt;10&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;max_subset_weight&lt;/span&gt;: &lt;span class=&#34;mi&#34;&gt;3&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;max_subsets_per_weight&lt;/span&gt;: &lt;span class=&#34;mi&#34;&gt;150&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;max_weight1_mutations&lt;/span&gt;: &lt;span class=&#34;mi&#34;&gt;150&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;max_mutations_per_subset&lt;/span&gt;: &lt;span class=&#34;mi&#34;&gt;50000&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;max_full_product&lt;/span&gt;: &lt;span class=&#34;mi&#34;&gt;50000&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;max_scan_budget&lt;/span&gt;: &lt;span class=&#34;mi&#34;&gt;80000&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;};&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;These numbers worked. Verifpal found attacks in Signal, Scuttlebutt, TLS 1.3, and 94 test models. But several independent dimensions were conflated into a single &amp;ldquo;stage&amp;rdquo; counter. The stage number simultaneously controlled:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Which values to mutate&lt;/strong&gt;: stages 1–3 only offered minimal replacements (nil, G^nil); stage 4+ offered all known values&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;How deeply to nest injected values&lt;/strong&gt;: stage 5+ enabled recursive injection&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Which primitives to consider&lt;/strong&gt;: stages 0–1 blocked primitive injection; stage 2 blocked &amp;ldquo;explosive&amp;rdquo; primitives (HASH, HKDF, CONCAT)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;When to give up&lt;/strong&gt;: exhaustion after stage 6&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This meant you couldn&amp;rsquo;t answer a simple question: &amp;ldquo;what class of attacks does Verifpal guarantee to find?&amp;rdquo; The answer depended on stage progression, budget exhaustion, and the interaction of four separate thresholds.&lt;/p&gt;
&lt;p&gt;On top of this, a special-case function called &lt;code&gt;verify_active_equation_bypass&lt;/code&gt; ran before the main search. It directly tried replacing Diffie-Hellman public keys with the attacker&amp;rsquo;s own key, the canonical man-in-the-middle attack. This was necessary because the staged search might not try this specific combination within its budget. But it was a symptom: if the general search needed a hand-coded shortcut for the most common attack class, the general search had blind spots.&lt;/p&gt;
&lt;h3 id=&#34;the-solution-one-parameter&#34;&gt;The Solution: One Parameter&lt;/h3&gt;
&lt;p&gt;The new search is controlled by a single depth parameter:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;SearchConfig&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;depth&lt;/span&gt;: &lt;span class=&#34;kt&#34;&gt;usize&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;                &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// Default: 3
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;max_subsets_per_weight&lt;/span&gt;: &lt;span class=&#34;kt&#34;&gt;usize&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;max_weight1_mutations&lt;/span&gt;: &lt;span class=&#34;kt&#34;&gt;usize&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;max_mutations_per_subset&lt;/span&gt;: &lt;span class=&#34;kt&#34;&gt;usize&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;max_full_product&lt;/span&gt;: &lt;span class=&#34;kt&#34;&gt;usize&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;max_scan_budget&lt;/span&gt;: &lt;span class=&#34;kt&#34;&gt;u32&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;At depth &lt;code&gt;d&lt;/code&gt;, the engine explores all k-subsets for k ≤ d, with all attacker-known values as potential replacements, and recursive injection nesting bounded by max(0, d − 1). The coverage guarantee is precise:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;At depth d, Verifpal explores all attacker strategies involving at most d simultaneous value substitutions, with injected values of nesting depth at most d.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;The ten stages collapsed into three depth levels. The &lt;code&gt;STAGE_MUTATION_EXPANSION&lt;/code&gt; constant was deleted, since all mutations are now available at every depth. The &lt;code&gt;STAGE_RECURSIVE_INJECTION&lt;/code&gt; constant and &lt;code&gt;inject_primitive_stage_restricted&lt;/code&gt; function were deleted too, because injection eligibility is controlled solely by the depth parameter.&lt;/p&gt;
&lt;p&gt;The equation bypass was deleted entirely. Replacing a received &lt;code&gt;G^sk&lt;/code&gt; with &lt;code&gt;G^nil&lt;/code&gt; is a natural first-order mutation at depth 1: &lt;code&gt;G^nil&lt;/code&gt; is always in the attacker&amp;rsquo;s knowledge set (it&amp;rsquo;s the attacker&amp;rsquo;s own DH public key), and equation-valued slots accept equation replacements. No special case needed. The fact that the old engine required one tells you the staged search was leaving holes that were patched ad hoc.&lt;/p&gt;
&lt;h3 id=&#34;the-result&#34;&gt;The Result&lt;/h3&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;&lt;/th&gt;
          &lt;th&gt;Old (10-stage)&lt;/th&gt;
          &lt;th&gt;New (depth-3)&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Parameters&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;7 empirical constants&lt;/td&gt;
          &lt;td&gt;1 depth + 5 budget caps&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Stages/depths&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;10&lt;/td&gt;
          &lt;td&gt;3&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Equation bypass&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;Special-case function&lt;/td&gt;
          &lt;td&gt;Natural depth-1 mutation&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Test suite time&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;~11s&lt;/td&gt;
          &lt;td&gt;~3.5s&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Coverage guarantee&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;Implicit&lt;/td&gt;
          &lt;td&gt;Explicit (k ≤ d substitutions, nesting ≤ d)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Attacks found&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;All 147 tests&lt;/td&gt;
          &lt;td&gt;All 147 tests (identical)&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;3x faster, clearer coverage semantics, less code. The default depth of 3 finds every attack that the 10-stage system found, in a third of the time.&lt;/p&gt;
&lt;h2 id=&#34;how-it-all-fits-together&#34;&gt;How It All Fits Together&lt;/h2&gt;
&lt;p&gt;These four changes are not independent. They build on each other:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The unified theory&lt;/strong&gt; gives the engine a single, auditable rule set. When the deduction loop asks &amp;ldquo;what can the attacker derive?&amp;rdquo;, the answer comes from one module with one doc comment listing every rule.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The three-phase pipeline&lt;/strong&gt; separates &lt;em&gt;what the attacker learns&lt;/em&gt; from &lt;em&gt;what security properties hold&lt;/em&gt;. The deduction loop runs to completion as a clean fixed point; query evaluation happens after, on the final knowledge set.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Provenance tagging&lt;/strong&gt; turns the visibility decision, meaning which value a principal perceives, into an explicit metadata check rather than an indirect inference from scattered boolean fields.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;The bounded-depth search&lt;/strong&gt; eliminates the staged complexity and the equation bypass special case, giving a clear coverage guarantee parameterized by a single number.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Each change made the next one easier. The unified theory simplified the deduction loop. The clean deduction loop enabled the phase separation. The phase separation made the provenance changes straightforward to verify. And the provenance model made the search redesign cleaner because mutation injection has simpler semantics when taint is explicit.&lt;/p&gt;
&lt;h2 id=&#34;updated-tooling&#34;&gt;Updated Tooling&lt;/h2&gt;
&lt;p&gt;The engine redesign was a good occasion to bring the rest of the Verifpal ecosystem up to date.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;VSCode extension.&lt;/strong&gt; The &lt;a href=&#34;https://github.com/symbolicsoft/verifpal-vscode&#34;&gt;VSCode extension&lt;/a&gt; has been updated to work with the Rust rewrite, restoring hover documentation, document formatting, attacker analysis with inline result decorations, and protocol diagram rendering. The interface between the extension and the Rust binary is simpler than the original: pre-formatted display strings replace the old JSON round-trip, cutting two subcommands and eliminating the need for JSON deserialization on the Rust side.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Neovim extension.&lt;/strong&gt; There&amp;rsquo;s a new &lt;a href=&#34;https://github.com/symbolicsoft/verifpal-nvim&#34;&gt;Neovim extension&lt;/a&gt; with syntax highlighting, inline diagnostics that mark passing and failing queries directly in the sign column, document formatting via &lt;code&gt;verifpal pretty&lt;/code&gt;, and hover documentation for all 21 primitives, all query types, and all keywords. It uses Neovim&amp;rsquo;s native diagnostic API, so no LSP server is required, and it installs with a single line in lazy.nvim or packer.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Verifpal Manual.&lt;/strong&gt; &lt;a href=&#34;https://static.verifpal.com/manual.pdf&#34;&gt;Print 18&lt;/a&gt; of the Verifpal Manual covers all of the changes described in this post, meaning the unified theory, the three-phase pipeline, provenance tagging, and the bounded-depth search, alongside the existing reference material.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Verifpal Workbench.&lt;/strong&gt; The &lt;a href=&#34;https://verifpal.com/workbench/&#34;&gt;browser-based Workbench&lt;/a&gt; has been updated with the latest WASM build, incorporating the engine redesign and all new primitives.&lt;/p&gt;
</content:encoded>
      <category>Software</category>
      <category>Research</category>
      <category>Verifpal</category>
      <category>Formal Verification</category>
      
    </item>
    
    <item>
      <title>Verifpal Workbench: Protocol Analysis in Your Browser</title>
      <link>https://symbolic.software/blog/2026-02-24-verifpal-workbench/</link>
      <pubDate>Tue, 24 Feb 2026 00:00:00 &#43;0000</pubDate>
      <guid>https://symbolic.software/blog/2026-02-24-verifpal-workbench/</guid>
      <description>Verifpal now runs entirely in the browser via WebAssembly. The new Workbench at verifpal.com/workbench lets anyone write, verify, and visualize cryptographic protocol models with zero installation.</description>
      <content:encoded>&lt;p&gt;Yesterday we &lt;a href=&#34;https://symbolic.software/blog/2026-02-23-verifpal-rust/&#34;&gt;announced the Rust rewrite of Verifpal&lt;/a&gt;. Today we&amp;rsquo;re shipping something that the rewrite made possible: &lt;strong&gt;&lt;a href=&#34;https://verifpal.com/workbench&#34;&gt;Verifpal Workbench&lt;/a&gt;&lt;/strong&gt;, a browser-based environment where you can write, verify, and visualize cryptographic protocol models with nothing to install.&lt;/p&gt;
&lt;p&gt;The entire Verifpal verification engine now compiles to a 350 KB WebAssembly binary. Open a browser tab, write a model, click Verify. The analysis runs client-side on your machine. No server, no upload, no account.&lt;/p&gt;
&lt;h2 id=&#34;why-a-browser-tool&#34;&gt;Why a Browser Tool?&lt;/h2&gt;
&lt;p&gt;Verifpal was designed to make formal verification accessible to engineers and students who don&amp;rsquo;t have a background in formal methods. But &amp;ldquo;accessible&amp;rdquo; has always meant &amp;ldquo;download a binary and learn the command line&amp;rdquo;, which is still a barrier, especially in a classroom setting.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;ve wanted to fix this for years. The old Go codebase used &lt;code&gt;cgo&lt;/code&gt; indirectly through several dependencies and relied on OS-specific terminal handling, making a clean WASM compilation impractical. The Rust rewrite is pure Rust with no C/FFI and no system calls in the core engine, which made WASM a natural target.&lt;/p&gt;
&lt;h2 id=&#34;verification-and-analysis&#34;&gt;Verification and Analysis&lt;/h2&gt;
&lt;p&gt;The screenshot below shows the Workbench analyzing a simplified model of the Signal Protocol. The left panel is the editor with syntax highlighting: keywords like &lt;code&gt;principal&lt;/code&gt; and &lt;code&gt;attacker&lt;/code&gt; in bold, primitives like &lt;code&gt;HASH&lt;/code&gt;, &lt;code&gt;HKDF&lt;/code&gt;, &lt;code&gt;AEAD_ENC&lt;/code&gt; in green, query types in red, and message arrows in amber. The right panel shows the verification results. &lt;code&gt;confidentiality? m1&lt;/code&gt; passes, but &lt;code&gt;authentication? Alice -&amp;gt; Bob: e1&lt;/code&gt; fails, because the active attacker can forge the encrypted message.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://symbolic.software/images/blog/verifpal-workbench-analysis.png&#34; alt=&#34;Verifpal Workbench analyzing the Signal protocol&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34; width=&#34;2568&#34; height=&#34;1802&#34;&gt;
&lt;/p&gt;
&lt;p&gt;Below the failed query, the Workbench displays the full attack trace, the same output you&amp;rsquo;d see on the command line, showing exactly how the attacker mutates values, which keys they derive, and how they reconstruct the forged ciphertext. This is the Signal model running under a &lt;code&gt;phase[1]&lt;/code&gt; key compromise scenario where both Alice and Bob leak their long-term private keys, so the authentication failure is expected: forward secrecy protects message &lt;em&gt;confidentiality&lt;/em&gt; after key compromise, but not authentication.&lt;/p&gt;
&lt;h2 id=&#34;protocol-diagrams&#34;&gt;Protocol Diagrams&lt;/h2&gt;
&lt;p&gt;Click &amp;ldquo;Diagram&amp;rdquo; to generate an SVG sequence diagram directly from the model text. The diagram below shows the same Signal model visualized as a message sequence chart: Alice and Bob&amp;rsquo;s computation blocks appear as annotation boxes on their lifelines, message arrows show the transmitted constants, and phase boundaries are marked with dashed red lines.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://symbolic.software/images/blog/verifpal-workbench-diagram.png&#34; alt=&#34;Verifpal Workbench protocol diagram for the Signal model&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34; width=&#34;2566&#34; height=&#34;1796&#34;&gt;
&lt;/p&gt;
&lt;p&gt;The diagrams are generated entirely client-side by parsing the Verifpal model text in JavaScript: no server round-trip, no external library. They render as inline SVG, so they scale cleanly and can be right-clicked and saved for use in papers or presentations.&lt;/p&gt;
&lt;h2 id=&#34;built-in-examples&#34;&gt;Built-in Examples&lt;/h2&gt;
&lt;p&gt;The dropdown menu includes twelve example models covering a range of protocol patterns:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Simple DH Exchange&lt;/strong&gt; — basic Diffie-Hellman with AEAD&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Challenge-Response&lt;/strong&gt; — nonce-based authentication with signatures&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Signal Protocol&lt;/strong&gt; — simplified X3DH + Double Ratchet with forward secrecy&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Needham-Schroeder&lt;/strong&gt; — classic symmetric-key protocol with known attacks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Blind Signature&lt;/strong&gt; — BLIND/UNBLIND protocol&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Double Ratchet&lt;/strong&gt; — DH ratchet with symmetric chain keys&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Salt Channel&lt;/strong&gt; — ephemeral DH + signed key exchange&lt;/li&gt;
&lt;li&gt;And five more covering PKE, Shamir secret sharing, digital signatures, and AEAD variants&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each example can be verified in the browser in under a second for simple models, or a few seconds for complex ones like Signal.&lt;/p&gt;
&lt;h2 id=&#34;how-it-works&#34;&gt;How It Works&lt;/h2&gt;
&lt;p&gt;The Workbench is a single HTML page that loads the Verifpal WASM module. The architecture is straightforward:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Rust side.&lt;/strong&gt; The crate exposes two &lt;code&gt;#[wasm_bindgen]&lt;/code&gt; entry points: &lt;code&gt;wasm_verify(input)&lt;/code&gt; and &lt;code&gt;wasm_pretty(input)&lt;/code&gt;. Each call resets all global state (principal names, value IDs, attacker knowledge, analysis counters), parses the model from source text, runs the full verification pipeline, and returns a JSON response with results and captured analysis messages.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;cp&#34;&gt;#[wasm_bindgen]&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;fn&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;wasm_verify&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;input&lt;/span&gt;: &lt;span class=&#34;kp&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;str&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;-&amp;gt; &lt;span class=&#34;nb&#34;&gt;String&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;wasm_reset_all_state&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;();&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;m&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;match&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;parser&lt;/span&gt;::&lt;span class=&#34;n&#34;&gt;parse_string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;workbench.vp&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;input&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;Ok&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;m&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;m&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;Err&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;e&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;return&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;json_error&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;e&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;};&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// ... sanity check, init results, run verification ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;json_results&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;results&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;code&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;messages&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Feature gating.&lt;/strong&gt; The &lt;code&gt;cli&lt;/code&gt; feature (clap, colored, rayon) and the &lt;code&gt;wasm&lt;/code&gt; feature (wasm-bindgen) are mutually exclusive. Rayon parallelism is replaced with sequential execution in the WASM build. The TUI, terminal color output, and OS-dependent code (&lt;code&gt;tput&lt;/code&gt;, &lt;code&gt;open&lt;/code&gt;, &lt;code&gt;thread::sleep&lt;/code&gt;) are compiled out. Analysis progress messages are captured into a buffer instead of printed to stdout.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;JavaScript side.&lt;/strong&gt; The page includes a syntax-highlighted code editor (custom tokenizer, no external library), a results panel showing pass/fail for each query with attack traces, and an SVG protocol diagram generator that parses the model text client-side.&lt;/p&gt;
&lt;h2 id=&#34;try-it&#34;&gt;Try It&lt;/h2&gt;
&lt;p&gt;Open &lt;strong&gt;&lt;a href=&#34;https://verifpal.com/workbench&#34;&gt;verifpal.com/workbench&lt;/a&gt;&lt;/strong&gt;, pick an example from the dropdown, and click Verify. Or write your own model from scratch: the &lt;a href=&#34;https://static.verifpal.com/manual.pdf&#34;&gt;Verifpal User Manual&lt;/a&gt; documents the full language.&lt;/p&gt;
&lt;p&gt;The Workbench is open source as part of the Verifpal project, licensed under GPLv3. The source is available on &lt;a href=&#34;https://github.com/symbolicsoft/verifpal&#34;&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
</content:encoded>
      <category>Software</category>
      <category>Research</category>
      <category>Verifpal</category>
      <category>Formal Verification</category>
      
    </item>
    
    <item>
      <title>Verifpal, Rewritten in Rust</title>
      <link>https://symbolic.software/blog/2026-02-23-verifpal-rust/</link>
      <pubDate>Mon, 23 Feb 2026 00:00:00 &#43;0000</pubDate>
      <guid>https://symbolic.software/blog/2026-02-23-verifpal-rust/</guid>
      <description>After seven years in Go, Verifpal has been completely rewritten in Rust, gaining a new analysis engine, massive performance improvements, a rich terminal interface, and a novel attack strategy that finds more attacks.</description>
      <content:encoded>&lt;p&gt;Today we&amp;rsquo;re releasing a complete rewrite of &lt;a href=&#34;https://verifpal.com&#34;&gt;Verifpal&lt;/a&gt; in Rust. Every line of Go has been replaced. The result is a faster, more correct, and more capable protocol verifier, with a new analysis technique, a rich terminal interface, 147 tests (up from 67), and a binary that ships with zero runtime dependencies.&lt;/p&gt;
&lt;p&gt;Verifpal 0.40.1 is a ground-up rearchitecture. It fixes longstanding design limitations and introduces features that weren&amp;rsquo;t possible in the old codebase.&lt;/p&gt;
&lt;video autoplay loop muted playsinline width=&#34;756&#34; height=&#34;800&#34; aria-label=&#34;Verifpal&#39;s new terminal interface, showing a live analysis run&#34;&gt;
  &lt;source src=&#34;https://symbolic.software/images/blog/verifpal-tui.mp4&#34; type=&#34;video/mp4&#34;&gt;
  Your browser does not support the video tag. &lt;a href=&#34;https://symbolic.software/images/blog/verifpal-tui.mp4&#34;&gt;Download the demo&lt;/a&gt;.
&lt;/video&gt;
&lt;h2 id=&#34;why-rewrite&#34;&gt;Why Rewrite?&lt;/h2&gt;
&lt;p&gt;Verifpal was first released in 2019, written in Go. Go served the project well for its first seven years: fast compilation, easy cross-platform builds, and a straightforward concurrency model. But as the analysis engine grew more sophisticated, Go&amp;rsquo;s limitations started to compound.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Parallel slices instead of structured data.&lt;/strong&gt; The core &lt;code&gt;PrincipalState&lt;/code&gt; type in Go maintained 18 separate parallel slices (&lt;code&gt;Constants&lt;/code&gt;, &lt;code&gt;Assigned&lt;/code&gt;, &lt;code&gt;Creator&lt;/code&gt;, &lt;code&gt;BeforeMutate&lt;/code&gt;, &lt;code&gt;BeforeRewrite&lt;/code&gt;, &lt;code&gt;KnownBy&lt;/code&gt;, &lt;code&gt;DeclaredAt&lt;/code&gt;, &lt;code&gt;Phase&lt;/code&gt;, and more), all correlated by index. Adding a new field meant updating every function that iterated over principal state. An off-by-one in any slice silently corrupted the analysis.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Interface boxing and runtime casts.&lt;/strong&gt; Go&amp;rsquo;s &lt;code&gt;Value&lt;/code&gt; type used an interface (&lt;code&gt;ValueData&lt;/code&gt;) with three implementing types. Every operation required a type switch and cast: &lt;code&gt;v.Data.(*Constant)&lt;/code&gt;. The compiler couldn&amp;rsquo;t help you if you forgot a case or cast to the wrong type. The &lt;code&gt;value.go&lt;/code&gt; file alone was 935 lines, much of it repetitive dispatch logic.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Global mutable state.&lt;/strong&gt; The attacker&amp;rsquo;s knowledge base was a package-level global behind a &lt;code&gt;sync.RWMutex&lt;/code&gt;. Every test run had to reinitialize it. Defensive snapshot copies on every read added allocation pressure in the hottest code paths.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No unit tests.&lt;/strong&gt; The Go codebase had a single &lt;code&gt;TestMain&lt;/code&gt; function that ran 67 integration tests. There were no unit tests for value equivalence, equation flattening, primitive rewriting, or any of the core symbolic operations. Bugs in these low-level functions only surfaced as incorrect results on full protocol models.&lt;/p&gt;
&lt;p&gt;None of these were insurmountable, and plenty of good software is written in Go. But for a symbolic verification engine where correctness is paramount and the core data structures are recursive algebraic types, Rust is a natural fit.&lt;/p&gt;
&lt;h2 id=&#34;what-changed&#34;&gt;What Changed&lt;/h2&gt;
&lt;h3 id=&#34;algebraic-types-all-the-way-down&#34;&gt;Algebraic Types All the Way Down&lt;/h3&gt;
&lt;p&gt;The new &lt;code&gt;Value&lt;/code&gt; type is a Rust enum:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;enum&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;Value&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Constant&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Constant&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Primitive&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Arc&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Primitive&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Equation&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Arc&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Equation&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Pattern matching is exhaustive, so the compiler rejects any function that forgets a variant. Accessor methods like &lt;code&gt;try_as_constant()&lt;/code&gt; return &lt;code&gt;Result&amp;lt;&amp;amp;Constant&amp;gt;&lt;/code&gt; instead of panicking, and errors propagate with &lt;code&gt;?&lt;/code&gt;. The 935-line &lt;code&gt;value.go&lt;/code&gt; has been split into five focused modules: &lt;code&gt;value.rs&lt;/code&gt;, &lt;code&gt;equivalence.rs&lt;/code&gt;, &lt;code&gt;hashing.rs&lt;/code&gt;, &lt;code&gt;resolution.rs&lt;/code&gt;, and &lt;code&gt;rewrite.rs&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&#34;structured-principal-state&#34;&gt;Structured Principal State&lt;/h3&gt;
&lt;p&gt;The 18 parallel slices are gone. Each protocol constant is now a single &lt;code&gt;SlotValues&lt;/code&gt; struct:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;SlotValues&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;assigned&lt;/span&gt;: &lt;span class=&#34;nc&#34;&gt;Value&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;before_rewrite&lt;/span&gt;: &lt;span class=&#34;nc&#34;&gt;Value&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;before_mutate&lt;/span&gt;: &lt;span class=&#34;nc&#34;&gt;Value&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;mutated&lt;/span&gt;: &lt;span class=&#34;kt&#34;&gt;bool&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;rewritten&lt;/span&gt;: &lt;span class=&#34;kt&#34;&gt;bool&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;creator&lt;/span&gt;: &lt;span class=&#34;nc&#34;&gt;PrincipalId&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;sender&lt;/span&gt;: &lt;span class=&#34;nc&#34;&gt;PrincipalId&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Immutable metadata (guard status, wire participation, phase membership) lives in a separate &lt;code&gt;SlotMeta&lt;/code&gt; struct shared via &lt;code&gt;Arc&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;PrincipalState&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;meta&lt;/span&gt;: &lt;span class=&#34;nc&#34;&gt;Arc&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;Vec&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;SlotMeta&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;       &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// shared, immutable
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;values&lt;/span&gt;: &lt;span class=&#34;nb&#34;&gt;Vec&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;SlotValues&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// deep-cloned per attack stage
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;index&lt;/span&gt;: &lt;span class=&#34;nc&#34;&gt;Arc&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;HashMap&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ValueId&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;usize&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When cloning state for a new attack stage, only &lt;code&gt;values&lt;/code&gt; is deep-copied. The metadata and index increment an atomic reference count, which is O(1). This pattern eliminates an entire class of parallel-slice indexing bugs while reducing clone overhead.&lt;/p&gt;
&lt;h3 id=&#34;three-valued-lifecycle&#34;&gt;Three-Valued Lifecycle&lt;/h3&gt;
&lt;p&gt;Every protocol constant tracks three snapshots of its value: &lt;code&gt;before_mutate&lt;/code&gt; (original), &lt;code&gt;before_rewrite&lt;/code&gt; (after attacker mutation but before primitive rewriting), and &lt;code&gt;assigned&lt;/code&gt; (final). The resolution system uses a simple rule to decide which value a principal actually sees:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;fn&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;should_use_before_mutate&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;i&lt;/span&gt;: &lt;span class=&#34;kt&#34;&gt;usize&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;-&amp;gt; &lt;span class=&#34;kt&#34;&gt;bool&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;values&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;].&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;creator&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;==&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;||&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;!&lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;meta&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;].&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;known&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;||&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;!&lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;meta&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;].&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;wire&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;contains&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;||&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;!&lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;values&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;].&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;mutated&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When a principal created a value themselves, or when it wasn&amp;rsquo;t sent over the wire, or when the attacker hasn&amp;rsquo;t mutated it, the principal sees the original. This prevents the false-positive authentication failures that plagued earlier versions, a subtle correctness issue that the structured data model makes explicit and verifiable.&lt;/p&gt;
&lt;h3 id=&#34;hand-written-parser&#34;&gt;Hand-Written Parser&lt;/h3&gt;
&lt;p&gt;The Go version used a PEG grammar (&lt;code&gt;libpeg.peg&lt;/code&gt;) that generated parser code. The Rust version replaces it with a hand-written recursive descent parser operating directly on byte slices: zero allocations during lexing, better error messages with position context, and no build-time code generation dependency. The parser is 841 lines, self-contained, and easy to maintain.&lt;/p&gt;
&lt;h3 id=&#34;trait-based-primitive-dispatch&#34;&gt;Trait-Based Primitive Dispatch&lt;/h3&gt;
&lt;p&gt;The 21 built-in cryptographic primitives (AEAD_ENC, AEAD_DEC, SIGN, SIGNVERIF, HASH, HKDF, DH operations, PKE, blind signatures, Shamir secret sharing, and more) are defined as structured specs with function pointer dispatch:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;PrimitiveSpec&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;name&lt;/span&gt;: &lt;span class=&#34;kp&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;&amp;#39;static&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;str&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;decompose&lt;/span&gt;: &lt;span class=&#34;nc&#34;&gt;DecomposeRule&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;recompose&lt;/span&gt;: &lt;span class=&#34;nc&#34;&gt;RecomposeRule&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;rewrite&lt;/span&gt;: &lt;span class=&#34;nc&#34;&gt;RewriteRule&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;rebuild&lt;/span&gt;: &lt;span class=&#34;nc&#34;&gt;RebuildRule&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;definition_check&lt;/span&gt;: &lt;span class=&#34;kt&#34;&gt;bool&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;explosive&lt;/span&gt;: &lt;span class=&#34;kt&#34;&gt;bool&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;pub&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;password_hashing&lt;/span&gt;: &lt;span class=&#34;nb&#34;&gt;Vec&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;usize&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Each rule encodes the primitive&amp;rsquo;s cryptographic semantics declaratively. For example, AEAD_ENC&amp;rsquo;s decompose rule says: &amp;ldquo;given the key (argument 0), reveal the plaintext (argument 1); associated data (argument 2) is always publicly extractable.&amp;rdquo; The Go version encoded the same logic through scattered switch statements across multiple files.&lt;/p&gt;
&lt;p&gt;The rewrite also cleanly separates two concepts that were previously conflated: &lt;strong&gt;definition check&lt;/strong&gt; (an inherent property of the primitive: AEAD_DEC always requires the correct key) and &lt;strong&gt;instance check&lt;/strong&gt; (set by the &lt;code&gt;?&lt;/code&gt; suffix in model syntax, marking one specific decryption operation as verified by the principal). This distinction is critical for correctly modeling protocols where some cryptographic operations are checked and others are blindly accepted.&lt;/p&gt;
&lt;h3 id=&#34;no-globals-explicit-context&#34;&gt;No Globals, Explicit Context&lt;/h3&gt;
&lt;p&gt;All mutable verification state lives in a &lt;code&gt;VerifyContext&lt;/code&gt; struct passed explicitly through the call stack. The only global mutable state in the entire codebase is a single &lt;code&gt;AtomicU32&lt;/code&gt; analysis counter used by the TUI, a pragmatic exception for a progress indicator that must be readable from a rendering thread without holding a reference to the verification context.&lt;/p&gt;
&lt;p&gt;RwLock poison recovery (&lt;code&gt;unwrap_or_else(|e| e.into_inner())&lt;/code&gt;) prevents cascading failures when a thread panics during testing, ensuring that one failing test doesn&amp;rsquo;t poison the lock for all subsequent tests running in parallel.&lt;/p&gt;
&lt;h2 id=&#34;equation-bypass-a-new-attack-strategy&#34;&gt;Equation Bypass: A New Attack Strategy&lt;/h2&gt;
&lt;p&gt;The biggest new feature has nothing to do with language or architecture. It&amp;rsquo;s a new analysis technique called &lt;strong&gt;Equation Bypass&lt;/strong&gt;, and it finds attacks the Go version sometimes missed.&lt;/p&gt;
&lt;h3 id=&#34;the-problem&#34;&gt;The Problem&lt;/h3&gt;
&lt;p&gt;Verifpal&amp;rsquo;s active attacker analysis works by systematically replacing wire values with attacker-controlled alternatives, then checking whether the attacker can derive secrets or forge messages. The search is staged: stage 1 tries single-value replacements, stage 2 tries pairs, and so on. For simple protocols, this works well.&lt;/p&gt;
&lt;p&gt;But for protocols like &lt;a href=&#34;https://ssbc.github.io/scuttlebutt-protocol-guide/&#34;&gt;Scuttlebutt&lt;/a&gt;, the brute-force search never terminates. The combinatorial space is too large, and the canonical attack, replacing an ephemeral DH public key with the attacker&amp;rsquo;s own, is buried in a haystack of useless mutations. The old Go verifier would run indefinitely on Scuttlebutt without ever finding the attack that any cryptographer would spot in minutes.&lt;/p&gt;
&lt;h3 id=&#34;the-solution&#34;&gt;The Solution&lt;/h3&gt;
&lt;p&gt;Equation Bypass is a targeted analysis pass that runs before the general mutation loop. It directly models the most common class of active attack: replacing unguarded Diffie-Hellman public keys with the attacker&amp;rsquo;s own public key (G^nil, where nil is the attacker&amp;rsquo;s known private key).&lt;/p&gt;
&lt;p&gt;The mechanism has four phases:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Replace.&lt;/strong&gt; For each principal, collect all equation-valued wire inputs that are received from another principal and not guarded (not sent in &lt;code&gt;[brackets]&lt;/code&gt;). Try replacing each one individually with G^nil, and also try replacing all of them simultaneously.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Identify bypassable guards.&lt;/strong&gt; After replacement, resolve all values and attempt primitive rewrites. Guarded primitives (AEAD_DEC?, SIGNVERIF?) whose rewrite fails are collected. For each failed guard, extract the &amp;ldquo;bypass key&amp;rdquo;, the value the attacker would need to forge a valid input. For AEAD_DEC, that&amp;rsquo;s the decryption key. For SIGNVERIF with a public key G^sk, that&amp;rsquo;s the private key sk. If the attacker can obtain the bypass key, either because it&amp;rsquo;s already known or because it&amp;rsquo;s reconstructible from known values, the guard is bypassable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Inject.&lt;/strong&gt; For each bypassable guard, inject G^nil into the guard&amp;rsquo;s output slot. This injection must update all three state vectors (assigned, before_rewrite, and before_mutate) to ensure correct propagation through downstream computations. The injection iterates up to 5 times to handle cascading guards: bypassing one guard may change downstream keys enough to make a later guard bypassable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Decompose.&lt;/strong&gt; With the fully-resolved bypass state, attempt to decompose all wire-carried primitive values. For example, AEAD_ENC(key, plaintext, nil) where the attacker now knows the key yields the plaintext.&lt;/p&gt;
&lt;h3 id=&#34;the-scuttlebutt-result&#34;&gt;The Scuttlebutt Result&lt;/h3&gt;
&lt;p&gt;For Bob&amp;rsquo;s state in the Scuttlebutt protocol, replacing &lt;code&gt;ephemeralAPub&lt;/code&gt; with G^nil causes a cascade:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Every shared secret Bob derives from Alice&amp;rsquo;s ephemeral key now uses G^nil, which the attacker knows.&lt;/li&gt;
&lt;li&gt;The master secret derived from these shared secrets is reconstructible by the attacker.&lt;/li&gt;
&lt;li&gt;The first AEAD_DEC guard fails, but the attacker knows the key, so G^nil is injected.&lt;/li&gt;
&lt;li&gt;This propagates through the second AEAD_DEC guard, which also becomes bypassable.&lt;/li&gt;
&lt;li&gt;The attacker decomposes both encrypted messages, learning m1 and m2.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;All 9 security queries fail. The attack is found in seconds. The Go version would have run forever.&lt;/p&gt;
&lt;h2 id=&#34;a-rich-terminal-interface&#34;&gt;A Rich Terminal Interface&lt;/h2&gt;
&lt;p&gt;The Rust rewrite introduces a full-screen terminal user interface for live analysis visualization. When running with the &lt;code&gt;--tui&lt;/code&gt; flag, Verifpal renders:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;protocol overview&lt;/strong&gt; showing principals, their private/generated/computed values, and message flows&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Live query status&lt;/strong&gt; with pass/fail indicators updating in real time&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Attacker activity tracking&lt;/strong&gt;: current scan target, mutation weight, budget consumption, and last worthwhile mutations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Knowledge gain feed&lt;/strong&gt;: newly deduced values color-coded by derivation method (decomposition, reconstruction, recomposition, equivalence, password guessing)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Contextual narrative text&lt;/strong&gt; describing the attacker&amp;rsquo;s actions in natural language&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The TUI uses an alternate screen buffer, hides the cursor, detects terminal width, and throttles redraws to 20fps to avoid flooding the terminal. All rendering uses 1-cell-wide Unicode characters (box drawing, block elements) for correct alignment across terminal emulators.&lt;/p&gt;
&lt;h3 id=&#34;character-modes&#34;&gt;Character Modes&lt;/h3&gt;
&lt;p&gt;Because formal verification should be fun: the &lt;code&gt;--character&lt;/code&gt; flag lets you switch the attacker&amp;rsquo;s narrative voice. The default is a clinical security analyst. But you can also run analysis as &lt;strong&gt;Jevil&lt;/strong&gt; or &lt;strong&gt;Spamton&lt;/strong&gt; from Deltarune, complete with character-appropriate commentary on every phase of the attack:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Jevil (on deduction):&lt;/strong&gt; &amp;ldquo;UEE HEE HEE! A SECRET FALLS INTO MY TINY HANDS!&amp;rdquo;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Spamton (on mutation):&lt;/strong&gt; &amp;ldquo;HEY Alice!! WANT SOME [[Slightly Used]] REPLACEMENT VALUES?? ONLY 3 [[Kromer]]!!&amp;rdquo;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Spamton (on query failure):&lt;/strong&gt; &amp;ldquo;[[DEAL OF A LIFETIME]]!! YOUR PROTOCOL IS [[Bankrupt]]!!&amp;rdquo;&lt;/p&gt;&lt;/blockquote&gt;
&lt;h2 id=&#34;expanded-test-suite&#34;&gt;Expanded Test Suite&lt;/h2&gt;
&lt;p&gt;The Go version had 67 integration tests and no unit tests. The Rust version has &lt;strong&gt;147 tests&lt;/strong&gt;: 70 unit tests covering value equivalence, equation flattening, primitive handling, and other core operations, plus 77 integration tests running full protocol models.&lt;/p&gt;
&lt;p&gt;Fifteen new protocol models were added, covering scenarios the old test suite never touched:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Double Ratchet&lt;/strong&gt; (&lt;code&gt;double_ratchet.vp&lt;/code&gt;): Signal-style DH ratcheting with alternating rounds and HKDF chain key derivation&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Triple DH&lt;/strong&gt; (&lt;code&gt;triple_dh.vp&lt;/code&gt;): X3DH-style key agreement combining identity, signed, and ephemeral DH computations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Four-Party Relay&lt;/strong&gt; (&lt;code&gt;four_party.vp&lt;/code&gt;): Multi-hop message relay with cross-principal authentication and re-encryption&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Phase Forward Secrecy&lt;/strong&gt; (&lt;code&gt;phase_forward_secrecy.vp&lt;/code&gt;): Ephemeral DH ensuring confidentiality survives long-term key compromise&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PSK + DH&lt;/strong&gt; (&lt;code&gt;psk_with_dh.vp&lt;/code&gt;): TLS PSK+DHE-style hybrid key agreement with forward secrecy under PSK compromise&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Key Ratchet&lt;/strong&gt; (&lt;code&gt;key_ratchet.vp&lt;/code&gt;): Symmetric HKDF chain key ratcheting across multiple messages&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shamir Reconstruction&lt;/strong&gt; (&lt;code&gt;shamir_reconstruction.vp&lt;/code&gt;): 2-of-3 threshold secret sharing with partial share leakage&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Blind Signature&lt;/strong&gt; (&lt;code&gt;blind_signature.vp&lt;/code&gt;): Message blinding, blind signing, and signature unblinding&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deep Nesting&lt;/strong&gt; (&lt;code&gt;deep_nesting.vp&lt;/code&gt;): Five layers of nested encryption testing value resolution depth&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Concat Bomb&lt;/strong&gt; variants: Maximum-arity concatenation testing decomposition, equivalence, key leakage, and unchecked assertions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Many Principals&lt;/strong&gt; (&lt;code&gt;many_principals.vp&lt;/code&gt;): Broadcast signature verification across six recipients&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Passive DH Chain&lt;/strong&gt; (&lt;code&gt;passive_dh_chain.vp&lt;/code&gt;): Three-hop relay testing chain-of-custody under passive attackers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These models test the protocol patterns that make up most real design work: ratcheting, multi-party flows, threshold cryptography, hybrid key agreement.&lt;/p&gt;
&lt;h2 id=&#34;by-the-numbers&#34;&gt;By the Numbers&lt;/h2&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;&lt;/th&gt;
          &lt;th&gt;Go (v0.31.2)&lt;/th&gt;
          &lt;th&gt;Rust&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Language&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;Go 1.22&lt;/td&gt;
          &lt;td&gt;Rust (2024 edition)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Source files&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;22&lt;/td&gt;
          &lt;td&gt;26&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Lines of code&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;~7,100&lt;/td&gt;
          &lt;td&gt;~9,600&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Dependencies&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;14 (go.sum: 562 lines)&lt;/td&gt;
          &lt;td&gt;3 (colored, clap, rayon)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Test count&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;67 integration&lt;/td&gt;
          &lt;td&gt;70 unit + 77 integration&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Parser&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;Generated PEG&lt;/td&gt;
          &lt;td&gt;Hand-written recursive descent&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Largest file&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;value.go (935 lines)&lt;/td&gt;
          &lt;td&gt;tui.rs (TUI, new feature)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Global mutable state&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;AttackerState + mutex&lt;/td&gt;
          &lt;td&gt;1 AtomicU32 (TUI counter)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Build&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;go build&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;cargo build --release&lt;/code&gt; (LTO, single codegen unit)&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The Rust binary ships with fat link-time optimization, a single codegen unit, symbol stripping, and panic=abort, which together produce a compact, fully static binary with no runtime dependencies.&lt;/p&gt;
&lt;h2 id=&#34;getting-verifpal&#34;&gt;Getting Verifpal&lt;/h2&gt;
&lt;p&gt;Verifpal is available now:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Homebrew&lt;/strong&gt; (Linux/macOS): &lt;code&gt;brew install verifpal&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scoop&lt;/strong&gt; (Windows): &lt;code&gt;scoop install verifpal&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Source&lt;/strong&gt;: &lt;code&gt;cargo build --release&lt;/code&gt; from the &lt;a href=&#34;https://github.com/symbolicsoft/verifpal&#34;&gt;GitHub repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Releases&lt;/strong&gt;: Pre-built binaries for Windows, Linux, macOS, and FreeBSD on &lt;a href=&#34;https://github.com/symbolicsoft/verifpal/releases&#34;&gt;GitHub Releases&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;a href=&#34;https://static.verifpal.com/manual.pdf&#34;&gt;Verifpal User Manual&lt;/a&gt; and the &lt;a href=&#34;https://github.com/symbolicsoft/verifpal-vscode&#34;&gt;VS Code extension&lt;/a&gt; (syntax highlighting, live analysis, diagram visualizations) remain fully compatible with the Rust version.&lt;/p&gt;
&lt;p&gt;This rewrite took weeks of careful work. Every test passes. Every protocol model produces identical results to the Go version, plus the new ones the Go version couldn&amp;rsquo;t handle. The codebase is smaller in spirit if not in line count: structured, typed, and tested in ways that weren&amp;rsquo;t possible before.&lt;/p&gt;
</content:encoded>
      <category>Software</category>
      <category>Verifpal</category>
      <category>Formal Verification</category>
      
    </item>
    
    <item>
      <title>Security Announcement: Bugs in Noise Explorer&#39;s Rust/WASM Code Generation</title>
      <link>https://symbolic.software/blog/2026-02-22-noiseexplorer-advisory/</link>
      <pubDate>Sun, 22 Feb 2026 00:00:00 &#43;0000</pubDate>
      <guid>https://symbolic.software/blog/2026-02-22-noiseexplorer-advisory/</guid>
      <description>We&#39;ve updated Noise Explorer to address two bugs in generated Rust and WebAssembly implementations of Noise Protocol Framework handshake patterns.</description>
      <content:encoded>&lt;p&gt;&lt;strong&gt;Update (February 23, 2026):&lt;/strong&gt; A second security advisory has been published: &lt;a href=&#34;https://github.com/symbolicsoft/noiseexplorer/security/advisories/GHSA-q6mw-qh5x-m2p8&#34;&gt;GHSA-q6mw-qh5x-m2p8&lt;/a&gt;. This advisory addresses four additional bugs across Go, Rust and WebAssembly code generation templates, including a truncated forbidden Curve25519 point value in Go, a panic-on-validation crash reachable by remote attackers in Go, a fixed-size buffer overflow enabling denial of service in Rust and WebAssembly, and a public key validation bypass in Rust and WebAssembly key generation. These issues are fixed in &lt;a href=&#34;https://github.com/symbolicsoft/noiseexplorer/releases/tag/v1.0.6&#34;&gt;Noise Explorer v1.0.6&lt;/a&gt;. &lt;strong&gt;Users who have generated Go, Rust or WebAssembly implementations should regenerate their code using version 1.0.6 or later.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://noiseexplorer.com&#34;&gt;Noise Explorer&lt;/a&gt; is Symbolic Software&amp;rsquo;s online engine for reasoning about &lt;a href=&#34;https://noiseprotocol.org&#34;&gt;Noise Protocol Framework&lt;/a&gt; Handshake Patterns. It allows users to design Noise Handshake Patterns, generate formal verification models, explore pre-computed formal verification results, and generate secure software implementations in Go, Rust and WebAssembly.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;ve updated Noise Explorer to address two bugs in its Rust and WebAssembly code generation templates. These bugs affected all Noise protocol implementations generated by Noise Explorer versions 1.0.4 and earlier. &lt;strong&gt;Users who have generated Rust or WebAssembly implementations using Noise Explorer should regenerate their code using &lt;a href=&#34;https://github.com/symbolicsoft/noiseexplorer/releases/tag/v1.0.5&#34;&gt;version 1.0.5&lt;/a&gt; or later.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A security advisory has been published: &lt;a href=&#34;https://github.com/symbolicsoft/noiseexplorer/security/advisories/GHSA-6pc6-w328-gw8x&#34;&gt;GHSA-6pc6-w328-gw8x&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;bug-1-public-key-validation-bypass-rust-only&#34;&gt;Bug 1: Public Key Validation Bypass (Rust Only)&lt;/h2&gt;
&lt;p&gt;The first bug was located in the Rust code generation template (&lt;code&gt;src/rs/1types.rs&lt;/code&gt;). The &lt;code&gt;PublicKey::from_str()&lt;/code&gt; function bypassed small-order Curve25519 point validation, allowing an attacker to supply a low-order public key. A Diffie-Hellman operation with a small-order point produces a predictable all-zero shared secret, which could lead to a loss of message confidentiality.&lt;/p&gt;
&lt;p&gt;This bug affected generated Rust implementations only. Go and WebAssembly implementations were not affected.&lt;/p&gt;
&lt;h2 id=&#34;bug-2-incorrect-cipherstate-rekeying-rust-and-wasm&#34;&gt;Bug 2: Incorrect Cipherstate Rekeying (Rust and WASM)&lt;/h2&gt;
&lt;p&gt;The second bug was located in the Rust and WebAssembly code generation templates (&lt;code&gt;src/rs/6processes.rs&lt;/code&gt; and &lt;code&gt;src/wasm/6processes.rs&lt;/code&gt;). The &lt;code&gt;rekey_remote_cipherstate&lt;/code&gt; function incorrectly operated on the local cipherstate instead of the remote one. This caused desynchronization between peers: after a rekey operation, subsequent sent messages would use a doubly-rekeyed key, causing decryption failures on the receiving side.&lt;/p&gt;
&lt;p&gt;This bug affected both generated Rust and WebAssembly implementations. Go implementations were not affected.&lt;/p&gt;
&lt;h2 id=&#34;unaffected-components&#34;&gt;Unaffected Components&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;All Go implementations generated by Noise Explorer.&lt;/li&gt;
&lt;li&gt;All ProVerif formal verification models generated by Noise Explorer.&lt;/li&gt;
&lt;li&gt;WebAssembly implementations (for the public key validation bug only).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;impact&#34;&gt;Impact&lt;/h2&gt;
&lt;p&gt;The public key validation bypass could allow an attacker to force a predictable shared secret, undermining the confidentiality of messages encrypted under the resulting session keys. The cipherstate rekeying bug would cause communication failures between peers after a rekey operation. Neither bug affected the correctness of Noise Explorer&amp;rsquo;s formal verification models.&lt;/p&gt;
&lt;h2 id=&#34;resolution&#34;&gt;Resolution&lt;/h2&gt;
&lt;p&gt;Both bugs have been fixed in &lt;a href=&#34;https://github.com/symbolicsoft/noiseexplorer/releases/tag/v1.0.5&#34;&gt;Noise Explorer v1.0.5&lt;/a&gt;. The Rust code generation templates have also been upgraded to the Rust 2024 edition with updated dependencies.&lt;/p&gt;
&lt;p&gt;Users who have previously generated Rust or WebAssembly Noise protocol implementations using Noise Explorer should regenerate their implementations using version 1.0.5 or later.&lt;/p&gt;
&lt;h2 id=&#34;acknowledgements&#34;&gt;Acknowledgements&lt;/h2&gt;
&lt;p&gt;We thank Elichai Turkel for identifying both issues through code review of a generated Noise-KK Rust implementation.&lt;/p&gt;
</content:encoded>
      <category>Security</category>
      <category>Noise Explorer</category>
      <category>Protocol Design</category>
      
    </item>
    
    <item>
      <title>Even More Bugs in CE Labs&#39; libcrux: ML-DSA</title>
      <link>https://symbolic.software/blog/2026-02-17-ce-labs-mldsa/</link>
      <pubDate>Tue, 17 Feb 2026 00:00:00 &#43;0000</pubDate>
      <guid>https://symbolic.software/blog/2026-02-17-ce-labs-mldsa/</guid>
      <description>Three findings in libcrux&#39;s ML-DSA implementation: a verifier norm check that is dead code due to a wrong constant, a missing bounds check in hint deserialization, and a wrong multiplication specification that renders AVX2 proofs unsound.</description>
      <content:encoded>




&lt;nav class=&#34;series-nav&#34; aria-label=&#34;CE Labs series (top)&#34;&gt;
  &lt;header class=&#34;series-nav-header&#34;&gt;
    &lt;span class=&#34;series-nav-label&#34;&gt;▶ CE Labs Series&lt;/span&gt;
    &lt;span class=&#34;series-nav-count&#34;&gt;5 posts · 2 papers · 1 talk&lt;/span&gt;
  &lt;/header&gt;
  &lt;ol class=&#34;series-nav-list&#34;&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-02-05-ce-labs/&#34;&gt;On the Promises of &amp;#39;High-Assurance&amp;#39; Cryptography&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.02.05&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-02-12-ce-labs-response/&#34;&gt;We Found Bugs in CE Labs&amp;#39; Verified Code&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.02.12&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li class=&#34;current&#34; aria-current=&#34;page&#34;&gt;
      
      &lt;span class=&#34;title&#34;&gt;Even More Bugs in CE Labs&amp;#39; libcrux: ML-DSA&lt;/span&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.02.17&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-03-07-ce-labs-tls/&#34;&gt;CE Labs&amp;#39; Approach to TLS: A Critical Analysis&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.03.07&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-04-07-cryspen-hax/&#34;&gt;The Verification Facade: Structural Gaps in Cryspen&amp;#39;s Hax Pipeline&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.04.07&lt;/span&gt;
    &lt;/li&gt;
    
  &lt;/ol&gt;
  &lt;div class=&#34;series-nav-footer&#34;&gt;
    &lt;div class=&#34;series-nav-row&#34;&gt;
      &lt;span class=&#34;series-nav-rowlabel&#34;&gt;Papers&lt;/span&gt;
      &lt;a href=&#34;https://eprint.iacr.org/2026/192&#34;&gt;&lt;em&gt;Verification Theatre&lt;/em&gt;&lt;/a&gt;
      &lt;span class=&#34;series-nav-sep&#34;&gt;·&lt;/span&gt;
      &lt;a href=&#34;https://eprint.iacr.org/2026/670&#34;&gt;&lt;em&gt;Verification Facade&lt;/em&gt;&lt;/a&gt;
    &lt;/div&gt;
    &lt;div class=&#34;series-nav-row&#34;&gt;
      &lt;span class=&#34;series-nav-rowlabel&#34;&gt;Talk&lt;/span&gt;
      &lt;a href=&#34;https://www.youtube.com/watch?v=TdOXza1-M_4&#34;&gt;&lt;em&gt;High Assurance Cryptography and the Ethics of Disclosure (OSTIF)&lt;/em&gt;&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/nav&gt;

&lt;p&gt;Over the past two weeks, we have published &lt;a href=&#34;https://symbolic.software/blog/2026-02-05-ce-labs/&#34;&gt;eight vulnerabilities&lt;/a&gt; across CE Labs&amp;rsquo; cryptographic libraries, including &lt;a href=&#34;https://symbolic.software/blog/2026-02-12-ce-labs-response/&#34;&gt;three bugs in formally verified code&lt;/a&gt; that CE Labs&amp;rsquo; own documentation claims is correct.&lt;/p&gt;
&lt;p&gt;We now report three additional findings in libcrux&amp;rsquo;s ML-DSA implementation, the post-quantum digital signature algorithm standardized as FIPS 204. The first two are FIPS 204 specification violations in runtime code. The third is a wrong specification in the F* formal verification infrastructure that renders AVX2 proofs unsound.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update (Feb 19):&lt;/strong&gt; Finding 3 added, an error in the ML-DSA AVX2 proof specification that makes the formally verified intrinsics axioms logically false.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update (Mar 6):&lt;/strong&gt; CE Labs has fixed Findings 1 and 2. Finding 1 (verifier norm check) was fixed in &lt;a href=&#34;https://github.com/celabshq/libcrux/pull/1347&#34;&gt;PR #1347&lt;/a&gt;, merged March 4, 2026. Finding 2 (hint deserialization) was fixed in &lt;a href=&#34;https://github.com/celabshq/libcrux/pull/1348&#34;&gt;PR #1348&lt;/a&gt;, merged March 4, 2026. Both PRs credit the discoverer.&lt;/p&gt;
&lt;h2 id=&#34;finding-1-ml-dsa-verifier-norm-check-is-dead-code&#34;&gt;Finding 1: ML-DSA Verifier Norm Check Is Dead Code&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Location:&lt;/strong&gt; &lt;code&gt;libcrux-ml-dsa/src/ml_dsa_generic.rs&lt;/code&gt;, line 404.&lt;/p&gt;
&lt;p&gt;The ML-DSA verifier checks whether the signer response vector $\mathbf{z}$ has an infinity norm below a certain bound. This is a critical security check: it ensures that $\mathbf{z}$ does not leak information about the secret key. FIPS 204, Algorithm 8 (ML-DSA.Verify_internal), step 13 specifies:&lt;/p&gt;
$$\text{return } \lVert\mathbf{z}\rVert_\infty &amp;lt; \gamma_1 - \beta \text{ and } \tilde{c} = \tilde{c}&#39;$$&lt;p&gt;And Algorithm 7 (ML-DSA.Sign_internal), step 23 specifies:&lt;/p&gt;
$$\text{if } \lVert\mathbf{z}\rVert_\infty \geq \gamma_1 - \beta \text{ or } \lVert r_0\rVert_\infty \geq \gamma_2 - \beta \text{, then } (\mathbf{z}, \mathbf{h}) \leftarrow \bot$$&lt;p&gt;Both use $\gamma_1 - \beta$. The libcrux verifier uses the wrong bound:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// Line 402-405 (verifier, BUG):
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;vector_infinity_norm_exceeds&lt;/span&gt;::&lt;span class=&#34;o&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;SIMDUnit&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;deserialized_signer_response&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;no&#34;&gt;GAMMA1_EXPONENT&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;no&#34;&gt;BETA&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;//   ^^^^^^^^^^^^^^^^^^^^^^^^ = 2·γ₁ - β
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;return&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;Err&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;VerificationError&lt;/span&gt;::&lt;span class=&#34;n&#34;&gt;SignerResponseExceedsBoundError&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The expression &lt;code&gt;2 &amp;lt;&amp;lt; GAMMA1_EXPONENT&lt;/code&gt; computes $2 \cdot 2^{\gamma_1\text{exp}} = 2\gamma_1$, not $\gamma_1$. Compare with the signer, which correctly uses &lt;code&gt;1 &amp;lt;&amp;lt; GAMMA1_EXPONENT&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// Line 284 (signer, correct):
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;vector_infinity_norm_exceeds&lt;/span&gt;::&lt;span class=&#34;o&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;SIMDUnit&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;mask&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;no&#34;&gt;GAMMA1_EXPONENT&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;no&#34;&gt;BETA&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;//                                                  ^^^^^^^^^^^^^^^^^^^^^^^^ = γ₁ - β
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The signer checks $\gamma_1 - \beta$. The verifier checks $2\gamma_1 - \beta$. One constant was doubled.&lt;/p&gt;
&lt;h3 id=&#34;the-check-can-never-trigger&#34;&gt;The Check Can Never Trigger&lt;/h3&gt;
&lt;p&gt;The signer response $\mathbf{z}$ is deserialized from a $\gamma_1$-encoded representation: each coefficient is encoded as a value in $[-\gamma_1, \gamma_1]$. This means that for any deserialized $\mathbf{z}$, $\lVert\mathbf{z}\rVert_\infty \leq \gamma_1$. The buggy bound is $2\gamma_1 - \beta$. Since $\beta &gt; 0$:&lt;/p&gt;
$$\gamma_1 &amp;lt; 2\gamma_1 - \beta$$&lt;p&gt;The check &lt;code&gt;vector_infinity_norm_exceeds(&amp;amp;deserialized_signer_response, (2 &amp;lt;&amp;lt; GAMMA1_EXPONENT) - BETA)&lt;/code&gt; can never return true. The verifier&amp;rsquo;s norm check is dead code. For each ML-DSA parameter set:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Parameter Set&lt;/th&gt;
          &lt;th&gt;$\gamma_1$&lt;/th&gt;
          &lt;th&gt;$\beta$&lt;/th&gt;
          &lt;th&gt;Max $\lVert\mathbf{z}\rVert_\infty$&lt;/th&gt;
          &lt;th&gt;Buggy bound ($2\gamma_1 - \beta$)&lt;/th&gt;
          &lt;th&gt;Correct bound ($\gamma_1 - \beta$)&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;ML-DSA-44&lt;/td&gt;
          &lt;td&gt;$2^{17} = 131072$&lt;/td&gt;
          &lt;td&gt;78&lt;/td&gt;
          &lt;td&gt;131072&lt;/td&gt;
          &lt;td&gt;262066&lt;/td&gt;
          &lt;td&gt;130994&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;ML-DSA-65&lt;/td&gt;
          &lt;td&gt;$2^{19} = 524288$&lt;/td&gt;
          &lt;td&gt;196&lt;/td&gt;
          &lt;td&gt;524288&lt;/td&gt;
          &lt;td&gt;1048380&lt;/td&gt;
          &lt;td&gt;524092&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;ML-DSA-87&lt;/td&gt;
          &lt;td&gt;$2^{19} = 524288$&lt;/td&gt;
          &lt;td&gt;120&lt;/td&gt;
          &lt;td&gt;524288&lt;/td&gt;
          &lt;td&gt;1048456&lt;/td&gt;
          &lt;td&gt;524168&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;In every case, the maximum possible infinity norm ($\gamma_1$) is less than the buggy bound ($2\gamma_1 - \beta$). The check is vacuously true. The verifier unconditionally passes any deserialized signer response, regardless of whether it satisfies the FIPS 204 requirement $\lVert\mathbf{z}\rVert_\infty &amp;lt; \gamma_1 - \beta$.&lt;/p&gt;
&lt;h3 id=&#34;concrete-impact&#34;&gt;Concrete Impact&lt;/h3&gt;
&lt;p&gt;A conforming signer will never produce a signature with $\lVert\mathbf{z}\rVert_\infty \geq \gamma_1 - \beta$, because the signing algorithm rejects such candidates and retries. But a malicious signer could produce signatures with $\gamma_1 - \beta \leq \lVert\mathbf{z}\rVert_\infty \leq \gamma_1$ that pass libcrux&amp;rsquo;s verifier but would be correctly rejected by any FIPS 204-conforming implementation.&lt;/p&gt;
&lt;p&gt;This creates an interoperability hazard. A system using libcrux for verification would accept signatures that every other conforming implementation rejects:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Post-quantum certificate chains.&lt;/strong&gt; A certificate authority or TLS implementation using libcrux to verify ML-DSA signatures on certificates would accept certificates that conforming verifiers reject. An attacker could issue a certificate that appears valid to libcrux-based systems but is rejected everywhere else, creating a split view of the PKI.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Firmware signing.&lt;/strong&gt; A secure boot chain using libcrux for signature verification would accept firmware images signed with out-of-bound $\mathbf{z}$ vectors. A malicious firmware image could pass libcrux&amp;rsquo;s verification while being correctly rejected by any other FIPS 204-conforming verifier on the same device or in audit tooling.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Consensus protocols.&lt;/strong&gt; In a distributed system where nodes verify ML-DSA signatures to reach agreement, libcrux nodes would accept messages that non-libcrux nodes reject. An adversary could exploit this to partition the network: libcrux nodes see a valid message, conforming nodes do not.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;provenance&#34;&gt;Provenance&lt;/h3&gt;
&lt;p&gt;Introduced in commit &lt;a href=&#34;https://github.com/celabshq/libcrux/commit/326c837a33&#34;&gt;&lt;code&gt;326c837a33&lt;/code&gt;&lt;/a&gt; on January 7, 2025, by Jonas Schneider-Bensch, with the message &amp;ldquo;Further macro monomorphization of parameter sets.&amp;rdquo; The signer&amp;rsquo;s correct bound was already present in the codebase. The verifier&amp;rsquo;s bound was written incorrectly in the same refactoring pass.&lt;/p&gt;
&lt;h2 id=&#34;finding-2-hint-deserialization-missing-final-bound-check&#34;&gt;Finding 2: Hint Deserialization Missing Final Bound Check&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Location:&lt;/strong&gt; &lt;code&gt;libcrux-ml-dsa/src/encoding/signature.rs&lt;/code&gt;, lines 91&amp;ndash;120.&lt;/p&gt;
&lt;p&gt;ML-DSA signatures contain a hint vector $\mathbf{h}$ that encodes which coefficients of the high-order bits need adjustment. The hint is serialized as a list of indices followed by cumulative counts, with a maximum of $\omega$ total true hints across all polynomials. FIPS 204, Algorithm 21 (HintBitUnpack), step 4 specifies, for every row $i$ from 0 to $k - 1$:&lt;/p&gt;
$$\text{if } y[\omega + i] &amp;lt; \text{Index} \text{ or } y[\omega + i] &gt; \omega \text{ then return } \bot$$&lt;p&gt;This checks the &lt;em&gt;current&lt;/em&gt; cumulative count $y[\omega + i]$ against $\omega$ on &lt;em&gt;every&lt;/em&gt; iteration. It ensures that no row&amp;rsquo;s cumulative count exceeds the allowed maximum.&lt;/p&gt;
&lt;p&gt;The libcrux implementation checks the wrong variable:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;in&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;..&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;rows_in_a&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;current_true_hints_seen&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;hint_serialized&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;max_ones_in_hint&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;+&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;as&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;usize&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;current_true_hints_seen&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;previous_true_hints_seen&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;||&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;previous_true_hints_seen&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;max_ones_in_hint&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;//      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;//      Checks PREVIOUS iteration&amp;#39;s count, not current.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;//      FIPS 204 checks: y[ω + i] &amp;gt; ω (i.e., current &amp;gt; max)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;malformed_hint&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;break&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;j&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;in&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;previous_true_hints_seen&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;..&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;current_true_hints_seen&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// ... process hint indices ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;set_hint&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;out_hint&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;hint_serialized&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;j&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;as&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;usize&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;previous_true_hints_seen&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;current_true_hints_seen&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The check uses &lt;code&gt;previous_true_hints_seen &amp;gt; max_ones_in_hint&lt;/code&gt; instead of &lt;code&gt;current_true_hints_seen &amp;gt; max_ones_in_hint&lt;/code&gt;. It validates the count from the &lt;em&gt;previous&lt;/em&gt; iteration rather than the current one. For the last row (&lt;code&gt;i = rows_in_a - 1&lt;/code&gt;), the current count is read, the hint indices are processed and applied via &lt;code&gt;set_hint&lt;/code&gt;, and &lt;code&gt;previous_true_hints_seen&lt;/code&gt; is updated, but the overflow check will not fire until the next iteration, which never comes.&lt;/p&gt;
&lt;p&gt;The subsequent padding check (line 122) is supposed to verify that all remaining bytes in the hint section are zero:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;j&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;in&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;previous_true_hints_seen&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;..&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;max_ones_in_hint&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;hint_serialized&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;j&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;!=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;malformed_hint&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;break&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;But if &lt;code&gt;previous_true_hints_seen&lt;/code&gt; equals or exceeds &lt;code&gt;max_ones_in_hint&lt;/code&gt; after the last row, this range is empty and the loop body never executes. The padding check is silently skipped.&lt;/p&gt;
&lt;h3 id=&#34;concrete-impact-1&#34;&gt;Concrete Impact&lt;/h3&gt;
&lt;p&gt;A crafted signature can set the last row&amp;rsquo;s cumulative count to a value greater than $\omega$ (&lt;code&gt;max_ones_in_hint&lt;/code&gt;). The inner loop will then iterate over bytes in the trailer region of the hint encoding, interpreting arbitrary byte values as hint indices and setting corresponding positions via &lt;code&gt;set_hint&lt;/code&gt;. For each parameter set:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Parameter Set&lt;/th&gt;
          &lt;th&gt;$\omega$ (&lt;code&gt;max_ones_in_hint&lt;/code&gt;)&lt;/th&gt;
          &lt;th&gt;Rows ($k$)&lt;/th&gt;
          &lt;th&gt;Trailer bytes&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;ML-DSA-44&lt;/td&gt;
          &lt;td&gt;80&lt;/td&gt;
          &lt;td&gt;4&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;hint_serialized[80..84]&lt;/code&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;ML-DSA-65&lt;/td&gt;
          &lt;td&gt;55&lt;/td&gt;
          &lt;td&gt;6&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;hint_serialized[55..61]&lt;/code&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;ML-DSA-87&lt;/td&gt;
          &lt;td&gt;75&lt;/td&gt;
          &lt;td&gt;8&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;hint_serialized[75..83]&lt;/code&gt;&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The trailer bytes are the cumulative count bytes for each row. An attacker controls these values as part of the signature. By setting the last row&amp;rsquo;s count to &lt;code&gt;max_ones_in_hint + rows_in_a&lt;/code&gt; (or any value up to 255), the inner loop reads the trailer bytes as hint indices, setting arbitrary hint positions in the last polynomial. This produces a hint vector that a FIPS 204-conforming implementation would reject.&lt;/p&gt;
&lt;p&gt;As with the verifier norm bound, this creates a divergence between libcrux and conforming implementations. A signature accepted by libcrux may be rejected elsewhere, or worse, the corrupted hint vector may cause the verification to produce a different result than intended:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Signature forgery assistance.&lt;/strong&gt; The hint vector directly affects which high-order bits are adjusted during verification. By injecting arbitrary hint positions through the overflow, an attacker gains additional degrees of freedom in crafting signatures that pass verification. This weakens the effective security of the scheme by relaxing constraints that the signer is supposed to satisfy.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cross-implementation disagreement.&lt;/strong&gt; A signature with an overflowed hint count would verify successfully under libcrux but fail under any conforming implementation. In a system where multiple parties independently verify the same signature, such as a transparency log or a multi-verifier protocol, this produces contradictory verification results with no indication of which is correct.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Strong unforgeability violation.&lt;/strong&gt; FIPS 204 mandates a unique hint encoding precisely to ensure strong unforgeability: each valid signature has exactly one valid serialization. The missing bound check allows multiple distinct serializations to decode to the same logical signature, breaking this property. An attacker can produce a second valid encoding of an existing signature without access to the signing key.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;provenance-1&#34;&gt;Provenance&lt;/h3&gt;
&lt;p&gt;The hint deserialization logic has been touched by multiple authors across several commits. The current loop structure was established by Jonas Schneider-Bensch in commit &lt;a href=&#34;https://github.com/celabshq/libcrux/commit/83ffae727d&#34;&gt;&lt;code&gt;83ffae727d&lt;/code&gt;&lt;/a&gt; on January 2, 2025, and further modified by Franziskus Kiefer (CE Labs&amp;rsquo; CEO) in commit &lt;a href=&#34;https://github.com/celabshq/libcrux/commit/404115bc08&#34;&gt;&lt;code&gt;404115bc08&lt;/code&gt;&lt;/a&gt; on January 13, 2025. The early-return logic was later replaced with flag-and-break by Jonas Schneider-Bensch in commit &lt;a href=&#34;https://github.com/celabshq/libcrux/commit/d979b001f9&#34;&gt;&lt;code&gt;d979b001f9&lt;/code&gt;&lt;/a&gt; on November 17, 2025, as a workaround for a bug in Eurydice, with the comment: &amp;ldquo;We would like to use early returns below, but doing so triggers a bug in Eurydice.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;The workaround for Eurydice&amp;rsquo;s inability to handle early returns inside loops restructured the control flow in a way that obscured the missing final bound check. The original early-return code may or may not have had the same bug, but the flag-and-break rewrite made it harder to spot.&lt;/p&gt;
&lt;h2 id=&#34;finding-3-wrong-multiplication-specification-in-ml-dsa-avx2-proofs&#34;&gt;Finding 3: Wrong Multiplication Specification in ML-DSA AVX2 Proofs&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Location:&lt;/strong&gt; &lt;code&gt;libcrux-ml-dsa/proofs/fstar/spec/Spec.Intrinsics.fsti&lt;/code&gt;, line 97.&lt;/p&gt;
&lt;p&gt;The F* specification file &lt;code&gt;Spec.Intrinsics.fsti&lt;/code&gt; defines the semantics of low-level AVX2 intrinsics used in the ML-DSA proof infrastructure. The function &lt;code&gt;i16_mul_32extended&lt;/code&gt; specifies 16-to-32-bit widening multiplication, the primitive underlying Intel&amp;rsquo;s &lt;code&gt;VPMADDWD&lt;/code&gt; and &lt;code&gt;VPMULLW&lt;/code&gt; instructions. It takes two parameters &lt;code&gt;x&lt;/code&gt; and &lt;code&gt;y&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;[@@ &amp;#34;opaque_to_smt&amp;#34;]
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;let i16_mul_32extended (x y: i16): i32 =
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  (cast x &amp;lt;: i32) *! (cast x &amp;lt;: i32)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;(*               ^^^ should be: cast y *)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The body uses &lt;code&gt;x&lt;/code&gt; for both operands. The parameter &lt;code&gt;y&lt;/code&gt; is completely ignored. The function computes $x^2$ instead of $x \cdot y$. This is likely a copy-paste error: &lt;code&gt;cast x&lt;/code&gt; was duplicated instead of writing &lt;code&gt;cast y&lt;/code&gt; for the second operand.&lt;/p&gt;
&lt;h3 id=&#34;why-formal-verification-cannot-catch-this&#34;&gt;Why Formal Verification Cannot Catch This&lt;/h3&gt;
&lt;p&gt;Three mechanisms conspire to make this bug invisible to the F* proof system:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. The definition is hidden from the solver.&lt;/strong&gt; The &lt;code&gt;[@@ &amp;quot;opaque_to_smt&amp;quot;]&lt;/code&gt; attribute on line 96 prevents Z3 from unfolding the function body. The solver never sees the &lt;code&gt;x * x&lt;/code&gt; computation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. All lemmas are unproven axioms.&lt;/strong&gt; The file contains approximately five &lt;code&gt;val&lt;/code&gt; declarations that characterize &lt;code&gt;i16_mul_32extended&lt;/code&gt;&amp;rsquo;s behavior: that multiplying by a power of two equals a left shift, for instance, or that multiplying by zero yields zero. These are stated as F* &lt;code&gt;val&lt;/code&gt; declarations without bodies. No corresponding &lt;code&gt;Spec.Intrinsics.fst&lt;/code&gt; implementation file exists. In F*, a &lt;code&gt;val&lt;/code&gt; without a corresponding &lt;code&gt;let&lt;/code&gt; in an implementation file is an axiom: the solver accepts it without proof. Every lemma about &lt;code&gt;i16_mul_32extended&lt;/code&gt; is an article of faith.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. The axioms claim correct $x \cdot y$ behavior.&lt;/strong&gt; The axioms describe what the function &lt;em&gt;should&lt;/em&gt; do (multiply &lt;code&gt;x&lt;/code&gt; by &lt;code&gt;y&lt;/code&gt;), not what it &lt;em&gt;actually&lt;/em&gt; does (square &lt;code&gt;x&lt;/code&gt;). Since the solver reasons from the axioms and never sees the definition, the contradiction is invisible. The axioms are logically false, but the solver has no way to discover this.&lt;/p&gt;
&lt;h3 id=&#34;impact-chain&#34;&gt;Impact Chain&lt;/h3&gt;
&lt;p&gt;The buggy function propagates through the specification:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;i16_mul_32extended_i16&lt;/code&gt;&lt;/strong&gt; (line 99) calls &lt;code&gt;i16_mul_32extended x y&lt;/code&gt;, inheriting the $x^2$ behavior.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;mm256_madd_epi16_lemma&lt;/code&gt;&lt;/strong&gt; (line 377) specifies AVX2&amp;rsquo;s &lt;code&gt;VPMADDWD&lt;/code&gt; instruction using &lt;code&gt;i16_mul_32extended&lt;/code&gt;. The claimed semantics are $a_{2k} \cdot b_{2k} + a_{2k+1} \cdot b_{2k+1}$; the actual definition computes $a_{2k}^2 + a_{2k+1}^2$, ignoring the second vector entirely.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;mm256_mullo_epi16_bv_lemma&lt;/code&gt;&lt;/strong&gt; (line 511) specifies AVX2&amp;rsquo;s &lt;code&gt;VPMULLW&lt;/code&gt; via &lt;code&gt;i16_mul_32extended_i16&lt;/code&gt;. Same issue: claims $a_i \cdot b_i$, computes $a_i^2$.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The ML-DSA build system&amp;rsquo;s Makefile lists nine AVX2 modules in &lt;code&gt;VERIFIED_MODULES&lt;/code&gt;, including &lt;code&gt;Simd.Avx2.Encoding.Error.fst&lt;/code&gt;, &lt;code&gt;Simd.Avx2.Encoding.Commitment.fst&lt;/code&gt;, &lt;code&gt;Simd.Avx2.Ntt.fst&lt;/code&gt;, and &lt;code&gt;Simd.Avx2.Arithmetic.fst&lt;/code&gt;. These modules are designated for full verification against the axioms in &lt;code&gt;Spec.Intrinsics.fsti&lt;/code&gt;. Any proofs built on these axioms inherit the unsoundness: the solver would accept incorrect code as correct, because it reasons from false premises.&lt;/p&gt;
&lt;p&gt;Put plainly: every ML-DSA proof CE Labs will ever produce for their AVX2 backend, the backend that runs on every modern x86 server and desktop, rests on roughly forty axioms that are provably false. No amount of future verification effort fixes that until someone finds and corrects the wrong definition, which has been sitting in the file since the day it was committed.&lt;/p&gt;
&lt;h3 id=&#34;what-this-does-not-affect&#34;&gt;What This Does Not Affect&lt;/h3&gt;
&lt;p&gt;The runtime Rust code is unaffected. The actual ML-DSA implementation uses real Intel AVX2 intrinsics (&lt;code&gt;_mm256_madd_epi16&lt;/code&gt;, &lt;code&gt;_mm256_mullo_epi16&lt;/code&gt;) that perform correct $x \cdot y$ multiplication in hardware. The F* specification is never compiled into runtime code. This bug cannot cause incorrect signatures or verification failures in deployed software.&lt;/p&gt;
&lt;p&gt;The impact is on &lt;em&gt;verification trustworthiness&lt;/em&gt;: the formal proofs that are supposed to guarantee correctness of the AVX2 code path are built on a false foundation. The proofs would say &amp;ldquo;this code is correct&amp;rdquo; regardless of whether it actually is, because the axioms they reason from do not match the operations they claim to model.&lt;/p&gt;
&lt;h3 id=&#34;structural-comparison&#34;&gt;Structural Comparison&lt;/h3&gt;
&lt;p&gt;Findings 1 and 2 are runtime bugs in admitted (unverified) modules, which the verification system never looks at. Finding 3 is different: the bug is &lt;em&gt;inside&lt;/em&gt; the verification infrastructure itself. The wrong specification was committed before the dependent proofs were written, establishing a false axiomatic foundation. Even running every proof with full solver dispatch would not catch it, because &lt;code&gt;val&lt;/code&gt; declarations without implementations are accepted by F* as axioms unconditionally.&lt;/p&gt;
&lt;p&gt;This is the same copy-paste pattern we have seen repeatedly: the &lt;a href=&#34;https://symbolic.software/blog/2026-02-12-ce-labs-response/&#34;&gt;ML-KEM decompression bug&lt;/a&gt; duplicated 1664 instead of writing &lt;code&gt;pow2 (d-1)&lt;/code&gt;, and the &lt;a href=&#34;https://symbolic.software/blog/2026-02-12-ce-labs-response/&#34;&gt;false serialization proof&lt;/a&gt; duplicated bound 1 instead of writing bound 12. Here, &lt;code&gt;cast x&lt;/code&gt; was duplicated instead of writing &lt;code&gt;cast y&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&#34;provenance-2&#34;&gt;Provenance&lt;/h3&gt;
&lt;p&gt;Introduced in commit &lt;a href=&#34;https://github.com/celabshq/libcrux/commit/cb165295de&#34;&gt;&lt;code&gt;cb165295de&lt;/code&gt;&lt;/a&gt; on May 21, 2025, by Lucas Franceschino, with the message &amp;ldquo;F*: mldsa: avx2: encoding: commitment.&amp;rdquo; The function was added as part of the ML-DSA AVX2 proof infrastructure. The bug has been present since the function&amp;rsquo;s introduction.&lt;/p&gt;
&lt;h2 id=&#34;the-pattern-continues&#34;&gt;The Pattern Continues&lt;/h2&gt;
&lt;p&gt;These three findings bring the total to eleven vulnerabilities across CE Labs&amp;rsquo; cryptographic libraries in two weeks. The pattern we identified in our &lt;a href=&#34;https://symbolic.software/blog/2026-02-05-ce-labs/&#34;&gt;original post&lt;/a&gt; and &lt;a href=&#34;https://eprint.iacr.org/2026/192&#34;&gt;paper&lt;/a&gt; remains consistent: bugs cluster in code that is unverified or lax-verified, while the marketing claims make no such distinction.&lt;/p&gt;
&lt;p&gt;Findings 1 and 2 are in ML-DSA&amp;rsquo;s core verification path, the code that determines whether a digital signature is valid. Both are straightforward deviations from FIPS 204 that a careful comparison against the specification would catch. Both reside in modules that are admitted by default in the build system, meaning that even if someone wrote formal specifications for them, the proofs would not be checked.&lt;/p&gt;
&lt;p&gt;Finding 3 reveals a different failure mode: the bug is not in admitted code but in the verification specification itself. The axiomatic foundation for AVX2 proofs contains a wrong definition hidden behind an opacity barrier, with unproven lemmas that claim correct behavior. That is not a gap in verification coverage. It is a corruption of the verification infrastructure.&lt;/p&gt;
&lt;p&gt;The ML-DSA Makefile makes the verification boundary explicit. Twenty-seven modules are in &lt;code&gt;VERIFIED_MODULES&lt;/code&gt;: SIMD arithmetic, NTT, encoding primitives, and type definitions. Everything else is in &lt;code&gt;ADMIT_MODULES&lt;/code&gt;, including the top-level signing and verification logic, signature encoding, and key generation. The verified modules are the leaves. The roots are unverified. And as Finding 3 shows, even the verified leaves rest on unproven axioms.&lt;/p&gt;
&lt;p&gt;CE Labs&amp;rsquo; response to our previous findings was to claim that &amp;ldquo;no bugs have been found in the verified code.&amp;rdquo; We &lt;a href=&#34;https://symbolic.software/blog/2026-02-12-ce-labs-response/&#34;&gt;showed that this was false&lt;/a&gt;. Finding 3 reinforces this: the specification that the verified code is checked against is itself wrong. When the axioms are false, &amp;ldquo;verified&amp;rdquo; is an empty word.&lt;/p&gt;
&lt;p&gt;Formal verification is supposed to provide assurance that goes beyond testing. But assurance requires that the verification boundary be communicated honestly. When a library is marketed as &amp;ldquo;formally verified&amp;rdquo; and &amp;ldquo;high assurance,&amp;rdquo; users reasonably expect that the core cryptographic operations, meaning signing, verification and key generation, are within that boundary. In libcrux&amp;rsquo;s ML-DSA, they are not. And even within that boundary, the axiomatic foundations have not been proved. Anyone who cited CE Labs&amp;rsquo; verification claims to justify deploying libcrux ML-DSA where signature correctness carries legal or contractual weight was relying on a foundation whose specification for multiplication computes the wrong operation.&lt;/p&gt;





&lt;nav class=&#34;series-nav&#34; aria-label=&#34;CE Labs series (bottom)&#34;&gt;
  &lt;header class=&#34;series-nav-header&#34;&gt;
    &lt;span class=&#34;series-nav-label&#34;&gt;▶ CE Labs Series&lt;/span&gt;
    &lt;span class=&#34;series-nav-count&#34;&gt;5 posts · 2 papers · 1 talk&lt;/span&gt;
  &lt;/header&gt;
  &lt;ol class=&#34;series-nav-list&#34;&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-02-05-ce-labs/&#34;&gt;On the Promises of &amp;#39;High-Assurance&amp;#39; Cryptography&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.02.05&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-02-12-ce-labs-response/&#34;&gt;We Found Bugs in CE Labs&amp;#39; Verified Code&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.02.12&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li class=&#34;current&#34; aria-current=&#34;page&#34;&gt;
      
      &lt;span class=&#34;title&#34;&gt;Even More Bugs in CE Labs&amp;#39; libcrux: ML-DSA&lt;/span&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.02.17&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-03-07-ce-labs-tls/&#34;&gt;CE Labs&amp;#39; Approach to TLS: A Critical Analysis&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.03.07&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-04-07-cryspen-hax/&#34;&gt;The Verification Facade: Structural Gaps in Cryspen&amp;#39;s Hax Pipeline&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.04.07&lt;/span&gt;
    &lt;/li&gt;
    
  &lt;/ol&gt;
  &lt;div class=&#34;series-nav-footer&#34;&gt;
    &lt;div class=&#34;series-nav-row&#34;&gt;
      &lt;span class=&#34;series-nav-rowlabel&#34;&gt;Papers&lt;/span&gt;
      &lt;a href=&#34;https://eprint.iacr.org/2026/192&#34;&gt;&lt;em&gt;Verification Theatre&lt;/em&gt;&lt;/a&gt;
      &lt;span class=&#34;series-nav-sep&#34;&gt;·&lt;/span&gt;
      &lt;a href=&#34;https://eprint.iacr.org/2026/670&#34;&gt;&lt;em&gt;Verification Facade&lt;/em&gt;&lt;/a&gt;
    &lt;/div&gt;
    &lt;div class=&#34;series-nav-row&#34;&gt;
      &lt;span class=&#34;series-nav-rowlabel&#34;&gt;Talk&lt;/span&gt;
      &lt;a href=&#34;https://www.youtube.com/watch?v=TdOXza1-M_4&#34;&gt;&lt;em&gt;High Assurance Cryptography and the Ethics of Disclosure (OSTIF)&lt;/em&gt;&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/nav&gt;

</content:encoded>
      <category>Research</category>
      <category>Formal Verification</category>
      <category>Post-Quantum</category>
      
    </item>
    
    <item>
      <title>We Found Bugs in CE Labs&#39; Verified Code</title>
      <link>https://symbolic.software/blog/2026-02-12-ce-labs-response/</link>
      <pubDate>Thu, 12 Feb 2026 00:00:00 &#43;0000</pubDate>
      <guid>https://symbolic.software/blog/2026-02-12-ce-labs-response/</guid>
      <description>CE Labs said they&#39;d be &#39;very interested&#39; if someone found a bug in their verified code. We found three.</description>
      <content:encoded>




&lt;nav class=&#34;series-nav&#34; aria-label=&#34;CE Labs series (top)&#34;&gt;
  &lt;header class=&#34;series-nav-header&#34;&gt;
    &lt;span class=&#34;series-nav-label&#34;&gt;▶ CE Labs Series&lt;/span&gt;
    &lt;span class=&#34;series-nav-count&#34;&gt;5 posts · 2 papers · 1 talk&lt;/span&gt;
  &lt;/header&gt;
  &lt;ol class=&#34;series-nav-list&#34;&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-02-05-ce-labs/&#34;&gt;On the Promises of &amp;#39;High-Assurance&amp;#39; Cryptography&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.02.05&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li class=&#34;current&#34; aria-current=&#34;page&#34;&gt;
      
      &lt;span class=&#34;title&#34;&gt;We Found Bugs in CE Labs&amp;#39; Verified Code&lt;/span&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.02.12&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-02-17-ce-labs-mldsa/&#34;&gt;Even More Bugs in CE Labs&amp;#39; libcrux: ML-DSA&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.02.17&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-03-07-ce-labs-tls/&#34;&gt;CE Labs&amp;#39; Approach to TLS: A Critical Analysis&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.03.07&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-04-07-cryspen-hax/&#34;&gt;The Verification Facade: Structural Gaps in Cryspen&amp;#39;s Hax Pipeline&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.04.07&lt;/span&gt;
    &lt;/li&gt;
    
  &lt;/ol&gt;
  &lt;div class=&#34;series-nav-footer&#34;&gt;
    &lt;div class=&#34;series-nav-row&#34;&gt;
      &lt;span class=&#34;series-nav-rowlabel&#34;&gt;Papers&lt;/span&gt;
      &lt;a href=&#34;https://eprint.iacr.org/2026/192&#34;&gt;&lt;em&gt;Verification Theatre&lt;/em&gt;&lt;/a&gt;
      &lt;span class=&#34;series-nav-sep&#34;&gt;·&lt;/span&gt;
      &lt;a href=&#34;https://eprint.iacr.org/2026/670&#34;&gt;&lt;em&gt;Verification Facade&lt;/em&gt;&lt;/a&gt;
    &lt;/div&gt;
    &lt;div class=&#34;series-nav-row&#34;&gt;
      &lt;span class=&#34;series-nav-rowlabel&#34;&gt;Talk&lt;/span&gt;
      &lt;a href=&#34;https://www.youtube.com/watch?v=TdOXza1-M_4&#34;&gt;&lt;em&gt;High Assurance Cryptography and the Ethics of Disclosure (OSTIF)&lt;/em&gt;&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/nav&gt;

&lt;p&gt;On February 5, we published &lt;a href=&#34;https://symbolic.software/blog/2026-02-05-ce-labs/&#34;&gt;five new vulnerabilities&lt;/a&gt; in CE Labs&amp;rsquo; libcrux, hpke-rs, and libcrux-psq cryptographic libraries, along with a &lt;a href=&#34;https://eprint.iacr.org/2026/192&#34;&gt;paper&lt;/a&gt; analyzing the structural pattern behind them. CE Labs&amp;rsquo; response was to block our GitHub account, close our pull requests containing working fixes, and accuse us of acting in bad faith.&lt;/p&gt;
&lt;p&gt;One week later, on February 12, CE Labs published a 1,570-word response titled &amp;ldquo;&lt;a href=&#34;https://cryspen.com/post/strengths-and-limitations/&#34;&gt;The strengths and limits of formal verification&lt;/a&gt;.&amp;rdquo; In it, they write:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;So far, no bugs have been found in the verified code, although we would be very interested to know if any of our code generation tools inadvertently introduced a bug, and even more interested if the bug should have been caught by formal verification.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;We are happy to oblige. We forked libcrux and found three bugs in the verified code: two in the ML-KEM mathematical specification itself, and one in a correctness proof that CE Labs&amp;rsquo; documentation claims is complete.&lt;/p&gt;
&lt;h2 id=&#34;what-ce-labs-response-left-out&#34;&gt;What CE Labs&amp;rsquo; Response Left Out&lt;/h2&gt;
&lt;p&gt;Before we get to the bugs, look at what CE Labs chose to address and what they chose to ignore. Their 1,570-word response acknowledged, obliquely, two of six vulnerabilities (one reported not by us, in November 2025):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;V1&lt;/strong&gt; (platform-dependent cryptographic output failure): described as &amp;ldquo;a fallback implementation for a platform-specific intrinsic.&amp;rdquo;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;V5&lt;/strong&gt; (Ed25519 double clamping): described as &amp;ldquo;one in an unverified wrapper around HACL* code.&amp;rdquo;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What they did not mention, in 1,570 words ostensibly about the strengths and limits of their verification methodology:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;V3: Nonce reuse via integer overflow.&lt;/strong&gt; A &lt;code&gt;u32&lt;/code&gt; sequence counter in hpke-rs that silently wraps to zero in release builds, reusing AEAD nonces. For AES-GCM, nonce reuse leaks the GHASH authentication key, enabling universal forgeries. For ChaCha20-Poly1305, it enables direct plaintext recovery. Not one word.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;V6: Denial of service via &lt;code&gt;.unwrap()&lt;/code&gt;.&lt;/strong&gt; A single malformed ciphertext crashes the process in the PSQ protocol&amp;rsquo;s AES-GCM decryption path. Breaks IND-CCA security for 9 of 18 supported PSQ ciphersuites. The correct error handling code was present but commented out. Not one word.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;V2: Missing X25519 all-zero validation.&lt;/strong&gt; An attacker can supply a low-order public key and force the HPKE shared secret to zero, making session keys deterministic and predictable. Forward secrecy completely broken. Not one word.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;V4: ECDSA signature malleability.&lt;/strong&gt; The ECDSA P-256 implementation lacks low-S normalization, a standard requirement for over a decade. Not one word.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The word &amp;ldquo;hpke-rs&amp;rdquo; does not appear anywhere in CE Labs&amp;rsquo; response. This is the library that implements RFC 9180, the one depended upon by Signal&amp;rsquo;s &lt;code&gt;signal-crypto&lt;/code&gt; crate and by OpenMLS. Its two most severe vulnerabilities, nonce reuse and broken forward secrecy, were simply not mentioned.&lt;/p&gt;
&lt;p&gt;CE Labs wrote 1,570 words about &amp;ldquo;the strengths and limits of formal verification&amp;rdquo; without acknowledging four of the six vulnerabilities, including the two most dangerous ones.&lt;/p&gt;
&lt;p&gt;Four days after blocking our account and closing our pull requests, CE Labs&amp;rsquo; CEO manually copied our fixes and merged them under his own name. The fixes that were characterized as &amp;ldquo;not a good-faith attempt to improve the project&amp;rsquo;s security posture&amp;rdquo; were adopted verbatim.&lt;/p&gt;
&lt;h2 id=&#34;bugs-in-the-verified-code&#34;&gt;Bugs in the Verified Code&lt;/h2&gt;
&lt;p&gt;With that context, let us return to CE Labs&amp;rsquo; invitation. They said they would be &amp;ldquo;very interested&amp;rdquo; if someone found a bug that should have been caught by formal verification. We forked libcrux and looked.&lt;/p&gt;
&lt;p&gt;We found three.&lt;/p&gt;
&lt;h3 id=&#34;bug-1-wrong-specification-for-ml-kem-decompression&#34;&gt;Bug 1: Wrong Specification for ML-KEM Decompression&lt;/h3&gt;
&lt;p&gt;The F* specification function &lt;code&gt;decompress_d&lt;/code&gt; in &lt;code&gt;Spec.MLKEM.Math.fst&lt;/code&gt; (line 238) uses the wrong rounding constant. This is not a proof-level issue. This is the mathematical specification itself, the correctness target for the entire ML-KEM decompression pipeline, computing the wrong function.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;let decompress_d (d: dT {d &amp;lt;&amp;gt; 12}) (x: field_element_d d): field_element
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  = let r = (x * v v_FIELD_MODULUS + 1664) / pow2 d in
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    r                            -- ^^^^ should be: pow2 (d - 1)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When rounding integer division by $N$, the correct procedure is to add $N/2$ before dividing. For &lt;code&gt;decompress_d&lt;/code&gt;, the denominator is $2^d$, so the rounding constant should be $2^{d-1}$. The value $1664 = (q-1)/2$ is the correct rounding constant for the &lt;code&gt;compress_d&lt;/code&gt; function directly above it, which divides by $q = 3329$. The author used 1664 for both functions.&lt;/p&gt;
&lt;p&gt;Compare with &lt;code&gt;compress_d&lt;/code&gt; (line 225), where 1664 is correct:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;let compress_d (d: dT {d &amp;lt;&amp;gt; 12}) (x: field_element): field_element_d d
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  = let r = (pow2 d * x + 1664) / v v_FIELD_MODULUS in
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    ...                  -- ^^^^ correct here: dividing by q, so add q/2 = 1664
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The pattern is clear. For &lt;code&gt;compress_d&lt;/code&gt;, $N = q = 3329$, so $N/2 = 1664$. For &lt;code&gt;decompress_d&lt;/code&gt;, $N = 2^d$, so $N/2 = 2^{d-1}$. One constant was copied to both.&lt;/p&gt;
&lt;h4 id=&#34;concrete-counterexamples&#34;&gt;Concrete Counterexamples&lt;/h4&gt;
&lt;p&gt;FIPS 203 defines $\text{Decompress}_d(y) = \text{round}\!\left(\frac{q}{2^d} \cdot y\right)$, implemented as $\left\lfloor\frac{q \cdot y + 2^{d-1}}{2^d}\right\rfloor$.&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;d&lt;/th&gt;
          &lt;th&gt;x&lt;/th&gt;
          &lt;th&gt;Spec (wrong: 1664)&lt;/th&gt;
          &lt;th&gt;Correct ($2^{d-1}$)&lt;/th&gt;
          &lt;th&gt;FIPS 203&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;1&lt;/td&gt;
          &lt;td&gt;0&lt;/td&gt;
          &lt;td&gt;832&lt;/td&gt;
          &lt;td&gt;0&lt;/td&gt;
          &lt;td&gt;0&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;1&lt;/td&gt;
          &lt;td&gt;1&lt;/td&gt;
          &lt;td&gt;2496&lt;/td&gt;
          &lt;td&gt;1665&lt;/td&gt;
          &lt;td&gt;1665&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;4&lt;/td&gt;
          &lt;td&gt;0&lt;/td&gt;
          &lt;td&gt;104&lt;/td&gt;
          &lt;td&gt;0&lt;/td&gt;
          &lt;td&gt;0&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;4&lt;/td&gt;
          &lt;td&gt;1&lt;/td&gt;
          &lt;td&gt;312&lt;/td&gt;
          &lt;td&gt;208&lt;/td&gt;
          &lt;td&gt;208&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;10&lt;/td&gt;
          &lt;td&gt;0&lt;/td&gt;
          &lt;td&gt;1&lt;/td&gt;
          &lt;td&gt;0&lt;/td&gt;
          &lt;td&gt;0&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;10&lt;/td&gt;
          &lt;td&gt;1&lt;/td&gt;
          &lt;td&gt;4&lt;/td&gt;
          &lt;td&gt;3&lt;/td&gt;
          &lt;td&gt;3&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The spec gives &lt;code&gt;decompress_d(d, 0) != 0&lt;/code&gt; for $d &amp;lt; 12$. Decompressing zero should always yield zero. The spec and FIPS 203 disagree on every input for $d \in \{1, 4, 5, 10, 11\}$.&lt;/p&gt;
&lt;h4 id=&#34;the-implementation-is-correct-the-spec-is-wrong&#34;&gt;The Implementation Is Correct; the Spec Is Wrong&lt;/h4&gt;
&lt;p&gt;Libcrux&amp;rsquo;s own Rust implementation (&lt;code&gt;libcrux-ml-kem/src/vector/portable/compress.rs&lt;/code&gt;, line 317) uses the correct formula:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;mut&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;decompressed&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;a&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;elements&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;].&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;as_i32&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;no&#34;&gt;FIELD_MODULUS&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;classify&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;().&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;as_i32&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;();&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;decompressed&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;decompressed&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;+&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;i32&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;no&#34;&gt;COEFFICIENT_BITS&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;decompressed&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;decompressed&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;no&#34;&gt;COEFFICIENT_BITS&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;+&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This computes $(x \cdot q + 2^{d-1}) / 2^d$, the correct rounding. The &lt;a href=&#34;https://github.com/pq-crystals/kyber&#34;&gt;CRYSTALS-Kyber reference implementation&lt;/a&gt; in C uses the same correct formula. The implementation is right. The specification is wrong. They disagree.&lt;/p&gt;
&lt;h4 id=&#34;why-it-goes-undetected&#34;&gt;Why It Goes Undetected&lt;/h4&gt;
&lt;p&gt;The spec function &lt;code&gt;decompress_d&lt;/code&gt; is the correctness target for the entire decompression pipeline. It is called via &lt;code&gt;byte_decode_then_decompress&lt;/code&gt; (line 270), which feeds into &lt;code&gt;decode_then_decompress_message&lt;/code&gt; ($d=1$), &lt;code&gt;decode_then_decompress_u&lt;/code&gt; ($d=d_u$), and &lt;code&gt;decode_then_decompress_v&lt;/code&gt; ($d=d_v$), which together cover every decompression path in ML-KEM encryption and decryption.&lt;/p&gt;
&lt;p&gt;The implementation&amp;rsquo;s &lt;code&gt;ensures&lt;/code&gt; clauses in &lt;code&gt;ind_cpa.rs&lt;/code&gt; reference these spec functions. But &lt;code&gt;Libcrux_ml_kem.Ind_cpa.fst&lt;/code&gt; is in &lt;code&gt;ADMIT_MODULES&lt;/code&gt; (line 4 of the extraction Makefile), so the proof that the implementation matches the spec is admitted and the mismatch is never checked. Meanwhile, &lt;code&gt;verification_status.md&lt;/code&gt; claims ind_cpa is yes/yes/yes.&lt;/p&gt;
&lt;h4 id=&#34;provenance-and-impact&#34;&gt;Provenance and Impact&lt;/h4&gt;
&lt;p&gt;Introduced in commit &lt;a href=&#34;https://github.com/celabshq/libcrux/commit/c1935b9cf8&#34;&gt;&lt;code&gt;c1935b9cf8&lt;/code&gt;&lt;/a&gt; on August 12, 2024, by Karthikeyan Bhargavan (CE Labs&amp;rsquo; Chief Research Scientist and corresponding author of their response post), with the commit message &amp;ldquo;spec&amp;rdquo;. This was the very first version of the ML-KEM specification file. The bug has been present since inception.&lt;/p&gt;
&lt;p&gt;This is a specification-level bug. The formal specification, the mathematical reference that correctness proofs are verified against, computes a different function than FIPS 203. Even if all proofs were completed and fully verified, they would prove the implementation correct against the wrong mathematical definition. The overall claim &amp;ldquo;ML-KEM implementation is correct with respect to FIPS 203&amp;rdquo; is undermined at the foundation.&lt;/p&gt;
&lt;p&gt;The fix is to change &lt;code&gt;1664&lt;/code&gt; to &lt;code&gt;pow2 (d - 1)&lt;/code&gt; on line 238:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-diff&#34; data-lang=&#34;diff&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt; let decompress_d (d: dT {d &amp;lt;&amp;gt; 12}) (x: field_element_d d): field_element
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gd&#34;&gt;-  = let r = (x * v v_FIELD_MODULUS + 1664) / pow2 d in
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gd&#34;&gt;&lt;/span&gt;&lt;span class=&#34;gi&#34;&gt;+  = let r = (x * v v_FIELD_MODULUS + pow2 (d - 1)) / pow2 d in
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gi&#34;&gt;&lt;/span&gt;     r
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;bug-2-missing-inverse-ntt-in-ml-kem-encryption-specification&#34;&gt;Bug 2: Missing Inverse NTT in ML-KEM Encryption Specification&lt;/h3&gt;
&lt;p&gt;The F* specification function &lt;code&gt;ind_cpa_encrypt_unpacked&lt;/code&gt; in &lt;code&gt;Spec.MLKEM.fst&lt;/code&gt; (line 277) computes the ciphertext component &lt;code&gt;v&lt;/code&gt; by adding an NTT-domain polynomial directly to coefficient-domain polynomials, without first applying the inverse NTT. This is a domain mismatch, the mathematical equivalent of adding a frequency-domain signal to a time-domain signal. The Rust implementation correctly applies &lt;code&gt;invert_ntt_montgomery&lt;/code&gt; before the addition.&lt;/p&gt;
&lt;p&gt;FIPS 203, Algorithm 14 (K-PKE.Encrypt), specifies two structurally identical computations:&lt;/p&gt;
$$\text{Step 19:} \quad \mathbf{u} \leftarrow \text{NTT}^{-1}(\hat{\mathbf{A}}^T \circ \hat{\mathbf{y}}) + \mathbf{e}_1$$$$\text{Step 21:} \quad \mathbf{v} \leftarrow \text{NTT}^{-1}(\hat{\mathbf{t}}^T \circ \hat{\mathbf{y}}) + \mathbf{e}_2 + \mu$$&lt;p&gt;Both require $\text{NTT}^{-1}$ on the NTT-domain product before adding coefficient-domain error terms. The specification implements step 19 correctly but omits $\text{NTT}^{-1}$ from step 21:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;-- Line 275 (u, correct):
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;let u = vector_add (vector_inv_ntt (matrix_vector_mul_ntt ...)) error_1
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                     ^^^^^^^^^^^^^^
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                     NTT⁻¹ applied
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;-- Line 277 (v, BUG):
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;let v = poly_add (poly_add (vector_dot_product_ntt t_as_ntt r_as_ntt) error_2) mu
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                             Returns NTT-domain. Missing: poly_inv_ntt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The operand domains confirm the mismatch. &lt;code&gt;vector_dot_product_ntt&lt;/code&gt; computes $\hat{\mathbf{t}}^T \circ \hat{\mathbf{y}}$ via pointwise NTT-domain multiplication and summation (&lt;code&gt;vector_sum (vector_mul_ntt a b)&lt;/code&gt;, line 185), returning an NTT-domain polynomial. Meanwhile, &lt;code&gt;error_2&lt;/code&gt; (from &lt;code&gt;sample_poly_cbd2&lt;/code&gt;, line 165, which is CBD sampling with no NTT) and &lt;code&gt;mu&lt;/code&gt; (from &lt;code&gt;decode_then_decompress_message&lt;/code&gt;, line 212, byte decode and decompress) are both in the coefficient domain. The spec adds them directly.&lt;/p&gt;
&lt;p&gt;The decryption specification, thirty lines below, performs the same operation correctly:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;-- Line 307 (decryption, correct):
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;let w = poly_sub v (poly_inv_ntt (vector_dot_product_ntt secret_as_ntt (vector_ntt u)))
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                     ^^^^^^^^^^^^^^
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                     NTT⁻¹ applied
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id=&#34;the-implementation-is-correct-the-spec-is-wrong-1&#34;&gt;The Implementation Is Correct; the Spec Is Wrong&lt;/h4&gt;
&lt;p&gt;The Rust implementation in &lt;code&gt;libcrux-ml-kem/src/matrix.rs&lt;/code&gt; (line 77) has a correct English comment:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;sd&#34;&gt;/// Compute InverseNTT(tᵀ ◦ r̂) + e₂ + message
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And the code (lines 99&amp;ndash;105) correctly applies the inverse NTT before the addition:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;k&#34;&gt;in&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;..&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;K&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;let&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;product&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;t_as_ntt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;].&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ntt_multiply&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;r_as_ntt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]);&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;result&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;add_to_ring_element&lt;/span&gt;::&lt;span class=&#34;o&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;K&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;product&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;invert_ntt_montgomery&lt;/span&gt;::&lt;span class=&#34;o&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;K&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Vector&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;mut&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;result&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// NTT⁻¹ applied correctly
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;result&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;error_2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;add_message_error_reduce&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;message&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;result&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;But its F* postcondition (line 88) matches the buggy spec, not the correct implementation:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;res_spec == Spec.MLKEM.(poly_add (poly_add (vector_dot_product_ntt ...) e2_spec) m_spec)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;--                                          ^^^ Missing poly_inv_ntt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The programmer knew what the code should do. The English is right, the implementation is right, but the formal annotation is wrong.&lt;/p&gt;
&lt;h4 id=&#34;why-it-goes-undetected-1&#34;&gt;Why It Goes Undetected&lt;/h4&gt;
&lt;p&gt;The function &lt;code&gt;compute_ring_element_v&lt;/code&gt; is marked &lt;code&gt;verification_status(lax)&lt;/code&gt; (line 79 of &lt;code&gt;matrix.rs&lt;/code&gt;), so its postcondition is only lax-checked, not proven. And &lt;code&gt;Libcrux_ml_kem.Ind_cpa.fst&lt;/code&gt; is in &lt;code&gt;ADMIT_MODULES&lt;/code&gt;, so the linking proof between the implementation and the specification is fully admitted.&lt;/p&gt;
&lt;h4 id=&#34;provenance-and-impact-1&#34;&gt;Provenance and Impact&lt;/h4&gt;
&lt;p&gt;This bug was introduced in the same commit as Bug 1: &lt;a href=&#34;https://github.com/celabshq/libcrux/commit/c1935b9cf8&#34;&gt;&lt;code&gt;c1935b9cf8&lt;/code&gt;&lt;/a&gt; on August 12, 2024, the initial creation of the ML-KEM specification file by Karthikeyan Bhargavan.&lt;/p&gt;
&lt;p&gt;This is a specification-level bug in ML-KEM encryption. The NTT representation of a polynomial is a completely different sequence of 256 field elements than its coefficient representation. Adding them directly produces a result that is neither the correct NTT-domain nor coefficient-domain value, so the spec computes a ciphertext &lt;code&gt;v&lt;/code&gt; that differs from what FIPS 203 prescribes for virtually all inputs. Both the &lt;code&gt;u&lt;/code&gt; computation (line 275) and the decryption (line 307) correctly apply the inverse NTT, making this an inconsistency within the specification itself.&lt;/p&gt;
&lt;p&gt;The fix is to add &lt;code&gt;poly_inv_ntt&lt;/code&gt; on line 277:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-diff&#34; data-lang=&#34;diff&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gd&#34;&gt;-    let v = poly_add (poly_add (vector_dot_product_ntt t_as_ntt r_as_ntt) error_2) mu in
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gd&#34;&gt;&lt;/span&gt;&lt;span class=&#34;gi&#34;&gt;+    let v = poly_add (poly_add (poly_inv_ntt (vector_dot_product_ntt t_as_ntt r_as_ntt)) error_2) mu in
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;bug-3-false-proof-in-ml-kem-serialization&#34;&gt;Bug 3: False Proof in ML-KEM Serialization&lt;/h3&gt;
&lt;p&gt;The F* lemma &lt;code&gt;deserialize_12_bit_vec_lemma_bounded&lt;/code&gt; in &lt;code&gt;libcrux-ml-kem/src/vector/portable/serialize.rs&lt;/code&gt; (line 691) claims that all elements produced by &lt;code&gt;deserialize_12&lt;/code&gt; are bounded by &lt;strong&gt;1&lt;/strong&gt;, meaning they fit in a single bit. This is mathematically false. The &lt;code&gt;deserialize_12&lt;/code&gt; function extracts 12-bit values from a byte array; its outputs range from 0 to 4095. The bound should be &lt;strong&gt;12&lt;/strong&gt;.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;let deserialize_12_bit_vec_lemma_bounded (v: t_Array u8 (sz 24))
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  : squash (
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    let result = ${deserialize_12} v in
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    (forall (i: nat {i &amp;lt; 16}).
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      Rust_primitives.bounded (Seq.index result.f_elements i) 1)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                                                               ^
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  ) =                                      should be 12, not 1
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt; _ by (Tactics.GetBit.prove_bit_vector_equality&amp;#39; ())
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This is not a subtle semantic question. The lemma asserts that a function producing 12-bit values only ever produces values that fit in 1 bit. It is a false mathematical statement embedded in a proof that CE Labs&amp;rsquo; own documentation claims is complete.&lt;/p&gt;
&lt;p&gt;The interface-level lemma &lt;code&gt;deserialize_12_lemma&lt;/code&gt; (line 706) correctly states the bound as 12. But it calls &lt;code&gt;deserialize_12_bit_vec_lemma_bounded&lt;/code&gt; to discharge its proof obligation, so the entire correctness proof for &lt;code&gt;deserialize_12&lt;/code&gt; rests on a false statement:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;let deserialize_12_lemma inputs =
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  deserialize_12_bit_vec_lemma inputs;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  deserialize_12_bit_vec_lemma_bounded inputs;   &amp;lt;-- false lemma
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  BitVecEq.bit_vec_equal_intro ...
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h4 id=&#34;a-copy-paste-bug&#34;&gt;A Copy-Paste Bug&lt;/h4&gt;
&lt;p&gt;Every other deserializer in the same file has the correct bound. The &lt;code&gt;1&lt;/code&gt; was copied from &lt;code&gt;deserialize_1&lt;/code&gt; and never updated:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Lemma&lt;/th&gt;
          &lt;th&gt;Bound&lt;/th&gt;
          &lt;th&gt;Correct?&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;deserialize_1_bit_vec_lemma&lt;/code&gt; (line 108)&lt;/td&gt;
          &lt;td&gt;1&lt;/td&gt;
          &lt;td&gt;Yes&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;deserialize_4_bit_vec_lemma_bounded&lt;/code&gt; (line 274)&lt;/td&gt;
          &lt;td&gt;4&lt;/td&gt;
          &lt;td&gt;Yes&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;deserialize_10_bit_vec_lemma_bounded&lt;/code&gt; (line 478)&lt;/td&gt;
          &lt;td&gt;10&lt;/td&gt;
          &lt;td&gt;Yes&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;deserialize_12_bit_vec_lemma_bounded&lt;/code&gt; (line 691)&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;1&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;No&lt;/strong&gt;&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The bug was introduced in commit &lt;a href=&#34;https://github.com/celabshq/libcrux/commit/5f6e3c2083&#34;&gt;&lt;code&gt;5f6e3c2083&lt;/code&gt;&lt;/a&gt; on March 25, 2025, by a CE Labs developer, with the commit message: &amp;ldquo;fix(ml-kem): deserialize_10/12: remove &lt;code&gt;admit&lt;/code&gt;.&amp;rdquo; The commit replaced &lt;code&gt;admit()&lt;/code&gt; calls with dedicated bounded lemmas. The &lt;code&gt;deserialize_10&lt;/code&gt; version was added correctly with bound 10. The &lt;code&gt;deserialize_12&lt;/code&gt; version was copy-pasted from &lt;code&gt;deserialize_1&lt;/code&gt; with the bound left as &lt;code&gt;1&lt;/code&gt;. The irony of a commit titled &amp;ldquo;remove admit&amp;rdquo; introducing a false proof should not be lost.&lt;/p&gt;
&lt;h4 id=&#34;three-layers-of-silent-failure&#34;&gt;Three Layers of Silent Failure&lt;/h4&gt;
&lt;p&gt;The false lemma goes undetected because three independent mechanisms conspire to prevent it from ever being checked:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. &amp;ldquo;Slow modules&amp;rdquo; are admitted by default.&lt;/strong&gt; In &lt;code&gt;fstar-helpers/Makefile.base&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-makefile&#34; data-lang=&#34;makefile&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nv&#34;&gt;VERIFY_SLOW_MODULES&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;?=&lt;/span&gt; no
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;ifeq&lt;/span&gt; &lt;span class=&#34;err&#34;&gt;(${VERIFY_SLOW_MODULES},no)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nv&#34;&gt;ADMIT_MODULES&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+=&lt;/span&gt; &lt;span class=&#34;si&#34;&gt;${&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;SLOW_MODULES&lt;/span&gt;&lt;span class=&#34;si&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The portable serialize module is listed in &lt;code&gt;SLOW_MODULES&lt;/code&gt;. When &lt;code&gt;VERIFY_SLOW_MODULES&lt;/code&gt; is &lt;code&gt;no&lt;/code&gt;, which is the default, the module is added to &lt;code&gt;ADMIT_MODULES&lt;/code&gt; and verified with &lt;code&gt;--admit_smt_queries true&lt;/code&gt;, which accepts all SMT proofs without checking them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. The proof tactic has a lax-mode escape hatch.&lt;/strong&gt; In &lt;code&gt;fstar-helpers/fstar-bitvec/Tactics.GetBit.fst&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;let prove_bit_vector_equality&amp;#39; (): Tac unit =
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  if lax_on ()
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  then iterAll tadmit
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  else prove_bit_vector_equality&amp;#39;&amp;#39; ()
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;When the module is lax-checked, the tactic calls &lt;code&gt;tadmit&lt;/code&gt; on all goals unconditionally, bypassing the actual bit-vector equality prover entirely. The false lemma is never subjected to any mathematical scrutiny.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Full verification is never triggered.&lt;/strong&gt; A search of the entire libcrux repository finds that &lt;code&gt;VERIFY_SLOW_MODULES=yes&lt;/code&gt; is never set in any CI configuration, Makefile target, or script. The proof is never actually checked in any automated pipeline.&lt;/p&gt;
&lt;h4 id=&#34;the-documentation-says-yes&#34;&gt;The Documentation Says &amp;ldquo;Yes&amp;rdquo;&lt;/h4&gt;
&lt;p&gt;Meanwhile, CE Labs&amp;rsquo; own &lt;code&gt;verification_status.md&lt;/code&gt; claims the portable serialize module is fully verified:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Category&lt;/th&gt;
          &lt;th&gt;File&lt;/th&gt;
          &lt;th&gt;Lax Checking&lt;/th&gt;
          &lt;th&gt;Runtime Safety&lt;/th&gt;
          &lt;th&gt;Correctness&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;em&gt;Portable&lt;/em&gt;&lt;/td&gt;
          &lt;td&gt;serialize&lt;/td&gt;
          &lt;td&gt;yes&lt;/td&gt;
          &lt;td&gt;yes&lt;/td&gt;
          &lt;td&gt;yes&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Three &amp;ldquo;yes&amp;rdquo; entries. Full verification claimed for lax checking, runtime safety, and correctness.&lt;/p&gt;
&lt;p&gt;But the file&amp;rsquo;s own header comment says:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-fallback&#34; data-lang=&#34;fallback&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;//! Verification status: Lax
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;And the build system admits the module by default. So we have three sources that contradict each other:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Source&lt;/th&gt;
          &lt;th&gt;Claimed status&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;verification_status.md&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;Fully verified (yes / yes / yes)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;File header comment&lt;/td&gt;
          &lt;td&gt;Lax&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Actual build configuration&lt;/td&gt;
          &lt;td&gt;Admitted by default&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The code that CE Labs documents as formally verified for correctness contains a false mathematical statement that is silently accepted because the proof is never actually checked.&lt;/p&gt;
&lt;p&gt;The fix is to change &lt;code&gt;1&lt;/code&gt; to &lt;code&gt;12&lt;/code&gt; on line 691:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-diff&#34; data-lang=&#34;diff&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt; let deserialize_12_bit_vec_lemma_bounded (v: t_Array u8 (sz 24))
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;   : squash (
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;     let result = ${deserialize_12} v in
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gd&#34;&gt;-    (forall (i: nat {i &amp;lt; 16}). Rust_primitives.bounded (Seq.index result.f_elements i) 1)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gd&#34;&gt;&lt;/span&gt;&lt;span class=&#34;gi&#34;&gt;+    (forall (i: nat {i &amp;lt; 16}). Rust_primitives.bounded (Seq.index result.f_elements i) 12)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;gi&#34;&gt;&lt;/span&gt;   ) =
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  _ by (Tactics.GetBit.prove_bit_vector_equality&amp;#39; ())
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;what-this-means&#34;&gt;What This Means&lt;/h2&gt;
&lt;p&gt;CE Labs invited scrutiny of their verified code. We accepted the invitation, and it took one fork and a careful read to find:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A wrong mathematical specification&lt;/strong&gt; for ML-KEM decompression, the correctness target the entire verification effort is built on top of. It disagrees with FIPS 203 on every input and has been wrong since the spec file was first created, authored by CE Labs&amp;rsquo; Chief Research Scientist himself.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A missing inverse NTT&lt;/strong&gt; in the ML-KEM encryption specification, which adds an NTT-domain polynomial to coefficient-domain polynomials. This is mathematically nonsensical, and it was introduced in the same commit by the same author.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;A false proof annotation&lt;/strong&gt; in ML-KEM&amp;rsquo;s portable serialization that claims 12-bit values fit in 1 bit, silently accepted by a build system that defaults to not checking proofs.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;All three bugs survive for the same structural reason: the modules that would expose them are admitted by default. The proofs are never checked. The documentation says &amp;ldquo;yes&amp;rdquo; when the answer is &amp;ldquo;no.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;CE Labs&amp;rsquo; response to our original findings was to block us, close our fixes, and then quietly adopt them. Their public response acknowledged two of six vulnerabilities and ignored the four most severe. Their verification status documentation claims that code is verified when it is not. Their proof infrastructure silently admits false mathematical statements rather than checking them. And their mathematical specification, the foundation on which every correctness claim rests, computes a different function than the standard it claims to formalize, in two independent places.&lt;/p&gt;
&lt;p&gt;The formal verification methodology that CE Labs markets as providing &amp;ldquo;the highest level of assurance&amp;rdquo; contains a build system that defaults to not checking proofs, a tactic library that silently accepts all claims in lax mode, specifications that disagree with the standards they model, and documentation that says &amp;ldquo;yes&amp;rdquo; when the answer is &amp;ldquo;no.&amp;rdquo; The result is a formally verified cryptographic library where wrong specifications and false proofs survive indefinitely because nobody, and no automated system, ever actually verifies them.&lt;/p&gt;
&lt;p&gt;We continue to believe that formal verification is a valuable technique when applied honestly and communicated precisely. What we object to is the gap between what is claimed and what is delivered. That gap is not a limitation of formal methods. It is a choice.&lt;/p&gt;





&lt;nav class=&#34;series-nav&#34; aria-label=&#34;CE Labs series (bottom)&#34;&gt;
  &lt;header class=&#34;series-nav-header&#34;&gt;
    &lt;span class=&#34;series-nav-label&#34;&gt;▶ CE Labs Series&lt;/span&gt;
    &lt;span class=&#34;series-nav-count&#34;&gt;5 posts · 2 papers · 1 talk&lt;/span&gt;
  &lt;/header&gt;
  &lt;ol class=&#34;series-nav-list&#34;&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-02-05-ce-labs/&#34;&gt;On the Promises of &amp;#39;High-Assurance&amp;#39; Cryptography&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.02.05&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li class=&#34;current&#34; aria-current=&#34;page&#34;&gt;
      
      &lt;span class=&#34;title&#34;&gt;We Found Bugs in CE Labs&amp;#39; Verified Code&lt;/span&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.02.12&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-02-17-ce-labs-mldsa/&#34;&gt;Even More Bugs in CE Labs&amp;#39; libcrux: ML-DSA&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.02.17&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-03-07-ce-labs-tls/&#34;&gt;CE Labs&amp;#39; Approach to TLS: A Critical Analysis&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.03.07&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-04-07-cryspen-hax/&#34;&gt;The Verification Facade: Structural Gaps in Cryspen&amp;#39;s Hax Pipeline&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.04.07&lt;/span&gt;
    &lt;/li&gt;
    
  &lt;/ol&gt;
  &lt;div class=&#34;series-nav-footer&#34;&gt;
    &lt;div class=&#34;series-nav-row&#34;&gt;
      &lt;span class=&#34;series-nav-rowlabel&#34;&gt;Papers&lt;/span&gt;
      &lt;a href=&#34;https://eprint.iacr.org/2026/192&#34;&gt;&lt;em&gt;Verification Theatre&lt;/em&gt;&lt;/a&gt;
      &lt;span class=&#34;series-nav-sep&#34;&gt;·&lt;/span&gt;
      &lt;a href=&#34;https://eprint.iacr.org/2026/670&#34;&gt;&lt;em&gt;Verification Facade&lt;/em&gt;&lt;/a&gt;
    &lt;/div&gt;
    &lt;div class=&#34;series-nav-row&#34;&gt;
      &lt;span class=&#34;series-nav-rowlabel&#34;&gt;Talk&lt;/span&gt;
      &lt;a href=&#34;https://www.youtube.com/watch?v=TdOXza1-M_4&#34;&gt;&lt;em&gt;High Assurance Cryptography and the Ethics of Disclosure (OSTIF)&lt;/em&gt;&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/nav&gt;

</content:encoded>
      <category>Research</category>
      <category>Formal Verification</category>
      
    </item>
    
    <item>
      <title>Verifpal Verifies Signal Across Three Messages</title>
      <link>https://symbolic.software/blog/2026-02-10-verifpal-signal/</link>
      <pubDate>Tue, 10 Feb 2026 00:00:00 &#43;0000</pubDate>
      <guid>https://symbolic.software/blog/2026-02-10-verifpal-signal/</guid>
      <description>Verifpal 0.31.2 ships a major overhaul to active attacker analysis. Verifpal can now fully verify a model of Signal&#39;s three-message protocol, a result other tools reached years ago and that is new only for Verifpal.</description>
      <content:encoded>&lt;p&gt;Today we&amp;rsquo;re releasing Verifpal 0.31.2, the biggest upgrade to Verifpal&amp;rsquo;s analysis engine we&amp;rsquo;ve ever shipped. The headline result: &lt;strong&gt;Verifpal can now fully verify a model of Signal&amp;rsquo;s X3DH key agreement and Double Ratchet protocol across three messages&lt;/strong&gt;, under an active attacker, and it terminates in minutes. No earlier version of Verifpal could complete this analysis.&lt;/p&gt;
&lt;p&gt;The verified models are published on VerifHub:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://verifhub.verifpal.com/7b33a06c46b93d14cefc36d417386101&#34;&gt;&lt;strong&gt;signal.vp&lt;/strong&gt; — Guarded long-term keys&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://verifhub.verifpal.com/35335d609b1de8cf8138c1e534356697&#34;&gt;&lt;strong&gt;signal_unguarded.vp&lt;/strong&gt; — Unguarded long-term keys&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This isn&amp;rsquo;t a novel research result: I myself &lt;a href=&#34;https://bblanche.gitlabpages.inria.fr/publications/KobeissiBhargavanBlanchetEuroSP17.pdf&#34;&gt;analyzed a similar Signal model using ProVerif back in 2016&lt;/a&gt;, a full decade ago. Tools like ProVerif and Tamarin have been able to handle models of this complexity for years.&lt;/p&gt;
&lt;p&gt;But Verifpal was never built to compete with ProVerif or Tamarin. It was built as a learning tool, accessible to undergraduates and first-year graduate students who are encountering protocol verification for the first time. What matters is that Verifpal can now handle models complex enough to be pedagogically interesting, and Signal is the canonical example.&lt;/p&gt;
&lt;h2 id=&#34;what-the-models-show&#34;&gt;What the Models Show&lt;/h2&gt;
&lt;p&gt;The Signal model captures the full three-message flow: X3DH key agreement followed by two rounds of Double Ratchet.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Message 1 (Bob → Alice): Pre-key bundle.&lt;/strong&gt; Bob publishes his long-term identity key, a signed semi-static pre-key, and a one-time pre-key. Alice verifies the signature and computes a master secret from the four Diffie-Hellman computations at the core of X3DH.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Message 2 (Alice → Bob): Initial ciphertext.&lt;/strong&gt; Alice derives chain and message keys via HKDF, then encrypts her first message using AEAD with associated data binding both parties&amp;rsquo; identity keys and her current ephemeral key.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Message 3 (Bob → Alice) and Message 4 (Alice → Bob): Double Ratchet.&lt;/strong&gt; Each subsequent message introduces a new ephemeral key, performs a DH ratchet step, and derives fresh encryption keys through the HKDF-MAC chain.&lt;/p&gt;
&lt;p&gt;After all messages are exchanged, the model enters a post-compromise phase where both Alice&amp;rsquo;s and Bob&amp;rsquo;s long-term private keys are leaked, testing forward secrecy.&lt;/p&gt;
&lt;h3 id=&#34;guarded-model-results&#34;&gt;Guarded Model Results&lt;/h3&gt;
&lt;p&gt;In the &lt;a href=&#34;https://verifhub.verifpal.com/7b33a06c46b93d14cefc36d417386101&#34;&gt;guarded model&lt;/a&gt;, long-term identity keys are distributed over an authenticated channel (denoted by &lt;code&gt;[brackets]&lt;/code&gt; in Verifpal syntax, meaning the active attacker cannot substitute them):&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Query&lt;/th&gt;
          &lt;th&gt;Result&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;confidentiality? m1&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;Pass&lt;/strong&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;authentication? Alice -&amp;gt; Bob: e1&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;Fail&lt;/strong&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;confidentiality? m2&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;Pass&lt;/strong&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;authentication? Bob -&amp;gt; Alice: e2&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;Pass&lt;/strong&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;confidentiality? m3&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;Pass&lt;/strong&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;authentication? Alice -&amp;gt; Bob: e3&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;Pass&lt;/strong&gt;&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Five of six queries pass. The single failure, authentication of Alice&amp;rsquo;s first message to Bob, is a well-known property of the X3DH handshake. Because X3DH is designed for asynchronous messaging (Bob may be offline when Alice initiates), Alice&amp;rsquo;s first message cannot be fully authenticated until Bob responds and the ratchet completes a full round-trip. This is documented in the Signal specification and is not a vulnerability; it&amp;rsquo;s an inherent tradeoff of the asynchronous design.&lt;/p&gt;
&lt;p&gt;All three confidentiality queries pass even after long-term key compromise, confirming that the protocol provides forward secrecy.&lt;/p&gt;
&lt;h3 id=&#34;unguarded-model-results&#34;&gt;Unguarded Model Results&lt;/h3&gt;
&lt;p&gt;In the &lt;a href=&#34;https://verifhub.verifpal.com/35335d609b1de8cf8138c1e534356697&#34;&gt;unguarded model&lt;/a&gt;, long-term identity keys are sent over an unauthenticated channel, allowing the active attacker to substitute them:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Query&lt;/th&gt;
          &lt;th&gt;Result&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;confidentiality? m1&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;Fail&lt;/strong&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;authentication? Alice -&amp;gt; Bob: e1&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;Fail&lt;/strong&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;confidentiality? m2&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;Fail&lt;/strong&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;authentication? Bob -&amp;gt; Alice: e2&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;Fail&lt;/strong&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;confidentiality? m3&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;Fail&lt;/strong&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;authentication? Alice -&amp;gt; Bob: e3&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;Fail&lt;/strong&gt;&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;All six queries fail. When an active attacker can man-in-the-middle both parties&amp;rsquo; long-term identity keys, every security property collapses, confidentiality and authentication alike, across all three messages. This is the expected and correct result: without an authenticated channel for identity key distribution, no amount of cryptographic ratcheting can save you.&lt;/p&gt;
&lt;h2 id=&#34;why-verifpal-couldnt-do-this-before&#34;&gt;Why Verifpal Couldn&amp;rsquo;t Do This Before&lt;/h2&gt;
&lt;p&gt;Verifpal&amp;rsquo;s active attacker analysis works by systematically mutating protocol values to candidate replacements drawn from the attacker&amp;rsquo;s knowledge. For each mutation combination, the engine checks whether the attacker can derive secrets, forge messages, or break authentication. The problem is combinatorial: as models grow in complexity, the number of mutation combinations explodes.&lt;/p&gt;
&lt;p&gt;Two sources of explosion were responsible for non-termination on the Signal model:&lt;/p&gt;
&lt;h3 id=&#34;mutation-map-cartesian-product-explosion&#34;&gt;Mutation Map Cartesian Product Explosion&lt;/h3&gt;
&lt;p&gt;The mutation map associates each mutable constant with a list of candidate replacement values. Previously, the verifier enumerated the &lt;strong&gt;full cartesian product&lt;/strong&gt; of all mutation lists simultaneously. For a model where four constants have mutation lists of sizes [2, 7, 7, 527], the full product is 51,646 combinations, each spawning a goroutine. At higher analysis stages, where injection expands these lists further, the product grows into the hundreds of thousands.&lt;/p&gt;
&lt;h3 id=&#34;injection-cartesian-product-explosion&#34;&gt;Injection Cartesian Product Explosion&lt;/h3&gt;
&lt;p&gt;The injection engine constructs candidate primitive values by combining known values into each argument slot. For a three-argument primitive like &lt;code&gt;AEAD_ENC&lt;/code&gt; where the attacker knows 20+ values per slot, the cartesian product can exceed 8,000 injected values, per primitive, per stage, per principal.&lt;/p&gt;
&lt;p&gt;Together, these two explosions caused the verifier to spawn millions of goroutines on the Signal model, leading to memory exhaustion, goroutine stack overflow, or effective non-termination.&lt;/p&gt;
&lt;h2 id=&#34;the-fixes&#34;&gt;The Fixes&lt;/h2&gt;
&lt;h3 id=&#34;weight-ordered-mutation-scanning&#34;&gt;Weight-Ordered Mutation Scanning&lt;/h3&gt;
&lt;p&gt;The core architectural change replaces brute-force cartesian product enumeration with a &lt;strong&gt;weight-ordered scanning strategy&lt;/strong&gt; that explores mutations in layers of increasing complexity.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Injection cap:&lt;/strong&gt; a hard limit of 500 injected values per primitive (&lt;code&gt;maxInjectionsPerPrimitive&lt;/code&gt;). The injection loop now pre-computes the total product size with overflow-safe arithmetic and breaks early once the cap is reached. This prevents memory exhaustion from a single &lt;code&gt;inject()&lt;/code&gt; call while preserving enough injection diversity for attack discovery.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Mutation map subsetting:&lt;/strong&gt; two new functions, &lt;code&gt;mutationMapSubset&lt;/code&gt; and &lt;code&gt;mutationMapSubsetCapped&lt;/code&gt;, let the scanner extract and budget-constrain subsets of the full mutation map. When a subset&amp;rsquo;s product exceeds the cap, each dimension is truncated to the &lt;em&gt;n&lt;/em&gt;th root of the limit (computed via binary search), distributing the budget evenly across dimensions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Weight-ordered scanning:&lt;/strong&gt; the new &lt;code&gt;verifyActiveScanWeighted&lt;/code&gt; function replaces direct cartesian product enumeration.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Weight 1:&lt;/strong&gt; Each mutable variable is tested alone, capped to 50 mutations per variable. This catches attacks that require substituting only a single value.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Weight 2:&lt;/strong&gt; Each pair of variables is tested simultaneously. Pairs whose product exceeds 20,000 are skipped entirely (not truncated), preserving the combinatorial structure of pairs that fit.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Weight 3:&lt;/strong&gt; Each triple of variables, same skip logic.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Full product:&lt;/strong&gt; Only attempted if the total product across all variables is within 50,000.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A per-principal-per-stage scan budget of 20,000 (tracked via atomic counter) prevents any single stage from consuming unbounded work. At most 50 subsets are scanned per weight level, preventing combinatorial blowup in &lt;code&gt;C(n, k)&lt;/code&gt; when the number of mutable variables is large.&lt;/p&gt;
&lt;p&gt;The maximum stage limit was also reduced from 64 to 8, ensuring the verifier declares exhaustion sooner when higher stages stop discovering new knowledge. That matters for multi-phase models, where phase 0 must exhaust before phase 1 analysis can begin.&lt;/p&gt;
&lt;h3 id=&#34;lock-free-concurrency-and-hash-based-lookups&#34;&gt;Lock-Free Concurrency and Hash-Based Lookups&lt;/h3&gt;
&lt;p&gt;The second fix targeted performance bottlenecks in the multithreaded analysis engine.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Lock-free attacker state snapshots.&lt;/strong&gt; The attacker state, the central data structure tracking everything the attacker knows, was previously guarded by a read-write mutex that became a bottleneck under high concurrency. The new implementation uses &lt;code&gt;atomic.Pointer[AttackerState]&lt;/code&gt; to publish immutable snapshots on every write. Read-side operations (&lt;code&gt;attackerStateGetRead&lt;/code&gt;, &lt;code&gt;attackerStateGetExhausted&lt;/code&gt;, &lt;code&gt;attackerStateGetKnownCount&lt;/code&gt;) now try the snapshot first, avoiding the read lock entirely. Write-side operations use optimistic double-checked locking: check under read lock, then acquire write lock only if necessary.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Hash-based O(1) value lookups.&lt;/strong&gt; The &lt;code&gt;valueEquivalentValueInValues&lt;/code&gt; function, which checks whether a value already exists in the attacker&amp;rsquo;s known set, was a linear scan called in every hot path. It&amp;rsquo;s now replaced by &lt;code&gt;valueEquivalentValueInValuesMap&lt;/code&gt;, backed by an FNV-like hash map (&lt;code&gt;map[uint64][]int&lt;/code&gt;). Dedicated hashers handle primitives, equations, and constants, with special handling for the commutativity of three-element DH equations. The attacker state&amp;rsquo;s &lt;code&gt;KnownMap&lt;/code&gt; is maintained alongside the known values list and included in atomic snapshots.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Principal state clone optimization.&lt;/strong&gt; &lt;code&gt;constructPrincipalStateClone&lt;/code&gt; previously deep-copied every slice in the principal state. Since several slices (Constants, Guard, Known, Wire, KnownBy, DeclaredAt, MutatableTo, Phase) are immutable after initialization, they&amp;rsquo;re now shared by reference.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Parallel stage execution.&lt;/strong&gt; Analysis stages now run two at a time when possible, improving throughput on multi-core machines.&lt;/p&gt;
&lt;h3 id=&#34;aead-check-semantics-and-the-final-bug&#34;&gt;AEAD Check Semantics and the Final Bug&lt;/h3&gt;
&lt;p&gt;The last release fixed a subtle bug in how checked AEAD primitives were interpreted. In Verifpal, &lt;code&gt;AEAD_DEC(k, c, ad)?&lt;/code&gt; (with the &lt;code&gt;?&lt;/code&gt; suffix) means the decryption is &lt;em&gt;checked&lt;/em&gt;: the principal verifies that decryption succeeded and aborts if it doesn&amp;rsquo;t. Without the &lt;code&gt;?&lt;/code&gt;, the principal blindly accepts whatever comes out.&lt;/p&gt;
&lt;p&gt;The authentication and freshness query handlers were not correctly distinguishing between checked and unchecked AEAD decryptions, causing incorrect pass/fail indices. The fix adds an early-continue path for non-checked primitives in &lt;code&gt;queryAuthenticationGetPassIndices&lt;/code&gt; and &lt;code&gt;queryFreshness&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The Signal model itself was also corrected: three &lt;code&gt;AEAD_DEC&lt;/code&gt; calls that should have been checked (with &lt;code&gt;?&lt;/code&gt;) were missing the suffix, masking authentication failures that the analysis should have detected.&lt;/p&gt;
&lt;p&gt;This was the fix that made the Signal model terminate with correct results.&lt;/p&gt;
&lt;h2 id=&#34;what-this-means-for-verifpal&#34;&gt;What This Means for Verifpal&lt;/h2&gt;
&lt;p&gt;Once more, before academics try to strangle me (again): verifying Signal is not new. ProVerif could do this in 2016. Tamarin can do it. CryptoVerif can do it. Verifpal reaching this milestone in 2026 is not a claim of parity with these tools.&lt;/p&gt;
&lt;p&gt;What it is, is a claim of &lt;em&gt;relevance&lt;/em&gt;. Verifpal is used in university classrooms to teach protocol verification to students who have never seen a formal method before. When a student asks &amp;ldquo;can we model Signal?&amp;rdquo;, the answer is no longer &amp;ldquo;not in Verifpal.&amp;rdquo; That matters.&lt;/p&gt;
&lt;p&gt;The performance improvements also benefit every other Verifpal model. Analysis that previously timed out or exhausted memory will now terminate. Models with multi-argument primitives and complex key derivation chains, the bread and butter of real-world protocols, are now within reach.&lt;/p&gt;
&lt;p&gt;Verifpal 0.31.2 is available now. You can install it via &lt;a href=&#34;https://brew.sh&#34;&gt;Homebrew&lt;/a&gt;, &lt;a href=&#34;https://scoop.sh&#34;&gt;Scoop&lt;/a&gt;, or download it directly from &lt;a href=&#34;https://verifpal.com&#34;&gt;verifpal.com&lt;/a&gt;.&lt;/p&gt;
</content:encoded>
      <category>Software</category>
      <category>Research</category>
      <category>Verifpal</category>
      <category>Formal Verification</category>
      <category>Protocol Design</category>
      
    </item>
    
    <item>
      <title>On the Promises of &#39;High-Assurance&#39; Cryptography</title>
      <link>https://symbolic.software/blog/2026-02-05-ce-labs/</link>
      <pubDate>Thu, 05 Feb 2026 00:00:00 &#43;0000</pubDate>
      <guid>https://symbolic.software/blog/2026-02-05-ce-labs/</guid>
      <description>A case study on CE Labs&#39; libcrux exposing the gap between formal verification marketing and engineering reality.</description>
      <content:encoded>




&lt;nav class=&#34;series-nav&#34; aria-label=&#34;CE Labs series (top)&#34;&gt;
  &lt;header class=&#34;series-nav-header&#34;&gt;
    &lt;span class=&#34;series-nav-label&#34;&gt;▶ CE Labs Series&lt;/span&gt;
    &lt;span class=&#34;series-nav-count&#34;&gt;5 posts · 2 papers · 1 talk&lt;/span&gt;
  &lt;/header&gt;
  &lt;ol class=&#34;series-nav-list&#34;&gt;
    
    
    &lt;li class=&#34;current&#34; aria-current=&#34;page&#34;&gt;
      
      &lt;span class=&#34;title&#34;&gt;On the Promises of &amp;#39;High-Assurance&amp;#39; Cryptography&lt;/span&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.02.05&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-02-12-ce-labs-response/&#34;&gt;We Found Bugs in CE Labs&amp;#39; Verified Code&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.02.12&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-02-17-ce-labs-mldsa/&#34;&gt;Even More Bugs in CE Labs&amp;#39; libcrux: ML-DSA&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.02.17&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-03-07-ce-labs-tls/&#34;&gt;CE Labs&amp;#39; Approach to TLS: A Critical Analysis&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.03.07&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-04-07-cryspen-hax/&#34;&gt;The Verification Facade: Structural Gaps in Cryspen&amp;#39;s Hax Pipeline&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.04.07&lt;/span&gt;
    &lt;/li&gt;
    
  &lt;/ol&gt;
  &lt;div class=&#34;series-nav-footer&#34;&gt;
    &lt;div class=&#34;series-nav-row&#34;&gt;
      &lt;span class=&#34;series-nav-rowlabel&#34;&gt;Papers&lt;/span&gt;
      &lt;a href=&#34;https://eprint.iacr.org/2026/192&#34;&gt;&lt;em&gt;Verification Theatre&lt;/em&gt;&lt;/a&gt;
      &lt;span class=&#34;series-nav-sep&#34;&gt;·&lt;/span&gt;
      &lt;a href=&#34;https://eprint.iacr.org/2026/670&#34;&gt;&lt;em&gt;Verification Facade&lt;/em&gt;&lt;/a&gt;
    &lt;/div&gt;
    &lt;div class=&#34;series-nav-row&#34;&gt;
      &lt;span class=&#34;series-nav-rowlabel&#34;&gt;Talk&lt;/span&gt;
      &lt;a href=&#34;https://www.youtube.com/watch?v=TdOXza1-M_4&#34;&gt;&lt;em&gt;High Assurance Cryptography and the Ethics of Disclosure (OSTIF)&lt;/em&gt;&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/nav&gt;

&lt;p&gt;&lt;strong&gt;Update (March 7, 2026):&lt;/strong&gt; A fifth finding has been added to this post: a denial-of-service vulnerability in libcrux-psq&amp;rsquo;s AES-GCM decryption path (Finding 5), discovered after the original publication.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://celabs.eu&#34;&gt;CE Labs&lt;/a&gt; is a company that markets itself as providing &amp;ldquo;services and software for high assurance software in order to establish trust into your critical systems.&amp;rdquo; Their flagship product, libcrux, is billed as a cryptographic library that &amp;ldquo;has been formally verified&amp;rdquo; and offers &amp;ldquo;the highest level of assurance&amp;rdquo;, translating into that it is safe to use and free of bugs.&lt;/p&gt;
&lt;p&gt;In the past two days, we at Symbolic Software have submitted four pull requests to CE Labs&amp;rsquo; repositories addressing security vulnerabilities and implementation defects. These findings follow an incident from late 2025 in which libcrux produced silently incorrect cryptographic outputs on certain platforms: a bug that CE Labs addressed quietly, without any public disclosure, and which only received a security advisory because third parties took it upon themselves to file one.&lt;/p&gt;
&lt;p&gt;Taken together, these events raise the question of what &amp;ldquo;high assurance cryptography&amp;rdquo; actually means, and whether the formal verification community has developed a habit of overpromising and underdelivering.&lt;/p&gt;
&lt;h2 id=&#34;the-original-incident-platform-dependent-cryptographic-failures&#34;&gt;The Original Incident: Platform-Dependent Cryptographic Failures&lt;/h2&gt;
&lt;p&gt;In November 2025, Filippo Valsorda &lt;a href=&#34;https://github.com/celabshq/libcrux/issues/1220&#34;&gt;reported&lt;/a&gt; that libcrux-ml-dsa v0.0.3 produced different outputs depending on the execution environment. The same seed input generated different public keys and signatures on Alpine Linux with Ampere Altra ARM64 hardware compared to macOS on Apple Silicon. This was not an edge case in error handling or a performance regression. This was the core cryptographic functionality producing incorrect results.&lt;/p&gt;
&lt;p&gt;The bug resided in an unverified fallback implementation for the &lt;code&gt;vxarq_u64&lt;/code&gt; intrinsic. On platforms lacking native SHA-3 instruction support, the fallback passed incorrect arguments, corrupting SHA-3 digests and causing downstream cryptographic operations to fail silently. Any system relying on this code for ML-DSA signatures or ML-KEM key exchange would have experienced authentication failures or key agreement mismatches, depending on which platform generated the cryptographic material.&lt;/p&gt;
&lt;p&gt;CE Labs fixed the bug. What they did not do was issue any public disclosure, security advisory, or acknowledgment that their &amp;ldquo;formally verified&amp;rdquo; library had shipped with a defect that caused silent cryptographic failures in production environments.&lt;/p&gt;
&lt;h2 id=&#34;the-burial&#34;&gt;The Burial&lt;/h2&gt;
&lt;p&gt;The only security advisory that exists for this vulnerability was not initiated by CE Labs. It was &lt;a href=&#34;https://github.com/rustsec/advisory-db/pull/2493&#34;&gt;filed&lt;/a&gt; by Joe Birr-Pixton (ctz) in the RustSec advisory database, a third-party effort to document the vulnerability for the Rust ecosystem. The advisory was merged on December 4, 2025.&lt;/p&gt;
&lt;p&gt;When CE Labs&amp;rsquo; maintainer engaged with the advisory process, they requested that the advisory target &lt;code&gt;libcrux-intrinsics&lt;/code&gt;, an internal dependency, rather than the user-facing &lt;code&gt;libcrux-ml-kem&lt;/code&gt; or &lt;code&gt;libcrux-ml-dsa&lt;/code&gt; crates. While technically accurate (the bug originated in the intrinsics crate), this framing minimizes the user-visible impact. Most developers who depend on &lt;code&gt;libcrux-ml-kem&lt;/code&gt; do not monitor advisories for transitive dependencies; targeting the advisory at the internal crate reduces the likelihood that affected users will learn of it through automated tools like &lt;code&gt;cargo audit&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This framing is technically accurate and practically misleading. The intrinsics crate is an internal dependency of libcrux-ml-kem and libcrux-ml-dsa. Users of those libraries experienced corrupted cryptographic operations. The distinction between &amp;ldquo;the intrinsic layer had a bug&amp;rdquo; and &amp;ldquo;the cryptographic library produced wrong outputs&amp;rdquo; matters to CE Labs&amp;rsquo; marketing narrative but not to anyone whose systems were affected.&lt;/p&gt;
&lt;p&gt;CE Labs themselves made absolutely no announcement anywhere regarding the bug. There was no blog post. No security bulletin. No announcement on any CE Labs communication channel. No email to known users of the library. The RustSec advisory, which most developers will never see unless they actively run &lt;code&gt;cargo audit&lt;/code&gt;, represents the entirety of CE Labs&amp;rsquo; disclosure posture for a bug that caused their formally verified cryptographic library to produce incorrect outputs.&lt;/p&gt;
&lt;p&gt;Compare this to how other cryptographic library maintainers handle similar situations. When the Kyber reference implementation contained a variable timing vulnerability in late 2023, it was the subject of extensive public discussion, a dedicated tracking website (&lt;a href=&#34;https://kyberslash.cr.yp.to/libraries.html&#34;&gt;KyberSlash&lt;/a&gt;), and advisories from affected downstream implementations. When we discovered the same vulnerability in our own &lt;a href=&#34;https://symbolic.software/blog/2023-12-19-kyberk2sovariabletiming/&#34;&gt;Kyber-K2SO&lt;/a&gt; implementation, we issued a public security announcement within 24 hours; we were among the first to do so.&lt;/p&gt;
&lt;p&gt;CE Labs&amp;rsquo; approach was to fix the bug, avoid any public acknowledgment, and wait for someone else to file an advisory in an obscure database.&lt;/p&gt;
&lt;h2 id=&#34;five-new-findings&#34;&gt;Five New Findings&lt;/h2&gt;
&lt;p&gt;Between February 3 and February 4, 2026, we identified four additional issues in CE Labs&amp;rsquo; cryptographic libraries, with a fifth discovered subsequently. These range from specification non-compliance to entropy reduction to potential nonce reuse to denial of service.&lt;/p&gt;
&lt;h3 id=&#34;1-missing-x25519-all-zero-validation-hpke-rs&#34;&gt;1. Missing X25519 All-Zero Validation (hpke-rs)&lt;/h3&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/celabshq/hpke-rs/pull/117&#34;&gt;Pull request #117&lt;/a&gt; addresses a missing validation required by RFC 9180, the HPKE specification. Section 7.1.4 of the RFC mandates that implementations &amp;ldquo;MUST check whether the Diffie-Hellman shared secret is the all-zero value and abort if so.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;The X25519 function, when given certain low-order points as input (such as the identity element or points of small order), produces an all-zero output. If an attacker can supply a malicious public key, they can force the shared secret to be zero, making the subsequent key derivation deterministic and predictable.&lt;/p&gt;
&lt;p&gt;This vulnerability concerns an explicit requirement in the specification that the implementation claimed to follow. The fix is trivial: a single comparison after the DH computation. Its absence indicates that either the specification was not read carefully or the requirement was deprioritized.&lt;/p&gt;
&lt;h3 id=&#34;2-nonce-reuse-via-sequence-number-overflow-hpke-rs&#34;&gt;2. Nonce Reuse via Sequence Number Overflow (hpke-rs)&lt;/h3&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/celabshq/hpke-rs/pull/118&#34;&gt;Pull request #118&lt;/a&gt; addresses a flaw in how HPKE sequence numbers are managed. The library stores the sequence number as a &lt;code&gt;u32&lt;/code&gt;, which has a maximum value of approximately 4.3 billion. RFC 9180 specifies that the sequence number must be checked against 2^96 - 1 for standard 12-byte nonces.&lt;/p&gt;
&lt;p&gt;The overflow check in the original code compared a &lt;code&gt;u32&lt;/code&gt; against a value that a &lt;code&gt;u32&lt;/code&gt; can never reach. In debug builds, Rust&amp;rsquo;s overflow checking would cause a panic when the counter wrapped. In release builds, which are the builds that actually run in production, the counter would silently wrap to zero, reusing nonces.&lt;/p&gt;
&lt;p&gt;Nonce reuse in authenticated encryption is catastrophic. For AES-GCM, it enables recovery of the authentication key and XOR of plaintexts. For ChaCha20-Poly1305, it directly enables plaintext recovery through XOR attacks. Any application encrypting more than 4.3 billion messages with the same HPKE context would be vulnerable.&lt;/p&gt;
&lt;p&gt;The fix is one line: replace &lt;code&gt;+&lt;/code&gt; with &lt;code&gt;checked_add()&lt;/code&gt;. The fact that this was not already the implementation suggests that the security implications of integer overflow in cryptographic contexts were not adequately considered.&lt;/p&gt;
&lt;h3 id=&#34;3-ecdsa-signature-malleability-libcrux&#34;&gt;3. ECDSA Signature Malleability (libcrux)&lt;/h3&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/celabshq/libcrux/pull/1315&#34;&gt;Pull request #1315&lt;/a&gt; addresses signature malleability in the ECDSA P-256 implementation. For any valid ECDSA signature (r, s), there exists an alternative valid signature (r, n - s), where n is the curve order. Without low-S normalization, which constrains s to be at most n/2, signatures are malleable: an attacker can produce alternative valid signatures without knowing the private key.&lt;/p&gt;
&lt;p&gt;This matters in practice. Bitcoin&amp;rsquo;s BIP 62 and BIP 146 mandate low-S normalization because signature malleability can alter transaction identifiers, potentially enabling theft and breaking transaction chains. More generally, any system that uses signatures for deduplication, audit trails, or caching can be confused by malleable signatures.&lt;/p&gt;
&lt;p&gt;The libcrux implementation validates that r and s are nonzero but does not compare s against n/2 or apply normalization. This is a well-known requirement that has been standard practice in ECDSA implementations for over a decade.&lt;/p&gt;
&lt;h3 id=&#34;4-ed25519-double-clamping-libcrux&#34;&gt;4. Ed25519 Double Clamping (libcrux)&lt;/h3&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/celabshq/libcrux/pull/1316&#34;&gt;Pull request #1316&lt;/a&gt; addresses an unnecessary entropy reduction in Ed25519 key generation. The &lt;code&gt;generate_key_pair&lt;/code&gt; function was applying scalar clamping operations to the raw seed before hashing:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;sk&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;248&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;u8&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;sk&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;31&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;127&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;u8&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;sk&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;31&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;|=&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;64&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;u8&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;According to RFC 8032, clamping should occur after SHA-512 hashing of the seed, not before. Applying these operations to the raw seed removes 5 bits of entropy (3 from the first byte, 2 from the last), reducing the effective key space from 256 bits to 251 bits.&lt;/p&gt;
&lt;p&gt;While 251 bits remains computationally secure against brute force, there is no benefit to this reduction. The SHA-512 hash produces a uniformly distributed output regardless of input patterns; clamping the input serves no cryptographic purpose. It only reduces entropy without providing any compensating advantage.&lt;/p&gt;
&lt;p&gt;This is not a subtle issue. The Ed25519 specification is explicit about when clamping occurs. Implementing it incorrectly suggests either that the specification was not consulted or that its requirements were misunderstood.&lt;/p&gt;
&lt;h3 id=&#34;5-denial-of-service-via-aes-gcm-decryption-panic-libcrux-psq&#34;&gt;5. Denial of Service via AES-GCM Decryption Panic (libcrux-psq)&lt;/h3&gt;
&lt;p&gt;The &lt;a href=&#34;https://github.com/celabshq/libcrux&#34;&gt;libcrux-psq&lt;/a&gt; crate implements a post-quantum pre-shared-key protocol. In the &lt;code&gt;decrypt_out&lt;/code&gt; method, the AES-GCM 128 decryption path calls &lt;code&gt;.unwrap()&lt;/code&gt; on the decryption result instead of propagating the error. A single malformed ciphertext crashes the process.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-rust&#34; data-lang=&#34;rust&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;AEADKey&lt;/span&gt;::&lt;span class=&#34;n&#34;&gt;AesGcm128&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;key&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;libcrux_aesgcm&lt;/span&gt;::&lt;span class=&#34;n&#34;&gt;AesGcm128&lt;/span&gt;::&lt;span class=&#34;n&#34;&gt;decrypt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;plaintext&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;key&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;bp&#34;&gt;self&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;nonce&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;        &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;aad&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ciphertext&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;n&#34;&gt;tag&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;unwrap&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;();&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;    &lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// .map_err(|_| AEADError::CryptoError)?;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The correct error handling is commented out directly below the &lt;code&gt;.unwrap()&lt;/code&gt; call. Beyond the denial-of-service impact, this defect renders the PSQ implementation not IND-CCA secure: the IND-CCA security game requires a decryption oracle that returns ⊥ on invalid ciphertexts, but a panicking implementation terminates execution instead, making the decryption oracle unavailable after the first invalid query.&lt;/p&gt;
&lt;p&gt;The verified AES-GCM primitive correctly returns an error on authentication failure. The unverified wrapper discards it. The fix is a &lt;a href=&#34;https://github.com/nadimkobeissi/libcrux/commit/96d3d8a751aa536a619476a8b45c7c12b51a74ec&#34;&gt;one-line change&lt;/a&gt;: replace &lt;code&gt;.unwrap()&lt;/code&gt; with the error propagation that was already written and commented out.&lt;/p&gt;
&lt;h2 id=&#34;what-does-this-tell-us-about-high-assurance-cryptography&#34;&gt;What Does This Tell Us About &amp;ldquo;High Assurance Cryptography&amp;rdquo;?&lt;/h2&gt;
&lt;p&gt;The formal verification community has developed a vocabulary for marketing that systematically overstates what verification actually accomplishes. When CE Labs claims their library is &amp;ldquo;formally verified&amp;rdquo; and &amp;ldquo;free of bugs,&amp;rdquo; they are making statements that their own bug history contradicts.&lt;/p&gt;
&lt;p&gt;The November 2025 incident is instructive. The bug was in an ARM SIMD intrinsic fallback, precisely the kind of low-level, platform-specific code that formal verification tools cannot reason about. Formal verification in practice means verifying an abstract model, then trusting that the compiler, the intrinsics, the runtime, and the hardware all faithfully implement that model.&lt;/p&gt;
&lt;p&gt;This is not verification in the sense that users understand the term. It is verification of a Rust model, plus hope that LLVM generates correct code, plus hope that the ARM instruction set behaves as documented, plus hope that every CPU implementation thereof is free of errata, plus hope that the operating system, memory allocator, and runtime environment introduce no observable differences.&lt;/p&gt;
&lt;p&gt;The honest description would be: &amp;ldquo;We have verified portions of our Rust code against certain properties using the hax toolchain. The actual security of deployed systems depends on your compiler version, target architecture, CPU microcode, operating system, and numerous other components we cannot verify.&amp;rdquo; But that does not sell. It does not win grants. It does not attract customers.&lt;/p&gt;
&lt;p&gt;Instead, we get &amp;ldquo;formally verified&amp;rdquo; as a marketing term, a way to claim superiority over libraries that rely on extensive testing, fuzzing, cross-platform validation, and code review. Those are the engineering practices that might have caught the platform-dependent output bug before it shipped.&lt;/p&gt;
&lt;p&gt;The five new findings reinforce this pattern. None of them involve subtle interactions between verified and unverified code. They are straightforward implementation defects:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A specification requirement that was ignored (X25519 zero check).&lt;/li&gt;
&lt;li&gt;An integer type that was too small for its purpose (sequence number overflow).&lt;/li&gt;
&lt;li&gt;A normalization step that was omitted (ECDSA low-S).&lt;/li&gt;
&lt;li&gt;A clamping operation that was applied at the wrong point (Ed25519 double clamping).&lt;/li&gt;
&lt;li&gt;A verified primitive&amp;rsquo;s error discarded by an unverified wrapper (AES-GCM decryption panic).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These are not the kinds of bugs that formal verification is designed to catch. They are the kinds of bugs that careful code review, thorough testing, and attention to specifications would catch. They suggest that the development process prioritized the verifiable core over the engineering fundamentals that surround it.&lt;/p&gt;
&lt;h2 id=&#34;the-verification-theater-problem&#34;&gt;The Verification Theater Problem&lt;/h2&gt;
&lt;p&gt;There is a pattern in the formal methods community that might be called &amp;ldquo;verification theater&amp;rdquo;: the deployment of formal methods in ways that create the appearance of rigor without delivering its substance.&lt;/p&gt;
&lt;p&gt;A cryptographic library that is &amp;ldquo;formally verified&amp;rdquo; but ships with platform-dependent incorrect outputs has not achieved high assurance. It has achieved a verified core surrounded by an unverified periphery that can fail in ways that make the verification meaningless.&lt;/p&gt;
&lt;p&gt;A company that fixes critical bugs without disclosure has not demonstrated trustworthiness. It has demonstrated that marketing concerns outweigh transparency obligations.&lt;/p&gt;
&lt;p&gt;A codebase that lacks basic specification compliance, such as checking for all-zero DH outputs, preventing integer overflow, and normalizing signatures, has not been developed with the care that &amp;ldquo;highest level of assurance&amp;rdquo; implies.&lt;/p&gt;
&lt;p&gt;The formal verification community needs to develop more honest communication practices. Verification is valuable. It catches certain classes of bugs that testing cannot. But it is not a substitute for engineering discipline, and claiming otherwise undermines both the credibility of formal methods and the security of users who trust those claims.&lt;/p&gt;
&lt;p&gt;When a formally verified cryptographic library produces platform-dependent incorrect outputs for its core primitive, and that library&amp;rsquo;s maintainers bury the disclosure while continuing to market the library as &amp;ldquo;free of bugs,&amp;rdquo; we must ask: free of which bugs, verified against which properties, and trustworthy to whom?&lt;/p&gt;
&lt;p&gt;The answers, in CE Labs&amp;rsquo; case, appear to be: not the bugs that matter, not the properties users care about, and not to anyone paying close attention.&lt;/p&gt;





&lt;nav class=&#34;series-nav&#34; aria-label=&#34;CE Labs series (bottom)&#34;&gt;
  &lt;header class=&#34;series-nav-header&#34;&gt;
    &lt;span class=&#34;series-nav-label&#34;&gt;▶ CE Labs Series&lt;/span&gt;
    &lt;span class=&#34;series-nav-count&#34;&gt;5 posts · 2 papers · 1 talk&lt;/span&gt;
  &lt;/header&gt;
  &lt;ol class=&#34;series-nav-list&#34;&gt;
    
    
    &lt;li class=&#34;current&#34; aria-current=&#34;page&#34;&gt;
      
      &lt;span class=&#34;title&#34;&gt;On the Promises of &amp;#39;High-Assurance&amp;#39; Cryptography&lt;/span&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.02.05&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-02-12-ce-labs-response/&#34;&gt;We Found Bugs in CE Labs&amp;#39; Verified Code&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.02.12&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-02-17-ce-labs-mldsa/&#34;&gt;Even More Bugs in CE Labs&amp;#39; libcrux: ML-DSA&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.02.17&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-03-07-ce-labs-tls/&#34;&gt;CE Labs&amp;#39; Approach to TLS: A Critical Analysis&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.03.07&lt;/span&gt;
    &lt;/li&gt;
    
    
    &lt;li&gt;
      
      &lt;a href=&#34;https://symbolic.software/blog/2026-04-07-cryspen-hax/&#34;&gt;The Verification Facade: Structural Gaps in Cryspen&amp;#39;s Hax Pipeline&lt;/a&gt;
      
      &lt;span class=&#34;date&#34;&gt;2026.04.07&lt;/span&gt;
    &lt;/li&gt;
    
  &lt;/ol&gt;
  &lt;div class=&#34;series-nav-footer&#34;&gt;
    &lt;div class=&#34;series-nav-row&#34;&gt;
      &lt;span class=&#34;series-nav-rowlabel&#34;&gt;Papers&lt;/span&gt;
      &lt;a href=&#34;https://eprint.iacr.org/2026/192&#34;&gt;&lt;em&gt;Verification Theatre&lt;/em&gt;&lt;/a&gt;
      &lt;span class=&#34;series-nav-sep&#34;&gt;·&lt;/span&gt;
      &lt;a href=&#34;https://eprint.iacr.org/2026/670&#34;&gt;&lt;em&gt;Verification Facade&lt;/em&gt;&lt;/a&gt;
    &lt;/div&gt;
    &lt;div class=&#34;series-nav-row&#34;&gt;
      &lt;span class=&#34;series-nav-rowlabel&#34;&gt;Talk&lt;/span&gt;
      &lt;a href=&#34;https://www.youtube.com/watch?v=TdOXza1-M_4&#34;&gt;&lt;em&gt;High Assurance Cryptography and the Ethics of Disclosure (OSTIF)&lt;/em&gt;&lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/nav&gt;

</content:encoded>
      <category>Research</category>
      <category>Formal Verification</category>
      
    </item>
    
    <item>
      <title>Announcing Cedarcrypt: Applied Cryptography in the Mediterranean</title>
      <link>https://symbolic.software/blog/2026-02-03-cedarcrypt/</link>
      <pubDate>Tue, 03 Feb 2026 00:00:00 &#43;0000</pubDate>
      <guid>https://symbolic.software/blog/2026-02-03-cedarcrypt/</guid>
      <description>Announcing Cedarcrypt, a new applied cryptography summer school and conference.</description>
      <content:encoded>&lt;p&gt;Come be part of Cedarcrypt, our new initiative to grow cryptography research, development and representation in the Levant region.&lt;/p&gt;
&lt;p&gt;Cryptography&amp;rsquo;s major conferences meet in a handful of the same cities every year, and researchers from the Levant are rarely in the room. We want to fix that.&lt;/p&gt;
&lt;h2 id=&#34;what-is-cedarcrypt&#34;&gt;What is Cedarcrypt?&lt;/h2&gt;
&lt;p&gt;This July 13-16, 2026, we&amp;rsquo;re bringing together researchers, practitioners, and students at the American University of Beirut&amp;rsquo;s Mediterraneo campus in Paphos, Cyprus, for four days of lectures, workshops and talks. Secure messaging, post-quantum cryptography, zero-knowledge proofs, formal verification: if it counts as applied cryptography, it belongs at Cedarcrypt.&lt;/p&gt;
&lt;p&gt;Cedarcrypt exists to show that cryptography work comes out of our region too. We want a place where the next generation of cryptographers from the Levant and beyond can learn from established experts, present their own research, and meet the people they will be working with for the rest of their careers.&lt;/p&gt;
&lt;h2 id=&#34;call-for-submissions&#34;&gt;Call for Submissions&lt;/h2&gt;
&lt;p&gt;We need you to make this happen. We&amp;rsquo;re seeking:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Workshop leaders&lt;/strong&gt; to teach hands-on skills.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lecturers&lt;/strong&gt; to cover both the foundations and the newest work.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Researchers&lt;/strong&gt; to present their latest results.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Senior professors and first-year PhD students are equally welcome to submit.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;re particularly interested in work on secure messaging protocols and the problems of running private communication systems at scale, and in post-quantum cryptography as the community works through the migration. Zero-knowledge proofs and their applications in privacy-preserving systems are another focus.&lt;/p&gt;
&lt;p&gt;We also want submissions on formal verification of cryptographic implementations, on secure implementation practices, and on privacy technologies more broadly. If your work touches on how cryptography is built, deployed, or analyzed in practice, we want to hear from you.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cedarcrypt.org/#cfp&#34;&gt;&lt;strong&gt;Submit your proposal!&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&#34;the-venue&#34;&gt;The Venue&lt;/h2&gt;
&lt;p&gt;Paphos is a UNESCO World Heritage city on the southwest coast of Cyprus, with beaches and a good deal of archaeology. The AUB Mediterraneo campus is in the city itself, and most major European cities have direct flights.&lt;/p&gt;
&lt;h2 id=&#34;get-involved&#34;&gt;Get Involved&lt;/h2&gt;
&lt;p&gt;This is the first edition, and we intend to run it every year. Come help us get it right.&lt;/p&gt;
&lt;p&gt;Visit &lt;a href=&#34;https://cedarcrypt.org&#34;&gt;cedarcrypt.org&lt;/a&gt; to learn more and submit your proposal.&lt;/p&gt;
</content:encoded>
      <category>Announcement</category>
      <category>Cedarcrypt</category>
      <category>Applied Cryptography</category>
      
    </item>
    
    <item>
      <title>Kyber-K2SO 1.0: Now Implementing ML-KEM</title>
      <link>https://symbolic.software/blog/2025-12-21-kyberk2so10/</link>
      <pubDate>Sun, 21 Dec 2025 00:00:00 &#43;0000</pubDate>
      <guid>https://symbolic.software/blog/2025-12-21-kyberk2so10/</guid>
      <description>Kyber-K2SO version 1.0 upgrades from Kyber v3 to ML-KEM, the NIST-standardized post-quantum key encapsulation mechanism.</description>
      <content:encoded>&lt;p&gt;&lt;a href=&#34;https://github.com/symbolicsoft/kyber-k2so&#34;&gt;Kyber-K2SO&lt;/a&gt; is Symbolic Software&amp;rsquo;s clean Go implementation of the post-quantum key encapsulation mechanism that won the NIST post-quantum cryptography competition. Today, we&amp;rsquo;re releasing version 1.0, which upgrades the implementation from Kyber v3 to &lt;a href=&#34;https://csrc.nist.gov/pubs/fips/203/final&#34;&gt;ML-KEM (FIPS 203)&lt;/a&gt;, the official NIST standard finalized in August 2024.&lt;/p&gt;
&lt;h2 id=&#34;why-ml-kem&#34;&gt;Why ML-KEM?&lt;/h2&gt;
&lt;p&gt;While Kyber was the competition submission, ML-KEM is the standardized version. The two are closely related but not identical. ML-KEM incorporates refinements based on years of cryptanalysis and implementation feedback. Most importantly, ML-KEM is the version that will be widely deployed in protocols like TLS 1.3 and other security-critical applications.&lt;/p&gt;
&lt;p&gt;By upgrading to ML-KEM, Kyber-K2SO ensures interoperability with other conforming implementations and alignment with the official NIST standard.&lt;/p&gt;
&lt;h2 id=&#34;whats-new-in-version-10&#34;&gt;What&amp;rsquo;s New in Version 1.0&lt;/h2&gt;
&lt;h3 id=&#34;ml-kem-fips-203-compliance&#34;&gt;ML-KEM (FIPS 203) Compliance&lt;/h3&gt;
&lt;p&gt;The core implementation now follows the FIPS 203 specification. We&amp;rsquo;ve replaced the old Kyber v3 test vectors with the official &lt;a href=&#34;https://github.com/C2SP/CCTV/tree/main/ML-KEM&#34;&gt;ML-KEM test vectors from C2SP/CCTV&lt;/a&gt;, which are the reference intermediate test vectors for FIPS 203 compliance testing:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;TestMLKEM768Vector&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;t&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;testing&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;T&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;nx&#34;&gt;dkBytes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;hex&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;DecodeString&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;mlkem768TestVector&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;dk&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;!=&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;		&lt;span class=&#34;nx&#34;&gt;t&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Fatal&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;nx&#34;&gt;cBytes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;hex&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;DecodeString&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;mlkem768TestVector&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;c&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;!=&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;		&lt;span class=&#34;nx&#34;&gt;t&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Fatal&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;nx&#34;&gt;expectedK&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;hex&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;DecodeString&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;mlkem768TestVector&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;K&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;!=&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;		&lt;span class=&#34;nx&#34;&gt;t&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Fatal&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;dk&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Kyber768SKBytes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;byte&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;c&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Kyber768CTBytes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;byte&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;nb&#34;&gt;copy&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;dk&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[:],&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;dkBytes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;nb&#34;&gt;copy&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;c&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[:],&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;cBytes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;nx&#34;&gt;K&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;KemDecrypt768&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;c&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;dk&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;!=&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;		&lt;span class=&#34;nx&#34;&gt;t&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Fatal&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;subtle&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;ConstantTimeCompare&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;K&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[:],&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;expectedK&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;		&lt;span class=&#34;nx&#34;&gt;t&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Errorf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;ML-KEM-768 test vector failed\nExpected: %x\nGot: %x&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;expectedK&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;K&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[:])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;All three security levels pass their respective test vectors: ML-KEM-512, ML-KEM-768, and ML-KEM-1024.&lt;/p&gt;
&lt;h3 id=&#34;best-effort-secret-zeroization&#34;&gt;Best-Effort Secret Zeroization&lt;/h3&gt;
&lt;p&gt;Cryptographic implementations should clear sensitive data from memory as soon as it&amp;rsquo;s no longer needed. Version 1.0 introduces systematic zeroization of secrets and intermediate values throughout the KEM operations:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// byteopsZeroBytes zeroes a byte slice to clear sensitive data from memory.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;byteopsZeroBytes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;b&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;byte&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;i&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;range&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;b&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;		&lt;span class=&#34;nx&#34;&gt;b&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This function is now called after encryption and decryption operations to clear intermediate values like the message hash, key derivation inputs, and the implicit rejection key:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;KemEncrypt768&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;publicKey&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Kyber768PKBytes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;byte&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Kyber768CTBytes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;byte&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;KyberSSBytes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;byte&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;error&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;c1&#34;&gt;// ... encryption logic ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;nf&#34;&gt;byteopsZeroBytes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;d&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[:])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;nf&#34;&gt;byteopsZeroBytes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;m&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[:])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;nf&#34;&gt;byteopsZeroBytes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;krInput&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[:])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;nf&#34;&gt;byteopsZeroBytes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;kr&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[:])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;ciphertextFixedLength&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;sharedSecretFixedLength&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Additionally, if random number generation fails during key generation, the partially-constructed private key is now explicitly zeroed before returning an error:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;_&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;rand&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Read&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;privateKeyFixedLength&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;skStart&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;!=&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;nil&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;i&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;range&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;privateKeyFixedLength&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;		&lt;span class=&#34;nx&#34;&gt;privateKeyFixedLength&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;privateKeyFixedLength&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;publicKeyFixedLength&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We call this &amp;ldquo;best-effort&amp;rdquo; because Go&amp;rsquo;s garbage collector and compiler optimizations may still leave copies of sensitive data in memory. However, explicit zeroization remains a valuable defense-in-depth measure.&lt;/p&gt;
&lt;h3 id=&#34;defense-in-depth-bounded-rejection-sampling&#34;&gt;Defense in Depth: Bounded Rejection Sampling&lt;/h3&gt;
&lt;p&gt;The matrix generation routine uses rejection sampling to uniformly sample polynomial coefficients. While the probability of needing more than one iteration is astronomically low (approximately 10^-82), version 1.0 adds an explicit iteration bound as a safety measure:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;r&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;][&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;j&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;],&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;ctr&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;indcpaRejUniform&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;buf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[:&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;504&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;],&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;504&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;paramsN&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// Retry with remaining buffer bytes if needed&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// Bound iterations as a safety measure (probability of needing &amp;gt;1 iteration is ~10^-82)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;iterations&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;ctr&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;paramsN&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;iterations&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;100&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;iterations&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;++&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;nx&#34;&gt;missing&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;ctrn&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;indcpaRejUniform&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;buf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;504&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:],&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;168&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;paramsN&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ctr&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;k&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;ctr&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;k&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;paramsN&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;k&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;++&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;		&lt;span class=&#34;nx&#34;&gt;r&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;][&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;j&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;][&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;k&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;missing&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;k&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ctr&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	&lt;span class=&#34;nx&#34;&gt;ctr&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;ctrn&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This prevents any theoretical infinite loop scenario, even though such a scenario is practically impossible with a correctly functioning random number generator.&lt;/p&gt;
&lt;h3 id=&#34;performance-improvements&#34;&gt;Performance Improvements&lt;/h3&gt;
&lt;p&gt;Version 1.0 includes several performance optimizations that reduce heap allocations and improve efficiency:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fixed-size arrays instead of slices:&lt;/strong&gt; Internal buffers are now declared as fixed-size arrays where possible, reducing pressure on the garbage collector:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// Before&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;buf&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;make&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;([]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;byte&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;672&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// After&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;buf&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;672&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;byte&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Pre-computed Barrett reduction constant:&lt;/strong&gt; The Barrett reduction now uses a pre-computed constant rather than computing it at runtime:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// Before&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;v&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;int16&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;int16&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(((&lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;uint32&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;26&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;uint32&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;paramsQ&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;/&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;/&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;uint32&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;paramsQ&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;t&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;int16&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;int32&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;v&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;int32&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;a&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;26&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// After&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;t&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;int16&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;((&lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;int32&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;paramsBarrettV&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;int32&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;a&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;26&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;Streamlined key construction:&lt;/strong&gt; Private key assembly now uses direct copy operations into the fixed-length output array, eliminating intermediate slice allocations:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// Before&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;privateKey&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;append&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;indcpaPrivateKey&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;indcpaPublicKey&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;...&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;privateKey&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;append&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;privateKey&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;pkh&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[:]&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;...&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;privateKey&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;append&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;privateKey&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;rnd&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;...&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;copy&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;privateKeyFixedLength&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[:],&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;privateKey&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// After&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;skStart&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;copy&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;privateKeyFixedLength&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[:],&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;indcpaPrivateKey&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;skStart&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;copy&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;privateKeyFixedLength&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;skStart&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:],&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;indcpaPublicKey&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;skStart&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;copy&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;privateKeyFixedLength&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;skStart&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:],&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;pkh&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[:])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nx&#34;&gt;_&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;err&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;rand&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Read&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;privateKeyFixedLength&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;skStart&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:])&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;These changes result in measurably faster operations across all security levels.&lt;/p&gt;
&lt;h3 id=&#34;code-quality-improvements&#34;&gt;Code Quality Improvements&lt;/h3&gt;
&lt;p&gt;The polynomial type definition has been corrected to use the semantically appropriate constant:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// Before&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;type&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;poly&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;paramsPolyBytes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int16&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// After&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;type&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;poly&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;paramsN&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int16&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Since &lt;code&gt;paramsN&lt;/code&gt; (256) represents the polynomial degree and is the actual dimension of coefficient arrays, this change makes the code more self-documenting without affecting functionality.&lt;/p&gt;
&lt;h2 id=&#34;upgrading&#34;&gt;Upgrading&lt;/h2&gt;
&lt;p&gt;To upgrade to version 1.0:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;go get -u github.com/symbolicsoft/kyber-k2so
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The API remains unchanged. If you were previously using Kyber-K2SO with Kyber v3, your code will continue to work without modification. However, the key material and ciphertexts are now ML-KEM format, which means they are not interoperable with Kyber v3 implementations.&lt;/p&gt;
&lt;h2 id=&#34;acknowledgements&#34;&gt;Acknowledgements&lt;/h2&gt;
&lt;p&gt;We thank the &lt;a href=&#34;https://github.com/C2SP/CCTV/tree/main/ML-KEM&#34;&gt;C2SP project&lt;/a&gt; for maintaining the official test vectors that made compliance testing straightforward.&lt;/p&gt;
</content:encoded>
      <category>Software</category>
      <category>Kyber-K2SO</category>
      <category>Post-Quantum</category>
      
    </item>
    
    <item>
      <title>2PC-MPC in Rust: Audit Report</title>
      <link>https://symbolic.software/blog/2024-06-04-2pcmpc/</link>
      <pubDate>Tue, 04 Jun 2024 00:00:00 &#43;0000</pubDate>
      <guid>https://symbolic.software/blog/2024-06-04-2pcmpc/</guid>
      <description>Our initial audit of the Rust implementation of dWallet Labs&#39; 2PC-MPC protocol.</description>
      <content:encoded>&lt;p&gt;&lt;strong&gt;→ &lt;a href=&#34;https://symbolic.software/pdf/dw-01.pdf&#34;&gt;Download Full Report&lt;/a&gt; (PDF)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;At Symbolic Software, we recently concluded, in close collaboration with &lt;a href=&#34;https://www.3milabs.tech&#34;&gt;3MI Labs&lt;/a&gt;, an extensive audit of the &lt;a href=&#34;https://github.com/dwallet-labs/2pc-mpc&#34;&gt;2PC-MPC Rust crate&lt;/a&gt; developed by &lt;a href=&#34;https://dwalletlabs.com&#34;&gt;dWallet Labs&lt;/a&gt;. We&amp;rsquo;re excited to share our findings and offer our perspectives on the strengths and areas for improvement of this innovative protocol implementation.&lt;/p&gt;
&lt;p&gt;dWallet Labs&amp;rsquo; 2PC-MPC crate represents a practical software implementation of the &lt;a href=&#34;https://eprint.iacr.org/2024/253&#34;&gt;&amp;ldquo;2PC-MPC: Emulating Two Party ECDSA in Large-Scale MPC&amp;rdquo; protocol&lt;/a&gt;. This novel cryptographic structure enables a non-collusive and UC-secure two-party ECDSA scheme, allowing one party to be fully centralized while abstracting away the decentralization of the second party, enabling it to scale to an arbitrary number of virtual parties.&lt;/p&gt;
&lt;h3 id=&#34;audit-methodology-and-focus-areas&#34;&gt;Audit Methodology and Focus Areas&lt;/h3&gt;
&lt;p&gt;Our audit covered the following aspects:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Functional Correctness Assessment&lt;/strong&gt;: We verified that the implementation of the protocol in the 2PC-MPC crate aligns with the specifications outlined in the accompanying paper. This included ensuring that the cryptographic operations are correctly implemented and that the protocol&amp;rsquo;s security properties are maintained.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Security Assessment&lt;/strong&gt;: We evaluated the security of the 2PC-MPC crate and its underlying crates, examining them for potential vulnerabilities that could compromise the security of the protocol. This included assessing the crates for common cryptographic vulnerabilities and ensuring their adherence to secure implementation best practices.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;key-findings-and-recommendations&#34;&gt;Key Findings and Recommendations&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Functional Correctness&lt;/strong&gt;: The implementation generally aligns with the protocol specifications, with the cryptographic operations and protocol steps being correctly realized in the Rust code. However, due to the high complexity of the target, further analysis is recommended to ensure complete functional correctness.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Security&lt;/strong&gt;: We identified three main security findings of varying severity levels, including one critical issue of nonce reuse in the decentralized party presigning step.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Code Complexity&lt;/strong&gt;: The high complexity of the Rust implementation can make the codebase challenging to understand, maintain, and audit. We recommend strategies such as code simplification, comprehensive documentation, rigorous testing, and regular security audits to address this issue.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In addition, Symbolic Software provided a roadmap for future work that proposes several avenues for further exploration and development to enhance the protocol&amp;rsquo;s security, efficiency, and usability. These include an in-depth cryptographic review, cryptographic optimizations, protocol API correctness and usability analysis, state machine transition analysis, understanding the decentralized party in a protocol setting, performance and scalability testing, integration with existing systems and frameworks, and real-world applications and case studies.&lt;/p&gt;
&lt;h3 id=&#34;read-the-report&#34;&gt;Read the Report&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;→ &lt;a href=&#34;https://symbolic.software/pdf/dw-01.pdf&#34;&gt;Download Full Report&lt;/a&gt; (PDF)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Symbolic Software would like to thank Erik Takke and Tomer Ashur from 3MI Labs for their collaboration and support throughout the assessment process. We appreciate their expertise, insights, and dedication to advancing the field of secure multi-party computation. We would also like to extend our sincere thanks to Yehonathan Cohen Scaly and Dolev Mutzari of dWallet Labs for their valuable contributions and feedback on the assessment findings. Their expertise and feedback have been instrumental in enhancing the quality and accuracy of the assessment results.&lt;/p&gt;
</content:encoded>
      <category>Security</category>
      <category>Multi-Party Computation</category>
      <category>Rust</category>
      
    </item>
    
    <item>
      <title>Security Announcement: Variable Timing Issue in Kyber Code</title>
      <link>https://symbolic.software/blog/2023-12-19-kyberk2sovariabletiming/</link>
      <pubDate>Tue, 19 Dec 2023 00:00:00 &#43;0000</pubDate>
      <guid>https://symbolic.software/blog/2023-12-19-kyberk2sovariabletiming/</guid>
      <description>We&#39;ve updated Kyber-K2SO to address a variable timing logic issue.</description>
      <content:encoded>&lt;p&gt;&lt;em&gt;&lt;strong&gt;Update (December 30, 2023):&lt;/strong&gt; Additional potential variable timing issues were &lt;a href=&#34;https://groups.google.com/a/list.nist.gov/g/pqc-forum/c/ldX0ThYJuBo&#34;&gt;found&lt;/a&gt; and subsequently &lt;a href=&#34;https://github.com/symbolicsoft/kyber-k2so/commit/2d16efee71ae195a6aef2fb36f5ed60768d78c98&#34;&gt;fixed&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/symbolicsoft/kyber-k2so&#34;&gt;Kyber-K2SO&lt;/a&gt; is Symbolic Software&amp;rsquo;s clean implementation of the &lt;a href=&#34;https://pq-crystals.org/kyber/&#34;&gt;Kyber&lt;/a&gt; IND-CCA2-secure key encapsulation mechanism (KEM), whose security is based on the hardness of solving the learning-with-errors (LWE) problem over module lattices. Kyber was recently chosen as the winning candidate algorithm of those submitted to the NIST post-quantum cryptography project.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;ve updated Kyber-K2SO to address a variable timing logic vulnerability. This vulnerability also existed in the original Kyber reference implementation and continues to exist in many third-party implementations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;We&amp;rsquo;re proud to be among the first third-party implementations to patch the issue, and the first vendor to issue an actual security advisory.&lt;/strong&gt; Our implemented fix is &lt;a href=&#34;https://github.com/symbolicsoft/kyber-k2so/pull/9&#34;&gt;available for review&lt;/a&gt; on the Kyber-K2SO GitHub repository.&lt;/p&gt;
&lt;h2 id=&#34;background&#34;&gt;Background&lt;/h2&gt;
&lt;p&gt;The vulnerability was discovered by the &lt;a href=&#34;https://celabs.eu&#34;&gt;CE Labs&lt;/a&gt; team two weeks ago and reported directly to the Kyber team, whereupon it was &lt;a href=&#34;https://github.com/pq-crystals/kyber/commit/dda29cc63af721981ee2c831cf00822e69be3220&#34;&gt;patched&lt;/a&gt; in the official Kyber reference implementation.&lt;/p&gt;
&lt;p&gt;Despite fixing the issue, the Kyber team chose not to release an advisory. This lack of an advisory from the Kyber team meant that third-party implementations such as ours were not made aware of the vulnerability&amp;rsquo;s existence.&lt;/p&gt;
&lt;p&gt;On December 16, roughly two weeks later, we were made aware of the vulnerability&amp;rsquo;s existence through &lt;a href=&#34;https://github.com/symbolicsoft/kyber-k2so/issues/7&#34;&gt;a GitHub issue&lt;/a&gt; opened in the Kyber-K2SO repository by the CE Labs team. On December 17, we additionally received an independent email notification from Prof. Daniel J. Bernstein (who had later independently rediscovered the issue) notifying us of the vulnerability.&lt;/p&gt;
&lt;p&gt;We then issued an update containing a software patch within 24 hours. Users are asked to update to &lt;a href=&#34;https://github.com/symbolicsoft/kyber-k2so/releases&#34;&gt;Kyber-K2SO version 0.2.2&lt;/a&gt; (or higher) to benefit from our fix for this issue.&lt;/p&gt;
&lt;h2 id=&#34;the-vulnerability&#34;&gt;The Vulnerability&lt;/h2&gt;
&lt;p&gt;The variable timing logic arises from how the original implementation divides by Q in the decapsulation function, as seen below in Kyber-K2SO&amp;rsquo;s original code:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// polyToMsg converts a polynomial to a 32-byte message&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// and represents the inverse of polyFromMsg.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;polyToMsg&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;a&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;poly&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;byte&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nx&#34;&gt;msg&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;make&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;([]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;byte&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;paramsSymBytes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;t&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;uint16&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;nx&#34;&gt;a&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;polyCSubQ&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;a&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;i&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;i&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;paramsN&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;/&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;8&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;++&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nx&#34;&gt;msg&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;j&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;j&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;8&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;j&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;++&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;c1&#34;&gt;// NOTE: the following assignment is responsible&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;c1&#34;&gt;// for the variable timing issue, through the&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;c1&#34;&gt;// division by paramsQ.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;nx&#34;&gt;t&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;((&lt;/span&gt;&lt;span class=&#34;nb&#34;&gt;uint16&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;a&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;8&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;+&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;j&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;])&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;uint16&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;paramsQ&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;/&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;o&#34;&gt;/&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;uint16&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;paramsQ&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;      &lt;span class=&#34;nx&#34;&gt;msg&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;|=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;byte&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;t&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;j&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;msg&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;As seen below, this vulnerability was also present in the &lt;a href=&#34;https://github.com/pq-crystals/kyber/commit/dda29cc63af721981ee2c831cf00822e69be3220&#34;&gt;reference C Kyber code&lt;/a&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;KYBER_N&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;/&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;8&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;++&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;n&#34;&gt;msg&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;k&#34;&gt;for&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;j&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;j&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;8&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;j&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;++&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;t&lt;/span&gt;  &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;a&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;coeffs&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;8&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;+&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;j&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;];&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;t&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;((&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int16_t&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;t&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;15&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;KYBER_Q&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;t&lt;/span&gt;  &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(((&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;t&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;KYBER_Q&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;/&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;/&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;KYBER_Q&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;// Irrelevant code snipped
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;  &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It is unclear whether this division will be compiled into a multiplication instruction, depending on compiler optimizations. &lt;a href=&#34;https://github.com/symbolicsoft/kyber-k2so/issues/7&#34;&gt;As noted by Goutam Tamvada&lt;/a&gt;, we can see that the output of some C compilers indeed produces a division instruction even when strong optimizations are specified:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://symbolic.software/images/blog/kyber-timing-1.png&#34; alt=&#34;Compilation result: x64 msvc v19.latest&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34; width=&#34;1596&#34; height=&#34;417&#34;&gt;
&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://symbolic.software/images/blog/kyber-timing-2.png&#34; alt=&#34;Compilation result: RISC-V (32-bit) gcc 13.2.0&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34; width=&#34;1573&#34; height=&#34;349&#34;&gt;
&lt;/p&gt;
&lt;p&gt;Should a division instruction be emitted, its execution time would likely be variable and leak information about its secret input.&lt;/p&gt;
&lt;h2 id=&#34;most-implementations-affected&#34;&gt;Most Implementations Affected&lt;/h2&gt;
&lt;p&gt;Even if you do not use Kyber-K2SO, there is a strong chance that your chosen Kyber implementation suffers from the same variable timing vulnerability. The &lt;a href=&#34;https://kyberslash.cr.yp.to/libraries.html&#34;&gt;KyberSlash&lt;/a&gt; website is currently tracking which libraries are vulnerable and which have issued patches or security advisories.&lt;/p&gt;
&lt;h2 id=&#34;impact&#34;&gt;Impact&lt;/h2&gt;
&lt;p&gt;As far as we can tell, the impact of this vulnerability is the same as any standard variable timing vulnerability in low-level asymmetric cryptographic primitives: real-world impact can vary dramatically depending on practical use cases. However, in the worst case scenario, secret key information leakage could be possible. Therefore, we encourage everyone to examine their chosen Kyber implementations to ensure that they are protected from this issue.&lt;/p&gt;
&lt;h2 id=&#34;acknowledgements&#34;&gt;Acknowledgements&lt;/h2&gt;
&lt;p&gt;We thank Goutam Tamvada, Karthikeyan Bhargavan, Franziskus Kiefer, Prof. Daniel J. Bernstein and Prof. Peter Schwabe for their valuable feedback regarding this issue.&lt;/p&gt;
</content:encoded>
      <category>Security</category>
      <category>Kyber-K2SO</category>
      <category>Post-Quantum</category>
      <category>Side Channels</category>
      
    </item>
    
    <item>
      <title>Supporting Real World Crypto 2024</title>
      <link>https://symbolic.software/blog/2023-11-01-rwc2024/</link>
      <pubDate>Wed, 01 Nov 2023 00:00:00 &#43;0000</pubDate>
      <guid>https://symbolic.software/blog/2023-11-01-rwc2024/</guid>
      <description>Symbolic Software continues its support for the IACR Real World Cryptography Symposium.</description>
      <content:encoded>&lt;p&gt;We are pleased to announce that Symbolic Software is once again sponsoring the &lt;a href=&#34;https://rwc.iacr.org&#34;&gt;IACR Real World Cryptography symposium for its 2024 edition&lt;/a&gt;. This marks the sixth consecutive year of our association with this esteemed event, and we couldn&amp;rsquo;t be prouder.&lt;/p&gt;
&lt;p&gt;The IACR Real World Cryptography symposium is a significant event in the cryptography community. It brings together researchers, practitioners, and industry experts to discuss the latest advancements, challenges, and practical applications of cryptographic techniques in the real world. Over the years, the symposium has been a platform for groundbreaking research, fostering collaborations, and promoting the importance of cryptography in ensuring a safer digital world.&lt;/p&gt;
&lt;p&gt;Through supporting the IACR Real World Cryptography symposium, we hope to concretely demonstrate Symbolic Software&amp;rsquo;s commitment to advancing the field of cryptography. We believe in the power of collaboration and the importance of bridging the gap between theory and practice. By supporting this symposium, we aim to contribute to the growth of the cryptographic community and the broader goal of enhancing digital security.&lt;/p&gt;
&lt;p&gt;We would like to extend our gratitude to the organizers of the IACR Real World Cryptography symposium for their tireless efforts in making this event a success year after year. We also want to acknowledge the participants, speakers, and attendees who bring their expertise, insights, and enthusiasm, making the symposium a vibrant and enriching experience.&lt;/p&gt;
&lt;p&gt;As we look forward to the 2024 edition, we invite everyone to join us in celebrating the advancements in real-world cryptography. Let&amp;rsquo;s continue to work together, share knowledge, and drive innovation in this crucial field.&lt;/p&gt;
&lt;p&gt;Thank you for your continued support and trust in Symbolic Software. We hope to see you at the IACR Real World Cryptography symposium in 2024!&lt;/p&gt;
</content:encoded>
      <category>Announcement</category>
      <category>Real World Crypto</category>
      
    </item>
    
    <item>
      <title>Address to French Law Enforcement Officials</title>
      <link>https://symbolic.software/blog/2023-09-29-cnam/</link>
      <pubDate>Fri, 29 Sep 2023 00:00:00 &#43;0000</pubDate>
      <guid>https://symbolic.software/blog/2023-09-29-cnam/</guid>
      <description>An address to France&#39;s law enforcement officials on Internet surveillance policy in France.</description>
      <content:encoded>&lt;p&gt;In a recent address to top French law enforcement officials, directors, ambassadors, and foreign dignitaries, Nadim Kobeissi, the director of Symbolic Software, a French cryptography expertise company, delved deep into the pressing topic of Internet surveillance law in France. The speech, which we are pleased to share in its entirety below, critically examines the current trajectory of French legislative proposals concerning digital privacy, encryption, and cybersecurity.&lt;/p&gt;
&lt;p&gt;The speech aimed to use technical expertise in order to challenge the prevailing narratives and highlight the potential pitfalls of certain regulatory approaches. From the fundamental right to encryption to the potential dangers of turning smartphones into surveillance tools, the speech offers a comprehensive overview of the challenges and opportunities presented by the digital age.&lt;/p&gt;
&lt;p&gt;As you read, we invite you to reflect on the balance between security and individual rights, the importance of informed legislation, and the broader implications of the choices we make today for the future of the digital world in France.&lt;/p&gt;
&lt;p&gt;The speech is available below in its original French:&lt;/p&gt;
&lt;hr&gt;
&lt;div lang=&#34;fr&#34;&gt;
&lt;p&gt;Au cours des dernières années, nous avons assisté à une évolution majeure de l&amp;rsquo;approche réglementaire de l&amp;rsquo;Union européenne concernant l&amp;rsquo;Internet et les technologies personnelles. De nombreuses propositions ont vu le jour, certaines d&amp;rsquo;entre elles suscitant de vives inquiétudes quant à leur applicabilité, leur efficacité et leur respect des droits fondamentaux.&lt;/p&gt;
&lt;p&gt;Je suis ici pour souligner qu&amp;rsquo;une grande partie de l&amp;rsquo;élan qui sous-tend cette législation est mal informée. Ce n&amp;rsquo;est pas parce que je veux moins de réglementation, parce que je déteste les règles, ou quoi que ce soit de ce genre. C&amp;rsquo;est simplement parce que, du point de vue d&amp;rsquo;un expert technique en sécurité numérique et en cryptographie, la France essaie de réglementer l&amp;rsquo;ère numérique d&amp;rsquo;une manière qui me fait penser qu&amp;rsquo;elle réglementerait également les voitures en excès de vitesse en exigeant que toutes les roues soient dorénavant en béton.&lt;/p&gt;
&lt;p&gt;Mais plus que tout, je tiens à souligner l&amp;rsquo;importance cruciale d&amp;rsquo;élargir le débat : je ne suis pas ici pour critiquer juste pour critiquer. Je ne suis pas là pour faire un récital de poésie sur les &amp;ldquo;droits de l&amp;rsquo;homme&amp;rdquo; sans tenir compte des situations graves et souvent très laides auxquelles des professionnels comme par exemple monsieur le général de la gendarmerie doivent faire face en permanence. Je suis ici parce que l&amp;rsquo;establishment français aborde parfois certains sujets profondément importants avec ce que l&amp;rsquo;on peut, en toute justice, décrire comme des réactions de panique ou une simple paresse intellectuelle, et qui, par conséquent, ne font qu&amp;rsquo;affaiblir la confiance du public dans les institutions de l&amp;rsquo;État, tout en ne contribuant guère à résoudre les questions de sécurité et d&amp;rsquo;ordre public.&lt;/p&gt;
&lt;h3 id=&#34;droit-au-chiffrement&#34;&gt;Droit au chiffrement&lt;/h3&gt;
&lt;p&gt;Le chiffrement est aujourd&amp;rsquo;hui un droit fondamental. Il est essentiel pour garantir la confidentialité des communications et protéger les citoyens contre la surveillance de masse. Chiffrer ses communications est une pratique courante qui garantit que nos échanges ne soient lus que par leurs destinataires légitimes. C&amp;rsquo;est une extension de notre droit à la vie privée, protégé par l&amp;rsquo;article 8 de la Convention européenne des droits de l&amp;rsquo;homme. Cette pratique est adoptée par diverses professions, des militants aux journalistes, en passant par les avocats et les médecins, et est popularisée par des applications comme WhatsApp ou Signal.&lt;/p&gt;
&lt;p&gt;Cependant, dans une affaire récente dans laquelle 7 personnes ont été mises en examen pour « association de malfaiteurs terroristes » en décembre 2020, La DGSI a publié un communiqué dans lequel elle attribue l&amp;rsquo;utilisation de Tor, une technologie de navigation privée sur Internet, comme étant un signe de &amp;ldquo;comportements clandestins&amp;rdquo;. Un juge chargé de l&amp;rsquo;affaire a en outre déclaré que l&amp;rsquo;utilisation de l&amp;rsquo;application de messagerie populaire Signal, ainsi que l&amp;rsquo;utilisation d&amp;rsquo;ordinateurs portables ou de téléphones chiffrés, constituaient des motifs de comportement suspect et potentiellement criminel.&lt;/p&gt;
&lt;p&gt;Après leurs arrestations, les mis en examen sont, comme noté par La Quadrature du Net, systématiquement questionnés sur leur utilisation des outils de chiffrement et sommés de se justifier : « Utilisez-vous des messageries cryptées comme WhatsApp, Signal, Telegram, ou ProtonMail? », « Pour vos données personnelles, utilisez-vous un système de chiffrement ? », « Pourquoi utilisez-vous ce genre d’applications de chiffrement et d’anonymisation sur internet ? ». Le lien supposé entre chiffrement et criminalité est clair: au total, on dénombre plus de 150 questions liées aux pratiques numériques.&lt;/p&gt;
&lt;p&gt;Il est extrêmement important de noter ce qui suit :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Signal est une application de messagerie qui compte des millions d&amp;rsquo;utilisateurs dans le monde et qui a été largement financée par les fondateurs de WhatsApp. La technologie de chiffrement de Signal est exactement la même que celle utilisée par WhatsApp. Des milliards de personnes utilisent quotidiennement Signal et WhatsApp.&lt;/li&gt;
&lt;li&gt;Tous les iPhone, téléphones Samsung et téléphones Google chiffrent par défaut toutes les données stockées localement. Tous les ordinateurs portables Mac sont livrés avec une fonction de chiffrement prête à l&amp;rsquo;emploi, appelée FileVault, et tous les ordinateurs portables Windows de qualité professionnelle sont livrés avec une fonction de chiffrement de disque équivalente, appelée BitLocker.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Il est très important que tout le monde ici se rende compte que ce genre de réflexion, de la part des législateurs, ne fait que signaler aux citoyens français que les institutions gouvernementales sont incompétentes et qu&amp;rsquo;elles ne sont pas dignes de confiance. Aucun criminel n&amp;rsquo;est arrêté de cette manière, aucune sécurité n&amp;rsquo;est appliquée de cette manière. C&amp;rsquo;est un spectacle qui réduit la confiance du public dans les institutions que beaucoup d&amp;rsquo;entre vous représentent ici, et qui ne présente aucun avantage. Ces actions semblent suggérer que la protection de la vie privée est synonyme de comportement suspect ou clandestin. Cette perspective est non seulement erronée, mais elle est également dangereuse, car elle risque de dissuader les citoyens d&amp;rsquo;utiliser des outils essentiels pour protéger leurs données personnelles.&lt;/p&gt;
&lt;p&gt;Le plus ironique (et le plus drôle), c&amp;rsquo;est que la plupart des cybercriminels n&amp;rsquo;ont pas vraiment besoin de se préoccuper d’utiliser le chiffrement pour commencer ! Une grande partie de la cybercriminalité en France aujourd&amp;rsquo;hui est en fait réalisée par le biais de choses comme ces faux SMS que tout le monde ici a sûrement reçus, qui vous demandent de payer une fausse amende ou de suivre un faux colis Amazon. Beaucoup de ces SMS proviennent de centres de données en Moldavie, et certains pourraient en fait faire partie d&amp;rsquo;initiatives parrainées par des États étrangers pour saper la cybersécurité civile française à grande échelle.&lt;/p&gt;
&lt;h3 id=&#34;projet-de-loi-sren&#34;&gt;Projet de loi SREN&lt;/h3&gt;
&lt;p&gt;Le Projet de loi visant à sécuriser et réguler l’espace numérique (dit SREN) est un autre exemple frappant des défis auxquels nous sommes confrontés en matière de réglementation de l&amp;rsquo;Internet. Ce projet de loi, qui vise à réguler les contenus en ligne, semble ignorer la réalité complexe et nuancée de l&amp;rsquo;espace numérique. Par exemple, il est proposé d&amp;rsquo;obliger les plateformes à conserver les messages éphémères échangés, une mesure qui pourrait avoir des implications profondes pour la vie privée des utilisateurs.&lt;/p&gt;
&lt;p&gt;De plus, la proposition du projet de loi SREN d&amp;rsquo;intégrer un code de censure directement dans les navigateurs web est particulièrement alarmante. Mandater l&amp;rsquo;intégration d&amp;rsquo;un code spécifique dans tous les principaux navigateurs web est une idée qui, à première vue, peut sembler séduisante pour certains régulateurs souhaitant exercer un contrôle sur l&amp;rsquo;espace numérique. Cependant, une telle proposition est non seulement techniquement irréalisable, mais elle est aussi profondément problématique sur le plan éthique et pratique. Les navigateurs web, en tant que portails d&amp;rsquo;accès à l&amp;rsquo;Internet, sont conçus pour être neutres, offrant aux utilisateurs la liberté de naviguer et d&amp;rsquo;accéder à l&amp;rsquo;information sans entrave. Imposer un code gouvernemental dans ces navigateurs reviendrait à compromettre cette neutralité fondamentale, transformant des outils d&amp;rsquo;accès libre en instruments de surveillance ou de censure.&lt;/p&gt;
&lt;p&gt;De plus, l&amp;rsquo;idée de mandater un code dans tous les navigateurs majeurs sous-estime la diversité et la complexité de l&amp;rsquo;écosystème des navigateurs. Avec une multitude de navigateurs disponibles, allant des géants comme Google Chrome, Mozilla Firefox, Microsoft Edge ou Apple Safari à des alternatives plus spécialisées et open-source, l&amp;rsquo;application uniforme d&amp;rsquo;un tel mandat serait un cauchemar logistique. Sans parler des implications en matière de sécurité : introduire un code gouvernemental pourrait créer des vulnérabilités, exposant potentiellement des millions d&amp;rsquo;utilisateurs à des risques.&lt;/p&gt;
&lt;p&gt;De plus, l&amp;rsquo;histoire nous a montré qu&amp;rsquo;il est très difficile pour une autorité centrale et établie de décider avec autorité de ce qui est une menace et de ce qui ne l&amp;rsquo;est pas. Nous avons vu ce même type d&amp;rsquo;autorité, tout récemment encore, déclarer que Signal et WhatsApp étaient des indicateurs de menace. Qui décide quel type d&amp;rsquo;expression est si menaçant que nous devons modifier tous les navigateurs web pour l&amp;rsquo;interdire purement et simplement ?&lt;/p&gt;
&lt;p&gt;L&amp;rsquo;approche actuelle du gouvernement en matière de réglementation semble être axée sur la censure et l&amp;rsquo;autoritarisme. Sur le plan éthique, une telle initiative ouvrirait la porte à un glissement dangereux. Aujourd&amp;rsquo;hui, il pourrait s&amp;rsquo;agir d&amp;rsquo;un code pour des raisons de sécurité ou de réglementation, mais demain, qu&amp;rsquo;est-ce qui empêcherait l&amp;rsquo;extension de ces mandats à des fins plus autoritaires ou restrictives ? Les responsables des institutions ici présentes me paraissent largement érudits et raisonnables ; mais le citoyen n&amp;rsquo;a aucune garantie que vos successeurs partageront le même état d&amp;rsquo;esprit. Une fois que le précédent est établi, il devient plus facile d&amp;rsquo;étendre et d&amp;rsquo;abuser de ce pouvoir, menant potentiellement à une censure accrue, à une surveillance de masse et à une érosion de la confiance des utilisateurs dans les outils numériques qu&amp;rsquo;ils utilisent au quotidien.&lt;/p&gt;
&lt;p&gt;Aucun pays, jusqu&amp;rsquo;à présent, n&amp;rsquo;a tenté d&amp;rsquo;imposer l&amp;rsquo;intégration d&amp;rsquo;un code de censure ou de surveillance contrôlé par le gouvernement dans tous les principaux navigateurs web. C&amp;rsquo;est une démarche sans précédent qui soulève des questions fondamentales sur la liberté d&amp;rsquo;expression, la vie privée et la confiance dans l&amp;rsquo;espace numérique. La France doit se demander si elle souhaite véritablement être le premier pays de l&amp;rsquo;Union européenne, voire le premier au monde, à emprunter cette voie.&lt;/p&gt;
&lt;p&gt;Il est essentiel de se rappeler que la censure, sous quelque forme que ce soit, est une atteinte à la liberté d&amp;rsquo;expression. Permettez-moi de préciser ici que je comprends la priorité que beaucoup peuvent raisonnablement accorder aux questions d&amp;rsquo;ordre social, de cohésion sociale. Ma riposte serait que les discours violents sont probablement le symptôme d&amp;rsquo;un manque d&amp;rsquo;ordre social, et non la cause. Si nous admettons que c&amp;rsquo;est le cas, c&amp;rsquo;est à la cause qu&amp;rsquo;il faut s&amp;rsquo;attaquer, et non au symptôme.&lt;/p&gt;
&lt;h3 id=&#34;surveillance-des-smartphones&#34;&gt;Surveillance des smartphones&lt;/h3&gt;
&lt;p&gt;L’article 3 du projet de loi dite “d’orientation et de programmation du ministère de la Justice”, récemment discuté en France, vise à légitimiser la transformation des smartphones en outils de surveillance, Il s&amp;rsquo;agit d&amp;rsquo;un pouvoir dont les forces de l&amp;rsquo;ordre disposent déjà en France, mais cette loi le légitime davantage, en permettant aux forces de l&amp;rsquo;ordre de transformer à distance n&amp;rsquo;importe quel smartphone en dispositif d&amp;rsquo;espionnage en prenant le contrôle de sa géolocalisation, de son appareil photo et de son microphone grâce à l&amp;rsquo;exploitation de bogues logiciels et en piratant essentiellement le téléphone.&lt;/p&gt;
&lt;p&gt;Je ne sais pas si monsieur le général de la gendarmerie est encore dans l&amp;rsquo;assistance, mais si c&amp;rsquo;est le cas, il est probablement agacé que je n&amp;rsquo;aie pas mentionné que cette capacité existe déjà (même si sous une forme moins juridiquement encadrée), à quel point elle sera contrôlée et restreinte par les juges, ou le fait qu&amp;rsquo;elle ne sera utilisée que dans des cas de sécurité extrêmement graves, qu’elle serait autorisée seulement pour les infractions punies d’au moins cinq ans d’emprisonnement, ou encore qu&amp;rsquo;elle nécessite un mandat valable pendant 15 jours et renouvelable une seule fois. Voilà, tout est sur la table.&lt;/p&gt;
&lt;p&gt;Le problème, c&amp;rsquo;est qu&amp;rsquo;à chaque fois que nous parlons des risques de mesures comme celle-ci, nous parlons de la question d&amp;rsquo;un nouveau précédent judiciaire. Personnellement, je peux dire sincèrement que j’ai eu l&amp;rsquo;impression ce matin que mon général semble être un chef mesuré et responsable. Mais ce n&amp;rsquo;est pas lui qui me préoccupe : ce qui m&amp;rsquo;inquiète, c&amp;rsquo;est les personnes qui pourrait lui succéder dans les années à venir. Ces personnes, mesurée ou carrément autoritaire, seraient armés des nouveaux précédents judiciaires qui sont accordés grâce à ces lois. C&amp;rsquo;est, simplement, trop de pouvoir à donner à l’état en perpétuité.&lt;/p&gt;
&lt;p&gt;De plus, cette mesure pourrait avoir des conséquences imprévues en matière de sécurité. Si notre gouvernement peut activer à distance les appareils électroniques, cela signifie qu&amp;rsquo;il existe une porte dérobée ou une vulnérabilité qui permet cette action. Ces vulnérabilités pourraient être exploitées par des acteurs malveillants, tels que des pirates informatiques, pour accéder aux informations des utilisateurs ou pour commettre d&amp;rsquo;autres actes malveillants. Au lieu de renforcer la sécurité, une telle mesure pourrait la compromettre davantage. Et je suis désolé de dire que je ne crois tout simplement pas que la France dispose d&amp;rsquo;un avantage en matière de cybersécurité qui puisse empêcher, par exemple, la Chine d&amp;rsquo;exploiter ces scénarios pour espionner les citoyens français. En clair, si la France découvre une vulnérabilité de type &amp;ldquo;zero-day&amp;rdquo;, la Chine l&amp;rsquo;a probablement déjà stockée pour une semaine. Il est donc préférable de corriger autant de failles que possible, en les rendant impuissantes pour toutes les parties, plutôt que de les stocker comme des armes.&lt;/p&gt;
&lt;p&gt;Aujourd&amp;rsquo;hui, posséder un smartphone n&amp;rsquo;est plus un simple luxe ou un gadget technologique ; c&amp;rsquo;est devenu une nécessité absolue pour la survie dans notre société moderne. Ces appareils sont bien plus que de simples téléphones. Ils sont essentiels pour travailler, étudier, gérer nos finances, accéder à des services de santé, et même pour des tâches aussi basiques que faire ses courses ou utiliser les transports en commun. Dans de nombreux cas, ne pas avoir de smartphone équivaut à être coupé du monde moderne, à être désavantagé socialement, économiquement et même culturellement.&lt;/p&gt;
&lt;p&gt;Transformer les smartphones, qui sont devenus des extensions de nous-mêmes, en outils de surveillance omniprésents, c&amp;rsquo;est forcer les citoyens à choisir entre participer à la société moderne et protéger leurs droits les plus élémentaires.&lt;/p&gt;
&lt;h3 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Il est apparent que la France semble aujourd&amp;rsquo;hui s&amp;rsquo;égarer dans un labyrinthe de propositions législatives malavisées et potentiellement liberticides. L&amp;rsquo;élément le plus stupéfiant dans cette situation est qu&amp;rsquo;il existe des preuves accablantes de compétences réelles à des postes d&amp;rsquo;autorité. Puisque c&amp;rsquo;est le cas, pourquoi nous retrouvons-nous continuellement avec des propositions législatives qui semblent fondamentalement mal interpréter l&amp;rsquo;approche de la cybersécurité ? Pourquoi l&amp;rsquo;approche est-elle si inévitablement maladroite et directement contraire à la façon dont la technologie fonctionne de nos jours ?&lt;/p&gt;
&lt;p&gt;Ma position est que ces mesures ne contribuent que très peu à l&amp;rsquo;ordre social, tout en contribuant fortement à l&amp;rsquo;érosion de la confiance dans nos institutions et dans la manière dont elles choisissent de relever les défis de l&amp;rsquo;ère numérique. Il est urgent que la France change de cap. Il est temps d&amp;rsquo;ouvrir le débat, d&amp;rsquo;écouter les experts, de construire une législation efficace, respectueuse des droits fondamentaux et à la hauteur des enjeux du XXIe siècle. Si nous ne le faisons pas, nous risquons de laisser en héritage une France affaiblie, divisée et en décalage avec son temps. Et cela, aucun citoyen français ne devrait l&amp;rsquo;accepter.&lt;/p&gt;
&lt;/div&gt;
</content:encoded>
      <category>Announcement</category>
      <category>Policy</category>
      
    </item>
    
    <item>
      <title>Native Labs: Audit Report</title>
      <link>https://symbolic.software/blog/2023-08-30-nativelabs/</link>
      <pubDate>Wed, 30 Aug 2023 00:00:00 &#43;0000</pubDate>
      <guid>https://symbolic.software/blog/2023-08-30-nativelabs/</guid>
      <description>Our extensive audit for the Native Labs smart contracts.</description>
      <content:encoded>&lt;p&gt;At Symbolic Software, we recently concluded an extensive audit of the &lt;a href=&#34;https://native.org&#34;&gt;Native Labs&lt;/a&gt; smart contracts. We&amp;rsquo;re excited to share our findings and offer our perspectives on the strengths and areas for improvement.&lt;/p&gt;
&lt;p&gt;Native Labs offers a decentralized exchange system that focuses on operational efficiency, code quality, interoperability, on-chain and off-chain transactions, liquidity models, and user experience. Our evaluations aimed to present a holistic view of the smart contracts&amp;rsquo; functionality, efficiency, and usability.&lt;/p&gt;
&lt;h3 id=&#34;audit-methodology-and-focus-areas&#34;&gt;Audit Methodology and Focus Areas&lt;/h3&gt;
&lt;p&gt;Our audit covered the following aspects:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Performance Evaluation&lt;/strong&gt;: We assessed the operational efficiency, with emphasis on gas usage, scalability, and transaction speed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Code Quality Inspection&lt;/strong&gt;: This centered around code readability, maintainability, and best practices.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Interoperability Review&lt;/strong&gt;: We examined the integration capabilities with both internal and third-party entities.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;On-chain and Off-chain Analysis&lt;/strong&gt;: We investigated the handling of transactions, ensuring their accuracy, security, and effectiveness.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Evaluation of Liquidity Models&lt;/strong&gt;: We delved into the liquidity models provided by Native.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;User Experience Audit&lt;/strong&gt;: We gauged how the smart contracts impact the overall user experience.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;key-findings-and-recommendations&#34;&gt;Key Findings and Recommendations&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Performance&lt;/strong&gt;: The smart contracts showcased sufficient efficiency, with optimized gas consumption leading to reduced transaction costs. However, given the evolving Ethereum gas landscape, continuous monitoring and optimization are suggested.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Code Quality&lt;/strong&gt;: Despite some inconsistencies in coding style, the comprehensive documentation provided by the Native team played a pivotal role in our understanding of the codebase. We recommend the adoption of a consistent coding style and possibly leveraging a linter.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Interoperability&lt;/strong&gt;: The smart contracts demonstrated impressive interoperability capabilities, especially with significant platforms like PancakeSwap, Uniswap v3, and 1inch.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;On-chain and Off-chain Operations&lt;/strong&gt;: The system effectively handles both transaction types. We advise the team to consistently review these operations, particularly in light of potential changes in the blockchain environment.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Liquidity Models&lt;/strong&gt;: Native&amp;rsquo;s models proved efficient and user-centric, although we emphasize the need to educate users on potential risks like impermanent loss.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;User Experience&lt;/strong&gt;: The contracts excel in accessibility, transparency, and ease of use. Regular user feedback will be invaluable to further enhance the user experience.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;read-the-report&#34;&gt;Read the Report&lt;/h3&gt;
&lt;p&gt;Our comprehensive audit reveals that the Native Labs smart contracts are developed meticulously with a clear focus on user experience, performance, and security. While there are areas for improvement, the overarching impression is positive. These insights should provide users and stakeholders with a more profound understanding of the platform&amp;rsquo;s integrity and reliability.&lt;/p&gt;
&lt;p&gt;To dive deeper into our analysis and findings, you can &lt;a href=&#34;https://symbolic.software/pdf/nat-001.pdf&#34;&gt;download the full audit report&lt;/a&gt;.&lt;/p&gt;
</content:encoded>
      <category>Security</category>
      <category>Smart Contracts</category>
      
    </item>
    
    <item>
      <title>One Year of Verifpal</title>
      <link>https://symbolic.software/blog/2020-09-02-verifpaloneyear/</link>
      <pubDate>Wed, 02 Sep 2020 00:00:00 &#43;0000</pubDate>
      <guid>https://symbolic.software/blog/2020-09-02-verifpaloneyear/</guid>
      <description>Understanding Verifpal&#39;s relationship with cryptographic protocol security.</description>
      <content:encoded>&lt;p&gt;Last week, Verifpal, our cryptographic protocol modeling and analysis framework, celebrated its one-year anniversary. In its first year, Verifpal has made some significant achievements.&lt;/p&gt;
&lt;h3 id=&#34;improvements-to-usability-features-and-reliability&#34;&gt;Improvements to Usability, Features, and Reliability&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;First, Verifpal has improved its relationship with users.&lt;/strong&gt; Verifpal was &lt;a href=&#34;https://nlnet.nl/news/2020/20200611-SecureVideochatVerifpal.html&#34;&gt;part of Zoom&amp;rsquo;s effort&lt;/a&gt; to modernize its cryptography. &lt;a href=&#34;https://www.youtube.com/watch?v=it_hJkVU-UA&#34;&gt;Verifpal for Visual Studio Code&lt;/a&gt; and &lt;a href=&#34;https://verifhub.verifpal.com&#34;&gt;VerifHub&lt;/a&gt; allowed engineers to model and collaborate on cryptographic protocols more easily than ever. And &lt;a href=&#34;https://twitter.com/verifpal/status/1285662686894850050&#34;&gt;better attack traces&lt;/a&gt; allowed for easier to understand analysis results.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://symbolic.software/images/blog/EWtWLhcXsAEwUpV.jpeg&#34; alt=&#34;Verifpal for Visual Studio Code allows visualizing and analyzing Verifpal models straight inside Visual Studio Code, with easy access to documentation for primitives and other functionality by hovering with the cursor.&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34; width=&#34;3578&#34; height=&#34;2152&#34;&gt;
&lt;/p&gt;
&lt;p&gt;Verifpal for Visual Studio Code allows visualizing and analyzing Verifpal models straight inside Visual Studio Code, with easy access to documentation for primitives and other functionality by hovering with the cursor.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Second, Verifpal obtained new features that allow it to capture more of a protocol&amp;rsquo;s workings.&lt;/strong&gt; Verifpal gained the ability to model protocols where users can potentially input &lt;strong&gt;weak or strong passwords&lt;/strong&gt;. This is very useful for modeling protocols that include user-provided passwords at points, and is a great way to separate guessable passwords from material that can actually securely be used as an encryption key of some kind. &lt;strong&gt;New primitives&lt;/strong&gt; were introduced, allowing the usage of &lt;a href=&#34;https://github.com/symbolicsoft/verifpal/commit/fbc4d7372c0fe7df484d7331f045ff5710a63d37&#34;&gt;ring signatures&lt;/a&gt;, public key encryption, &lt;a href=&#34;https://github.com/symbolicsoft/verifpal/commit/7467e8abb274778a1b668bed46d5ee67b8574478&#34;&gt;Shamir secret sharing&lt;/a&gt; and &lt;a href=&#34;https://github.com/symbolicsoft/verifpal/commit/d2261c6b0daddbdccee9ef72e06604ede463d405&#34;&gt;blind signatures&lt;/a&gt;. The &lt;em&gt;leaks&lt;/em&gt; keyword now exists, allowing principals to leak constants to the attacker without necessarily sending them to another principal. &lt;strong&gt;&lt;a href=&#34;https://github.com/symbolicsoft/verifpal/commit/221b85db613c6634069f323f84d21c89454db6db&#34;&gt;Phases&lt;/a&gt;&lt;/strong&gt; are an exciting new feature that allows Verifpal to reliably model post-compromise security properties such as forward secrecy or future secrecy.  &lt;strong&gt;&lt;a href=&#34;https://github.com/symbolicsoft/verifpal/commit/780dc774226390eb8183c7ccfa61ed83a9d26e56&#34;&gt;Query preconditions&lt;/a&gt;&lt;/strong&gt; allow for illustrating relationships between different Verifpal queries on a protocol. And dozens of small improvements and changes were made to Verifpal across its application stack.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Finally, and most importantly, Verifpal improved its analysis strength and reliability.&lt;/strong&gt; As documented in a previous post, Verifpal&amp;rsquo;s formal semantics and passive attacker analysis are now &lt;a href=&#34;https://symbolic.software/blog/2020-04-14-queries/&#34;&gt;fully modeled in the Coq theorem prover&lt;/a&gt;. While this does not include active attacker analysis methodology, it does indicate that we can capture Verifpal&amp;rsquo;s semantics elegantly in existing formalism methods as well as a basic component of its analysis methodology. Our &lt;a href=&#34;https://verifpal.com/paper&#34;&gt;revised Verifpal paper&lt;/a&gt; goes into further detail on the Verifpal active attacker analysis methodology and describes formally how primitives are defined, deconstructed and analyzed in Verifpal. Furthermore, valuable feedback was received from over a dozen Verifpal community members that allowed detecting and fixing analysis errors, misleading analysis results and that pushed us to more concretely define the meaning and function of Verifpal queries such as freshness and authentication. These discussions occurred on the &lt;a href=&#34;https://verifpal.com/list&#34;&gt;Verifpal Mailing List&lt;/a&gt;, the &lt;a href=&#34;https://verifpal.com/discord&#34;&gt;Verifpal Discord&lt;/a&gt; as well as via private feedback.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://symbolic.software/images/blog/image.webp&#34; alt=&#34;&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34; width=&#34;1023&#34; height=&#34;738&#34;&gt;
&lt;/p&gt;
&lt;p&gt;Verifpal&amp;rsquo;s paper was revised to go into further detail regarding the analysis methodology, formal semantics and the limits of Verifpal analysis. &lt;em&gt;PrimitiveSpecs&lt;/em&gt; were introduced in order to standardize how primitives are defined and how their semantics operate within Verifpal analysis, i.e. via the following operations:&lt;br&gt;
• Decompose. Given a primitive’s output and a defined subset of its inputs, reveal one of its inputs. (Given ENC(k, m) and k, reveal m).&lt;br&gt;
• Recompose. Given a subset of a primitive’s outputs, reveal one of its inputs. (Given a, b, reveal x if a ,b,_ = SHAMIR_SPLIT(x)).&lt;br&gt;
• Rewrite. Given a matching defined pattern within a primitive’s inputs, rewrite the primitive expression itself into a logical subset of its inputs. (Given DEC(k, ENC(k, m)), rewrite the entire expression DEC(k, ENC(k, m)) to m).&lt;br&gt;
• Rebuild. Given a primitive whose inputs are all the outputs of some same other primitive, rewrite the primitive expression itself into a logical subset of its inputs. (Given SHAMIR_JOIN(a, b) where a, b, c = SHAMIR_SPLIT(x), rewrite the entire expression SHAMIR_JOIN(a, b) to x).&lt;/p&gt;
&lt;h2 id=&#34;understanding-verifpals-purpose-in-the-protocol-security-space&#34;&gt;Understanding Verifpal&amp;rsquo;s Purpose in the Protocol Security Space&lt;/h2&gt;
&lt;p&gt;Given Verifpal&amp;rsquo;s progress over its first year, it becomes ever more important to clearly answer the question: &lt;em&gt;&amp;ldquo;Where is Verifpal positioned within the protocol security space in comparison to other tools?&amp;rdquo;&lt;/em&gt; What is Verifpal able to do for me, the protocol analyst, engineer or applied cryptography practitioner?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Verifpal appears to be evolving towards being software best suited for designing, modeling, analyzing and testing cryptographic protocols.&lt;/strong&gt; Notably, this means that Verifpal is &lt;em&gt;not&lt;/em&gt; likely to progress in the direction where it functions as a producer of &lt;em&gt;protocol security proofs&lt;/em&gt;, nor does our experience lead us to believe that this is a space where more contributions are likely to be useful or impactful. This latter functionality will likely remain the jurisdiction of tools such as &lt;a href=&#34;https://tamarin-prover.github.io/&#34;&gt;Tamarin&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Verifpal&amp;rsquo;s goal is to focus on the real-world priority of exceeding these tools in terms of how much time and effort it will take for the engineer or practitioner to obtain a meaningful model of their protocol that gives them meaningful analysis answers.&lt;/strong&gt; However, the compromises to ease of use and modeling/analysis rapidity required for Verifpal to offer full proving capabilities are deemed too great given Verifpal&amp;rsquo;s design and functionality goals, and therefore will likely never be adopted fully into its functionality.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://symbolic.software/images/blog/image-1.png&#34; alt=&#34;&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34; width=&#34;903&#34; height=&#34;912&#34;&gt;
&lt;/p&gt;
&lt;p&gt;For example, Verifpal&amp;rsquo;s strategy for dealing with state space explosion imposes limitations on the &lt;em&gt;completeness&lt;/em&gt; of its analysis, but this appears to only affect protocols that are unlikely to ever appear in real-world scenarios, while also granting Verifpal greater analysis speed and likelihood for analysis termination than other tools.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;In deciding Verifpal&amp;rsquo;s priorities, we slam the brakes at the moment where the learning curve, effort and analysis cost begin to have strongly diminishing returns for the user.&lt;/strong&gt; Our bet is that this path forward for Verifpal will lead to a hugely more substantial impact for engineers and practitioners than traditional automated proof modeling tools. In the example above, we can see how Verifpal makes compromises in analysis completeness that preclude its ability to output full proofs but that greatly increase the likelihood of analysis termination (a significant problem for tools such as &lt;a href=&#34;https://prosecco.gforge.inria.fr/personal/bblanche/proverif/&#34;&gt;ProVerif&lt;/a&gt;) without having an apparently significant impact on the analysis of real-world, non-Ivory-Tower protocols.&lt;/p&gt;
&lt;p&gt;Ergo, Verifpal&amp;rsquo;s main responsibility is to straddle a balance between soundness/reliability and ease of use/relevance to real-world practitioners. The way we approach this responsibility is by making sure that Verifpal&amp;rsquo;s semantics are formally specified, that its analysis methodology is amply documented, and that its code is easy to understand. &lt;a href=&#34;https://web.archive.org/web/20230322041020/https://source.symbolic.software/verifpal/verifpal/-/blob/master/internal/libcoq/libcoqtemplate.v&#34;&gt;Verifpal&amp;rsquo;s formal semantics in Coq&lt;/a&gt;, the analysis methodology details in the &lt;a href=&#34;https://verifpal.com/paper&#34;&gt;Verifpal paper&lt;/a&gt; documented and easy-to-understand &lt;a href=&#34;https://github.com/symbolicsoft/verifpal&#34;&gt;Go codebase&lt;/a&gt; aim to fulfill this purpose.&lt;/p&gt;
&lt;p&gt;Simultaneously, Verifpal utilizes this formally specified base in order to maintain the development of a language and framework that is idiomatic to the extreme. The Verifpal language is meant to illustrate protocols close to how one may describe them in an informal conversation, while still being precise and expressive enough for formal modeling. Verifpal avoids user error by not allowing users to define their own cryptographic primitives. Instead, it comes with built-in cryptographic functions which nevertheless are defined and which operate according to a formally specified standard with concrete semantics. All of this is coupled with a high standard for documentation, accessibility and support in popular workflows and code editors (via Verifpal for Visual Studio Code and VerifHub).&lt;/p&gt;
&lt;p&gt;So far, Verifpal&amp;rsquo;s chosen path has allowed it to provide value in the &lt;a href=&#34;https://symbolic.software/blog/2020-04-05-dp3t/&#34;&gt;quick modeling and correct analysis of the DP-3T pandemic contact tracing protocol as it was being specified&lt;/a&gt;, and has helped Zoom, Monocypher &lt;a href=&#34;https://verifpal.com/getinvolved/&#34;&gt;and others&lt;/a&gt; achieve quick protocol modeling and prototyping insight as they developed their protocols, by offering a methodology and framework that allowed results to be obtained in hours (sometimes minutes!) instead of weeks. We hope to continue making exciting developments in Verifpal well into 2021, and can&amp;rsquo;t wait to see how the community makes use of it.&lt;/p&gt;
</content:encoded>
      <category>Software</category>
      <category>Verifpal</category>
      <category>Formal Verification</category>
      
    </item>
    
    <item>
      <title>Better Queries for Verifpal</title>
      <link>https://symbolic.software/blog/2020-04-14-queries/</link>
      <pubDate>Tue, 14 Apr 2020 00:00:00 &#43;0000</pubDate>
      <guid>https://symbolic.software/blog/2020-04-14-queries/</guid>
      <description>Towards new queries, automated model translation and formalized semantics in Verifpal.</description>
      <content:encoded>&lt;p&gt;For the past few weeks, repeated requests have appeared for Verifpal to provide more analysis features in the way of &lt;strong&gt;detecting replay attacks&lt;/strong&gt; and also in supporting the analysis of the &lt;strong&gt;unlinkability&lt;/strong&gt; of values in protocols.&lt;/p&gt;
&lt;p&gt;Friedrich Wiemer pointed out that Verifpal was not flexible enough to &lt;a href=&#34;https://lists.symbolic.software/pipermail/verifpal/2020/000208.html&#34;&gt;detect replay attacks in Needham-Schroeder&lt;/a&gt;, while Anders N. also &lt;a href=&#34;https://lists.symbolic.software/pipermail/verifpal/2020/000218.html&#34;&gt;requested replay attack detection&lt;/a&gt; recently in relation to other protocols. Others have also made such a request. Unlinkability became a pertinent feature especially after our attempts to &lt;a href=&#34;https://symbolic.software/blog/2020-04-05-dp3t/&#34;&gt;sketch a model of the DP-3T pandemic-tracing protocol in Verifpal&lt;/a&gt; last week.&lt;/p&gt;
&lt;p&gt;Furthermore, there have been many requests since Verifpal&amp;rsquo;s inception to allow for more in-depth analysis of Verifpal models using tools that have existed for decades longer, such as &lt;a href=&#34;https://proverif.inria.fr&#34;&gt;ProVerif&lt;/a&gt;, &lt;a href=&#34;https://cryptoverif.inria.fr/&#34;&gt;CryptoVerif&lt;/a&gt;, &lt;a href=&#34;https://tamarin-prover.github.io/&#34;&gt;Tamarin&lt;/a&gt; and &lt;a href=&#34;https://coq.inria.fr&#34;&gt;Coq&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;As such, &lt;strong&gt;Verifpal 0.12.0&lt;/strong&gt; will introduce many new features that will allow it to expand and mature in ways meant to address the above:&lt;/p&gt;
&lt;h2 id=&#34;freshness-queries-in-verifpal&#34;&gt;Freshness Queries in Verifpal&lt;/h2&gt;
&lt;p&gt;Freshness queries are useful for detecting replay attacks. In passive attacker mode, a freshness query will check whether a value is &amp;ldquo;fresh&amp;rdquo; between sessions (i.e. if it has at least one composing element that is generated, non-static). In active attacker mode, it will check whether a value can be rendered &amp;ldquo;non-fresh” (i.e. static between sessions) and subsequently successfully used between sessions. An example of freshness queries is available in &lt;code&gt;examples/test/freshness.vp&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Freshness queries are currently fairly well-defined in themselves, but it is unclear still whether they are flexible enough to detect the kinds of replay attacks that are envisioned by our users. As such, further discussion is welcome on the &lt;a href=&#34;https://verifpal.com/list&#34;&gt;Verifpal Mailing List&lt;/a&gt; in order to understand how replay attack detection in Verifpal can be further improved.&lt;/p&gt;
&lt;h2 id=&#34;unlinkability-queries-in-verifpal&#34;&gt;Unlinkability Queries in Verifpal&lt;/h2&gt;
&lt;p&gt;Protocols such as DP-3T, voting protocols and RFID-based protocols posit an &lt;strong&gt;“unlinkability”&lt;/strong&gt; security property on some of their components or processes. Definitions for unlinkability vary wildly despite the best efforts of researchers. Complicating matters further, the actually-formalized definitions for unlinkability tend to pertain to &lt;em&gt;processes&lt;/em&gt; and not to &lt;em&gt;values&lt;/em&gt; like the definition used by, for example, DP-3T.&lt;/p&gt;
&lt;p&gt;In DP-3T, definitions for unlinkability are suggested to go along these lines: &lt;em&gt;&amp;ldquo;for two observed ephIDs, the adversary cannot distinguish between a game in which they belong to the same user and a game in which they belong to two different users.&lt;/em&gt;&amp;rdquo; (definition elucidated with &lt;a href=&#34;https://www.benjaminlipp.de/&#34;&gt;Benjamin Lipp&lt;/a&gt;; the DP-3T whitepaper itself &lt;a href=&#34;https://github.com/DP-3T/documents/issues/103&#34;&gt;does not currently have one&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Hirschi, Delaune et al have a couple of papers exploring the formalization of unlinkability: &lt;a href=&#34;https://arxiv.org/abs/1710.02049&#34;&gt;&lt;em&gt;A Method for Unbounded Verification of Privacy-type Properties&lt;/em&gt;&lt;/a&gt;, followed by &lt;em&gt;&lt;a href=&#34;https://hal.archives-ouvertes.fr/hal-02459984/document&#34;&gt;A Method for Proving Unlinkability of Stateful Protocols&lt;/a&gt;&lt;/em&gt;. In these papers, the unlinkability of processes is defined as the satisfaction of two properties: &lt;em&gt;“frame opacity”&lt;/em&gt; and &lt;em&gt;“well-authentication”:&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Frame opacity:&lt;/strong&gt; &lt;em&gt;&amp;ldquo;Intuitively, this condition aims to prevent attacks in which, for some possible behaviour of the attacker, there exists a relation between messages that leaks information about the involved agents. Practically speaking, this condition requires that any reachable frame must be statically equivalent to an idealised frame that does not depend on identity parameters. A very simple way to obtain an idealisation of a frame is to replace each output message by a fresh nonce. In that case, if the real frame and the idealised frame are statically equivalent, it is obvious that the attacker cannot learn anything by analysing the relations between the messages, since there is no relation between disctinct fresh nonces. Nevertheless, it is not satisfying because too restrictive as e.g. a pair is distinguishable from a nonce.”&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Well-authentication:&lt;/strong&gt; &lt;em&gt;&amp;ldquo;The idea behind this second condition is to avoid that the outcome of conditionals leaks information about identities to the attacker. To do so, we require that whenever a conditional (let or lookup) is positively evaluated, the corresponding agent is having an honest interaction with another participant. In practice, protocols often have some conditionals for which the attacker already knows the outcome: these safe conditionals can (and must) be excluded from our condition.”&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Frame opacity seems clear enough, and well-authentication seems to be the combination of the traditional notion of “strong authentication” or “mutual authentication”, combined with an assumption of honest protocol-following on behalf of the other party (i.e. the protocol &amp;ldquo;executing correctly&amp;rdquo;).&lt;/p&gt;
&lt;p&gt;Based on the above, Verifpal 0.12.0 will introduce experimental support for a notion of unlinkability in Verifpal that centers more on values than on processes. For example, one could write &lt;code&gt;unlinkability? a, b&lt;/code&gt; as a query to test whether &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt; are unlinkable from one another. Unlinkability checks for the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;First, Verifpal checks to see if &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt; satisfy freshness. If they do not, the query fails. Similarly to regular freshness queries, if an attacker can coerce a value to be non-fresh across sessions, then it is non-fresh and the query fails.&lt;/li&gt;
&lt;li&gt;If &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt; both satisfy freshness, Verifpal then checks to see if the attacker can determine them as being the output of the same primitive (for example, the first and second output of the same HKDF construction with the same inputs.) Of course, &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt; can indeed be the outputs of that HKDF and be unlinkable; unless the attacker is able to reconstruct that same HKDF primitive and thereby use it to determine that both values are the outputs of it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Further testing of this definition of unlinkability in Verifpal is &lt;strong&gt;strongly welcome&lt;/strong&gt;, as &lt;strong&gt;it is very much&lt;/strong&gt; &lt;strong&gt;expected&lt;/strong&gt; that it will be further elucidated since it is highly doubtful that it covers all cases on unlinkability. Again, &lt;a href=&#34;https://verifpal.com/list&#34;&gt;Verifpal Mailing List&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;formalizing-verifpal-in-coq-and-experimental-proverifcoq-model-generation&#34;&gt;Formalizing Verifpal in Coq, and Experimental ProVerif/Coq Model Generation&lt;/h2&gt;
&lt;p&gt;Work has been ongoing on allowing for the automated translation of Verifpal models to the languages of other tools for those interested in carrying out further analysis:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;ProVerif:&lt;/strong&gt; &lt;a href=&#34;https://proverif.inria.fr&#34;&gt;ProVerif&lt;/a&gt; is the verification software that inspired Verifpal and which was written by my former thesis co-advisor Bruno Blanchet. ProVerif has existed for around 20 years and can perform a more diverse set of analyses than Verifpal despite both softwares belonging to the same category of verifiers (symbolic model protocol verifiers).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Coq&lt;/strong&gt;: &lt;a href=&#34;https://coq.inria.fr&#34;&gt;Coq&lt;/a&gt; is a full-fat theorem prover that can do way more than handle the description of protocols. &lt;a href=&#34;https://georgio.xyz&#34;&gt;Georgio Nicolas&lt;/a&gt; has been handling Coq translations, with &lt;a href=&#34;https://scholar.google.com.au/citations?user=o8zCZV4AAAAJ&amp;amp;hl=en&#34;&gt;Mukesh Tiwari&lt;/a&gt; recently joining our effort and lending a hand (welcome, Mukesh!). &lt;strong&gt;Currently, we have fully formalized the semantics of Verifpal in Coq&lt;/strong&gt;, and we are working on also formalizing the attacker and verification logic in Coq as well for both active and passive attackers. &lt;strong&gt;At that point, we will have fully implemented Verifpal in Coq.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Experimental ProVerif and Coq model generation will be available for testing in Verifpal 0.12.0. Type &lt;code&gt;verifpal help&lt;/code&gt; to find out more on how to use Verifpal to generate these models.&lt;/p&gt;
&lt;p&gt;Once support for ProVerif and Coq in Verifpal has matured, and especially once attacker and verification logic is captured in Coq on top of the current semantics, another more detailed blog post will follow.&lt;/p&gt;
&lt;h2 id=&#34;looking-towards-a-bright-2020&#34;&gt;Looking Towards a Bright 2020&lt;/h2&gt;
&lt;p&gt;Finally, Verifpal 0.12.0 will also support phases in passive attacker mode, and not just in active attacker mode as before.&lt;/p&gt;
&lt;p&gt;These developments in Verifpal are incredibly exciting. They come hot on the heels of many other new features and improvements that have been introduced so far in 2020:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;New primitives&lt;/strong&gt;, such as &lt;a href=&#34;https://lists.symbolic.software/pipermail/verifpal/2020/000126.html&#34;&gt;public-key encryption&lt;/a&gt;, &lt;a href=&#34;https://lists.symbolic.software/pipermail/verifpal/2020/000127.html&#34;&gt;Shamir secret sharing&lt;/a&gt; and &lt;a href=&#34;https://lists.symbolic.software/pipermail/verifpal/2020/000161.html&#34;&gt;ring signatures&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-threaded,&lt;/strong&gt; &lt;a href=&#34;https://lists.symbolic.software/pipermail/verifpal/2020/000128.html&#34;&gt;concurrent analysis&lt;/a&gt; using Go&amp;rsquo;s excellent multithreading support. Put that gaming PC to work!&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Password-hashing&lt;/strong&gt; modeling, which allows for &lt;a href=&#34;https://lists.symbolic.software/pipermail/verifpal/2020/000132.html&#34;&gt;models to capture&lt;/a&gt; when a weak/guessable/bruteforceable password is used as, for example, an encryption or signing key.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;leaks&lt;/code&gt; expression&lt;/strong&gt;, which allows simulating the leaking of a value on the network without sending it as a message.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&#34;https://github.com/symbolicsoft/verifpal/commit/780dc774226390eb8183c7ccfa61ed83a9d26e56&#34;&gt;Queries as preconditions&lt;/a&gt;&lt;/strong&gt; for messages.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All of this is in line with our announced &lt;a href=&#34;https://lists.symbolic.software/pipermail/verifpal/2020/000129.html&#34;&gt;Verifpal 2020 plans&lt;/a&gt;, and the progress on these plans so far has been highly encouraging.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&#34;https://lists.symbolic.software/pipermail/verifpal/2020/000134.html&#34;&gt;list of projects using Verifpal&lt;/a&gt; continues to grow, and none of this would have been possible without the help of our supporters:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;NLNet Foundation:&lt;/strong&gt; Verifpal is sponsored by the &lt;a href=&#34;https://nlnet.nl/&#34;&gt;NLNet Foundation&lt;/a&gt;. Funding was provided through the &lt;a href=&#34;https://www.ngi.eu/about/ngi-zero/&#34;&gt;&lt;em&gt;NGI0 Privacy Enhancing Technologies Fund&lt;/em&gt;&lt;/a&gt;, a fund established by NLnet with financial support from the European Commission’s &lt;em&gt;Next Generation Internet&lt;/em&gt; program, under the aegis of DG Communications Networks, Content and Technology under grant agreement №825310.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cure53:&lt;/strong&gt; Verifpal is also supported by &lt;a href=&#34;https://cure53.de/&#34;&gt;Cure53&lt;/a&gt;, a Berlin-based security auditing firm which provides penetration testing for online services, security analysis and architectural advice for security and cryptographic applications, training, consulting, incident management and malware analysis.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And, of course, users like you. Thank you!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Get involved&lt;/strong&gt; in Verifpal discussions today, either through:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&#34;https://verifpal.com/list&#34;&gt;Verifpal Mailing List&lt;/a&gt;&lt;/strong&gt;, for more long-form and serious discussions, or,&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&#34;https://verifpal.com/discord&#34;&gt;Verifpal Discord&lt;/a&gt;&lt;/strong&gt;, where you can chat with Verifpal contributors, users and enthusiasts!&lt;/li&gt;
&lt;/ul&gt;
</content:encoded>
      <category>Software</category>
      <category>Research</category>
      <category>Verifpal</category>
      <category>Formal Verification</category>
      
    </item>
    
    <item>
      <title>Modeling DP-3T With Verifpal</title>
      <link>https://symbolic.software/blog/2020-04-05-dp3t/</link>
      <pubDate>Sun, 05 Apr 2020 00:00:00 &#43;0000</pubDate>
      <guid>https://symbolic.software/blog/2020-04-05-dp3t/</guid>
      <description>How Verifpal sped up the formal modeling efforts for a new pandemic-tracing Protocol.</description>
      <content:encoded>&lt;p&gt;Last week, numerous researchers published the timely fruits of their recent collaboration to provide a proximity-tracking solution that can help during pandemics while still being &lt;em&gt;privacy-preserving&lt;/em&gt;: the result, &lt;a href=&#34;https://github.com/DP-3T/documents&#34;&gt;&lt;strong&gt;Decentralized Privacy-Preserving Proximity Tracing&lt;/strong&gt; (DP-3T)&lt;/a&gt;, provides a promising first step in bringing real-world cryptography into the effort to combat the COVID-19 pandemic. As mentioned in Troncoso et al.&amp;rsquo;s whitepaper, the goal of DP-3T is:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&amp;quot;&amp;hellip;to simplify and accelerate the process of identifying people who have been in contact with an infected person, thus providing a technological foundation to help slow the spread of the SARS-CoV-2 virus. The system aims to minimise privacy and security risks for individuals and communities and guarantee the highest level of data protection.&amp;quot;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Troncoso et al, Decentralized Privacy-Preserving Proximity Tracing&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;In support of this project, we at Symbolic Software decided to test out &lt;a href=&#34;https://verifpal.com&#34;&gt;Verifpal&lt;/a&gt;, our open source protocol verification framework, to see how well its promises of easier and more accessible protocol modeling and verification can hold up in the face of a new and ambitious protocol that targets a novel use case.&lt;/p&gt;
&lt;p&gt;In this post, we will go through DP-3T while modeling it using the Verifpal Language, and conclude by comparing our results to the designers&amp;rsquo; security goals.&lt;/p&gt;
&lt;h3 id=&#34;modeling-dp-3t&#34;&gt;Modeling DP-3T&lt;/h3&gt;
&lt;p&gt;To demonstrate DP-3T, we will assume that the principals participating in this simulation are the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A population of 3 individuals: Alice, Bob, and Charlie, each of them possessing a smartphone: &lt;code&gt;SmartphoneA&lt;/code&gt;, &lt;code&gt;SmartphoneB&lt;/code&gt;, and &lt;code&gt;SmartphoneC&lt;/code&gt; respectively;&lt;/li&gt;
&lt;li&gt;A Healthcare Authority serving this population;&lt;/li&gt;
&lt;li&gt;A Backend Server, that individuals can communicate with to obtain daily information.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After installing &lt;a href=&#34;https://verifpal.com&#34;&gt;Verifpal&lt;/a&gt;, we can start by creating a new model called &amp;ldquo;dp-3t.vp&amp;rdquo; in which we begin by defining an attacker which matches with our security model. In this case we will be using an active attacker (i.e. one that can not only monitor but also intercept and overwrite unprotected messages on the network):&lt;/p&gt;
&lt;div class=&#34;highlight&#34; data-lang=&#34;verifpal&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-verifpal&#34; data-lang=&#34;verifpal&#34;&gt;&lt;span class=&#34;vp-kw&#34;&gt;attacker&lt;/span&gt;[&lt;span class=&#34;vp-kw&#34;&gt;active&lt;/span&gt;]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We then proceed to illustrate our model as a sequence of days in which DP-3T is in operation within the lifecycle of a pandemic.&lt;/p&gt;
&lt;h3 id=&#34;day-0-setup-phase&#34;&gt;Day 0 (setup phase)&lt;/h3&gt;
&lt;p&gt;We assume that no new individuals were diagnosed with the disease on Day 0 of using DP-3T. This means that the Healthcare Authority and the Backend Server will not act at this stage and we can simply ignore them for now.&lt;/p&gt;
&lt;p&gt;The DP-3T specification states that every principal, when first joining the system, should generate a random secret key (&lt;code&gt;SK&lt;/code&gt;) to be used for one day only. For every &lt;code&gt;SK&lt;/code&gt; value, and the knowledge of a public &amp;ldquo;&lt;em&gt;broadcast key&lt;/em&gt;&amp;rdquo; value, principals should compute multiple &lt;em&gt;Unique Ephemeral ID&lt;/em&gt; values (&lt;code&gt;EphID&lt;/code&gt;) using a combination of a PRG and a PRF. The method of generating &lt;code&gt;EphID&lt;/code&gt; is analogous with the &lt;code&gt;HKDF&lt;/code&gt; function from Verifpal. We could add the following lines of code to our file in order to model Alice&amp;rsquo;s &lt;code&gt;SmartphoneA&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34; data-lang=&#34;verifpal&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-verifpal&#34; data-lang=&#34;verifpal&#34;&gt;&lt;span class=&#34;vp-cmt&#34;&gt;// All lines that start with &amp;#34;//&amp;#34; are treated as comments and ignored by Verifpal&lt;/span&gt;
&lt;span class=&#34;vp-cmt&#34;&gt;// A principal block looks like the following&lt;/span&gt;
&lt;span class=&#34;vp-kw&#34;&gt;principal&lt;/span&gt; &lt;span class=&#34;vp-prin&#34;&gt;SmartphoneA&lt;/span&gt;[
    &lt;span class=&#34;vp-cmt&#34;&gt;// In the line below we state that Alice knows the public BroadcastKey&lt;/span&gt;
    
    &lt;span class=&#34;vp-kw&#34;&gt;knows&lt;/span&gt; &lt;span class=&#34;vp-kw&#34;&gt;public&lt;/span&gt; BroadcastKey
    
    &lt;span class=&#34;vp-cmt&#34;&gt;// SK is going to be a secret random value&lt;/span&gt;
    &lt;span class=&#34;vp-cmt&#34;&gt;// To define it we use the &amp;#34;generates&amp;#34; keyword&lt;/span&gt;
    &lt;span class=&#34;vp-cmt&#34;&gt;// We will use the following template for SK variable names&lt;/span&gt;
    &lt;span class=&#34;vp-cmt&#34;&gt;// SK[day number][principal initial]&lt;/span&gt;
    
    &lt;span class=&#34;vp-kw&#34;&gt;generates&lt;/span&gt; SK0A
    
    &lt;span class=&#34;vp-cmt&#34;&gt;// We will use the following template for EphID variable names&lt;/span&gt;
    &lt;span class=&#34;vp-cmt&#34;&gt;// EphID[day number][value number][principal initial]&lt;/span&gt;
    
    EphID00A, EphID01A, EphID02A = &lt;span class=&#34;vp-prim&#34;&gt;HKDF&lt;/span&gt;(&lt;span class=&#34;vp-lit&#34;&gt;nil&lt;/span&gt;, SK0A, BroadcastKey)
]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The same thing goes for Bob, and Charlie:&lt;/p&gt;
&lt;div class=&#34;highlight&#34; data-lang=&#34;verifpal&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-verifpal&#34; data-lang=&#34;verifpal&#34;&gt;&lt;span class=&#34;vp-kw&#34;&gt;principal&lt;/span&gt; &lt;span class=&#34;vp-prin&#34;&gt;SmartphoneB&lt;/span&gt;[
    &lt;span class=&#34;vp-kw&#34;&gt;knows&lt;/span&gt; &lt;span class=&#34;vp-kw&#34;&gt;public&lt;/span&gt; BroadcastKey
    &lt;span class=&#34;vp-kw&#34;&gt;generates&lt;/span&gt; SK0B
    EphID00B, EphID01B, EphID02B = &lt;span class=&#34;vp-prim&#34;&gt;HKDF&lt;/span&gt;(&lt;span class=&#34;vp-lit&#34;&gt;nil&lt;/span&gt;, SK0B, BroadcastKey)
]

&lt;span class=&#34;vp-kw&#34;&gt;principal&lt;/span&gt; &lt;span class=&#34;vp-prin&#34;&gt;SmartphoneC&lt;/span&gt;[
    &lt;span class=&#34;vp-kw&#34;&gt;knows&lt;/span&gt; &lt;span class=&#34;vp-kw&#34;&gt;public&lt;/span&gt; BroadcastKey
    &lt;span class=&#34;vp-kw&#34;&gt;generates&lt;/span&gt; SK0C
    EphID00C, EphID01C, EphID02C = &lt;span class=&#34;vp-prim&#34;&gt;HKDF&lt;/span&gt;(&lt;span class=&#34;vp-lit&#34;&gt;nil&lt;/span&gt;, SK0C, BroadcastKey)
]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Whenever two principals would come into physical proximity of each other, they would automatically exchange &lt;code&gt;EphIDs&lt;/code&gt;. Once a principal uses an &lt;code&gt;EphID&lt;/code&gt; value, they discard it and use another one when performing an exchange with another principal.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s imagine that Alice and Bob came into contact. It would mean that Alice sent &lt;code&gt;EphID00A&lt;/code&gt; in a message to Bob and that Bob sent &lt;code&gt;EphID00B&lt;/code&gt; to Alice:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://symbolic.software/images/blog/axb.webp&#34; alt=&#34;&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34; width=&#34;1024&#34; height=&#34;81&#34;&gt;
&lt;/p&gt;
&lt;p&gt;Here is how the above message exchange is modeled in Verifpal:&lt;/p&gt;
&lt;div class=&#34;highlight&#34; data-lang=&#34;verifpal&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-verifpal&#34; data-lang=&#34;verifpal&#34;&gt;&lt;span class=&#34;vp-cmt&#34;&gt;// Sender -&amp;gt; Recipient : Name of Value&lt;/span&gt;

&lt;span class=&#34;vp-prin&#34;&gt;SmartphoneA&lt;/span&gt; &lt;span class=&#34;vp-arrow&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;vp-prin&#34;&gt;SmartphoneB&lt;/span&gt;: EphID00A
&lt;span class=&#34;vp-prin&#34;&gt;SmartphoneB&lt;/span&gt; &lt;span class=&#34;vp-arrow&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;vp-prin&#34;&gt;SmartphoneA&lt;/span&gt;: EphID00B&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now, let&amp;rsquo;s say that in the conclusion of Day 0, Bob sits behind Charlie in the bus:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://symbolic.software/images/blog/bxc.webp&#34; alt=&#34;&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34; width=&#34;1024&#34; height=&#34;81&#34;&gt;
&lt;/p&gt;
&lt;p&gt;Modeling this is equally simple:&lt;/p&gt;
&lt;div class=&#34;highlight&#34; data-lang=&#34;verifpal&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-verifpal&#34; data-lang=&#34;verifpal&#34;&gt;&lt;span class=&#34;vp-prin&#34;&gt;SmartphoneC&lt;/span&gt; &lt;span class=&#34;vp-arrow&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;vp-prin&#34;&gt;SmartphoneB&lt;/span&gt;: EphID01C
&lt;span class=&#34;vp-prin&#34;&gt;SmartphoneB&lt;/span&gt; &lt;span class=&#34;vp-arrow&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;vp-prin&#34;&gt;SmartphoneC&lt;/span&gt;: EphID01B&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&#34;day-1&#34;&gt;Day 1&lt;/h3&gt;
&lt;p&gt;On Day 1, the Backend Server will automatically publish the &lt;code&gt;SK&lt;/code&gt; values of people who were infected to the members of the general population. These values were previously unpublished and thus were private and only known by their generators and the server.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://symbolic.software/images/blog/d1s.webp&#34; alt=&#34;&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34; width=&#34;1024&#34; height=&#34;104&#34;&gt;
&lt;/p&gt;
&lt;div class=&#34;highlight&#34; data-lang=&#34;verifpal&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-verifpal&#34; data-lang=&#34;verifpal&#34;&gt;&lt;span class=&#34;vp-cmt&#34;&gt;// A server is just like any other principal&lt;/span&gt;

&lt;span class=&#34;vp-kw&#34;&gt;principal&lt;/span&gt; &lt;span class=&#34;vp-prin&#34;&gt;BackendServer&lt;/span&gt;[
    &lt;span class=&#34;vp-cmt&#34;&gt;// Let&amp;#39;s assume that infectedPatients0 is the list of infected patients on day 0&lt;/span&gt;
    &lt;span class=&#34;vp-kw&#34;&gt;knows&lt;/span&gt; &lt;span class=&#34;vp-kw&#34;&gt;private&lt;/span&gt; infectedPatients0
]

&lt;span class=&#34;vp-prin&#34;&gt;BackendServer&lt;/span&gt; &lt;span class=&#34;vp-arrow&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;vp-prin&#34;&gt;SmartphoneA&lt;/span&gt;: infectedPatients0
&lt;span class=&#34;vp-prin&#34;&gt;BackendServer&lt;/span&gt; &lt;span class=&#34;vp-arrow&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;vp-prin&#34;&gt;SmartphoneB&lt;/span&gt;: infectedPatients0
&lt;span class=&#34;vp-prin&#34;&gt;BackendServer&lt;/span&gt; &lt;span class=&#34;vp-arrow&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;vp-prin&#34;&gt;SmartphoneC&lt;/span&gt;: infectedPatients0&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We should not forget that every day starting from Day 1, DP-3T mandates that principals will generate new &lt;code&gt;SK&lt;/code&gt; values. The new value will be equal to the hash of the &lt;code&gt;SK&lt;/code&gt; value from the day before. Principals will also generate &lt;code&gt;EphIDs&lt;/code&gt; just like before.&lt;/p&gt;
&lt;div class=&#34;highlight&#34; data-lang=&#34;verifpal&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-verifpal&#34; data-lang=&#34;verifpal&#34;&gt;&lt;span class=&#34;vp-kw&#34;&gt;principal&lt;/span&gt; &lt;span class=&#34;vp-prin&#34;&gt;SmartphoneA&lt;/span&gt;[
    SK1A = &lt;span class=&#34;vp-prim&#34;&gt;HASH&lt;/span&gt;(SK0A)
    EphID10A, EphID11A, EphID12A = &lt;span class=&#34;vp-prim&#34;&gt;HKDF&lt;/span&gt;(&lt;span class=&#34;vp-lit&#34;&gt;nil&lt;/span&gt;, SK1A, BroadcastKey)
]

&lt;span class=&#34;vp-kw&#34;&gt;principal&lt;/span&gt; &lt;span class=&#34;vp-prin&#34;&gt;SmartphoneB&lt;/span&gt;[
    SK1B = &lt;span class=&#34;vp-prim&#34;&gt;HASH&lt;/span&gt;(SK0B)
    EphID10B, EphID11B, EphID12B = &lt;span class=&#34;vp-prim&#34;&gt;HKDF&lt;/span&gt;(&lt;span class=&#34;vp-lit&#34;&gt;nil&lt;/span&gt;, SK1B, BroadcastKey)
]

&lt;span class=&#34;vp-kw&#34;&gt;principal&lt;/span&gt; &lt;span class=&#34;vp-prin&#34;&gt;SmartphoneC&lt;/span&gt;[
    SK1C = &lt;span class=&#34;vp-prim&#34;&gt;HASH&lt;/span&gt;(SK0C)
    EphID10C, EphID11C, EphID12C = &lt;span class=&#34;vp-prim&#34;&gt;HKDF&lt;/span&gt;(&lt;span class=&#34;vp-lit&#34;&gt;nil&lt;/span&gt;, SK1C, BroadcastKey)
]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Thankfully, Alice, Bob and Charlie are committed to self-confinement and have stayed at home, so they did not exchange &lt;code&gt;EphIDs&lt;/code&gt; with anyone.&lt;/p&gt;
&lt;h3 id=&#34;day-2&#34;&gt;Day 2&lt;/h3&gt;
&lt;p&gt;On Day 2, a similar sequence of events takes place. Since it is sufficient to define the values that we will need later on in our model, we will just define a block for Alice.&lt;/p&gt;
&lt;div class=&#34;highlight&#34; data-lang=&#34;verifpal&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-verifpal&#34; data-lang=&#34;verifpal&#34;&gt;&lt;span class=&#34;vp-kw&#34;&gt;principal&lt;/span&gt; &lt;span class=&#34;vp-prin&#34;&gt;SmartphoneA&lt;/span&gt;[
    SK2A = &lt;span class=&#34;vp-prim&#34;&gt;HASH&lt;/span&gt;(SK1A)
    EphID20A, EphID21A, EphID22A = &lt;span class=&#34;vp-prim&#34;&gt;HKDF&lt;/span&gt;(&lt;span class=&#34;vp-lit&#34;&gt;nil&lt;/span&gt;, SK2A, BroadcastKey)
]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&#34;fast-forward-to-day-15&#34;&gt;Fast-Forward to Day 15&lt;/h3&gt;
&lt;p&gt;Unfortunately, Alice tests positive for COVID-19. Since this breaks the routine that happened between Day 1 and Day 15, we will &lt;a href=&#34;https://lists.symbolic.software/pipermail/verifpal/2020/000135.html&#34;&gt;announce a new phase&lt;/a&gt; in our protocol model:&lt;/p&gt;
&lt;div class=&#34;highlight&#34; data-lang=&#34;verifpal&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-verifpal&#34; data-lang=&#34;verifpal&#34;&gt;&lt;span class=&#34;vp-kw&#34;&gt;phase&lt;/span&gt;[&lt;span class=&#34;vp-lit&#34;&gt;1&lt;/span&gt;]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Alice decides to announce her infection anonymously using DP-3T. This means that she will have to securely communicate &lt;code&gt;SK1A&lt;/code&gt; (her &lt;code&gt;SK&lt;/code&gt; value from 14 days ago) to the Backend Server, using a unique trigger token provided by the healthcare authority. Assuming that the Backend Server and the Healthcare Authority share a secure connection, and that a private key encryption key &lt;code&gt;ephemeral_sk&lt;/code&gt; has been exchanged off the wire by the Healthcare Authority, Alice, and the Backend Server, the Healthcare Authority will encrypt a freshly generated &lt;code&gt;triggerToken&lt;/code&gt; using &lt;code&gt;ephemeral_sk&lt;/code&gt; and send it to both Alice and the Backend Server.&lt;/p&gt;
&lt;div class=&#34;highlight&#34; data-lang=&#34;verifpal&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-verifpal&#34; data-lang=&#34;verifpal&#34;&gt;&lt;span class=&#34;vp-kw&#34;&gt;principal&lt;/span&gt; &lt;span class=&#34;vp-prin&#34;&gt;HealthCareAuthority&lt;/span&gt;[
    &lt;span class=&#34;vp-kw&#34;&gt;generates&lt;/span&gt; triggerToken
    &lt;span class=&#34;vp-kw&#34;&gt;knows&lt;/span&gt; &lt;span class=&#34;vp-kw&#34;&gt;private&lt;/span&gt; ephemeral_sk
    m1 = &lt;span class=&#34;vp-prim&#34;&gt;ENC&lt;/span&gt;(ephemeral_sk, triggerToken)
]

&lt;span class=&#34;vp-cmt&#34;&gt;// The brackets around m1 here mean that the value is guarded&lt;/span&gt;
&lt;span class=&#34;vp-cmt&#34;&gt;// ie: an active attacker cannot inject a value in its place&lt;/span&gt;

&lt;span class=&#34;vp-prin&#34;&gt;HealthCareAuthority&lt;/span&gt; &lt;span class=&#34;vp-arrow&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;vp-prin&#34;&gt;BackendServer&lt;/span&gt; : [m1]
&lt;span class=&#34;vp-prin&#34;&gt;HealthCareAuthority&lt;/span&gt; &lt;span class=&#34;vp-arrow&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;vp-prin&#34;&gt;SmartphoneA&lt;/span&gt; : m1&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then, Alice would have to use an AEAD cipher to encrypt &lt;code&gt;SK1A&lt;/code&gt; using &lt;code&gt;ephemeral_sk&lt;/code&gt; as the key and &lt;code&gt;triggerToken&lt;/code&gt; as additional data and send the output to the Backend Server. Note that Alice can only obtain &lt;code&gt;triggerToken&lt;/code&gt; after decrypting &lt;code&gt;m1&lt;/code&gt; using &lt;code&gt;ephemeral_sk&lt;/code&gt;.&lt;/p&gt;
&lt;div class=&#34;highlight&#34; data-lang=&#34;verifpal&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-verifpal&#34; data-lang=&#34;verifpal&#34;&gt;&lt;span class=&#34;vp-kw&#34;&gt;principal&lt;/span&gt; &lt;span class=&#34;vp-prin&#34;&gt;SmartphoneA&lt;/span&gt;[
    &lt;span class=&#34;vp-kw&#34;&gt;knows&lt;/span&gt; &lt;span class=&#34;vp-kw&#34;&gt;private&lt;/span&gt; ephemeral_sk
    m1_dec = &lt;span class=&#34;vp-prim&#34;&gt;DEC&lt;/span&gt;(ephemeral_sk, m1)
    m2 = &lt;span class=&#34;vp-prim&#34;&gt;AEAD_ENC&lt;/span&gt;(ephemeral_sk, SK1A, m1_dec)
]

&lt;span class=&#34;vp-prin&#34;&gt;SmartphoneA&lt;/span&gt; &lt;span class=&#34;vp-arrow&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;vp-prin&#34;&gt;BackendServer&lt;/span&gt;: m2&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The Backend Server will now have to decrypt &lt;code&gt;m1&lt;/code&gt; to receive the &lt;code&gt;triggerToken&lt;/code&gt; in the same way that Alice did, then attempt to decrypt &lt;code&gt;m2&lt;/code&gt;. If that decryption was successful, the server would obtain &lt;code&gt;SK1A&lt;/code&gt; and would be sure that the value came from Alice because it is only Alice who knows both &lt;code&gt;triggerToken&lt;/code&gt; and &lt;code&gt;SK1A&lt;/code&gt; at the same time as defined in the protocol.&lt;/p&gt;
&lt;p&gt;Finally, the Backend Server will add &lt;code&gt;SK1A&lt;/code&gt; to the list of infected patients previously defined, and then send this list to all of the individuals in this community.&lt;/p&gt;
&lt;div class=&#34;highlight&#34; data-lang=&#34;verifpal&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-verifpal&#34; data-lang=&#34;verifpal&#34;&gt;&lt;span class=&#34;vp-kw&#34;&gt;principal&lt;/span&gt; &lt;span class=&#34;vp-prin&#34;&gt;BackendServer&lt;/span&gt; [
    &lt;span class=&#34;vp-kw&#34;&gt;knows&lt;/span&gt; &lt;span class=&#34;vp-kw&#34;&gt;private&lt;/span&gt; ephemeral_sk
    m2_dec = &lt;span class=&#34;vp-prim&#34;&gt;AEAD_DEC&lt;/span&gt;(ephemeral_sk, m2, &lt;span class=&#34;vp-prim&#34;&gt;DEC&lt;/span&gt;(ephemeral_sk, m1))&lt;span class=&#34;vp-op&#34;&gt;?&lt;/span&gt;
    infectedPatients1 = &lt;span class=&#34;vp-prim&#34;&gt;CONCAT&lt;/span&gt;(infectedPatients0, m2_dec)
]

&lt;span class=&#34;vp-prin&#34;&gt;BackendServer&lt;/span&gt; &lt;span class=&#34;vp-arrow&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;vp-prin&#34;&gt;SmartphoneA&lt;/span&gt;: infectedPatients1
&lt;span class=&#34;vp-prin&#34;&gt;BackendServer&lt;/span&gt; &lt;span class=&#34;vp-arrow&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;vp-prin&#34;&gt;SmartphoneB&lt;/span&gt;: infectedPatients1
&lt;span class=&#34;vp-prin&#34;&gt;BackendServer&lt;/span&gt; &lt;span class=&#34;vp-arrow&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;vp-prin&#34;&gt;SmartphoneC&lt;/span&gt;: infectedPatients1&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Everything that happened in Day 15 can be summarized in the following diagram:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://symbolic.software/images/blog/sab.webp&#34; alt=&#34;&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34; width=&#34;1024&#34; height=&#34;220&#34;&gt;
&lt;/p&gt;
&lt;h3 id=&#34;queries&#34;&gt;Queries&lt;/h3&gt;
&lt;p&gt;Now, we may finally define the queries block, in which we ask Verifpal about the state of certain security guarantees that we expect from the protocol.&lt;/p&gt;
&lt;p&gt;Since &lt;code&gt;SK1A&lt;/code&gt; is now shared publicly, the DP-3T software running on anyone&amp;rsquo;s phone should be able to re-generate all &lt;code&gt;EphID&lt;/code&gt; values generated by the owner of &lt;code&gt;SK1A&lt;/code&gt; starting from 14 days prior to the day of diagnosis. These values would then be compared with the list of &lt;code&gt;EphIDs&lt;/code&gt; they have received. Everyone who came in contact with Alice will therefore be notified that they have exchanged &lt;code&gt;EphIDs&lt;/code&gt; with someone who has been diagnosed with the illness without revealing the identity of that person.&lt;/p&gt;
&lt;div class=&#34;highlight&#34; data-lang=&#34;verifpal&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-verifpal&#34; data-lang=&#34;verifpal&#34;&gt;&lt;span class=&#34;vp-kw&#34;&gt;queries&lt;/span&gt;[
    &lt;span class=&#34;vp-cmt&#34;&gt;// Would someone who shared a value 15 days before they got tested get flagged?&lt;/span&gt;
    &lt;span class=&#34;vp-cmt&#34;&gt;// ie in phase[0], before phase[1]&lt;/span&gt;
    &lt;span class=&#34;vp-kw&#34;&gt;confidentiality&lt;/span&gt;&lt;span class=&#34;vp-op&#34;&gt;?&lt;/span&gt; EphID02A
    &lt;span class=&#34;vp-cmt&#34;&gt;// Will people who came in contact with Alice be able to compute&lt;/span&gt;
        &lt;span class=&#34;vp-cmt&#34;&gt;// all of Alice&amp;#39;s EphIDs starting from Day 1&lt;/span&gt;
    &lt;span class=&#34;vp-kw&#34;&gt;confidentiality&lt;/span&gt;&lt;span class=&#34;vp-op&#34;&gt;?&lt;/span&gt; EphID10A
    &lt;span class=&#34;vp-kw&#34;&gt;confidentiality&lt;/span&gt;&lt;span class=&#34;vp-op&#34;&gt;?&lt;/span&gt; EphID11A
    &lt;span class=&#34;vp-kw&#34;&gt;confidentiality&lt;/span&gt;&lt;span class=&#34;vp-op&#34;&gt;?&lt;/span&gt; EphID12A
    &lt;span class=&#34;vp-kw&#34;&gt;confidentiality&lt;/span&gt;&lt;span class=&#34;vp-op&#34;&gt;?&lt;/span&gt; EphID20A
    &lt;span class=&#34;vp-kw&#34;&gt;confidentiality&lt;/span&gt;&lt;span class=&#34;vp-op&#34;&gt;?&lt;/span&gt; EphID21A
    &lt;span class=&#34;vp-kw&#34;&gt;confidentiality&lt;/span&gt;&lt;span class=&#34;vp-op&#34;&gt;?&lt;/span&gt; EphID22A
    &lt;span class=&#34;vp-cmt&#34;&gt;// Is the server able to Authenticate Alice as the sender of m2&lt;/span&gt;
    &lt;span class=&#34;vp-kw&#34;&gt;authentication&lt;/span&gt;&lt;span class=&#34;vp-op&#34;&gt;?&lt;/span&gt; &lt;span class=&#34;vp-prin&#34;&gt;SmartphoneA&lt;/span&gt; &lt;span class=&#34;vp-arrow&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;vp-prin&#34;&gt;BackendServer&lt;/span&gt;: m2
]&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The results of our initial modeling in Verifpal suggest to us the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No &lt;code&gt;EphIDs&lt;/code&gt; generated by Alice are known by any parties before Alice announces her illness.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;EphID02A&lt;/code&gt; remains confidential even after Alice declaring her illness. Note that it was generated 15 Days before Alice got tested.&lt;/li&gt;
&lt;li&gt;All of the following values &lt;code&gt;EphID10A&lt;/code&gt;, &lt;code&gt;EphID11A&lt;/code&gt;, &lt;code&gt;EphID12A&lt;/code&gt;, &lt;code&gt;EphID20A&lt;/code&gt;, &lt;code&gt;EphID21A&lt;/code&gt;, &lt;code&gt;EphID22A&lt;/code&gt; have been recoverable by an attacker in &lt;code&gt;phase[1]&lt;/code&gt; after Alice announces her illness.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These results come in line with what is expected from the protocol. We note that the security of communication channels between Healthcare Authorities, Backend Servers, and Individuals have not been defined, and we have placed our own hypothetical security conditions in order to focus on quickly sketching the DP-3T protocol. Further analysis will be required in order to better elucidate the extent of the obtained security guarantees.&lt;/p&gt;
&lt;h3 id=&#34;generating-models-for-further-analysis-in-proverif-and-coq&#34;&gt;Generating Models for Further Analysis in ProVerif and Coq&lt;/h3&gt;
&lt;p&gt;&lt;img src=&#34;https://symbolic.software/images/blog/euwfggmxyaetit9-1.webp&#34; alt=&#34;Verifpal generating ProVerif models live from the Verifpal model, through the Verifpal Visual Studio Code extension.&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34; width=&#34;1024&#34; height=&#34;576&#34;&gt;
&lt;/p&gt;
&lt;p&gt;Verifpal generating ProVerif models live from the Verifpal model, through the Verifpal Visual Studio Code extension.&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;re very excited to be working on automatically generating &lt;a href=&#34;https://proverif.inria.fr&#34;&gt;ProVerif&lt;/a&gt; and &lt;a href=&#34;https://coq.inria.fr&#34;&gt;Coq&lt;/a&gt; models directly from Verifpal models. The resulting ProVerif and Coq models will be human-readable and thus easily extensible, allowing for more profound protocol analysis and supplementing Verifpal&amp;rsquo;s insight with that of tools that have existed for more than two decades.&lt;/p&gt;
&lt;p&gt;In working on DP-3T, we were able to generate baseline ProVerif and Coq models after less than an hour of work on the Verifpal model itself. This is an immeasurably huge leap forward in terms of obtaining working material for the formal modeling and analysis of a novel protocol, and we are &lt;em&gt;very&lt;/em&gt; excited to post further updates as this new feature matures in Verifpal.&lt;/p&gt;
&lt;p&gt;ProVerif and Coq model generation is currently under development, and we expect a beta release to be ready by the end of April.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://symbolic.software/images/blog/verif-coq-test.webp&#34; alt=&#34;&#34; loading=&#34;lazy&#34; decoding=&#34;async&#34; width=&#34;724&#34; height=&#34;1023&#34;&gt;
&lt;/p&gt;
&lt;p&gt;Coming soon!&lt;/p&gt;
</content:encoded>
      <category>Software</category>
      <category>Research</category>
      <category>Verifpal</category>
      <category>Formal Verification</category>
      
    </item>
    
  </channel>
</rss>
