The Noise Protocol Framework for Zig: handshakes by pattern name over Curve25519, verified against cacophony's vectors
  • Zig 90.9%
  • Nix 9.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Jeffrey C. Ollie c367369df7
All checks were successful
test / test (push) Successful in 7m54s
test / docs (push) Successful in 5m9s
Build with the Zig 0.17.0 release
The toolchain comes from zig-overlay, since nixpkgs has no 0.17 yet, and
build.zig.zon.nix is regenerated with `zon2nix --17`. package.nix takes
the Zig and the dependency farm from the flake.

The patched `fuzzableZig` goes: 0.17's test runner compiles a fuzz build
as it is. With `use_llvm` on the test binaries the built-in fuzzer has
coverage to steer by, so `zig build fuzz --fuzz` now works; the
standalone loop in `tools/fuzz.zig` stays for fixed-time and seeded runs.

Ports: `b.args` to `addPassthruArgs`, the docs server takes the emitted
docs directory rather than an install path, `std.builtin.OptimizeMode`
to `std.lang.Optimize` with its lowercase modes, enum fields read through
`field_names`, and the two `**` repetitions become `@splat`, since 0.17
dropped the operator.

`X25519.recoverPublicKey` can no longer fail, so `KeyPair.fromSecret`
returns a `KeyPair` rather than an error union, and `KeyPair.generate`
no longer retries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XQouVYD4jYm3r7AtS21rdy
2026-10-05 23:38:26 -05:00
.forgejo/workflows The Noise Protocol Framework for Zig 2026-09-24 21:35:58 -05:00
LICENSES The Noise Protocol Framework for Zig 2026-09-24 21:35:58 -05:00
src Build with the Zig 0.17.0 release 2026-10-05 23:38:26 -05:00
tests Build with the Zig 0.17.0 release 2026-10-05 23:38:26 -05:00
tools Build with the Zig 0.17.0 release 2026-10-05 23:38:26 -05:00
.gitignore Fuzzing: protocol names, honest handshakes, a party reading, the ciphers 2026-09-26 18:02:17 -05:00
build.zig Build with the Zig 0.17.0 release 2026-10-05 23:38:26 -05:00
build.zig.zon Build with the Zig 0.17.0 release 2026-10-05 23:38:26 -05:00
build.zig.zon.nix Build with the Zig 0.17.0 release 2026-10-05 23:38:26 -05:00
flake.lock Build with the Zig 0.17.0 release 2026-10-05 23:38:26 -05:00
flake.nix Build with the Zig 0.17.0 release 2026-10-05 23:38:26 -05:00
package.nix Build with the Zig 0.17.0 release 2026-10-05 23:38:26 -05:00
README.md Build with the Zig 0.17.0 release 2026-10-05 23:38:26 -05:00
REUSE.toml The Noise Protocol Framework for Zig 2026-09-24 21:35:58 -05:00

zig-noise

The Noise Protocol Framework, revision 34, in Zig: handshakes taken by pattern name, over Curve25519, with ChaCha20-Poly1305 or AES-256-GCM, and SHA-256, SHA-512, BLAKE2s or BLAKE2b.

The API documentation, generated from the doc comments, is published at https://jeff.jcollie.page/zig-noise/.

const noise = @import("noise");

var hs = try noise.HandshakeState.init(.{
    .protocol_name = "Noise_KKpsk2_25519_ChaChaPoly_SHA256",
    .role = .responder,
    .prologue = prologue,
    .s = my_identity,
    .rs = their_public_key,
    .random = random,
});
const payload = try hs.readMessage(message_1, &buf);
try hs.setPsk(0, psk); // once message 1 has said which
const message_2 = try hs.writeMessage("{}", &out);

var transport = try hs.split();
const sealed = try transport.encrypt("hello", &out);

Requires Zig 0.17. It depends on nothing but the standard library, whose primitives it uses throughout.

What it does

