mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-06-08 08:19:26 +00:00
* feat: add basic commands for communicating with keycard * initialize changes * reorganization * add script file for easier wallet access * update commands * fixes * fixed load for non continuous run * Updates for signatures with keycard * fix BIP-340 signatures for fixed sized messages * fmt * refactor and add pin support to program facades * fix unit test * fixes * Revert "fixes" This reverts commit 41f34f4ff4145b7abb60fd9bec168ae4b60f23b4. * fixes * fixes * Removed privacy keycard calls * Revert "Removed privacy keycard calls" This reverts commit d70ef505a1f40b87159099761f5fce5a31e3f17b. * Add domain separators * Removed privacy txs for keycard * CI fixes * CI fixes * addressed some comments * fix ci * initialize branch * ci fixes * fix integration test issue and updated keycard firmware * addressed more comments * fixed deny * remove keycard-py * fixed from earlier merge * add hash_message tests * add test * fix deny * CI fixes * fixed integration tests * Update public.rs * update artifacts * privacy command fixes * ci and comments * addressed comments * comment fixes * fixes from merging main * adding support to other programs * expanded support * ci fixes * ci and add private account keys test * some fixes and setup notes * Ci fixes * ci fixes * update key paths to avoid collisions in tests * added separated files for keycard_tests_2.sh * first round of comments * Revert "Merge branch 'main' into marvin/keycard-commands" This reverts commit 3fce53f663a3996938dddf77680854570063ca21, reversing changes made to e7b42a5177641455a8917bd2e29db20afd9690e5. * python comments * addressed comments * compile error fixed * fix artifacts * fix main merge error * adjust signer logic workflow * updating logic * fmt * refactored * clippy fix * minor fix * addressing comments * minor fix * ci fix * addressed deferred comments * clean up * minor cleanup * ci fixes * fmt fix * feat!(wallet): Merged `SigningGroup` with `AccountManager` (#500) * feat: account manager extension * feat(wallet): added unified way of sending public transactions to all facades * fix(wallet): no sign option added * fix(deny): deny fix * fix(wallet): suggestion 1 * fix(wallet): suggestion fix 1 * feat!: Add new path for externally provided seed to the circuit. BREAKING CHANGE: add identity variants to the circuit and change semantics for `Claim::Authorized` for private PDAs * feat(ci): use separate job per each integration tests module * feat(ci): cache rust artifacts * feat(ci): build integration tests binary once and reuse it * fix(wallet): fmt * ci: add bench-regression workflow with criterion-compare for crypto_primitives_bench * fix(wallet): merge postfix * feat!(wallet): SigningGroup merged with AccountManager * fix(ci): deny and artifacts fix * fix(deny): deny fix * fix keycard and lint --------- Co-authored-by: Sergio Chouhy <sergio.chouhy@gmail.com> Co-authored-by: Daniil Polyakov <arjentix@gmail.com> Co-authored-by: Moudy <m.ellaz@hotmail.com> Co-authored-by: Sergio Chouhy <41742639+schouhy@users.noreply.github.com> Co-authored-by: jonesmarvin8 <83104039+jonesmarvin8@users.noreply.github.com> * addressed comments * minor comments * Rebase to main * CI fixes --------- Co-authored-by: Pravdyvy <46261001+Pravdyvy@users.noreply.github.com> Co-authored-by: Sergio Chouhy <sergio.chouhy@gmail.com> Co-authored-by: Daniil Polyakov <arjentix@gmail.com> Co-authored-by: Moudy <m.ellaz@hotmail.com> Co-authored-by: Sergio Chouhy <41742639+schouhy@users.noreply.github.com>
222 lines
7.8 KiB
Python
222 lines
7.8 KiB
Python
from smartcard.System import readers
|
|
from keycard.exceptions import APDUError, TransportError
|
|
from ecdsa import VerifyingKey, SECP256k1
|
|
|
|
from keycard.keycard import KeyCard
|
|
from keycard.commands.export_lee_key import export_lee_key
|
|
from mnemonic import Mnemonic
|
|
from keycard import constants
|
|
|
|
import os
|
|
import secrets
|
|
|
|
DEFAULT_PAIRING_PASSWORD = "KeycardDefaultPairing"
|
|
|
|
def _pairing_password() -> str:
|
|
return os.environ.get("KEYCARD_PAIRING_PASSWORD", DEFAULT_PAIRING_PASSWORD)
|
|
|
|
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 initialize(self, pin: str, pairing_password: str | None = None) -> bool:
|
|
try:
|
|
self.card.select()
|
|
|
|
if self.card.is_initialized:
|
|
raise RuntimeError("Card is already initialized")
|
|
|
|
puk = ''.join(secrets.choice('0123456789') for _ in range(12))
|
|
self.card.init(pin, puk, pairing_password or _pairing_password())
|
|
print(f"Keycard PUK: {puk}")
|
|
print("Record this PUK and store it somewhere safe. It cannot be recovered.")
|
|
return True
|
|
except Exception as e:
|
|
raise RuntimeError(f"Error initializing keycard: {e}") from e
|
|
|
|
def _reconnect(self) -> None:
|
|
self.card = KeyCard()
|
|
self.card.select()
|
|
|
|
def _pair(self, pin: str, password: str) -> tuple[int, bytes]:
|
|
self.card.select()
|
|
|
|
if not self.card.is_initialized:
|
|
raise RuntimeError("Card is not initialized — run 'wallet keycard init' first")
|
|
|
|
pairing_index, pairing_key = self.card.pair(password)
|
|
self.pairing_index = pairing_index
|
|
self.pairing_key = pairing_key
|
|
|
|
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 opening secure channel after fresh pair: {e}") from e
|
|
|
|
return pairing_index, pairing_key
|
|
|
|
def pair(self, pin: str, password: str | None = None) -> tuple[int, bytes]:
|
|
password = password or _pairing_password()
|
|
try:
|
|
return self._pair(pin, password)
|
|
except TransportError as e:
|
|
print(f"Transport error during fresh pair ({e}), attempting card reset and retry...")
|
|
try:
|
|
self._reconnect()
|
|
result = self._pair(pin, password)
|
|
print("Retry succeeded after card reset.")
|
|
return result
|
|
except TransportError as e2:
|
|
raise RuntimeError(
|
|
"Card lost power and did not recover after reset. "
|
|
"Try reseating the card in the reader."
|
|
) from e2
|
|
|
|
def _setup_communication_with_pairing(self, pin: str, pairing_index: int, pairing_key: bytes) -> bool:
|
|
self.card.select()
|
|
|
|
if not self.card.is_initialized:
|
|
raise RuntimeError("Card is not initialized — run 'wallet keycard init' first")
|
|
|
|
self.pairing_index = pairing_index
|
|
self.pairing_key = pairing_key
|
|
|
|
try:
|
|
self.card.open_secure_channel(pairing_index, pairing_key)
|
|
self.card.verify_pin(pin)
|
|
except Exception as e:
|
|
raise RuntimeError(f"Error setting up communication with stored pairing: {e}") from e
|
|
|
|
return True
|
|
|
|
def setup_communication_with_pairing(self, pin: str, pairing_index: int, pairing_key: bytes) -> bool:
|
|
try:
|
|
return self._setup_communication_with_pairing(pin, pairing_index, pairing_key)
|
|
except TransportError as e:
|
|
print(f"Transport error during stored pairing ({e}), attempting card reset and retry...")
|
|
try:
|
|
self._reconnect()
|
|
result = self._setup_communication_with_pairing(pin, pairing_index, pairing_key)
|
|
print("Retry succeeded after card reset.")
|
|
return result
|
|
except TransportError as e2:
|
|
raise RuntimeError(
|
|
"Card lost power and did not recover after reset. "
|
|
"Try reseating the card in the reader."
|
|
) from e2
|
|
|
|
def close_session(self) -> bool:
|
|
return True
|
|
|
|
def load_mnemonic(self, mnemonic: str) -> bool:
|
|
try:
|
|
# Convert mnemonic to seed
|
|
mnemo = Mnemonic("english")
|
|
if not mnemo.check(mnemonic):
|
|
raise RuntimeError("Invalid mnemonic phrase — check spelling and word count")
|
|
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
|
|
|
|
def get_private_keys_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
|
|
|
|
private_keys = export_lee_key(
|
|
self.card,
|
|
constants.DerivationOption.DERIVE,
|
|
path
|
|
)
|
|
|
|
nsk = private_keys.lee_nsk
|
|
vsk = private_keys.lee_vsk
|
|
|
|
return (nsk, vsk)
|
|
|
|
except Exception as e:
|
|
raise RuntimeError(f"Error getting private keys: {e}") from e
|
|
|