This commit is contained in:
jonesmarvin8 2026-04-22 21:23:33 -04:00
parent f892b92ee7
commit 096522ebb9
4 changed files with 94 additions and 272 deletions

15
python/keycard_test.py Normal file
View File

@ -0,0 +1,15 @@
import keycard_wallet as keycard_wallet
import time # For testing
pin = '111111'
my_wallet = keycard_wallet.KeycardWallet()
print("Setup communication with card...", my_wallet.setup_communication(pin))
print("Load mnemonic...", my_wallet.load_mnemonic())
print("Public key", my_wallet.get_public_key_for_path())
print("Signature", my_wallet.sign_message_for_path())
print("Disconnection", my_wallet.disconnect())

View File

@ -10,7 +10,10 @@ from keycard import constants
import keycard
PIN = '123456'
PUK = '123456123456'
DEFAULT_PAIRING_PASSWORD = "KeycardDefaultPairing"
DEFAULT_MNEMONIC = "fashion degree mountain wool question damp current pond grow dolphin chronic then"
DEFAULT_PASSPHRASE = ""
class KeycardWallet:
def __init__(self):
@ -46,144 +49,88 @@ class KeycardWallet:
self.card.select()
if not self.card.is_initialized:
# TODO: need to be able to initialize a card.
return False
if self.pairing_index is None:
pairing_index, pairing_key = self.card.pair(password) #Testing
pairing_index, pairing_key = self.card.pair(password)
self.pairing_index = pairing_index
self.pairing_key = pairing_key
self.card.open_secure_channel(pairing_index, pairing_key)
self.card.verify_pin(PIN)
self.card.verify_pin(pin)
return True
except Exception as e:
print(f"Error: {e}")
return False
"""
# Needs to be more robust to handle card removal and reinsertion
def is_selected_card_available(self) -> bool:
if self.transport.connection is None:
return False
def load_mnemonic(self, mnemonic = DEFAULT_MNEMONIC, passphrase = DEFAULT_PASSPHRASE) -> bool:
try:
#TODO: fix this up Try a lightweight operation
# Card is present
self.card.send_apdu(cla=0x00, ins=0xA4, p1=0x04, p2=0x00, data=b'')
# return True
except Exception:
return False
# TODO: attempt to prevent a new card from being inserted
return self.card.is_selected
# Convert mnemonic to seed
mnemo = Mnemonic("english")
seed = mnemo.to_seed(mnemonic, passphrase)
"""
print(f"PIN verified: {self.card.is_pin_verified}")
print(f"Secure channel open: {self.card.is_secure_channel_open}")
print(f"Card initialized: {self.card.status.get('initialized', False)}")
print(f"Seed length: {len(seed)}")
# Load the LEE seed onto the card
result = self.card.load_key(
key_type = constants.LoadKeyType.BIP39_SEED,
bip39_seed = seed
)
# Wrapped
def disconnect(self) -> bool:
try:
self.card.unpair(self.pairing_index)
self.pairing_index = None
self.pairing_key = None
return True
except Exception as e:
print(f"Error during disconnect: {e}")
return False
# TODO: add path?
# Wrapped
def get_public_signing_key(self):
uncompressed_pub_key = self.card.export_current_key(public_only=True).public_key
# Convert to VerifyingKey object
vk = VerifyingKey.from_string(uncompressed_pub_key, curve=SECP256k1)
return vk.to_string("compressed")[1:]
"""
# TODO: don't think this possible; blocked by firmware
def get_private_signing_key(self):
def disconnect(self) -> bool:
try:
exported = self.card.export_current_key(public_only=False)
print(f"Exported key: {exported}")
print(f"Public key: {exported.public_key.hex() if exported.public_key else 'None'}")
print(f"Private key: {exported.private_key.hex() if exported.private_key else 'None'}")
print(f"Chain code: {exported.chain_code.hex() if exported.chain_code else 'None'}")
if not self.card.is_secure_channel_open:
return None
if exported.private_key is None:
raise ValueError("No private key returned - key may not be loaded on card")
return exported.private_key
except Exception as e:
print(f"Error exporting key: {e}")
raise
"""
# TODO: delete this function
def debug_key_export(self):
"""Debug why key export fails with SW=6985"""
# 1. Check if a key exists
try:
status = self.card.status
print(f"Status: {status}")
except Exception as e:
print(f"Cannot get status: {e}")
# 2. Try public key export first
try:
exported = self.card.export_current_key(public_only=True)
print(f"Public key export: {exported.public_key.hex() if exported.public_key else 'None'}")
except Exception as e:
print(f"Public key export failed: {e}")
# 3. Check if key needs to be generated
try:
key_uid = self. card.generate_key()
print(f"Generated key UID: {key_uid.hex()}")
except Exception as e:
print(f"Key generation failed: {e}")
# 4. Try private export again
try:
exported = self.card.export_current_key(public_only=False)
if exported.private_key:
print(f"Private key: {exported.private_key.hex()}")
else:
print("Private key is None - key may not allow export")
except Exception as e:
print(f"Private key export failed: {e}")
#TODO: check well formed?
# Wrapped
def change_path(self, path):
self.card.derive_key(path)
self.card.unpair(self.pairing_index)
self.pairing_index = None
self.pairing_key = None
# Message must be 32 bytes
# TODO: rename to current_path
# Wrapped
def sign_message_current_key(self, message = b"TestMessageMustBe32Bytes!\x00\x00\x00\x00\x00\x00\x00"):
# Message must be sent bytes
return self.card.sign(message, constants.SigningAlgorithm.SCHNORR_BIP340)
# Does not update the path
# Wrapped
def sign_message_with_path(self, path, message = b"TestMessageMustBe32Bytes!\x00\x00\x00\x00\x00\x00\x00"):
# must be sent bytes
return self.card.sign_with_path(message, path, False, constants.SigningAlgorithm.SCHNORR_BIP340)
# Wrapped
def remove_account_keys(self):
self.card.remove_key()
# TODO: update to accept a different language?
def load_account_keys(self, mnemonic) :
mnemo = Mnemonic("english")
seed = mnemo.to_seed(mnemonic, passphrase="")
return True
except Exception as e:
print(f"Error during unpair: {e}")
return False
# Load the seed onto the card
result = self.card.load_key(
key_type= constants.LoadKeyType.BIP39_SEED,
lee_seed=seed
)
def get_public_key_for_path(self, path: str = "m/44'/60'/0'/0/0") -> str | 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
)
return public_key.public_key.hex()
except Exception as e:
print(f"Error getting public key: {e}")
return None
def sign_message_for_path(self, message: bytes = b"DefaultMessageTestDefaultMessage", path: str = "m/44'/60'/0'/0/0") -> str | 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,
make_current = False
)
return signature.signature.hex()
except Exception as e:
print(f"Error signing message: {e}")
return None