HandshakeState is one party's side of a handshake: writeMessage produces the next message and readMessage takes the other side's, each with a payload, until the pattern is done. split then gives the two ciphers for everything after, as a Transport. It performs no I/O and holds no allocations. Messages go into and come out of buffers the caller supplies, and a key pair is made from a std.Random the caller supplies, so the library can be driven from anywhere.

Patterns are taken by name from the specification's tables: the one-way patterns (N, K, X), the twelve fundamental interactive patterns, and the 23 deferred ones of §18. Any of them can take the psk modifiers of §9.3, alone or combined — KKpsk2, NNpsk0+psk2. A PSK needed by a later message can be given after the handshake has started, with setPsk, for the protocols where the first message says which PSK to use.

Primitives are the standard library's X25519, ChaCha20-Poly1305, AES-256-GCM, SHA-2 and BLAKE2. The specification defines the HMAC, the HKDF, the two nonce layouts and Rekey around them; those are here. A Diffie-Hellman result of all zeros, from a peer's low-order point, is refused, as §12.1 permits.

Not implemented: Curve448, which the standard library lacks, and the fallback modifier and compound protocols of §10.

Testing

$ nix develop
$ zig build test --summary all

Beyond the unit tests, tests/vectors.zig runs cacophony's test vectors — all 472 of them in the suites this implements. That covers every pattern and psk modifier above, with both ciphers and all four hashes. Each runs the whole handshake with the vector's fixed keys, then the transport messages after it, and compares every ciphertext byte for byte and the handshake hash. The vectors are in the public domain, and are kept in tests/vectors/cacophony.json, filtered to those suites.

Fuzzing

A handshake reads messages from a peer it does not yet trust, so the paths from untrusted bytes are fuzzed: protocol names and patterns, which must parse into what they say or be refused; honest handshakes between two parties, which must always finish and agree on the handshake hash and the transport after it; one party reading whatever it is sent at each turn; and the ciphers, where anything sealed opens as what was sealed and anything altered does not, leaving the nonce where it was. The seeds are every protocol name the vectors cover and the messages an honest peer sends in each of those handshakes, recorded at build time by tools/fuzz_corpus.zig with the fixed keys in tests/fuzz_party.zig. zig build test checks the properties on them, and that every one of those handshakes finishes.

$ zig build fuzz --fuzz                              # Zig's fuzzer, until interrupted
$ zig build fuzz --fuzz=1M                           # a bounded run, then a report
$ zig build fuzz-run                                 # a minute of each target
$ zig build fuzz-run -- --seconds 300 --target reader
$ zig build fuzz-run -Dfuzz-optimize=debug -- --input fuzz-findings/x.bin --target reader

zig build fuzz --fuzz is Zig's own coverage-guided fuzzer; a finding is saved under .zig-cache/f/. fuzz-run is a loop of this project's own in tools/fuzz.zig, with no coverage feedback, that runs for a fixed time or from a fixed seed. A failing input is written to fuzz-findings/, and --input runs it again.

Using it

$ zig fetch --save git+https://git.jcollie.dev/jeff/zig-noise.git#main
const noise = b.dependency("noise", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("noise", noise.module("noise"));

zig-sendspin uses it for Sendspin's Noise_KKpsk2_25519_ChaChaPoly_SHA256.

Where this lives

The repository has three homes, and they hold the same history.

Where How to get it
Forgejo git clone https://git.jcollie.dev/jeff/zig-noise.git
Tangled git clone https://tangled.org/jcollie.dev/zig-noise
Radicle rad clone rad:z36tvh6nHxnTmZWyiYCjdgNMMaqQn

A Radicle repository is findable only by its ID, so that one is written out in full: rad:z36tvh6nHxnTmZWyiYCjdgNMMaqQn.

Licensing

MIT, following the REUSE specification; reuse lint checks it. The test vectors are cacophony's, and in the public domain.

References cited

Kept in the zig-noise Zotero collection.