hiss
Static Noise. The Noise Protocol Framework, resolved at compile time.
A Noise<Pattern, Curve, Cipher, Hash> is zero-sized: the handshake pattern, curve,
cipher, and hash are encoded as types, so every buffer size is a const and every
protocol misuse — a token out of order, a wrong-direction message — is a compile
error. Get the handshake wrong and it never builds.
Secret keys live in software or behind a pluggable, hardware-backed provider (Apple Secure Enclave today), and are wiped on drop.
Status: pre-release (
0.1), not yet published. The API is unstable and the code has not been independently audited. See Security before relying on it.
What it offers
- Compile-time protocol selection. The pattern, curve, cipher, and hash are zero-sized type parameters; message sizes are associated constants. Misuse is rejected by the type system rather than at runtime.
- A small, deliberate crypto core. Production cryptography is
cryptoxide+eccoxideonly — no sprawling dependency surface. - Pluggable crypto providers. A trait family —
CryptoKeyProvider<C>(keygen) at the base, withDhProvider<C>adding the ECDH the handshake actually consumes andSigningProviderfor identity signing, each paired with an…Asyncrefinement — lets the same handshake run against a software backend or a hardware-backed one (Apple Secure Enclave). See Providers. - Two streaming drivers. A blocking
std::iodriver (SyncHandshake, always available) and an optionaltokio::iodriver (AsyncHandshake, behind theasync-iofeature). The buffer / no-syscall / one-shot case is simply passing an in-memoryIo— aCursor,Vec, or&mut [u8]— to the synchronous driver; there is no separate buffer API.
Supported suite
This 0.1 targets one cipher suite and a fixed set of patterns:
| Axis | Supported |
|---|---|
| Patterns | N, K, Kpsk0, IKpsk1, IK, NK, IX, XK, NN, XX, X |
| Curves | NIST P-256 (secp256r1), X25519 (Curve25519, the Noise 25519 curve), and X448 (the Noise 448 curve) |
| Cipher | ChaCha20-Poly1305 |
| Hash | BLAKE2b |
Conformance is anchored against snow via an interop
test suite. All eleven fundamental patterns are present; additional hashes and ciphers
(AES-GCM) are planned for 0.2+.
The fallback modifier — and the compound protocols it enables (e.g. Noise Pipes /
0-RTT-with-retry) — is an intentional non-goal, not a missing feature. It is optional
in the Noise spec, which presents it only as an illustrative building block, and is
unnecessary for the targeted use cases; snow omits it for the same reason.
Quickstart
The N pattern is a one-way, sender-anonymous seal: anyone who knows a recipient's static
public key can send it one confidential, authenticated message, with no reply. The whole
exchange is the single Noise message -> e, es; we build it over X25519 in five steps.
(This mirrors the crate-level doctest, which compiles and runs each step.)
1. The recipient's static key pair
N authenticates the recipient, so it owns a long-term static key pair and the sender must
already know its public half (shared out of band). X25519 is Diffie–Hellman over
Curve25519 (RFC 7748) — the curve Noise calls 25519.
use ;
use X25519;
// `EphemeralOnly` is the software backend; it wraps a CSPRNG.
let mut recipient = new;
let recipient_static = recipient.?; // secret half — never shared
let recipient_pub = recipient.public?; // public half — the sender knows this
2. The sender begins N and pins the recipient's static
The sender drives the Initiator side. N's initiator is anonymous — no static key of
its own — so the recipient learns only that the sender knew its public key. set_rs
supplies that known key (N's <- s pre-message).
use ;
// The full protocol name: Noise_N_25519_ChaChaPoly_BLAKE2b.
type NoiseN = ;
let handshake = initiate
.set_rs;
3. Write the message (-> e, es) and seal the payload
e writes a fresh ephemeral public key to the wire; es mixes
DH(ephemeral, recipient-static) into the cipher key. After es the channel is keyed, so
into_parts returns the live sender and the handshake message; the payload then rides in
the first transport record.
let = handshake.e?.es?.into_parts;
let quote = b"Not all those who wander are lost.";
let mut sealed = vec!; // +16 for the AEAD tag
let n = sender.send?;
4. The recipient receives the message
The recipient drives the Responder side with its static private key (set_s) and replays
the same tokens, recomputing the identical es secret without ever putting a key on the
wire.
use Responder;
let handshake = respond
.set_s?;
let = handshake.recv.e?;
let mut transport = recv.es?;
5. Decrypt
Both ends now hold the same transport key, so the recipient opens the sealed record — authenticated end to end: only someone who knew the recipient's public key could have produced it.
let mut opened = vec!;
let m = transport.transport.receive?;
opened.truncate;
assert_eq!; // "Not all those who wander are lost."
The buffer / no-syscall case is just an in-memory Io (a Cursor, Vec, or
&mut [u8]), as the example above shows; with the async-io feature the same chain runs
over tokio::io via AsyncHandshake. The mutual-authentication patterns (K, Kpsk0, IKpsk1, IK) follow the
same builder shape with additional pre-message setters; NK is interactive and
responder-authenticated (the initiator is anonymous) and likewise pre-knows the
responder's static key via set_rs. XK is a three-message, mutually-authenticated
handshake with strong initiator-identity privacy: it pre-knows the responder's static
via set_rs/set_s like IK, but defers the initiator's own static to an
encrypted third flight, so the initiator's identity is hidden from a passive
eavesdropper. IX is interactive and mutually authenticated
but has no pre-message setters — neither side pre-knows the other's static; both
transmit their static keys during the handshake as s tokens (the initiator's in the
clear, the responder's encrypted). NN is the unauthenticated interactive pattern:
both parties are anonymous (no static keys, no pre-message setters), so it offers
confidentiality only against a passive eavesdropper — there is no protection against an
active man-in-the-middle — with full forward secrecy once the ephemerals are mixed.
XX is the canonical three-message, mutually-authenticated pattern: like IX it has
no pre-message setters — neither side pre-knows the other's static — but unlike IX
both statics are transmitted encrypted during the handshake (after ee keys the
cipher), so both identities are hidden from a passive eavesdropper. Both parties send
their static keys via s tokens; full forward secrecy follows the ee DH.
X is a one-way authenticated seal: like the Quickstart's N it is a single message
to a recipient whose static key the sender already pre-knows (via set_rs), but it
additionally transmits the sender's own static key encrypted in-band (after es
keys the cipher). The message is sender-authenticated via ss and the sender's identity
stays hidden from a passive eavesdropper — IK's msg1 with no reply. Compared with K
(which pre-shares both statics out of band), X carries the sender's static on the wire.
Providers
Standard Noise authenticates and key-agrees only via raw ECDH — there is no signature token in the handshake. A backend can therefore serve the Noise DH (key-agreement) role only if it can yield a value Noise can mix (the raw shared secret, or the result of Noise's exact HKDF over it). Backends that can only sign fit a separate identity/attestation layer around the channel, not inside it.
| Backend | DH (Noise channel) | Identity / signing | Status |
|---|---|---|---|
Software (eccoxide) |
✅ | ✅ | implemented |
| Apple Secure Enclave (macOS/iOS) | ✅ | ✅ | implemented |
| Android Keystore / StrongBox | ✅ | ✅ | planned (0.2+) |
| Linux TPM2 | ✅ (policy-permitting) | ✅ | planned (0.2+) |
| AWS KMS | ✅ (DeriveSharedSecret) |
✅ | planned (0.2+) |
| Windows CNG / Azure / GCP KMS | ❌ (no raw ECDH) | ✅ | identity role only |
| PKCS#11 HSM, YubiKey, Ledger | ❌ (no raw ECDH export) | — | out of scope |
A DH-capable backend is selected through the DhProvider / DhProviderAsync traits
(both refining the CryptoKeyProvider keygen base), so additional backends can be added
without touching the Noise core.
Platforms
- All platforms: the software backend (
EphemeralOnly) and the blockingstd::iohandshake driver. - macOS / iOS: the Apple Secure Enclave backend. Its blocking Security-framework calls are offloaded to a Tokio blocking thread pool for the async provider path.
Cargo features
| Feature | Default | Effect |
|---|---|---|
async-io |
no | Adds the tokio::io streaming handshake driver (AsyncHandshake), pulling in tokio with its I/O extension traits. The blocking std::io driver needs no feature and no runtime. |
Security
This crate has not been independently audited and is pre-1.0. Do not use it to protect anything you cannot afford to lose. That said, the crypto core is built to be responsible:
- Constant-time P-256 scalar multiplication via
eccoxide's constant-time backend. - Deterministic ECDSA (RFC 6979) with low-S normalization; no signing RNG.
- Peer-key and DH-output validation — operations on attacker-supplied points return
Resultrather than panicking; a degenerate (point-at-infinity) shared secret is rejected. - Noise's 65535-byte message-length limit is enforced at the cipher-state chokepoint.
- Secret material is zeroized on drop and is never required to be
Clone.
Please report security issues privately to the maintainers rather than opening a public issue.
Minimum supported Rust version
hiss uses the Rust 2024 edition and declares an MSRV of 1.96, enforced in CI by
the msrv job (cargo check --all-features --all-targets on the pinned toolchain).
The MSRV tracks a recent stable, floored at stable − 3: it is bumped only once it
would fall more than three releases behind current stable. It is set at the current
stable today and will begin moving once stable advances past 1.99. The declared value
lives in Cargo.toml (rust-version); keep it and the msrv CI job in lockstep.
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.