View File

@ -25,121 +25,8 @@ pub enum KeycardSubcommand {
)]
mnemonic: Option<String>,
},
Remove,
}
/// Represents generic register CLI subcommand.
/*
#[derive(Subcommand, Debug, Clone)]
pub enum NewSubcommand {
/// Register new public account.
Public {
#[arg(long)]
/// Chain index of a parent node.
cci: Option<ChainIndex>,
#[arg(short, long)]
/// Label to assign to the new account.
label: Option<String>,
},
/// Register new private account.
Private {
#[arg(long)]
/// Chain index of a parent node.
cci: Option<ChainIndex>,
#[arg(short, long)]
/// Label to assign to the new account.
label: Option<String>,
},
}
*/
/*
impl WalletSubcommand for NewSubcommand {
async fn handle_subcommand(
self,
wallet_core: &mut WalletCore,
) -> Result<SubcommandReturnValue> {
match self {
Self::Public { cci, label } => {
if let Some(label) = &label
&& wallet_core
.storage
.labels
.values()
.any(|l| l.to_string() == *label)
{
anyhow::bail!("Label '{label}' is already in use by another account");
}
let (account_id, chain_index) = wallet_core.create_new_account_public(cci);
let private_key = wallet_core
.storage
.user_data
.get_pub_account_signing_key(account_id)
.unwrap();
let public_key = PublicKey::new_from_private_key(private_key);
if let Some(label) = label {
wallet_core
.storage
.labels
.insert(account_id.to_string(), Label::new(label));
}
println!(
"Generated new account with account_id Public/{account_id} at path {chain_index}"
);
println!("With pk {}", hex::encode(public_key.value()));
wallet_core.store_persistent_data().await?;
Ok(SubcommandReturnValue::RegisterAccount { account_id })
}
Self::Private { cci, label } => {
if let Some(label) = &label
&& wallet_core
.storage
.labels
.values()
.any(|l| l.to_string() == *label)
{
anyhow::bail!("Label '{label}' is already in use by another account");
}
let (account_id, chain_index) = wallet_core.create_new_account_private(cci);
if let Some(label) = label {
wallet_core
.storage
.labels
.insert(account_id.to_string(), Label::new(label));
}
let (key, _) = wallet_core
.storage
.user_data
.get_private_account(account_id)
.unwrap();
println!(
"Generated new account with account_id Private/{account_id} at path {chain_index}",
);
println!("With npk {}", hex::encode(key.nullifier_public_key.0));
println!(
"With vpk {}",
hex::encode(key.viewing_public_key.to_bytes())
);
wallet_core.store_persistent_data().await?;
Ok(SubcommandReturnValue::RegisterAccount { account_id })
}
}
}
}
*/
impl WalletSubcommand for KeycardSubcommand {
#[expect(clippy::cognitive_complexity, reason = "TODO: fix later")]
async fn handle_subcommand(

View File

@ -23,7 +23,7 @@ impl KeycardWallet {
/// Calls Python: is_unpaired_keycard_available()
pub fn is_unpaired_keycard_available(&self, py: Python) -> PyResult<bool> {
self.instance
.bind(py) // replaces as_ref(py)
.bind(py)
.call_method0("is_unpaired_keycard_available")?
.extract()
}
@ -44,22 +44,6 @@ impl KeycardWallet {
.extract()
}
pub fn get_public_signing_key(&self, py: Python) -> PyResult<[u8; 32]> {
self.instance
.bind(py)
.call_method0("get_public_signing_key")?
.extract()
}
pub fn derive_path(&self, py: Python, path: Vec<u32>) -> PyResult<()> {
let path = Self::convert_path_to_string(path);
self.instance
.bind(py)
.call_method1("change_path", (path,))?;
Ok(())
}
fn convert_path_to_string(path: Vec<u32>) -> String {
format!(
"m/{}",
@ -70,23 +54,19 @@ impl KeycardWallet {
)
}
pub fn sign_message_current_key(&self, py: Python, message: &[u8; 32]) -> PyResult<[u8; 64]> {
let py_message = pyo3::types::PyBytes::new_bound(py, message);
let py_signature: Vec<u8> = self.instance
pub fn get_public_key_for_path(
&self,
py: Python,
path: &str,
) -> PyResult<Option<[u8;32]>> {
let public_key: Vec<u8> = self.instance
.bind(py)
.call_method1("sign_message_current_key", (py_message,))?
.getattr("signature")? // or "bytes", "data", "value", etc.
.call_method1("get_public_key_for_path", (py_message, path))?
.getattr("public_key")?
.extract()?;
let signature: [u8; 64] = py_signature
.try_into()
.map_err(|_| PyErr::new::<pyo3::exceptions::PyValueError, _>(
"Expected signature of exactly 64 bytes"
))?;
Ok(signature)
}
Ok(Some(public_key.bytes()))
}
pub fn sign_message_with_path(&self, py: Python, path: Vec<u32>, message: &[u8; 32]) -> PyResult<[u8; 64]> {
let py_message = pyo3::types::PyBytes::new_bound(py, message);
@ -94,8 +74,8 @@ impl KeycardWallet {
let py_signature: Vec<u8> = self.instance
.bind(py)
.call_method1("sign_message_with_path", (path, py_message))?
.getattr("signature")? // or "bytes", "data", "value", etc.
.call_method1("sign_message_with_path", (py_message, path))?
.getattr("signature")?
.extract()?;
let signature: [u8; 64] = py_signature
@ -107,17 +87,10 @@ impl KeycardWallet {
Ok(signature)
}
pub fn remove_account_keys(&self, py: Python) -> PyResult<()> {
pub fn load_mnemonic(&self, py: Python, mnemonic: &str) -> PyResult<()> {
self.instance
.bind(py)
.call_method0("remove_account_keys")?;
Ok(())
}
pub fn load_account_keys(&self, py: Python, mnemonic: &str) -> PyResult<()> {
self.instance
.bind(py)
.call_method1("load_account_keys", (mnemonic,))?;
.call_method1("load_mnemonic", (mnemonic,))?;
Ok(())
}
}