Skip to content

feat(snow): rind-snow, a snow CryptoResolver on rind - #470

Closed
kixelated wants to merge 6 commits into
mainfrom
quest/m2/adoption/rind-snow
Closed

kixelated wants to merge 6 commits into
mainfrom
quest/m2/adoption/rind-snow

Conversation

@kixelated

@kixelated kixelated commented Nov 7, 2026 •

Copy link
Copy Markdown
Collaborator

Claims and completes quest/m2/adoption/rind-snow.md, and deletes it.

Summary

A new published crate, rind-snow 0.10.0, implements snow 0.10's CryptoResolver on rind: X25519, ChaCha20-Poly1305, AES-256-GCM, SHA-256, SHA-512, and the system random source. It returns None for everything else, so BLAKE2 and Curve448 come from a resolver the caller puts behind it with snow's FallbackResolver. It never falls back on its own.

let initiator = snow::Builder::with_resolver(params, rind_snow::resolver()?)
    .local_private_key(&key.private)?
    .build_initiator()?;
  • rind_snow::resolver() runs rind::cpu::check() and returns snow's own BoxedCryptoResolver. That box goes straight into Builder::with_resolver or FallbackResolver::new. No primitive runs before the check, since the resolver type is private.
  • snow passes Dh::dh its whole remote-key buffer (56 bytes, or 65 with snow's p256 feature), so the adapter takes the first 32 bytes. snow's Dh::set cannot fail, so a wrong-length private key leaves no key, and the first DH fails with Error::Dh. Noise uses every static key it sends in a DH, so the handshake fails before it completes. snow's own resolver instead zero-pads a short key and panics on a long one.
  • A small-order peer key fails with Error::Dh (rind's agree refuses the all-zero output, as aws-lc-rs does). snow's default resolver accepts it.
  • Each resolved cipher starts under a fresh random key until snow calls set, so sealing or rekeying before set cannot panic, and its output opens under no key anyone holds. Opening before set fails with Error::Decrypt.
  • The adapter wipes nothing itself. Its keys are rind types (the AEAD key, the X25519 PrivateKey, and the Scalar kept for Dh::privkey), so rind's wipe rule covers them: on drop today, and under rind's wipe feature once moq-dev/rind#461 lands. rind-snow does not forward that feature, like the other adapters. Digest state is not wiped, per AGENTS.md.
  • Wiring: workspace member, build.yml and sh/armv7.sh test lists, the README adapter table, MIGRATING.md (adapter map and the crate survey's snow row, which said an adapter was waiting on static X25519 key loading; PrivateKey::from_scalar now covers that), and MIGRATE.md's adapter list and CPU-check line.

Tests

  • tests/vectors.rs: cacophony's Noise vectors (Unlicense, at 8ee9d41e, the same file snow ships), cut to rind's four suites and ten patterns that together use every token, a one-way handshake, and PSKs. That makes 40 vectors in 62 KB, instead of 375 KB for every pattern. Each vector checks every message's ciphertext and the handshake hash.
  • tests/resolver.rs: XX, IK, KK, and NNpsk0 handshakes in both directions against snow's default (RustCrypto) resolver for every suite, then transport at 0 to 65519 bytes with rekeys both ways. Also: differential checks of each cipher, hash, HMAC, HKDF, rekey, and X25519 against the default resolver; malformed input (short ciphertext, small output, tampering, wrong nonce or AAD, no key set, short or small-order peer key, wrong-length private key); and BLAKE2s through FallbackResolver.
  • tests/ctgrind.rs: X25519, both ciphers with rekey, and HKDF on secret chaining keys, run by admin/ctgrind adapters.
  • Wycheproof and aws-lc-rs differential coverage of the primitives themselves stays in rind. snow has no aws-lc-rs resolver, so the adapter's own differential peer is snow's default resolver.

Benchmark

rind-bench/benches/snow.rs times one transport message sealed by one peer and opened by the other, per cipher and size, on rind, ring (snow's RingResolver), and rustcrypto (snow's default). The nightly native rows pick up rind and ring through their library filter; CodSpeed runs all three.

A local --quick run on a loaded i7-8700, for orientation only:

32B 1200B 1350B 16KB
ChaChaPoly rind 274 ns 1.23 µs 1.42 µs 13.7 µs
ChaChaPoly ring 266 ns 1.32 µs 1.48 µs 14.1 µs
ChaChaPoly rustcrypto 1.95 µs 3.96 µs 4.07 µs 25.9 µs
AESGCM rind 177 ns 635 ns 713 ns 8.91 µs
AESGCM ring 166 ns 705 ns 827 ns 7.88 µs
AESGCM rustcrypto 399 ns 1.98 µs 2.47 µs 23.2 µs

The AES-GCM 16KB gap to ring is most likely rind's AES-GCM itself on this AVX2-without-VAES CPU: the adapter adds only the plaintext copy into snow's output buffer, which ring's resolver makes too. The nightly rows will show whether it holds on the runners.

Decisions

  • Two panics behind snow's infallible trait methods, an exception to REVIEW.md's panic rule. Cipher::encrypt returns usize and Hash::result returns () (snow 0.10 src/types.rs:64, :111), so an output buffer too short for the sealed message or the digest has no error path. Within snow only the first is reachable, through its Token::S length check that leaves out the tag (reproduced on 0.10.0 with a 64-byte buffer for XX message 2; snow's main branch still has it); the second needs a caller using the resolved Hash directly with a short buffer. snow's own default and ring resolvers panic at both spots. ✅ Keep both panics, documented on each method: they're loud and match snow's resolvers (rejected: degrade silently by returning a zero length or a truncated digest, which would send a truncated handshake message while reporting success; wait for snow to make the methods fallible, a breaking snow change). Settled by the maintainer on 2026-11-07.

Choices made without asking

  • snow 0.10 only, and rathole is not named in the README. rathole reaches snow through snowstorm 0.4, which pins snow 0.9 and was last pushed in May 2022. rathole also calls snowstorm's Builder::new, which uses snow's built-in resolver. So rathole cannot opt in without changing its own code, whichever snow version this crate targets, and a snow 0.9 build would not fix that. The quest's Goal ("any snow user, such as rathole, opts in with no upstream change") holds for snow itself: no snow change is needed, but each user must call Builder::with_resolver.
  • Entry point resolver(), not default_provider(): snow calls this a resolver, and it returns snow's BoxedCryptoResolver. It still has its own MissingFeature and re-exports no rind types.
  • No XChaChaPoly or P-256, which snow offers only behind non-standard feature flags. The match arms tolerate those variants when another crate enables them.
  • snow's default HMAC and HKDF run over rind's SHA-2. They only matter for the handshake, whose cost is X25519.

Upstream draft (follow-up, not posted)

Posting this is outside the moq-dev and kixelated orgs, so the maintainer decides it separately; it does not block this PR.

snow has no AI or contribution policy beyond its license terms (README, SECURITY.md). This is a local misuse panic, not a remotely reachable one, so it suits a public issue rather than a security advisory. Draft for mcginty/snow:

Title: write_message panics when the buffer fits a static key but not its tag

HandshakeState::write_message checks byte_index + self.s.pub_len() > message.len() before encrypting the static key (src/handshakestate.rs, the Token::S arm), but once a key is set the encrypted key is pub_len + TAGLEN bytes. A buffer that passes the check but lacks room for the tag panics inside the resolver's Cipher::encrypt instead of returning Error::Input.

Reproduction on 0.10.0 with the default resolver: build an XX initiator and responder, deliver message 1, then call responder.write_message(&[], &mut [0u8; 64]). Message 2 needs 96 bytes (e, encrypted s, payload tag); 64 passes the check and panics at src/resolvers/default.rs:389. The ring resolver panics the same way.

Including the tag when the symmetric state has a key (self.s.pub_len() + if self.symmetricstate.has_key() { TAGLEN } else { 0 }) makes it return Error::Input, as the E1 and payload checks already account for it.

Public API and wire impact

New crate rind-snow 0.10.0: resolver() -> Result<BoxedCryptoResolver, MissingFeature> and MissingFeature. No change to rind or any other crate's API. Noise wire format is unchanged (verified against cacophony's vectors and snow's default resolver).

Caused by

None: new adapter.

Validation

  • just check on a77595e, and again on 8ee3141 after merging main (00610af): passed (rs, msrv, cross, deny, versions, bench, fixtures, patch, quest, gh, shell, nix), including every rind-snow test and its doctest.
  • sh/adapter-versions.sh: passes with rind-snow 0.10.0 on snow ^0.10.
  • ctgrind under valgrind did not run locally; ctgrind.yml runs admin/ctgrind adapters.

Follow-ups

  • Post the snow issue above: plan as a quest whose ## Waiting holds the maintainer's approval to post outside the orgs. It's a one-line upstream fix that removes the only snow-reachable panic rind-snow keeps.
  • rathole on rind-snow: drop. It needs snowstorm moved to snow 0.10 (snowstorm has been inactive since 2022) plus a rathole change to pass a resolver, so it's two outside parties for one consumer.

Models

Size: M
Implementer: Claude Code, claude-opus-5-5, high effort

(Written by Claude Opus 5.5 with high)

🤖 Generated with Claude Code

Review

Rewritten by quest review after each round.

  • Reviewer: Codex, gpt-6.1-sol, high effort
  • Session: 01a1b509-8d4d-7e23-995b-5ba8c5db7a00, round 4
  • Head: a77595e
  • Base: origin/main at 00610af
  • Score: 10/10, No findings remain after honoring the recorded maintainer exceptions; the new changes only document those decisions.
  • Findings: none
  • The last round's findings, at 118b1dc:
    • Refuted: medium, rind-snow/src/lib.rs:221: Reject undersized handshake output before encryption; The maintainer decision recorded in .tmp/body.md:46 explicitly accepts this panic, now documented at rind-snow/src/lib.rs:211.
    • Refuted: medium, rind-snow/src/lib.rs:274: Prevent short hash output buffers from reaching unchecked slicing; The same decision in .tmp/body.md:46 explicitly accepts short hash-output panics, now documented at rind-snow/src/lib.rs:277.

Head 8ee3141 adds only a conflict-free merge of main (00610af); git diff origin/main...8ee3141d prints the same as the diff of the reviewed head a77595e, so this record covers it.

kixelated and others added 2 commits November 6, 2026 23:27
Resolves X25519, ChaCha20-Poly1305, AES-256-GCM, SHA-256, SHA-512, and the
system random source on rind, and nothing else, so BLAKE2 and Curve448 come
from a resolver the caller puts behind it with snow's FallbackResolver.
Tested with cacophony's Noise vectors, handshakes and transport against
snow's default resolver, per-primitive differential checks, and ctgrind.
A transport benchmark compares it with snow's RustCrypto and ring
resolvers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title rind-snow feat(snow): rind-snow, a snow CryptoResolver on rind Nov 7, 2026
kixelated and others added 2 commits November 7, 2026 00:09
snow 0.10's default resolver has no Curve448, so the fallback recipe
covers BLAKE2 alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A caller that seals through the resolver's Cipher before set now gets a
message no one can open, rather than a panic.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
kixelated and others added 2 commits November 7, 2026 10:37
The maintainer kept the panics behind snow's infallible `Cipher::encrypt`
and `Hash::result`; each method now says when it panics. The adapter wipes
nothing itself, so the scalar comment no longer promises an unconditional
wipe that #461 makes opt-in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated marked this pull request as ready for review November 7, 2026 18:51
@pullfrog

pullfrog Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

Your OpenCode Go account is out of credit. Top it up, then re-trigger Pullfrog.

OpenCode Go's response
provider error: Insufficient account funds

Top up OpenCode Go →

Pullfrog  | Rerun failed job ➔ | View workflow run | via Pullfrog | Using glm-5.3

@coderabbitai

coderabbitai Bot commented Nov 7, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 48 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 5e9a7c61-57d7-4c5e-990f-227f4e6173cc
Commits

Reviewing files that changed from the base of the PR and between 00610af and 8ee3141.

Files ignored due to path filters: 1

  • Cargo.lock is excluded by !**/*.lock
Files selected for processing: 18

  • .github/workflows/build.yml
  • Cargo.toml
  • MIGRATE.md
  • MIGRATING.md
  • README.md
  • quest/m2/adoption/README.md
  • quest/m2/adoption/rind-snow.md
  • rind-bench/Cargo.toml
  • rind-bench/benches/snow.rs
  • rind-snow/Cargo.toml
  • rind-snow/LICENSE
  • rind-snow/README.md
  • rind-snow/src/lib.rs
  • rind-snow/tests/ctgrind.rs
  • rind-snow/tests/resolver.rs
  • rind-snow/tests/vectors.rs
  • rind-snow/tests/vectors/cacophony.json
  • sh/armv7.sh
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@kixelated
kixelated added this pull request to the merge queue Nov 7, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Nov 7, 2026
kixelated added a commit that referenced this pull request Nov 9, 2026
…0m's upstream fixes

The maintainer dropped them on 2026-11-09, once the launch post was up:
Deno as deprecated, rind-snow by closing #470 on purpose (which takes the
snow fix that existed for it), and str0m's upstream fixes as not worth
pursuing. Bun is the one outside post that goes ahead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant