Post-quantum Noise handshakes for Go. Implements the PQNoise (alt) extensions (ACM CCS 2022) that replace classical DH key exchanges with quantum-resistant KEMs while preserving Noise's formal security guarantees.
Ported from Rust Clatter v2.2.0 by Joni Lepisto and verified against it byte-for-byte. Built on Go stdlib crypto (crypto/mlkem, crypto/ecdh) with golang.org/x/crypto for ChaCha20-Poly1305 and BLAKE2. No other external dependencies for Noise handshakes.
Warning
- This library has not received any formal audit
- While we use Go's standard library cryptographic primitives, it is up to you to evaluate whether they meet your security and integrity requirements
- Post-quantum cryptography is not as established as classical cryptography. Users are encouraged to use hybrid handshakes (
HybridHandshake,HybridDualLayerHandshake) that combine classical and post-quantum primitives for defense in depth
go get github.com/shurlinet/go-clatter
Requires Go 1.26+ (for crypto/mlkem).
This library tracks Noise protocol framework revision 34. The following features are not supported:
- Curve 448 DH support - No suitable Go implementation exists
- Deferred pattern support - Can be implemented by the user
- Fallback pattern support - Can be implemented by the user
go-clatter adopts the same modified PSK validity interpretation as Rust Clatter for post-quantum patterns.
When PQ patterns are used, sending either an e or ekem token provides the required self-chosen randomness
equivalent to the e token in classical Noise patterns. skem also satisfies this requirement if it comes
before any psk tokens in the message pattern.
- NQ (
NqHandshake) - Classical DH-only handshakes using X25519 - PQ (
PqHandshake) - KEM-only handshakes using ML-KEM-768 or ML-KEM-1024 - Hybrid (
HybridHandshake) - True hybrid handshakes combining DH and KEM operations in a single symmetric state - DualLayer (
DualLayerHandshake) - Outer-encrypts-inner piped handshake with independent layers - HybridDualLayer (
HybridDualLayerHandshake) - Outer-encrypts-inner piped handshake with cryptographic binding between layers
90 handshake patterns. 4 hash functions. 2 AEAD ciphers. 2 ML-KEM sizes. 3 HQC sizes (experimental).
- ML-DSA-65 (
crypto/sign/mldsa65) - FIPS 204 lattice-based digital signatures (NIST Level 3, ~192-bit security). Seed = 32 B, PubKey = 1952 B, Sig = 3309 B. - SLH-DSA (
crypto/sign/slhdsa) - FIPS 205 hash-based digital signatures. NIST's backup to ML-DSA. 18 parameter sets: 12 FIPS (SHA2 + SHAKE) and 6 non-FIPS BLAKE3 variants. Security levels 1/3/5 with fast-sign and small-sig tradeoffs. Verified against 1,260 NIST ACVP vectors and 192 PQC Suite B BLAKE3 vectors.
Standalone signing modules. Not integrated into the Noise handshake - these are general-purpose signing primitives for application-layer use.
- Stdlib crypto - All Noise handshake primitives use Go's standard library (
crypto/mlkem,crypto/ecdh,crypto/aes) orgolang.org/x/crypto(ChaCha20-Poly1305, BLAKE2). No hand-rolled cryptography. - Secret zeroing - Every type holding secrets implements an explicit
Destroy()method. Fixed-size secrets (cipher keys, chaining keys, PSKs) live in fixed-size arrays; variable-size KEM secrets live in heap slices thatDestroy()zeroes in place. Callers must callDestroy()when keys are no longer needed. - Errors, not panics - All error conditions return Go
errorvalues. Invalid inputs, state violations, and crypto failures are handled through Go's standard error pattern. - HKDF key derivation - Manual HKDF implementation matching Noise spec (not a wrapper), with HMAC key length validation and full error propagation through HKDF2/HKDF3.
- Concurrency safety - Handshake objects detect concurrent use via atomic guards and return errors. Transport ciphers use independent nonce counters per direction.
- Minimal dependencies - Noise handshakes depend only on Go stdlib +
golang.org/x/crypto. Signing modules addfilippo.io/mldsa(ML-DSA) and embedded Trail of Bits engine (SLH-DSA).
| Feature | Status | Notes |
|---|---|---|
| Noise rev 34 one-way patterns | Supported | N, K, X |
| Noise rev 34 interactive patterns | Supported | NN, NK, NX, KN, KK, KX, XN, XK, XX, IN, IK, IX |
| PQNoise KEM patterns (ACM CCS 2022) | Supported | pqNN, pqNK, pqNX, pqKN, pqKK, pqKX, pqXN, pqXK, pqXX, pqIN, pqIK, pqIX |
| Hybrid DH+KEM patterns | Supported | All interactive patterns with combined DH and KEM |
| Dual-layer piped handshakes | Supported | DualLayer and HybridDualLayer with cryptographic binding |
| PSK modifiers (psk0-psk4) | Supported | Modified validity rule for PQ patterns (see PSK section) |
| Curve 448 DH | Not supported | No suitable Go implementation |
| Deferred/fallback patterns | Not supported | Can be implemented by the user |
| Primitive | Implementation | Protocol Name |
|---|---|---|
| X25519 DH | crypto/ecdh |
25519 |
| ML-KEM-768 | crypto/mlkem (FIPS 203) |
MLKEM768 |
| ML-KEM-1024 | crypto/mlkem (FIPS 203) |
MLKEM1024 |
| HQC-128 | go-hqc (experimental, build tag hqc) |
HQC128 |
| HQC-192 | go-hqc (experimental) |
HQC192 |
| HQC-256 | go-hqc (experimental) |
HQC256 |
| ChaCha20-Poly1305 | golang.org/x/crypto |
ChaChaPoly |
| AES-256-GCM | crypto/aes |
AESGCM |
| SHA-256 | crypto/sha256 |
SHA256 |
| SHA-512 | crypto/sha512 |
SHA512 |
| BLAKE2s | golang.org/x/crypto/blake2s |
BLAKE2s |
| BLAKE2b | golang.org/x/crypto/blake2b |
BLAKE2b |
| ML-DSA-65 | filippo.io/mldsa (FIPS 204) |
- |
| SLH-DSA (SHA2) | Embedded Trail of Bits go-slh-dsa (FIPS 205) | - |
| SLH-DSA (SHAKE) | Embedded Trail of Bits + golang.org/x/crypto/sha3 |
- |
| SLH-DSA (BLAKE3) | Embedded Trail of Bits + lukechampine.com/blake3 |
- |
go-clatter uses the same naming scheme as Rust Clatter for cross-implementation compatibility:
Noise_NN_25519_ChaChaPoly_SHA256 (NQ)
Noise_pqNN_MLKEM768_ChaChaPoly_SHA256 (PQ, same KEM)
Noise_pqNN_MLKEM768+MLKEM1024_ChaChaPoly_SHA256 (PQ, different KEMs)
Noise_hybridNN_25519+MLKEM768_ChaChaPoly_SHA256 (Hybrid)
import (
clatter "github.com/shurlinet/go-clatter"
"github.com/shurlinet/go-clatter/crypto/cipher"
"github.com/shurlinet/go-clatter/crypto/dh"
"github.com/shurlinet/go-clatter/crypto/hash"
"github.com/shurlinet/go-clatter/crypto/kem"
)
// NQ handshake (classical)
suite := clatter.CipherSuite{
DH: dh.NewX25519(),
Cipher: cipher.NewChaChaPoly(),
Hash: hash.NewSha256(),
}
alice, err := clatter.NewNqHandshake(clatter.PatternXX, true, suite,
clatter.WithStaticKey(aliceKeys),
clatter.WithPrologue([]byte("my-app/v1")),
)
if err != nil {
panic(err)
}
// Hybrid handshake (quantum-resistant)
hybridSuite := clatter.CipherSuite{
DH: dh.NewX25519(),
EKEM: kem.NewMlKem768(),
SKEM: kem.NewMlKem768(),
Cipher: cipher.NewChaChaPoly(),
Hash: hash.NewSha256(),
}
alice, err = clatter.NewHybridHandshake(clatter.PatternHybridXX, true, hybridSuite,
clatter.WithStaticKey(aliceDHKeys),
clatter.WithStaticKEMKey(aliceKEMKeys),
clatter.WithPrologue([]byte("my-app/v1")),
)
if err != nil {
panic(err)
}HQC is NIST's backup KEM (code-based, different math from ML-KEM). It requires two deliberate opt-ins:
- Build tag: compile with
-tags hqc - Runtime flag:
clatter.AllowExperimental.Store(true)
//go:build hqc
import (
clatter "github.com/shurlinet/go-clatter"
"github.com/shurlinet/go-clatter/crypto/cipher"
"github.com/shurlinet/go-clatter/crypto/hash"
"github.com/shurlinet/go-clatter/crypto/kem"
)
clatter.AllowExperimental.Store(true)
suite := clatter.CipherSuite{
EKEM: kem.NewHqc128(),
SKEM: kem.NewHqc128(),
Cipher: cipher.NewChaChaPoly(),
Hash: hash.NewSha256(),
Experimental: true,
}
alice, err := clatter.NewPqHandshake(clatter.PatternPqXX, true, suite,
clatter.WithStaticKey(aliceKeys),
)
if err != nil {
panic(err)
}Without the build tag, HQC code is not compiled (zero binary bloat). Without AllowExperimental, every KEM operation returns ErrExperimentalNotAllowed. Both gates will be relaxed as HQC progresses through FIPS standardization.
import "github.com/shurlinet/go-clatter/crypto/sign/mldsa65"
// Generate a key pair.
sk, err := mldsa65.GenerateKey()
if err != nil {
panic(err)
}
defer sk.Destroy() // zeros seed on cleanup
// Sign (hedged randomness - recommended for production).
sig, err := sk.Sign([]byte("message to sign"))
if err != nil {
panic(err)
}
// Verify.
ok := sk.PublicKey().Verify([]byte("message to sign"), sig)
// Context separation prevents cross-purpose replay.
sig, err = sk.SignWithContext([]byte("data"), "my-app/transfers/v1")
if err != nil {
panic(err)
}
ok = sk.PublicKey().VerifyWithContext([]byte("data"), sig, "my-app/transfers/v1")
// Seed export/import for key persistence.
seed, err := sk.Seed() // 32 bytes - store securely
if err != nil {
panic(err)
}
sk2, err := mldsa65.NewPrivateKeyFromSeed(seed) // reconstruct later
if err != nil {
panic(err)
}
defer sk2.Destroy()import "github.com/shurlinet/go-clatter/crypto/sign/slhdsa"
// Generate a key pair (SHA2-128f: fastest FIPS 205 param set).
priv, err := slhdsa.GenerateKey(slhdsa.SHA2_128f)
if err != nil {
panic(err)
}
defer priv.Destroy()
// Sign (hedged randomness - recommended for production).
sig, err := priv.SignMessage([]byte("message"))
if err != nil {
panic(err)
}
// Verify.
ok := priv.PublicKey().Verify([]byte("message"), sig)
// BLAKE3 variant (non-FIPS, faster on x86 with SIMD).
// Same API, just a different ParamSet constant.
privB, err := slhdsa.GenerateKey(slhdsa.BLAKE3_128f)
if err != nil {
panic(err)
}
defer privB.Destroy()
sigB, err := privB.SignMessage([]byte("blake3 message"))
if err != nil {
panic(err)
}
ok = privB.PublicKey().Verify([]byte("blake3 message"), sigB)
// Pre-hash mode for large files (SHA2/SHAKE param sets only).
largeFile := []byte("contents of a large file")
sig, err = priv.SignPreHash(largeFile, slhdsa.HashSHA2_256)
if err != nil {
panic(err)
}
ok = priv.PublicKey().VerifyPreHash(largeFile, sig, slhdsa.HashSHA2_256)18 parameter sets available. See the SLH-DSA godoc for the full API and parameter set guide.
Attach an Observer to any handshake to receive real-time notifications about message processing, key exchange events, and errors:
type myObserver struct{}
func (o *myObserver) OnMessage(e clatter.HandshakeEvent) {
fmt.Printf("[msg %d] %s type=%s payload=%d bytes\n",
e.MessageIndex, e.Direction, e.HandshakeType, e.PayloadLen)
}
func (o *myObserver) OnError(e clatter.HandshakeErrorEvent) {
fmt.Printf("[msg %d] ERROR: %v\n", e.MessageIndex, e.Err)
}
alice, err := clatter.NewNqHandshake(clatter.PatternXX, true, suite,
clatter.WithStaticKey(aliceKeys),
clatter.WithObserver(&myObserver{}),
)
if err != nil {
panic(err)
}Observer events report learned remote keys (DH and KEM), handshake hash, protocol name, phase (for dual-layer), and completion status. A nil observer has zero overhead. Panics in observer callbacks are recovered. See examples/observer/ and the Observer godoc.
| Example | Description |
|---|---|
examples/nq/ |
Classical NQ (X25519) handshake |
examples/pq/ |
Post-quantum PQ (ML-KEM) handshake |
examples/hqc/ |
Post-quantum PQ (HQC-128) handshake (experimental, -tags hqc) |
examples/hybrid/ |
Hybrid DH+KEM handshake |
examples/dual_layer/ |
Dual-layer piped handshake |
examples/psk/ |
Pre-shared key handshake |
examples/observer/ |
Observer callbacks for handshake events |
examples/mldsa65/ |
ML-DSA-65 post-quantum signing |
examples/slhdsa/ |
SLH-DSA signing (SHA2/SHAKE) |
examples/slhdsa_blake3/ |
SLH-DSA signing (BLAKE3) |
- Call
Destroy()on private keys when they are no longer needed.Destroy()zeroes secret material. The Go garbage collector does not guarantee timely zeroing of freed memory. - Use hybrid handshakes for defense in depth. PQC is newer than classical crypto.
HybridHandshakeandHybridDualLayerHandshakecombine both so that security holds even if one primitive is broken. - Set a prologue for protocol binding. The prologue is mixed into the handshake hash and prevents cross-protocol replay. Use a unique string per application (e.g.,
"my-app/v1"). - Do not reuse handshake objects. Each handshake instance is single-use. Create a new one per connection.
- Transport ciphers are directional. After handshake completion,
TransportStateprovides separate send/receive ciphers. Do not swap them.
| Feature | go-clatter | Rust Clatter |
|---|---|---|
| Type dispatch | CipherSuite struct (runtime) |
Generic type parameters (compile-time) |
| Error handling | Go error values (idiomatic Go) |
Panics for invariant violations (idiomatic Rust) |
| ML-KEM-512 | Not available (Go stdlib ships 768+1024 only) | Available |
| Cross-vendor KEM tests | N/A (Go has one stdlib impl) | Tests against multiple KEM implementations |
| Signing modules | ML-DSA-65 (FIPS 204) + SLH-DSA (FIPS 205) | Not in scope |
| Observer callbacks | Supported | Not in scope |
| Max message size | Configurable per-handshake | Fixed |
| pqKN / pqNK naming | Distinct patterns, per the PQNoise paper (Figure 2) | noise_pqkn() mislabels its pattern "pqNK", shipping two patterns under one name |
| Low-order X25519 input | Rejected with ErrDHLowOrder (Noise spec section 4.1 option 2) |
Continues with all-zero shared secret (option 1; both are spec-compliant, divergence only on adversarial input) |
go-clatter is verified by:
- Unit tests across all packages
- Smoke tests - 26,112 handshakes across all pattern/cipher/hash/KEM combinations (NQ + PQ + Hybrid + DualLayer + HybridDualLayer)
- Property tests - All 90 patterns verified with independent overhead calculator, per-message runtime cross-check, actual-bytes-written oracle, constructor boundary validation, and transport enforcement
- HQC smoke tests - PQ (all 3 param sets), Hybrid, DualLayer, mixed KEM, experimental gate, and mid-handshake toggle tests (build tag
hqc) - HQC property tests - All 90 patterns verified with HQC-128 overhead calculator (build tag
hqc) - Fuzz tests - 9 Noise fuzz targets matching Rust Clatter + 1 HQC fuzz (build tag
hqc) + 2 SLH-DSA fuzz targets (sign-verify round-trip + loader crash resistance) - MaxMsgLen fuzz - Boundary sharpness verification across 3 patterns (NQ, PQ, Hybrid) covering all message count shapes
- Cacophony and Snow test vectors - 408 cross-implementation vectors verified byte-for-byte (vectors/)
- 10 Rust interop vectors generated from Rust Clatter with deterministic RNG, all verified byte-for-byte including the DualLayer and HybridDualLayer composites (wire bytes, per-message handshake hashes, and transport ciphertexts)
- NIST ACVP vectors - 1,260 SLH-DSA test vectors (keygen + sigGen + sigVer) across all 12 FIPS 205 parameter sets including pre-hash mode
- NIST ACVP ML-DSA vectors - ML-DSA-65 keyGen verified seed-to-public-key byte-for-byte plus sigVer including all NIST negative cases (modified message, commitment, z, hint)
- PQC Suite B BLAKE3 vectors - 192 cross-implementation vectors for all 6 BLAKE3 parameter sets
make test
Or equivalently: go test -race -count=1 -timeout 30m ./...
The SLH-DSA package includes 1,452 ACVP/BLAKE3 vectors across all 18 parameter sets. The -s (small signature) variants build deep hypertrees and take 15+ minutes with -race on Apple Silicon, longer on CI runners. The default Go timeout of 10 minutes is not enough. The Makefile ensures the correct flags are always used. CI uses a 90-minute timeout for the slhdsa package to account for slower shared runners.
- SM3 hash function support - SM3 is China's national cryptographic hash (ISO/IEC 10118-3:2018, 256-bit, equivalent security to SHA-256). SLH-DSA's hash-agnostic architecture enables SM3-instantiated parameter sets alongside SHA2/SHAKE/BLAKE3. Go library:
emmansun/gmsm. Waiting for official SLH-DSA-SM3 spec. - HQC KEM FIPS finalization - HQC-128/192/256 are available as experimental KEMs (build tag
hqc) backed bygo-hqc. TheExperimentalgate will be removed once NIST publishes FIPS 207 (expected late 2026 / early 2027). - MAYO signing - MAYO is a multivariate signature scheme in NIST's additional signatures Round 3 (40 submissions → 14 → 9 survivors). Compact signatures (~320 bytes at Level 1) compared to ML-DSA-65's 3309 bytes. Selections expected 2027-2028.
- HAWK signing - HAWK is an NTRU lattice-based signature scheme, also in NIST Round 3. Different lattice construction from ML-DSA (NTRU vs module-LWE), providing mathematical diversity. Compact signatures (~700 bytes). By Thomas Pornin (author of FALCON/FN-DSA).
- China NGCC algorithms - China's Next-Generation Commercial Cryptographic Algorithms Program (NGCC, launched February 2025) is running its own PQC standardization independently of NIST, with submissions closing June 2026 and algorithm selections expected 2027-2028. go-clatter's modular architecture (CipherSuite, ParamSetFuncs interface) is designed to accommodate new KEM and signature algorithms as they are standardized, regardless of origin.
| Dependency | Purpose |
|---|---|
| Go 1.26+ | Required for crypto/mlkem |
golang.org/x/crypto |
ChaCha20-Poly1305, BLAKE2, SHA3/SHAKE |
filippo.io/mldsa |
ML-DSA-65 signing. Pre-release of Go's upcoming crypto/mldsa stdlib package, maintained by Filippo Valsorda. Migration to stdlib is a single import path change when crypto/mldsa ships (proposal #77626 accepted). |
lukechampine.com/blake3 |
SLH-DSA BLAKE3 param sets. Pure Go + SIMD assembly for amd64/arm64. |
github.com/shurlinet/go-hqc |
HQC KEM (experimental, build tag hqc). NIST backup KEM. |
Special thanks to Joni Lepisto for creating Rust Clatter. His clean, well-tested implementation made this Go port possible and his test infrastructure (interop vectors, smoke tests, fuzz targets) set the standard we verify against.
Thanks also to the projects whose test vector datasets we use to verify correctness:
- Cacophony (Haskell Noise implementation) - 944 test vectors
- Snow (Rust Noise implementation) - 408 test vectors
And to the authors of the PQNoise paper for the foundational research this library implements.
Thanks to Filippo Valsorda for filippo.io/mldsa - the ML-DSA implementation that powers our signing module. Filippo maintains Go's cryptography standard library and designed this package as the direct precursor to crypto/mldsa. His work gives the Go ecosystem production-ready post-quantum signatures years before the stdlib ships them.
Thanks to Trail of Bits for go-slh-dsa - the SLH-DSA engine embedded in our signing module. Their implementation is pure Go, side-channel resistant, and covers all 12 FIPS 205 parameter sets.
Thanks to JP Aumasson, Zooko Wilcox-O'Hearn, and Alex Pruden for PQC Suite B - the BLAKE3 variant research and test vectors that validate our BLAKE3 parameter sets.
Thanks to Luke Champine for lukechampine.com/blake3 - the BLAKE3 implementation with SIMD acceleration for amd64 and arm64.
This Go port was written with AI assistance (Claude) and reviewed by a human. All code is verified against the Rust reference implementation byte-for-byte, and tested with 26,000+ handshakes, 408 cross-implementation vectors, 1,452 NIST/PQC-Suite-B vectors, and 13 fuzz targets. The AI generated code; the human made every design decision, reviewed every line, and owns every bug.
MIT - matching the upstream Rust Clatter license. See THIRD_PARTY_LICENSES for embedded dependencies.