feat: add post-quantum hybrid Noise handshake#665
Draft
paschal533 wants to merge 1 commit intoChainSafe:masterfrom
Draft
feat: add post-quantum hybrid Noise handshake#665paschal533 wants to merge 1 commit intoChainSafe:masterfrom
paschal533 wants to merge 1 commit intoChainSafe:masterfrom
Conversation
Adds Noise_XXhfs_25519+XWing_ChaChaPoly_SHA256 as a second connection
encrypter alongside the existing classical noise(). Both can coexist in
the same libp2p node via protocol negotiation.
The new noiseHFS() factory implements the Noise HFS pattern which adds
two KEM tokens (e1 and ekem1) to the classical XX handshake, giving
quantum-safe forward secrecy against Store-Now-Decrypt-Later attacks.
Forward secrecy is secure if either X25519 or ML-KEM-768 (the two
underlying KEMs in X-Wing) is unbroken, so classical security is fully
preserved.
KEM: X-Wing (ML-KEM-768 + X25519 via SHA3-256 combiner)
- IETF draft-connolly-cfrg-xwing-kem
- Implemented via @noble/post-quantum v0.6.0 (pure JS)
New exports:
noiseHFS(), NoiseHFS, NoiseHFSInit
pqcKem, pqcCrypto, IKem, KemKeyPair, KemEncapsulateResult
XXhfsHandshakeState, NOISE_HFS_PROTOCOL_NAME
HfsHandshakeStateInit, HfsHandshakeParams
Wire sizes (empty payload):
Classical XX : 192 bytes
XXhfs : 2,544 bytes (+2,352 bytes)
Benchmark (Node.js v22.17.1, pure JS):
Classical XX handshake : 114 ops/s (8.75 ms)
XXhfs handshake : 23 ops/s (44.18 ms)
Tests: 99 new tests across 4 spec files + 5 deterministic test vectors
test/pqc-kem.spec.ts (17 tests - IKem / pqcKem)
test/pqc-protocol.spec.ts (18 tests - XXhfsHandshakeState)
test/pqc-noise.spec.ts (12 tests - integration with libp2p)
test/pqc-vectors.spec.ts (52 tests - test vector verification)
Also includes:
NOISE_HFS_SPEC.md - full wire format and protocol spec
benchmarks/benchmark-pqc.js - benchmark runner
benchmarks/results.md - measured results + PR #3432 analysis
scripts/generate-pqc-vectors.js - deterministic vector generator
src/crypto/pqc.node.ts - Node.js native KEM backend slot (TODO
when Node.js adds ML-KEM-768 support)
PR #3432 note: when js-libp2p adds ML-DSA identity (MLDSA65), NoiseHFS
will support it automatically. privateKey.sign() is key-type aware so
no changes are needed in this layer. The full-PQ wire size would be
approximately 9,400 bytes total (KEM overhead + MLDSA65 identity on
both sides).
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.
What this PR does
This adds a second connection encrypter to the package alongside the existing
noise(): a post-quantum hybrid handshake callednoiseHFS()that implements theNoise_XXhfs_25519+XWing_ChaChaPoly_SHA256pattern.The idea is that you can swap
noise()fornoiseHFS()in your libp2p config and get quantum-safe forward secrecy without giving up any classical security. The handshake is secure if either X25519 or ML-KEM-768 is unbroken.Background
Store-Now-Decrypt-Later is the main near-term quantum threat for libp2p connections. An adversary capturing encrypted traffic today could decrypt it once a large enough quantum computer exists. Upgrading forward secrecy now (before quantum computers arrive) is the practical defense.
The Noise HFS spec (https://github.com/noiseprotocol/noise_hfs_spec) defines how to add a KEM alongside the existing DH operations in any Noise pattern. This PR applies that to XX, using X-Wing as the KEM.
X-Wing (IETF draft-connolly-cfrg-xwing-kem) is a hybrid of ML-KEM-768 (FIPS 203) and X25519, combined via SHA3-256. Using a hybrid means neither primitive has to be trusted alone -- the combined secret is secure as long as at least one of them is.
Protocol details
Noise_XXhfs_25519+XWing_ChaChaPoly_SHA256/noise-pq/1.0.0@noble/post-quantumv0.6.0 (pure JS, works in browsers and Node.js without WASM or native bindings)The handshake pattern is:
The two new tokens are
e1(initiator sends KEM ephemeral public key in Message A) andekem1(responder encapsulates and sends back the encrypted KEM ciphertext in Message B, then both sides mix the KEM shared secret into the chaining key).Message C is unchanged from classical XX.
Wire sizes (empty payload)
With a real
NoiseHandshakePayload(Ed25519 identity + extensions): approximately 500 B classical XX vs approximately 2,852 B for XXhfs.Performance (Node.js v22.17.1, pure JS)
The ~5x slowdown over classical XX is dominated by X-Wing (about 21 ms per round-trip). Native WASM or Node.js native ML-KEM support (when it lands) would improve this by roughly 3-10x. See
benchmarks/results.mdfor the full breakdown.New files
src/kem.tsIKeminterfacesrc/crypto/pqc.tssrc/crypto/pqc.node.tssrc/protocol-pqc.tsXXhfsHandshakeStatestate machinesrc/performHandshake-hfs.tssrc/noise-hfs.tsNoiseHFSencrypter +noiseHFS()factoryNOISE_HFS_SPEC.mdbenchmarks/benchmark-pqc.jsbenchmarks/results.mdscripts/generate-pqc-vectors.jstest/fixtures/pqc-test-vectors.jsontest/pqc-kem.spec.tstest/pqc-protocol.spec.tstest/pqc-noise.spec.tstest/pqc-vectors.spec.tsUsage
Both peers must use
noiseHFS(). It is not backward-compatible with the classical/noiseprotocol because the handshake message layout differs.Notes on PR #3432
js-libp2p PR #3432 (feat: Post quantum identities with ML-DSA by @dozyio) adds ML-DSA identity support. When that lands,
NoiseHFSwill support ML-DSA identity keys automatically becauseprivateKey.sign()is key-type aware. No changes needed in this layer.With ML-DSA identity on both sides, the full-PQ wire size would be approximately 9,400 bytes total.
benchmarks/results.mdhas the full breakdown. For most use cases,noiseHFS()with Ed25519 identity is a reasonable first step that covers the main Store-Now-Decrypt-Later threat without waiting for the identity layer migration.Test plan
pnpm buildpasses with no TypeScript errorspnpm test:nodepasses all 99 new tests (plus existing suite)node benchmarks/benchmark-pqc.jsruns and produces output matchingbenchmarks/results.mdnode scripts/generate-pqc-vectors.jsregenerates identical vectors (deterministic)NOISE_HFS_SPEC.mdfor protocol correctness