mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-05-14 20:19:51 +00:00
532 lines
16 KiB
Python
532 lines
16 KiB
Python
import pytest
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from keycard import constants
|
|
from keycard.apdu import APDUResponse
|
|
from keycard.exceptions import APDUError
|
|
from keycard.parsing.exported_key import ExportedKey
|
|
from keycard.keycard import KeyCard
|
|
from keycard.transport import Transport
|
|
|
|
|
|
def test_keycard_init_with_transport():
|
|
transport = MagicMock(spec=Transport)
|
|
kc = KeyCard(transport)
|
|
assert kc.transport == transport
|
|
assert kc.card_public_key is None
|
|
assert kc.session is None
|
|
|
|
|
|
def test_select_sets_card_pubkey():
|
|
mock_info = MagicMock()
|
|
mock_info.ecc_public_key = b'pubkey'
|
|
with patch('keycard.keycard.commands.select', return_value=mock_info):
|
|
kc = KeyCard(MagicMock())
|
|
result = kc.select()
|
|
assert kc.card_public_key == b'pubkey'
|
|
assert result == mock_info
|
|
|
|
|
|
def test_init_calls_command():
|
|
transport = MagicMock()
|
|
with patch('keycard.keycard.commands.init') as mock_init:
|
|
kc = KeyCard(transport)
|
|
kc.card_public_key = b'pub'
|
|
kc.init(b'pin', b'puk', b'secret')
|
|
mock_init.assert_called_once_with(kc, b'pin', b'puk', b'secret')
|
|
|
|
|
|
def test_ident_calls_command():
|
|
with patch('keycard.keycard.commands.ident', return_value='identity') as m:
|
|
kc = KeyCard(MagicMock())
|
|
result = kc.ident(b'challenge')
|
|
m.assert_called_once()
|
|
assert result == 'identity'
|
|
|
|
|
|
def test_open_secure_channel_with_mutual_authentication():
|
|
with patch(
|
|
'keycard.keycard.commands.open_secure_channel'
|
|
) as mock_osc:
|
|
with patch(
|
|
'keycard.keycard.commands.mutually_authenticate'
|
|
) as mock_ma:
|
|
mock_osc.return_value = 'session'
|
|
kc = KeyCard(MagicMock())
|
|
kc._card_public_key = b'pub'
|
|
kc.open_secure_channel(1, b'pairing_key')
|
|
mock_osc.assert_called_once_with(kc, 1, b'pairing_key')
|
|
mock_ma.assert_called_once_with(kc)
|
|
assert kc.session == 'session'
|
|
|
|
|
|
def test_open_secure_channel_without_mutual_authentication():
|
|
with patch(
|
|
'keycard.keycard.commands.open_secure_channel'
|
|
) as mock_osc:
|
|
with patch(
|
|
'keycard.keycard.commands.mutually_authenticate'
|
|
) as mock_ma:
|
|
mock_osc.return_value = 'session'
|
|
kc = KeyCard(MagicMock())
|
|
kc._card_public_key = b'pub'
|
|
kc.open_secure_channel(1, b'pairing_key', False)
|
|
mock_osc.assert_called_once_with(kc, 1, b'pairing_key')
|
|
mock_ma.assert_not_called()
|
|
assert kc.session == 'session'
|
|
|
|
|
|
def test_mutually_authenticate_calls_command():
|
|
with patch('keycard.keycard.commands.mutually_authenticate') as mock_auth:
|
|
kc = KeyCard(MagicMock())
|
|
kc.secure_session = 'sess'
|
|
kc.mutually_authenticate()
|
|
mock_auth.assert_called_once()
|
|
|
|
|
|
def test_pair_returns_expected_tuple():
|
|
with patch('keycard.keycard.commands.pair', return_value=(1, b'crypt')):
|
|
kc = KeyCard(MagicMock())
|
|
result = kc.pair(b'shared')
|
|
assert result == (1, b'crypt')
|
|
|
|
|
|
def test_verify_pin_delegates_call_and_returns_result():
|
|
with patch(
|
|
'keycard.keycard.commands.verify_pin',
|
|
return_value=True
|
|
) as mock_cmd:
|
|
kc = KeyCard(MagicMock())
|
|
kc.secure_session = 'sess'
|
|
result = kc.verify_pin('1234')
|
|
mock_cmd.assert_called_once_with(kc, b'1234')
|
|
assert result is True
|
|
|
|
|
|
def test_unpair_delegates_call():
|
|
transport = MagicMock()
|
|
with patch('keycard.keycard.commands.unpair') as mock_unpair:
|
|
kc = KeyCard(transport)
|
|
kc.secure_session = 'sess'
|
|
kc.unpair(2)
|
|
mock_unpair.assert_called_once_with(kc, 2)
|
|
|
|
|
|
def test_send_secure_apdu_success():
|
|
mock_session = MagicMock()
|
|
mock_session.wrap_apdu.return_value = b'encrypted'
|
|
mock_session.unwrap_response.return_value = (b'plaintext', 0x9000)
|
|
mock_transport = MagicMock()
|
|
mock_response = MagicMock()
|
|
mock_response.status_word = 0x9000
|
|
mock_response.data = b'ciphertext'
|
|
mock_transport.send_apdu.return_value = mock_response
|
|
|
|
kc = KeyCard(mock_transport)
|
|
kc.session = mock_session
|
|
|
|
result = kc.send_secure_apdu(0xA4, 0x01, 0x02, b'data')
|
|
|
|
mock_session.wrap_apdu.assert_called_once_with(
|
|
cla=kc.transport.send_apdu.call_args[0][0][0],
|
|
ins=0xA4,
|
|
p1=0x01,
|
|
p2=0x02,
|
|
data=b'data'
|
|
)
|
|
mock_transport.send_apdu.assert_called_once()
|
|
mock_session.unwrap_response.assert_called_once_with(mock_response)
|
|
assert result == b'plaintext'
|
|
|
|
|
|
def test_send_secure_apdu_raises_on_transport_status_word():
|
|
mock_session = MagicMock()
|
|
mock_session.wrap_apdu.return_value = b'encrypted'
|
|
mock_transport = MagicMock()
|
|
mock_transport.send_apdu.return_value = APDUResponse(
|
|
b'', status_word=0x6A82)
|
|
|
|
kc = KeyCard(mock_transport)
|
|
kc.session = mock_session
|
|
|
|
with pytest.raises(APDUError) as exc:
|
|
kc.send_secure_apdu(0xA4, 0x00, 0x00, b'data')
|
|
assert exc.value.args[0] == 'APDU failed with SW=6A82'
|
|
|
|
|
|
def test_send_secure_apdu_raises_on_unwrap_status_word():
|
|
mock_session = MagicMock()
|
|
mock_session.wrap_apdu.return_value = b'encrypted'
|
|
mock_session.unwrap_response.return_value = (b'plaintext', 0x6A84)
|
|
mock_transport = MagicMock()
|
|
mock_transport.send_apdu.return_value = APDUResponse(
|
|
b'', status_word=0x9000)
|
|
|
|
kc = KeyCard(mock_transport)
|
|
kc.session = mock_session
|
|
|
|
with pytest.raises(APDUError) as exc:
|
|
kc.send_secure_apdu(0xA4, 0x00, 0x00, b'data')
|
|
assert exc.value.args[0] == 'APDU failed with SW=6A84'
|
|
|
|
|
|
def test_send_apdu_success(monkeypatch):
|
|
mock_transport = MagicMock()
|
|
mock_response = MagicMock()
|
|
mock_response.status_word = 0x9000
|
|
mock_response.data = b'response'
|
|
mock_transport.send_apdu.return_value = mock_response
|
|
|
|
kc = KeyCard(mock_transport)
|
|
|
|
result = kc.send_apdu(ins=0xA4, p1=0x01, p2=0x02, data=b'data')
|
|
expected_apdu = bytes([0x80, 0xA4, 0x01, 0x02, 4]) + b'data'
|
|
mock_transport.send_apdu.assert_called_once_with(expected_apdu)
|
|
assert result == b'response'
|
|
|
|
|
|
def test_send_apdu_raises_on_non_success_status(monkeypatch):
|
|
mock_transport = MagicMock()
|
|
mock_transport.send_apdu.return_value = APDUResponse(b'', 0x6A82)
|
|
|
|
kc = KeyCard(mock_transport)
|
|
|
|
with pytest.raises(APDUError) as exc:
|
|
kc.send_apdu(ins=0xA4, p1=0x00, p2=0x00, data=b'')
|
|
assert exc.value.args[0] == 'APDU failed with SW=6A82'
|
|
|
|
|
|
def test_send_apdu_with_custom_cla(monkeypatch):
|
|
mock_transport = MagicMock()
|
|
mock_response = MagicMock()
|
|
mock_response.status_word = 0x9000
|
|
mock_response.data = b'abc'
|
|
mock_transport.send_apdu.return_value = mock_response
|
|
|
|
kc = KeyCard(mock_transport)
|
|
|
|
result = kc.send_apdu(ins=0xA4, p1=0x01, p2=0x02, data=b'data', cla=0x90)
|
|
expected_apdu = bytes([0x90, 0xA4, 0x01, 0x02, 4]) + b'data'
|
|
mock_transport.send_apdu.assert_called_once_with(expected_apdu)
|
|
assert result == b'abc'
|
|
|
|
|
|
def test_unblock_pin_calls_command_with_bytes():
|
|
with patch('keycard.keycard.commands.unblock_pin') as mock_unblock:
|
|
kc = KeyCard(MagicMock())
|
|
puk = b'123456789012'
|
|
new_pin = b'654321'
|
|
kc.unblock_pin(puk, new_pin)
|
|
mock_unblock.assert_called_once_with(kc, puk + new_pin)
|
|
|
|
|
|
def test_unblock_pin_calls_command_with_str():
|
|
with patch('keycard.keycard.commands.unblock_pin') as mock_unblock:
|
|
kc = KeyCard(MagicMock())
|
|
puk = '123456789012'
|
|
new_pin = '654321'
|
|
kc.unblock_pin(puk, new_pin)
|
|
mock_unblock.assert_called_once_with(
|
|
kc,
|
|
(puk + new_pin).encode('utf-8')
|
|
)
|
|
|
|
|
|
def test_unblock_pin_calls_command_with_mixed_types():
|
|
with patch('keycard.keycard.commands.unblock_pin') as mock_unblock:
|
|
kc = KeyCard(MagicMock())
|
|
puk = '123456789012'
|
|
new_pin = b'654321'
|
|
kc.unblock_pin(puk, new_pin)
|
|
mock_unblock.assert_called_once_with(kc, puk.encode('utf-8') + new_pin)
|
|
|
|
|
|
def test_remove_key_calls_command():
|
|
with patch('keycard.keycard.commands.remove_key') as mock_remove_key:
|
|
kc = KeyCard(MagicMock())
|
|
kc.remove_key()
|
|
mock_remove_key.assert_called_once_with(kc)
|
|
|
|
|
|
def test_store_data_calls_command_with_default_slot():
|
|
with patch('keycard.keycard.commands.store_data') as mock_store_data:
|
|
kc = KeyCard(MagicMock())
|
|
data = b'testdata'
|
|
kc.store_data(data)
|
|
mock_store_data.assert_called_once_with(
|
|
kc, data, constants.StorageSlot.PUBLIC
|
|
)
|
|
|
|
|
|
def test_store_data_calls_command_with_custom_slot():
|
|
with patch('keycard.keycard.commands.store_data') as mock_store_data:
|
|
kc = KeyCard(MagicMock())
|
|
data = b'testdata'
|
|
slot = MagicMock()
|
|
kc.store_data(data, slot)
|
|
mock_store_data.assert_called_once_with(kc, data, slot)
|
|
|
|
|
|
def test_store_data_raises_value_error_on_invalid_slot():
|
|
with patch(
|
|
'keycard.keycard.commands.store_data',
|
|
side_effect=ValueError("Invalid slot")
|
|
):
|
|
kc = KeyCard(MagicMock())
|
|
with pytest.raises(ValueError, match="Invalid slot"):
|
|
kc.store_data(b'testdata', slot="INVALID")
|
|
|
|
|
|
def test_store_data_raises_value_error_on_data_too_long():
|
|
with patch(
|
|
'keycard.keycard.commands.store_data',
|
|
side_effect=ValueError("data is too long")
|
|
):
|
|
kc = KeyCard(MagicMock())
|
|
long_data = b'a' * 128
|
|
with pytest.raises(ValueError, match="data is too long"):
|
|
kc.store_data(long_data)
|
|
|
|
|
|
def test_get_data_calls_command_with_default_slot():
|
|
with patch(
|
|
'keycard.keycard.commands.get_data',
|
|
return_value=b'data'
|
|
) as mock_get_data:
|
|
kc = KeyCard(MagicMock())
|
|
result = kc.get_data()
|
|
mock_get_data.assert_called_once_with(kc, constants.StorageSlot.PUBLIC)
|
|
assert result == b'data'
|
|
|
|
|
|
def test_get_data_calls_command_with_custom_slot():
|
|
with patch(
|
|
'keycard.keycard.commands.get_data',
|
|
return_value=b'data'
|
|
) as mock_get_data:
|
|
kc = KeyCard(MagicMock())
|
|
slot = MagicMock()
|
|
result = kc.get_data(slot)
|
|
mock_get_data.assert_called_once_with(kc, slot)
|
|
assert result == b'data'
|
|
|
|
|
|
def test_export_key_delegates_and_returns_result():
|
|
mock_exported = MagicMock(spec=ExportedKey)
|
|
with patch(
|
|
'keycard.keycard.commands.export_key',
|
|
return_value=mock_exported
|
|
) as mock_cmd:
|
|
kc = KeyCard(MagicMock())
|
|
result = kc.export_key(
|
|
derivation_option=constants.DerivationOption.DERIVE,
|
|
public_only=True,
|
|
keypath="m/44'/60'/0'/0/0",
|
|
make_current=True,
|
|
source=constants.DerivationSource.PARENT
|
|
)
|
|
|
|
mock_cmd.assert_called_once_with(
|
|
kc,
|
|
derivation_option=constants.DerivationOption.DERIVE,
|
|
public_only=True,
|
|
keypath="m/44'/60'/0'/0/0",
|
|
make_current=True,
|
|
source=constants.DerivationSource.PARENT
|
|
)
|
|
assert result is mock_exported
|
|
|
|
|
|
def test_export_current_key_delegates_and_returns_result():
|
|
mock_exported = MagicMock(spec=ExportedKey)
|
|
with patch(
|
|
'keycard.keycard.commands.export_key',
|
|
return_value=mock_exported
|
|
) as mock_cmd:
|
|
kc = KeyCard(MagicMock())
|
|
result = kc.export_current_key(public_only=False)
|
|
|
|
mock_cmd.assert_called_once_with(
|
|
kc,
|
|
derivation_option=constants.DerivationOption.CURRENT,
|
|
public_only=False,
|
|
keypath=None,
|
|
make_current=False,
|
|
source=constants.DerivationSource.MASTER
|
|
)
|
|
assert result is mock_exported
|
|
|
|
|
|
def test_sign_current_key():
|
|
with patch("keycard.keycard.commands.sign") as mock_sign:
|
|
card = KeyCard(MagicMock())
|
|
digest = b"\xAA" * 32
|
|
mock_sign.return_value = "signed"
|
|
|
|
result = card.sign(digest)
|
|
|
|
mock_sign.assert_called_once_with(
|
|
card,
|
|
digest,
|
|
constants.DerivationOption.CURRENT,
|
|
constants.SigningAlgorithm.ECDSA_SECP256K1
|
|
)
|
|
assert result == "signed"
|
|
|
|
|
|
def test_sign_with_path():
|
|
with patch("keycard.keycard.commands.sign") as mock_sign:
|
|
card = KeyCard(MagicMock())
|
|
digest = b"\xBB" * 32
|
|
path = [0x8000002C, 0x8000003C, 0, 0, 0] # m/44'/60'/0'/0/0
|
|
mock_sign.return_value = "sig"
|
|
|
|
result = card.sign_with_path(digest, path)
|
|
|
|
mock_sign.assert_called_once_with(
|
|
card,
|
|
digest,
|
|
constants.DerivationOption.DERIVE,
|
|
constants.SigningAlgorithm.ECDSA_SECP256K1,
|
|
derivation_path=path
|
|
)
|
|
assert result == "sig"
|
|
|
|
|
|
def test_sign_with_path_make_current():
|
|
with patch("keycard.keycard.commands.sign") as mock_sign:
|
|
card = KeyCard(MagicMock())
|
|
digest = b"\xCC" * 32
|
|
path = [0x8000002C, 0x8000003C, 0, 0, 0]
|
|
mock_sign.return_value = "sig"
|
|
|
|
result = card.sign_with_path(digest, path, make_current=True)
|
|
|
|
mock_sign.assert_called_once_with(
|
|
card,
|
|
digest,
|
|
constants.DerivationOption.DERIVE_AND_MAKE_CURRENT,
|
|
constants.SigningAlgorithm.ECDSA_SECP256K1,
|
|
derivation_path=path
|
|
)
|
|
assert result == "sig"
|
|
|
|
|
|
def test_sign_pinless():
|
|
with patch("keycard.keycard.commands.sign") as mock_sign:
|
|
card = KeyCard(MagicMock())
|
|
digest = b"\xDD" * 32
|
|
mock_sign.return_value = "sig"
|
|
|
|
result = card.sign_pinless(digest)
|
|
|
|
mock_sign.assert_called_once_with(
|
|
card,
|
|
digest,
|
|
constants.DerivationOption.PINLESS,
|
|
constants.SigningAlgorithm.ECDSA_SECP256K1
|
|
)
|
|
assert result == "sig"
|
|
|
|
|
|
def test_load_key_bip39_seed():
|
|
with patch("keycard.keycard.commands.load_key") as mock_load_key:
|
|
card = KeyCard(MagicMock())
|
|
seed = b"\xAB" * 64
|
|
mock_load_key.return_value = b"uid"
|
|
|
|
result = card.load_key(
|
|
key_type=constants.LoadKeyType.BIP39_SEED,
|
|
bip39_seed=seed
|
|
)
|
|
|
|
mock_load_key.assert_called_once_with(
|
|
card,
|
|
key_type=constants.LoadKeyType.BIP39_SEED,
|
|
public_key=None,
|
|
private_key=None,
|
|
chain_code=None,
|
|
bip39_seed=seed,
|
|
lee_seed=None
|
|
)
|
|
assert result == b"uid"
|
|
|
|
|
|
def test_load_key_ecc_pair():
|
|
with patch("keycard.keycard.commands.load_key") as mock_load_key:
|
|
card = KeyCard(MagicMock())
|
|
pub = b"\x04" + b"\x01" * 64
|
|
priv = b"\x02" * 32
|
|
mock_load_key.return_value = b"uid"
|
|
|
|
result = card.load_key(
|
|
key_type=constants.LoadKeyType.ECC,
|
|
public_key=pub,
|
|
private_key=priv
|
|
)
|
|
|
|
mock_load_key.assert_called_once_with(
|
|
card,
|
|
key_type=constants.LoadKeyType.ECC,
|
|
public_key=pub,
|
|
private_key=priv,
|
|
chain_code=None,
|
|
bip39_seed=None,
|
|
lee_seed=None
|
|
)
|
|
assert result == b"uid"
|
|
|
|
|
|
def test_load_key_extended():
|
|
with patch("keycard.keycard.commands.load_key") as mock_load_key:
|
|
card = KeyCard(MagicMock())
|
|
pub = b"\x04" + b"\x01" * 64
|
|
priv = b"\x02" * 32
|
|
chain = b"\x00" * 32
|
|
mock_load_key.return_value = b"uid"
|
|
|
|
result = card.load_key(
|
|
key_type=constants.LoadKeyType.EXTENDED_ECC,
|
|
public_key=pub,
|
|
private_key=priv,
|
|
chain_code=chain
|
|
)
|
|
|
|
mock_load_key.assert_called_once_with(
|
|
card,
|
|
key_type=constants.LoadKeyType.EXTENDED_ECC,
|
|
public_key=pub,
|
|
private_key=priv,
|
|
chain_code=chain,
|
|
bip39_seed=None,
|
|
lee_seed=None
|
|
)
|
|
assert result == b"uid"
|
|
|
|
|
|
def test_keycard_set_pinless_path():
|
|
with patch("keycard.keycard.commands.set_pinless_path") as mock_cmd:
|
|
card = KeyCard(MagicMock())
|
|
card.set_pinless_path("m/44'/60'/0'/0/0")
|
|
|
|
mock_cmd.assert_called_once_with(card, "m/44'/60'/0'/0/0")
|
|
|
|
|
|
def test_keycard_generate_mnemonic():
|
|
with patch("keycard.keycard.commands.generate_mnemonic") as mock_cmd:
|
|
card = KeyCard(None)
|
|
mock_cmd.return_value = [0, 2047, 1337, 42]
|
|
|
|
result = card.generate_mnemonic(checksum_size=6)
|
|
|
|
mock_cmd.assert_called_once_with(card, 6)
|
|
assert result == [0, 2047, 1337, 42]
|
|
|
|
|
|
def test_keycard_derive_key():
|
|
with patch("keycard.keycard.commands.derive_key") as mock_cmd:
|
|
card = KeyCard(MagicMock())
|
|
card.derive_key("m/44'/60'/0'/0/0")
|
|
|
|
mock_cmd.assert_called_once_with(card, "m/44'/60'/0'/0/0")
|