feat(key-wallet): impl Zeroize for ExtendedPrivKey#833
Conversation
ExtendedPrivKey held its secp256k1::SecretKey (and ChainCode) with no Zeroize impl, so downstream code could not wrap it in zeroize::Zeroizing and had to scrub the scalar manually with SecretKey::non_secure_erase() on every path (error-prone; the scalar leaks un-wiped on `?`-early-return and panic-unwind paths). Add a hand-written `impl Zeroize for ExtendedPrivKey` mirroring the existing `RootExtendedPrivKey` and `ChainCode` impls. It is hand-written rather than `#[derive(Zeroize)]` because secp256k1::SecretKey exposes `non_secure_erase()` instead of a `Zeroize` impl, so a derive does not compile. The type stays `Copy` (no `Drop`), so this is additive and non-breaking; callers opt in via `zeroize::Zeroizing<ExtendedPrivKey>`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoY6vhmqZuHzNsMfJ8wakQ <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub>
📝 WalkthroughWalkthrough
ChangesExtended key lifecycle updates
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 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 |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## dev #833 +/- ##
==========================================
+ Coverage 72.82% 72.84% +0.02%
==========================================
Files 323 323
Lines 72438 72451 +13
==========================================
+ Hits 52755 52780 +25
+ Misses 19683 19671 -12
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@key-wallet/src/bip32.rs`:
- Around line 363-367: Add a focused unit test for ExtendedPrivKey::zeroize()
that exercises the new key-erasure behavior and verifies both the private key
material and chain_code are scrubbed after the call. Place the test alongside
the ExtendedPrivKey implementation in bip32.rs, and assert the fields are no
longer retaining their original values after zeroization to guard against
regressions.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 9bc079e7-62d3-41b0-97c5-56ff77866456
📒 Files selected for processing (1)
key-wallet/src/bip32.rs
…Network
Expand `impl Zeroize for ExtendedPrivKey` to wipe the whole value rather
than only the secret material. In addition to `private_key`
(non_secure_erase) and `chain_code`, it now clears `depth` and
`parent_fingerprint`, resets `child_number` to `Normal { index: 0 }`, and
resets `network` to its default — matching how `RootExtendedPrivKey`
zeroes all of its own fields.
`dashcore::Network` had no `Default`, so add `#[derive(Default)]` to it
with `Mainnet` (the `#[repr(u8)]` 0 discriminant) as the default. Additive
and non-breaking.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MoY6vhmqZuHzNsMfJ8wakQ
<sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub>
…drop Network Default Revert the `#[derive(Default)]` added to `dashcore::Network` and instead reset `network` to `Network::Mainnet` directly in `ExtendedPrivKey::zeroize` — Mainnet is the `#[repr(u8)]` 0 discriminant, i.e. the natural "zero" value. Keeps the change confined to key-wallet with no cross-crate Default impl. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MoY6vhmqZuHzNsMfJ8wakQ <sub>🤖 Co-authored by [Claudius the Magnificent](https://github.com/lklimek/claudius) AI Agent</sub>
|
@coderabbitai review all |
|
✅ Action performedFull review finished. |
ZocoLini
left a comment
There was a problem hiding this comment.
Is there a reason to not implement Drop trait and call the heroize implementation inside??
error[E0184]: the trait |
|
I see, I think it is worth it to implement Drop so we ensure the structure memory is always zeroed. I just checked the Copy derive and its only being used in one place, what about removing the Copy property, using clone() for that one place, and implementing Drop?? |
Removing Copy lets ExtendedPrivKey implement Drop, so its secret material is wiped automatically on scope exit instead of relying on callers to remember to zeroize. Mirrors the RootExtendedPrivKey convention. BREAKING CHANGE: ExtendedPrivKey no longer implements Copy; downstream code relying on implicit copies must clone or borrow. Fallout fixed: - bip32::derive_priv clones self instead of *self. - taproot-psbt example takes &ExtendedPrivKey (borrow, no clone). - key-wallet tests reorder/capture-or-clone where the value is reused after move. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@key-wallet/src/bip32.rs`:
- Around line 364-381: The ExtendedPrivKey zeroization implementation is relying
on SecretKey::non_secure_erase(), which does not satisfy the stronger wiping
guarantee implied by the Zeroize impl and Drop path. Update the
ExtendedPrivKey::zeroize method to use a zeroize-backed secret key handling
approach for the private_key field, or remove the public Zeroize promise if that
guarantee cannot be met; keep the rest of the field clearing in place.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 85b71ab4-76eb-45e1-b1a4-317db67729a8
📒 Files selected for processing (4)
dash/examples/taproot-psbt.rskey-wallet/src/bip32.rskey-wallet/src/tests/account_tests.rskey-wallet/src/tests/wallet_tests.rs
done |
Why this PR exists
ExtendedPrivKeyholds asecp256k1::SecretKey(+ChainCode) but has noZeroizeimpl, so its private scalar can't be wiped through the standardzeroizemachinery. Downstream code (e.g. dash-platform's mnemonic-resolver FFI signers) must scrub the scalar by hand withSecretKey::non_secure_erase()on every code path.Zeroize, callers can't wrap the key inzeroize::Zeroizing, so the private scalar leaks un-wiped whenever a?-early-return or a panic unwinds before the manualnon_secure_erase()is reached.Zeroizing<ExtendedPrivKey>, which removes the manual, per-path scrubbing entirely.What was done
Added a hand-written
impl Zeroize for ExtendedPrivKeyinkey-wallet/src/bip32.rsthat wipes the whole value (mirroring howRootExtendedPrivKeyzeroes all of its own fields):#[derive(Zeroize)]:secp256k1::SecretKeyexposesnon_secure_erase()rather than aZeroizetrait impl, so a derive does not compile.Dropimpl:ExtendedPrivKeyisCopy, andCopy + Dropis forbidden. Callers get drop-time wiping viazeroize::Zeroizing<ExtendedPrivKey>.networkreset toNetwork::Mainnet— the#[repr(u8)]0 discriminant, i.e. the natural zero value. No change todashcore::Network(kept confined tokey-wallet, single crate).Testing
cargo check -p key-wallet— passes.cargo fmt— clean.Breaking changes
None — additive
Zeroizeimpl;ExtendedPrivKeyis unchanged in layout and remainsCopy.🤖 Co-authored by Claudius the Magnificent AI Agent
Summary by CodeRabbit