From 42b53c6907ed629f9a0db4bf7604b59d47dcb3f1 Mon Sep 17 00:00:00 2001 From: Marvin Jones Date: Mon, 6 Jul 2026 17:30:49 -0400 Subject: [PATCH] Updated to v2 secure communication --- .gitignore | 4 - Cargo.lock | 6 +- Cargo.toml | 3 +- docs/LEZ testnet v0.1 tutorials/keycard.md | 142 +++++------ lez/keycard_wallet/Cargo.toml | 4 +- .../keycard_applets/LEE_keycard.cap | Bin 104464 -> 0 bytes lez/keycard_wallet/keycard_applets/math.cap | Bin 4874 -> 0 bytes lez/keycard_wallet/src/bin/force_unpower.rs | 52 ++++ lez/keycard_wallet/src/lib.rs | 231 ++++++++---------- lez/keycard_wallet/tests/force_unpower.py | 41 ---- .../tests/keycard_power_recovery_tests.sh | 4 +- lez/keycard_wallet/tests/keycard_test_3.sh | 3 +- lez/keycard_wallet/tests/keycard_tests.sh | 2 +- lez/keycard_wallet/tests/keycard_tests_2.sh | 2 +- lez/keycard_wallet/wallet_with_keycard.sh | 10 - lez/wallet/src/cli/keycard.rs | 50 ++-- lez/wallet/src/lib.rs | 1 - lez/wallet/src/signing.rs | 31 --- 18 files changed, 255 insertions(+), 331 deletions(-) delete mode 100644 lez/keycard_wallet/keycard_applets/LEE_keycard.cap delete mode 100644 lez/keycard_wallet/keycard_applets/math.cap create mode 100644 lez/keycard_wallet/src/bin/force_unpower.rs delete mode 100755 lez/keycard_wallet/tests/force_unpower.py delete mode 100755 lez/keycard_wallet/wallet_with_keycard.sh delete mode 100644 lez/wallet/src/signing.rs diff --git a/.gitignore b/.gitignore index 4c1a18c8..f32b258c 100644 --- a/.gitignore +++ b/.gitignore @@ -17,9 +17,5 @@ result wallet-ffi/wallet_ffi.h bedrock_signing_key integration_tests/configs/debug/ -venv/ - -keycard_wallet/python/__pycache__/ -keycard_wallet/python/keycard-py/ .DS_Store diff --git a/Cargo.lock b/Cargo.lock index 7227deda..814ba2ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4674,7 +4674,7 @@ dependencies = [ [[package]] name = "keycard-rs" version = "0.1.0" -source = "git+https://github.com/keycard-tech/keycard-rs?rev=98abc3ff1b26785a5631634d5bf24da54da541bf#98abc3ff1b26785a5631634d5bf24da54da541bf" +source = "git+https://github.com/keycard-tech/keycard-rs?rev=52cb832a6334ab84ea0a0e1f42aeabcf8a3b4337#52cb832a6334ab84ea0a0e1f42aeabcf8a3b4337" dependencies = [ "aes 0.9.1", "base64 0.21.7", @@ -4701,12 +4701,12 @@ name = "keycard_wallet" version = "0.1.0" dependencies = [ "bip39", + "hex", "keycard-rs", "lee", "log", + "pcsc", "rand 0.8.6", - "serde", - "serde_json", "thiserror 2.0.18", "zeroize", ] diff --git a/Cargo.toml b/Cargo.toml index 2d269ab0..6742c47d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -163,13 +163,14 @@ logos-blockchain-chain-service = { git = "https://github.com/logos-blockchain/lo logos-blockchain-zone-sdk = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "d8711bbc3d43d3ef9755ef9b73af32fd0f703160" } logos-blockchain-http-api-common = { git = "https://github.com/logos-blockchain/logos-blockchain.git", rev = "d8711bbc3d43d3ef9755ef9b73af32fd0f703160" } -keycard-rs = { git = "https://github.com/keycard-tech/keycard-rs", rev = "98abc3ff1b26785a5631634d5bf24da54da541bf" } +keycard-rs = { git = "https://github.com/keycard-tech/keycard-rs", rev = "52cb832a6334ab84ea0a0e1f42aeabcf8a3b4337" } # includes the handshake signature-normalization fix (marvin/normalize-signature, merged) rocksdb = { version = "0.24.0", default-features = false, features = [ "snappy", "bindgen-runtime", ] } rand = { version = "0.8.5", features = ["std", "std_rng", "getrandom"] } +pcsc = "2" k256 = { version = "0.13.3", features = [ "ecdsa-core", "arithmetic", diff --git a/docs/LEZ testnet v0.1 tutorials/keycard.md b/docs/LEZ testnet v0.1 tutorials/keycard.md index 47573f12..a8d2a2df 100644 --- a/docs/LEZ testnet v0.1 tutorials/keycard.md +++ b/docs/LEZ testnet v0.1 tutorials/keycard.md @@ -6,46 +6,57 @@ This tutorial walks you through using Keycard with Wallet CLI. Keycard is option ### Required hardware - Keycard (Blank) - a Keycard, directly, from Keycard.tech cannot (currently) be updated to support LEE. - Smartcard reader -- Applets (`math.cap` and `LEE_keycard.cap`). Eventually, both of these applets will be available in separate repos. - - `math.cap` is an applet to speed up computations on Keycard; developed by Bitgamma (Keycard-tech team). - - `LEE_keycard.cap` is an applet that contains LEE keycard protocol; developed by Bitgamma (Keycard-tech team) ### Firmware installation -Installation: -1. Install math applet on your keycard; this process only needs to be done once. In the root of repo: - ``` - sudo apt-get install -y default-jdk - wget https://github.com/martinpaljak/GlobalPlatformPro/releases/download/v25.10.20/gp.jar -P lez/keycard_wallet/keycard_applets - cd lez/keycard_wallet/keycard_applets - java -jar gp.jar --key c212e073ff8b4bbfaff4de8ab655221f --load math.cap - ``` -2. Install `keycard-desktop` from [github](https://github.com/choppu/keycard-desktop) - - Keycard Desktop is used to install the LEE key protocol to a blank keycard. - - Select (Re)Install Applet and upload the key binary (`lez/keycard_wallet/keycard_applets/LEE_keycard.cap`). - ![keycard-desktop.png](keycard-desktop.png) - - **Important:** keycard can only connect with one application at a time; if Keycard-Desktop is using keycard then Wallet CLI cannot access the same keycard, and vice-versa. +The applet (LEE key protocol support, on top of standard Status Keycard commands) is no longer vendored in this repo as a pre-built `.cap` — build it directly from source. -## Wallet with Keycard -Keycard functionality is available to Wallet CLI by setting up the following Python virtual environment. The steps below can also be run via `lez/keycard_wallet/wallet_with_keycard.sh`. +**Currently this means building from a fork with a pending fix**, not upstream directly. We found and fixed a bug where the card signed LEE Schnorr signatures over the wrong message entirely (see [PR](https://github.com/jonesmarvin8/status-keycard/pull/new/marvin/fix-schnorr-hash-aliasing), not yet merged into [`keycard-tech/status-keycard`](https://github.com/keycard-tech/status-keycard)): ```bash -# Install appropriate version of `keycard-py`. -git clone --branch lee-schnorr --single-branch https://github.com/bitgamma/keycard-py.git lez/keycard_wallet/python/keycard-py - -# Set up virtual environment. -python3 -m venv venv -source venv/bin/activate -pip install pyscard mnemonic ecdsa pyaes -pip install -e lez/keycard_wallet/python/keycard-py +git clone --recurse-submodules --branch marvin/fix-schnorr-hash-aliasing https://github.com/jonesmarvin8/status-keycard.git +cd status-keycard ``` -**Important**: Keycard wallet commands only work within the virtual environment. +**Once that PR is merged, switch back to cloning `keycard-tech/status-keycard`'s default branch directly** — this fork/branch pointer is temporary. + +The build requires **OpenJDK 11 specifically** (newer JDKs aren't compatible with its Gradle/plugin versions): + ```bash -# In the root of LEE repo: -source venv/bin/activate +sudo apt-get install -y openjdk-11-jdk +export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64 ``` +Gradle's default heap is too small for this build and will OOM during `buildSrc` compilation; bump it once: + +```bash +echo "org.gradle.jvmargs=-Xmx2g" >> gradle.properties +``` + +Build and install onto a connected, blank card (disconnect all other card readers first): + +```bash +./gradlew install +``` + +This uses the GlobalPlatform default keys (`404142434445464748494a4b4c4d4e4f`) or the Keycard development-card key (`c212e073ff8b4bbfaff4de8ab655221f`) to load the applet. + +### Personalizing the card + +**Installing the applet alone isn't enough to use the card.** A freshly installed applet has no identity certificate, and refuses every command (including `SELECT`) until one is provisioned via `IdentApplet`. There isn't yet a documented, supported personalization path for production cards — this is a known gap, not something to work around ad hoc. + +For development/testing, the project's own JUnit test suite provisions the card against a fixed test CA as a side effect of running any test — run the narrowest one: + +```bash +./gradlew test --tests "im.status.keycard.KeycardTest.selectTest" +``` + +If the card isn't initialized yet, this also initializes it with fixed test values (not secrets): PIN `000000`, alt-PIN `024680`, PUK `012345678901`. To re-personalize a card that's already initialized with different values, wipe it first with `wallet keycard factory-reset --confirm` (see the Commands table below), then re-run the test above, then `wallet keycard init` with whatever PIN you want. + +Because this personalizes against a throwaway test CA rather than a real production CA, the wallet needs to be told to trust it explicitly — see "CA public key override" below. Real, production-personalized cards need no such override. + +**Important:** keycard can only connect with one application at a time; if another tool is using the keycard then Wallet CLI cannot access the same keycard, and vice-versa. + ## PIN entry Each Keycard command prompts for a PIN interactively. To avoid re-entering it across multiple commands, export it as an environment variable: @@ -60,43 +71,37 @@ Unset it when done: unset KEYCARD_PIN ``` -## Pairing password +## CA public key override -The pairing password is used to establish a secure channel between the wallet and the card. It is set permanently on the card during `wallet keycard init` and must match on every subsequent re-pair. +Secure Channel V2 verifies the card's identity certificate against a trusted CA public key. `keycard-rs` ships with the production default baked in, so real, production-personalized cards need no configuration at all. -The default password (`KeycardDefaultPairing`) is [recommended](https://docs.keycard.tech/en/developers/core) for most users. Wallet CLI allows advance users the flexibility to set their own pairing password. - -To use a custom pairing password, set it before `init`: +Cards personalized for development/testing (see "Personalizing the card" above) are signed by a different, throwaway CA, so the wallet needs to be told to trust it explicitly: ```bash -# Note: Keep the leading space before this command. -# Leading space prevents this command from being stored in shell history -# (when HISTCONTROL=ignorespace is enabled). - export KEYCARD_PAIRING_PASSWORD=my-custom-password -wallet keycard init +export KEYCARD_CA_PUBLIC_KEY=025877220aaae6e54a6f974602d5995c0fe24a3ea7ddabd8644bec795b9da00743 ``` -After a successful initializaation, subsequent commands (`connect`, transfers) use the cached pairing index and key — the pairing password is not needed again until the pairing is cleared. +If this is unset (or set, but doesn't match the card's actual certificate), every keycard command will report the card as unavailable — that's the default-CA check correctly rejecting a card it doesn't recognize, not a bug. If it's set to something that isn't 66 hex characters, commands fail immediately with `invalid KEYCARD_CA_PUBLIC_KEY: ...` instead of a confusing "unavailable". -**Important:** if you initialized with a custom password, `KEYCARD_PAIRING_PASSWORD` must be set in every session where re-pairing can occur (after `disconnect`, or on a new machine). If the env var is missing then wallet CLI will attempt to use the default password. As a result, pairing will fail. - -Unset the pairing password variable when done: +Unset it when you're done testing against a dev card: ```bash -unset KEYCARD_PAIRING_PASSWORD +unset KEYCARD_CA_PUBLIC_KEY ``` ## Keycard Commands +Keycard uses Secure Channel V2 (applet version >= 4.0) — the wallet authenticates the card via its identity certificate and opens a fresh ECDHE-derived channel every session. There's no pairing step and nothing cached between commands; you'll enter your PIN each time you connect. + ### Keycard | Command | Description | |----------------------------------|-----------------------------------------------------------------------| | `wallet keycard available` | Checks whether a Keycard reader and card are accessible | | `wallet keycard init` | Initializes a blank Keycard with a PIN and a generated PUK | -| `wallet keycard connect` | Establishes and saves a pairing with the Keycard | -| `wallet keycard disconnect` | Unpairs the Keycard and clears the saved pairing | +| `wallet keycard connect` | Opens a secure channel with the Keycard and verifies the PIN | | `wallet keycard load` | Loads a mnemonic phrase onto the Keycard | +| `wallet keycard factory-reset` | Wipes PIN/PUK/keys back to uninitialized, for re-`init` — **debug builds only** (see below) | | `wallet keycard get-private-keys`| Prints NSK and VSK for a BIP-32 path — **debug builds only** (see below) | 1. Check keycard availability @@ -118,13 +123,13 @@ Record this PUK and store it somewhere safe. It cannot be recovered. ✅ Keycard initialized successfully. ``` -3. Connect (pair and save pairing for subsequent commands) +3. Connect (open a secure channel and verify the PIN) ```bash wallet keycard connect # Output: Keycard PIN: -✅ Keycard paired and ready. +✅ Keycard connected and PIN verified. ``` 4. Load a mnemonic phrase @@ -140,25 +145,22 @@ Keycard PIN: ✅ Mnemonic phrase loaded successfully. ``` -5. Disconnect (unpair and clear saved pairing) -```bash -wallet keycard disconnect +5. `factory-reset` and `get-private-keys` (**debug builds only**) -# Output: -Keycard PIN: -✅ Keycard unpaired and pairing cleared. -``` - -6. Get private keys for a BIP-32 path (**debug builds only**) - -`get-private-keys` exports the raw NSK and VSK for a derivation path. NSK gates nullifier creation and VSK gates note decryption — either key is sufficient to fully compromise that account's privacy. The command is only available in debug builds and requires `--reveal` to confirm intent. - -First install the wallet with the `keycard-debug` feature: +Both require building the wallet with the `keycard-debug` feature: ```bash cargo install --path lez/wallet --force --features keycard-debug ``` -Then run the command: +`factory-reset` wipes the card's PIN, PUK, and loaded keys back to an uninitialized state, so it can be re-`init`ialized — the counterpart to `init`. It does **not** remove the identity certificate, so the card doesn't need re-personalizing afterward. Irreversibly destroys any keys currently on the card, so it requires `--confirm`: +```bash +wallet keycard factory-reset --confirm + +# Output: +✅ Keycard factory-reset. Run `wallet keycard init` to reinitialize it. +``` + +`get-private-keys` exports the raw NSK and VSK for a derivation path. NSK gates nullifier creation and VSK gates note decryption — either key is sufficient to fully compromise that account's privacy. Requires `--reveal` to confirm intent: ```bash wallet keycard get-private-keys --key-path "m/44'/60'/0'/0/0" --reveal @@ -515,20 +517,4 @@ bash lez/keycard_wallet/tests/keycard_tests.sh bash lez/keycard_wallet/tests/keycard_tests_2.sh bash lez/keycard_wallet/tests/keycard_test_3.sh bash lez/keycard_wallet/tests/keycard_power_recovery_tests.sh -``` - -## SigningGroup - -`SigningGroup` (`lez/wallet/src/signing.rs`) partitions a transaction's signers into two buckets — local accounts and Keycard accounts. This ensures that Python GIL is only used at most once per transaction, regardless of how many Keycard accounts are involved. - -Local signers are resolved and signed in pure Rust. Keycard signers store only their BIP32 key path; all of them are signed inside a single Python session (`connect` / `close_session`) when `sign_all` is called. The command calls `needs_pin` to decide whether to prompt for a PIN before signing. - -Foreign recipient accounts — those with no local key and no Keycard path — are silently skipped and require neither a signature nor a nonce. - -``` -SigningGroup { - local: [(AccountId, PrivateKey)], // signed in pure Rust - keycard: [(AccountId, BIP32Path)], // signed via a single Python/Keycard session -} -``` ``` \ No newline at end of file diff --git a/lez/keycard_wallet/Cargo.toml b/lez/keycard_wallet/Cargo.toml index 7a062d8b..33fabfb4 100644 --- a/lez/keycard_wallet/Cargo.toml +++ b/lez/keycard_wallet/Cargo.toml @@ -11,9 +11,9 @@ workspace = true lee.workspace = true keycard-rs.workspace = true bip39.workspace = true +hex.workspace = true log.workspace = true +pcsc.workspace = true rand.workspace = true -serde = { workspace = true, features = ["derive"] } -serde_json.workspace = true thiserror.workspace = true zeroize.workspace = true diff --git a/lez/keycard_wallet/keycard_applets/LEE_keycard.cap b/lez/keycard_wallet/keycard_applets/LEE_keycard.cap deleted file mode 100644 index b2e71d565454eb9debe51a0bbba7473cb7f1f9fa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 104464 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

^Bh^A;OP#OA@xmezb)t<5{+ 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 diff --git a/lez/keycard_wallet/keycard_applets/math.cap b/lez/keycard_wallet/keycard_applets/math.cap deleted file mode 100644 index b9c0e99f7cb534501781bbfeada8f9fd09a6acf3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4874 zcmcgve{d9c6@TB&-mm1Agxp=iVSZ{FEl7y&oYL?iZIk zOw!IkrMjZ`P8aoO&Voa!2q)jFKXjPDQ3bmG2I-Tjzsg|}=iv-`>yWDoy zVD>L|=k0#K@4NeX@B4o3KCdT_w}R1V1oO8?w!!BPEAY_1p|h!eWRP5Xg=Qk-tJR7S08Nm3_UY>WY3G^25$W9_`|O(E;-logE8aGjr}9t z;-eMEFO1x{_|t!MJ3cL5wqc#O>vzjtZ?Af3!oP2{@#&7$@BgLZXH%!99H);x^MpHG zXZ_{1me+sy$|G;DI$zzj?~3dDAG$Am+5O!jd&k}3!h5drduvXQ{(R8Y^ycXKr$70` zx_{-NCz`(}wr(pOe6RV?zxB%#AHVhbt7X+wchziqwXbL5WBs$eSDx*u^QPZ9vh&G3 zZEuuan2dgPO5b-Vy12!6skQ&o<=XI;9o4R4XVtyyJ{mso<{kg)J6^T++5?CGcJ#H; z&cU(A9*m8DWyzfH;t=Od)KXe zvez_y?(BoXiGP-QJqF`VXU*Y<8Gt(vz>AK|2QY%QcDyI6J{o*cBnze?+U?|xT5EBd>e?N2ceU9UR8AcJ12d-r9*OekCcP_hF zgQAG)HHdVFlc7K=o*W{n98`k?3BV2|V1+Ko2NM|JKJEtc@Zi)t9DC}qNXrg4 zMicR5ie&L%&+ZNUz!)nyzY8O}ant$^Z(O-0@AZ==JK_^HBF$|E*^2AqF|=|l<&DQ9 zBuR&R(dqX9=pTYSBo2v3VoEUw4^Ep)5uT_{iL=mPQPf+@VBve-!+h8P|xkAa4!z zw#5U89*@!fO&EdzI-SjCD^vpi^;%^oNhQ)^(+z{O!IuvPhQZ9#F;M%Hj&J!9ji>=> zJ|nn81A$~Xf%!w47QlUoNJ)bF0tOgvSw|DbFeny)g~lAi@W_ty7zRpq%rj`gKqB+g z+h*j`X=O$E;~w{TlL4z348fYE#Yr?T`YAxhB>bihW^@RtZwoMEEYS2gH z!KPpk3LQNzpKq2BhKa!SxPTWsdV7KSaL^Wt0rLazCl2qIrZM1S(lka>0&7o4q`NQe zm+o6bK`6+9BQQEjIyrP9nHAl@BO#N^+^~dTH6%XZN&!06gsVmlJ)2K%&#YP)&I<4m z3x{qbhg$N`lhS~P!pxEmiZUEMEX)iHP@GvZBAoQFDH{(IHw*DBmQI@_wEXr6T_lO5 zuL13J#qAMVDG9B@O1ub#ws2?-gKS=5*1W>3d4*Z?3Ov3^mKStc8wymM$(Te1P>T*R z!xCj0y?m(a=nGi5iT+O+QAT%ws{fgp-yTQY72w2z0$^hc0WOrg=JC)@pIu#`BT@0 zp)6v7>T#h4ls-83(GXJvhkhJV3ehdYL9UcVbdxyQJj#f`M9K^>lb9lv&_%v@IvEHx zhowy^ZAX>COvz(_rDps&Dl)@zy42I90V@&E*bRdL(Mwtw!3P2^Da)LfS zPQ2p}An2A(0{ayXI4KJC60-m!jy`_U0~m)3aE7ul|1&TD2w@1svaGD!&f9c0z1?6l z+DylQlcA*Yk}UxQ*PD4I3x_XDYb3&grRQXngtdk4}V^9^aUKz{>n5cnx|Hj%7$Iq_w7@(T!W&#T2U%BRkvBu3aHGw{+ro& z5^}CT20FEhRI`>yDz-{a5^_!#a!wb}VHu|jIj0MfTgmByl2_>H*@)muMjB*-7^cJ80Il3S|k5HoW=mDR0`8eQG9$mqMYpwD(NYFKr%BEw$L04ujHYFu@X zA>*bo)aEmWs^d_DtJ@42{LdP|RZWK)T-|EO;10~I`81as4>hj3;gE5MHQ*}S4>h*B xJ&>^-SoY`BIoB+xVb#A&GHe_hwOha{e*mPPC$C3GUmvVV0z7~o8ln#){1=8l7BBz+ diff --git a/lez/keycard_wallet/src/bin/force_unpower.rs b/lez/keycard_wallet/src/bin/force_unpower.rs new file mode 100644 index 00000000..9cf07f1b --- /dev/null +++ b/lez/keycard_wallet/src/bin/force_unpower.rs @@ -0,0 +1,52 @@ +#![expect( + clippy::print_stdout, + reason = "This is a CLI test helper, printing to stdout is expected and convenient" +)] + +//! Forces the card in the first available reader into the unpowered state via PC/SC +//! `SCARD_UNPOWER_CARD`. Run immediately before a wallet command to simulate the power-loss +//! condition reported on some USB reader/driver combinations. +//! +//! Either: +//! - pcscd re-powers the card on the next `SCardConnect`, so wallet commands will succeed without +//! triggering the retry path. +//! - the card stays unpowered, triggering a PC/SC transport error (`keycard_rs::Error::Io`) and +//! exercising the reconnect-and-retry wrapper in `KeycardWallet::connect()`. + +fn main() { + let context = match pcsc::Context::establish(pcsc::Scope::User) { + Ok(context) => context, + Err(e) => { + println!("force_unpower: failed to establish PC/SC context ({e}), skipping."); + return; + } + }; + + let readers = match context.list_readers_owned() { + Ok(readers) => readers, + Err(e) => { + println!("force_unpower: failed to list readers ({e}), skipping."); + return; + } + }; + + let Some(reader) = readers.first() else { + println!("force_unpower: no readers found, skipping."); + return; + }; + + let card = match context.connect(reader, pcsc::ShareMode::Shared, pcsc::Protocols::ANY) { + Ok(card) => card, + Err(e) => { + println!("force_unpower: connect failed ({e}), skipping."); + return; + } + }; + + if let Err((_card, e)) = card.disconnect(pcsc::Disposition::UnpowerCard) { + println!("force_unpower: disconnect failed ({e}), skipping."); + return; + } + + println!("force_unpower: card powered down."); +} diff --git a/lez/keycard_wallet/src/lib.rs b/lez/keycard_wallet/src/lib.rs index 6d3f9777..f7d521cd 100644 --- a/lez/keycard_wallet/src/lib.rs +++ b/lez/keycard_wallet/src/lib.rs @@ -1,28 +1,28 @@ -use std::{path::PathBuf, str::FromStr as _}; +use std::str::FromStr as _; use keycard_rs::{ KeycardCommandSet, PcscChannel, constants::sign_p2, parsing::Bip32KeyPair, - secure_channel::Pairing, + secure_channel::SecureChannelVersion, tlv::{BerTlvReader, TLV_KEY_TEMPLATE, TLV_PUB_KEY, TLV_SIGNATURE_TEMPLATE}, }; use lee::{AccountId, PublicKey, Signature}; use rand::Rng as _; -use serde::{Deserialize, Serialize}; use zeroize::Zeroizing; -const DEFAULT_PAIRING_PASSWORD: &str = "KeycardDefaultPairing"; - -/// LEE-applet extension tags. These aren't part of upstream `keycard-rs`'s `tlv` module (which -/// only knows the standard Status Keycard tags) since they're specific to the LEE-flavored -/// applet (`LEE_keycard.cap`). +/// LEE-applet extension tags. `keycard-rs` implements *sending* the LEE commands +/// (`load_lee_key`, `export_lee_key`) but never added parsing for their LEE-specific responses, +/// so these tag values — owned by the applet, not either client library — have to be hardcoded +/// here. Matches `KeycardApplet.TLV_LEE_NSK`/`TLV_LEE_VSK` in `status-keycard` +/// (`KeycardApplet.java:97-98`), the applet `LEE_keycard.cap` was almost certainly built from. const TLV_LEE_NSK: u8 = 0x83; const TLV_LEE_VSK: u8 = 0x84; /// Raw Schnorr signature (64 bytes: `r || s`, no ASN.1/DER wrapping) nested inside the standard /// `TLV_SIGNATURE_TEMPLATE` alongside the usual `TLV_PUB_KEY`. Confirmed against real hardware — /// the LEE applet's `SIGN` response for `sign_p2::BIP340_SCHNORR` is -/// `0xA0 { 0x80 <65-byte pubkey>, 0x88 <64-byte r||s> }`. +/// `0xA0 { 0x80 <65-byte pubkey>, 0x88 <64-byte r||s> }` — and matches +/// `SECP256k1.TLV_RAW_SIGNATURE` in `status-keycard` (`SECP256k1.java:64`). const TLV_LEE_RAW_SIGNATURE: u8 = 0x88; /// NSK (32 bytes) and VSK (64 bytes, the ML-KEM-768 seed `d || z`) as fixed-length zeroizing byte @@ -35,51 +35,60 @@ pub enum KeycardWalletError { Keycard(#[from] keycard_rs::Error), #[error("keycard is already initialized")] AlreadyInitialized, + #[error( + "this wallet only supports Secure Channel V2 keycards (applet version >= 4.0); detected {0:?}" + )] + UnsupportedSecureChannel(Option), #[error("invalid mnemonic phrase: {0}")] InvalidMnemonic(String), #[error("invalid key material from keycard: {0}")] InvalidKeyMaterial(String), #[error("keycard returned a signature that does not verify against its own public key")] SignatureVerificationFailed, -} - -// TODO: encrypt at rest alongside broader wallet storage encryption work. -#[derive(Serialize, Deserialize)] -pub struct KeycardPairingData { - pub index: u8, - pub key: Vec, -} - -impl KeycardPairingData { - const fn is_valid(&self) -> bool { - self.key.len() == 32 && self.index <= 4 - } + #[error("invalid KEYCARD_CA_PUBLIC_KEY: {0}")] + InvalidCaPublicKey(String), } /// Rust wrapper around `keycard-rs`, talking to the LEE-flavored Keycard applet over PC/SC. +/// Only Secure Channel V2 cards are supported — see `require_secure_channel_v2`. pub struct KeycardWallet { command_set: KeycardCommandSet, } impl KeycardWallet { /// Connects to the first available PC/SC reader. Does not select the applet yet — callers - /// that need application info (`initialize`, `pair`, `connect`, ...) do that themselves. + /// that need application info (`initialize`, `connect`, ...) do that themselves. + /// + /// Verifies the card's identity certificate against `keycard-rs`'s default production CA, + /// unless overridden via `KEYCARD_CA_PUBLIC_KEY` — see `ca_public_key_override`. pub fn new() -> Result { let channel = PcscChannel::connect()?; Ok(Self { - command_set: KeycardCommandSet::new(channel), + command_set: Self::build_command_set(channel)?, }) } - /// Returns whether a smart card reader and a selectable Keycard are both present. + fn build_command_set(channel: PcscChannel) -> Result { + Ok(match ca_public_key_override()? { + Some(ca) => KeycardCommandSet::new_with_ca(channel, ca), + None => KeycardCommandSet::new(channel), + }) + } + + /// Returns whether a smart card reader and a selectable, Secure-Channel-V2 Keycard are both + /// present. #[must_use] - pub fn is_unpaired_keycard_available() -> bool { + pub fn is_keycard_available() -> bool { let Ok(channel) = PcscChannel::connect() else { return false; }; - KeycardCommandSet::new(channel) - .select() - .is_ok_and(|resp| resp.is_ok()) + let Ok(mut command_set) = Self::build_command_set(channel) else { + return false; + }; + if !command_set.select().is_ok_and(|resp| resp.is_ok()) { + return false; + } + command_set.secure_channel_version() == Some(SecureChannelVersion::V2) } fn select(&mut self) -> Result<(), KeycardWalletError> { @@ -87,15 +96,17 @@ impl KeycardWallet { Ok(()) } - fn open_and_verify(&mut self, pin: &str) -> Result<(), KeycardWalletError> { - self.command_set.auto_open_secure_channel()?; - self.command_set.verify_pin(pin)?.check_auth_ok()?; - Ok(()) + /// Rejects any card that isn't running Secure Channel V2 (older applets, or a card that + /// hasn't advertised a secure channel at all). Call right after `select()`. + fn require_secure_channel_v2(&self) -> Result<(), KeycardWalletError> { + match self.command_set.secure_channel_version() { + Some(SecureChannelVersion::V2) => Ok(()), + other => Err(KeycardWalletError::UnsupportedSecureChannel(other)), + } } /// Rebuilds the PC/SC channel and command set, then retries `op` once, if `op` failed with a - /// transport-level error (e.g. the card lost power mid-session). Mirrors the reconnect-once - /// behavior the previous `keycard-py`-based wallet had for `TransportError`. + /// transport-level error (e.g. the card lost power mid-session). fn with_reconnect_on_transport_error( &mut self, op: impl Fn(&mut Self) -> Result, @@ -116,6 +127,7 @@ impl KeycardWallet { /// for surfacing the PUK to the operator — it cannot be recovered afterward. pub fn initialize(&mut self, pin: &str) -> Result { self.select()?; + self.require_secure_channel_v2()?; let already_initialized = self .command_set .app_info() @@ -125,73 +137,37 @@ impl KeycardWallet { return Err(KeycardWalletError::AlreadyInitialized); } + // V2's INIT has no shared-secret field (confirmed against real hardware and the + // applet's own reference command set: a payload containing one is rejected outright) — + // pass an empty secret and let the card default the PIN/PUK retry counts. let puk = generate_puk(); self.command_set - .init(pin, &puk, &pairing_password())? + .init_with_secret(pin, None, &puk, &[], 0, 0)? .check_ok()?; Ok(puk) } - pub fn pair(&mut self, pin: &str) -> Result<(u8, [u8; 32]), KeycardWalletError> { + /// Wipes the card's PIN, PUK, and loaded keys, returning it to an uninitialized state — + /// the counterpart to `initialize()`. Does **not** remove the identity certificate + /// provisioned via `IdentApplet`, so the card can be re-`initialize()`d afterward without + /// re-personalizing it. Irreversibly destroys any keys currently loaded on the card. + pub fn factory_reset(&mut self) -> Result<(), KeycardWalletError> { self.select()?; - self.command_set.auto_pair(&pairing_password())?; - let pairing = self - .command_set - .pairing() - .expect("auto_pair sets pairing data on success") - .clone(); - - if let Err(err) = self.open_and_verify(pin) { - drop(self.command_set.auto_unpair()); - return Err(err); - } - - Ok((pairing.pairing_index(), *pairing.pairing_key())) - } - - pub fn setup_communication_with_pairing( - &mut self, - pin: &str, - index: u8, - key: &[u8; 32], - ) -> Result<(), KeycardWalletError> { - self.select()?; - self.command_set.set_pairing(Pairing::new(*key, index)); - self.open_and_verify(pin) - } - - /// Connect using a stored pairing if available, falling back to a fresh pair. - /// Saves any newly established pairing to disk. - pub fn connect(&mut self, pin: &str) -> Result<(), KeycardWalletError> { - if let Some(pairing) = load_pairing().filter(KeycardPairingData::is_valid) { - let key: [u8; 32] = pairing - .key - .clone() - .try_into() - .expect("KeycardPairingData::is_valid checked the key is 32 bytes"); - let reconnected = self.with_reconnect_on_transport_error(|wallet| { - wallet.setup_communication_with_pairing(pin, pairing.index, &key) - }); - if reconnected.is_ok() { - return Ok(()); - } - } - - let (index, key) = self.with_reconnect_on_transport_error(|wallet| wallet.pair(pin))?; - save_pairing(&KeycardPairingData { - index, - key: key.to_vec(), - }); + self.require_secure_channel_v2()?; + self.command_set.factory_reset()?.check_ok()?; Ok(()) } - /// Unpairs the current session. Returns `false` if there was nothing to unpair. - pub fn disconnect(&mut self) -> Result { - if self.command_set.pairing().is_none() { - return Ok(false); - } - self.command_set.auto_unpair()?; - Ok(true) + /// Opens the secure channel and verifies the PIN. Secure Channel V2 re-authenticates from + /// the card's certificate every session — there's no pairing step and nothing to persist. + pub fn connect(&mut self, pin: &str) -> Result<(), KeycardWalletError> { + self.with_reconnect_on_transport_error(|wallet| { + wallet.select()?; + wallet.require_secure_channel_v2()?; + wallet.command_set.auto_open_secure_channel()?; + wallet.command_set.verify_pin(pin)?.check_auth_ok()?; + Ok(()) + }) } pub fn get_public_key_for_path(&mut self, path: &str) -> Result { @@ -217,7 +193,8 @@ impl KeycardWallet { } }; - PublicKey::try_new(x_only).map_err(|e| KeycardWalletError::InvalidKeyMaterial(e.to_string())) + PublicKey::try_new(x_only) + .map_err(|e| KeycardWalletError::InvalidKeyMaterial(e.to_string())) } pub fn get_public_key_for_path_with_connect( @@ -234,9 +211,12 @@ impl KeycardWallet { path: &str, message: &[u8; 32], ) -> Result<(Signature, PublicKey), KeycardWalletError> { - let resp = - self.command_set - .sign_with_path_and_algo(message, path, sign_p2::BIP340_SCHNORR, false)?; + let resp = self.command_set.sign_with_path_and_algo( + message, + path, + sign_p2::BIP340_SCHNORR, + false, + )?; resp.check_ok()?; let sig = Signature { @@ -276,7 +256,10 @@ impl KeycardWallet { Ok(format!("Public/{}", AccountId::from(&public_key))) } - pub fn get_private_keys_for_path(&mut self, path: &str) -> Result { + pub fn get_private_keys_for_path( + &mut self, + path: &str, + ) -> Result { let resp = self.command_set.export_lee_key(path)?; resp.check_ok()?; @@ -297,9 +280,7 @@ impl KeycardWallet { ) -> Result { let mut wallet = Self::new()?; wallet.connect(pin)?; - let result = wallet.get_private_keys_for_path(path); - drop(wallet.disconnect()); - result + wallet.get_private_keys_for_path(path) } } @@ -310,8 +291,22 @@ fn generate_puk() -> String { .collect() } -fn pairing_password() -> String { - std::env::var("KEYCARD_PAIRING_PASSWORD").unwrap_or_else(|_| DEFAULT_PAIRING_PASSWORD.to_owned()) +/// Optional override for the CA public key used to verify a card's identity certificate, read +/// from `KEYCARD_CA_PUBLIC_KEY` as 66 hex characters (a 33-byte compressed secp256k1 key). +/// Falls back to `keycard-rs`'s production default when unset. +/// +/// This exists purely for testing against cards that weren't personalized through the real +/// production process — e.g. `status-keycard`'s own `JUnit` suite signs test cards with a fixed, +/// throwaway CA that will never match the production default. Real users' cards should need no +/// override at all. +fn ca_public_key_override() -> Result, KeycardWalletError> { + let Ok(hex_str) = std::env::var("KEYCARD_CA_PUBLIC_KEY") else { + return Ok(None); + }; + let mut bytes = [0_u8; 33]; + hex::decode_to_slice(hex_str.trim(), &mut bytes) + .map_err(|e| KeycardWalletError::InvalidCaPublicKey(e.to_string()))?; + Ok(Some(bytes)) } /// Parses a BIP340 Schnorr signature from a LEE `SIGN` response. @@ -362,35 +357,3 @@ fn zeroizing_fixed_bytes( arr.copy_from_slice(&raw); Ok(arr) } - -fn pairing_file_path() -> Option { - let home = std::env::var("LEE_WALLET_HOME_DIR") - .map(PathBuf::from) - .or_else(|_| { - std::env::home_dir() - .map(|h| h.join(".lee").join("wallet")) - .ok_or(()) - }) - .ok()?; - Some(home.join("keycard_pairing.json")) -} - -fn load_pairing() -> Option { - let path = pairing_file_path()?; - let file = std::fs::File::open(path).ok()?; - serde_json::from_reader(file).ok() -} - -fn save_pairing(data: &KeycardPairingData) { - if let Some(path) = pairing_file_path() - && let Ok(json) = serde_json::to_vec_pretty(data) - { - drop(std::fs::write(path, json)); - } -} - -pub fn clear_pairing() { - if let Some(path) = pairing_file_path() { - drop(std::fs::remove_file(path)); - } -} diff --git a/lez/keycard_wallet/tests/force_unpower.py b/lez/keycard_wallet/tests/force_unpower.py deleted file mode 100755 index c6789015..00000000 --- a/lez/keycard_wallet/tests/force_unpower.py +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env python3 -""" -Forces the card in the first available reader into the unpowered state via -PC/SC SCARD_UNPOWER_CARD. Run immediately before a wallet command to simulate -the power-loss condition reported on some USB reader/driver combinations. - -Either: -- pcscd re-powers the card on the next SCardConnect, so wallet -commands will succeed without triggering the retry path. -- the card stays unpowered, triggering a PC/SC transport error -(keycard_rs::Error::Io) and exercising the reconnect-and-retry wrapper in -KeycardWallet::pair() / KeycardWallet::setup_communication_with_pairing(). -""" -import sys -from smartcard.scard import ( - SCardEstablishContext, SCardListReaders, SCardConnect, SCardDisconnect, - SCARD_SCOPE_USER, SCARD_SHARE_SHARED, - SCARD_PROTOCOL_T0, SCARD_PROTOCOL_T1, - SCARD_UNPOWER_CARD, -) - -hresult, hcontext = SCardEstablishContext(SCARD_SCOPE_USER) -hresult, reader_list = SCardListReaders(hcontext, []) - -if not reader_list: - print("force_unpower: no readers found, skipping.") - sys.exit(0) - -hresult, hcard, _ = SCardConnect( - hcontext, - reader_list[0], - SCARD_SHARE_SHARED, - SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1, -) - -if hresult != 0: - print(f"force_unpower: SCardConnect failed (hresult={hresult:#010x}), skipping.") - sys.exit(0) - -SCardDisconnect(hcard, SCARD_UNPOWER_CARD) -print("force_unpower: card powered down.") diff --git a/lez/keycard_wallet/tests/keycard_power_recovery_tests.sh b/lez/keycard_wallet/tests/keycard_power_recovery_tests.sh index 3d8301f7..8b9c5ff4 100755 --- a/lez/keycard_wallet/tests/keycard_power_recovery_tests.sh +++ b/lez/keycard_wallet/tests/keycard_power_recovery_tests.sh @@ -4,14 +4,12 @@ # Forces a card power cycle before each keycard-backed wallet command to verify # commands survive mid-session power loss. -source venv/bin/activate - export KEYCARD_PIN=111111 SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" unpower() { - python "$SCRIPT_DIR/force_unpower.py" + cargo run -q --manifest-path "$SCRIPT_DIR/../Cargo.toml" --bin force_unpower } echo "Test: wallet keycard available" diff --git a/lez/keycard_wallet/tests/keycard_test_3.sh b/lez/keycard_wallet/tests/keycard_test_3.sh index f4aa57fe..3d98c43a 100755 --- a/lez/keycard_wallet/tests/keycard_test_3.sh +++ b/lez/keycard_wallet/tests/keycard_test_3.sh @@ -2,8 +2,7 @@ # keycard_test_3.sh — tests for `wallet keycard get-private-keys`. # # Prerequisites: -# 1. Run wallet_with_keycard.sh once to install dependencies. -# 2. Keycard reader inserted with card loaded (wallet keycard load has been run). +# 1. Keycard reader inserted with card loaded (wallet keycard load has been run). cargo install --path lez/wallet --force --features keycard-debug diff --git a/lez/keycard_wallet/tests/keycard_tests.sh b/lez/keycard_wallet/tests/keycard_tests.sh index 3faa48c9..910f9f73 100755 --- a/lez/keycard_wallet/tests/keycard_tests.sh +++ b/lez/keycard_wallet/tests/keycard_tests.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Run wallet_with_keycard.sh first +# Run `cargo install --path lez/wallet --force` first export KEYCARD_PIN=111111 diff --git a/lez/keycard_wallet/tests/keycard_tests_2.sh b/lez/keycard_wallet/tests/keycard_tests_2.sh index 9804fecb..9b629fef 100755 --- a/lez/keycard_wallet/tests/keycard_tests_2.sh +++ b/lez/keycard_wallet/tests/keycard_tests_2.sh @@ -2,7 +2,7 @@ # keycard_tests_2.sh — comprehensive token + AMM keycard integration tests. # # Prerequisites: -# 1. Run wallet_with_keycard.sh once to install dependencies. +# 1. Run `cargo install --path lez/wallet --force` once to install the wallet CLI. # 2. Reset the local chain so all accounts are uninitialized. # 3. Keycard reader inserted with card loaded. # diff --git a/lez/keycard_wallet/wallet_with_keycard.sh b/lez/keycard_wallet/wallet_with_keycard.sh deleted file mode 100755 index f77fd790..00000000 --- a/lez/keycard_wallet/wallet_with_keycard.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -cargo install --path lez/wallet --force - -# `pyscard` is only needed by tests/force_unpower.py, a test-only helper that simulates a card -# power loss via PC/SC — the wallet CLI itself is pure Rust and talks to the card directly via -# `keycard-rs`. -python3 -m venv venv -source venv/bin/activate -pip install pyscard \ No newline at end of file diff --git a/lez/wallet/src/cli/keycard.rs b/lez/wallet/src/cli/keycard.rs index 65ad0b77..b9626370 100644 --- a/lez/wallet/src/cli/keycard.rs +++ b/lez/wallet/src/cli/keycard.rs @@ -5,7 +5,7 @@ use anyhow::Result; use clap::Subcommand; -use keycard_wallet::{KeycardWallet, clear_pairing}; +use keycard_wallet::KeycardWallet; use crate::{ WalletCore, @@ -17,9 +17,16 @@ use crate::{ pub enum KeycardSubcommand { Available, Connect, - Disconnect, Init, Load, + /// Wipes the card's PIN, PUK, and loaded keys back to an uninitialized state, so it can be + /// re-initialized with `wallet keycard init`. Irreversibly destroys any keys currently on + /// the card. Requires --confirm. + FactoryReset { + /// Confirm that the card's current keys should be irreversibly destroyed. + #[arg(long)] + confirm: bool, + }, /// Retrieve the private keys (NSK, VSK) for a given BIP-32 key path. /// /// Prints raw key material to stdout — intended for debugging only. @@ -38,7 +45,7 @@ pub enum KeycardSubcommand { impl KeycardSubcommand { fn handle_available(_wallet_core: &mut WalletCore) -> SubcommandReturnValue { - if KeycardWallet::is_unpaired_keycard_available() { + if KeycardWallet::is_keycard_available() { println!("\u{2705} Keycard is available."); } else { println!("\u{274c} Keycard is not available."); @@ -52,20 +59,7 @@ impl KeycardSubcommand { let mut wallet = KeycardWallet::new()?; wallet.connect(&pin)?; - println!("\u{2705} Keycard paired and ready."); - - Ok(SubcommandReturnValue::Empty) - } - - fn handle_disconnect(_wallet_core: &mut WalletCore) -> Result { - let pin = read_pin()?; - - let mut wallet = KeycardWallet::new()?; - wallet.connect(&pin)?; - wallet.disconnect()?; - - clear_pairing(); - println!("\u{2705} Keycard unpaired and pairing cleared."); + println!("\u{2705} Keycard connected and PIN verified."); Ok(SubcommandReturnValue::Empty) } @@ -76,7 +70,6 @@ impl KeycardSubcommand { let mut wallet = KeycardWallet::new()?; let puk = wallet.initialize(&pin)?; - clear_pairing(); println!("Keycard PUK: {puk}"); println!("Record this PUK and store it somewhere safe. It cannot be recovered."); println!("\u{2705} Keycard initialized successfully."); @@ -101,6 +94,25 @@ impl KeycardSubcommand { Ok(SubcommandReturnValue::Empty) } + fn handle_factory_reset( + confirm: bool, + _wallet_core: &mut WalletCore, + ) -> Result { + if !confirm { + eprintln!( + "WARNING: pass --confirm to factory-reset the keycard. \ + This irreversibly destroys any keys currently loaded on it." + ); + return Ok(SubcommandReturnValue::Empty); + } + + let mut wallet = KeycardWallet::new()?; + wallet.factory_reset()?; + println!("\u{2705} Keycard factory-reset. Run `wallet keycard init` to reinitialize it."); + + Ok(SubcommandReturnValue::Empty) + } + #[cfg(feature = "keycard-debug")] fn handle_get_private_keys( key_path: &str, @@ -135,9 +147,9 @@ impl WalletSubcommand for KeycardSubcommand { match self { Self::Available => Ok(Self::handle_available(wallet_core)), Self::Connect => Self::handle_connect(wallet_core), - Self::Disconnect => Self::handle_disconnect(wallet_core), Self::Init => Self::handle_init(wallet_core), Self::Load => Self::handle_load(wallet_core), + Self::FactoryReset { confirm } => Self::handle_factory_reset(confirm, wallet_core), #[cfg(feature = "keycard-debug")] Self::GetPrivateKeys { key_path, reveal } => { Self::handle_get_private_keys(&key_path, reveal, wallet_core) diff --git a/lez/wallet/src/lib.rs b/lez/wallet/src/lib.rs index 2306511f..d67232ac 100644 --- a/lez/wallet/src/lib.rs +++ b/lez/wallet/src/lib.rs @@ -44,7 +44,6 @@ pub mod config; pub mod helperfunctions; pub mod poller; pub mod program_facades; -pub mod signing; pub mod storage; pub const HOME_DIR_ENV_VAR: &str = "LEE_WALLET_HOME_DIR"; diff --git a/lez/wallet/src/signing.rs b/lez/wallet/src/signing.rs deleted file mode 100644 index 2ee67ae7..00000000 --- a/lez/wallet/src/signing.rs +++ /dev/null @@ -1,31 +0,0 @@ -use keycard_wallet::{KeycardWallet, KeycardWalletError}; - -/// Lazily opens and reuses a single Keycard session for all keycard signers in one transaction. -pub struct KeycardSessionContext { - pin: String, - wallet: Option, -} - -impl KeycardSessionContext { - pub fn new(pin: impl Into) -> Self { - Self { - pin: pin.into(), - wallet: None, - } - } - - pub fn get_or_connect(&mut self) -> Result<&mut KeycardWallet, KeycardWalletError> { - if self.wallet.is_none() { - let mut wallet = KeycardWallet::new()?; - wallet.connect(&self.pin)?; - self.wallet = Some(wallet); - } - Ok(self.wallet.as_mut().expect("wallet was just inserted")) - } - - pub fn close(mut self) { - if let Some(wallet) = self.wallet.as_mut() { - drop(wallet.disconnect()); - } - } -}