diff --git a/.bandit b/.bandit index fc539098..6e4e7d26 100644 --- a/.bandit +++ b/.bandit @@ -1,2 +1,2 @@ [bandit] -exclude: /test,/.tox +exclude: /tests diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e790e023..3fc30a84 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,8 +11,9 @@ repos: rev: 1.6.2 hooks: - id: bandit - exclude: (^.tox/|^test/) # exclude .tox and test repo, keep in sync with .bandit file + exclude: ^test(s)?/ # keep in sync with .bandit file - repo: https://github.com/pre-commit/mirrors-mypy rev: v0.782 hooks: - id: mypy + exclude: ^tests/ # keep in sync with mypy.ini diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..2b5a5458 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,5 @@ +[mypy] +files = yubikit/,ykman/ + +[mypy-smartcard.*] +ignore_missing_imports = True diff --git a/pyproject.toml b/pyproject.toml index 89175d9c..879fc255 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ pywin32 = {version = ">=223", platform = "win32"} [tool.poetry.dev-dependencies] pytest = "^6.0" pyOpenSSL = "^17.0" +makefun = "^1.9.5" [tool.poetry.scripts] ykman = "ykman.cli.__main__:main" diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/on_yubikey/__init__.py b/tests/on_yubikey/__init__.py new file mode 100644 index 00000000..aef0c1e8 --- /dev/null +++ b/tests/on_yubikey/__init__.py @@ -0,0 +1,71 @@ +from yubikit.core import TRANSPORT +from ykman.device import is_fips_version +from inspect import signature, Parameter, isgeneratorfunction +from makefun import wraps + +import pytest + + +def condition(check, message="Condition not satisfied"): + def deco(func): + sig = signature(func) + if "info" not in sig.parameters: + params = [Parameter("info", kind=Parameter.POSITIONAL_OR_KEYWORD)] + params.extend(sig.parameters.values()) + sig = sig.replace(parameters=params) + + if isgeneratorfunction(func): + + def wrapper(info, *args, **kwargs): + if not check(info): + pytest.skip(message) + yield from func(*args, **kwargs) + + else: + + def wrapper(info, *args, **kwargs): + if not check(info): + pytest.skip(message) + return func(*args, **kwargs) + + return wraps(func, new_sig=sig)(wrapper) + + return deco + + +def register_condition(cond): + setattr(condition, cond.__name__, cond) + return cond + + +@register_condition +def capability(capability): + return condition( + lambda info: capability in info.config.enabled_capabilities[TRANSPORT.USB], + "Requires %s" % capability, + ) + + +@register_condition +def min_version(major, minor=0, micro=0): + if isinstance(major, tuple): + version = major + else: + version = (major, minor, micro) + return condition(lambda info: info.version >= version, "Version < %s" % (version,)) + + +@register_condition +def max_version(major, minor=0, micro=0): + if isinstance(major, tuple): + version = major + else: + version = (major, minor, micro) + return condition(lambda info: info.version <= version, "Version > %s" % (version,)) + + +@register_condition +def fips(status=True): + return condition( + lambda info: is_fips_version(info.version), "Requires FIPS = %s" % status + ) diff --git a/tests/on_yubikey/cli/__init__.py b/tests/on_yubikey/cli/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/on_yubikey/cli/conftest.py b/tests/on_yubikey/cli/conftest.py new file mode 100644 index 00000000..f1af7c20 --- /dev/null +++ b/tests/on_yubikey/cli/conftest.py @@ -0,0 +1,19 @@ +from ykman.cli.__main__ import cli +from ykman.cli.aliases import apply_aliases +from click.testing import CliRunner +from functools import partial +import pytest + + +@pytest.fixture(scope="module") +def ykman_cli(info): + return partial(ykman_cli_for_serial, info.serial) + + +def ykman_cli_for_serial(serial, *argv, **kwargs): + argv = apply_aliases(["ykman"] + [str(a) for a in argv]) + runner = CliRunner() + result = runner.invoke(cli, argv[1:], obj={}, **kwargs) + if result.exit_code != 0: + raise result.exception + return result diff --git a/tests/on_yubikey/cli/test_piv_cli.py b/tests/on_yubikey/cli/test_piv_cli.py new file mode 100644 index 00000000..1603cb41 --- /dev/null +++ b/tests/on_yubikey/cli/test_piv_cli.py @@ -0,0 +1,151 @@ +from yubikit.core import Tlv +from yubikit.piv import SLOT, OBJECT_ID +from yubikit.management import CAPABILITY +from cryptography.hazmat.primitives import serialization +from ...util import generate_self_signed_certificate +from .. import condition + +import pytest +import contextlib +import os +import io + + +DEFAULT_MANAGEMENT_KEY = "010203040506070801020304050607080102030405060708" + + +@pytest.fixture(autouse=True) +@condition.capability(CAPABILITY.PIV) +def ensure_piv(ykman_cli): + ykman_cli("piv", "reset", "-f") + + +def test_piv_info(ykman_cli): + ykman_cli("piv", "info") + + +def test_write_read_preserves_ansi_escapes(ykman_cli): + red = b"\x00\x1b[31m" + blue = b"\x00\x1b[34m" + reset = b"\x00\x1b[0m" + data = ( + b"Hello, " + + red + + b"red" + + reset + + b" and " + + blue + + b"blue" + + reset + + b" world!" + ) + ykman_cli( + "piv", + "objects", + "import", + "-m", + DEFAULT_MANAGEMENT_KEY, + "0x5f0001", + "-", + input=data, + ) + output_data = ykman_cli("piv", "objects", "export", "0x5f0001", "-").stdout_bytes + assert data == output_data + + +def test_read_write_read_is_noop(ykman_cli): + data = os.urandom(32) + + ykman_cli( + "piv", + "objects", + "import", + hex(OBJECT_ID.AUTHENTICATION), + "-", + "-m", + DEFAULT_MANAGEMENT_KEY, + input=data, + ) + + output1 = ykman_cli( + "piv", "objects", "export", hex(OBJECT_ID.AUTHENTICATION), "-" + ).stdout_bytes + assert output1 == data + + ykman_cli( + "piv", + "objects", + "import", + hex(OBJECT_ID.AUTHENTICATION), + "-", + "-m", + DEFAULT_MANAGEMENT_KEY, + input=output1, + ) + + output2 = ykman_cli( + "piv", "objects", "export", hex(OBJECT_ID.AUTHENTICATION), "-" + ).stdout_bytes + assert output2 == data + + +def test_read_write_aliases(ykman_cli): + data = os.urandom(32) + + with io.StringIO() as buf: + with contextlib.redirect_stderr(buf): + ykman_cli( + "piv", + "write-object", + hex(OBJECT_ID.AUTHENTICATION), + "-", + "-m", + DEFAULT_MANAGEMENT_KEY, + input=data, + ) + + output1 = ykman_cli( + "piv", "read-object", hex(OBJECT_ID.AUTHENTICATION), "-" + ).stdout_bytes + err = buf.getvalue() + assert output1 == data + assert "piv objects import" in err + assert "piv objects export" in err + + +def test_read_write_certificate_as_object(ykman_cli): + with pytest.raises(SystemExit): + ykman_cli("piv", "objects", "export", hex(OBJECT_ID.AUTHENTICATION), "-") + + cert = generate_self_signed_certificate() + cert_bytes_der = cert.public_bytes(encoding=serialization.Encoding.DER) + + input_tlv = Tlv(0x70, cert_bytes_der) + Tlv(0x71, b"\0") + Tlv(0xFE, b"") + + ykman_cli( + "piv", + "objects", + "import", + hex(OBJECT_ID.AUTHENTICATION), + "-", + "-m", + DEFAULT_MANAGEMENT_KEY, + input=input_tlv, + ) + + output1 = ykman_cli( + "piv", "objects", "export", hex(OBJECT_ID.AUTHENTICATION), "-" + ).stdout_bytes + output_cert_bytes = Tlv.parse_dict(output1)[0x70] + assert output_cert_bytes == cert_bytes_der + + output2 = ykman_cli( + "piv", + "certificates", + "export", + hex(SLOT.AUTHENTICATION), + "-", + "--format", + "DER", + ).stdout_bytes + assert output2 == cert_bytes_der diff --git a/tests/on_yubikey/conftest.py b/tests/on_yubikey/conftest.py new file mode 100644 index 00000000..d8ed1a67 --- /dev/null +++ b/tests/on_yubikey/conftest.py @@ -0,0 +1,52 @@ +from ykman.device import connect_to_device, list_all_devices +from yubikit.core.otp import OtpConnection +from yubikit.core.fido import FidoConnection +from yubikit.core.smartcard import SmartCardConnection + +import pytest +import os + + +@pytest.fixture(scope="session") +def _pid_info(): + devices = list_all_devices() + assert len(devices) == 1 + dev, info = devices[0] + assert info.serial == int(os.environ.get("DESTRUCTIVE_TEST_YUBIKEY_SERIALS")) + return dev.pid, info + + +@pytest.fixture(scope="session") +def pid(_pid_info): + return _pid_info[0] + + +@pytest.fixture(scope="session") +def info(_pid_info): + return _pid_info[1] + + +@pytest.fixture(scope="session") +def key_type(pid): + return pid.get_type() + + +connection_scope = os.environ.get("CONNECTION_SCOPE", "module") + + +@pytest.fixture(scope=connection_scope) +def otp_connection(info): + with connect_to_device(info.serial, [OtpConnection])[0] as c: + yield c + + +@pytest.fixture(scope=connection_scope) +def fido_connection(info): + with connect_to_device(info.serial, [FidoConnection])[0] as c: + yield c + + +@pytest.fixture(scope=connection_scope) +def ccid_connection(info): + with connect_to_device(info.serial, [SmartCardConnection])[0] as c: + yield c diff --git a/tests/on_yubikey/test_ccid.py b/tests/on_yubikey/test_ccid.py new file mode 100644 index 00000000..d43e4516 --- /dev/null +++ b/tests/on_yubikey/test_ccid.py @@ -0,0 +1,8 @@ +from yubikit.core.smartcard import SmartCardProtocol, ApplicationNotAvailableError +import pytest + + +def test_select_wrong_app(ccid_connection): + p = SmartCardProtocol(ccid_connection) + with pytest.raises(ApplicationNotAvailableError): + p.select(b"not_a_real_aid") diff --git a/tests/on_yubikey/test_interfaces.py b/tests/on_yubikey/test_interfaces.py new file mode 100644 index 00000000..82aab1b9 --- /dev/null +++ b/tests/on_yubikey/test_interfaces.py @@ -0,0 +1,28 @@ +from ykman.device import connect_to_device +from yubikit.core.otp import OtpConnection +from yubikit.core.fido import FidoConnection +from yubikit.core.smartcard import SmartCardConnection +from yubikit.management import USB_INTERFACE + + +def try_connection(conn_type): + with connect_to_device(None, [conn_type])[0]: + return True + + +def test_switch_interfaces(pid): + interfaces = pid.get_interfaces() + if USB_INTERFACE.FIDO in interfaces: + assert try_connection(FidoConnection) + if USB_INTERFACE.OTP in interfaces: + assert try_connection(OtpConnection) + if USB_INTERFACE.FIDO in interfaces: + assert try_connection(FidoConnection) + if USB_INTERFACE.CCID in interfaces: + assert try_connection(SmartCardConnection) + if USB_INTERFACE.OTP in interfaces: + assert try_connection(OtpConnection) + if USB_INTERFACE.CCID in interfaces: + assert try_connection(SmartCardConnection) + if USB_INTERFACE.FIDO in interfaces: + assert try_connection(FidoConnection) diff --git a/tests/on_yubikey/test_piv.py b/tests/on_yubikey/test_piv.py new file mode 100644 index 00000000..c4bcbee2 --- /dev/null +++ b/tests/on_yubikey/test_piv.py @@ -0,0 +1,495 @@ +import datetime +import random +import pytest + +from cryptography import x509 +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import ec, padding + +from yubikit.core import AID +from yubikit.core.smartcard import ApduError +from yubikit.management import CAPABILITY +from yubikit.piv import ( + PivSession, + KEY_TYPE, + PIN_POLICY, + TOUCH_POLICY, + SLOT, + MANAGEMENT_KEY_TYPE, + InvalidPinError, +) +from ykman.piv import ( + check_key, + get_pivman_data, + get_pivman_protected_data, + generate_self_signed_certificate, + generate_csr, + pivman_set_mgm_key, +) +from ykman.util import parse_certificates, parse_private_key +from ..util import open_file +from . import condition + + +DEFAULT_PIN = "123456" +NON_DEFAULT_PIN = "654321" +DEFAULT_PUK = "12345678" +NON_DEFAULT_PUK = "87654321" +DEFAULT_MANAGEMENT_KEY = bytes.fromhex( + "010203040506070801020304050607080102030405060708" +) # noqa: E501 +NON_DEFAULT_MANAGEMENT_KEY = bytes.fromhex( + "010103040506070801020304050607080102030405060708" +) # noqa: E501 + + +now = datetime.datetime.now + + +def get_test_cert(): + with open_file("rsa_2048_cert.pem") as f: + return parse_certificates(f.read(), None)[0] + + +def get_test_key(): + with open_file("rsa_2048_key.pem") as f: + return parse_private_key(f.read(), None) + + +@pytest.fixture +@condition.capability(CAPABILITY.PIV) +def session(ccid_connection): + piv = PivSession(ccid_connection) + piv.reset() + yield piv + reset_state(piv) + + +def reset_state(session): + session.protocol.connection.connection.disconnect() + session.protocol.connection.connection.connect() + session.protocol.select(AID.PIV) + + +def assert_mgm_key_is(session, key): + session.authenticate(MANAGEMENT_KEY_TYPE.TDES, key) + + +def assert_mgm_key_is_not(session, key): + with pytest.raises(ApduError): + session.authenticate(MANAGEMENT_KEY_TYPE.TDES, key) + + +def generate_key( + session, + slot=SLOT.AUTHENTICATION, + alg=KEY_TYPE.ECCP256, + pin_policy=PIN_POLICY.DEFAULT, +): + session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY) + key = session.generate_key(slot, alg, pin_policy=pin_policy) + reset_state(session) + return key + + +def test_delete_certificate_requires_authentication(session): + generate_key(session, SLOT.AUTHENTICATION) + + with pytest.raises(ApduError): + session.delete_certificate(SLOT.AUTHENTICATION) + + session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY) + session.delete_certificate(SLOT.AUTHENTICATION) + + +def test_generate_csr_works(session): + public_key = generate_key(session, SLOT.AUTHENTICATION) + + session.verify_pin(DEFAULT_PIN) + csr = generate_csr(session, SLOT.AUTHENTICATION, public_key, "alice") + + assert csr.public_key().public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) == public_key.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + + assert ( + csr.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)[0].value == "alice" + ) + + +def test_generate_self_signed_certificate_requires_pin(session): + session.verify_pin(DEFAULT_PIN) + public_key = generate_key(session, SLOT.AUTHENTICATION) + + with pytest.raises(ApduError): + generate_self_signed_certificate( + session, SLOT.AUTHENTICATION, public_key, "alice", now(), now() + ) + + session.verify_pin(DEFAULT_PIN) + generate_self_signed_certificate( + session, SLOT.AUTHENTICATION, public_key, "alice", now(), now() + ) + + +def _test_generate_self_signed_certificate(session, slot): + public_key = generate_key(session, slot) + session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY) + session.verify_pin(DEFAULT_PIN) + cert = generate_self_signed_certificate( + session, slot, public_key, "alice", now(), now() + ) + + assert cert.public_key().public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) == public_key.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + assert ( + cert.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)[0].value + == "alice" + ) + + +def test_generate_self_signed_certificate_slot_9a_works(session): + _test_generate_self_signed_certificate(session, SLOT.AUTHENTICATION) + + +def test_generate_self_signed_certificate_slot_9c_works(session): + _test_generate_self_signed_certificate(session, SLOT.SIGNATURE) + + +def test_generate_key_requires_authentication(session): + with pytest.raises(ApduError): + session.generate_key( + SLOT.AUTHENTICATION, KEY_TYPE.ECCP256, touch_policy=TOUCH_POLICY.DEFAULT + ) + + session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY) + session.generate_key( + SLOT.AUTHENTICATION, KEY_TYPE.ECCP256, touch_policy=TOUCH_POLICY.DEFAULT + ) + + +def test_put_certificate_requires_authentication(session): + cert = get_test_cert() + with pytest.raises(ApduError): + session.put_certificate(SLOT.AUTHENTICATION, cert) + + session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY) + session.put_certificate(SLOT.AUTHENTICATION, cert) + + +def _test_put_key_pairing(session, alg1, alg2): + # Set up a key in the slot and create a certificate for it + public_key = generate_key(session, SLOT.AUTHENTICATION, alg=alg1) + session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY) + session.verify_pin(DEFAULT_PIN) + cert = generate_self_signed_certificate( + session, + SLOT.AUTHENTICATION, + public_key, + "test", + datetime.datetime.now(), + datetime.datetime.now(), + ) + session.put_certificate(SLOT.AUTHENTICATION, cert) + assert check_key(session, SLOT.AUTHENTICATION, cert.public_key()) + + cert2 = session.get_certificate(SLOT.AUTHENTICATION) + assert cert == cert2 + + session.delete_certificate(SLOT.AUTHENTICATION) + + # Overwrite the key with one of the same type + generate_key(session, SLOT.AUTHENTICATION, alg=alg1) + session.verify_pin(DEFAULT_PIN) + assert not check_key(session, SLOT.AUTHENTICATION, cert.public_key()) + + # Overwrite the key with one of a different type + generate_key(session, SLOT.AUTHENTICATION, alg=alg2) + session.verify_pin(DEFAULT_PIN) + assert not check_key(session, SLOT.AUTHENTICATION, cert.public_key()) + + +def not_roca(info): + return not ((4, 2, 0) <= info.version < (4, 3, 5)) + + +@condition(not_roca) +def test_put_certificate_verifies_key_pairing_rsa1024(session): + _test_put_key_pairing(session, KEY_TYPE.RSA1024, KEY_TYPE.ECCP256) + + +@condition(not_roca) +def test_put_certificate_verifies_key_pairing_rsa2048(session): + _test_put_key_pairing(session, KEY_TYPE.RSA2048, KEY_TYPE.ECCP256) + + +@condition(not_roca) +def test_put_certificate_verifies_key_pairing_eccp256_a(session): + _test_put_key_pairing(session, KEY_TYPE.ECCP256, KEY_TYPE.RSA2048) + + +@condition.min_version(4) +def test_put_certificate_verifies_key_pairing_eccp256_b(session): + _test_put_key_pairing(session, KEY_TYPE.ECCP256, KEY_TYPE.ECCP384) + + +@condition.min_version(4) +def test_put_certificate_verifies_key_pairing_eccp384(session): + _test_put_key_pairing(session, KEY_TYPE.ECCP384, KEY_TYPE.ECCP256) + + +def test_put_key_requires_authentication(session): + private_key = get_test_key() + with pytest.raises(ApduError): + session.put_key(SLOT.AUTHENTICATION, private_key) + + session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY) + session.put_key(SLOT.AUTHENTICATION, private_key) + + +def test_get_certificate_does_not_require_authentication(session): + cert = get_test_cert() + session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY) + session.put_certificate(SLOT.AUTHENTICATION, cert) + reset_state(session) + + assert session.get_certificate(SLOT.AUTHENTICATION) + + +def test_authenticate_twice_does_not_throw(session): + session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY) + session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY) + + +def test_reset_resets_has_stored_key_flag(session): + pivman = get_pivman_data(session) + assert not pivman.has_stored_key + + session.verify_pin(DEFAULT_PIN) + session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY) + pivman_set_mgm_key( + session, + NON_DEFAULT_MANAGEMENT_KEY, + MANAGEMENT_KEY_TYPE.TDES, + store_on_device=True, + ) + + pivman = get_pivman_data(session) + assert pivman.has_stored_key + + reset_state(session) + session.reset() + + pivman = get_pivman_data(session) + assert not pivman.has_stored_key + + +# Should this really fail? +def disabled_test_reset_while_verified_throws_nice_ValueError(session): + session.verify_pin(DEFAULT_PIN) + with pytest.raises(ValueError) as cm: + session.reset() + assert "Cannot read remaining tries from status word: 9000" in str(cm.exception) + + +def test_set_mgm_key_does_not_change_key_if_not_authenticated(session): + with pytest.raises(ApduError): + session.set_management_key(MANAGEMENT_KEY_TYPE.TDES, NON_DEFAULT_MANAGEMENT_KEY) + assert_mgm_key_is(session, DEFAULT_MANAGEMENT_KEY) + + +@condition.min_version(3, 5) +def test_set_stored_mgm_key_does_not_destroy_key_if_pin_not_verified(session): + session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY) + with pytest.raises(ApduError): + pivman_set_mgm_key( + session, + NON_DEFAULT_MANAGEMENT_KEY, + MANAGEMENT_KEY_TYPE.TDES, + store_on_device=True, + ) + + assert_mgm_key_is(session, DEFAULT_MANAGEMENT_KEY) + + +def test_set_mgm_key_changes_mgm_key(session): + session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY) + session.set_management_key(MANAGEMENT_KEY_TYPE.TDES, NON_DEFAULT_MANAGEMENT_KEY) + + assert_mgm_key_is_not(session, DEFAULT_MANAGEMENT_KEY) + assert_mgm_key_is(session, NON_DEFAULT_MANAGEMENT_KEY) + + +def test_set_stored_mgm_key_succeeds_if_pin_is_verified(session): + session.verify_pin(DEFAULT_PIN) + session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY) + pivman_set_mgm_key( + session, + NON_DEFAULT_MANAGEMENT_KEY, + MANAGEMENT_KEY_TYPE.TDES, + store_on_device=True, + ) + + assert_mgm_key_is_not(session, DEFAULT_MANAGEMENT_KEY) + assert_mgm_key_is(session, NON_DEFAULT_MANAGEMENT_KEY) + + pivman_prot = get_pivman_protected_data(session) + assert pivman_prot.key == NON_DEFAULT_MANAGEMENT_KEY + + pivman_prot = get_pivman_protected_data(session) + assert_mgm_key_is(session, pivman_prot.key) + + +def sign(session, slot, key_type, message): + return session.sign(slot, key_type, message, hashes.SHA256(), padding.PKCS1v15()) + + +@condition.min_version(4) +def test_sign_with_pin_policy_always_requires_pin_every_time(session): + generate_key(session, pin_policy=PIN_POLICY.ALWAYS) + + with pytest.raises(ApduError): + sign(session, SLOT.AUTHENTICATION, KEY_TYPE.ECCP256, b"foo") + + session.verify_pin(DEFAULT_PIN) + sig = sign(session, SLOT.AUTHENTICATION, KEY_TYPE.ECCP256, b"foo") + assert sig + + with pytest.raises(ApduError): + sign(session, SLOT.AUTHENTICATION, KEY_TYPE.ECCP256, b"foo") + + session.verify_pin(DEFAULT_PIN) + sig = sign(session, SLOT.AUTHENTICATION, KEY_TYPE.ECCP256, b"foo") + assert sig + + +@condition.fips(False) +@condition.min_version(4) +def test_sign_with_pin_policy_never_does_not_require_pin(session): + generate_key(session, pin_policy=PIN_POLICY.NEVER) + sig = sign(session, SLOT.AUTHENTICATION, KEY_TYPE.ECCP256, b"foo") + assert sig + + +@condition.fips() +def test_pin_policy_never_blocked_on_fips(session): + with pytest.raises(ApduError): + generate_key(session, pin_policy=PIN_POLICY.NEVER) + + +@condition.min_version(4) +def test_sign_with_pin_policy_once_requires_pin_once_per_session(session): + generate_key(session, pin_policy=PIN_POLICY.ONCE) + + with pytest.raises(ApduError): + sign(session, SLOT.AUTHENTICATION, KEY_TYPE.ECCP256, b"foo") + + session.verify_pin(DEFAULT_PIN) + sig = sign(session, SLOT.AUTHENTICATION, KEY_TYPE.ECCP256, b"foo") + assert sig + + sig = sign(session, SLOT.AUTHENTICATION, KEY_TYPE.ECCP256, b"foo") + assert sig + + reset_state(session) + + with pytest.raises(ApduError): + sign(session, SLOT.AUTHENTICATION, KEY_TYPE.ECCP256, b"foo") + + session.verify_pin(DEFAULT_PIN) + sig = sign(session, SLOT.AUTHENTICATION, KEY_TYPE.ECCP256, b"foo") + assert sig + + sig = sign(session, SLOT.AUTHENTICATION, KEY_TYPE.ECCP256, b"foo") + assert sig + + +def test_signature_can_be_verified_by_public_key(session): + public_key = generate_key(session) + + signed_data = bytes(random.randint(0, 255) for i in range(32)) + + session.verify_pin(DEFAULT_PIN) + sig = sign(session, SLOT.AUTHENTICATION, KEY_TYPE.ECCP256, signed_data) + assert sig + + public_key.verify(sig, signed_data, ec.ECDSA(hashes.SHA256())) + + +def block_pin(session): + while session.get_pin_attempts() > 0: + try: + session.verify_pin(NON_DEFAULT_PIN) + except Exception: + pass + + +def test_unblock_pin_requires_no_previous_authentication(session): + session.unblock_pin(DEFAULT_PUK, NON_DEFAULT_PIN) + + +def test_unblock_pin_with_wrong_puk_throws_InvalidPinError(session): + with pytest.raises(InvalidPinError): + session.unblock_pin(NON_DEFAULT_PUK, NON_DEFAULT_PIN) + + +def test_unblock_pin_resets_pin_and_retries(session): + session.reset() + reset_state(session) + + block_pin(session) + + with pytest.raises(InvalidPinError): + session.verify_pin(DEFAULT_PIN) + + session.unblock_pin(DEFAULT_PUK, NON_DEFAULT_PIN) + + assert session.get_pin_attempts() == 3 + session.verify_pin(NON_DEFAULT_PIN) + + +def test_set_pin_retries_requires_pin_and_mgm_key(session): + # Fails with no authentication + with pytest.raises(ApduError): + session.set_pin_attempts(4, 4) + + # Fails with only PIN + session.verify_pin(DEFAULT_PIN) + with pytest.raises(ApduError): + session.set_pin_attempts(4, 4) + + reset_state(session) + + # Fails with only management key + session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY) + with pytest.raises(ApduError): + session.set_pin_attempts(4, 4) + + # Succeeds with both PIN and management key + session.verify_pin(DEFAULT_PIN) + session.set_pin_attempts(4, 4) + + +def test_set_pin_retries_sets_pin_and_puk_tries(session): + pin_tries = 9 + puk_tries = 7 + + session.verify_pin(DEFAULT_PIN) + session.authenticate(MANAGEMENT_KEY_TYPE.TDES, DEFAULT_MANAGEMENT_KEY) + session.set_pin_attempts(pin_tries, puk_tries) + + reset_state(session) + + assert session.get_pin_attempts() == pin_tries + with pytest.raises(InvalidPinError) as ctx: + session.change_puk(NON_DEFAULT_PUK, DEFAULT_PUK) + assert ctx.value.attempts_remaining == puk_tries - 1