Skip to content

fix(security): zeroize signer secret key material#345

Open
cryptocake wants to merge 6 commits into
vianetwork:mainfrom
cryptocake:fix/issue-337-zeroize-secrets
Open

fix(security): zeroize signer secret key material#345
cryptocake wants to merge 6 commits into
vianetwork:mainfrom
cryptocake:fix/issue-337-zeroize-secrets

Conversation

@cryptocake

@cryptocake cryptocake commented Mar 26, 2026

Copy link
Copy Markdown

Summary

This addresses issue #337 by moving signer secret storage to zeroizing byte buffers and reconstructing ephemeral SecretKey values only when needed for signing.

Changes

  • core/lib/via_btc_client
    • add zeroize dependency
    • store KeyManager secret key as Zeroizing<[u8; 32]>
    • derive ephemeral SecretKey / Keypair for signing operations
  • via_verifier/lib/via_musig2
    • add zeroize dependency
    • store Signer secret key as Zeroizing<[u8; 32]>
    • reconstruct ephemeral SecretKey when creating nonce / partial signature

Why

  • reduces lingering in-memory exposure of long-lived private key material
  • removes explicit long-lived Keypair / SecretKey retention where not strictly necessary

Closes #337.

@gemini-code-assist

Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly improves the security posture of the application by implementing a strategy to minimize the in-memory exposure of cryptographic secret keys. By utilizing zeroizing byte buffers, sensitive key material is actively cleared from memory when no longer strictly required, thereby mitigating risks associated with memory forensics or accidental leakage. This change ensures that long-lived private key data is not persistently held in its raw form, enhancing overall system resilience against certain types of attacks.

Highlights

  • Enhanced Security for Key Material: The pull request introduces the zeroize crate to securely handle sensitive key material, ensuring that secret keys are zeroized from memory after use, reducing their lingering exposure.
  • KeyManager Refactoring: The KeyManager in via_btc_client now stores the secret key as Zeroizing<[u8; 32]> and reconstructs ephemeral SecretKey and Keypair instances only when needed for signing operations (ECDSA and Schnorr) and public key retrieval.
  • Signer Refactoring: The Signer in via_musig2 has been updated to store its secret key as Zeroizing<[u8; 32]> and reconstructs an ephemeral SecretKey when creating nonces and partial signatures.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request enhances security by integrating the zeroize crate to handle secret keys more securely. The KeyManager and Signer structs now store secret keys as Zeroizing<[u8; 32]>, ensuring they are zeroed out from memory when no longer needed. Signing operations reconstruct SecretKey instances on demand from these zeroized byte arrays. The review suggests extracting the secret key reconstruction logic in KeyManager into a helper method for improved clarity and consistency, and using SecretKey::from_slice for consistency across crates.

Comment on lines 112 to 131
fn sign_ecdsa(&self, msg: Message) -> BitcoinSignerResult<ECDSASignature> {
let signature = self.secp.sign_ecdsa(&msg, &self.sk);
let sk = SecretKey::from_slice(self.sk.as_ref())
.map_err(|e| BitcoinError::SigningError(format!("Invalid cached secret key: {}", e)))?;
let signature = self.secp.sign_ecdsa(&msg, &sk);
Ok(signature)
}

fn sign_schnorr(&self, msg: Message) -> BitcoinSignerResult<SchnorrSignature> {
let signature = self.secp.sign_schnorr_no_aux_rand(&msg, &self.keypair);
let sk = SecretKey::from_slice(self.sk.as_ref())
.map_err(|e| BitcoinError::SigningError(format!("Invalid cached secret key: {}", e)))?;
let keypair = Keypair::from_secret_key(&self.secp, &sk);
let signature = self.secp.sign_schnorr_no_aux_rand(&msg, &keypair);
Ok(signature)
}

