fix(sdk-js): verify ML-DSA-65 signatures and align senderPub with wire-format-v1 spec#25
Merged
Merged
Conversation
Two related post-quantum bugs in GhostPipe: 1. _decrypt() built the message-to-verify but never called sig.verify(). Every signed blob was accepted regardless of signature validity, so the post-quantum authentication claim was effectively bypassed. 2. _encrypt() wrote the KEM public key into the wire-format senderPub field for signed blobs. The wire-format-v1 spec test vectors and the Python reference SDK both place the *signing* public key there so the receiver can verify the embedded signature without an out-of-band lookup. With the KEM pubkey there, no receiver could ever verify authenticity. Fix: - _encrypt now writes sig_pub into senderPub when sigId != 0x0000 (and keeps kem_pub for anonymous blobs as a stable opaque sender id). - _decrypt now actually calls sig.verify() and throws SignatureError on any failure or exception inside the verifier. Regression tests: - tampered-signature blob is rejected with SignatureError. - signed blob's senderPub is exactly 1952 bytes (ML-DSA-65 pubkey size), not 1184 (ML-KEM-768 pubkey size). The relay/ package-lock change is the automatic version-field sync npm performed on install; no dependency graph change.
ML-KEM-512 and ML-KEM-1024 each have a "rejects wrong ciphertext size" assertion in their standalone *.test.js files. ML-KEM-768 is tested in the consolidated impls.test.js, which had every other parity test but was missing this one. Add it for symmetry; the impl already validates.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two related post-quantum bugs in
sdk-js/index.js(the npm-publishedparamant-sdk@3.0.0"real post-quantum ML-KEM-768 + ML-DSA-65" SDK):CRITICAL —
_decrypt()never calledsig.verify(). The function built the message-to-verify into a localmsgvariable and stopped there. Every signed blob was accepted regardless of signature validity, so the post-quantum authentication claim was effectively bypassed.Wire-format / interop bug —
_encrypt()wrote the KEM pubkey intosenderPub. Thedocs/wire-format-v1.mdspec test vectors and the Python reference SDK both place the signing public key (1952 B for ML-DSA-65) in that field so the receiver can verify the embedded signature without an out-of-band lookup. With the KEM pubkey there (1184 B), no receiver could ever verify authenticity even after fix feat: Add thunderbird filelink extension #1.Fix
_encrypt:senderPub = sig_pubwhensigId != 0x0000; keepskem_pubfor anonymous blobs as a stable opaque sender identifier._decrypt: actually callssig.verify(parsed.signature, msg, parsed.senderPub)and throwsSignatureErroron failure or on any exception inside the verifier.ML-KEM-768 rejects wrong ciphertext sizeto match the standalone tests forML-KEM-512/ML-KEM-1024.Test plan
sdk-js: 44/44 (node --test test/*.test.js) — including 2 new regression tests:SignatureErrorsenderPub.length === 1952(ML-DSA-65 pubkey size), not 1184relay: 59/59 wire-format/registry/bootstrap/impls + 61/61 standalone impl testssdk-py: 36/36 — confirms wire-format compatibility with the Python reference SDKCompatibility
sigId=0x0000) is unchanged. Pre-fix anonymous blobs decrypt unchanged.senderPubsemantics now match the Python SDK and the spec test vectors. Any pre-fix signed blob produced byparamant-sdk@3.0.0-jswas already un-verifiable (verification was a no-op), so no real-world signed blob is invalidated by this change.Out of scope (documented but not touched)
crypto-wasm/Cargo.tomlml-kem 0.3.0-rc.2→ bump deferred until awasm-packbuild can re-validate the nobleExpandedKeyEncoding::from_expanded_bytesinterop.falcon512.jssigSize: 666is intentional (variable-length max per FIPS 206; wire format length-prefixes signatures).Generated by Claude Code