Skip to content

fix(rs-sdk-ffi): zeroize private key arrays after use in crypto/signer FFI#3433

Merged
QuantumExplorer merged 2 commits into
v3.1-devfrom
fix/zeroize-private-key-arrays
Apr 3, 2026
Merged

fix(rs-sdk-ffi): zeroize private key arrays after use in crypto/signer FFI#3433
QuantumExplorer merged 2 commits into
v3.1-devfrom
fix/zeroize-private-key-arrays

Conversation

@QuantumExplorer
Copy link
Copy Markdown
Member

@QuantumExplorer QuantumExplorer commented Apr 3, 2026

Issue Being Fixed

Security audit finding M5: Private key byte arrays in non-shielded FFI functions (crypto/mod.rs and signer_simple.rs) were not being zeroized after use, leaving key material lingering on the stack.

The shielded crypto module (shielded/crypto/address.rs, bundle_build.rs, decrypt.rs) already handles this correctly via zeroize::Zeroize. This PR brings the non-shielded FFI functions to the same standard.

What was changed

Wrapped all [u8; 32] private key arrays with zeroize::Zeroizing<> so they are automatically zeroed on drop. This covers all return paths including early returns, with zero runtime overhead beyond the memset on drop.

Files changed:

  • packages/rs-sdk-ffi/src/crypto/mod.rs — 3 functions: dash_sdk_validate_private_key_for_public_key, dash_sdk_private_key_to_wif, dash_sdk_public_key_data_from_private_key_data
  • packages/rs-sdk-ffi/src/signer_simple.rs — 1 function: dash_sdk_signer_create_from_private_key

Breaking Changes

None. Zeroizing<[u8; 32]> implements Deref<Target=[u8; 32]>, so all existing call sites (.as_slice(), &key_array, indexing) work transparently.

Tests

Existing compilation and runtime behavior unchanged — Zeroizing is a zero-cost wrapper that only adds a zeroize call on drop.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Security

    • Enhanced cryptographic key material handling in FFI modules to ensure sensitive data is properly cleared from memory.
  • Chores

    • Removed unnecessary dependency from build configuration.

…r FFI

Private key byte arrays in non-shielded FFI functions were not being
zeroized after use, leaving key material on the stack. The shielded
crypto module already handles this correctly via zeroize::Zeroize.

Wrap all `[u8; 32]` key arrays with `Zeroizing<>` so they are
automatically zeroed on drop, covering all return paths including
early returns.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@QuantumExplorer QuantumExplorer requested a review from shumkov as a code owner April 3, 2026 18:20
@github-actions github-actions Bot added this to the v3.1.0 milestone Apr 3, 2026
@thepastaclaw
Copy link
Copy Markdown
Collaborator

thepastaclaw commented Apr 3, 2026

Review Gate

Commit: 53513248

  • Debounce: 19m ago (need 30m)

  • CI checks: checks still running (2 pending)

  • CodeRabbit review: comment found

  • Off-peak hours: off-peak (12:11 PM PT Friday)

  • Run review now (check to override)

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 3, 2026

📝 Walkthrough

Walkthrough

Removed serde_json dependency from build configuration and introduced zeroize::Zeroizing wrappers around 32-byte private-key buffers in two cryptographic FFI functions to ensure memory is securely cleared when buffers are dropped.

Changes

Cohort / File(s) Summary
Dependency Management
packages/rs-scripts/Cargo.toml
Removed unused serde_json = "1" dependency from build configuration.
Cryptographic Memory Zeroization
packages/rs-sdk-ffi/src/crypto/mod.rs, packages/rs-sdk-ffi/src/signer_simple.rs
Wrapped local private-key buffers ([0u8; 32]) with zeroize::Zeroizing to ensure sensitive key material is securely zeroed when buffers are dropped in FFI functions. Function signatures and control flow remain unchanged.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 A rabbit hops through memory lanes,
Sweeping zeros, breaking chains,
Keys once held now drift away,
Safely wiped—no trace to stay!
Zeroize magic, clean and bright,
Security bunny guards the night. 🔐

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main security-focused change: wrapping private key arrays with zeroize::Zeroizing in FFI functions across crypto/mod.rs and signer_simple.rs.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/zeroize-private-key-arrays

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

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 3, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 80.49%. Comparing base (7011b62) to head (5351324).
⚠️ Report is 6 commits behind head on v3.1-dev.

Additional details and impacted files
@@             Coverage Diff              @@
##           v3.1-dev    #3433      +/-   ##
============================================
+ Coverage     80.22%   80.49%   +0.26%     
============================================
  Files          2855     2855              
  Lines        281685   284101    +2416     
============================================
+ Hits         225983   228678    +2695     
+ Misses        55702    55423     -279     
Components Coverage Δ
dpp 73.21% <ø> (+0.20%) ⬆️
drive 82.49% <ø> (+0.14%) ⬆️
drive-abci 86.67% <ø> (ø)
sdk 36.55% <ø> (ø)
dapi-client 79.06% <ø> (ø)
platform-version ∅ <ø> (∅)
platform-value 88.44% <ø> (+4.20%) ⬆️
platform-wallet 76.09% <ø> (ø)
drive-proof-verifier 55.26% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

