{"article":{"slug":"please-stop-hashing-the-public-key-first-or-ml-dsas-annoys-me","title":"Please stop hashing the public key first (or: ML-DSA's µ annoys me)","subtitle":null,"summary":"Erincandescent explains why signature schemes such as Ed25519 and ML-DSA hash the public key before the message, why that ordering blocks a useful optimisation for verifying one message against many candidate keys, and how the constructions could have been arranged differently.","content_type":"blog_post","language":"en","canonical_url":"https://blog.erincandescent.net/2026/10/mu-annoys-me/","author":{"name":"Erincandescent","url":null,"person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"blog.erincandescent.net","url":"https://blog.erincandescent.net/","listing_slug":null,"listing":null},"topics":[{"name":"Security","slug":"security","url":"https://listedarticles.com/topics/security"},{"name":"Programming","slug":"programming","url":"https://listedarticles.com/topics/programming"},{"name":"Research","slug":"research","url":"https://listedarticles.com/topics/research"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":784,"reading_minutes":3,"published_at":"2026-10-13T00:00:00.000Z","added_at":"2026-10-13T23:11:46.469Z","updated_at":"2026-10-13T23:11:46.469Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/please-stop-hashing-the-public-key-first-or-ml-dsas-annoys-me","markdown_url":"https://listedarticles.com/articles/please-stop-hashing-the-public-key-first-or-ml-dsas-annoys-me.md","example":false,"citation":"Erincandescent, blog.erincandescent.net. \"Please stop hashing the public key first (or: ML-DSA's µ annoys me).\" 13 Oct 2026. https://blog.erincandescent.net/2026/10/mu-annoys-me/ (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://blog.erincandescent.net/2026/10/mu-annoys-me/"},"body_markdown":"A goal one might have when designing a signing algorithm is to prevent signing a message one has not seen.\n\nSigning a message one hasn't seen might sound difficult at first, but [most](https://www.rfc-editor.org/info/rfc8017/#section-8.1) [classical](https://www.rfc-editor.org/info/rfc8017/#section-8.2) [signature](https://en.wikipedia.org/wiki/Digital_Signature_Algorithm) [algorithms](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm), as commonly used, sign a hash of the message, rather than the message itself. This kind of arrangement is necessitated by the fact that the underlying algorithms are only capable of signing messages of a limited size. You might not have seen the message, but you might have seen the hash of it, or another signature which encodes said hash. In many protocols, this lets you produce a valid signature on a message you've never seen. That's rarely a problem in practice, but it can be surprising, and it's a pitfall for the unsuspecting protocol designer.\n\nMore recent algorithms take steps to prevent this:\n\n- Ed25519 signs `SHA-512(R || PubK || M)` where R is functionally a random curve point, PubK is the public key, M is the message, and`||` is the concatenation operator\n- ML-DSA signs `SHAKE-256(SHAKE-256(PubK, 64) || 0x00 || len(ctx) || ctx || m, 64)` (where ctx is a 0 to 255 byte context string)\n\nThis can complicate your signing code, particularly when dealing with HSMs or smart cards, [but it doesn't require majorly reworking](https://keymaterial.net/2024/11/05/hashml-dsa-considered-harmful/) your signing process. Unfortunately, on the verification side things are less rosy.\n\n## The challenge\n\nAs a general rule of thumb, we should avoid touching the body of a signed message until we've verified the signature. Also as a general rule of thumb, cryptographic protocols where there are multiple signers put the signer's identity within said body (X.509, most JWT and CWT usages, SAML, I'm looking at all of you, but you're not the only ones).\n\nThis means the verification process ends up being\n\n1. Gingerly parse the message\n2. Dig through as much message as necessary to identify the signing key\n3. Actually verify the message\n4. Actually consume the message\n\nOften (JWT, CWT and SAML again), there's either no key identifier present in the signed message, or it's permitted for multiple signing keys to share a key identifier<sup>†</sup>. This means that, if the issuer has multiple keys, you may have to do multiple trial-verifications to work out if the message is actually valid. \n\nOne nice property that all of these protocols have (except XML Digital Signatures, but *nobody* thinks XML Digital Signatures were a good idea) is that they carry the signing method in a header. So, with a traditional signing algorithm, you have the option of\n\n1. Parsing the header\n2. Identify algorithm and hence the hash algorithm\n3. Hash the message\n4. Gingerly parse the message\n5. Dig through as much message as necessary to identify the signing key\n6. Actually verify the message\n7. Actually consume the message\n\nThis comes with two advantages:\n\n1. After step 3, you can throw away the raw message, reducing memory usage\n2. In step 6, you pay for the cost of verifying a signature for every attempted key rather than also the cost of re-hashing the message. In most cases messages are small and this cost is also small, but every little helps.\n\nUnfortunately, it has the slight flaw that it doesn't work with either Ed25519 or ML-DSA as defined.\n\nFor ML-DSA, it would have been quite easy to re-arrange matters. If we instead computed `SHAKE-256(0x00 || len(ctx) || ctx || m || SHAKE-256(PubK, 64), 64)` (i.e. we put the public key hash in *at the end*), then we can hold our SHAKE-256 context around and copy it for each trial verification. We'd end up keeping just over 200 bytes of SHAKE-256 context around (instead of 64 bytes of output), but that's not too bad. Unfortunately, that's not the choice that was made and we'll just have to deal with it.\n\nEd25519 is slightly more challenging. If we reversed the hash computation to be `SHA-512(M || R || PubK)` instead, it would be vulnerable to length extension attacks (for any `SHA-512(X)` you can extend it into `SHA-512(X || some predictable garbage || Y)` for any arbitrary Y). \n\nFixing that when Ed25519 was published in 2011 would be somewhat tricky, though the easiest option without truncating the hash is just to sign `SHA-512(SHA-512(\"ed25519 domain separation\" || M || R || PubK))`. \n\nSince we don't have a time machine, it's all a bit academic. In 2026 you can just avoid the problem by using SHA-3, BLAKE2, a truncated SHA-2 variant, or any other modern hash function which isn't vulnerable to length extension attacks.\n\nIn the meantime I'll just sigh and hope we can reduce the number of signatures we pass around, given they're really becoming rather large and expensive.\n\n† You may be thinking it somewhat defeats the purpose of having a key identifier if they aren't unique. I don't disagree!\n","body_html":"<p>A goal one might have when designing a signing algorithm is to prevent signing a message one has not seen.</p>\n<p>Signing a message one hasn&#39;t seen might sound difficult at first, but <a href=\"https://www.rfc-editor.org/info/rfc8017/#section-8.1\" rel=\"nofollow ugc noopener\">most</a> <a href=\"https://www.rfc-editor.org/info/rfc8017/#section-8.2\" rel=\"nofollow ugc noopener\">classical</a> <a href=\"https://en.wikipedia.org/wiki/Digital_Signature_Algorithm\" rel=\"nofollow ugc noopener\">signature</a> <a href=\"https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm\" rel=\"nofollow ugc noopener\">algorithms</a>, as commonly used, sign a hash of the message, rather than the message itself. This kind of arrangement is necessitated by the fact that the underlying algorithms are only capable of signing messages of a limited size. You might not have seen the message, but you might have seen the hash of it, or another signature which encodes said hash. In many protocols, this lets you produce a valid signature on a message you&#39;ve never seen. That&#39;s rarely a problem in practice, but it can be surprising, and it&#39;s a pitfall for the unsuspecting protocol designer.</p>\n<p>More recent algorithms take steps to prevent this:</p>\n<ul><li>Ed25519 signs <code>SHA-512(R || PubK || M)</code> where R is functionally a random curve point, PubK is the public key, M is the message, and<code>||</code> is the concatenation operator</li><li>ML-DSA signs <code>SHAKE-256(SHAKE-256(PubK, 64) || 0x00 || len(ctx) || ctx || m, 64)</code> (where ctx is a 0 to 255 byte context string)</li></ul>\n<p>This can complicate your signing code, particularly when dealing with HSMs or smart cards, <a href=\"https://keymaterial.net/2024/11/05/hashml-dsa-considered-harmful/\" rel=\"nofollow ugc noopener\">but it doesn&#39;t require majorly reworking</a> your signing process. Unfortunately, on the verification side things are less rosy.</p>\n<h2 id=\"the-challenge\">The challenge</h2>\n<p>As a general rule of thumb, we should avoid touching the body of a signed message until we&#39;ve verified the signature. Also as a general rule of thumb, cryptographic protocols where there are multiple signers put the signer&#39;s identity within said body (X.509, most JWT and CWT usages, SAML, I&#39;m looking at all of you, but you&#39;re not the only ones).</p>\n<p>This means the verification process ends up being</p>\n<ol><li>Gingerly parse the message</li><li>Dig through as much message as necessary to identify the signing key</li><li>Actually verify the message</li><li>Actually consume the message</li></ol>\n<p>Often (JWT, CWT and SAML again), there&#39;s either no key identifier present in the signed message, or it&#39;s permitted for multiple signing keys to share a key identifier&lt;sup&gt;†&lt;/sup&gt;. This means that, if the issuer has multiple keys, you may have to do multiple trial-verifications to work out if the message is actually valid. </p>\n<p>One nice property that all of these protocols have (except XML Digital Signatures, but <em>nobody</em> thinks XML Digital Signatures were a good idea) is that they carry the signing method in a header. So, with a traditional signing algorithm, you have the option of</p>\n<ol><li>Parsing the header</li><li>Identify algorithm and hence the hash algorithm</li><li>Hash the message</li><li>Gingerly parse the message</li><li>Dig through as much message as necessary to identify the signing key</li><li>Actually verify the message</li><li>Actually consume the message</li></ol>\n<p>This comes with two advantages:</p>\n<ol><li>After step 3, you can throw away the raw message, reducing memory usage</li><li>In step 6, you pay for the cost of verifying a signature for every attempted key rather than also the cost of re-hashing the message. In most cases messages are small and this cost is also small, but every little helps.</li></ol>\n<p>Unfortunately, it has the slight flaw that it doesn&#39;t work with either Ed25519 or ML-DSA as defined.</p>\n<p>For ML-DSA, it would have been quite easy to re-arrange matters. If we instead computed <code>SHAKE-256(0x00 || len(ctx) || ctx || m || SHAKE-256(PubK, 64), 64)</code> (i.e. we put the public key hash in <em>at the end</em>), then we can hold our SHAKE-256 context around and copy it for each trial verification. We&#39;d end up keeping just over 200 bytes of SHAKE-256 context around (instead of 64 bytes of output), but that&#39;s not too bad. Unfortunately, that&#39;s not the choice that was made and we&#39;ll just have to deal with it.</p>\n<p>Ed25519 is slightly more challenging. If we reversed the hash computation to be <code>SHA-512(M || R || PubK)</code> instead, it would be vulnerable to length extension attacks (for any <code>SHA-512(X)</code> you can extend it into <code>SHA-512(X || some predictable garbage || Y)</code> for any arbitrary Y). </p>\n<p>Fixing that when Ed25519 was published in 2011 would be somewhat tricky, though the easiest option without truncating the hash is just to sign <code>SHA-512(SHA-512(&quot;ed25519 domain separation&quot; || M || R || PubK))</code>. </p>\n<p>Since we don&#39;t have a time machine, it&#39;s all a bit academic. In 2026 you can just avoid the problem by using SHA-3, BLAKE2, a truncated SHA-2 variant, or any other modern hash function which isn&#39;t vulnerable to length extension attacks.</p>\n<p>In the meantime I&#39;ll just sigh and hope we can reduce the number of signatures we pass around, given they&#39;re really becoming rather large and expensive.</p>\n<p>† You may be thinking it somewhat defeats the purpose of having a key identifier if they aren&#39;t unique. I don&#39;t disagree!</p>","headings":[{"level":2,"text":"The challenge","id":"the-challenge"}]}}