mirror of
https://github.com/logos-blockchain/logos-execution-zone.git
synced 2026-08-27 04:11:08 +00:00
fixes
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
import sys
|
||||
import pytest
|
||||
|
||||
from unittest.mock import Mock, patch
|
||||
from keycard import constants
|
||||
from keycard.card_interface import CardInterface
|
||||
from keycard.exceptions import APDUError
|
||||
from keycard.commands.change_secret import change_secret
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_card():
|
||||
card = Mock(spec=CardInterface)
|
||||
card.send_secure_apdu = Mock()
|
||||
return card
|
||||
|
||||
|
||||
def test_change_secret_pairing_str_success(mock_card):
|
||||
change_secret_module = sys.modules['keycard.commands.change_secret']
|
||||
with patch.object(
|
||||
change_secret_module, 'generate_pairing_token'
|
||||
) as mock_generate:
|
||||
mock_generate.return_value = bytes(32)
|
||||
change_secret(mock_card, 'pairingtoken', constants.PinType.PAIRING)
|
||||
mock_generate.assert_called_once_with('pairingtoken')
|
||||
mock_card.send_secure_apdu.assert_called_once_with(
|
||||
ins=constants.INS_CHANGE_SECRET,
|
||||
p1=constants.PinType.PAIRING.value,
|
||||
data=mock_generate.return_value
|
||||
)
|
||||
|
||||
|
||||
def test_change_secret_user_pin_str_success(mock_card):
|
||||
pin = '123456'
|
||||
change_secret(mock_card, pin, constants.PinType.USER)
|
||||
mock_card.send_secure_apdu.assert_called_once_with(
|
||||
ins=constants.INS_CHANGE_SECRET,
|
||||
p1=constants.PinType.USER.value,
|
||||
data=pin.encode('utf-8')
|
||||
)
|
||||
|
||||
|
||||
def test_change_secret_user_pin_invalid_length(mock_card):
|
||||
with pytest.raises(ValueError, match="User PIN must be exactly 6 digits."):
|
||||
change_secret(mock_card, b'12345', constants.PinType.USER)
|
||||
with pytest.raises(ValueError, match="User PIN must be exactly 6 digits."):
|
||||
change_secret(mock_card, '12345', constants.PinType.USER)
|
||||
|
||||
|
||||
def test_change_secret_puk_success(mock_card):
|
||||
puk = b'123456789012'
|
||||
change_secret(mock_card, puk, constants.PinType.PUK)
|
||||
mock_card.send_secure_apdu.assert_called_once_with(
|
||||
ins=constants.INS_CHANGE_SECRET,
|
||||
p1=constants.PinType.PUK.value,
|
||||
data=puk
|
||||
)
|
||||
|
||||
|
||||
def test_change_secret_puk_str_success(mock_card):
|
||||
puk = '123456789012'
|
||||
change_secret(mock_card, puk, constants.PinType.PUK)
|
||||
mock_card.send_secure_apdu.assert_called_once_with(
|
||||
ins=constants.INS_CHANGE_SECRET,
|
||||
p1=constants.PinType.PUK.value,
|
||||
data=puk.encode('utf-8')
|
||||
)
|
||||
|
||||
|
||||
def test_change_secret_puk_invalid_length(mock_card):
|
||||
with pytest.raises(ValueError, match="PUK must be exactly 12 digits."):
|
||||
change_secret(mock_card, b'1234567890', constants.PinType.PUK)
|
||||
with pytest.raises(ValueError, match="PUK must be exactly 12 digits."):
|
||||
change_secret(mock_card, '1234567890', constants.PinType.PUK)
|
||||
|
||||
|
||||
def test_change_secret_pairing_bytes_success(mock_card):
|
||||
secret = b'a' * 32
|
||||
change_secret(mock_card, secret, constants.PinType.PAIRING)
|
||||
mock_card.send_secure_apdu.assert_called_once_with(
|
||||
ins=constants.INS_CHANGE_SECRET,
|
||||
p1=constants.PinType.PAIRING.value,
|
||||
data=secret
|
||||
)
|
||||
|
||||
|
||||
def test_change_secret_pairing_bytes_invalid_length(mock_card):
|
||||
with pytest.raises(ValueError, match="Pairing secret must be 32 bytes."):
|
||||
change_secret(mock_card, b'a' * 31, constants.PinType.PAIRING)
|
||||
|
||||
|
||||
def test_change_secret_raises_apdu_error(mock_card):
|
||||
mock_card.send_secure_apdu.side_effect = APDUError(0x6A80)
|
||||
with pytest.raises(APDUError):
|
||||
change_secret(mock_card, b'123456', constants.PinType.USER)
|
||||
@@ -0,0 +1,14 @@
|
||||
from keycard.commands import derive_key
|
||||
from keycard.constants import INS_DERIVE_KEY, DerivationSource
|
||||
from keycard.parsing.keypath import KeyPath
|
||||
|
||||
|
||||
def test_derive_key_valid_master(card):
|
||||
key_path = KeyPath("m/44'/60'/0'/0/0")
|
||||
derive_key(card, key_path.to_string())
|
||||
|
||||
card.send_secure_apdu.assert_called_once_with(
|
||||
ins=INS_DERIVE_KEY,
|
||||
p1=DerivationSource.MASTER,
|
||||
data=key_path.data
|
||||
)
|
||||
@@ -0,0 +1,100 @@
|
||||
import pytest
|
||||
from keycard.commands.export_key import export_key
|
||||
from keycard.constants import DerivationOption, DerivationSource
|
||||
from keycard.parsing.exported_key import ExportedKey
|
||||
|
||||
|
||||
def test_export_key_success_public_only(card):
|
||||
public_key = b'\x04' + b'\x01' * 64
|
||||
inner_tlv = b'\x80' + bytes([len(public_key)]) + public_key
|
||||
outer_tlv = b'\xA1' + bytes([len(inner_tlv)]) + inner_tlv
|
||||
card.send_secure_apdu.return_value = outer_tlv
|
||||
|
||||
result = export_key(
|
||||
card,
|
||||
derivation_option=DerivationOption.CURRENT,
|
||||
public_only=True,
|
||||
keypath=None,
|
||||
make_current=False,
|
||||
source=DerivationSource.MASTER
|
||||
)
|
||||
|
||||
assert isinstance(result, ExportedKey)
|
||||
assert result.public_key == public_key
|
||||
assert result.private_key is None
|
||||
assert result.chain_code is None
|
||||
|
||||
|
||||
def test_export_key_with_path_string(card):
|
||||
public_key = b'\x04' + b'\x02' * 64
|
||||
inner_tlv = b'\x80' + bytes([len(public_key)]) + public_key
|
||||
outer_tlv = b'\xA1' + bytes([len(inner_tlv)]) + inner_tlv
|
||||
card.send_secure_apdu.return_value = outer_tlv
|
||||
|
||||
result = export_key(
|
||||
card,
|
||||
derivation_option=DerivationOption.DERIVE,
|
||||
public_only=True,
|
||||
keypath="m/44'/60'/0'/0/0",
|
||||
make_current=True,
|
||||
source=DerivationSource.MASTER
|
||||
)
|
||||
|
||||
assert isinstance(result, ExportedKey)
|
||||
assert result.public_key == public_key
|
||||
|
||||
|
||||
def test_export_key_invalid_keypath_length_bytes(card):
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match="Byte keypath must be a multiple of 4"
|
||||
):
|
||||
export_key(
|
||||
card,
|
||||
derivation_option=DerivationOption.DERIVE,
|
||||
public_only=True,
|
||||
keypath=b'\x01\x02\x03',
|
||||
make_current=False,
|
||||
source=DerivationSource.PARENT
|
||||
)
|
||||
|
||||
|
||||
def test_export_key_requires_keypath_if_not_current(card):
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match="Keypath required unless using CURRENT derivation"
|
||||
):
|
||||
export_key(
|
||||
card,
|
||||
derivation_option=DerivationOption.DERIVE,
|
||||
public_only=True,
|
||||
keypath=None,
|
||||
make_current=False,
|
||||
source=DerivationSource.CURRENT
|
||||
)
|
||||
|
||||
|
||||
def test_export_key_invalid_keypath_type(card):
|
||||
with pytest.raises(TypeError, match="Keypath must be a string or bytes"):
|
||||
export_key(
|
||||
card,
|
||||
derivation_option=DerivationOption.DERIVE,
|
||||
public_only=True,
|
||||
keypath=123,
|
||||
make_current=False,
|
||||
source=DerivationSource.CURRENT
|
||||
)
|
||||
|
||||
|
||||
def test_export_key_missing_keypair_template(card):
|
||||
card.send_secure_apdu.return_value = b'\xA0\x00'
|
||||
|
||||
with pytest.raises(ValueError, match="Missing keypair template"):
|
||||
export_key(
|
||||
card,
|
||||
derivation_option=DerivationOption.CURRENT,
|
||||
public_only=True,
|
||||
keypath=None,
|
||||
make_current=False,
|
||||
source=DerivationSource.MASTER
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
import pytest
|
||||
from unittest.mock import Mock
|
||||
from keycard import constants
|
||||
from keycard.commands.factory_reset import factory_reset
|
||||
from keycard.exceptions import APDUError
|
||||
|
||||
|
||||
def test_factory_reset_success(card):
|
||||
mock_response = Mock()
|
||||
mock_response.status_word = 0x9000
|
||||
card.send_apdu.return_value = mock_response
|
||||
|
||||
factory_reset(card)
|
||||
card.send_apdu.assert_called_once_with(
|
||||
ins=constants.INS_FACTORY_RESET,
|
||||
p1=0xAA,
|
||||
p2=0x55
|
||||
)
|
||||
|
||||
|
||||
def test_factory_reset_failure(card):
|
||||
card.send_apdu.side_effect = APDUError(0x6A80)
|
||||
|
||||
with pytest.raises(APDUError):
|
||||
factory_reset(card)
|
||||
@@ -0,0 +1,20 @@
|
||||
import pytest
|
||||
from keycard import constants
|
||||
from keycard.commands.generate_key import generate_key
|
||||
from keycard.exceptions import APDUError
|
||||
|
||||
|
||||
def test_generate_key_success(card):
|
||||
mock_id = b'\x01' * 32
|
||||
card.send_secure_apdu.return_value = mock_id
|
||||
result = generate_key(card)
|
||||
assert result == mock_id
|
||||
card.send_secure_apdu.assert_called_once_with(
|
||||
ins=constants.INS_GENERATE_KEY)
|
||||
|
||||
|
||||
def test_generate_key_apdu_error(card):
|
||||
card.send_secure_apdu.side_effect = APDUError(0x6A80)
|
||||
|
||||
with pytest.raises(APDUError):
|
||||
generate_key(card)
|
||||
@@ -0,0 +1,40 @@
|
||||
import pytest
|
||||
from keycard.constants import INS_GENERATE_MNEMONIC
|
||||
from keycard.commands.generate_mnemonic import generate_mnemonic
|
||||
|
||||
|
||||
def test_generate_mnemonic_valid(card):
|
||||
card.send_secure_apdu.return_value = bytes([
|
||||
0x00, 0x00,
|
||||
0x07, 0xFF,
|
||||
0x05, 0x39,
|
||||
0x00, 0x2A
|
||||
])
|
||||
|
||||
result = generate_mnemonic(card, checksum_size=6)
|
||||
|
||||
card.send_secure_apdu.assert_called_once_with(
|
||||
ins=INS_GENERATE_MNEMONIC,
|
||||
p1=6
|
||||
)
|
||||
|
||||
assert result == [0, 2047, 1337, 42]
|
||||
|
||||
|
||||
def test_generate_mnemonic_invalid_checksum(card):
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match="Checksum size must be between 4 and 8"
|
||||
):
|
||||
generate_mnemonic(card, checksum_size=2)
|
||||
|
||||
|
||||
def test_generate_mnemonic_odd_length_response(card):
|
||||
# Simulate invalid odd-length byte response
|
||||
card.send_secure_apdu.return_value = b'\x00\x01\x02'
|
||||
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match="Response must contain an even number of bytes"
|
||||
):
|
||||
generate_mnemonic(card, checksum_size=6)
|
||||
@@ -0,0 +1,31 @@
|
||||
import pytest
|
||||
from keycard.commands.get_data import get_data
|
||||
from keycard import constants
|
||||
|
||||
|
||||
def test_get_data_secure_channel(card):
|
||||
card.is_secure_channel_open = True
|
||||
card.send_secure_apdu.return_value = b"secure_data"
|
||||
result = get_data(card, slot=constants.StorageSlot.PUBLIC)
|
||||
card.send_secure_apdu.assert_called_once_with(
|
||||
ins=constants.INS_GET_DATA,
|
||||
p1=constants.StorageSlot.PUBLIC,
|
||||
)
|
||||
assert result == card.send_secure_apdu.return_value
|
||||
|
||||
|
||||
def test_get_data_proprietary_channel(card):
|
||||
card.is_secure_channel_open = False
|
||||
card.send_apdu.return_value = b"proprietary_data"
|
||||
result = get_data(card, slot=constants.StorageSlot.NDEF)
|
||||
card.send_apdu.assert_called_once_with(
|
||||
ins=constants.INS_GET_DATA,
|
||||
p1=constants.StorageSlot.NDEF.value,
|
||||
cla=constants.CLA_PROPRIETARY
|
||||
)
|
||||
assert result == card.send_apdu.return_value
|
||||
|
||||
|
||||
def test_get_data_invalid_slot(card):
|
||||
with pytest.raises(AttributeError):
|
||||
get_data(card, slot="INVALID_SLOT")
|
||||
@@ -0,0 +1,24 @@
|
||||
from keycard.commands.get_status import get_status
|
||||
|
||||
|
||||
def test_get_application_status(card):
|
||||
card.send_secure_apdu.return_value = bytes.fromhex(
|
||||
'A309020103020102010101')
|
||||
|
||||
result = get_status(card)
|
||||
|
||||
assert result['pin_retry_count'] == 3
|
||||
assert result['puk_retry_count'] == 2
|
||||
assert result['initialized'] is True
|
||||
|
||||
|
||||
def test_get_key_path_status(card):
|
||||
key_path = [0x8000002C, 0x8000003C]
|
||||
|
||||
card.send_secure_apdu.return_value = b''.join(
|
||||
i.to_bytes(4, 'big') for i in key_path
|
||||
)
|
||||
|
||||
result = get_status(card, key_path=True)
|
||||
|
||||
assert result == key_path
|
||||
@@ -0,0 +1,85 @@
|
||||
import sys
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
from keycard.commands.init import init
|
||||
from keycard.exceptions import APDUError
|
||||
from keycard import constants
|
||||
|
||||
|
||||
PIN = b'1234'
|
||||
PUK = b'5678'
|
||||
PAIRING_SECRET = b'abcdefgh'
|
||||
CARD_PUBLIC_KEY = b'\x04' + b'\x00' * 64 # Valid uncompressed pubkey format
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ecc_patches():
|
||||
init_module = sys.modules['keycard.commands.init']
|
||||
with (
|
||||
patch.object(init_module, 'urandom', return_value=b'\x00' * 16),
|
||||
patch.object(
|
||||
init_module,
|
||||
'aes_cbc_encrypt',
|
||||
side_effect=lambda k, iv,
|
||||
pt: b'\xAA' * len(pt)
|
||||
),
|
||||
patch.object(init_module, 'SigningKey') as mock_signing_key_cls,
|
||||
patch.object(init_module, 'VerifyingKey') as mock_verifying_key_cls,
|
||||
patch.object(init_module, 'ECDH') as mock_ecdh_cls,
|
||||
):
|
||||
mock_gen = mock_signing_key_cls.generate
|
||||
fake_privkey = MagicMock()
|
||||
fake_privkey.verifying_key.to_string.return_value = b'\x01' * 65
|
||||
mock_gen.return_value = fake_privkey
|
||||
|
||||
mock_parse = mock_verifying_key_cls.from_string
|
||||
mock_parse.return_value = 'parsed-pubkey'
|
||||
|
||||
ecdh_instance = MagicMock()
|
||||
ecdh_instance.generate_sharedsecret_bytes.return_value = b'\xBB' * 32
|
||||
mock_ecdh_cls.return_value = ecdh_instance
|
||||
|
||||
yield
|
||||
|
||||
|
||||
def test_init_success(card, ecc_patches):
|
||||
card.send_apdu.return_value = b''
|
||||
card.card_public_key = CARD_PUBLIC_KEY
|
||||
|
||||
init(card, PIN, PUK, PAIRING_SECRET)
|
||||
|
||||
card.send_apdu.assert_called_once_with(
|
||||
ins=constants.INS_INIT,
|
||||
data=bytes.fromhex(
|
||||
'4101010101010101010101010101010101010101010101010101010'
|
||||
'1010101010101010101010101010101010101010101010101010101'
|
||||
'010101010101010101010100000000000000000000000000000000'
|
||||
'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa')
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('secret_length', [10, 240])
|
||||
def test_init_data_length(card, ecc_patches, secret_length):
|
||||
card.send_apdu.return_value = b''
|
||||
card.card_public_key = CARD_PUBLIC_KEY
|
||||
|
||||
pairing_secret = b'x' * secret_length
|
||||
plaintext = PIN + PUK + pairing_secret
|
||||
total_data_len = 1 + 65 + 16 + len(plaintext)
|
||||
|
||||
if total_data_len > 255:
|
||||
with pytest.raises(ValueError, match='Data too long'):
|
||||
init(card, PIN, PUK, pairing_secret)
|
||||
else:
|
||||
init(card, PIN, PUK, pairing_secret)
|
||||
assert card.send_apdu.call_count == 1
|
||||
|
||||
|
||||
def test_init_apdu_error(card, ecc_patches):
|
||||
card.send_apdu.side_effect = APDUError(0x6A84)
|
||||
card.card_public_key = CARD_PUBLIC_KEY
|
||||
|
||||
with pytest.raises(APDUError) as excinfo:
|
||||
init(card, PIN, PUK, PAIRING_SECRET)
|
||||
|
||||
assert excinfo.value.sw == 0x6A84
|
||||
@@ -0,0 +1,57 @@
|
||||
import pytest
|
||||
from keycard.parsing.keypath import KeyPath
|
||||
from keycard.constants import DerivationSource
|
||||
|
||||
|
||||
def test_keypath_from_string_master():
|
||||
path = KeyPath("m/44'/60'/0'/0/0")
|
||||
assert path.source == DerivationSource.MASTER
|
||||
assert path.data == bytes.fromhex(
|
||||
'8000002c8000003c800000000000000000000000')
|
||||
assert path.to_string() == "m/44'/60'/0'/0/0"
|
||||
|
||||
|
||||
def test_keypath_from_string_parent():
|
||||
path = KeyPath('../1/2/3')
|
||||
assert path.source == DerivationSource.PARENT
|
||||
assert path.to_string() == '../1/2/3'
|
||||
|
||||
|
||||
def test_keypath_from_string_current_default():
|
||||
path = KeyPath('1/2/3')
|
||||
assert path.source == DerivationSource.CURRENT
|
||||
assert path.to_string() == './1/2/3'
|
||||
|
||||
|
||||
def test_keypath_from_bytes():
|
||||
data = bytes.fromhex('8000002c00000001')
|
||||
path = KeyPath(data, source=DerivationSource.PARENT)
|
||||
assert path.source == DerivationSource.PARENT
|
||||
assert path.data == data
|
||||
assert path.to_string() == "../44'/1"
|
||||
|
||||
|
||||
def test_keypath_empty_string_raises():
|
||||
with pytest.raises(ValueError, match="Empty path"):
|
||||
KeyPath('')
|
||||
|
||||
|
||||
def test_keypath_invalid_component():
|
||||
with pytest.raises(ValueError, match="Invalid component: abc"):
|
||||
KeyPath('m/abc')
|
||||
|
||||
|
||||
def test_keypath_too_many_components():
|
||||
long_path = 'm/' + '/'.join('0' for _ in range(11))
|
||||
with pytest.raises(ValueError, match="Too many components"):
|
||||
KeyPath(long_path)
|
||||
|
||||
|
||||
def test_keypath_invalid_byte_length():
|
||||
with pytest.raises(ValueError, match="Byte path must be a multiple of 4"):
|
||||
KeyPath(b'\x00\x01')
|
||||
|
||||
|
||||
def test_keypath_invalid_type():
|
||||
with pytest.raises(TypeError, match="Path must be a string or bytes"):
|
||||
KeyPath(123)
|
||||
@@ -0,0 +1,89 @@
|
||||
import pytest
|
||||
from keycard.commands.load_key import load_key
|
||||
from keycard import constants
|
||||
from hashlib import sha256
|
||||
from keycard.parsing import tlv
|
||||
|
||||
|
||||
def test_load_key_bip39(card):
|
||||
seed = b"\xAA" * 64
|
||||
fake_uid = b"\xBB" * 32
|
||||
card.send_secure_apdu.return_value = fake_uid
|
||||
|
||||
result = load_key(
|
||||
card,
|
||||
key_type=constants.LoadKeyType.BIP39_SEED,
|
||||
bip39_seed=seed
|
||||
)
|
||||
|
||||
card.send_secure_apdu.assert_called_once_with(
|
||||
ins=constants.INS_LOAD_KEY,
|
||||
p1=constants.LoadKeyType.BIP39_SEED,
|
||||
p2=0,
|
||||
data=seed
|
||||
)
|
||||
assert result == fake_uid
|
||||
|
||||
|
||||
def test_load_key_pair(card):
|
||||
public_key = b'\x04' + b'\x01' * 64
|
||||
private_key = b'\x02' * 32
|
||||
uid = sha256(public_key).digest()
|
||||
card.send_secure_apdu.return_value = uid
|
||||
|
||||
encoded = tlv.encode_tlv(
|
||||
0xA1,
|
||||
tlv.encode_tlv(0x80, public_key) +
|
||||
tlv.encode_tlv(0x81, private_key)
|
||||
)
|
||||
|
||||
result = load_key(
|
||||
card,
|
||||
key_type=constants.LoadKeyType.ECC,
|
||||
public_key=public_key,
|
||||
private_key=private_key
|
||||
)
|
||||
|
||||
card.send_secure_apdu.assert_called_once_with(
|
||||
ins=constants.INS_LOAD_KEY,
|
||||
p1=constants.LoadKeyType.ECC,
|
||||
p2=0,
|
||||
data=encoded
|
||||
)
|
||||
assert result == uid
|
||||
|
||||
|
||||
def test_bip39_seed_too_short(card):
|
||||
with pytest.raises(ValueError, match="BIP39/LEE seed must be 16-64 bytes"):
|
||||
load_key(
|
||||
card,
|
||||
key_type=constants.LoadKeyType.BIP39_SEED,
|
||||
bip39_seed=b"\xAA" * 8
|
||||
)
|
||||
|
||||
|
||||
def test_bip39_seed_missing(card):
|
||||
with pytest.raises(ValueError, match="Either bip39_seed or lee_seed must be provided for key_type = BIP39_SEED"):
|
||||
load_key(
|
||||
card,
|
||||
key_type=constants.LoadKeyType.BIP39_SEED
|
||||
)
|
||||
|
||||
|
||||
def test_ecc_missing_private_key(card):
|
||||
with pytest.raises(ValueError, match="Private key.*required"):
|
||||
load_key(
|
||||
card,
|
||||
key_type=constants.LoadKeyType.ECC,
|
||||
public_key=b"\x04" + b"\x01" * 64
|
||||
)
|
||||
|
||||
|
||||
def test_extended_ecc_missing_private_key(card):
|
||||
with pytest.raises(ValueError, match="Private key.*required"):
|
||||
load_key(
|
||||
card,
|
||||
key_type=constants.LoadKeyType.EXTENDED_ECC,
|
||||
public_key=b"\x04" + b"\x02" * 64,
|
||||
chain_code=b"\x00" * 32
|
||||
)
|
||||
@@ -0,0 +1,48 @@
|
||||
import pytest
|
||||
|
||||
from keycard import constants
|
||||
from keycard.commands.mutually_authenticate import mutually_authenticate
|
||||
from keycard.exceptions import APDUError
|
||||
|
||||
|
||||
def test_mutually_authenticate_success(card):
|
||||
client_challenge = bytes(32)
|
||||
card.send_secure_apdu.return_value = bytes(32)
|
||||
|
||||
mutually_authenticate(card, client_challenge)
|
||||
|
||||
card.send_secure_apdu.assert_called_once_with(
|
||||
ins=constants.INS_MUTUALLY_AUTHENTICATE,
|
||||
data=client_challenge
|
||||
)
|
||||
|
||||
|
||||
def test_mutually_authenticate_invalid_status_word(card):
|
||||
card.send_secure_apdu.side_effect = APDUError(0x6F00)
|
||||
|
||||
with pytest.raises(APDUError, match='APDU failed with SW=6F00'):
|
||||
mutually_authenticate(card, bytes(32))
|
||||
|
||||
|
||||
def test_mutually_authenticate_invalid_response_length(card):
|
||||
client_challenge = b'\xAA' * 32
|
||||
response = b'\xBB' * 16 # Invalid length
|
||||
|
||||
card.send_secure_apdu.return_value = response
|
||||
|
||||
with pytest.raises(
|
||||
ValueError,
|
||||
match='Response to MUTUALLY AUTHENTICATE is not 32 bytes'
|
||||
):
|
||||
mutually_authenticate(card, client_challenge)
|
||||
|
||||
|
||||
def test_mutually_authenticate_auto_challenge(card, monkeypatch):
|
||||
fake_challenge = b'\xCC' * 32
|
||||
monkeypatch.setattr('os.urandom', lambda n: fake_challenge)
|
||||
|
||||
card.send_secure_apdu.return_value = fake_challenge
|
||||
|
||||
mutually_authenticate(card)
|
||||
|
||||
card.send_secure_apdu.assert_called_once()
|
||||
@@ -0,0 +1,109 @@
|
||||
import sys
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch
|
||||
from ecdsa import SECP256k1
|
||||
|
||||
from keycard.card_interface import CardInterface
|
||||
from keycard.commands.open_secure_channel import open_secure_channel
|
||||
from keycard.exceptions import APDUError
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_ecdsa():
|
||||
open_secure_channel_module = sys.modules[
|
||||
'keycard.commands.open_secure_channel'
|
||||
]
|
||||
with (
|
||||
patch.object(
|
||||
open_secure_channel_module,
|
||||
'SecureChannel'
|
||||
) as mock_secure_channel,
|
||||
patch.object(
|
||||
open_secure_channel_module,
|
||||
'VerifyingKey'
|
||||
) as mock_verifying_key,
|
||||
patch.object(
|
||||
open_secure_channel_module,
|
||||
'ECDH'
|
||||
) as mock_ecdh,
|
||||
patch.object(
|
||||
open_secure_channel_module,
|
||||
'SigningKey'
|
||||
) as mock_signing_key,
|
||||
):
|
||||
yield {
|
||||
'secure_channel': mock_secure_channel,
|
||||
'verifying_key': mock_verifying_key,
|
||||
'ecdh': mock_ecdh,
|
||||
'signing_key': mock_signing_key,
|
||||
}
|
||||
|
||||
|
||||
def test_open_secure_channel_success(mock_ecdsa):
|
||||
mock_verifying_key = mock_ecdsa['verifying_key']
|
||||
mock_ecdh = mock_ecdsa['ecdh']
|
||||
mock_signing_key = mock_ecdsa['signing_key']
|
||||
mock_secure_channel = mock_ecdsa['secure_channel']
|
||||
|
||||
pairing_index = 1
|
||||
pairing_key = b'pairing_key'
|
||||
card = MagicMock(spec=CardInterface)
|
||||
card.card_public_key = b'\x04' + b'\x01' * 64
|
||||
|
||||
salt = b'A' * 32
|
||||
seed_iv = b'B' * 16
|
||||
response_data = salt + seed_iv
|
||||
card.send_apdu.return_value = response_data
|
||||
|
||||
# Mock SigningKey.generate
|
||||
mock_signing_key_instance = MagicMock()
|
||||
mock_signing_key_instance.verifying_key.to_string.return_value = \
|
||||
b'\x04' + b'\x02' * 64
|
||||
mock_signing_key.generate.return_value = mock_signing_key_instance
|
||||
|
||||
mock_verifying_key.from_string.return_value = MagicMock()
|
||||
mock_ecdh_instance = MagicMock()
|
||||
mock_ecdh.return_value = mock_ecdh_instance
|
||||
mock_ecdh_instance.generate_sharedsecret_bytes.return_value = (
|
||||
b'shared_secret'
|
||||
)
|
||||
mock_secure_channel.open.return_value = 'secure_session'
|
||||
|
||||
result = open_secure_channel(
|
||||
card,
|
||||
pairing_index,
|
||||
pairing_key
|
||||
)
|
||||
|
||||
card.send_apdu.assert_called_once()
|
||||
mock_verifying_key.from_string.assert_called_once_with(
|
||||
card.card_public_key, curve=SECP256k1
|
||||
)
|
||||
mock_ecdh.assert_called_once()
|
||||
mock_ecdh_instance.generate_sharedsecret_bytes.assert_called_once()
|
||||
mock_secure_channel.open.assert_called_once_with(
|
||||
b'shared_secret', pairing_key, salt, seed_iv
|
||||
)
|
||||
assert result == 'secure_session'
|
||||
|
||||
|
||||
def test_open_secure_channel_raises_apdu_error(card, mock_ecdsa):
|
||||
mock_signing_key = mock_ecdsa['signing_key']
|
||||
|
||||
# Mock SigningKey.generate
|
||||
mock_signing_key_instance = MagicMock()
|
||||
mock_signing_key_instance.verifying_key.to_string.return_value = \
|
||||
b'\x04' + b'\x02' * 64
|
||||
mock_signing_key.generate.return_value = mock_signing_key_instance
|
||||
|
||||
pairing_index = 1
|
||||
pairing_key = b'pairing_key'
|
||||
card.card_public_key = b'\x04' + b'\x01' * 64
|
||||
card.send_apdu.side_effect = APDUError(0x6A80)
|
||||
|
||||
with pytest.raises(APDUError):
|
||||
open_secure_channel(
|
||||
card,
|
||||
pairing_index,
|
||||
pairing_key
|
||||
)
|
||||
@@ -0,0 +1,106 @@
|
||||
import sys
|
||||
import pytest
|
||||
import hashlib
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from keycard.constants import INS_PAIR, PairingMode
|
||||
from keycard.commands.pair import pair
|
||||
from keycard.exceptions import APDUError, InvalidResponseError
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_urandom():
|
||||
pair_module = sys.modules['keycard.commands.pair']
|
||||
with patch.object(pair_module, 'urandom', return_value=b'\x01' * 32):
|
||||
yield
|
||||
|
||||
|
||||
def test_pair_success(card, mock_urandom):
|
||||
shared_secret = b'\xAA' * 32
|
||||
client_challenge = b'\x01' * 32
|
||||
card_challenge = b'\x02' * 32
|
||||
expected_card_cryptogram = hashlib.sha256(
|
||||
shared_secret + client_challenge).digest()
|
||||
expected_client_cryptogram = hashlib.sha256(
|
||||
shared_secret + card_challenge).digest()
|
||||
|
||||
first_response = expected_card_cryptogram + card_challenge
|
||||
second_response = b'\x05' + card_challenge
|
||||
|
||||
card.send_apdu.side_effect = [first_response, second_response]
|
||||
|
||||
result = pair(card, shared_secret)
|
||||
|
||||
assert result == (5, expected_client_cryptogram)
|
||||
assert card.send_apdu.call_count == 2
|
||||
|
||||
|
||||
def test_pairing_mode(card, mock_urandom):
|
||||
shared_secret = b'\xAA' * 32
|
||||
client_challenge = b'\x01' * 32
|
||||
card_challenge = b'\x02' * 32
|
||||
expected_card_cryptogram = hashlib.sha256(
|
||||
shared_secret + client_challenge).digest()
|
||||
first_response = expected_card_cryptogram + card_challenge
|
||||
second_response = b'\x05' + card_challenge
|
||||
|
||||
card.send_apdu.side_effect = [first_response, second_response]
|
||||
|
||||
pair(card, shared_secret, PairingMode.EPHEMERAL)
|
||||
card.send_apdu.assert_any_call(
|
||||
ins=INS_PAIR,
|
||||
p2=PairingMode.EPHEMERAL,
|
||||
data=client_challenge
|
||||
)
|
||||
|
||||
|
||||
def test_pair_invalid_shared_secret(card, mock_urandom):
|
||||
with pytest.raises(ValueError, match='Shared secret must be 32 bytes'):
|
||||
pair(card, b'short')
|
||||
|
||||
|
||||
def test_pair_apdu_error_on_first(card, mock_urandom):
|
||||
card.send_apdu.side_effect = APDUError(0x6A82)
|
||||
|
||||
with pytest.raises(APDUError):
|
||||
pair(card, b'\x00' * 32)
|
||||
|
||||
|
||||
def test_pair_invalid_response_length_first(card, mock_urandom):
|
||||
card.send_apdu.return_value = bytes(10)
|
||||
|
||||
with pytest.raises(
|
||||
InvalidResponseError,
|
||||
match='Unexpected response length'
|
||||
):
|
||||
pair(card, b'\x00' * 32)
|
||||
|
||||
|
||||
def test_pair_cryptogram_mismatch(card, mock_urandom):
|
||||
wrong_card_cryptogram = b'\xAB' * 32
|
||||
card_challenge = b'\x02' * 32
|
||||
response = wrong_card_cryptogram + card_challenge
|
||||
|
||||
card.send_apdu.side_effect = [response]
|
||||
|
||||
with pytest.raises(InvalidResponseError, match='Card cryptogram mismatch'):
|
||||
pair(card, b'\xAA' * 32)
|
||||
|
||||
|
||||
def test_pair_invalid_response_second_apdu(card, mock_urandom):
|
||||
shared_secret = b'\xAA' * 32
|
||||
client_challenge = b'\x01' * 32
|
||||
card_challenge = b'\x02' * 32
|
||||
card_cryptogram = hashlib.sha256(shared_secret + client_challenge).digest()
|
||||
|
||||
first_response = card_cryptogram + card_challenge
|
||||
second_response = b'\x00' * 10
|
||||
|
||||
card.send_apdu.side_effect = [first_response, second_response]
|
||||
|
||||
with pytest.raises(
|
||||
InvalidResponseError,
|
||||
match='Unexpected response length'
|
||||
):
|
||||
pair(card, shared_secret)
|
||||
@@ -0,0 +1,11 @@
|
||||
from keycard.commands.remove_key import remove_key
|
||||
|
||||
|
||||
def test_remove_key_calls_send_secure_apdu_with_correct_ins(card):
|
||||
remove_key(card)
|
||||
card.send_secure_apdu.assert_called_once_with(ins=0xD3)
|
||||
|
||||
|
||||
def test_remove_key_returns_none(card):
|
||||
result = remove_key(card)
|
||||
assert result is None
|
||||
@@ -0,0 +1,41 @@
|
||||
import sys
|
||||
import pytest
|
||||
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
from keycard.commands.select import select
|
||||
from keycard.exceptions import APDUError
|
||||
from keycard import constants
|
||||
|
||||
|
||||
def test_select_success():
|
||||
select_module = sys.modules['keycard.commands.select']
|
||||
dummy_info = MagicMock()
|
||||
response_data = b'\x01\x02\x03\x04'
|
||||
|
||||
card = MagicMock()
|
||||
card.send_apdu.return_value = response_data
|
||||
|
||||
with patch.object(select_module, 'ApplicationInfo') as mock_app_info:
|
||||
mock_app_info.parse.return_value = dummy_info
|
||||
result = select(card)
|
||||
|
||||
card.send_apdu.assert_called_once_with(
|
||||
cla=constants.CLAISO7816,
|
||||
ins=constants.INS_SELECT,
|
||||
p1=0x04,
|
||||
p2=0x00,
|
||||
data=constants.KEYCARD_AID
|
||||
)
|
||||
mock_app_info.parse.assert_called_once_with(response_data)
|
||||
assert result == dummy_info
|
||||
|
||||
|
||||
def test_select_apdu_error():
|
||||
card = MagicMock()
|
||||
card.send_apdu.side_effect = APDUError(0x6A82)
|
||||
|
||||
with pytest.raises(APDUError) as excinfo:
|
||||
select(card)
|
||||
|
||||
assert excinfo.value.sw == 0x6A82
|
||||
@@ -0,0 +1,24 @@
|
||||
from keycard.commands.set_pinless_path import set_pinless_path
|
||||
from keycard.constants import INS_SET_PINLESS_PATH
|
||||
from keycard.parsing.keypath import KeyPath
|
||||
|
||||
|
||||
def test_set_pinless_path(card):
|
||||
path = "m/44'/60'/0'/0/0"
|
||||
expected_data = KeyPath(path).data
|
||||
|
||||
set_pinless_path(card, path)
|
||||
|
||||
card.send_secure_apdu.assert_called_once_with(
|
||||
ins=INS_SET_PINLESS_PATH,
|
||||
data=expected_data
|
||||
)
|
||||
|
||||
|
||||
def test_set_pinless_path_empty(card):
|
||||
set_pinless_path(card, "")
|
||||
|
||||
card.send_secure_apdu.assert_called_once_with(
|
||||
ins=INS_SET_PINLESS_PATH,
|
||||
data=b""
|
||||
)
|
||||
@@ -0,0 +1,101 @@
|
||||
import sys
|
||||
import pytest
|
||||
|
||||
from unittest import mock
|
||||
|
||||
from keycard.commands.sign import sign
|
||||
from keycard import constants
|
||||
from keycard.exceptions import InvalidStateError
|
||||
from keycard.parsing.keypath import KeyPath
|
||||
|
||||
|
||||
def test_sign_current_key(card):
|
||||
sign_module = sys.modules['keycard.commands.sign']
|
||||
digest = b'\xAA' * 32
|
||||
raw = b'\x01' * 64 + b'\x1f'
|
||||
encoded = b'\x80' + bytes([len(raw)]) + raw
|
||||
card.send_secure_apdu.return_value = encoded
|
||||
with mock.patch.object(sign_module, "SignatureResult"):
|
||||
sign(card, digest)
|
||||
|
||||
card.send_secure_apdu.assert_called_once_with(
|
||||
ins=constants.INS_SIGN,
|
||||
p1=constants.DerivationOption.CURRENT,
|
||||
p2=constants.SigningAlgorithm.ECDSA_SECP256K1,
|
||||
data=digest,
|
||||
)
|
||||
|
||||
|
||||
def test_sign_with_derivation_path(card):
|
||||
sign_module = sys.modules['keycard.commands.sign']
|
||||
digest = bytes(32)
|
||||
raw = bytes(65)
|
||||
encoded = b'\x80' + bytes([len(raw)]) + raw
|
||||
card.send_secure_apdu.return_value = encoded
|
||||
key_path = KeyPath("m/44'/60'/0'/0/0")
|
||||
expected_data = digest + key_path.data
|
||||
|
||||
with mock.patch.object(sign_module, "SignatureResult"):
|
||||
sign(
|
||||
card,
|
||||
digest,
|
||||
p1=constants.DerivationOption.DERIVE,
|
||||
derivation_path=key_path.to_string()
|
||||
)
|
||||
|
||||
card.send_secure_apdu.assert_called_once_with(
|
||||
ins=constants.INS_SIGN,
|
||||
p1=constants.DerivationOption.DERIVE,
|
||||
p2=constants.SigningAlgorithm.ECDSA_SECP256K1,
|
||||
data=expected_data,
|
||||
)
|
||||
|
||||
|
||||
def test_sign_requires_pin(card):
|
||||
card.is_pin_verified = False
|
||||
digest = b'\xCC' * 32
|
||||
|
||||
with pytest.raises(
|
||||
InvalidStateError,
|
||||
match="PIN must be verified to sign with this derivation option"
|
||||
):
|
||||
sign(card, digest)
|
||||
|
||||
|
||||
def test_sign_short_digest(card):
|
||||
short_digest = b'\xDD' * 10
|
||||
|
||||
with pytest.raises(ValueError, match="Digest must be exactly 32 bytes"):
|
||||
sign(card, short_digest)
|
||||
|
||||
|
||||
def test_sign_missing_path(card):
|
||||
digest = b'\xEE' * 32
|
||||
|
||||
with pytest.raises(ValueError, match="Derivation path cannot be empty"):
|
||||
sign(
|
||||
card,
|
||||
digest,
|
||||
p1=constants.DerivationOption.DERIVE,
|
||||
derivation_path=None
|
||||
)
|
||||
|
||||
|
||||
def test_sign_not_implemented_algo(card):
|
||||
digest = b'\xAB' * 32
|
||||
|
||||
with pytest.raises(
|
||||
NotImplementedError,
|
||||
match="Signature algorithm 255 not supported"
|
||||
):
|
||||
sign(card, digest, p2=0xFF)
|
||||
|
||||
|
||||
def test_sign_raw_signature_wrong_length(card):
|
||||
digest = b'\xCC' * 32
|
||||
raw = b'\x01' * 64 # Should be 65 bytes
|
||||
encoded = b'\x80' + bytes([len(raw)]) + raw
|
||||
card.send_secure_apdu.return_value = encoded
|
||||
card.is_pin_verified = True
|
||||
with pytest.raises(ValueError, match="Expected 65-byte raw signature"):
|
||||
sign(card, digest)
|
||||
@@ -0,0 +1,29 @@
|
||||
import pytest
|
||||
|
||||
from keycard.commands import store_data
|
||||
from keycard import constants
|
||||
|
||||
|
||||
def test_store_data_calls_send_secure_apdu_with_correct_args(card):
|
||||
store_data(card, b"hello", constants.StorageSlot.PUBLIC)
|
||||
|
||||
card.send_secure_apdu.assert_called_once_with(
|
||||
ins=constants.INS_STORE_DATA,
|
||||
p1=constants.StorageSlot.PUBLIC.value,
|
||||
data=b'hello'
|
||||
)
|
||||
|
||||
|
||||
def test_store_data_uses_default_slot(card):
|
||||
store_data(card, b'world')
|
||||
|
||||
card.send_secure_apdu.assert_called_once_with(
|
||||
ins=constants.INS_STORE_DATA,
|
||||
p1=constants.StorageSlot.PUBLIC,
|
||||
data=b'world'
|
||||
)
|
||||
|
||||
|
||||
def test_store_data_raises_value_error_on_too_long_data(card):
|
||||
with pytest.raises(ValueError, match="Data too long"):
|
||||
store_data(card, b'a' * 128, constants.StorageSlot.PUBLIC)
|
||||
@@ -0,0 +1,43 @@
|
||||
import pytest
|
||||
from keycard.commands.unblock_pin import unblock_pin
|
||||
from keycard import constants
|
||||
|
||||
|
||||
def test_unblock_pin_with_valid_str(card):
|
||||
puk = '123456789012'
|
||||
pin = '123456'
|
||||
unblock_pin(card, puk + pin)
|
||||
card.send_secure_apdu.assert_called_once_with(
|
||||
ins=constants.INS_UNBLOCK_PIN,
|
||||
data=(puk + pin).encode('utf-8')
|
||||
)
|
||||
|
||||
|
||||
def test_unblock_pin_with_valid_bytes(card):
|
||||
data = b'123456789012123456'
|
||||
unblock_pin(card, data)
|
||||
card.send_secure_apdu.assert_called_once_with(
|
||||
ins=constants.INS_UNBLOCK_PIN,
|
||||
data=data
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('bad_input', [
|
||||
'12345678901212345', # Too short
|
||||
'1234567890121234567', # Too long
|
||||
b'12345678901212345', # Too short (bytes)
|
||||
b'1234567890121234567', # Too long (bytes)
|
||||
])
|
||||
def test_unblock_pin_invalid_length(card, bad_input):
|
||||
with pytest.raises(ValueError, match='exactly 18 digits'):
|
||||
unblock_pin(card, bad_input)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('bad_input', [
|
||||
'12345678901A123456', # Non-digit in PUK
|
||||
'12345678901212345A', # Non-digit in PIN
|
||||
'ABCDEFGHIJKL123456', # All non-digits in PUK
|
||||
])
|
||||
def test_unblock_pin_invalid_digits(card, bad_input):
|
||||
with pytest.raises(ValueError, match='must be numeric digits'):
|
||||
unblock_pin(card, bad_input)
|
||||
@@ -0,0 +1,25 @@
|
||||
import pytest
|
||||
from keycard.commands.unpair import unpair
|
||||
from keycard.apdu import APDUResponse
|
||||
from keycard.exceptions import APDUError
|
||||
from keycard import constants
|
||||
|
||||
|
||||
def test_unpair_success(card):
|
||||
card.send_secure_apdu.return_value = APDUResponse(b'', 0x9000)
|
||||
|
||||
unpair(card, 1)
|
||||
|
||||
card.send_secure_apdu.assert_called_once_with(
|
||||
ins=constants.INS_UNPAIR,
|
||||
p1=0x01,
|
||||
)
|
||||
|
||||
|
||||
def test_unpair_apdu_error(card):
|
||||
card.send_secure_apdu.side_effect = APDUError(0x6A84)
|
||||
|
||||
with pytest.raises(APDUError) as excinfo:
|
||||
unpair(card, 1)
|
||||
|
||||
assert excinfo.value.sw == 0x6A84
|
||||
@@ -0,0 +1,29 @@
|
||||
import pytest
|
||||
from keycard.commands.verify_pin import verify_pin
|
||||
from keycard.exceptions import APDUError
|
||||
from keycard import constants
|
||||
|
||||
|
||||
def test_verify_pin_success(card):
|
||||
assert verify_pin(card, '1234') is True
|
||||
card.send_secure_apdu.assert_called_once_with(
|
||||
ins=constants.INS_VERIFY_PIN,
|
||||
data=b'1234'
|
||||
)
|
||||
|
||||
|
||||
def test_verify_pin_incorrect_but_allowed(card):
|
||||
card.send_secure_apdu.side_effect = APDUError(0x63C2)
|
||||
assert verify_pin(card, '0000') is False
|
||||
|
||||
|
||||
def test_verify_pin_blocked(card):
|
||||
card.send_secure_apdu.side_effect = APDUError(0x63C0)
|
||||
with pytest.raises(RuntimeError, match='PIN is blocked'):
|
||||
verify_pin(card, '0000')
|
||||
|
||||
|
||||
def test_verify_pin_other_apdu_error(card):
|
||||
card.send_secure_apdu.side_effect = APDUError(0x6A80)
|
||||
with pytest.raises(APDUError):
|
||||
verify_pin(card, '0000')
|
||||
Reference in New Issue
Block a user