diff --git a/docs/LEZ testnet v0.1 tutorials/keycard.md b/docs/LEZ testnet v0.1 tutorials/keycard.md index 78ce8441..460b70f2 100644 --- a/docs/LEZ testnet v0.1 tutorials/keycard.md +++ b/docs/LEZ testnet v0.1 tutorials/keycard.md @@ -12,12 +12,12 @@ Installation: 1. Install math applet on your keycard; this process only needs to be done once. In the root of repo: ``` - cd python/keycard_applets + cd 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 (`python/keycard_applets/LEE_keycard.cap`). + - Select (Re)Install Applet and upload the key binary (`keycard_wallet/keycard_applets/LEE_keycard.cap`). ![keycard-desktop.png](keycard-desktop.png) ## Wallet with Keycard @@ -25,13 +25,13 @@ Keycard functionality is available to Wallet CLI by setting up the following Pyt ```bash # Install appropriate version of `keycard-py`. -git clone --branch lee-schnorr --single-branch https://github.com/bitgamma/keycard-py.git python/keycard-py +git clone --branch lee-schnorr --single-branch https://github.com/bitgamma/keycard-py.git 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 python/keycard-py +pip install -e keycard_wallet/python/keycard-py ``` **Important**: Keycard wallet commands only work within the virtual environment. diff --git a/keycard_wallet/keycard_applets/LEE_keycard.cap b/keycard_wallet/keycard_applets/LEE_keycard.cap new file mode 100644 index 00000000..b44835c4 Binary files /dev/null and b/keycard_wallet/keycard_applets/LEE_keycard.cap differ diff --git a/keycard_wallet/keycard_applets/math.cap b/keycard_wallet/keycard_applets/math.cap new file mode 100644 index 00000000..b9c0e99f Binary files /dev/null and b/keycard_wallet/keycard_applets/math.cap differ diff --git a/keycard_wallet/python/keycard_wallet.py b/keycard_wallet/python/keycard_wallet.py new file mode 100644 index 00000000..767c1aba --- /dev/null +++ b/keycard_wallet/python/keycard_wallet.py @@ -0,0 +1,122 @@ +from smartcard.System import readers +from keycard.exceptions import APDUError, TransportError +from ecdsa import VerifyingKey, SECP256k1 + +from keycard.keycard import KeyCard + +from mnemonic import Mnemonic +from keycard import constants + +import keycard + +DEFAULT_PAIRING_PASSWORD = "KeycardDefaultPairing" + +class KeycardWallet: + def __init__(self): + self.card = KeyCard() + + def _is_smart_card_reader_detected(self) -> bool: + try: + return len(readers()) > 0 + except Exception: + return False + + def _is_keycard_detected(self) -> bool: + try: + KeyCard().select() + return True + except (TransportError, APDUError, Exception): + # No readers, no card, or card doesn't respond. + return False + + def is_unpaired_keycard_available(self) -> bool: + if not self._is_smart_card_reader_detected(): + return False + elif not self._is_keycard_detected(): + return False + return True + + def setup_communication(self, pin: str, password = DEFAULT_PAIRING_PASSWORD) -> bool: + self.card.select() + + if not self.card.is_initialized: + raise RuntimeError(f"Error setting up communication: uninitialized keycard") + + pairing_index, pairing_key = self.card.pair(password) + self.pairing_index = pairing_index + + try: + self.card.open_secure_channel(pairing_index, pairing_key) + self.card.verify_pin(pin) + except Exception as e: + try: + self.card.unpair(pairing_index) + except Exception: + pass + raise RuntimeError(f"Error setting up communication: {e}") from e + + return True + + def load_mnemonic(self, mnemonic: str) -> bool: + try: + # Convert mnemonic to seed + mnemo = Mnemonic("english") + seed = mnemo.to_seed(mnemonic) + + # Load the LEE seed onto the card + result = self.card.load_key( + key_type = constants.LoadKeyType.LEE_SEED, + lee_seed = seed + ) + return True + except Exception as e: + raise RuntimeError(f"Error loading mnemonic: {e}") from e + + def disconnect(self) -> bool: + try: + if not self.card.is_secure_channel_open: + return False + + self.card.unpair(self.pairing_index) + + return True + except Exception as e: + raise RuntimeError(f"Error during disconnect: {e}") from e + + def get_public_key_for_path(self, path: str = "m/44'/60'/0'/0/0") -> bytes | None: + try: + if not self.card.is_secure_channel_open or not self.card.is_pin_verified: + return None + + public_key = self.card.export_key( + derivation_option = constants.DerivationOption.DERIVE, + public_only = True, + keypath = path + ) + + public_key = public_key.public_key + public_key = VerifyingKey.from_string(public_key[1:], curve=SECP256k1) + public_key = public_key.to_string("compressed")[1:] + + return public_key + + except Exception as e: + raise RuntimeError(f"Error getting public key: {e}") from e + + + def sign_message_for_path(self, message: bytes, path: str = "m/44'/60'/0'/0/0") -> bytes | None: + try: + if not self.card.is_secure_channel_open or not self.card.is_pin_verified: + return None + + signature = self.card.sign_with_path( + digest = message, + path = path, + algorithm = constants.SigningAlgorithm.SCHNORR_BIP340, + make_current = False + ) + + return signature.signature + + except Exception as e: + raise RuntimeError(f"Error signing message: {e}") from e \ No newline at end of file diff --git a/keycard_wallet/src/python_path.rs b/keycard_wallet/src/python_path.rs index b7159295..8251f7a3 100644 --- a/keycard_wallet/src/python_path.rs +++ b/keycard_wallet/src/python_path.rs @@ -12,8 +12,11 @@ pub fn add_python_path(py: Python<'_>) -> PyResult<()> { .unwrap_or_else(|| current_dir.clone()); let mut paths_to_add: Vec = vec![ - python_base.join("python"), - python_base.join("python").join("keycard-py"), + python_base.join("keycard_wallet").join("python"), + python_base + .join("keycard_wallet") + .join("python") + .join("keycard-py"), ]; // If a virtualenv is active, add its site-packages so that dependencies diff --git a/wallet/src/cli/account.rs b/wallet/src/cli/account.rs index 5ed99d90..4943aae1 100644 --- a/wallet/src/cli/account.rs +++ b/wallet/src/cli/account.rs @@ -236,7 +236,7 @@ impl WalletSubcommand for AccountSubcommand { let account_id: nssa::AccountId = account_id_str.parse()?; // Add account id to the display for keycard users. - log::info!("Account Id: {resolved}"); + println!("Account Id: {resolved}"); if let Some(label) = wallet_core.storage.labels.get(&account_id_str) { println!("Label: {label}"); diff --git a/wallet_with_keycard.sh b/wallet_with_keycard.sh index a0b4de46..c5331a12 100644 --- a/wallet_with_keycard.sh +++ b/wallet_with_keycard.sh @@ -1,10 +1,10 @@ cargo install --path wallet --force # Install appropriate version of `keycard-py`. -git clone --branch lee-schnorr --single-branch https://github.com/bitgamma/keycard-py.git python/keycard-py +git clone --branch lee-schnorr --single-branch https://github.com/bitgamma/keycard-py.git 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 python/keycard-py \ No newline at end of file +pip install -e keycard_wallet/python/keycard-py \ No newline at end of file