Skip to content

Add Keycard support#451

Open
jonesmarvin8 wants to merge 28 commits intomarvin/refactor-wallet-pub-accfrom
marvin/keycard-commands
Open

Add Keycard support#451
jonesmarvin8 wants to merge 28 commits intomarvin/refactor-wallet-pub-accfrom
marvin/keycard-commands

Conversation

@jonesmarvin8
Copy link
Copy Markdown
Collaborator

@jonesmarvin8 jonesmarvin8 commented Apr 17, 2026

🎯 Purpose

Integrates Keycard for public transactions for auth-transfers.

⚙️ Approach

  • Keycard python class
  • Rust wrapper for Keycard python class
  • Add Wallet command to check availability of Keycard and load new keys
  • Update Wallet auth-transfer and pinata to use Keycard

🧪 How to Test

Follow setup for Keycard before this.

In root:

  • Run wallet_with_keycard.sh to install Wallet and the necessary python packages.
  • With Keycard inserted, run keycard_tests.sh (in root)

🔗 Dependencies

PR 452 (and PR 457) must be merged first.

🔜 Future Work

  • PR 461 addresses Token program and privacy commands.
  • Remove keycard-py from being included in the repo; currently necessary due to updates to keycard-py that is incompatible with the firmware.

📋 PR Completion Checklist

Mark only completed items. A complete PR should have all boxes ticked.

  • Complete PR description
  • Implement the core functionality
  • Add/update tests
  • Add/update documentation and inline comments

@jonesmarvin8 jonesmarvin8 changed the base branch from main to marvin/refactor-wallet-pub-acc April 21, 2026 17:41
@jonesmarvin8 jonesmarvin8 marked this pull request as ready for review April 28, 2026 00:46
@jonesmarvin8 jonesmarvin8 marked this pull request as draft April 28, 2026 19:55
@jonesmarvin8 jonesmarvin8 marked this pull request as ready for review April 29, 2026 00:46
Comment thread keycard_wallet/src/python_path.rs Outdated
let current_dir = env::current_dir().expect("Failed to get current working directory");

let paths_to_add: Vec<PathBuf> = vec![
current_dir.join("python"),
Copy link
Copy Markdown
Collaborator

@moudyellaz moudyellaz Apr 29, 2026

Choose a reason for hiding this comment

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

two issues in the python integration:
current_dir().join("python") puts cwd-relative python/ at sys.path[0]. after cargo install --path wallet --force the binary is global, so running it from any directory containing a malicious python/keycard_wallet.py executes that file in-process with PIN and mnemonic access.

wallet_with_keycard.sh:14 does git clone --branch lee-schnorr of bitgamma/keycard-py unpinned. whoever pushes there controls signing for every install.

both need fixing before this lands. resolve the path from current_exe(), and either vendor keycard-py properly or pin a commit hash and verify in CI.

Comment thread python/keycard_wallet.py Outdated

import keycard

PIN = '123456'
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

let's drop these. setup_communication(pin=PIN, …) and load_mnemonic(mnemonic=DEFAULT_MNEMONIC, …) use them as defaults, so a future caller forgetting an arg silently signs with 123456 or loads "fashion degree mountain...". if test fixtures need a known mnemonic, move it to a test-only file.

Comment thread wallet/src/cli/keycard.rs Outdated
#[arg(short, long)]
mnemonic: Option<String>,
#[arg(short, long)]
pin: Option<String>,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

--pin on argv leaks to shell history, ps, xtrace, and stays in heap unzeroed. for a hardware key the PIN is the whole moat once the card is taken. let's read it from a TTY prompt (rpassword) or env var, and store it in Zeroizing<String>. same applies to the other subcommands taking pin: Option<String> (programs/native_token_transfer.rs:40,73, pinata.rs:31, account.rs:42, token.rs:80,111).

Comment thread keycard_wallet/src/lib.rs Outdated
.call_method1("get_public_key_for_path", (path,))?
.extract()?;

let public_key: [u8; 32] = public_key.try_into().expect("Expect 32 bytes");
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

wrong PIN, removed card, secure-channel timeout, all the everyday failure modes hit .expect() and panic the tokio worker. these aren't invariants. let's propagate PyResult (or a typed KeycardError) up to the CLI handler and reserve expect for things that can't fail.

(also Expect a valid public key1 / key2 look like leftover debug.)

same applies to the matching expects in program_facades/native_token_transfer/public.rs:60,72,113,118,122.

let mut nonces = self
.0
.get_accounts_nonces(account_ids.clone())
.get_accounts_nonces(vec![from])
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

silent semantic change unrelated to keycard. pre-PR fetched both [from, to] nonces unconditionally; now from only, with to extended only if its key is local. when to is foreign (the normal case), account_ids.len() == 2 but nonces.len() == 1 and Message::try_new(...).unwrap() either panics or builds a malformed tx.

let's revert to get_accounts_nonces(account_ids.clone()) and put any conditional-nonce logic in its own PR with a foreign-recipient test.

Comment thread keycard_wallet/src/lib.rs Outdated
))
})?;

Ok(Signature { value: signature })
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

the 64 bytes from the card go straight into Signature { value } with no rust-side verify. if keycard_wallet.py (or keycard-py) ever returns a wrong signature, the only signal is a sequencer rejection, nonce burned, message digest leaked.

cheap to add: VerifyingKey::from_bytes(pub_key.value())?.verify_prehash(message, &sig)? after sign, refuse on failure.

Comment thread keycard_wallet/src/lib.rs Outdated
}

#[must_use]
pub fn get_public_key_for_path_with_connect(pin: &str, path: &str) -> PublicKey {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

each _with_connect does select/pair/open_secure_channel/verify_pin/.../unpair. with get_public_key_for_path_with_connect + sign_message_for_path_with_connect, that's two PIN verifications per send. the card locks after 3 bad PINs.

let's collapse to one session: connect → get_pubkey → sign → disconnect. or longer-term, hold pairing state across ops in a process-global mutex with keycard load setting it up once.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good point. I can collapse this into a single session. Thanks!

Ideally, I would hold pairing across ops state. Unfortunately, each wallet command in CLI is treated as its own instance. So, I cannot retain the python state in memory (the original approach I wanted to use). Storing the python class on disk seems problematic to me as a keycard may be removed between usage.

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