fn get_public_key(&self) -> PublicKey {
self.sk.public_key(&self.secp)
SecretKey::from_slice(self.sk.as_ref())
.map(|sk| sk.public_key(&self.secp))
.expect("KeyManager secret key must be valid")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

To improve code clarity and reduce duplication, consider extracting the logic for reconstructing the SecretKey into a private helper method within impl KeyManager. This pattern is already used in via_verifier/lib/via_musig2/src/lib.rs with signing_secret_key.

You could add a method like this to impl KeyManager:

    fn reconstruct_secret_key(&self) -> BitcoinSignerResult<SecretKey> {
        SecretKey::from_slice(self.sk.as_ref())
            .map_err(|e| BitcoinError::SigningError(format!("Invalid cached secret key: {}", e)))
    }

Then, you can simplify sign_ecdsa, sign_schnorr, and get_public_key to use this new helper method.

Comment thread via_verifier/lib/via_musig2/src/lib.rs Outdated
}

fn signing_secret_key(&self) -> Result<SecretKey, MusigError> {
SecretKey::from_byte_array(self.secret_key.as_ref())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

For consistency with core/lib/via_btc_client/src/signer/mod.rs, consider using SecretKey::from_slice instead of SecretKey::from_byte_array. The from_slice method is standard in secp256k1 and would make the code more uniform across the crates.

Suggested change
SecretKey::from_byte_array(self.secret_key.as_ref())
SecretKey::from_slice(self.secret_key.as_ref())

@cryptocake

Copy link
Copy Markdown
Author

@gemini-code-assist Thanks — addressed your review requests.

Updated in latest commit (aa360a012):

  • extracted KeyManager::signing_secret_key() helper to avoid repeated reconstruction logic
  • switched to SecretKey::from_slice(...) for consistency in via_musig2 (including helper + signer constructors)

Could you please take another look?
/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request integrates the zeroize crate to enhance the secure handling of private keys within both the KeyManager in via_btc_client and the Signer in via_musig2. The change involves storing secret keys as Zeroizing<[u8; 32]> and reconstructing SecretKey instances only when required for signing operations. A review comment highlights a potential panic and performance inefficiency in KeyManager::get_public_key due to the use of .expect() and repeated public key derivation, suggesting caching the public key as an improvement.

Comment on lines 130 to 134
fn get_public_key(&self) -> PublicKey {
self.sk.public_key(&self.secp)
self.signing_secret_key()
.map(|sk| sk.public_key(&self.secp))
.expect("KeyManager secret key must be valid")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

Using .expect() here introduces a potential panic if signing_secret_key() were to fail. While the key should be valid if constructed via KeyManager::new or KeyManager::default, it's safer and more efficient to avoid re-deriving the public key on every call.

A better approach is to cache the PublicKey in the KeyManager struct when it's created. This avoids the potential panic and is more performant.

This would involve:

  1. Adding a pk: PublicKey field to the KeyManager struct.
  2. In KeyManager::new and KeyManager::default, deriving the public key once and storing it in the new pk field.
  3. This function can then simply return the stored public key: self.pk.

@cryptocake

Copy link
Copy Markdown
Author

@gemini-code-assist Thanks for the follow-up review — addressed.

Updated in latest commit (e976eef6f):

  • KeyManager now caches public_key in struct state
  • get_public_key() now returns the cached value directly
  • removed repeated derivation and eliminated the expect() path there

Please re-review.
/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request enhances security by integrating the zeroize crate to manage secret keys within the KeyManager and Signer structs. Secret keys are now stored using Zeroizing<[u8; 32]> to ensure their memory is zeroed upon dropping. The KeyManager struct has been refactored to store the public key directly and reconstruct the SecretKey and Keypair on demand for signing operations, improving memory hygiene and reducing the exposure of sensitive data. Additionally, the SecretKey creation method was updated from from_byte_array to from_slice in relevant functions. There is no feedback to provide.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses issue #337 by reducing long-lived in-memory exposure of Bitcoin/secp256k1 secret key material. It migrates signer implementations to store secrets in zeroizing byte buffers and reconstruct SecretKey/Keypair values only at the point of signing.

Changes:

  • Add zeroize as a dependency to via_btc_client and via_musig2.
  • Store signer secret material as Zeroizing<[u8; 32]> in KeyManager and Signer.
  • Reconstruct ephemeral SecretKey/Keypair values during signing instead of retaining long-lived SecretKey/Keypair fields.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
core/lib/via_btc_client/src/signer/mod.rs Switch KeyManager to store secret key bytes in Zeroizing<[u8; 32]> and derive ephemeral keys for ECDSA/Schnorr signing.
core/lib/via_btc_client/Cargo.toml Add zeroize dependency.
via_verifier/lib/via_musig2/src/lib.rs Store Signer secret as Zeroizing<[u8; 32]> and reconstruct SecretKey when creating nonces / partial signatures.
via_verifier/lib/via_musig2/Cargo.toml Add zeroize dependency.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +19 to +23
#[derive(Clone)]
pub struct KeyManager {
secp: Secp256k1<All>,
sk: SecretKey,
sk: Zeroizing<[u8; 32]>,
public_key: PublicKey,

Copilot AI Mar 26, 2026

Copy link

Choose a reason for hiding this comment

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

KeyManager still derives Clone, which will duplicate the underlying Zeroizing<[u8; 32]> secret buffer whenever the struct is cloned. That increases the number of in-memory copies of the secret key material and undercuts the goal of minimizing exposure; consider removing Clone (it doesn’t appear required by BitcoinSigner) or implementing a custom clone strategy that doesn’t copy secret material (e.g., wrap the secret bytes in an Arc<Zeroizing<_>> if sharing is needed).

Copilot uses AI. Check for mistakes.
Comment thread via_verifier/lib/via_musig2/src/lib.rs Outdated
Comment on lines +280 to +281
let secret_key = secp256k1_musig2::SecretKey::from_slice(&private_key.inner.secret_bytes())
.with_context(|| "Error to compute the coordinator sk")?;

Copilot AI Mar 26, 2026

Copy link

Choose a reason for hiding this comment

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

This constructs a SecretKey from &private_key.inner.secret_bytes(), which creates a temporary [u8; 32] copy of the secret on the stack that is dropped without being zeroized. Since this PR is explicitly about reducing lingering key material, consider wrapping the secret_bytes() result in Zeroizing (or otherwise explicitly zeroizing that temporary) before calling SecretKey::from_slice.

Suggested change
let secret_key = secp256k1_musig2::SecretKey::from_slice(&private_key.inner.secret_bytes())
.with_context(|| "Error to compute the coordinator sk")?;
let secret_key_bytes = Zeroizing::new(private_key.inner.secret_bytes());
let secret_key =
secp256k1_musig2::SecretKey::from_slice(&*secret_key_bytes)
.with_context(|| "Error to compute the coordinator sk")?;

Copilot uses AI. Check for mistakes.
Comment on lines 306 to 309
let private_key = PrivateKey::from_wif(private_key_wif)?;
let secret_key =
secp256k1_musig2::SecretKey::from_byte_array(&private_key.inner.secret_bytes())
.with_context(|| "Error to compute the coordinator sk")?;
let secret_key = secp256k1_musig2::SecretKey::from_slice(&private_key.inner.secret_bytes())
.with_context(|| "Error to compute the coordinator sk")?;
let secp = secp256k1_musig2::Secp256k1::new();

Copilot AI Mar 26, 2026

Copy link

Choose a reason for hiding this comment

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

Same as above: private_key.inner.secret_bytes() produces a temporary [u8; 32] secret copy that is not zeroized after SecretKey::from_slice returns. To align with the zeroization goal, use a Zeroizing<[u8; 32]> (or explicit zeroize() on the temp) for these bytes as well.

Copilot uses AI. Check for mistakes.
@cryptocake

Copy link
Copy Markdown
Author

@copilot-pull-request-reviewer Thanks — addressed your comments in latest commit (e8c52b9b9).

Changes made:

  • removed Clone derive from KeyManager to avoid duplicating secret material buffers
  • wrapped temporary secret_bytes() values in Zeroizing<[u8; 32]> in both signer constructors before SecretKey::from_slice

Please re-review.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread core/lib/via_btc_client/src/signer/mod.rs Outdated
Comment thread core/lib/via_btc_client/src/signer/mod.rs Outdated
Comment thread via_verifier/lib/via_musig2/src/lib.rs
@cryptocake

cryptocake commented Mar 26, 2026

Copy link
Copy Markdown
Author

@copilot-pull-request-reviewer Thanks — addressed the 3 new suggestions in commit 8eaea5d80.

Changes:

  • fixed inaccurate error text for wpubkey_hash() failure to "public key is uncompressed"
  • fixed KeyManager::default() network mismatch (Address::p2wpkh(..., Network::Regtest))
  • updated Signer::new API to accept Zeroizing<[u8; 32]> secret bytes and updated call sites (including example), reducing long-lived SecretKey usage at call boundaries

Please re-review. @romanornr

@romanornr

Copy link
Copy Markdown
Collaborator

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request integrates the zeroize crate to improve the security of sensitive cryptographic material by ensuring secret keys are cleared from memory after use. The KeyManager and Signer structures were refactored to store secret keys within Zeroizing wrappers, and helper methods were added to safely retrieve SecretKey instances for signing. Furthermore, the KeyManager now caches the public key, and the default network in its Default implementation has been updated to Regtest. I have no feedback to provide as no review comments were present.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread via_verifier/lib/via_musig2/src/lib.rs Outdated
Comment on lines +300 to +304
let signer = Signer::new(
Zeroizing::new(secret_key.secret_bytes()),
signer_index,
all_pubkeys.clone(),
None,

Copilot AI Mar 27, 2026

Copy link

Choose a reason for hiding this comment

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

get_signer currently creates secret_key_bytes (Zeroizing) and later creates a second copy of the secret via Zeroizing::new(secret_key.secret_bytes()), and also clones all_pubkeys even though it’s no longer used after this call. This increases in-memory exposure and allocations unnecessarily; prefer reusing/moving the existing secret_key_bytes into Signer::new and pass all_pubkeys by value (no clone) after computing public_key in a narrower scope.

Copilot uses AI. Check for mistakes.
Comment thread via_verifier/lib/via_musig2/src/lib.rs Outdated
Comment on lines +334 to +336
Zeroizing::new(secret_key.secret_bytes()),
signer_index,
all_pubkeys.clone(),

Copilot AI Mar 27, 2026

Copy link

Choose a reason for hiding this comment

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

Same issue as get_signer: this call site duplicates the secret key bytes with Zeroizing::new(secret_key.secret_bytes()) despite already having secret_key_bytes, and clones all_pubkeys unnecessarily. Reuse/move the original Zeroizing<[u8;32]> into Signer::new and avoid cloning the pubkey vector to reduce secret copies and allocations.

Suggested change
Zeroizing::new(secret_key.secret_bytes()),
signer_index,
all_pubkeys.clone(),
secret_key_bytes,
signer_index,
all_pubkeys,

Copilot uses AI. Check for mistakes.
@cryptocake

cryptocake commented Mar 27, 2026

Copy link
Copy Markdown
Author

@copilot-pull-request-reviewer Thanks, addressed both new comments in commit d29c013.

Changes made:

  • Reused existing secret_key_bytes in both get_signer and get_signer_with_merkle_root (no second Zeroizing::new(secret_key.secret_bytes()) copy).
  • Removed unnecessary all_pubkeys.clone() at both call sites; pass all_pubkeys by value into Signer::new.

Please re-review.

@cryptocake

cryptocake commented Mar 27, 2026

Copy link
Copy Markdown
Author

Review update

I did a full local pass on PR #345 in a checked-out branch (pr-345) and validated the touched security paths.

What I checked:

  • via_verifier/lib/via_musig2/src/lib.rs
  • core/lib/via_btc_client/src/signer/mod.rs
  • dependency additions for zeroize

Validation run:

  • cargo check --manifest-path via_verifier/lib/via_musig2/Cargo.toml
  • cargo check --manifest-path core/lib/via_btc_client/Cargo.toml

Result:

  • No blocking compile issues found locally.
  • Secret handling changes (moving to zeroized byte buffers + ephemeral SecretKey reconstruction) look consistent with the stated goal in Secret key zeroization #337.

From this pass, I don't see a blocker to merge.

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.

Secret key zeroization

3 participants