cargo-machete flagged serde_json as unused in rs-scripts, causing CI
failure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/rs-sdk-ffi/src/crypto/mod.rs (1)

53-71: ⚠️ Potential issue | 🟠 Major

Zeroizing the stack buffer is good, but key bytes still persist in a non-zeroized Vec<u8>.

At lines 53, 113, and 188, private keys are decoded into private_key_bytes (Vec<u8>) and then copied into Zeroizing<[u8; 32]>. The Vec<u8> copy is not zeroized on drop, so sensitive material can still linger in heap memory.

Use hex::decode_to_slice directly into the Zeroizing buffer to avoid creating the extra secret copy.

Suggested pattern (apply in all 3 functions)
-    let private_key_bytes = match hex::decode(private_key_str) {
-        Ok(bytes) if bytes.len() == 32 => bytes,
-        Ok(_) => {
-            return DashSDKResult::error(DashSDKError::new(
-                DashSDKErrorCode::InvalidParameter,
-                "Private key must be exactly 32 bytes".to_string(),
-            ))
-        }
-        Err(e) => {
-            return DashSDKResult::error(DashSDKError::new(
-                DashSDKErrorCode::InvalidParameter,
-                format!("Invalid private key hex: {}", e),
-            ))
-        }
-    };
-
-    let mut key_array = Zeroizing::new([0u8; 32]);
-    key_array.copy_from_slice(&private_key_bytes);
+    let mut key_array = Zeroizing::new([0u8; 32]);
+    if let Err(e) = hex::decode_to_slice(private_key_str, &mut *key_array) {
+        return DashSDKResult::error(DashSDKError::new(
+            DashSDKErrorCode::InvalidParameter,
+            format!("Invalid private key hex: {}", e),
+        ));
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/rs-sdk-ffi/src/crypto/mod.rs` around lines 53 - 71, The code decodes
hex into a temporary Vec<u8> named private_key_bytes and then copies into a
Zeroizing<[u8; 32]> (key_array), leaving the heap copy around; change each
occurrence (where private_key_str is decoded at lines referenced) to allocate
key_array as Zeroizing::new([0u8; 32]) first and use
hex::decode_to_slice(private_key_str, &mut *key_array) (or equivalent) to decode
directly into the zeroized stack buffer, validate the returned length/error and
avoid calling copy_from_slice on a Vec so no intermediate Vec<u8> containing the
secret is ever created (apply same change for all three functions/locations that
use private_key_str / private_key_bytes / key_array).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/rs-sdk-ffi/src/crypto/mod.rs`:
- Around line 53-71: The code decodes hex into a temporary Vec<u8> named
private_key_bytes and then copies into a Zeroizing<[u8; 32]> (key_array),
leaving the heap copy around; change each occurrence (where private_key_str is
decoded at lines referenced) to allocate key_array as Zeroizing::new([0u8; 32])
first and use hex::decode_to_slice(private_key_str, &mut *key_array) (or
equivalent) to decode directly into the zeroized stack buffer, validate the
returned length/error and avoid calling copy_from_slice on a Vec so no
intermediate Vec<u8> containing the secret is ever created (apply same change
for all three functions/locations that use private_key_str / private_key_bytes /
key_array).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 58226d2a-5e04-43de-b13e-df78a5f28a68

📥 Commits

Reviewing files that changed from the base of the PR and between 7969880 and 5351324.

📒 Files selected for processing (3)
  • packages/rs-scripts/Cargo.toml
  • packages/rs-sdk-ffi/src/crypto/mod.rs
  • packages/rs-sdk-ffi/src/signer_simple.rs
💤 Files with no reviewable changes (1)
  • packages/rs-scripts/Cargo.toml

@QuantumExplorer
Copy link
Copy Markdown
Member Author

Self reviewed

@QuantumExplorer QuantumExplorer merged commit f131258 into v3.1-dev Apr 3, 2026
40 checks passed
@QuantumExplorer QuantumExplorer deleted the fix/zeroize-private-key-arrays branch April 3, 2026 19:18
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 3, 2026

✅ DashSDKFFI.xcframework built for this PR.

SwiftPM (host the zip at a stable URL, then use):

.binaryTarget(
  name: "DashSDKFFI",
  url: "https://your.cdn.example/DashSDKFFI.xcframework.zip",
  checksum: "6af15c010dd65b2dab699725e8ab77c6ded24a7145096f4571e12d1380bb898f"
)

Xcode manual integration:

  • Download 'DashSDKFFI.xcframework' artifact from the run link above.
  • Drag it into your app target (Frameworks, Libraries & Embedded Content) and set Embed & Sign.
  • If using the Swift wrapper package, point its binaryTarget to the xcframework location or add the package and place the xcframework at the expected path.

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.

2 participants