hiss is a maximally-static implementation of a curated subset of the
Noise Protocol Framework, with a pluggable
crypto-provider abstraction (software and Apple Secure Enclave backends).
⚠️ hissis pre-1.0 (0.1.x) and has not been independently audited. Do not use it to protect anything you cannot afford to lose. The API and the security posture described here may change before 1.0.
This document states what hiss defends against, what it does not, how to report
a vulnerability, and the concrete cryptographic posture of each backend — with the
validated evidence behind every claim.
Please report security issues privately. Do not open a public issue or PR for a suspected vulnerability.
Use GitHub's private vulnerability reporting:
➡️ https://github.com/primetype/hiss/security/advisories/new
(Repository → Security tab → Report a vulnerability.)
Please include enough detail to reproduce — the affected backend/pattern, a proof of concept or failing test if you have one, and the version/commit. We will acknowledge the report, work with you on an assessment, and coordinate a fix and disclosure. As a pre-1.0 project run by a small team there is no formal SLA, but we treat cryptographic issues as the highest priority.
If you cannot use GitHub advisories, contact the maintainer listed in
Cargo.toml.
| Version | Supported |
|---|---|
0.1.x (latest) |
✅ |
| anything older | ❌ |
Being pre-1.0, only the most recent 0.1.x release receives fixes; there are no
backports.
hiss deliberately implements a small, curated surface rather than the full
Noise matrix. The security claims below apply only to this surface:
| Component | Supported |
|---|---|
| Handshake patterns | N, K, Kpsk0, IKpsk1, IK, NK, IX, XK, NN, XX, X, NX, XN, KN, KK, KX, IN, NNpsk0, NNpsk2, XXpsk3 — all fifteen of the specification's fundamental patterns plus five PSK variants, one for every position a pskN modifier can name |
| Noise DH curves | NIST P-256 (the Noise DH curve), X25519 (the Noise 25519 curve, Curve25519 / RFC 7748), and X448 (the Noise 448 curve, RFC 7748) |
| Signing curve | Ed25519 (standalone signing; not a Noise DH curve — and as of this release not a DhCurve at all, so the type system enforces it) |
| AEAD cipher | ChaCha20-Poly1305 and AES-256-GCM — the Noise specification's two (§12.3 ChaChaPoly, §12.4 AESGCM) |
| Hash | BLAKE2b-512, SHA-512, SHA-256, BLAKE2s — the Noise specification's four (§12.5 SHA256, §12.6 SHA512, §12.7 BLAKE2s, §12.8 BLAKE2b) |
| Backends | Software (default, all platforms incl. WASM) and Apple Secure Enclave (macOS/iOS, opt-in) |
The non-P-256 DH curves carry a narrower assurance surface. All three DH curves (
P-256,25519,448) are fully shipped Noise handshake curves, but the validation behind them differs:
- Contributory-behaviour / low-order check. P-256 ECDH rejects a degenerate (point-at-infinity) shared secret. X25519 and X448 do not perform any low-order or contributory key check — per RFC 7748 a low-order peer key simply yields an all-zero shared secret rather than an error. This is spec-conformant (the check is optional in RFC 7748), but it means the Curve25519 / Curve448 paths give the caller no signal on a degenerate DH.
- Frozen known-answer coverage now spans all three, but from different sources. X25519 and X448 carry frozen third-party (
cacophony) Noise vectors across all seventeen patterns and all four hashes, and X25519 additionally has byte-for-byte live interop with snow — which runs in the separatehiss-interopcrate, on a cron and on demand, not undercargo test. P-256 keeps its own frozen corpus, but that one is agreement-with-snow by necessity — P-256 is not in the Noise specification, so no third-party P-256 Noise vectors exist. See Validated test vectors.Ed25519 is exposed for standalone signing through the provider API; it is not used as the curve inside a Noise pattern, and cannot be.
We assume an active network attacker who can read, drop, reorder, replay, and inject ciphertext on the wire, and who may know the long-term static public keys of the parties. The attacker does not know the private keys held by an honest party or a pre-shared key (PSK) they do not possess, and does not control the honest party's host OS or RNG.
- Confidentiality and integrity of payloads, to the degree each handshake pattern provides (see the table below).
- Sender/recipient authentication, to the degree each pattern provides.
- Rejection of tampered, truncated, over-length, replayed, and out-of-order messages (systematically tested; see Validated test vectors).
- Rejection of malformed or invalid peer public keys without panicking. For P-256, rejection extends to a degenerate (identity) DH output; for X25519 / X448, a low-order peer key yields an all-zero shared secret rather than an error (per RFC 7748 — see the Scope note).
- Deterministic, non-malleable ECDSA signing (RFC 6979 + low-S).
- Constant-time secret-scalar point multiplication in the software P-256 backend (subject to the upstream caveat in Side-channel posture).
Mutual authentication is conditional on the application checking the peer's static key. Completing IK / IKpsk1 / IX / XK / XX proves the peer holds the private half of a static key — not that you trust that identity. hiss surfaces the verified key via
Transport::remote_static()(and the handshake state'sremote_static()accessor) but does not check it against any policy for you; the application must compare it to its own trust store / allow-list and reject unknown peers. Seeexamples/tcp_xx_channel.rs.
- A compromised host OS, a malicious or broken RNG supplied by the caller, or physical/hardware attacks beyond what the platform or Secure Enclave mitigates.
- Traffic analysis (message sizes and timing of the application protocol itself).
- Patterns, curves, ciphers, or hashes outside the Scope table.
- Replay protection for the one-way patterns (
N/K/Kpsk0/X) — they are one-shot seals by construction; replay resistance is a property only of the interactiveIK/IKpsk1/NK/IX/XK/NN/XXhandshakes. See the per-pattern table. - Anything a caller does with secret bytes after copying them out of a
hisstype (e.g. viaseed()).
These follow the payload-security properties defined by the Noise specification for
the one-way and interactive patterns. Two different things establish hiss's
conformance to them, and they are worth keeping apart.
What every build checks is the frozen corpora: the P-256 known-answer
vectors across the full per-pattern matrix, and the third-party cacophony corpus
replayed over X25519 and X448 across all seventeen patterns in both roles. These are
files of bytes; replaying them needs no second Noise implementation, so they run on
every cargo test and in every release gate.
Where the P-256 bytes came from is byte-for-byte agreement with
snow, the reference implementation — not an
independent standards-body vector set, because P-256 is not part of the Noise spec
(see the provenance note under
Validated test vectors). That agreement is a statement
about generation time. It is re-checked live — including X25519 across
N / IK / XX — by the suite in the separate hiss-interop crate, which runs
weekly and on demand rather than per-commit.
| Pattern | Flow | Sender authentication | Confidentiality to recipient | Forward secrecy | Replay resistance | PSK |
|---|---|---|---|---|---|---|
| N | one message (-> e, es) |
none — sender is anonymous | encrypted to the recipient's known static key | sender-side only (fresh ephemeral per message); lost if the recipient's static key is compromised | no (one-way) | — |
| K | one message (-> e, es, ss) |
sender's static key, vulnerable to key-compromise impersonation (auth rests on the static–static ss DH) |
encrypted to the recipient's known static key | sender-side only | no (one-way) | — |
| Kpsk0 | one message (-> psk, e, es, ss) |
sender's static key plus a PSK (a second factor) — but the static-key half is the same static–static ss DH as K's and is equally vulnerable to key-compromise impersonation; the PSK is what an attacker additionally needs, not a repair of ss. (The specification tabulates no psk-modified pattern, so this is read across from K, whose msg1 this is with psk prepended.) |
recipient's static key plus the PSK — an attacker needs the relevant private key and the PSK | sender-side only | no (one-way) | position 0 |
| IKpsk1 | two messages (-> e, es, s, ss, psk / <- e, ee, se) |
mutual — responder authenticated to the initiator via es/ss (the DHs that bind the responder's static key); initiator authenticated to the responder via ss then se — plus a PSK |
recipient's static key plus the PSK; the initiator's identity is hidden from a passive eavesdropper | full once both ephemerals are mixed (ee) |
yes for the handshake (the responder contributes a fresh ephemeral) — but the msg1 (0-RTT) payload is replayable on its own, since nothing of the responder's contributes to it, and it is authenticated only by the static–static ss DH, so it is KCI-forgeable; both harden once msg2 lands. The PSK does not stop the replay: an attacker replays recorded ciphertext and needs no PSK knowledge |
position 1 |
| IK | two messages (-> e, es, s, ss / <- e, ee, se) |
mutual — responder authenticated to the initiator via es/ss (the DHs that bind the responder's static key); initiator authenticated to the responder via ss then se |
encrypted to the recipient's known static key; the initiator's identity is hidden from a passive eavesdropper | full once both ephemerals are mixed (ee) |
yes for the handshake (the responder contributes a fresh ephemeral) — but the msg1 (0-RTT) payload is replayable on its own, since nothing of the responder's contributes to it, and it is authenticated only by the static–static ss DH, so it is KCI-forgeable; both harden once msg2 lands |
— |
| NK | two messages (-> e, es / <- e, ee) |
none for the initiator — it is anonymous (no static key); the responder is authenticated to the initiator via the es DH that binds the responder's known static key |
encrypted to the responder's known static key | full once both ephemerals are mixed (ee) |
yes for the handshake (the responder contributes a fresh ephemeral) — but the msg1 payload is replayable on its own, for the same reason as IK's; unlike IK it carries no sender authentication at all, so there is no KCI clause |
— |
| IX | two messages (-> e, s / <- e, ee, se, s, es) |
mutual — no pre-known statics: the initiator is authenticated to the responder via se, the responder to the initiator via es; both statics are sent in-handshake |
payload encrypted after ee; the initiator's static identity is sent in the clear in msg1 (before any DH) and is exposed to a passive eavesdropper; the responder's static is sent in msg2 after ee and is encrypted |
full once both ephemerals are mixed (ee) |
yes (responder contributes a fresh ephemeral) | — |
| XK | three messages (-> e, es / <- e, ee / -> s, se) |
mutual — the responder is authenticated to the initiator via the es DH that binds the responder's known static key; the initiator is authenticated to the responder via se |
encrypted to the responder's known static key; the initiator's static identity is sent encrypted in msg3 (after ee) and is hidden from a passive eavesdropper |
full once both ephemerals are mixed (ee) |
yes for the handshake (the responder contributes a fresh ephemeral) — but the msg1 payload is replayable on its own, exactly as NK's is, and likewise carries no sender authentication |
— |
| NN | two messages (-> e / <- e, ee) |
none — both parties are anonymous (no static keys); no protection against an active man-in-the-middle | only against a passive eavesdropper, and only from msg2 onward — there are no static keys, so a passive observer cannot read the traffic once ee keys the cipher, but an active MITM can impersonate either side. A payload declared on msg1 (-> e) closes before any DH and therefore travels in cleartext, readable by anyone |
full once both ephemerals are mixed (ee) |
yes (responder contributes a fresh ephemeral) | — |
| XX | three messages (-> e / <- e, ee, s, es / -> s, se) |
mutual — no pre-known statics: the initiator is authenticated to the responder via se, the responder to the initiator via es; both statics are sent in-handshake |
payload encrypted after ee; both statics are sent encrypted (the responder's in msg2, the initiator's in msg3, both after ee), so both identities are hidden from a passive eavesdropper |
full once both ephemerals are mixed (ee) |
yes (responder contributes a fresh ephemeral) | — |
| X | one message (-> e, es, s, ss) |
sender's static key, vulnerable to key-compromise impersonation (auth rests on the static–static ss DH); the sender's static is sent encrypted in-band (after es), not pre-shared |
encrypted to the recipient's known static key; the sender's identity is hidden from a passive eavesdropper (its static rides encrypted after es) |
sender-side only (fresh ephemeral per message); lost if the recipient's static key is compromised | no (one-way) | — |
| NX | two messages (-> e / <- e, ee, s, es) |
responder only — the initiator is anonymous (it has no static key); the responder is authenticated to the initiator via es |
payload encrypted after ee; the responder's static is sent encrypted in msg2, so it is hidden from a passive eavesdropper — but any anonymous initiator can ask for it, since nothing gates the handshake |
full once both ephemerals are mixed (ee) |
yes (responder contributes a fresh ephemeral) | — |
| XN | three messages (-> e / <- e, ee / -> s, se) |
initiator only — the responder is anonymous (it has no static key); the initiator is authenticated to the responder via se |
payload encrypted after ee; the initiator's static is sent encrypted in msg3, so it is hidden from a passive eavesdropper — but it is sent to a responder that was never authenticated, and an active MITM can impersonate that responder outright |
full once both ephemerals are mixed (ee) |
yes (responder contributes a fresh ephemeral) | — |
| KN | two messages (-> e / <- e, ee, se), initiator's static pre-shared (-> s) |
initiator only — authenticated via se; the responder is never authenticated |
payload encrypted after ee, but the responder's identity is unverified, so the responder-to-initiator direction has only weak forward secrecy until the responder receives a transport message; nothing identifying is transmitted, though an active attacker impersonating the initiator who later obtains a candidate for the initiator's private key can confirm it |
full once both ephemerals are mixed (ee) |
yes (responder contributes a fresh ephemeral) | — |
| KK | two messages (-> e, es, ss / <- e, ee, se), both statics pre-shared (-> s, <- s) |
mutual — the responder via es/ss, the initiator via ss then se. The msg1 (0-RTT) payload's authentication rests on the static–static ss DH alone and is therefore KCI-forgeable; it hardens once msg2 lands |
encrypted to the recipient's known static key; nothing identifying is transmitted, but a passive attacker can test candidate (responder private key, initiator public key) pairs against a recording | full once both ephemerals are mixed (ee) |
yes for the handshake (the responder contributes a fresh ephemeral) — but the msg1 (0-RTT) payload is replayable on its own, exactly as IK's is |
— |
| KX | two messages (-> e / <- e, ee, se, s, es), initiator's static pre-shared (-> s) |
mutual — the initiator via se, the responder via es |
payload encrypted after ee; the responder's static is sent encrypted in msg2 but with only weak forward secrecy — the initiator's alleged ephemeral may have been forged by an active attacker, who could later compromise the initiator's static key, decrypt msg2, and learn which responder answered. The initiator's own pre-shared key carries KN's active-attacker caveat |
full once both ephemerals are mixed (ee) |
yes (responder contributes a fresh ephemeral) | — |
| IN | two messages (-> e, s / <- e, ee, se) |
initiator only — authenticated via se; the responder is never authenticated |
payload encrypted after ee; the initiator's static identity is sent in the clear in msg1 (before any DH) and is exposed to a passive eavesdropper — the weakest identity exposure of any pattern this crate ships. The responder's identity is unverified, so its own sends have only weak forward secrecy until it receives a transport message |
full once both ephemerals are mixed (ee) |
yes (responder contributes a fresh ephemeral) | — |
Notes:
-
N,K,Kpsk0, andXare one-way seals (a single message, no response). They provide no replay protection and only sender-side forward secrecy; they are intended for encrypting data at rest to a known public key (for example, sealing a per-pair PSK to a device's own Secure Enclave key). Because there is no recipient ephemeral, compromise of the recipient's static private key exposes past payloads. -
Xis a one-way, sender-authenticated seal with sender-identity hiding. LikeKit authenticates the sender via the static–staticssDH, but whereKpre-shares both statics out of band,Xpre-knows only the recipient's static (<- s) and transmits the sender's static encrypted in-band (aftereskeys the cipher), so the sender's identity is hidden from a passive eavesdropper. Its single message is the same token sequence asIK's msg1 with no responder reply. -
IKandIKpsk1are the interactive mutually-authenticated handshakes (IKpsk1layers a PSK on top ofIK). Forward secrecy and replay resistance are established only after the second message (theeeDH). -
NKis an interactive, responder-authenticated handshake: the initiator is anonymous (it carries no static key), and only the responder is authenticated — to the initiator — via theesDH that binds the responder's known static key. Like the other interactive patterns, forward secrecy and replay resistance follow the second message (theeeDH). -
IXis an interactive, mutually-authenticated handshake with no pre-messages: neither party knows the other's static key in advance, so both transmit their statics during the handshake. The trade-off is identity privacy for the initiator — its static is sent in msg1 before any DH runs, in the clear, so a passive eavesdropper learns the initiator's identity (the responder's static, sent in msg2 afteree, is encrypted). Forward secrecy and replay resistance follow the second message (theeeDH). UseIXwhen neither side can pre-share the other's static and exposing the initiator's identity is acceptable; preferIKwhen the responder's static is known in advance and initiator identity privacy matters. -
XKis an interactive, mutually-authenticated, three-message handshake with strong initiator-identity privacy. The initiator pre-knows the responder's static (pre-message<- s) and authenticates the responder early via theesDH; the initiator's own static is transmitted encrypted in msg3, aftereekeys the cipher, so it is hidden from a passive eavesdropper, and is authenticated viase. Compared withIK(which sends the initiator's static in msg1),XKspends an extra round trip to give the initiator's identity full forward-secret confidentiality. Forward secrecy and replay resistance follow the second message (theeeDH). -
NNis the unauthenticated interactive handshake: both parties are anonymous (there are no static keys and no pre-messages), so it provides no authentication of either side and no protection against an active man-in-the-middle — an attacker who sits on the wire can complete a separate handshake with each party and relay traffic. Confidentiality holds only against a passive eavesdropper; forward secrecy and replay resistance follow the second message (theeeDH). UseNNonly when an authenticated pattern is genuinely impossible, or layer authentication above it. -
XXis the canonical interactive, mutually-authenticated, three-message handshake with no pre-messages: neither party knows the other's static key in advance, so both transmit their statics during the handshake. UnlikeIX, both statics are sent encrypted — the responder's in msg2 and the initiator's in msg3, each aftereekeys the cipher — so both identities are hidden from a passive eavesdropper. The initiator is authenticated to the responder viaseand the responder to the initiator viaes. Forward secrecy and replay resistance follow the second message (theeeDH). PreferXXoverIXwhen initiator identity privacy matters and neither side can pre-share the other's static; preferIK/XKwhen the responder's static is known in advance. -
A PSK (
Kpsk0,IKpsk1) is an additional authentication and confidentiality factor layered on top of the asymmetric authentication — not a replacement for it. -
NXandXNare the half-authenticatedXXvariants: one side has no static key at all. InNXthe initiator is unauthenticated, so any anonymous caller can make the responder identify itself; inXNthe responder is unauthenticated, so the initiator hands over its identity without knowing who received it. Both are therefore vulnerable to an active man-in-the-middle impersonating the anonymous side — the authenticated half is genuinely authenticated, the other half is not there to be. -
KN,KKandKXpre-share the initiator's static (-> s), which means it is never transmitted — not that it is private. The specification distinguishes two cases here and so should we. ForKNandKXit is an active attacker: one who impersonates the initiator without holding its private key and later obtains a candidate for that private key can confirm the guess. ForKKit is a passive attacker: one who records a handshake can test candidate (responder private key, initiator public key) pairs. Different attacker, different secret. -
KKis the zero-RTT pattern — the only interactive one here whose first message already carries an encrypted payload — and that payload is both KCI-vulnerable (its authentication is the static–staticssDH, forgeable by anyone holding the responder's private key) and replayable (nothing of the responder's contributes to msg1). This is the same warning the one-wayKrow carries, for the same reason, and it applies toIKandIKpsk1's msg1 payloads too. Both properties harden once msg2 lands. -
The
K/Iresponder caveat. For every pattern whose name begins withKorI—KN,KK,KX,IN, and the already-shippedIK,IKpsk1andIX— the responder is only guaranteed weak forward secrecy for the transport messages it sends until it receives a transport message from the initiator. The initiator's static is either pre-shared or arrives early, so the responder starts sending before it has evidence binding the initiator's ephemeral to a key it has verified. It does not apply to the one-wayK/Kpsk0, whose responder never sends.
Every secret-scalar point multiplication — key generation (public()), ECDH
(dh()), and the ECDSA signing nonce's k·G — routes to a constant-time routine
in the underlying eccoxide curve library: a fixed-base comb (mul_base) for
base-point multiplies and a fixed-window algorithm with a constant-time table scan
(scale_am3_ct) for variable-base multiplies. There is no secret-dependent branch
or table index on these paths.
Honest caveats:
- This is an upstream property.
hissdoes not implement or independently verify constant-time behaviour; it relies on theeccoxiderelease it depends on — currently the crates.ioeccoxide0.4, whose P-256 backend carries the constant-time scalar multiplication. The guarantee is therefore only as strong as that upstream code. - The fixed-base comb relies on
eccoxide'stablefeature (enabled by default in this crate'seccoxidedependency). Without it the fixed-base multiply is still correct and constant-time, just not the precomputed comb — this is a performance, not a constant-time, difference. - Scope is point multiplication. ECDSA signing also performs a secret-scalar
modular inversion (
k⁻¹) and scalar arithmetic. Ineccoxide0.4 the inversion is a fixed addition-chain exponentiation (no secret-dependent branching by construction); the residual trust iseccoxide's underlying scalar field being constant-time, which is not independently verified here. - Verification is intentionally variable-time. ECDSA signature verification
computes
u1·G + u2·Qwith a variable-time multiply (mul_vartime). Every input on the verify path — public key, signature, message hash — is public, so this is safe and faster; no secret is involved.
P-256 static private keys are generated inside the Secure Enclave and stored in the Data Protection Keychain as non-exportable key references; the private key bytes never leave the hardware. Side-channel resistance for those operations is the platform's responsibility.
Honest caveats:
- Ed25519 is not enclave-backed. The Secure Enclave has no Ed25519 support, so
Ed25519 signing is performed in software (via
cryptoxide). For the Enclave provider, only the Ed25519 seed's at-rest storage is hardware-protected: the seed is sealed (Noise-Nto the device's Secure-Enclave P-256 key) into a 129-byte envelope and stored as a data-protection Keychain item, identically on macOS and iOS. Protection of that item isAfterFirstUnlockThisDeviceOnly— i.e. device first-unlock, not a per-use biometric prompt. - The Keychain seal/store/load round-trip is not exercised in CI (it needs a
codesigned binary with the keychain entitlement and real Secure Enclave hardware);
that test is marked
#[ignore].
Ed25519 signing and verification are delegated to cryptoxide. hiss does not add an
independent constant-time audit of that implementation.
The crypto and protocol layers are pinned against the following corpora, run as in-tree tests:
| Suite | Count / scope | Source & format |
|---|---|---|
| Wycheproof ECDSA (secp256r1, SHA-256) | 484 vectors | Project Wycheproof, DER/ASN.1; every vector decoded and verified |
| Wycheproof ECDH (secp256r1) | 355 vectors | Project Wycheproof, ecpoint encoding |
| Wycheproof AES-GCM (AES-256, 96-bit nonce, 128-bit tag) | 66 vectors (39 valid, 27 invalid) | Project Wycheproof aes_gcm_test.json, the subset matching Noise §12.4's parameters, run against the primitive; ciphertext and tag pinned on every valid vector, every invalid one rejected. No truncated-tag vectors exist at the pin — truncation and single-bit tag flips are covered by bespoke tests in src/noise/cipher.rs |
| RFC 6979 deterministic ECDSA | Appendix A.2.5 (P-256/SHA-256) KAT | RFC 6979; raw (r, s) pinned for "sample" and "test" |
| NIST ECC CDH | P-256 Count=0 |
NIST CAVP ECDH vector |
| Noise handshake KATs (P-256 / BLAKE2b) | all seventeen patterns | frozen, replayed byte-for-byte (handshake ciphertexts + handshake hash + transport) |
| Noise handshake KATs (P-256 / SHA-256) | patterns N / IKpsk1 / XX |
frozen, replayed byte-for-byte |
Noise handshake KATs, third-party (cacophony) |
20 patterns (the seventeen shipped plus NNpsk0/NNpsk2/XXpsk3) × {25519, 448} × {ChaChaPoly, AESGCM} × {BLAKE2b, BLAKE2s, SHA256, SHA512} — 320 vectors |
frozen, replayed byte-for-byte (handshake ciphertexts + recovered payloads + revealed statics + handshake hash + every transport message); every pattern additionally replayed in the responder role on all sixteen suites — 320 initiator + 320 responder replays, plus the staged IK and IKpsk1 responder reads per suite = 672 tests. The thirteen interactive patterns pin responder-written bytes; the four one-way patterns have no responder write, so theirs pin the recipient read path and its transport receives. For AESGCM the transport replays are what pin the §12.4 big-endian nonce, which first differs from a little-endian one at transport message 2 (one-way patterns; message 4 for interactive ones, whose senders alternate). See tests/vectors/cacophony/PROVENANCE.md |
| FIPS 180-4 SHA-256 digests | "", "abc", the 448-bit message |
NIST, pinned as hex |
| RFC 4231 HMAC-SHA-256 | cases 1, 2, 3, 6 | RFC 4231 §4, pinned as hex |
| FIPS 180-4 SHA-512 digests | "", "abc", the 896-bit message |
NIST, pinned as hex |
| RFC 4231 HMAC-SHA-512 | cases 1, 2, 3, 6 | RFC 4231 §4, pinned as hex |
| RFC 7693 BLAKE2s digest | "abc" |
RFC 7693 Appendix B, pinned as hex |
| HMAC-BLAKE2s | RFC 4231 inputs 1, 2, 3, 6 | not standards-body vectors — none exist for HMAC-BLAKE2; cross-generated and agreed by two implementations independent of cryptoxide |
| Noise interop, X25519 | patterns N / IK / XX |
byte-for-byte agreement with snow, live (unfrozen). Runs in hiss-interop — a weekly cron plus workflow_dispatch — not under cargo test and not a release gate |
| Noise interop, X25519 × AESGCM | patterns N / NN / XX / IK / Kpsk0 × all four hashes × both role assignments |
byte-for-byte agreement with snow's RustCrypto AES-GCM, live (unfrozen), four transport messages per direction so the nonce counter passes 0 in every pattern shape. Same hiss-interop schedule — not a release gate |
| Noise round-trip, X448 | pattern XX |
hiss↔hiss self round-trip; snow has no 448 resolver, so no live interop is possible |
| Negative / boundary sweeps | per-pattern, deterministic | every-byte tamper, every-prefix truncation, over-length, ciphertext bit-flip, replay, out-of-order, wrong-PSK → all rejected |
The Wycheproof corpora are third-party authoritative. The negative sweeps are generated deterministically (each driver runs a genuine handshake that must complete first, so the tests are non-vacuous).
Provenance of the Noise KATs (read this). There are two corpora here and they are not equally strong.
The P-256 corpora — BLAKE2b and SHA-256 — are "agreement with snow": they assert byte-for-byte equality with the snow reference implementation, not with a standards body. That is unavoidable rather than lazy: P-256 is not a curve in the Noise specification, so no third-party P-256 Noise vectors exist anywhere. A latent bug shared with snow would not be caught by them. They are regenerated by the
#[ignore]generators inhiss-interop; the procedure, and the additions-only discipline that governs the resulting diff, are inhiss-interop/README.md.The
cacophonycorpus is third-party: 320 frozen vectors over25519and448, both ciphers, from a community corpus neitherhissnorsnowproduced, acquired fromsnow's package and verified byte-identical to the copy in the Cacophony Haskell implementation's own repository. The assertions are stricter thansnow's own harness, which does not even deserialize thehandshake_hashfield. For X448 this is the only cross-implementation check that exists at all:snow's default resolver returnsNonefor448, sosnowskips every448vector it ships.Scope that claim honestly: these are third-party relative to
snow, not standards-body vectors.hissdid not audit the Cacophony implementation and its generation was not reviewed here. What they buy is agreement with a second, independent implementation.tests/vectors/cacophony/PROVENANCE.mdrecords the pins, the filter and the licence chain.Residual: these replays call only the plain readers; the
read_message_N_withper-peer-PSK and verification-closure variants are never exercised against third-party vectors.The primitives underneath are separately anchored: the SHA-256, SHA-512 and BLAKE2s rows above are NIST / RFC 4231 / RFC 7693 vectors — except HMAC-BLAKE2s, for which no standards body publishes vectors at all. RFC 7693 defines no HMAC and Wycheproof ships no HMAC-BLAKE2 file, so those four values are cross-generated and agreed by two implementations independent of
cryptoxide. They are not presented as standards-body vectors. The stronger check on that path is thecacophonycorpus, which runsBlake2s::hmacon everymix_keyof 22 handshakes against an implementationhissdid not write.
- Deterministic nonces. ECDSA signing uses RFC 6979 deterministic nonces (HMAC-SHA256 DRBG); no randomness is involved in signing, eliminating the catastrophic private-key leak from nonce reuse or a biased RNG.
- Low-S on signing. Produced signatures are normalized to the canonical low-S
form (
s = min(s, n − s)), sohissnever emits a malleable high-S signature. - Verification accepts high-S. Verification follows standard ECDSA and accepts
both low-S and high-S encodings (matching the Wycheproof
validexpectations). A caller that needs strict low-S on inbound signatures must enforce it itself. - Strict DER (Apple path only). When parsing DER signatures produced by Apple's
Security framework, the ASN.1 reader is strict: it rejects long-form/indefinite
lengths, non-minimal integer encodings, trailing data, and negative
r/s(per X.690 §8.3.2). The pure-software P-256 path uses the fixed 64-byter‖srepresentation and does not parse DER at all.
hiss pulls in no entropy source of its own. getrandom is not a runtime
dependency of the published crate.
- The software provider is
EphemeralOnly<R>, owning a caller-suppliedR: CryptoRng(rand_core0.10, re-exported ashiss::rand_core). You must supply a cryptographically secure RNG. - With
rand0.10,SysRng— the system source — is fallible (TryRng, withError = SysError) and so does not implement the infallibleCryptoRngthe bound wants; seed a real CSPRNG, e.g.rand::make_rng::<StdRng>(). - P-256 key generation rejection-samples the secret scalar into
[1, n−1]. On a broken RNG it fails with a typedScalarSamplingFailederror after a bounded number of retries (per-iteration miss probability< 2⁻³²) rather than returning a biased key. (With a sound CSPRNG this path is effectively unreachable.) Psk::generateis infallible: any 32 random bytes form a valid PSK, so there is no error path to surface.
Primary secret types are wiped on drop using volatile writes followed by a best-effort compiler fence:
- P-256 and Ed25519 private keys
PskSharedSecretCipherState(AEAD key + nonce) — including the cipher's expanded key, whichCipher::Keyrequires each implementation to scrub on dropSymmetricState(chaining key; handshake mix/splitintermediates are wiped inline)- The datagram receive ratchet's current and previous epoch keys
The live AES-GCM key schedule is wiped by hiss, not left to cryptoxide. Since
0.4.0 the Cipher trait holds an expanded key for the life of a session, so for
AesGcm the AES-256 round keys and the GHASH subkey are a long-lived struct field
rather than a per-call stack value — and the first two AES-256 round keys are the
Noise key verbatim. cryptoxide does zero them in its own Drop impls, but with
ordinary stores: built under fat LTO, every one of those stores is absent from the
disassembly, because the compiler is entitled to drop a write to storage that is about
to be released. AesGcmKey's destructor therefore runs cryptoxide's and then wipes
the bytes itself with the same volatile-write-plus-fence loop as everything else above.
Honest limits:
- The fence is best-effort. It defeats dead-store elimination of the wipe, but it is not a hardware memory barrier and gives no guarantee against secret copies that the compiler placed in CPU registers or spilled to the stack.
- Transient copies of the expanded key are not wiped. A move in Rust copies the
value's bytes and does not erase the source, so every move of a key-bearing value can
leave a dead image that nothing scrubs:
AesGcm256::newreturns its value and it is then moved into the wrapper, into anOption, and into a struct field; andTransport::into_datagram_with_epochmoves the receive key out of itsCipherStateinto the epoch ratchet, leaving the moved-from slot's key bytes as dead stack of that function (the source state is unkeyed afterwards, so its drop wipes nothing). This is the same class as the point above — once per key or once per session, rather than once per message as before 0.4.0. - Library-owned intermediates are not zeroized. Secret values materialized inside
the underlying libraries —
eccoxideScalar/Pointvalues created during DH, signing, and key generation, andcryptoxide-internal scratch buffers other than the AEAD key schedule described above — are not wiped, becausehisscannot reach inside those library-owned types. This is a known, accepted limitation that depends on upstream support to close. seed()exposes raw secret bytes.SoftwareEd25519PrivateKey::seed()returns the raw 32-byte seed (needed to persist/seal the key). The owning key zeroizes on drop, but any bytes a caller copies out are the caller's responsibility.
- Not audited; pre-1.0. No external cryptographic audit has been performed; the API and posture may change before 1.0.
- Curated surface. Only the patterns, curves, ciphers, and hashes in Scope are implemented — not the full Noise matrix.
- No
fallback/ compound protocols. The Noisefallbackmodifier (XXfallback, Noise Pipes / 0-RTT-with-retry) is intentionally not implemented. It is optional in the spec and unnecessary for the targeted use cases — a deliberate scoping decision, not an oversight.snowomits it likewise. - Noise KAT provenance. The frozen P-256 Noise vectors are agreement-with-snow,
not standards-body vectors. X25519 and X448 now carry frozen third-party
(
cacophony) vectors, but "third-party" there means independent of snow, not from a standards body — nobody in this chain audited the Cacophony implementation. - Upstream constant-time dependency. Constant-time P-256 rests on
eccoxide's released crates.io0.4; the property is only as strong as that upstream backend, whichhissdoes not independently verify. - Unproven Enclave path. The Apple Keychain seal/store/load path is not exercised in CI (requires codesigned binary + Secure Enclave hardware).
This document describes the security posture of hiss as of 0.1.x. If you find a
discrepancy between this document and the code, that is itself a bug worth reporting.