Expand description
hiss — the Noise Protocol Framework, resolved at compile time.
hiss is a Noise Protocol Framework implementation in which the
handshake is chosen at compile time: you name a pattern, a curve, a
cipher, and a hash, and the compiler builds — and checks — exactly
that protocol for you. There is nothing to configure at runtime and
nothing to negotiate; if it builds, the handshake is well-formed.
Concretely, a Noise<Pattern, Curve, Cipher, Hash>
is zero-sized: the pattern, curve, cipher, and hash are type
parameters, so every message size is an associated const and every
protocol misuse — a token out of order, a wrong-direction message, a
malformed pattern — is a compile error, rejected by the type-state
and the WellFormed pattern guard rather than
at runtime. Get the handshake wrong and it never builds.
§Quickstart
Two peers authenticate each other and exchange an encrypted message in
each direction, neither knowing the other’s key in advance. Four steps,
each one a doctest that compiles and runs. Assembled into a single
program it is the quickstart example in the repository —
cargo run --example quickstart.
You write the pattern in Noise’s own notation together with a concrete
suite, and the noise! macro generates a type-state
state machine for it: one method per handshake message, every message a
fixed-size [u8; N] known at compile time. It performs no I/O —
write_message_N hands you the bytes and you move them however you
already move bytes. See the noise! docs for the DSL
and the full generated API.
You need two crates. hiss never picks a random-number generator for
you, so the CSPRNG is a dependency you choose and hand in:
[dependencies]
hiss = "0.2"
rand = "0.10"§1. Describe the handshake you want
This one is XX: three messages, both sides proving who they are
along the way. Name the type after its pattern — the name you write
goes on the wire as part of the protocol identity, as
noise! spells out.
use hiss::noise::{Blake2b, ChaChaPoly, X25519};
hiss::noise! {
/// Mutual authentication; neither side pre-knows the other's key.
pub XX<X25519, ChaChaPoly, Blake2b> {
-> e
<- e, ee, s, es
-> s, se
}
}§2. Give each side a long-term key
XX authenticates both parties, so each owns a key pair that
outlives the connection; nothing is shared in advance. Keep the
public halves — step 3 is where each side checks the other against
one.
use hiss::provider::{EphemeralOnly, ProviderExt};
let mut alice_keys = EphemeralOnly::new(rand::rng());
let alice_static = alice_keys.generate::<X25519>()?;
let alice_pub = alice_keys.public(&alice_static)?;
let mut bob_keys = EphemeralOnly::new(rand::rng());
let bob_static = bob_keys.generate::<X25519>()?;
let bob_pub = bob_keys.public(&bob_static)?;§3. Run the handshake — and decide whether to trust the peer
Each call hands you the bytes to send; moving them — socket, queue,
QR code — is yours, because hiss does no I/O.
Completing XX proves the peer holds a static private key, never
that it is one you trust. read_message_N_with is where that
decision goes: the closure sees the peer’s key as it decrypts, and an
Err aborts before any Transport exists. Leave
it out and you have an encrypted channel to a stranger.
The prologue is any context both sides already agree on — a protocol
version, a channel name — mixed into the handshake so a mismatch fails
it; pass &[] if you have none.
use hiss::noise::HandshakeError;
const PROLOGUE: &[u8] = b"prologue";
// Your trust policy: a pin, an enrolment record, an allow-list. Here, the key we expect.
let accept = |ok: bool| match ok {
true => Ok(()),
false => Err(HandshakeError::PeerRejected {
reason: "unknown peer".into(),
}),
};
let (msg1, alice) = XX::initiator(alice_keys, PROLOGUE).write_message_1()?;
let bob = XX::responder(bob_keys, PROLOGUE).read_message_1(&msg1)?;
let (msg2, bob) = bob.write_message_2(bob_static)?;
let alice = alice.read_message_2_with(&msg2, |peer| accept(peer == &bob_pub))?;
let (msg3, mut alice) = alice.write_message_3(alice_static)?;
let mut bob = bob.read_message_3_with(&msg3, |peer| accept(peer == &alice_pub))?;§4. Talk
Both ends now hold a Transport. OVERHEAD is what the authentication
tag costs you per message: give send a buffer of
plaintext.len() + OVERHEAD, and receive one that fits the plaintext.
b"ping" is 4 bytes, so 4 is the size below. One record carries at most
65519 bytes of plaintext — chunk anything larger yourself.
use hiss::noise::Transport;
let mut wire = [0u8; 4 + Transport::<XX>::OVERHEAD];
let mut got = [0u8; 4];
let n = alice.send(b"ping", &mut wire)?;
let m = bob.receive(&wire[..n], &mut got)?;
assert_eq!(&got[..m], b"ping");
let n = bob.send(b"pong", &mut wire)?;
let m = alice.receive(&wire[..n], &mut got)?;
assert_eq!(&got[..m], b"pong");§Suite and breadth
There is no default suite. Noise<P, Cu, Ci, H>
declares no default type parameters, and a noise!
declaration that omits <Curve, Cipher, Hash> is not a shorthand for
one — it is marker mode, which generates the pattern marker alone,
with no state machine and no wire-size constants. Every usable
declaration names all three.
Both of the specification’s ciphers ship. ChaChaPoly
is what the Quickstart uses and what every frozen P-256 vector was
generated over; AesGcm (§12.4) is pinned by the same
third-party cacophony corpus over 25519 and 448, and is over five
times as fast on Apple Silicon, where cryptoxide reaches the ARMv8 AES
and pmull instructions — on every other target its AES-GCM is portable
software, and ChaChaPoly stays the performance default. The speed has a
price in space: Cipher holds each cipher’s expanded
key, so an AES-GCM CipherState is 528 bytes on aarch64 (992 on the
portable path) where a ChaChaPoly one is 48. For
the curve, reach for X25519 — what the Quickstart
uses — unless you need the Apple Secure Enclave, which speaks
P256 and nothing else, or want X448’s
larger margin. All four of the Noise specification’s official hashes
ship: Blake2b and Sha512 at
HASHLEN 64, Sha256 and Blake2s
at HASHLEN 32. Use Blake2b — it is what the Quickstart, the
examples and the crate’s own sealed-message helper use, and the only
hash carrying the full seventeen-pattern frozen P-256 matrix. Pick another
when a peer requires it. Every one of the four is pinned as a primitive
against the relevant standard, and as a Noise suite by frozen
third-party (cacophony)
known-answer vectors over 25519 and 448 across all seventeen patterns.
If you use X448, prefer a 512-bit hash with it (Blake2b or
Sha512), per the specification’s §13 guidance.
Seventeen patterns are provided as markers in
noise::pattern — all fifteen of Noise’s fundamental patterns plus
two PSK variants. Each is combined with a suite
through Noise<P, Cu, Ci, H>:
N, K,
Kpsk0, IKpsk1,
IK, NK,
IX, XK,
NN, XX,
X, NX,
XN, KN,
KK, KX, and
IN. One caveat is worth carrying up here:
IN transmits the initiator’s static key in the
clear, in msg1 before any DH — the only pattern that ships here where a
passive observer learns the initiator’s identity outright. In Noise’s own
naming the two curves above
are 25519 and 448; Ed25519 is reserved for identity and signing
rather than the handshake — it does not implement
DhCurve at all, so naming it as a suite’s curve is
a compile error rather than a protocol name no registry knows.
§Providers
A provider is where your private keys live and what performs the key
agreement — the handshake does no cryptography of its own. You
construct one and hand it to initiator / responder; it is the
alice_keys argument in the Quickstart. Two ship with the crate:
EphemeralOnly<R>— pure software over a caller-supplied CSPRNGR, viaeccoxide/cryptoxide. Works everywhere, including WASM, and is what the Quickstart uses. The name means no built-in persistence, not “no long-term keys”: it does generate the static key a mutual pattern authenticates you by. Storing that key between runs, and distributing the public halves your peers pin, are yours to do.AppleSecureEnclave(Apple platforms) — P-256 keys generated inside the Secure Enclave and never extractable; software Ed25519 over a hardware-sealed seed.
A backend hiss has never heard of — an HSM, a cloud KMS, a key store
you already have — plugs in by implementing the provider traits,
without touching the Noise core:
CryptoKeyProvider<C: Curve>is the key-generation base, refined for awaitable backends byCryptoKeyProviderAsync.DhProvider<C: DhCurve>(andDhProviderAsync) add the ECDH the handshake actually consumes.SigningProvider(andSigningProviderAsync) cover identity signing, which lives around the channel rather than inside the Noise handshake.
Noise key-agrees only via raw Diffie–Hellman, so a backend can carry the channel only if it will hand back the shared secret. One that can sign but never expose a DH result fits the identity layer instead.
§Security posture
These hold whatever provider you use:
- Noise’s 65535-byte message-length limit is enforced at the cipher-state chokepoint.
- Peer public keys are parsed and validated by
hissbefore a provider sees them; operations on attacker-supplied points returnResultrather than panicking. - Secret material is zeroised on drop (see
zeroize) — pre-shared keys, shared secrets, cipher and symmetric state, and the datagram receive ratchet — and no provider is required to make its private keyClone. - The Noise
25519and448curves perform no low-order or contributory-key check — per the spec (and RFC 7748) a low-order peer key yields an all-zero secret rather than an error.
Everything else — constant-time scalar multiplication, deterministic
signing, what happens to a private key — belongs to the backend that
computes it, and does not transfer between backends. See
provider for the per-provider posture.
This crate has not been independently audited and is pre-1.0.
§Feature flags
There is one, and it only picks a backend for a primitive — no feature turns any of this crate’s API on or off.
x25519-cryptoxide(default) — back X25519’s software DH withcryptoxide’sx25519(the faster backend). Build with--no-default-featuresto fall back to theeccoxideladder; output is byte-for-byte identical, so this only changes which dependency carries the primitive.
§Modules
-
curve— Elliptic-curve math and key/handle types: ECDH on NIST P-256 (secp256r1), X25519 and X448, signing on P-256 (ECDSA) and Ed25519, plus theCurvetrait tying them to the type-level protocol. -
provider— the backends that perform a curve’s operations:EphemeralOnly(pure software, viaeccoxide/cryptoxide) and, on Apple platforms,AppleSecureEnclave(P-256 in the Secure Enclave; software Ed25519 with a hardware-sealed seed). -
noise— Compile-time Noise protocol descriptor. Encodes the handshake pattern, curve, cipher, and hash as zero-sized types so all buffer sizes and operations are known at monomorphisation time. -
psk— Pre-shared keys for the*psk*patterns (Kpsk0,IKpsk1): a fixed-sizePskmixed into the handshake hash. -
zeroize— Volatile zeroing of secret material. Prevents the compiler from eliding zero-fills viaptr::write_volatileand a compiler fence.
Plus one re-export, rand_core — the exact version whose
CryptoRng this crate’s public bounds name, so a consumer can name it
too.
Internal modules (not re-exported):
asn1— Minimal ASN.1 DER reader (and test-only writer) used to decode ECDSA signatures produced by Apple’s Security framework, which returns them in X9.62 / DER format rather than raw(r, s)bytes.
Re-exports§
pub use rand_core;
Modules§
- curve
- Elliptic curve traits, types, and implementations.
- noise
- Type-level Noise protocol framework.
- provider
- Cryptographic providers — the backends that perform a curve’s operations, and the trait family that defines them.
- psk
- Pre-shared key type for the
Noise_*psk*patterns. - zeroize
- Volatile zeroing of secret material.
Macros§
- noise
- Define a Noise handshake in the specification’s own pattern notation — see the macro’s documentation for the DSL and the generated API.
- noise_
message_ size - Compute the total wire size of a Noise handshake message at compile time.