Skip to content

fix(sdk-js): verify ML-DSA-65 signatures and align senderPub with wire-format-v1 spec#25

Merged
Apolloccrypt merged 2 commits into
mainfrom
claude/fix-post-quantum-errors-dCjCG
Apr 25, 2026
Merged

fix(sdk-js): verify ML-DSA-65 signatures and align senderPub with wire-format-v1 spec#25
Apolloccrypt merged 2 commits into
mainfrom
claude/fix-post-quantum-errors-dCjCG

Conversation

@Apolloccrypt
Copy link
Copy Markdown
Owner

Summary

Two related post-quantum bugs in sdk-js/index.js (the npm-published paramant-sdk@3.0.0 "real post-quantum ML-KEM-768 + ML-DSA-65" SDK):

  1. CRITICAL — _decrypt() never called sig.verify(). The function built the message-to-verify into a local msg variable and stopped there. Every signed blob was accepted regardless of signature validity, so the post-quantum authentication claim was effectively bypassed.

  2. Wire-format / interop bug — _encrypt() wrote the KEM pubkey into senderPub. The docs/wire-format-v1.md spec 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_pub when sigId != 0x0000; keeps kem_pub for anonymous blobs as a stable opaque sender identifier.
  • _decrypt: actually calls sig.verify(parsed.signature, msg, parsed.senderPub) and throws SignatureError on failure or on any exception inside the verifier.
  • Plus a one-line test-parity addition for ML-KEM-768 rejects wrong ciphertext size to match the standalone tests for ML-KEM-512 / ML-KEM-1024.

Test plan

  • sdk-js: 44/44 (node --test test/*.test.js) — including 2 new regression tests:
    • tampered-signature blob is rejected with SignatureError
    • signed blob's senderPub.length === 1952 (ML-DSA-65 pubkey size), not 1184
  • relay: 59/59 wire-format/registry/bootstrap/impls + 61/61 standalone impl tests
  • sdk-py: 36/36 — confirms wire-format compatibility with the Python reference SDK

Compatibility

  • KEM-only path (anonymous blobs, sigId=0x0000) is unchanged. Pre-fix anonymous blobs decrypt unchanged.
  • Signed-blob senderPub semantics now match the Python SDK and the spec test vectors. Any pre-fix signed blob produced by paramant-sdk@3.0.0-js was 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.toml ml-kem 0.3.0-rc.2 → bump deferred until a wasm-pack build can re-validate the noble ExpandedKeyEncoding::from_expanded_bytes interop.
  • falcon512.js sigSize: 666 is intentional (variable-length max per FIPS 206; wire format length-prefixes signatures).

Generated by Claude Code

claude added 2 commits April 25, 2026 16:56
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.
@Apolloccrypt Apolloccrypt merged commit 11f822c into main Apr 25, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants