fix(core): route seed access through the zeroizing wrapper (#41)#64
Conversation
Remove Keypair::seed_bytes(), which returned a bare [u8; 32] copy of the Ed25519 private seed. Because [u8; 32] is Copy with no Drop, each caller's copy was released without being scrubbed, leaving secret key material in freed memory. The zeroizing accessor to_seed() -> Zeroizing<[u8; 32]> already existed beside it; this makes it the sole seed accessor and migrates the two in-crate callers (envelope open path and a crypto test) to it via deref coercion. No behavior change to envelope encryption: the derived X25519 secret is byte-identical, covered by the existing round-trip tests. Adds an identity test pinning to_seed() as the only seed accessor.
|
Warning Review limit reached
More reviews will be available in 38 minutes and 47 seconds. Learn how PR review limits work. To continue reviewing without waiting, enable usage-based billing in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits. 🚦 How do rate limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthrough
ChangesKeypair Seed Zeroization Hardening
Estimated code review effort🎯 2 (Simple) | ⏱️ ~8 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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 |
…-trip The to_seed_is_sole_seed_accessor test was named for a compile-time property its body couldn't assert, and its from_seed -> DID check duplicated from_seed_round_trip. Replace it with a type-annotated binding that fails to compile if to_seed ever returns a bare [u8; 32] again, plus the determinism check, which is the only behavior it uniquely covered.
main grew a third seed_bytes() caller (repos.rs, from #40) after this branch was cut. Route it through to_seed() too: node_seed is moved into a long-lived tokio::spawn pin task, so the Zeroizing wrapper scrubs the copy when that task ends. encrypt_and_pin takes &[u8; 32]; the wrapper deref-coerces, so the call site is unchanged.
|
@kevincodex1 #64 is ready when you have a minute. It removes the unzeroized One thing to flag: It's also the base of #66 (the X25519 follow-up), so merging this unblocks that one. |
x25519_secret_from_seed derived the X25519 private scalar from the Ed25519 seed and returned it as a bare [u8; 32]. Because [u8; 32] is Copy with no Drop (and sha2's digest output likewise has no zeroize), the scalar, the SHA-512 digest, and the returned temporary were released without being scrubbed, leaving secret-derived material in freed memory. This is the same Copy-no-Drop class fixed for the raw seed in #41, on the derived secret. Return Zeroizing<[u8; 32]>, build the scalar directly into a zeroizing buffer so no bare secret local persists, and explicitly wipe the SHA-512 digest before it drops. Callers deref the result into crypto_box::SecretKey, which already scrubs its own copy. No behavior change: the derived scalar and all decryption output are byte-identical, covered by the existing ed25519_to_x25519_keypair_agrees and seal_open_round_trip_for_recipients tests. Stacked on #41 (#64).
Summary
Keypair::seed_bytes()handed out a bare[u8; 32]copy of the Ed25519 private seed. Because[u8; 32]isCopywith noDrop, each caller's copy was released without being scrubbed, leaving secret key material in freed memory. This removes that accessor and routes seed access through the existing zeroizing wrapper.Motivation & context
Closes #41
to_seed() -> Zeroizing<[u8; 32]>already existed right besideseed_bytes()and scrubs the copy on drop. There was no reason to keep an unzeroized accessor: it was duplicated, and the only two callers were both ingitlawb-core. Removing it makesto_seed()the single seed accessor and closes the exposure window to core dumps, swap, and memory-disclosure bugs.Kind of change
What changed
Crate touched:
gitlawb-core.Keypair::seed_bytes()(raw, unzeroized seed accessor) inidentity.rs.to_seed(): the envelope-decryption path inencrypt.rs(open_blob) and a crypto agreement test. The migration is mechanical:Zeroizing<[u8; 32]>deref-coerces to&[u8; 32], which is whatx25519_secret_from_seedalready takes.to_seed_is_sole_seed_accessordocumenting and guarding the single-accessor invariant.No behavior change to envelope encryption/decryption: the X25519 secret derived from the seed is byte-identical, covered by the existing round-trip tests.
How a reviewer can verify
Before you request review
cargo test --workspacepasses locallycargo fmt --allandcargo clippy --workspace --all-targets -- -D warningsare cleanfeat(...),fix(...),docs(...)).env.exampleupdated if behavior or config changed (or N/A)Protocol & signing impact
did:key, Ed25519 / RFC 9421 signatures, UCAN, ref certs, or P2P wire formatsThis is in-memory hygiene only. It does not change the signature scheme, key format, DID derivation, or any wire format. The bytes produced and consumed are identical; only the container type of a transient changes.
Notes for reviewers
There is a related, deliberately out-of-scope leak one hop downstream:
x25519_secret_from_seedreturns the derived X25519 secret as a bare[u8; 32]. It will be filed as its own follow-up issue to keep this PR scoped to exactly #41.Summary by CodeRabbit