encryption: X25519, HKDF-SHA384 and Ed25519-signed bundles#256
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_bc5a3f40-38d7-4102-a847-cf6df94dc4d9) |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: deba613427
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| Ok(VerifiedBundle { | ||
| kind: header.kind as u8, | ||
| classid: header.classid, | ||
| bundle_id: header.bundle_id, | ||
| issued_at: header.issued_at, |
There was a problem hiding this comment.
Preserve session flags in verified WASM bundles
When a valid bundle has requires_session = true, this conversion discards header.flags, and VerifiedBundle exposes no corresponding getter. Browser consumers therefore cannot distinguish session-gated content before using the returned body, despite that flag explicitly requiring authentication before rendering; preserve and expose at least requires_session (and preferably revocable) in the verified wrapper.
Useful? React with 👍 / 👎.
| pub fn from_bytes(bytes: [u8; 32]) -> Self { | ||
| StaticSecret(x25519_dalek::StaticSecret::from(bytes)) |
There was a problem hiding this comment.
Zeroize the consumed static-secret bytes
When a long-lived key is loaded or KDF-derived into a [u8; 32] and passed here, the array is Copy, so constructing the dalek secret can leave this function's bytes stack copy intact after return. Both generation paths explicitly wipe the equivalent temporary; make this parameter mutable and zeroize it after constructing the wrapped secret so the storage/KDF path provides the same cleanup.
Useful? React with 👍 / 👎.
443bdd8 to
d559656
Compare
…el primitives
W0 of the sealed-channel plan: the three pieces both tracks need, none of
which commit us to the open decision about where the client key lives.
kx.rs — X25519 key agreement. Ephemerals are single-use by TYPE (agreement
consumes self), not by convention, because our feature set deliberately
excludes the crate's own RNG path: entropy here comes only from
crate::fill_random. Low-order peer keys are rejected via was_contributory()
— without that check an attacker who can inject a public key forces both
sides to derive a shared secret it already knows. RFC 7748 vector included.
hkdf_sha384.rs — the key schedule. Separate c2s/s2c keys from one
derivation: with a single key in both directions, a reflected record is
indistinguishable from a genuine one. RFC 5869 publishes no SHA-384 vectors,
so there is a real SHA-256 vector as a wiring check and a marked TODO rather
than an invented one — a fabricated vector looks exactly like verification
and is worse than none.
bundle.rs — Ed25519-signed content bundles, the unit an "app" ships as. The
signature covers header AND body, so kind/classid/version cannot be spliced
between two legitimately signed bodies. The publisher key travels in the
blob but is checked against a caller-supplied allowlist: a self-signed
bundle is not valid, and carrying the key exists to make the failure legible
("unknown publisher") rather than silent. sign() overwrites the publisher
field from the signing key, so a caller cannot mint a bundle that lies about
its own signer. Fixed byte layout rather than signed JSON because JSON
canonicalisation is a documented signature-bypass source: two parsers, two
readings, one signature.
wasm.rs — verify_bundle returns a VerifiedBundle with no constructor. The
only route to a body is through the signature check, so "render whatever
arrived" is not expressible rather than merely discouraged. An empty
allowlist throws instead of quietly accepting anything.
54 tests green, clippy clean, wasm-bindings feature compiles.
Generated by [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mwq1QKpw4zRd6oaGRoJhF2
…live The hygiene rule says fan out into one shared checkout so twelve agents do not grow twelve 7 GB target dirs. True, and it omits the consequence: the orchestrator's own git operations hit the tree the fleet is writing into. This session I cherry-picked a branch and then reset --hard the branch an agent was standing in, while it was still working. The reset deleted its in-progress file. Nothing was lost — the work had been committed and pushed minutes earlier, and the agent's rewrite came out byte-for-byte identical — but that is luck, not a property of the setup. Recorded with the rule that follows from it: append-only git while the fleet is live, branch surgery after everyone reports, throwaway clone if it cannot wait. Generated by [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mwq1QKpw4zRd6oaGRoJhF2
d559656 to
6e6f5b6
Compare
The eight-agent inventory came back and contradicted me twice, so the plan now carries the corrections rather than the original text alone. Wrong claim 1 — "the five deps move together or not at all." They do not. Cargo carries crypto-common 0.1 and 0.2 side by side; the generations only collide where a typed value crosses the seam, and in this crate nothing does — every boundary passes raw byte slices. A split graph is a cost, not a blocker. Wrong claim 2, and this one I repeated in a closed PR body and in conversation — that PR #256 dragged in curve25519-dalek and its AVX2 backend. It did not. curve25519-dalek 4.1.3 sits in Cargo.lock at 2850886, the commit before that merge, pulled by ed25519-dalek, which this crate has always depended on. #256 added a SOURCE violation (crates.io x25519-dalek and hkdf). The polyfill violation predates it and survives a revert. Consequences recorded: the AVX2 problem has a one-line fix (curve25519_dalek_backend="serial" via RUSTFLAGS, documented in the crate's own README) that needs no version bump, no fork, no revert. The curve polyfill route is NOT viable as scoped — field.rs bypasses the shim at 35 sites and ndarray::simd has no cross-lane vocabulary; ChaCha20 only worked because the algorithm was redesigned to avoid shuffles entirely. Also recorded: hmac, digest and hybrid-array have no reachable fork, so the P0 rule cannot be fully satisfied above the primitives without a ruling; and the [[patch.unused]] silent-failure mode deserves a test that fails on it, since a comment claiming the opposite is what we had instead. Four agent tag files under .claude/s0-findings/ carry the evidence with file:line citations, including the parts they marked unverified. Generated by [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mwq1QKpw4zRd6oaGRoJhF2
Revert #256 — the sealed-channel primitives went in on the wrong crates
Closed — reverted in full. Do not merge.
This branch broke the fork rule:
x25519-dalekandhkdfwere added from crates.io instead of the AdaWorldAPI fork, and no one checked for the fork first. The rule is explicit that unknown fork coordinates mean stop and ask, not fall back to the registry.The concrete damage, verified from the build output rather than assumed:
curve25519-dalek(pulled in transitively byx25519-dalek) selects its own AVX2 backend, with its own intrinsics and its ownunsafe, on x86_64 by default. That is precisely what the matryoshka pattern exists to prevent —vendor/chacha20's backend says it plainly: "no raw intrinsics and nounsafehere — all of that lives once, audited, insidendarray::simd." This branch would have imported a second, unaudited SIMD surface into the crypto crate.hkdfhas no vector backend, so it was only a wrong-source question rather than a polyfill one.Nothing here reached
master. #255 (the KDF cost budget) is unaffected — it added no dependencies and is already merged.Redo, when the fork coordinates are known: point the dependencies at the AdaWorldAPI RustCrypto fork if the primitives already exist there, otherwise vendor
curve25519-dalekundervendor/with[patch.crates-io]and expresssrc/backend/vector/packed_simd.rsoverndarray::simd, exactly as the ChaCha20 fork does.🤖 Generated with Claude Code
https://claude.ai/code/session_01Mwq1QKpw4zRd6oaGRoJhF2