- Zig 90.9%
- Nix 9.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
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 |
||
| .forgejo/workflows | ||
| LICENSES | ||
| src | ||
| tests | ||
| tools | ||
| .gitignore | ||
| build.zig | ||
| build.zig.zon | ||
| build.zig.zon.nix | ||
| flake.lock | ||
| flake.nix | ||
| package.nix | ||
| README.md | ||
| REUSE.toml | ||
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.
- Perrin, Trevor. The Noise Protocol Framework, revision 34. 2018. https://noiseprotocol.org/noise.html.
- Krawczyk, Hugo, Mihir Bellare, and Ran Canetti. HMAC: Keyed-Hashing for Message Authentication. RFC 2104. February 1997. https://www.rfc-editor.org/info/rfc2104.
- Langley, Adam, Mike Hamburg, and Sean Turner. Elliptic Curves for Security. RFC 7748. January 2016. https://www.rfc-editor.org/info/rfc7748.
- Nir, Yoav, and Adam Langley. ChaCha20 and Poly1305 for IETF Protocols. RFC 8439. June 2018. https://www.rfc-editor.org/info/rfc8439.
- haskell-cryptography. cacophony: A Haskell library implementing the Noise protocol, test vectors. https://github.com/haskell-cryptography/cacophony.