From e834296818c90921a5ba7de963b6db64614e10db Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Wed, 24 Apr 2024 09:58:33 +0200 Subject: [PATCH] Improve tests --- tests/device/cli/piv/conftest.py | 44 +++ .../cli/piv/test_generate_cert_and_csr.py | 162 +++++----- tests/device/cli/piv/test_key_management.py | 170 +++++----- tests/device/cli/piv/test_management_key.py | 67 ++-- tests/device/cli/piv/test_misc.py | 11 +- tests/device/cli/piv/test_pin_puk.py | 57 ++-- .../device/cli/piv/test_read_write_object.py | 17 +- tests/device/cli/piv/util.py | 4 +- tests/device/cli/test_hsmauth.py | 88 +++-- tests/device/cli/test_oath.py | 214 ++++++------ tests/device/cli/test_openpgp.py | 40 ++- tests/device/cli/test_otp.py | 10 + tests/device/test_hsmauth.py | 132 +++++--- tests/device/test_oath.py | 4 +- tests/device/test_openpgp.py | 125 ++++--- tests/device/test_otp.py | 7 + tests/device/test_piv.py | 304 ++++++++++-------- tests/test_piv.py | 31 +- ykman/_cli/fido.py | 43 +-- ykman/_cli/hsmauth.py | 221 +++++++++---- ykman/_cli/info.py | 8 +- ykman/_cli/oath.py | 57 +++- ykman/_cli/openpgp.py | 9 +- ykman/_cli/piv.py | 78 ++++- ykman/piv.py | 11 +- yubikit/core/__init__.py | 3 + yubikit/hsmauth.py | 19 +- yubikit/management.py | 38 +++ yubikit/piv.py | 88 ++++- 29 files changed, 1322 insertions(+), 740 deletions(-) diff --git a/tests/device/cli/piv/conftest.py b/tests/device/cli/piv/conftest.py index 2af43ba1..31163dc6 100644 --- a/tests/device/cli/piv/conftest.py +++ b/tests/device/cli/piv/conftest.py @@ -1,5 +1,7 @@ from yubikit.management import CAPABILITY from ... import condition +from .util import DEFAULT_PIN, DEFAULT_PUK, DEFAULT_MANAGEMENT_KEY +from typing import NamedTuple import pytest @@ -7,3 +9,45 @@ @condition.capability(CAPABILITY.PIV) def ensure_piv(ykman_cli): ykman_cli("piv", "reset", "-f") + + +class Keys(NamedTuple): + pin: str + puk: str + mgmt: str + + +@pytest.fixture +def default_keys(): + yield Keys(DEFAULT_PIN, DEFAULT_PUK, DEFAULT_MANAGEMENT_KEY) + + +@pytest.fixture +def keys(ykman_cli, info, default_keys): + if CAPABILITY.PIV in info.fips_capable: + new_keys = Keys( + "12345679", + "12345670", + "010203040506070801020304050607080102030405060709", + ) + + ykman_cli( + "piv", "access", "change-pin", "-P", default_keys.pin, "-n", new_keys.pin + ) + ykman_cli( + "piv", "access", "change-puk", "-p", default_keys.puk, "-n", new_keys.puk + ) + ykman_cli( + "piv", + "access", + "change-management-key", + "-m", + default_keys.mgmt, + "-n", + new_keys.mgmt, + "-f", + ) + + yield new_keys + else: + yield default_keys diff --git a/tests/device/cli/piv/test_generate_cert_and_csr.py b/tests/device/cli/piv/test_generate_cert_and_csr.py index 76a1ba8c..f2eb7269 100644 --- a/tests/device/cli/piv/test_generate_cert_and_csr.py +++ b/tests/device/cli/piv/test_generate_cert_and_csr.py @@ -3,7 +3,7 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.asymmetric import ec, rsa, padding -from .util import DEFAULT_PIN, DEFAULT_MANAGEMENT_KEY, NON_DEFAULT_MANAGEMENT_KEY +from .util import NON_DEFAULT_MANAGEMENT_KEY from ... import condition import pytest @@ -33,20 +33,20 @@ def not_roca(version): class TestNonDefaultMgmKey: @pytest.fixture(autouse=True) - def set_mgmt_key(self, ykman_cli): + def set_mgmt_key(self, ykman_cli, keys): ykman_cli( "piv", "access", "change-management-key", "-P", - DEFAULT_PIN, + keys.pin, "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, "-n", NON_DEFAULT_MANAGEMENT_KEY, ) - def _test_generate_self_signed(self, ykman_cli, slot, algo): + def _test_generate_self_signed(self, ykman_cli, keys, slot, algo): pubkey_output = ykman_cli( "piv", "keys", @@ -68,7 +68,7 @@ def _test_generate_self_signed(self, ykman_cli, slot, algo): "-s", "subject-" + algo, "-P", - DEFAULT_PIN, + keys.pin, "-", input=pubkey_output, ) @@ -82,37 +82,37 @@ def _test_generate_self_signed(self, ykman_cli, slot, algo): @condition.yk4_fips(False) @condition.check(not_roca) - def test_generate_self_signed_slot_9a_rsa1024(self, ykman_cli): - self._test_generate_self_signed(ykman_cli, "9a", "RSA1024") + def test_generate_self_signed_slot_9a_rsa2048(self, ykman_cli, keys): + self._test_generate_self_signed(ykman_cli, keys, "9a", "RSA2048") - def test_generate_self_signed_slot_9a_eccp256(self, ykman_cli): - self._test_generate_self_signed(ykman_cli, "9a", "ECCP256") + def test_generate_self_signed_slot_9a_eccp256(self, ykman_cli, keys): + self._test_generate_self_signed(ykman_cli, keys, "9a", "ECCP256") @condition.yk4_fips(False) @condition.check(not_roca) - def test_generate_self_signed_slot_9c_rsa1024(self, ykman_cli): - self._test_generate_self_signed(ykman_cli, "9c", "RSA1024") + def test_generate_self_signed_slot_9c_rsa2048(self, ykman_cli, keys): + self._test_generate_self_signed(ykman_cli, keys, "9c", "RSA2048") - def test_generate_self_signed_slot_9c_eccp256(self, ykman_cli): - self._test_generate_self_signed(ykman_cli, "9c", "ECCP256") + def test_generate_self_signed_slot_9c_eccp256(self, ykman_cli, keys): + self._test_generate_self_signed(ykman_cli, keys, "9c", "ECCP256") @condition.yk4_fips(False) @condition.check(not_roca) - def test_generate_self_signed_slot_9d_rsa1024(self, ykman_cli): - self._test_generate_self_signed(ykman_cli, "9d", "RSA1024") + def test_generate_self_signed_slot_9d_rsa2048(self, ykman_cli, keys): + self._test_generate_self_signed(ykman_cli, keys, "9d", "RSA2048") - def test_generate_self_signed_slot_9d_eccp256(self, ykman_cli): - self._test_generate_self_signed(ykman_cli, "9d", "ECCP256") + def test_generate_self_signed_slot_9d_eccp256(self, ykman_cli, keys): + self._test_generate_self_signed(ykman_cli, keys, "9d", "ECCP256") @condition.yk4_fips(False) @condition.check(not_roca) - def test_generate_self_signed_slot_9e_rsa1024(self, ykman_cli): - self._test_generate_self_signed(ykman_cli, "9e", "RSA1024") + def test_generate_self_signed_slot_9e_rsa2048(self, ykman_cli, keys): + self._test_generate_self_signed(ykman_cli, keys, "9e", "RSA2048") - def test_generate_self_signed_slot_9e_eccp256(self, ykman_cli): - self._test_generate_self_signed(ykman_cli, "9e", "ECCP256") + def test_generate_self_signed_slot_9e_eccp256(self, ykman_cli, keys): + self._test_generate_self_signed(ykman_cli, keys, "9e", "ECCP256") - def _test_generate_csr(self, ykman_cli, slot, algo): + def _test_generate_csr(self, ykman_cli, keys, slot, algo): subject_input = "subject-" + algo pubkey_output = ykman_cli( "piv", @@ -131,7 +131,7 @@ def _test_generate_csr(self, ykman_cli, slot, algo): "request", slot, "-P", - DEFAULT_PIN, + keys.pin, "-", "-", "-s", @@ -147,54 +147,54 @@ def _test_generate_csr(self, ykman_cli, slot, algo): @condition.yk4_fips(False) @condition.check(not_roca) - def test_generate_csr_slot_9a_rsa1024(self, ykman_cli): - self._test_generate_csr(ykman_cli, "9a", "RSA1024") + def test_generate_csr_slot_9a_rsa2048(self, ykman_cli, keys): + self._test_generate_csr(ykman_cli, keys, "9a", "RSA2048") - def test_generate_csr_slot_9a_eccp256(self, ykman_cli): - self._test_generate_csr(ykman_cli, "9a", "ECCP256") + def test_generate_csr_slot_9a_eccp256(self, ykman_cli, keys): + self._test_generate_csr(ykman_cli, keys, "9a", "ECCP256") @condition.yk4_fips(False) @condition.check(not_roca) - def test_generate_csr_slot_9c_rsa1024(self, ykman_cli): - self._test_generate_csr(ykman_cli, "9c", "RSA1024") + def test_generate_csr_slot_9c_rsa2048(self, ykman_cli, keys): + self._test_generate_csr(ykman_cli, keys, "9c", "RSA2048") - def test_generate_csr_slot_9c_eccp256(self, ykman_cli): - self._test_generate_csr(ykman_cli, "9c", "ECCP256") + def test_generate_csr_slot_9c_eccp256(self, ykman_cli, keys): + self._test_generate_csr(ykman_cli, keys, "9c", "ECCP256") @condition.yk4_fips(False) @condition.check(not_roca) - def test_generate_csr_slot_9d_rsa1024(self, ykman_cli): - self._test_generate_csr(ykman_cli, "9d", "RSA1024") + def test_generate_csr_slot_9d_rsa2048(self, ykman_cli, keys): + self._test_generate_csr(ykman_cli, keys, "9d", "RSA2048") - def test_generate_csr_slot_9d_eccp256(self, ykman_cli): - self._test_generate_csr(ykman_cli, "9d", "ECCP256") + def test_generate_csr_slot_9d_eccp256(self, ykman_cli, keys): + self._test_generate_csr(ykman_cli, keys, "9d", "ECCP256") @condition.yk4_fips(False) @condition.check(not_roca) - def test_generate_csr_slot_9e_rsa1024(self, ykman_cli): - self._test_generate_csr(ykman_cli, "9e", "RSA1024") + def test_generate_csr_slot_9e_rsa2048(self, ykman_cli, keys): + self._test_generate_csr(ykman_cli, keys, "9e", "RSA2048") - def test_generate_csr_slot_9e_eccp256(self, ykman_cli): - self._test_generate_csr(ykman_cli, "9e", "ECCP256") + def test_generate_csr_slot_9e_eccp256(self, ykman_cli, keys): + self._test_generate_csr(ykman_cli, keys, "9e", "ECCP256") class TestProtectedMgmKey: @pytest.fixture(autouse=True) - def protect_mgmt_key(self, ykman_cli): + def protect_mgmt_key(self, ykman_cli, keys): ykman_cli( "piv", "access", "change-management-key", "-p", "-P", - DEFAULT_PIN, + keys.pin, "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, ) - def _test_generate_self_signed(self, ykman_cli, slot, algo): + def _test_generate_self_signed(self, ykman_cli, keys, slot, algo): pubkey_output = ykman_cli( - "piv", "keys", "generate", slot, "-a", algo, "-P", DEFAULT_PIN, "-" + "piv", "keys", "generate", slot, "-a", algo, "-P", keys.pin, "-" ).output ykman_cli( "piv", @@ -202,7 +202,7 @@ def _test_generate_self_signed(self, ykman_cli, slot, algo): "generate", slot, "-P", - DEFAULT_PIN, + keys.pin, "-s", "subject-" + algo, "-", @@ -218,40 +218,40 @@ def _test_generate_self_signed(self, ykman_cli, slot, algo): @condition.yk4_fips(False) @condition.check(not_roca) - def test_generate_self_signed_slot_9a_rsa1024(self, ykman_cli): - self._test_generate_self_signed(ykman_cli, "9a", "RSA1024") + def test_generate_self_signed_slot_9a_rsa2048(self, ykman_cli, keys): + self._test_generate_self_signed(ykman_cli, keys, "9a", "RSA2048") - def test_generate_self_signed_slot_9a_eccp256(self, ykman_cli): - self._test_generate_self_signed(ykman_cli, "9a", "ECCP256") + def test_generate_self_signed_slot_9a_eccp256(self, ykman_cli, keys): + self._test_generate_self_signed(ykman_cli, keys, "9a", "ECCP256") @condition.yk4_fips(False) @condition.check(not_roca) - def test_generate_self_signed_slot_9c_rsa1024(self, ykman_cli): - self._test_generate_self_signed(ykman_cli, "9c", "RSA1024") + def test_generate_self_signed_slot_9c_rsa2048(self, ykman_cli, keys): + self._test_generate_self_signed(ykman_cli, keys, "9c", "RSA2048") - def test_generate_self_signed_slot_9c_eccp256(self, ykman_cli): - self._test_generate_self_signed(ykman_cli, "9c", "ECCP256") + def test_generate_self_signed_slot_9c_eccp256(self, ykman_cli, keys): + self._test_generate_self_signed(ykman_cli, keys, "9c", "ECCP256") @condition.yk4_fips(False) @condition.check(not_roca) - def test_generate_self_signed_slot_9d_rsa1024(self, ykman_cli): - self._test_generate_self_signed(ykman_cli, "9d", "RSA1024") + def test_generate_self_signed_slot_9d_rsa2048(self, ykman_cli, keys): + self._test_generate_self_signed(ykman_cli, keys, "9d", "RSA2048") - def test_generate_self_signed_slot_9d_eccp256(self, ykman_cli): - self._test_generate_self_signed(ykman_cli, "9d", "ECCP256") + def test_generate_self_signed_slot_9d_eccp256(self, ykman_cli, keys): + self._test_generate_self_signed(ykman_cli, keys, "9d", "ECCP256") @condition.yk4_fips(False) @condition.check(not_roca) - def test_generate_self_signed_slot_9e_rsa1024(self, ykman_cli): - self._test_generate_self_signed(ykman_cli, "9e", "RSA1024") + def test_generate_self_signed_slot_9e_rsa2048(self, ykman_cli, keys): + self._test_generate_self_signed(ykman_cli, keys, "9e", "RSA2048") - def test_generate_self_signed_slot_9e_eccp256(self, ykman_cli): - self._test_generate_self_signed(ykman_cli, "9e", "ECCP256") + def test_generate_self_signed_slot_9e_eccp256(self, ykman_cli, keys): + self._test_generate_self_signed(ykman_cli, keys, "9e", "ECCP256") - def _test_generate_csr(self, ykman_cli, slot, algo): + def _test_generate_csr(self, ykman_cli, keys, slot, algo): subject_input = "subject-" + algo pubkey_output = ykman_cli( - "piv", "keys", "generate", slot, "-a", algo, "-P", DEFAULT_PIN, "-" + "piv", "keys", "generate", slot, "-a", algo, "-P", keys.pin, "-" ).output csr_output = ykman_cli( "piv", @@ -259,7 +259,7 @@ def _test_generate_csr(self, ykman_cli, slot, algo): "request", slot, "-P", - DEFAULT_PIN, + keys.pin, "-", "-", "-s", @@ -275,32 +275,32 @@ def _test_generate_csr(self, ykman_cli, slot, algo): @condition.yk4_fips(False) @condition.check(not_roca) - def test_generate_csr_slot_9a_rsa1024(self, ykman_cli): - self._test_generate_csr(ykman_cli, "9a", "RSA1024") + def test_generate_csr_slot_9a_rsa2048(self, ykman_cli, keys): + self._test_generate_csr(ykman_cli, keys, "9a", "RSA2048") - def test_generate_csr_slot_9a_eccp256(self, ykman_cli): - self._test_generate_csr(ykman_cli, "9a", "ECCP256") + def test_generate_csr_slot_9a_eccp256(self, ykman_cli, keys): + self._test_generate_csr(ykman_cli, keys, "9a", "ECCP256") @condition.yk4_fips(False) @condition.check(not_roca) - def test_generate_csr_slot_9c_rsa1024(self, ykman_cli): - self._test_generate_csr(ykman_cli, "9c", "RSA1024") + def test_generate_csr_slot_9c_rsa2048(self, ykman_cli, keys): + self._test_generate_csr(ykman_cli, keys, "9c", "RSA2048") - def test_generate_csr_slot_9c_eccp256(self, ykman_cli): - self._test_generate_csr(ykman_cli, "9c", "ECCP256") + def test_generate_csr_slot_9c_eccp256(self, ykman_cli, keys): + self._test_generate_csr(ykman_cli, keys, "9c", "ECCP256") @condition.yk4_fips(False) @condition.check(not_roca) - def test_generate_csr_slot_9d_rsa1024(self, ykman_cli): - self._test_generate_csr(ykman_cli, "9d", "RSA1024") + def test_generate_csr_slot_9d_rsa2048(self, ykman_cli, keys): + self._test_generate_csr(ykman_cli, keys, "9d", "RSA2048") - def test_generate_csr_slot_9d_eccp256(self, ykman_cli): - self._test_generate_csr(ykman_cli, "9d", "ECCP256") + def test_generate_csr_slot_9d_eccp256(self, ykman_cli, keys): + self._test_generate_csr(ykman_cli, keys, "9d", "ECCP256") @condition.yk4_fips(False) @condition.check(not_roca) - def test_generate_csr_slot_9e_rsa1024(self, ykman_cli): - self._test_generate_csr(ykman_cli, "9e", "RSA1024") + def test_generate_csr_slot_9e_rsa2048(self, ykman_cli, keys): + self._test_generate_csr(ykman_cli, keys, "9e", "RSA2048") - def test_generate_csr_slot_9e_eccp256(self, ykman_cli): - self._test_generate_csr(ykman_cli, "9e", "ECCP256") + def test_generate_csr_slot_9e_eccp256(self, ykman_cli, keys): + self._test_generate_csr(ykman_cli, keys, "9e", "ECCP256") diff --git a/tests/device/cli/piv/test_key_management.py b/tests/device/cli/piv/test_key_management.py index c2eac6b4..128e501c 100644 --- a/tests/device/cli/piv/test_key_management.py +++ b/tests/device/cli/piv/test_key_management.py @@ -3,7 +3,7 @@ from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import ec from yubikit.core import NotSupportedError -from .util import DEFAULT_PIN, DEFAULT_MANAGEMENT_KEY +from yubikit.management import CAPABILITY from ... import condition import tempfile import os @@ -45,7 +45,7 @@ def tmp_file(): class TestKeyExport: @condition.min_version(5, 3) - def test_from_metadata(self, ykman_cli): + def test_from_metadata(self, ykman_cli, keys): pair = generate_pem_eccp256_keypair() ykman_cli( @@ -54,7 +54,7 @@ def test_from_metadata(self, ykman_cli): "import", "9a", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, "-", input=pair[0], ) @@ -62,7 +62,7 @@ def test_from_metadata(self, ykman_cli): assert exported == pair[1] @condition.min_version(4, 3) - def test_from_metadata_or_attestation(self, ykman_cli): + def test_from_metadata_or_attestation(self, ykman_cli, keys): der = ykman_cli( "piv", "keys", @@ -73,7 +73,7 @@ def test_from_metadata_or_attestation(self, ykman_cli): "-F", "der", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, "-", ).stdout_bytes exported = ykman_cli( @@ -81,7 +81,7 @@ def test_from_metadata_or_attestation(self, ykman_cli): ).stdout_bytes assert der == exported - def test_from_metadata_or_cert(self, ykman_cli): + def test_from_metadata_or_cert(self, ykman_cli, keys): private_key_pem, public_key_pem = generate_pem_eccp256_keypair() ykman_cli( "piv", @@ -89,7 +89,7 @@ def test_from_metadata_or_cert(self, ykman_cli): "import", "9a", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, "-", input=private_key_pem, ) @@ -100,9 +100,9 @@ def test_from_metadata_or_cert(self, ykman_cli): "9a", "-", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, "-P", - DEFAULT_PIN, + keys.pin, "-s", "test", input=public_key_pem, @@ -113,7 +113,7 @@ def test_from_metadata_or_cert(self, ykman_cli): assert public_key_pem == exported @condition.max_version(5, 2, 9) - def test_from_cert_verify(self, ykman_cli): + def test_from_cert_verify(self, ykman_cli, keys): private_key_pem, public_key_pem = generate_pem_eccp256_keypair() ykman_cli( "piv", @@ -121,7 +121,7 @@ def test_from_cert_verify(self, ykman_cli): "import", "9a", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, "-", input=private_key_pem, ) @@ -132,17 +132,17 @@ def test_from_cert_verify(self, ykman_cli): "9a", "-", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, "-P", - DEFAULT_PIN, + keys.pin, "-s", "test", input=public_key_pem, ) - ykman_cli("piv", "keys", "export", "9a", "--verify", "-P", DEFAULT_PIN, "-") + ykman_cli("piv", "keys", "export", "9a", "--verify", "-P", keys.pin, "-") @condition.max_version(5, 2, 9) - def test_from_cert_verify_fails(self, ykman_cli): + def test_from_cert_verify_fails(self, ykman_cli, keys): private_key_pem = generate_pem_eccp256_keypair()[0] public_key_pem = generate_pem_eccp256_keypair()[1] ykman_cli( @@ -151,7 +151,7 @@ def test_from_cert_verify_fails(self, ykman_cli): "import", "9a", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, "-", input=private_key_pem, ) @@ -162,35 +162,34 @@ def test_from_cert_verify_fails(self, ykman_cli): "9a", "-", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, "-P", - DEFAULT_PIN, + keys.pin, "-s", "test", input=public_key_pem, ) with pytest.raises(SystemExit): - ykman_cli("piv", "keys", "export", "9a", "--verify", "-P", DEFAULT_PIN, "-") + ykman_cli("piv", "keys", "export", "9a", "--verify", "-P", keys.pin, "-") class TestKeyManagement: @condition.check(not_roca) - def test_generate_key_default(self, ykman_cli): - output = ykman_cli( - "piv", "keys", "generate", "9a", "-m", DEFAULT_MANAGEMENT_KEY, "-" - ).output + def test_generate_key_default(self, ykman_cli, keys): + output = ykman_cli("piv", "keys", "generate", "9a", "-m", keys.mgmt, "-").output assert "BEGIN PUBLIC KEY" in output @condition.check(roca) - def test_generate_key_default_cve201715361(self, ykman_cli): + def test_generate_key_default_cve201715361(self, ykman_cli, keys): with pytest.raises(NotSupportedError): - ykman_cli( - "piv", "keys", "generate", "9a", "-m", DEFAULT_MANAGEMENT_KEY, "-" - ) + ykman_cli("piv", "keys", "generate", "9a", "-m", keys.mgmt, "-") @condition.check(not_roca) @condition.yk4_fips(False) - def test_generate_key_rsa1024(self, ykman_cli): + def test_generate_key_rsa1024(self, ykman_cli, info, keys): + if CAPABILITY.PIV in info.fips_capable: + pytest.skip("RSA1024 not available on FIPS") + output = ykman_cli( "piv", "keys", @@ -199,13 +198,13 @@ def test_generate_key_rsa1024(self, ykman_cli): "-a", "RSA1024", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, "-", ).output assert "BEGIN PUBLIC KEY" in output @condition.check(not_roca) - def test_generate_key_rsa2048(self, ykman_cli): + def test_generate_key_rsa2048(self, ykman_cli, keys): output = ykman_cli( "piv", "keys", @@ -214,14 +213,14 @@ def test_generate_key_rsa2048(self, ykman_cli): "-a", "RSA2048", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, "-", ).output assert "BEGIN PUBLIC KEY" in output @condition.yk4_fips(False) @condition.check(roca) - def test_generate_key_rsa1024_cve201715361(self, ykman_cli): + def test_generate_key_rsa1024_cve201715361(self, ykman_cli, keys): with pytest.raises(NotSupportedError): ykman_cli( "piv", @@ -231,12 +230,12 @@ def test_generate_key_rsa1024_cve201715361(self, ykman_cli): "-a", "RSA1024", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, "-", ) @condition.check(roca) - def test_generate_key_rsa2048_cve201715361(self, ykman_cli): + def test_generate_key_rsa2048_cve201715361(self, ykman_cli, keys): with pytest.raises(NotSupportedError): ykman_cli( "piv", @@ -246,11 +245,11 @@ def test_generate_key_rsa2048_cve201715361(self, ykman_cli): "-a", "RSA2048", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, "-", ) - def test_generate_key_eccp256(self, ykman_cli): + def test_generate_key_eccp256(self, ykman_cli, keys): output = ykman_cli( "piv", "keys", @@ -259,25 +258,25 @@ def test_generate_key_eccp256(self, ykman_cli): "-a", "ECCP256", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, "-", ).output assert "BEGIN PUBLIC KEY" in output - def test_import_key_eccp256(self, ykman_cli): + def test_import_key_eccp256(self, ykman_cli, keys): ykman_cli( "piv", "keys", "import", "9a", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, "-", input=generate_pem_eccp256_keypair()[0], ) @condition.min_version(4) - def test_generate_key_eccp384(self, ykman_cli): + def test_generate_key_eccp384(self, ykman_cli, keys): output = ykman_cli( "piv", "keys", @@ -286,13 +285,13 @@ def test_generate_key_eccp384(self, ykman_cli): "-a", "ECCP384", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, "-", ).output assert "BEGIN PUBLIC KEY" in output @condition.min_version(4) - def test_generate_key_pin_policy_always(self, ykman_cli): + def test_generate_key_pin_policy_always(self, ykman_cli, keys): output = ykman_cli( "piv", "keys", @@ -301,7 +300,7 @@ def test_generate_key_pin_policy_always(self, ykman_cli): "--pin-policy", "ALWAYS", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, "-a", "ECCP256", "-", @@ -309,7 +308,7 @@ def test_generate_key_pin_policy_always(self, ykman_cli): assert "BEGIN PUBLIC KEY" in output @condition.min_version(4) - def test_import_key_pin_policy_always(self, ykman_cli): + def test_import_key_pin_policy_always(self, ykman_cli, keys): for pin_policy in ["ALWAYS", "always"]: ykman_cli( "piv", @@ -319,13 +318,13 @@ def test_import_key_pin_policy_always(self, ykman_cli): "--pin-policy", pin_policy, "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, "-", input=generate_pem_eccp256_keypair()[0], ) @condition.min_version(4) - def test_generate_key_touch_policy_always(self, ykman_cli): + def test_generate_key_touch_policy_always(self, ykman_cli, keys): output = ykman_cli( "piv", "keys", @@ -334,7 +333,7 @@ def test_generate_key_touch_policy_always(self, ykman_cli): "--touch-policy", "ALWAYS", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, "-a", "ECCP256", "-", @@ -342,7 +341,7 @@ def test_generate_key_touch_policy_always(self, ykman_cli): assert "BEGIN PUBLIC KEY" in output @condition.min_version(4) - def test_import_key_touch_policy_always(self, ykman_cli): + def test_import_key_touch_policy_always(self, ykman_cli, keys): for touch_policy in ["ALWAYS", "always"]: ykman_cli( "piv", @@ -352,13 +351,13 @@ def test_import_key_touch_policy_always(self, ykman_cli): "--touch-policy", touch_policy, "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, "-", input=generate_pem_eccp256_keypair()[0], ) @condition.min_version(4, 3) - def test_attest_key(self, ykman_cli): + def test_attest_key(self, ykman_cli, keys): ykman_cli( "piv", "keys", @@ -367,13 +366,13 @@ def test_attest_key(self, ykman_cli): "-a", "ECCP256", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, "-", ) output = ykman_cli("piv", "keys", "attest", "9a", "-").output assert "BEGIN CERTIFICATE" in output - def _test_generate_csr(self, ykman_cli, tmp_file, algo): + def _test_generate_csr(self, ykman_cli, keys, tmp_file, algo): ykman_cli( "piv", "keys", @@ -382,7 +381,7 @@ def _test_generate_csr(self, ykman_cli, tmp_file, algo): "-a", algo, "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, tmp_file, ) output = ykman_cli( @@ -394,7 +393,7 @@ def _test_generate_csr(self, ykman_cli, tmp_file, algo): "-s", "test-subject", "-P", - DEFAULT_PIN, + keys.pin, "-", ).output csr = x509.load_pem_x509_csr(output.encode(), default_backend()) @@ -402,13 +401,18 @@ def _test_generate_csr(self, ykman_cli, tmp_file, algo): @condition.yk4_fips(False) @condition.check(not_roca) - def test_generate_csr_rsa1024(self, ykman_cli, tmp_file): - self._test_generate_csr(ykman_cli, tmp_file, "RSA1024") + def test_generate_csr_rsa1024(self, ykman_cli, keys, info, tmp_file): + if CAPABILITY.PIV in info.fips_capable: + pytest.skip("RSA1024 not available on FIPS") + + self._test_generate_csr(ykman_cli, keys, tmp_file, "RSA1024") - def test_generate_csr_eccp256(self, ykman_cli, tmp_file): - self._test_generate_csr(ykman_cli, tmp_file, "ECCP256") + def test_generate_csr_eccp256(self, ykman_cli, keys, tmp_file): + self._test_generate_csr(ykman_cli, keys, tmp_file, "ECCP256") - def test_import_verify_correct_cert_succeeds_with_pin(self, ykman_cli, tmp_file): + def test_import_verify_correct_cert_succeeds_with_pin( + self, ykman_cli, keys, tmp_file + ): # Set up a key in the slot and create a certificate for it public_key_pem = ykman_cli( "piv", @@ -418,7 +422,7 @@ def test_import_verify_correct_cert_succeeds_with_pin(self, ykman_cli, tmp_file) "-a", "ECCP256", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, "-", ).output @@ -429,9 +433,9 @@ def test_import_verify_correct_cert_succeeds_with_pin(self, ykman_cli, tmp_file) "9a", "-", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, "-P", - DEFAULT_PIN, + keys.pin, "-s", "test", input=public_key_pem, @@ -448,7 +452,7 @@ def test_import_verify_correct_cert_succeeds_with_pin(self, ykman_cli, tmp_file) "9a", tmp_file, "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, ) ykman_cli( @@ -459,9 +463,9 @@ def test_import_verify_correct_cert_succeeds_with_pin(self, ykman_cli, tmp_file) "9a", tmp_file, "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, "-P", - DEFAULT_PIN, + keys.pin, ) ykman_cli( "piv", @@ -471,11 +475,11 @@ def test_import_verify_correct_cert_succeeds_with_pin(self, ykman_cli, tmp_file) "9a", tmp_file, "-m", - DEFAULT_MANAGEMENT_KEY, - input=DEFAULT_PIN, + keys.mgmt, + input=keys.pin, ) - def test_import_verify_wrong_cert_fails(self, ykman_cli): + def test_import_verify_wrong_cert_fails(self, ykman_cli, keys): # Set up a key in the slot and create a certificate for it public_key_pem = ykman_cli( "piv", @@ -485,7 +489,7 @@ def test_import_verify_wrong_cert_fails(self, ykman_cli): "-a", "ECCP256", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, "-", ).output @@ -496,9 +500,9 @@ def test_import_verify_wrong_cert_fails(self, ykman_cli): "9a", "-", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, "-P", - DEFAULT_PIN, + keys.pin, "-s", "test", input=public_key_pem, @@ -515,7 +519,7 @@ def test_import_verify_wrong_cert_fails(self, ykman_cli): "-a", "ECCP256", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, "-", input=public_key_pem, ) @@ -529,13 +533,13 @@ def test_import_verify_wrong_cert_fails(self, ykman_cli): "9a", "-", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, "-P", - DEFAULT_PIN, + keys.pin, input=cert_pem, ) - def test_import_no_verify_wrong_cert_succeeds(self, ykman_cli): + def test_import_no_verify_wrong_cert_succeeds(self, ykman_cli, keys): # Set up a key in the slot and create a certificate for it public_key_pem = ykman_cli( "piv", @@ -545,7 +549,7 @@ def test_import_no_verify_wrong_cert_succeeds(self, ykman_cli): "-a", "ECCP256", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, "-", ).output @@ -556,9 +560,9 @@ def test_import_no_verify_wrong_cert_succeeds(self, ykman_cli): "9a", "-", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, "-P", - DEFAULT_PIN, + keys.pin, "-s", "test", input=public_key_pem, @@ -575,7 +579,7 @@ def test_import_no_verify_wrong_cert_succeeds(self, ykman_cli): "-a", "ECCP256", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, "-", input=public_key_pem, ) @@ -589,9 +593,9 @@ def test_import_no_verify_wrong_cert_succeeds(self, ykman_cli): "9a", "-", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, "-P", - DEFAULT_PIN, + keys.pin, input=cert_pem, ) @@ -602,9 +606,9 @@ def test_import_no_verify_wrong_cert_succeeds(self, ykman_cli): "9a", "-", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, "-P", - DEFAULT_PIN, + keys.pin, input=cert_pem, ) diff --git a/tests/device/cli/piv/test_management_key.py b/tests/device/cli/piv/test_management_key.py index 64b19b77..caab1f7a 100644 --- a/tests/device/cli/piv/test_management_key.py +++ b/tests/device/cli/piv/test_management_key.py @@ -1,37 +1,32 @@ -from .util import ( - old_new_new, - DEFAULT_PIN, - DEFAULT_MANAGEMENT_KEY, - NON_DEFAULT_MANAGEMENT_KEY, -) +from .util import old_new_new, NON_DEFAULT_MANAGEMENT_KEY import re import pytest class TestManagementKey: - def test_change_management_key_force_fails_without_generate(self, ykman_cli): + def test_change_management_key_force_fails_without_generate(self, ykman_cli, keys): with pytest.raises(SystemExit): ykman_cli( "piv", "access", "change-management-key", "-P", - DEFAULT_PIN, + keys.pin, "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, "-f", ) - def test_change_management_key_protect_random(self, ykman_cli): + def test_change_management_key_protect_random(self, ykman_cli, keys): ykman_cli( "piv", "access", "change-management-key", "-p", "-P", - DEFAULT_PIN, + keys.pin, "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, ) output = ykman_cli("piv", "info").output assert "Management key is stored on the YubiKey, protected by PIN" in output @@ -44,23 +39,23 @@ def test_change_management_key_protect_random(self, ykman_cli): "change-management-key", "-p", "-P", - DEFAULT_PIN, + keys.pin, "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, ) # Should succeed - PIN as key - ykman_cli("piv", "access", "change-management-key", "-p", "-P", DEFAULT_PIN) + ykman_cli("piv", "access", "change-management-key", "-p", "-P", keys.pin) - def test_change_management_key_protect_prompt(self, ykman_cli): + def test_change_management_key_protect_prompt(self, ykman_cli, keys): ykman_cli( "piv", "access", "change-management-key", "-p", "-P", - DEFAULT_PIN, - input=DEFAULT_MANAGEMENT_KEY, + keys.pin, + input=keys.mgmt, ) output = ykman_cli("piv", "info").output assert "Management key is stored on the YubiKey, protected by PIN" in output @@ -73,17 +68,17 @@ def test_change_management_key_protect_prompt(self, ykman_cli): "change-management-key", "-p", "-P", - DEFAULT_PIN, + keys.pin, "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, ) # Should succeed - PIN as key - ykman_cli("piv", "access", "change-management-key", "-p", "-P", DEFAULT_PIN) + ykman_cli("piv", "access", "change-management-key", "-p", "-P", keys.pin) - def test_change_management_key_no_protect_generate(self, ykman_cli): + def test_change_management_key_no_protect_generate(self, ykman_cli, keys): output = ykman_cli( - "piv", "access", "change-management-key", "-m", DEFAULT_MANAGEMENT_KEY, "-g" + "piv", "access", "change-management-key", "-m", keys.mgmt, "-g" ).output assert re.match( @@ -93,13 +88,13 @@ def test_change_management_key_no_protect_generate(self, ykman_cli): output = ykman_cli("piv", "info").output assert "Management key is stored on the YubiKey" not in output - def test_change_management_key_no_protect_arg(self, ykman_cli): + def test_change_management_key_no_protect_arg(self, ykman_cli, keys): output = ykman_cli( "piv", "access", "change-management-key", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, "-n", NON_DEFAULT_MANAGEMENT_KEY, ).output @@ -113,7 +108,7 @@ def test_change_management_key_no_protect_arg(self, ykman_cli): "access", "change-management-key", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, "-n", NON_DEFAULT_MANAGEMENT_KEY, ) @@ -125,28 +120,28 @@ def test_change_management_key_no_protect_arg(self, ykman_cli): "-m", NON_DEFAULT_MANAGEMENT_KEY, "-n", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, ).output assert "" == output - def test_change_management_key_no_protect_arg_bad_length(self, ykman_cli): + def test_change_management_key_no_protect_arg_bad_length(self, ykman_cli, keys): with pytest.raises(SystemExit): ykman_cli( "piv", "access", "change-management-key", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, "-n", "10020304050607080102030405060708", ) - def test_change_management_key_no_protect_prompt(self, ykman_cli): + def test_change_management_key_no_protect_prompt(self, ykman_cli, keys): output = ykman_cli( "piv", "access", "change-management-key", - input=old_new_new(DEFAULT_MANAGEMENT_KEY, NON_DEFAULT_MANAGEMENT_KEY), + input=old_new_new(keys.mgmt, NON_DEFAULT_MANAGEMENT_KEY), ).output assert "Generated" not in output output = ykman_cli("piv", "info").output @@ -157,25 +152,27 @@ def test_change_management_key_no_protect_prompt(self, ykman_cli): "piv", "access", "change-management-key", - input=old_new_new(DEFAULT_MANAGEMENT_KEY, NON_DEFAULT_MANAGEMENT_KEY), + input=old_new_new(keys.mgmt, NON_DEFAULT_MANAGEMENT_KEY), ) ykman_cli( "piv", "access", "change-management-key", - input=old_new_new(NON_DEFAULT_MANAGEMENT_KEY, DEFAULT_MANAGEMENT_KEY), + input=old_new_new(NON_DEFAULT_MANAGEMENT_KEY, keys.mgmt), ) assert "Generated" not in output - def test_change_management_key_new_key_conflicts_with_generate(self, ykman_cli): + def test_change_management_key_new_key_conflicts_with_generate( + self, ykman_cli, keys + ): with pytest.raises(SystemExit): ykman_cli( "piv", "access", "change-management-key", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, "-n", NON_DEFAULT_MANAGEMENT_KEY, "-g", diff --git a/tests/device/cli/piv/test_misc.py b/tests/device/cli/piv/test_misc.py index 1587cf02..8ab44910 100644 --- a/tests/device/cli/piv/test_misc.py +++ b/tests/device/cli/piv/test_misc.py @@ -2,9 +2,6 @@ import pytest -DEFAULT_MANAGEMENT_KEY = "010203040506070801020304050607080102030405060708" - - class TestMisc: def setUp(self, ykman_cli): ykman_cli("piv", "reset", "-f") @@ -17,7 +14,7 @@ def test_reset(self, ykman_cli): output = ykman_cli("piv", "reset", "-f").output assert "Success!" in output - def test_export_invalid_certificate_fails(self, ykman_cli): + def test_export_invalid_certificate_fails(self, ykman_cli, keys): ykman_cli( "piv", "objects", @@ -25,7 +22,7 @@ def test_export_invalid_certificate_fails(self, ykman_cli): hex(OBJECT_ID.AUTHENTICATION), "-", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, input="This is not a cert", ) @@ -34,7 +31,7 @@ def test_export_invalid_certificate_fails(self, ykman_cli): "piv", "certificates", "export", hex(OBJECT_ID.AUTHENTICATION), "-" ) - def test_info_with_invalid_certificate_does_not_crash(self, ykman_cli): + def test_info_with_invalid_certificate_does_not_crash(self, ykman_cli, keys): ykman_cli( "piv", "objects", @@ -42,7 +39,7 @@ def test_info_with_invalid_certificate_does_not_crash(self, ykman_cli): hex(OBJECT_ID.AUTHENTICATION), "-", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, input="This is not a cert", ) ykman_cli("piv", "info") diff --git a/tests/device/cli/piv/test_pin_puk.py b/tests/device/cli/piv/test_pin_puk.py index fe006622..a61712db 100644 --- a/tests/device/cli/piv/test_pin_puk.py +++ b/tests/device/cli/piv/test_pin_puk.py @@ -1,73 +1,67 @@ from .util import ( old_new_new, - DEFAULT_PIN, NON_DEFAULT_PIN, - DEFAULT_PUK, NON_DEFAULT_PUK, - DEFAULT_MANAGEMENT_KEY, ) from ykman.piv import OBJECT_ID_PIVMAN_DATA, PivmanData +from yubikit.management import CAPABILITY import pytest import re class TestPin: - def test_change_pin(self, ykman_cli): - ykman_cli( - "piv", "access", "change-pin", "-P", DEFAULT_PIN, "-n", NON_DEFAULT_PIN - ) - ykman_cli( - "piv", "access", "change-pin", "-P", NON_DEFAULT_PIN, "-n", DEFAULT_PIN - ) + def test_change_pin(self, ykman_cli, keys): + ykman_cli("piv", "access", "change-pin", "-P", keys.pin, "-n", NON_DEFAULT_PIN) + ykman_cli("piv", "access", "change-pin", "-P", NON_DEFAULT_PIN, "-n", keys.pin) - def test_change_pin_prompt(self, ykman_cli): + def test_change_pin_prompt(self, ykman_cli, keys): ykman_cli( "piv", "access", "change-pin", - input=old_new_new(DEFAULT_PIN, NON_DEFAULT_PIN), + input=old_new_new(keys.pin, NON_DEFAULT_PIN), ) ykman_cli( "piv", "access", "change-pin", - input=old_new_new(NON_DEFAULT_PIN, DEFAULT_PIN), + input=old_new_new(NON_DEFAULT_PIN, keys.pin), ) class TestPuk: - def test_change_puk(self, ykman_cli): + def test_change_puk(self, ykman_cli, keys): o1 = ykman_cli( - "piv", "access", "change-puk", "-p", DEFAULT_PUK, "-n", NON_DEFAULT_PUK + "piv", "access", "change-puk", "-p", keys.puk, "-n", NON_DEFAULT_PUK ).output assert "New PUK set." in o1 o2 = ykman_cli( - "piv", "access", "change-puk", "-p", NON_DEFAULT_PUK, "-n", DEFAULT_PUK + "piv", "access", "change-puk", "-p", NON_DEFAULT_PUK, "-n", keys.puk ).output assert "New PUK set." in o2 with pytest.raises(SystemExit): ykman_cli( - "piv", "access", "change-puk", "-p", NON_DEFAULT_PUK, "-n", DEFAULT_PUK + "piv", "access", "change-puk", "-p", NON_DEFAULT_PUK, "-n", keys.puk ) - def test_change_puk_prompt(self, ykman_cli): + def test_change_puk_prompt(self, ykman_cli, keys): ykman_cli( "piv", "access", "change-puk", - input=old_new_new(DEFAULT_PUK, NON_DEFAULT_PUK), + input=old_new_new(keys.puk, NON_DEFAULT_PUK), ) ykman_cli( "piv", "access", "change-puk", - input=old_new_new(NON_DEFAULT_PUK, DEFAULT_PUK), + input=old_new_new(NON_DEFAULT_PUK, keys.puk), ) - def test_unblock_pin(self, ykman_cli): + def test_unblock_pin(self, ykman_cli, keys): for _ in range(3): with pytest.raises(SystemExit): ykman_cli( @@ -77,7 +71,7 @@ def test_unblock_pin(self, ykman_cli): "-P", NON_DEFAULT_PIN, "-n", - DEFAULT_PIN, + keys.pin, ) o = ykman_cli("piv", "info").output @@ -85,11 +79,11 @@ def test_unblock_pin(self, ykman_cli): with pytest.raises(SystemExit): ykman_cli( - "piv", "access", "change-pin", "-p", DEFAULT_PIN, "-n", NON_DEFAULT_PIN + "piv", "access", "change-pin", "-p", keys.pin, "-n", NON_DEFAULT_PIN ) o = ykman_cli( - "piv", "access", "unblock-pin", "-p", DEFAULT_PUK, "-n", DEFAULT_PIN + "piv", "access", "unblock-pin", "-p", keys.puk, "-n", keys.pin ).output assert "PIN unblocked" in o o = ykman_cli("piv", "info").output @@ -97,14 +91,14 @@ def test_unblock_pin(self, ykman_cli): class TestSetRetries: - def test_set_retries(self, ykman_cli, version): + def test_set_retries(self, ykman_cli, default_keys, version): ykman_cli( "piv", "access", "set-retries", "5", "6", - input=f"{DEFAULT_MANAGEMENT_KEY}\n{DEFAULT_PIN}\ny\n", + input=f"{default_keys.mgmt}\n{default_keys.pin}\ny\n", ) o = ykman_cli("piv", "info").output @@ -112,7 +106,10 @@ def test_set_retries(self, ykman_cli, version): if version >= (5, 3): assert re.search(r"PUK tries remaining:\s+6/6", o) - def test_set_retries_clears_puk_blocked(self, ykman_cli): + def test_set_retries_clears_puk_blocked(self, ykman_cli, keys, info): + if CAPABILITY.PIV in info.fips_capable: + pytest.skip("YubiKey FIPS") + for _ in range(3): with pytest.raises(SystemExit): ykman_cli( @@ -122,7 +119,7 @@ def test_set_retries_clears_puk_blocked(self, ykman_cli): "-p", NON_DEFAULT_PUK, "-n", - DEFAULT_PUK, + keys.puk, ) pivman = PivmanData() @@ -135,7 +132,7 @@ def test_set_retries_clears_puk_blocked(self, ykman_cli): hex(OBJECT_ID_PIVMAN_DATA), "-", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, input=pivman.get_bytes(), ) @@ -148,7 +145,7 @@ def test_set_retries_clears_puk_blocked(self, ykman_cli): "set-retries", "3", "3", - input=f"{DEFAULT_MANAGEMENT_KEY}\n{DEFAULT_PIN}\ny\n", + input=f"{keys.mgmt}\n{keys.pin}\ny\n", ) o = ykman_cli("piv", "info").output diff --git a/tests/device/cli/piv/test_read_write_object.py b/tests/device/cli/piv/test_read_write_object.py index 340a32b5..f52d8e3e 100644 --- a/tests/device/cli/piv/test_read_write_object.py +++ b/tests/device/cli/piv/test_read_write_object.py @@ -7,11 +7,8 @@ import pytest -DEFAULT_MANAGEMENT_KEY = "010203040506070801020304050607080102030405060708" - - class TestReadWriteObject: - def test_write_read_preserves_ansi_escapes(self, ykman_cli): + def test_write_read_preserves_ansi_escapes(self, ykman_cli, keys): red = b"\x00\x1b[31m" blue = b"\x00\x1b[34m" reset = b"\x00\x1b[0m" @@ -31,7 +28,7 @@ def test_write_read_preserves_ansi_escapes(self, ykman_cli): "objects", "import", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, "0x5f0001", "-", input=data, @@ -41,7 +38,7 @@ def test_write_read_preserves_ansi_escapes(self, ykman_cli): ).stdout_bytes assert data == output_data - def test_read_write_read_is_noop(self, ykman_cli): + def test_read_write_read_is_noop(self, ykman_cli, keys): data = os.urandom(32) ykman_cli( @@ -51,7 +48,7 @@ def test_read_write_read_is_noop(self, ykman_cli): hex(OBJECT_ID.AUTHENTICATION), "-", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, input=data, ) @@ -67,7 +64,7 @@ def test_read_write_read_is_noop(self, ykman_cli): hex(OBJECT_ID.AUTHENTICATION), "-", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, input=output1, ) @@ -76,7 +73,7 @@ def test_read_write_read_is_noop(self, ykman_cli): ).stdout_bytes assert output2 == data - def test_read_write_certificate_as_object(self, ykman_cli): + def test_read_write_certificate_as_object(self, ykman_cli, keys): with pytest.raises(SystemExit): ykman_cli("piv", "objects", "export", hex(OBJECT_ID.AUTHENTICATION), "-") @@ -92,7 +89,7 @@ def test_read_write_certificate_as_object(self, ykman_cli): hex(OBJECT_ID.AUTHENTICATION), "-", "-m", - DEFAULT_MANAGEMENT_KEY, + keys.mgmt, input=input_tlv, ) diff --git a/tests/device/cli/piv/util.py b/tests/device/cli/piv/util.py index 5d9d8cf8..54dfd3fc 100644 --- a/tests/device/cli/piv/util.py +++ b/tests/device/cli/piv/util.py @@ -1,7 +1,7 @@ DEFAULT_PIN = "123456" -NON_DEFAULT_PIN = "654321" +NON_DEFAULT_PIN = "12341235" DEFAULT_PUK = "12345678" -NON_DEFAULT_PUK = "87654321" +NON_DEFAULT_PUK = "12341236" DEFAULT_MANAGEMENT_KEY = "010203040506070801020304050607080102030405060708" NON_DEFAULT_MANAGEMENT_KEY = "010103040506070801020304050607080102030405060708" diff --git a/tests/device/cli/test_hsmauth.py b/tests/device/cli/test_hsmauth.py index 7887fd1a..7def72e6 100644 --- a/tests/device/cli/test_hsmauth.py +++ b/tests/device/cli/test_hsmauth.py @@ -22,7 +22,28 @@ import struct DEFAULT_MANAGEMENT_KEY = "00000000000000000000000000000000" -NON_DEFAULT_MANAGEMENT_KEY = "11111111111111111111111111111111" +NON_DEFAULT_MANAGEMENT_KEY = "11111111111111111111111111111112" + + +# Test both password and key +@pytest.fixture(params=[DEFAULT_MANAGEMENT_KEY, "p4ssw0rd"]) +def management_key(request, ykman_cli, info): + key = request.param + if key == DEFAULT_MANAGEMENT_KEY and CAPABILITY.HSMAUTH in info.fips_capable: + key = "00000000000000000000000000000001" + + if key != DEFAULT_MANAGEMENT_KEY: + ykman_cli( + "hsmauth", + "access", + "change-management-password", + "-m", + "", + "-n", + key, + ) + + yield key def generate_pem_eccp256_keypair(): @@ -99,7 +120,7 @@ def verify_credential_password(self, ykman_cli, credential_password, label): # Try to calculate session keys using credential password ykman_cli("apdu", "-a", "hsmauth", apdu) - def test_import_credential_symmetric(self, ykman_cli): + def test_import_credential_symmetric(self, ykman_cli, management_key): ykman_cli( "hsmauth", "credentials", @@ -112,13 +133,13 @@ def test_import_credential_symmetric(self, ykman_cli): "-M", os.urandom(16).hex(), "-m", - DEFAULT_MANAGEMENT_KEY, + management_key, ) self.verify_credential_password(ykman_cli, "123456", "test-name-sym") creds = ykman_cli("hsmauth", "credentials", "list").output assert "test-name-sym" in creds - def test_import_credential_symmetric_generate(self, ykman_cli): + def test_import_credential_symmetric_generate(self, ykman_cli, management_key): output = ykman_cli( "hsmauth", "credentials", @@ -128,12 +149,12 @@ def test_import_credential_symmetric_generate(self, ykman_cli): "123456", "-g", "-m", - DEFAULT_MANAGEMENT_KEY, + management_key, ).output self.verify_credential_password(ykman_cli, "123456", "test-name-sym-gen") assert "Generated ENC and MAC keys" in output - def test_import_credential_symmetric_derived(self, ykman_cli): + def test_import_credential_symmetric_derived(self, ykman_cli, management_key): ykman_cli( "hsmauth", "credentials", @@ -143,13 +164,15 @@ def test_import_credential_symmetric_derived(self, ykman_cli): "123456", "-d", "password", + "-m", + management_key, ) self.verify_credential_password(ykman_cli, "123456", "test-name-sym-derived") creds = ykman_cli("hsmauth", "credentials", "list").output assert "test-name-sym-derived" in creds @condition.min_version(5, 6) - def test_import_credential_asymmetric(self, ykman_cli): + def test_import_credential_asymmetric(self, ykman_cli, management_key): pair = generate_pem_eccp256_keypair() ykman_cli( "hsmauth", @@ -159,7 +182,7 @@ def test_import_credential_asymmetric(self, ykman_cli): "-c", "123456", "-m", - DEFAULT_MANAGEMENT_KEY, + management_key, "-", input=pair[0], ) @@ -172,7 +195,7 @@ def test_import_credential_asymmetric(self, ykman_cli): assert pair[1] == public_key_exported @condition.min_version(5, 6) - def test_generate_credential_asymmetric(self, ykman_cli): + def test_generate_credential_asymmetric(self, ykman_cli, management_key): ykman_cli( "hsmauth", "credentials", @@ -181,13 +204,13 @@ def test_generate_credential_asymmetric(self, ykman_cli): "-c", "123456", "-m", - DEFAULT_MANAGEMENT_KEY, + management_key, ) creds = ykman_cli("hsmauth", "credentials", "list").output assert "test-name-asym-generated" in creds - def test_import_credential_touch_required(self, ykman_cli): + def test_import_credential_touch_required(self, ykman_cli, management_key): ykman_cli( "hsmauth", "credentials", @@ -198,6 +221,8 @@ def test_import_credential_touch_required(self, ykman_cli): "-d", "password", "-t", + "-m", + management_key, ) creds = ykman_cli("hsmauth", "credentials", "list").output @@ -205,7 +230,9 @@ def test_import_credential_touch_required(self, ykman_cli): assert "test-name-touch" in creds @condition.min_version(5, 6) - def test_export_public_key_to_file(self, ykman_cli, eccp256_keypair, tmp_file): + def test_export_public_key_to_file( + self, ykman_cli, management_key, eccp256_keypair, tmp_file + ): private_key_file, public_key = eccp256_keypair ykman_cli( "hsmauth", @@ -215,7 +242,7 @@ def test_export_public_key_to_file(self, ykman_cli, eccp256_keypair, tmp_file): "-c", "123456", "-m", - DEFAULT_MANAGEMENT_KEY, + management_key, private_key_file, ) @@ -231,7 +258,7 @@ def test_export_public_key_to_file(self, ykman_cli, eccp256_keypair, tmp_file): assert public_key_from_file == public_key @condition.min_version(5, 6) - def test_export_public_key_symmetric_credential(self, ykman_cli): + def test_export_public_key_symmetric_credential(self, ykman_cli, management_key): ykman_cli( "hsmauth", "credentials", @@ -242,13 +269,13 @@ def test_export_public_key_symmetric_credential(self, ykman_cli): "-d", "password", "-m", - DEFAULT_MANAGEMENT_KEY, + management_key, ) with pytest.raises(SystemExit): ykman_cli("hsmauth", "credentials", "export", "test-name-sym") - def test_delete_credential(self, ykman_cli): + def test_delete_credential(self, ykman_cli, management_key): ykman_cli( "hsmauth", "credentials", @@ -259,23 +286,25 @@ def test_delete_credential(self, ykman_cli): "-d", "password", "-m", - DEFAULT_MANAGEMENT_KEY, + management_key, ) old_creds = ykman_cli("hsmauth", "credentials", "list").output assert "delete-me" in old_creds - ykman_cli("hsmauth", "credentials", "delete", "delete-me", "-f") + ykman_cli( + "hsmauth", "credentials", "delete", "delete-me", "-f", "-m", management_key + ) new_creds = ykman_cli("hsmauth", "credentials", "list").output assert "delete-me" not in new_creds class TestManagementKey: - def test_change_management_key(self, ykman_cli): + def test_change_management_key(self, ykman_cli, management_key): ykman_cli( "hsmauth", "access", - "change-management-key", + "change-management-password", "-m", - DEFAULT_MANAGEMENT_KEY, + management_key, "-n", NON_DEFAULT_MANAGEMENT_KEY, ) @@ -285,31 +314,34 @@ def test_change_management_key(self, ykman_cli): ykman_cli( "hsmauth", "access", - "change-management-key", + "change-management-password", "-m", - DEFAULT_MANAGEMENT_KEY, + management_key, "-n", - DEFAULT_MANAGEMENT_KEY, + management_key, ) # Should succeed ykman_cli( "hsmauth", "access", - "change-management-key", + "change-management-password", "-m", NON_DEFAULT_MANAGEMENT_KEY, "-n", - DEFAULT_MANAGEMENT_KEY, + management_key, ) - def test_change_management_key_generate(self, ykman_cli): + def test_change_management_key_generate(self, ykman_cli, management_key): + if len(management_key) != 32: + pytest.skip("string management key") + output = ykman_cli( "hsmauth", "access", "change-management-key", "-m", - DEFAULT_MANAGEMENT_KEY, + management_key, "-g", ).output diff --git a/tests/device/cli/test_oath.py b/tests/device/cli/test_oath.py index dc5fbc1b..1dfc4451 100644 --- a/tests/device/cli/test_oath.py +++ b/tests/device/cli/test_oath.py @@ -39,6 +39,28 @@ def preconditions(ykman_cli): ykman_cli("oath", "reset", "-f") +@pytest.fixture() +def password(info): + if CAPABILITY.OATH in info.fips_capable: + yield PASSWORD + else: + yield None + + +@pytest.fixture() +def accounts_cli(ykman_cli, password): + if password: + ykman_cli("oath", "access", "change", "-n", password) + + def fn(*args, **kwargs): + argv = ["oath", "accounts", *args] + if password: + argv += ["-p", password] + return ykman_cli(*argv, **kwargs) + + yield fn + + class TestOATH: def test_oath_info(self, ykman_cli): output = ykman_cli("oath", "info").output @@ -49,65 +71,63 @@ def test_info_does_not_indicate_fips_mode_for_non_fips_key(self, ykman_cli): info = ykman_cli("oath", "info").output assert "FIPS:" not in info - def test_oath_add_credential(self, ykman_cli): - ykman_cli("oath", "accounts", "add", "test-name", "abba") - creds = ykman_cli("oath", "accounts", "list").output + def test_oath_add_credential(self, accounts_cli, password): + accounts_cli("add", "test-name", "abba") + creds = accounts_cli("list").output assert "test-name" in creds - def test_oath_add_credential_prompt(self, ykman_cli): - ykman_cli("oath", "accounts", "add", "test-name-2", input="abba") - creds = ykman_cli("oath", "accounts", "list").output + def test_oath_add_credential_prompt(self, accounts_cli): + accounts_cli("add", "test-name-2", input="abba") + creds = accounts_cli("list").output assert "test-name-2" in creds - def test_oath_add_credential_with_space(self, ykman_cli): - ykman_cli("oath", "accounts", "add", "test-name-space", "ab ba") - creds = ykman_cli("oath", "accounts", "list").output + def test_oath_add_credential_with_space(self, accounts_cli): + accounts_cli("add", "test-name-space", "ab ba") + creds = accounts_cli("list").output assert "test-name-space" in creds - def test_oath_hidden_cred(self, ykman_cli): - ykman_cli("oath", "accounts", "add", "_hidden:name", "abba") - creds = ykman_cli("oath", "accounts", "code").output + def test_oath_hidden_cred(self, accounts_cli): + accounts_cli("add", "_hidden:name", "abba") + creds = accounts_cli("code").output assert "_hidden:name" not in creds - creds = ykman_cli("oath", "accounts", "code", "-H").output + creds = accounts_cli("code", "-H").output assert "_hidden:name" in creds - def test_oath_add_uri_hotp(self, ykman_cli): - ykman_cli("oath", "accounts", "uri", URI_HOTP_EXAMPLE) - creds = ykman_cli("oath", "accounts", "list").output + def test_oath_add_uri_hotp(self, accounts_cli): + accounts_cli("uri", URI_HOTP_EXAMPLE) + creds = accounts_cli("list").output assert "Example:demo" in creds - def test_oath_add_uri_totp(self, ykman_cli): - ykman_cli("oath", "accounts", "uri", URI_TOTP_EXAMPLE) - creds = ykman_cli("oath", "accounts", "list").output + def test_oath_add_uri_totp(self, accounts_cli): + accounts_cli("uri", URI_TOTP_EXAMPLE) + creds = accounts_cli("list").output assert "john.doe" in creds - def test_oath_add_uri_totp_extra_parameter(self, ykman_cli): - ykman_cli("oath", "accounts", "uri", URI_TOTP_EXAMPLE_EXTRA_PARAMETER) - creds = ykman_cli("oath", "accounts", "list").output + def test_oath_add_uri_totp_extra_parameter(self, accounts_cli): + accounts_cli("uri", URI_TOTP_EXAMPLE_EXTRA_PARAMETER) + creds = accounts_cli("list").output assert "john.doe.extra" in creds - def test_oath_add_uri_totp_prompt(self, ykman_cli): - ykman_cli("oath", "accounts", "uri", input=URI_TOTP_EXAMPLE_B) - creds = ykman_cli("oath", "accounts", "list").output + def test_oath_add_uri_totp_prompt(self, accounts_cli): + accounts_cli("uri", input=URI_TOTP_EXAMPLE_B) + creds = accounts_cli("list").output assert "john.doe" in creds - def test_oath_code(self, ykman_cli): - ykman_cli("oath", "accounts", "add", "test-name2", "abba") - creds = ykman_cli("oath", "accounts", "code").output + def test_oath_code(self, accounts_cli): + accounts_cli("add", "test-name2", "abba") + creds = accounts_cli("code").output assert "test-name2" in creds - def test_oath_code_query_single(self, ykman_cli): - ykman_cli("oath", "accounts", "add", "query-me", "abba") - creds = ykman_cli("oath", "accounts", "code", "query-me").output + def test_oath_code_query_single(self, accounts_cli): + accounts_cli("add", "query-me", "abba") + creds = accounts_cli("code", "query-me").output assert "query-me" in creds - def test_oath_code_query_multiple(self, ykman_cli): - ykman_cli("oath", "accounts", "add", "foo", "abba") - ykman_cli("oath", "accounts", "add", "query-me", "abba") - ykman_cli("oath", "accounts", "add", "bar", "abba") - lines = ( - ykman_cli("oath", "accounts", "code", "query").output.strip().splitlines() - ) + def test_oath_code_query_multiple(self, accounts_cli): + accounts_cli("add", "foo", "abba") + accounts_cli("add", "query-me", "abba") + accounts_cli("add", "bar", "abba") + lines = accounts_cli("code", "query").output.strip().splitlines() assert len(lines) == 1 assert "query-me" in lines[0] @@ -115,10 +135,8 @@ def test_oath_reset(self, ykman_cli): output = ykman_cli("oath", "reset", "-f").output assert "Success! All OATH accounts have been deleted from the YubiKey" in output - def test_oath_hotp_vectors_6(self, ykman_cli): - ykman_cli( - "oath", - "accounts", + def test_oath_hotp_vectors_6(self, accounts_cli): + accounts_cli( "add", "-o", "HOTP", @@ -126,13 +144,11 @@ def test_oath_hotp_vectors_6(self, ykman_cli): b32encode(b"12345678901234567890").decode(), ) for code in ["755224", "287082", "359152", "969429", "338314"]: - words = ykman_cli("oath", "accounts", "code", "testvector").output.split() + words = accounts_cli("code", "testvector").output.split() assert code in words - def test_oath_hotp_vectors_8(self, ykman_cli): - ykman_cli( - "oath", - "accounts", + def test_oath_hotp_vectors_8(self, accounts_cli): + accounts_cli( "add", "-o", "HOTP", @@ -142,44 +158,42 @@ def test_oath_hotp_vectors_8(self, ykman_cli): b32encode(b"12345678901234567890").decode(), ) for code in ["84755224", "94287082", "37359152", "26969429", "40338314"]: - words = ykman_cli("oath", "accounts", "code", "testvector8").output.split() + words = accounts_cli("code", "testvector8").output.split() assert code in words - def test_oath_hotp_code(self, ykman_cli): - ykman_cli("oath", "accounts", "add", "-o", "HOTP", "hotp-cred", "abba") - words = ykman_cli("oath", "accounts", "code", "hotp-cred").output.split() + def test_oath_hotp_code(self, accounts_cli): + accounts_cli("add", "-o", "HOTP", "hotp-cred", "abba") + words = accounts_cli("code", "hotp-cred").output.split() assert "659165" in words - def test_oath_hotp_code_single(self, ykman_cli): - ykman_cli("oath", "accounts", "add", "-o", "HOTP", "hotp-cred", "abba") - words = ykman_cli( - "oath", "accounts", "code", "hotp-cred", "--single" - ).output.split() + def test_oath_hotp_code_single(self, accounts_cli): + accounts_cli("add", "-o", "HOTP", "hotp-cred", "abba") + words = accounts_cli("code", "hotp-cred", "--single").output.split() assert "659165" in words - def test_oath_totp_steam_code(self, ykman_cli): - ykman_cli("oath", "accounts", "add", "Steam:steam-cred", "abba") - cred = ykman_cli("oath", "accounts", "code", "steam-cred").output.strip() + def test_oath_totp_steam_code(self, accounts_cli): + accounts_cli("add", "Steam:steam-cred", "abba") + cred = accounts_cli("code", "steam-cred").output.strip() code = cred.split()[-1] assert 5 == len(code), f"cred wrong length: {code!r}" assert all( c in STEAM_CHAR_TABLE for c in code ), f"{code!r} contains non-steam characters" - def test_oath_totp_steam_code_single(self, ykman_cli): - ykman_cli("oath", "accounts", "add", "Steam:steam-cred", "abba") - code = ykman_cli("oath", "accounts", "code", "-s", "steam-cred").output.strip() + def test_oath_totp_steam_code_single(self, accounts_cli): + accounts_cli("add", "Steam:steam-cred", "abba") + code = accounts_cli("code", "-s", "steam-cred").output.strip() assert 5 == len(code), f"cred wrong length: {code!r}" assert all( c in STEAM_CHAR_TABLE for c in code ), f"{code!r} contains non-steam characters" - def test_oath_code_output_no_touch(self, ykman_cli): - ykman_cli("oath", "accounts", "add", "TOTP:normal", "aaaa") - ykman_cli("oath", "accounts", "add", "Steam:normal", "aaba") - ykman_cli("oath", "accounts", "add", "-o", "HOTP", "HOTP:normal", "abaa") + def test_oath_code_output_no_touch(self, accounts_cli): + accounts_cli("add", "TOTP:normal", "aaaa") + accounts_cli("add", "Steam:normal", "aaba") + accounts_cli("add", "-o", "HOTP", "HOTP:normal", "abaa") - lines = ykman_cli("oath", "accounts", "code").output.strip().splitlines() + lines = accounts_cli("code").output.strip().splitlines() entries = {line.split()[0]: line for line in lines} assert "HOTP Account" in entries["HOTP:normal"] @@ -194,14 +208,14 @@ def test_oath_code_output_no_touch(self, ykman_cli): int(code) @condition.min_version(4) - def test_oath_code_output(self, ykman_cli): - ykman_cli("oath", "accounts", "add", "TOTP:normal", "aaaa") - ykman_cli("oath", "accounts", "add", "--touch", "TOTP:touch", "aaab") - ykman_cli("oath", "accounts", "add", "Steam:normal", "aaba") - ykman_cli("oath", "accounts", "add", "--touch", "Steam:touch", "aabb") - ykman_cli("oath", "accounts", "add", "-o", "HOTP", "HOTP:normal", "abaa") - - lines = ykman_cli("oath", "accounts", "code").output.strip().splitlines() + def test_oath_code_output(self, accounts_cli): + accounts_cli("add", "TOTP:normal", "aaaa") + accounts_cli("add", "--touch", "TOTP:touch", "aaab") + accounts_cli("add", "Steam:normal", "aaba") + accounts_cli("add", "--touch", "Steam:touch", "aabb") + accounts_cli("add", "-o", "HOTP", "HOTP:normal", "abaa") + + lines = accounts_cli("code").output.strip().splitlines() entries = {line.split()[0]: line for line in lines} assert "Requires Touch" in entries["TOTP:touch"] assert "Requires Touch" in entries["Steam:touch"] @@ -218,50 +232,48 @@ def test_oath_code_output(self, ykman_cli): int(code) @condition.min_version(4) - def test_oath_totp_steam_touch_not_in_code_output(self, ykman_cli): - ykman_cli("oath", "accounts", "add", "--touch", "Steam:steam-cred", "abba") - ykman_cli("oath", "accounts", "add", "TOTP:totp-cred", "abba") - lines = ykman_cli("oath", "accounts", "code").output.strip().splitlines() + def test_oath_totp_steam_touch_not_in_code_output(self, accounts_cli): + accounts_cli("add", "--touch", "Steam:steam-cred", "abba") + accounts_cli("add", "TOTP:totp-cred", "abba") + lines = accounts_cli("code").output.strip().splitlines() assert "Requires Touch" in lines[0] - def test_oath_delete(self, ykman_cli): - ykman_cli("oath", "accounts", "add", "delete-me", "abba") - ykman_cli("oath", "accounts", "delete", "delete-me", "-f") - assert "delete-me", ykman_cli("oath", "accounts" not in "list") + def test_oath_delete(self, accounts_cli): + accounts_cli("add", "delete-me", "abba") + accounts_cli("delete", "delete-me", "-f") + assert "delete-me" not in accounts_cli("list").output - def test_oath_unicode(self, ykman_cli): - ykman_cli("oath", "accounts", "add", "😃", "abba") - ykman_cli("oath", "accounts", "code") - ykman_cli("oath", "accounts", "list") - ykman_cli("oath", "accounts", "delete", "😃", "-f") + def test_oath_unicode(self, accounts_cli): + accounts_cli("add", "😃", "abba") + accounts_cli("code") + accounts_cli("list") + accounts_cli("delete", "😃", "-f") @condition.yk4_fips(False) @condition.min_version(4, 3, 1) - def test_oath_sha512(self, ykman_cli): - ykman_cli("oath", "accounts", "add", "abba", "abba", "--algorithm", "SHA512") - ykman_cli("oath", "accounts", "delete", "abba", "-f") + def test_oath_sha512(self, accounts_cli): + accounts_cli("add", "abba", "abba", "--algorithm", "SHA512") + accounts_cli("delete", "abba", "-f") # NEO credential capacity may vary based on configuration @condition.min_version(4) - def test_add_max_creds(self, ykman_cli, version): + def test_add_max_creds(self, accounts_cli, version): n_creds = 32 if version < (5, 7, 0) else 64 for i in range(n_creds): - ykman_cli("oath", "accounts", "add", "test" + str(i), "abba") - output = ykman_cli("oath", "accounts", "list").output + accounts_cli("add", "test" + str(i), "abba") + output = accounts_cli("list").output lines = output.strip().split("\n") assert len(lines) == i + 1 with pytest.raises(SystemExit): - ykman_cli("oath", "accounts", "add", "testx", "abba") + accounts_cli("add", "testx", "abba") @condition.min_version(5, 3, 1) - def test_rename(self, ykman_cli): - ykman_cli("oath", "accounts", "uri", URI_TOTP_EXAMPLE) - ykman_cli( - "oath", "accounts", "rename", "john.doe", "Example:user@example.com", "-f" - ) + def test_rename(self, accounts_cli): + accounts_cli("uri", URI_TOTP_EXAMPLE) + accounts_cli("rename", "john.doe", "Example:user@example.com", "-f") - creds = ykman_cli("oath", "accounts", "list").output + creds = accounts_cli("list").output assert "john.doe" not in creds assert "Example:user@example.com" in creds diff --git a/tests/device/cli/test_openpgp.py b/tests/device/cli/test_openpgp.py index 93dfcd3b..e59ce4d3 100644 --- a/tests/device/cli/test_openpgp.py +++ b/tests/device/cli/test_openpgp.py @@ -3,9 +3,11 @@ import pytest DEFAULT_PIN = "123456" -NON_DEFAULT_PIN = "654321" +NON_DEFAULT_PIN = "12345679" +NON_DEFAULT_PIN_2 = "12345670" DEFAULT_ADMIN_PIN = "12345678" -NON_DEFAULT_ADMIN_PIN = "87654321" +NON_DEFAULT_ADMIN_PIN = "12345670" +NON_DEFAULT_ADMIN_PIN_2 = "12345679" def old_new_new(old, new): @@ -34,7 +36,13 @@ def test_change_pin(self, ykman_cli): "openpgp", "access", "change-pin", "-P", DEFAULT_PIN, "-n", NON_DEFAULT_PIN ) ykman_cli( - "openpgp", "access", "change-pin", "-P", NON_DEFAULT_PIN, "-n", DEFAULT_PIN + "openpgp", + "access", + "change-pin", + "-P", + NON_DEFAULT_PIN, + "-n", + NON_DEFAULT_PIN_2, ) def test_change_pin_prompt(self, ykman_cli): @@ -48,7 +56,7 @@ def test_change_pin_prompt(self, ykman_cli): "openpgp", "access", "change-pin", - input=old_new_new(NON_DEFAULT_PIN, DEFAULT_PIN), + input=old_new_new(NON_DEFAULT_PIN, NON_DEFAULT_PIN_2), ) @@ -70,7 +78,7 @@ def test_change_admin_pin(self, ykman_cli): "-a", NON_DEFAULT_ADMIN_PIN, "-n", - DEFAULT_ADMIN_PIN, + NON_DEFAULT_ADMIN_PIN_2, ) def test_change_pin_prompt(self, ykman_cli): @@ -84,18 +92,24 @@ def test_change_pin_prompt(self, ykman_cli): "openpgp", "access", "change-admin-pin", - input=old_new_new(NON_DEFAULT_ADMIN_PIN, DEFAULT_ADMIN_PIN), + input=old_new_new(NON_DEFAULT_ADMIN_PIN, NON_DEFAULT_ADMIN_PIN_2), ) class TestResetPin: def ensure_pin_changed(self, ykman_cli): ykman_cli( - "openpgp", "access", "change-pin", "-P", NON_DEFAULT_PIN, "-n", DEFAULT_PIN + "openpgp", + "access", + "change-pin", + "-P", + NON_DEFAULT_PIN, + "-n", + NON_DEFAULT_PIN_2, ) def test_set_and_use_reset_code(self, ykman_cli): - reset_code = "12345678" + reset_code = "00112233" ykman_cli( "openpgp", @@ -120,7 +134,7 @@ def test_set_and_use_reset_code(self, ykman_cli): self.ensure_pin_changed(ykman_cli) def test_set_and_use_reset_code_prompt(self, ykman_cli): - reset_code = "87654321" + reset_code = "11223344" ykman_cli( "openpgp", @@ -137,7 +151,13 @@ def test_set_and_use_reset_code_prompt(self, ykman_cli): ) ykman_cli( - "openpgp", "access", "change-pin", "-P", NON_DEFAULT_PIN, "-n", DEFAULT_PIN + "openpgp", + "access", + "change-pin", + "-P", + NON_DEFAULT_PIN, + "-n", + NON_DEFAULT_PIN_2, ) def test_unblock_pin_with_admin_pin(self, ykman_cli): diff --git a/tests/device/cli/test_otp.py b/tests/device/cli/test_otp.py index d37b552e..57e1cb7d 100644 --- a/tests/device/cli/test_otp.py +++ b/tests/device/cli/test_otp.py @@ -35,6 +35,11 @@ import pytest +def no_pin_complexity(info): + """PIN complexity enabled""" + return not info.pin_complexity + + @pytest.fixture(autouse=True) @condition.capability(CAPABILITY.OTP) def ensure_otp(): @@ -47,6 +52,7 @@ def test_ykman_otp_info(self, ykman_cli): assert "Slot 1:" in info assert "Slot 2:" in info + @condition.check(no_pin_complexity) def test_ykman_swap_slots(self, ykman_cli): info = ykman_cli("otp", "info").output if "programmed" not in info: @@ -65,6 +71,7 @@ def test_ykman_otp_info_does_not_indicate_fips_mode_for_non_fips_key( class TestReclaimTimeout: + @condition.check(no_pin_complexity) def test_update_after_reclaim(self, ykman_cli): info = ykman_cli("otp", "info").output if "programmed" not in info: @@ -78,6 +85,7 @@ def test_update_after_reclaim(self, ykman_cli): class TestSlotStaticPassword: @pytest.fixture(autouse=True) + @condition.check(no_pin_complexity) def delete_slot(self, ykman_cli): try: ykman_cli("otp", "delete", "2", "-f") @@ -145,6 +153,7 @@ def test_overwrite_prompt(self, ykman_cli): class TestSlotProgramming: @pytest.fixture(autouse=True) + @condition.check(no_pin_complexity) def delete_slot(self, ykman_cli): try: ykman_cli("otp", "delete", "2", "-f") @@ -567,6 +576,7 @@ def _check_slot_2_does_not_have_access_code(self, ykman_cli): class TestSlotCalculate: @pytest.fixture(autouse=True) + @condition.check(no_pin_complexity) def delete_slot(self, ykman_cli): try: ykman_cli("otp", "delete", "2", "-f") diff --git a/tests/device/test_hsmauth.py b/tests/device/test_hsmauth.py index fb4758bd..9a20f11b 100644 --- a/tests/device/test_hsmauth.py +++ b/tests/device/test_hsmauth.py @@ -18,7 +18,7 @@ import os DEFAULT_MANAGEMENT_KEY = bytes.fromhex("00000000000000000000000000000000") -NON_DEFAULT_MANAGEMENT_KEY = bytes.fromhex("11111111111111111111111111111111") +NON_DEFAULT_MANAGEMENT_KEY = bytes.fromhex("11111111111111111111111111111112") @pytest.fixture @@ -30,11 +30,22 @@ def session(ccid_connection): yield hsmauth +@pytest.fixture +def management_key(session, info): + if CAPABILITY.HSMAUTH in info.fips_capable: + key = bytes.fromhex("00000000000000000000000000000001") + session.put_management_key(DEFAULT_MANAGEMENT_KEY, key) + + yield key + else: + yield DEFAULT_MANAGEMENT_KEY + + def import_key_derived( session, management_key, - credential_password="123456", - derivation_password="password", + credential_password="12345679", + derivation_password="p4ssw0rd", ) -> Credential: credential = session.put_credential_derived( management_key, @@ -47,7 +58,7 @@ def import_key_derived( def import_key_symmetric( - session, management_key, key_enc, key_mac, credential_password="123456" + session, management_key, key_enc, key_mac, credential_password="12345679" ) -> Credential: credential = session.put_credential_symmetric( management_key, @@ -61,7 +72,7 @@ def import_key_symmetric( def import_key_asymmetric( - session, management_key, private_key, credential_password="123456" + session, management_key, private_key, credential_password="12345679" ) -> Credential: credential = session.put_credential_asymmetric( management_key, @@ -74,7 +85,7 @@ def import_key_asymmetric( def generate_key_asymmetric( - session, management_key, credential_password="123456" + session, management_key, credential_password="12345679" ) -> Credential: credential = session.generate_credential_asymmetric( management_key, @@ -101,49 +112,63 @@ def verify_credential_password( ): context = b"g\xfc\xf1\xfe\xb5\xf1\xd8\x83\xedv=\xbfI0\x90\xbb" - # Try to calculate session keys using credential password + # Try to calculate session keys using wrong credential password + with pytest.raises(InvalidPinError): + session.calculate_session_keys_symmetric( + label=credential.label, + context=context, + credential_password="wrongvalue", + ) + + # Try to calculate session keys using correct credential password session.calculate_session_keys_symmetric( label=credential.label, context=context, credential_password=credential_password, ) - def test_import_credential_symmetric_wrong_management_key(self, session): + def test_import_credential_symmetric_wrong_management_key( + self, session, management_key + ): with pytest.raises(InvalidPinError): import_key_derived(session, NON_DEFAULT_MANAGEMENT_KEY) - def test_import_credential_symmetric_wrong_key_length(self, session): + def test_import_credential_symmetric_wrong_key_length( + self, session, management_key + ): with pytest.raises(ValueError): import_key_symmetric( - session, DEFAULT_MANAGEMENT_KEY, os.urandom(24), os.urandom(24) + session, management_key, os.urandom(24), os.urandom(24) ) - def test_import_credential_symmetric_exists(self, session): - import_key_derived(session, DEFAULT_MANAGEMENT_KEY) + def test_import_credential_symmetric_exists(self, session, management_key): + import_key_derived(session, management_key) with pytest.raises(ApduError): - import_key_derived(session, DEFAULT_MANAGEMENT_KEY) + import_key_derived(session, management_key) - def test_import_credential_symmetric_works(self, session): - credential = import_key_derived(session, DEFAULT_MANAGEMENT_KEY, "1234") + def test_import_credential_symmetric_works(self, session, management_key): + credential = import_key_derived(session, management_key, "12345679") - self.verify_credential_password(session, "1234", credential) + self.verify_credential_password(session, "12345679", credential) self.check_credential_in_list(session, credential) - session.delete_credential(DEFAULT_MANAGEMENT_KEY, credential.label) + session.delete_credential(management_key, credential.label) @condition.min_version(5, 6) - def test_import_credential_asymmetric_unsupported_key(self, session): + def test_import_credential_asymmetric_unsupported_key( + self, session, management_key + ): private_key = ec.generate_private_key( ec.SECP224R1(), backend=default_backend() ) # curve secp224r1 is not supported with pytest.raises(ValueError): - import_key_asymmetric(session, DEFAULT_MANAGEMENT_KEY, private_key) + import_key_asymmetric(session, management_key, private_key) @condition.min_version(5, 6) - def test_import_credential_asymmetric_works(self, session): + def test_import_credential_asymmetric_works(self, session, management_key): private_key = ec.generate_private_key(ec.SECP256R1(), backend=default_backend()) - credential = import_key_asymmetric(session, DEFAULT_MANAGEMENT_KEY, private_key) + credential = import_key_asymmetric(session, management_key, private_key) public_key = private_key.public_key() assert public_key.public_bytes( @@ -153,11 +178,11 @@ def test_import_credential_asymmetric_works(self, session): ) self.check_credential_in_list(session, credential) - session.delete_credential(DEFAULT_MANAGEMENT_KEY, credential.label) + session.delete_credential(management_key, credential.label) @condition.min_version(5, 6) - def test_generate_credential_asymmetric_works(self, session): - credential = generate_key_asymmetric(session, DEFAULT_MANAGEMENT_KEY) + def test_generate_credential_asymmetric_works(self, session, management_key): + credential = generate_key_asymmetric(session, management_key) self.check_credential_in_list(session, credential) @@ -166,47 +191,47 @@ def test_generate_credential_asymmetric_works(self, session): assert isinstance(public_key, ec.EllipticCurvePublicKey) assert isinstance(public_key.curve, ec.SECP256R1) - session.delete_credential(DEFAULT_MANAGEMENT_KEY, credential.label) + session.delete_credential(management_key, credential.label) @condition.min_version(5, 6) - def test_export_public_key_symmetric_credential(self, session): - credential = import_key_derived(session, DEFAULT_MANAGEMENT_KEY) + def test_export_public_key_symmetric_credential(self, session, management_key): + credential = import_key_derived(session, management_key) with pytest.raises(ApduError): session.get_public_key(credential.label) - session.delete_credential(DEFAULT_MANAGEMENT_KEY, credential.label) + session.delete_credential(management_key, credential.label) - def test_delete_credential_wrong_management_key(self, session): - credential = import_key_derived(session, DEFAULT_MANAGEMENT_KEY) + def test_delete_credential_wrong_management_key(self, session, management_key): + credential = import_key_derived(session, management_key) with pytest.raises(InvalidPinError): session.delete_credential(NON_DEFAULT_MANAGEMENT_KEY, credential.label) - def test_delete_credential_non_existing(self, session): + def test_delete_credential_non_existing(self, session, management_key): with pytest.raises(ApduError): - session.delete_credential(DEFAULT_MANAGEMENT_KEY, "Default key") + session.delete_credential(management_key, "Default key") - def test_delete_credential_works(self, session): - credential = import_key_derived(session, DEFAULT_MANAGEMENT_KEY) + def test_delete_credential_works(self, session, management_key): + credential = import_key_derived(session, management_key) - session.delete_credential(DEFAULT_MANAGEMENT_KEY, credential.label) + session.delete_credential(management_key, credential.label) credentials = session.list_credentials() assert len(credentials) == 0 class TestAccess: - def test_change_management_key(self, session): - session.put_management_key(DEFAULT_MANAGEMENT_KEY, NON_DEFAULT_MANAGEMENT_KEY) + def test_change_management_key(self, session, management_key): + session.put_management_key(management_key, NON_DEFAULT_MANAGEMENT_KEY) # Can't import key with old management key with pytest.raises(InvalidPinError): - import_key_derived(session, DEFAULT_MANAGEMENT_KEY) + import_key_derived(session, management_key) - session.put_management_key(NON_DEFAULT_MANAGEMENT_KEY, DEFAULT_MANAGEMENT_KEY) + session.put_management_key(NON_DEFAULT_MANAGEMENT_KEY, management_key) - def test_management_key_retries(self, session): - session.put_management_key(DEFAULT_MANAGEMENT_KEY, DEFAULT_MANAGEMENT_KEY) + def test_management_key_retries(self, session, management_key): + session.put_management_key(management_key, management_key) initial_retries = session.get_management_key_retries() assert initial_retries == 8 @@ -218,11 +243,11 @@ def test_management_key_retries(self, session): class TestSessionKeys: - def test_calculate_session_keys_symmetric(self, session): - credential_password = "1234" + def test_calculate_session_keys_symmetric(self, session, management_key): + credential_password = "a password" credential = import_key_derived( session, - DEFAULT_MANAGEMENT_KEY, + management_key, credential_password=credential_password, derivation_password="pwd", ) @@ -246,8 +271,8 @@ def test_calculate_session_keys_symmetric(self, session): class TestHostChallenge: @condition.min_version(5, 6) - def test_get_challenge_symmetric(self, session): - credential = import_key_derived(session, DEFAULT_MANAGEMENT_KEY) + def test_get_challenge_symmetric(self, session, management_key): + credential = import_key_derived(session, management_key) challenge1 = session.get_challenge(credential.label) challenge2 = session.get_challenge(credential.label) @@ -255,17 +280,20 @@ def test_get_challenge_symmetric(self, session): assert len(challenge2) == 8 assert challenge1 != challenge2 - session.delete_credential(DEFAULT_MANAGEMENT_KEY, credential.label) + session.delete_credential(management_key, credential.label) @condition.min_version(5, 6) - def test_get_challenge_asymmetric(self, session): - credential = generate_key_asymmetric(session, DEFAULT_MANAGEMENT_KEY) + def test_get_challenge_asymmetric(self, session, management_key): + credential_password = "12345679" + credential = generate_key_asymmetric( + session, management_key, credential_password + ) - challenge1 = session.get_challenge(credential.label) - challenge2 = session.get_challenge(credential.label) + challenge1 = session.get_challenge(credential.label, credential_password) + challenge2 = session.get_challenge(credential.label, credential_password) assert len(challenge1) == 65 assert len(challenge2) == 65 assert challenge1 != challenge2 - session.delete_credential(DEFAULT_MANAGEMENT_KEY, credential.label) + session.delete_credential(management_key, credential.label) diff --git a/tests/device/test_oath.py b/tests/device/test_oath.py index ade4ca24..7994d11b 100644 --- a/tests/device/test_oath.py +++ b/tests/device/test_oath.py @@ -16,9 +16,11 @@ @pytest.fixture @condition.capability(CAPABILITY.OATH) -def session(ccid_connection): +def session(ccid_connection, info): oath = OathSession(ccid_connection) oath.reset() + if CAPABILITY.OATH in info.fips_capable: + oath.set_key(KEY) yield oath diff --git a/tests/device/test_openpgp.py b/tests/device/test_openpgp.py index ac8de929..0142c492 100644 --- a/tests/device/test_openpgp.py +++ b/tests/device/test_openpgp.py @@ -10,8 +10,9 @@ KdfNone, ) from yubikit.management import CAPABILITY -from yubikit.core.smartcard import ApduError +from yubikit.core.smartcard import ApduError, AID from . import condition +from typing import NamedTuple import pytest import time @@ -19,9 +20,9 @@ E = 65537 DEFAULT_PIN = "123456" -NON_DEFAULT_PIN = "654321" +NON_DEFAULT_PIN = "12345670" DEFAULT_ADMIN_PIN = "12345678" -NON_DEFAULT_ADMIN_PIN = "87654321" +NON_DEFAULT_ADMIN_PIN = "12345670" @pytest.fixture @@ -32,11 +33,48 @@ def session(ccid_connection): return pgp +class Keys(NamedTuple): + pin: str + admin: str + + +def reset_state(session): + session.protocol.connection.connection.disconnect() + session.protocol.connection.connection.connect() + session.protocol.select(AID.OPENPGP) + + def not_roca(version): """ROCA affected""" return not ((4, 2, 0) <= version < (4, 3, 5)) +def fips_capable(info): + """Not FIPS capable""" + return CAPABILITY.OPENPGP in info.fips_capable + + +def not_fips_capable(info): + """FIPS capable""" + return not fips_capable(info) + + +@pytest.fixture +def keys(session, info): + if fips_capable(info): + new_keys = Keys( + "12345679", + "12345679", + ) + session.change_pin(DEFAULT_PIN, new_keys.pin) + session.change_admin(DEFAULT_ADMIN_PIN, new_keys.admin) + reset_state(session) + + yield new_keys + else: + yield Keys(DEFAULT_PIN, DEFAULT_ADMIN_PIN) + + def test_import_requires_admin(session): priv = rsa.generate_private_key(E, RSA_SIZE.RSA2048, default_backend()) with pytest.raises(ApduError): @@ -51,83 +89,90 @@ def test_generate_requires_admin(session): @condition.min_version(5, 2) @pytest.mark.parametrize("oid", [x for x in OID if "25519" not in x.name]) -def test_import_sign_ecdsa(session, oid): +def test_import_sign_ecdsa(session, info, keys, oid): + if fips_capable(info) and oid == OID.SECP256K1: + pytest.skip("FIPS capable") + priv = ec.generate_private_key(getattr(ec, oid.name)()) - session.verify_admin(DEFAULT_ADMIN_PIN) + session.verify_admin(keys.admin) session.put_key(KEY_REF.SIG, priv) message = b"Hello world" - session.verify_pin(DEFAULT_PIN) + session.verify_pin(keys.pin) sig = session.sign(message, hashes.SHA256()) priv.public_key().verify(sig, message, ec.ECDSA(hashes.SHA256())) @condition.min_version(5, 2) -def test_import_sign_eddsa(session): +def test_import_sign_eddsa(session, keys): priv = ed25519.Ed25519PrivateKey.generate() - session.verify_admin(DEFAULT_ADMIN_PIN) + session.verify_admin(keys.admin) session.put_key(KEY_REF.SIG, priv) message = b"Hello world" - session.verify_pin(DEFAULT_PIN) + session.verify_pin(keys.pin) sig = session.sign(message, hashes.SHA256()) priv.public_key().verify(sig, message) @condition.min_version(5, 2) @pytest.mark.parametrize("oid", [x for x in OID if "25519" not in x.name]) -def test_import_ecdh(session, oid): +def test_import_ecdh(session, info, keys, oid): + if fips_capable(info) and oid == OID.SECP256K1: + pytest.skip("FIPS capable") priv = ec.generate_private_key(getattr(ec, oid.name)()) - session.verify_admin(DEFAULT_ADMIN_PIN) + session.verify_admin(keys.admin) session.put_key(KEY_REF.DEC, priv) e_priv = ec.generate_private_key(getattr(ec, oid.name)()) shared1 = e_priv.exchange(ec.ECDH(), priv.public_key()) - session.verify_pin(DEFAULT_PIN, extended=True) + session.verify_pin(keys.pin, extended=True) shared2 = session.decrypt(e_priv.public_key()) assert shared1 == shared2 +@condition.check(not_fips_capable) @condition.min_version(5, 2) -def test_import_ecdh_x25519(session): +def test_import_ecdh_x25519(session, keys): priv = x25519.X25519PrivateKey.generate() - session.verify_admin(DEFAULT_ADMIN_PIN) + session.verify_admin(keys.admin) session.put_key(KEY_REF.DEC, priv) e_priv = x25519.X25519PrivateKey.generate() shared1 = e_priv.exchange(priv.public_key()) - session.verify_pin(DEFAULT_PIN, extended=True) + session.verify_pin(keys.pin, extended=True) shared2 = session.decrypt(e_priv.public_key()) assert shared1 == shared2 @pytest.mark.parametrize("key_size", [2048, 3072, 4096]) -def test_import_sign_rsa(session, key_size, info): +def test_import_sign_rsa(session, keys, key_size, info): if key_size != 2048: if info.version[0] < 4: pytest.skip(f"RSA {key_size} requires YuibKey 4 or later") elif info.version[0] == 4 and info.is_fips: pytest.skip(f"RSA {key_size} not supported on YubiKey 4 FIPS") priv = rsa.generate_private_key(E, key_size, default_backend()) - session.verify_admin(DEFAULT_ADMIN_PIN) + session.verify_admin(keys.admin) session.put_key(KEY_REF.SIG, priv) if 0 < info.version[0] < 5: # Keys don't work without a generation time (or fingerprint) session.set_generation_time(KEY_REF.SIG, int(time.time())) message = b"Hello world" - session.verify_pin(DEFAULT_PIN) + session.verify_pin(keys.pin) sig = session.sign(message, hashes.SHA256()) priv.public_key().verify(sig, message, padding.PKCS1v15(), hashes.SHA256()) +@condition.check(not_fips_capable) @pytest.mark.parametrize("key_size", [2048, 3072, 4096]) -def test_import_decrypt_rsa(session, key_size, info): +def test_import_decrypt_rsa(session, keys, key_size, info): if key_size != 2048: if info.version[0] < 4: pytest.skip(f"RSA {key_size} requires YuibKey 4 or later") elif info.version[0] == 4 and info.is_fips: pytest.skip(f"RSA {key_size} not supported on YubiKey 4 FIPS") priv = rsa.generate_private_key(E, key_size, default_backend()) - session.verify_admin(DEFAULT_ADMIN_PIN) + session.verify_admin(keys.admin) session.put_key(KEY_REF.DEC, priv) if info.version[0] < 5: # Keys don't work without a generation time (or fingerprint) @@ -135,7 +180,7 @@ def test_import_decrypt_rsa(session, key_size, info): message = b"Hello world" cipher = priv.public_key().encrypt(message, padding.PKCS1v15()) - session.verify_pin(DEFAULT_PIN, extended=True) + session.verify_pin(keys.pin, extended=True) plain = session.decrypt(cipher) assert message == plain @@ -143,13 +188,13 @@ def test_import_decrypt_rsa(session, key_size, info): @condition.check(not_roca) @pytest.mark.parametrize("key_size", [2048, 3072, 4096]) -def test_generate_rsa(session, key_size, info): +def test_generate_rsa(session, keys, key_size, info): if key_size != 2048: if info.version[0] < 4: pytest.skip(f"RSA {key_size} requires YuibKey 4 or later") elif info.version[0] == 4 and info.is_fips: pytest.skip(f"RSA {key_size} not supported on YubiKey 4 FIPS") - session.verify_admin(DEFAULT_ADMIN_PIN) + session.verify_admin(keys.admin) pub = session.generate_rsa_key(KEY_REF.SIG, RSA_SIZE(key_size)) if info.version[0] < 5: # Keys don't work without a generation time (or fingerprint) @@ -158,51 +203,55 @@ def test_generate_rsa(session, key_size, info): assert pub.key_size == key_size message = b"Hello world" - session.verify_pin(DEFAULT_PIN) + session.verify_pin(keys.pin) sig = session.sign(message, hashes.SHA256()) pub.verify(sig, message, padding.PKCS1v15(), hashes.SHA256()) @condition.min_version(5, 2) @pytest.mark.parametrize("oid", [x for x in OID if "25519" not in x.name]) -def test_generate_ecdsa(session, oid): - session.verify_admin(DEFAULT_ADMIN_PIN) +def test_generate_ecdsa(session, info, keys, oid): + if fips_capable(info) and oid == OID.SECP256K1: + pytest.skip("FIPS capable") + + session.verify_admin(keys.admin) pub = session.generate_ec_key(KEY_REF.SIG, oid) message = b"Hello world" - session.verify_pin(DEFAULT_PIN) + session.verify_pin(keys.pin) sig = session.sign(message, hashes.SHA256()) pub.verify(sig, message, ec.ECDSA(hashes.SHA256())) @condition.min_version(5, 2) -def test_generate_ed25519(session): - session.verify_admin(DEFAULT_ADMIN_PIN) +def test_generate_ed25519(session, keys): + session.verify_admin(keys.admin) pub = session.generate_ec_key(KEY_REF.SIG, OID.Ed25519) message = b"Hello world" - session.verify_pin(DEFAULT_PIN) + session.verify_pin(keys.pin) sig = session.sign(message, hashes.SHA256()) pub.verify(sig, message) @condition.min_version(5, 2) -def test_generate_x25519(session): - session.verify_admin(DEFAULT_ADMIN_PIN) +@condition.check(not_fips_capable) +def test_generate_x25519(session, keys): + session.verify_admin(keys.admin) pub = session.generate_ec_key(KEY_REF.DEC, OID.X25519) e_priv = x25519.X25519PrivateKey.generate() shared1 = e_priv.exchange(pub) - session.verify_pin(DEFAULT_PIN, extended=True) + session.verify_pin(keys.pin, extended=True) shared2 = session.decrypt(e_priv.public_key()) assert shared1 == shared2 @condition.min_version(5, 2) -def test_kdf(session): +def test_kdf(session, keys): with pytest.raises(ApduError): session.set_kdf(KdfIterSaltedS2k.create()) - session.change_admin(DEFAULT_ADMIN_PIN, NON_DEFAULT_ADMIN_PIN) + session.change_admin(keys.admin, NON_DEFAULT_ADMIN_PIN) session.verify_admin(NON_DEFAULT_ADMIN_PIN) session.set_kdf(KdfIterSaltedS2k.create()) session.verify_admin(DEFAULT_ADMIN_PIN) @@ -218,14 +267,14 @@ def test_kdf(session): @condition.min_version(5, 2) -def test_attestation(session): +def test_attestation(session, keys): if not session.get_key_information()[KEY_REF.ATT]: pytest.skip("No attestation key") - session.verify_admin(DEFAULT_ADMIN_PIN) + session.verify_admin(keys.admin) pub = session.generate_ec_key(KEY_REF.SIG, OID.SECP256R1) - session.verify_pin(DEFAULT_PIN) + session.verify_pin(keys.pin) cert = session.attest_key(KEY_REF.SIG) assert cert.public_key() == pub diff --git a/tests/device/test_otp.py b/tests/device/test_otp.py index 0b6fe5ab..467bc1c3 100644 --- a/tests/device/test_otp.py +++ b/tests/device/test_otp.py @@ -25,6 +25,11 @@ def conn_type(request, version, transport): return conn_type +def no_pin_complexity(info): + """PIN complexity enabled""" + return not info.pin_complexity + + @pytest.fixture() @condition.capability(CAPABILITY.OTP) def session(conn_type, info, device): @@ -75,6 +80,7 @@ def call(): class TestProgrammingState: @pytest.fixture(autouse=True) @condition.min_version(2, 1) + @condition.check(no_pin_complexity) def clear_slots(self, session, read_config): state = read_config() for slot in (SLOT.ONE, SLOT.TWO): @@ -137,6 +143,7 @@ def test_slot_touch_triggered(self, session, read_config, slot): class TestChallengeResponse: @pytest.fixture(autouse=True) @condition.check(not_usb_ccid) + @condition.check(no_pin_complexity) def clear_slot2(self, session, read_config): state = read_config() if state.is_configured(SLOT.TWO): diff --git a/tests/device/test_piv.py b/tests/device/test_piv.py index d9138c4b..1b9fc762 100644 --- a/tests/device/test_piv.py +++ b/tests/device/test_piv.py @@ -21,7 +21,7 @@ OBJECT_ID, MANAGEMENT_KEY_TYPE, InvalidPinError, - check_key_support, + _do_check_key_support, ) from ykman.piv import ( check_key, @@ -34,12 +34,13 @@ from ykman.util import parse_certificates, parse_private_key from ..util import open_file from . import condition +from typing import NamedTuple DEFAULT_PIN = "123456" -NON_DEFAULT_PIN = "654321" +NON_DEFAULT_PIN = "12341235" DEFAULT_PUK = "12345678" -NON_DEFAULT_PUK = "87654321" +NON_DEFAULT_PUK = "12341236" DEFAULT_MANAGEMENT_KEY = bytes.fromhex( "010203040506070801020304050607080102030405060708" ) @@ -72,6 +73,36 @@ def session(ccid_connection): reset_state(piv) +class Keys(NamedTuple): + pin: str + puk: str + mgmt: bytes + + +@pytest.fixture +def default_keys(): + yield Keys(DEFAULT_PIN, DEFAULT_PUK, DEFAULT_MANAGEMENT_KEY) + + +@pytest.fixture +def keys(session, info, default_keys): + if info.pin_complexity: + new_keys = Keys( + "12345679" if CAPABILITY.PIV in info.fips_capable else "123458", + "12345670", + bytes.fromhex("010203040506070801020304050607080102030405060709"), + ) + session.change_pin(default_keys.pin, new_keys.pin) + session.change_puk(default_keys.puk, new_keys.puk) + session.authenticate(mgm_key_type(session), default_keys.mgmt) + session.set_management_key(mgm_key_type(session), new_keys.mgmt) + reset_state(session) + + yield new_keys + else: + yield default_keys + + def mgm_key_type(session): try: return session.get_management_key_metadata().key_type @@ -100,11 +131,12 @@ def assert_mgm_key_is_not(session, key): def generate_key( session, + keys, slot=SLOT.AUTHENTICATION, key_type=KEY_TYPE.ECCP256, pin_policy=PIN_POLICY.DEFAULT, ): - session.authenticate(mgm_key_type(session), DEFAULT_MANAGEMENT_KEY) + session.authenticate(mgm_key_type(session), keys.mgmt) key = session.generate_key(slot, key_type, pin_policy=pin_policy) reset_state(session) return key @@ -125,12 +157,13 @@ def generate_sw_key(key_type): def import_key( session, + keys, slot=SLOT.AUTHENTICATION, key_type=KEY_TYPE.ECCP256, pin_policy=PIN_POLICY.DEFAULT, ): private_key = generate_sw_key(key_type) - session.authenticate(mgm_key_type(session), DEFAULT_MANAGEMENT_KEY) + session.authenticate(mgm_key_type(session), keys.mgmt) session.put_key(slot, private_key, pin_policy) reset_state(session) return private_key.public_key() @@ -151,15 +184,14 @@ def verify_cert_signature(cert, public_key=None): public_key.verify(*args) -def skip_unsupported_key_type(key_type, info): - if key_type == KEY_TYPE.RSA1024 and info.is_fips and info.version[0] == 4: - pytest.skip("RSA1024 not available on YubiKey FIPS") +def skip_unsupported_key_type(key_type, info, pin_policy=PIN_POLICY.DEFAULT): try: - check_key_support( + _do_check_key_support( info.version, key_type, - PIN_POLICY.DEFAULT, + pin_policy, TOUCH_POLICY.DEFAULT, + fips_restrictions=CAPABILITY.PIV in info.fips_capable, ) except NotSupportedError as e: pytest.skip(f"{e}") @@ -171,14 +203,14 @@ class TestCertificateSignatures: "hash_algorithm", (hashes.SHA256, hashes.SHA384, hashes.SHA512) ) def test_generate_self_signed_certificate( - self, info, session, key_type, hash_algorithm + self, info, session, key_type, hash_algorithm, keys ): skip_unsupported_key_type(key_type, info) slot = SLOT.SIGNATURE - public_key = import_key(session, slot, key_type) - session.authenticate(mgm_key_type(session), DEFAULT_MANAGEMENT_KEY) - session.verify_pin(DEFAULT_PIN) + public_key = import_key(session, keys, slot, key_type) + session.authenticate(mgm_key_type(session), keys.mgmt) + session.verify_pin(keys.pin) cert = generate_self_signed_certificate( session, slot, public_key, "CN=alice", NOW, NOW, hash_algorithm ) @@ -195,66 +227,66 @@ class TestDecrypt: "key_type", [KEY_TYPE.RSA1024, KEY_TYPE.RSA2048, KEY_TYPE.RSA3072, KEY_TYPE.RSA4096], ) - def test_import_decrypt(self, session, info, key_type): + def test_import_decrypt(self, session, info, key_type, keys): skip_unsupported_key_type(key_type, info) - public_key = import_key(session, SLOT.KEY_MANAGEMENT, key_type=key_type) + public_key = import_key(session, keys, SLOT.KEY_MANAGEMENT, key_type=key_type) pt = os.urandom(32) ct = public_key.encrypt(pt, padding.PKCS1v15()) - session.verify_pin(DEFAULT_PIN) + session.verify_pin(keys.pin) pt2 = session.decrypt(SLOT.KEY_MANAGEMENT, ct, padding.PKCS1v15()) assert pt == pt2 class TestKeyAgreement: @pytest.mark.parametrize("key_type", ECDH_KEY_TYPES) - def test_generate_ecdh(self, session, info, key_type): + def test_generate_ecdh(self, session, info, key_type, keys): skip_unsupported_key_type(key_type, info) e_priv = generate_sw_key(key_type) - public_key = generate_key(session, SLOT.KEY_MANAGEMENT, key_type=key_type) + public_key = generate_key(session, keys, SLOT.KEY_MANAGEMENT, key_type=key_type) if key_type == KEY_TYPE.X25519: args = (public_key,) else: args = (ec.ECDH(), public_key) shared1 = e_priv.exchange(*args) - session.verify_pin(DEFAULT_PIN) + session.verify_pin(keys.pin) shared2 = session.calculate_secret(SLOT.KEY_MANAGEMENT, e_priv.public_key()) assert shared1 == shared2 @pytest.mark.parametrize("key_type", ECDH_KEY_TYPES) - def test_import_ecdh(self, session, info, key_type): + def test_import_ecdh(self, session, info, key_type, keys): skip_unsupported_key_type(key_type, info) e_priv = generate_sw_key(key_type) - public_key = import_key(session, SLOT.KEY_MANAGEMENT, key_type=key_type) + public_key = import_key(session, keys, SLOT.KEY_MANAGEMENT, key_type=key_type) if key_type == KEY_TYPE.X25519: args = (public_key,) else: args = (ec.ECDH(), public_key) shared1 = e_priv.exchange(*args) - session.verify_pin(DEFAULT_PIN) + session.verify_pin(keys.pin) shared2 = session.calculate_secret(SLOT.KEY_MANAGEMENT, e_priv.public_key()) assert shared1 == shared2 class TestKeyManagement: - def test_delete_certificate_requires_authentication(self, session): - generate_key(session, SLOT.AUTHENTICATION) + def test_delete_certificate_requires_authentication(self, session, keys): + generate_key(session, keys, SLOT.AUTHENTICATION) with pytest.raises(ApduError): session.delete_certificate(SLOT.AUTHENTICATION) - session.authenticate(mgm_key_type(session), DEFAULT_MANAGEMENT_KEY) + session.authenticate(mgm_key_type(session), keys.mgmt) session.delete_certificate(SLOT.AUTHENTICATION) - def test_generate_csr_works(self, session): - public_key = generate_key(session, SLOT.AUTHENTICATION) + def test_generate_csr_works(self, session, keys): + public_key = generate_key(session, keys, SLOT.AUTHENTICATION) - session.verify_pin(DEFAULT_PIN) + session.verify_pin(keys.pin) csr = generate_csr(session, SLOT.AUTHENTICATION, public_key, "CN=alice") assert csr.public_key().public_numbers() == public_key.public_numbers() @@ -263,25 +295,25 @@ def test_generate_csr_works(self, session): == "alice" ) - def test_generate_self_signed_certificate_requires_pin(self, session): - session.verify_pin(DEFAULT_PIN) - public_key = generate_key(session, SLOT.AUTHENTICATION) + def test_generate_self_signed_certificate_requires_pin(self, session, keys): + session.verify_pin(keys.pin) + public_key = generate_key(session, keys, SLOT.AUTHENTICATION) with pytest.raises(ApduError): generate_self_signed_certificate( session, SLOT.AUTHENTICATION, public_key, "CN=alice", NOW, NOW ) - session.verify_pin(DEFAULT_PIN) + session.verify_pin(keys.pin) generate_self_signed_certificate( session, SLOT.AUTHENTICATION, public_key, "CN=alice", NOW, NOW ) @pytest.mark.parametrize("slot", (SLOT.SIGNATURE, SLOT.AUTHENTICATION)) - def test_generate_self_signed_certificate(self, session, slot): - public_key = generate_key(session, slot) - session.authenticate(mgm_key_type(session), DEFAULT_MANAGEMENT_KEY) - session.verify_pin(DEFAULT_PIN) + def test_generate_self_signed_certificate(self, session, slot, keys): + public_key = generate_key(session, keys, slot) + session.authenticate(mgm_key_type(session), keys.mgmt) + session.verify_pin(keys.pin) cert = generate_self_signed_certificate( session, slot, public_key, "CN=alice", NOW, NOW ) @@ -292,28 +324,28 @@ def test_generate_self_signed_certificate(self, session, slot): == "alice" ) - def test_generate_key_requires_authentication(self, session): + def test_generate_key_requires_authentication(self, session, keys): with pytest.raises(ApduError): session.generate_key( SLOT.AUTHENTICATION, KEY_TYPE.ECCP256, touch_policy=TOUCH_POLICY.DEFAULT ) - session.authenticate(mgm_key_type(session), DEFAULT_MANAGEMENT_KEY) + session.authenticate(mgm_key_type(session), keys.mgmt) session.generate_key(SLOT.AUTHENTICATION, KEY_TYPE.ECCP256) - def test_put_certificate_requires_authentication(self, session): + def test_put_certificate_requires_authentication(self, session, keys): cert = get_test_cert() with pytest.raises(ApduError): session.put_certificate(SLOT.AUTHENTICATION, cert) - session.authenticate(mgm_key_type(session), DEFAULT_MANAGEMENT_KEY) + session.authenticate(mgm_key_type(session), keys.mgmt) session.put_certificate(SLOT.AUTHENTICATION, cert) - def _test_put_key_pairing(self, session, alg1, alg2): + def _test_put_key_pairing(self, session, keys, alg1, alg2): # Set up a key in the slot and create a certificate for it - public_key = generate_key(session, SLOT.AUTHENTICATION, key_type=alg1) - session.authenticate(mgm_key_type(session), DEFAULT_MANAGEMENT_KEY) - session.verify_pin(DEFAULT_PIN) + public_key = generate_key(session, keys, SLOT.AUTHENTICATION, key_type=alg1) + session.authenticate(mgm_key_type(session), keys.mgmt) + session.verify_pin(keys.pin) cert = generate_self_signed_certificate( session, SLOT.AUTHENTICATION, public_key, "CN=test", NOW, NOW ) @@ -326,47 +358,49 @@ def _test_put_key_pairing(self, session, alg1, alg2): session.delete_certificate(SLOT.AUTHENTICATION) # Overwrite the key with one of the same type - generate_key(session, SLOT.AUTHENTICATION, key_type=alg1) - session.verify_pin(DEFAULT_PIN) + generate_key(session, keys, SLOT.AUTHENTICATION, key_type=alg1) + session.verify_pin(keys.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, key_type=alg2) - session.verify_pin(DEFAULT_PIN) + generate_key(session, keys, SLOT.AUTHENTICATION, key_type=alg2) + session.verify_pin(keys.pin) assert not check_key(session, SLOT.AUTHENTICATION, cert.public_key()) @condition.check(not_roca) @condition.yk4_fips(False) - def test_put_certificate_verifies_key_pairing_rsa1024(self, session): - self._test_put_key_pairing(session, KEY_TYPE.RSA1024, KEY_TYPE.ECCP256) + def test_put_certificate_verifies_key_pairing_rsa1024(self, session, keys, info): + if CAPABILITY.PIV in info.fips_capable: + pytest.skip("RSA1024 not available on YubiKey FIPS") + self._test_put_key_pairing(session, keys, KEY_TYPE.RSA1024, KEY_TYPE.ECCP256) @condition.check(not_roca) - def test_put_certificate_verifies_key_pairing_rsa2048(self, session): - self._test_put_key_pairing(session, KEY_TYPE.RSA2048, KEY_TYPE.ECCP256) + def test_put_certificate_verifies_key_pairing_rsa2048(self, session, keys): + self._test_put_key_pairing(session, keys, KEY_TYPE.RSA2048, KEY_TYPE.ECCP256) @condition.check(not_roca) - def test_put_certificate_verifies_key_pairing_eccp256_a(self, session): - self._test_put_key_pairing(session, KEY_TYPE.ECCP256, KEY_TYPE.RSA2048) + def test_put_certificate_verifies_key_pairing_eccp256_a(self, session, keys): + self._test_put_key_pairing(session, keys, KEY_TYPE.ECCP256, KEY_TYPE.RSA2048) @condition.min_version(4) - def test_put_certificate_verifies_key_pairing_eccp256_b(self, session): - self._test_put_key_pairing(session, KEY_TYPE.ECCP256, KEY_TYPE.ECCP384) + def test_put_certificate_verifies_key_pairing_eccp256_b(self, session, keys): + self._test_put_key_pairing(session, keys, KEY_TYPE.ECCP256, KEY_TYPE.ECCP384) @condition.min_version(4) - def test_put_certificate_verifies_key_pairing_eccp384(self, session): - self._test_put_key_pairing(session, KEY_TYPE.ECCP384, KEY_TYPE.ECCP256) + def test_put_certificate_verifies_key_pairing_eccp384(self, session, keys): + self._test_put_key_pairing(session, keys, KEY_TYPE.ECCP384, KEY_TYPE.ECCP256) - def test_put_key_requires_authentication(self, session): + def test_put_key_requires_authentication(self, session, keys): private_key = get_test_key() with pytest.raises(ApduError): session.put_key(SLOT.AUTHENTICATION, private_key) - session.authenticate(mgm_key_type(session), DEFAULT_MANAGEMENT_KEY) + session.authenticate(mgm_key_type(session), keys.mgmt) session.put_key(SLOT.AUTHENTICATION, private_key) - def test_get_certificate_does_not_require_authentication(self, session): + def test_get_certificate_does_not_require_authentication(self, session, keys): cert = get_test_cert() - session.authenticate(mgm_key_type(session), DEFAULT_MANAGEMENT_KEY) + session.authenticate(mgm_key_type(session), keys.mgmt) session.put_certificate(SLOT.AUTHENTICATION, cert) reset_state(session) @@ -374,8 +408,8 @@ def test_get_certificate_does_not_require_authentication(self, session): class TestCompressedCertificate: - def test_put_and_read_compressed_certificate(self, session): - session.authenticate(mgm_key_type(session), DEFAULT_MANAGEMENT_KEY) + def test_put_and_read_compressed_certificate(self, session, keys): + session.authenticate(mgm_key_type(session), keys.mgmt) cert = get_test_cert() session.put_certificate(SLOT.AUTHENTICATION, cert) session.put_certificate(SLOT.SIGNATURE, cert, compress=True) @@ -395,16 +429,16 @@ class TestManagementKeyReadOnly: calls needed. """ - def test_authenticate_twice_does_not_throw(self, session): - session.authenticate(mgm_key_type(session), DEFAULT_MANAGEMENT_KEY) - session.authenticate(mgm_key_type(session), DEFAULT_MANAGEMENT_KEY) + def test_authenticate_twice_does_not_throw(self, session, keys): + session.authenticate(mgm_key_type(session), keys.mgmt) + session.authenticate(mgm_key_type(session), keys.mgmt) - def test_reset_resets_has_stored_key_flag(self, session): + def test_reset_resets_has_stored_key_flag(self, session, keys): pivman = get_pivman_data(session) assert not pivman.has_stored_key - session.verify_pin(DEFAULT_PIN) - session.authenticate(mgm_key_type(session), DEFAULT_MANAGEMENT_KEY) + session.verify_pin(keys.pin) + session.authenticate(mgm_key_type(session), keys.mgmt) pivman_set_mgm_key( session, NON_DEFAULT_MANAGEMENT_KEY, @@ -422,22 +456,24 @@ def test_reset_resets_has_stored_key_flag(self, session): assert not pivman.has_stored_key # Should this really fail? - def disabled_test_reset_while_verified_throws_nice_ValueError(self, session): - session.verify_pin(DEFAULT_PIN) + def disabled_test_reset_while_verified_throws_nice_ValueError(self, session, keys): + session.verify_pin(keys.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(self, session): + def test_set_mgm_key_does_not_change_key_if_not_authenticated(self, session, keys): with pytest.raises(ApduError): session.set_management_key( mgm_key_type(session), NON_DEFAULT_MANAGEMENT_KEY ) - assert_mgm_key_is(session, DEFAULT_MANAGEMENT_KEY) + assert_mgm_key_is(session, keys.mgmt) @condition.min_version(3, 5) - def test_set_stored_mgm_key_does_not_destroy_key_if_pin_not_verified(self, session): - session.authenticate(mgm_key_type(session), DEFAULT_MANAGEMENT_KEY) + def test_set_stored_mgm_key_does_not_destroy_key_if_pin_not_verified( + self, session, keys + ): + session.authenticate(mgm_key_type(session), keys.mgmt) with pytest.raises(ApduError): pivman_set_mgm_key( session, @@ -446,7 +482,7 @@ def test_set_stored_mgm_key_does_not_destroy_key_if_pin_not_verified(self, sessi store_on_device=True, ) - assert_mgm_key_is(session, DEFAULT_MANAGEMENT_KEY) + assert_mgm_key_is(session, keys.mgmt) class TestManagementKeyReadWrite: @@ -455,16 +491,16 @@ class TestManagementKeyReadWrite: key. """ - def test_set_mgm_key_changes_mgm_key(self, session): - session.authenticate(mgm_key_type(session), DEFAULT_MANAGEMENT_KEY) + def test_set_mgm_key_changes_mgm_key(self, session, keys): + session.authenticate(mgm_key_type(session), keys.mgmt) session.set_management_key(mgm_key_type(session), NON_DEFAULT_MANAGEMENT_KEY) - assert_mgm_key_is_not(session, DEFAULT_MANAGEMENT_KEY) + assert_mgm_key_is_not(session, keys.mgmt) assert_mgm_key_is(session, NON_DEFAULT_MANAGEMENT_KEY) - def test_set_stored_mgm_key_succeeds_if_pin_is_verified(self, session): - session.verify_pin(DEFAULT_PIN) - session.authenticate(mgm_key_type(session), DEFAULT_MANAGEMENT_KEY) + def test_set_stored_mgm_key_succeeds_if_pin_is_verified(self, session, keys): + session.verify_pin(keys.pin) + session.authenticate(mgm_key_type(session), keys.mgmt) pivman_set_mgm_key( session, NON_DEFAULT_MANAGEMENT_KEY, @@ -472,7 +508,7 @@ def test_set_stored_mgm_key_succeeds_if_pin_is_verified(self, session): store_on_device=True, ) - assert_mgm_key_is_not(session, DEFAULT_MANAGEMENT_KEY) + assert_mgm_key_is_not(session, keys.mgmt) assert_mgm_key_is(session, NON_DEFAULT_MANAGEMENT_KEY) pivman_prot = get_pivman_protected_data(session) @@ -488,43 +524,46 @@ def sign(session, slot, key_type, message): class TestOperations: @condition.min_version(4) - def test_sign_with_pin_policy_always_requires_pin_every_time(self, session): - generate_key(session, pin_policy=PIN_POLICY.ALWAYS) + def test_sign_with_pin_policy_always_requires_pin_every_time(self, session, keys): + generate_key(session, keys, pin_policy=PIN_POLICY.ALWAYS) with pytest.raises(ApduError): sign(session, SLOT.AUTHENTICATION, KEY_TYPE.ECCP256, b"foo") - session.verify_pin(DEFAULT_PIN) + session.verify_pin(keys.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) + session.verify_pin(keys.pin) sig = sign(session, SLOT.AUTHENTICATION, KEY_TYPE.ECCP256, b"foo") assert sig @condition.yk4_fips(False) + @condition.check(lambda info: CAPABILITY.PIV not in info.fips_capable) @condition.min_version(4) - def test_sign_with_pin_policy_never_does_not_require_pin(self, session): - generate_key(session, pin_policy=PIN_POLICY.NEVER) + def test_sign_with_pin_policy_never_does_not_require_pin(self, session, keys): + generate_key(session, keys, pin_policy=PIN_POLICY.NEVER) sig = sign(session, SLOT.AUTHENTICATION, KEY_TYPE.ECCP256, b"foo") assert sig @condition.yk4_fips(True) - def test_pin_policy_never_blocked_on_fips(self, session): + def test_pin_policy_never_blocked_on_fips(self, session, keys): with pytest.raises(NotSupportedError): - generate_key(session, pin_policy=PIN_POLICY.NEVER) + generate_key(session, keys, pin_policy=PIN_POLICY.NEVER) @condition.min_version(4) - def test_sign_with_pin_policy_once_requires_pin_once_per_session(self, session): - generate_key(session, pin_policy=PIN_POLICY.ONCE) + def test_sign_with_pin_policy_once_requires_pin_once_per_session( + self, session, keys + ): + generate_key(session, keys, pin_policy=PIN_POLICY.ONCE) with pytest.raises(ApduError): sign(session, SLOT.AUTHENTICATION, KEY_TYPE.ECCP256, b"foo") - session.verify_pin(DEFAULT_PIN) + session.verify_pin(keys.pin) sig = sign(session, SLOT.AUTHENTICATION, KEY_TYPE.ECCP256, b"foo") assert sig @@ -536,19 +575,19 @@ def test_sign_with_pin_policy_once_requires_pin_once_per_session(self, session): with pytest.raises(ApduError): sign(session, SLOT.AUTHENTICATION, KEY_TYPE.ECCP256, b"foo") - session.verify_pin(DEFAULT_PIN) + session.verify_pin(keys.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(self, session): - public_key = generate_key(session) + def test_signature_can_be_verified_by_public_key(self, session, keys): + public_key = generate_key(session, keys) signed_data = bytes(random.randint(0, 255) for i in range(32)) - session.verify_pin(DEFAULT_PIN) + session.verify_pin(keys.pin) sig = sign(session, SLOT.AUTHENTICATION, KEY_TYPE.ECCP256, signed_data) assert sig @@ -564,62 +603,64 @@ def block_pin(session): class TestUnblockPin: - def test_unblock_pin_requires_no_previous_authentication(self, session): - session.unblock_pin(DEFAULT_PUK, NON_DEFAULT_PIN) + def test_unblock_pin_requires_no_previous_authentication(self, session, keys): + session.unblock_pin(keys.puk, NON_DEFAULT_PIN) def test_unblock_pin_with_wrong_puk_throws_InvalidPinError(self, session): with pytest.raises(InvalidPinError): session.unblock_pin(NON_DEFAULT_PUK, NON_DEFAULT_PIN) - def test_unblock_pin_resets_pin_and_retries(self, session): - session.reset() - reset_state(session) - + def test_unblock_pin_resets_pin_and_retries(self, session, keys): block_pin(session) with pytest.raises(InvalidPinError): - session.verify_pin(DEFAULT_PIN) + session.verify_pin(keys.pin) - session.unblock_pin(DEFAULT_PUK, NON_DEFAULT_PIN) + session.unblock_pin(keys.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(self, session, version): + def test_set_pin_retries_requires_pin_and_mgm_key( + self, session, version, default_keys + ): + keys = default_keys + # Fails with no authentication with pytest.raises(ApduError): session.set_pin_attempts(4, 4) # Fails with only PIN - session.verify_pin(DEFAULT_PIN) + session.verify_pin(keys.pin) with pytest.raises(ApduError): session.set_pin_attempts(4, 4) reset_state(session) - session.authenticate(mgm_key_type(session), DEFAULT_MANAGEMENT_KEY) + session.authenticate(mgm_key_type(session), keys.mgmt) # Fails with only management key (requirement added in 0.1.3) if version >= (0, 1, 3): with pytest.raises(ApduError): session.set_pin_attempts(4, 4) # Succeeds with both PIN and management key - session.verify_pin(DEFAULT_PIN) + session.verify_pin(keys.pin) session.set_pin_attempts(4, 4) - def test_set_pin_retries_sets_pin_and_puk_tries(self, session): + def test_set_pin_retries_sets_pin_and_puk_tries(self, session, default_keys): + keys = default_keys pin_tries = 9 puk_tries = 7 - session.verify_pin(DEFAULT_PIN) - session.authenticate(mgm_key_type(session), DEFAULT_MANAGEMENT_KEY) + session.verify_pin(keys.pin) + session.authenticate(mgm_key_type(session), keys.mgmt) 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) + session.change_puk(NON_DEFAULT_PUK, keys.puk) assert ctx.value.attempts_remaining == puk_tries - 1 @@ -635,10 +676,10 @@ def test_pin_metadata(self, session): assert data.total_attempts == 3 assert data.attempts_remaining == 3 - def test_management_key_metadata(self, session, version): + def test_management_key_metadata(self, session, info): data = session.get_management_key_metadata() default_type = data.key_type - if version < (5, 7, 0): + if info.version < (5, 7, 0): assert data.key_type == MANAGEMENT_KEY_TYPE.TDES else: assert data.key_type == MANAGEMENT_KEY_TYPE.AES192 @@ -658,16 +699,19 @@ def test_management_key_metadata(self, session, version): data = session.get_management_key_metadata() assert data.default_value is True - session.set_management_key(MANAGEMENT_KEY_TYPE.TDES, NON_DEFAULT_MANAGEMENT_KEY) - data = session.get_management_key_metadata() - assert data.default_value is False + if CAPABILITY.PIV not in info.fips_capable: + session.set_management_key( + MANAGEMENT_KEY_TYPE.TDES, NON_DEFAULT_MANAGEMENT_KEY + ) + data = session.get_management_key_metadata() + assert data.default_value is False @pytest.mark.parametrize("key_type", list(KEY_TYPE)) - def test_slot_metadata_generate(self, session, info, key_type): + def test_slot_metadata_generate(self, session, info, keys, key_type): skip_unsupported_key_type(key_type, info) slot = SLOT.SIGNATURE - key = generate_key(session, slot, key_type) + key = generate_key(session, keys, slot, key_type) data = session.get_slot_metadata(slot) assert data.key_type == key_type @@ -700,8 +744,10 @@ def test_slot_metadata_generate(self, session, info, key_type): (SLOT.CARD_AUTH, PIN_POLICY.NEVER), ], ) - def test_slot_metadata_put(self, session, key, slot, pin_policy): - session.authenticate(mgm_key_type(session), DEFAULT_MANAGEMENT_KEY) + def test_slot_metadata_put(self, session, info, keys, key, slot, pin_policy): + key_type = KEY_TYPE.from_public_key(key.public_key()) + skip_unsupported_key_type(key_type, info, pin_policy) + session.authenticate(mgm_key_type(session), keys.mgmt) session.put_key(slot, key) data = session.get_slot_metadata(slot) @@ -724,9 +770,9 @@ class TestMoveAndDelete: def preconditions(self): pass - def test_move_key(self, session): + def test_move_key(self, session, keys): key = ec.generate_private_key(ec.SECP256R1(), default_backend()) - session.authenticate(mgm_key_type(session), DEFAULT_MANAGEMENT_KEY) + session.authenticate(mgm_key_type(session), keys.mgmt) session.put_key(SLOT.AUTHENTICATION, key) data_a = session.get_slot_metadata(SLOT.AUTHENTICATION) @@ -737,9 +783,9 @@ def test_move_key(self, session): with pytest.raises(ApduError): session.get_slot_metadata(SLOT.AUTHENTICATION) - def test_delete_key(self, session): + def test_delete_key(self, session, keys): key = ec.generate_private_key(ec.SECP256R1(), default_backend()) - session.authenticate(mgm_key_type(session), DEFAULT_MANAGEMENT_KEY) + session.authenticate(mgm_key_type(session), keys.mgmt) session.put_key(SLOT.AUTHENTICATION, key) session.get_slot_metadata(SLOT.AUTHENTICATION) diff --git a/tests/test_piv.py b/tests/test_piv.py index 283ba416..461612b8 100644 --- a/tests/test_piv.py +++ b/tests/test_piv.py @@ -6,7 +6,7 @@ MANAGEMENT_KEY_TYPE, PIN_POLICY, TOUCH_POLICY, - check_key_support, + _do_check_key_support, ) import pytest @@ -46,7 +46,7 @@ def test_generate_random_management_key(self): def test_supported_algorithms(self): with pytest.raises(NotSupportedError): - check_key_support( + _do_check_key_support( Version(3, 1, 1), KEY_TYPE.ECCP384, PIN_POLICY.DEFAULT, @@ -54,26 +54,45 @@ def test_supported_algorithms(self): ) with pytest.raises(NotSupportedError): - check_key_support( + _do_check_key_support( Version(4, 4, 1), KEY_TYPE.RSA1024, PIN_POLICY.DEFAULT, TOUCH_POLICY.DEFAULT, ) + for key_type in (KEY_TYPE.RSA1024, KEY_TYPE.X25519): + with pytest.raises(NotSupportedError): + _do_check_key_support( + Version(5, 7, 0), + key_type, + PIN_POLICY.DEFAULT, + TOUCH_POLICY.DEFAULT, + fips_restrictions=True, + ) + + with pytest.raises(NotSupportedError): + _do_check_key_support( + Version(5, 7, 0), + KEY_TYPE.RSA2048, + PIN_POLICY.NEVER, + TOUCH_POLICY.DEFAULT, + fips_restrictions=True, + ) + for key_type in (KEY_TYPE.RSA1024, KEY_TYPE.RSA2048): with pytest.raises(NotSupportedError): - check_key_support( + _do_check_key_support( Version(4, 3, 4), key_type, PIN_POLICY.DEFAULT, TOUCH_POLICY.DEFAULT ) for key_type in (KEY_TYPE.ED25519, KEY_TYPE.X25519): with pytest.raises(NotSupportedError): - check_key_support( + _do_check_key_support( Version(5, 6, 0), key_type, PIN_POLICY.DEFAULT, TOUCH_POLICY.DEFAULT ) for key_type in KEY_TYPE: - check_key_support( + _do_check_key_support( Version(5, 7, 0), key_type, PIN_POLICY.DEFAULT, TOUCH_POLICY.DEFAULT ) diff --git a/ykman/_cli/fido.py b/ykman/_cli/fido.py index 5e37ec3b..d1b66122 100755 --- a/ykman/_cli/fido.py +++ b/ykman/_cli/fido.py @@ -98,14 +98,18 @@ def info(ctx): """ Display general status of the FIDO2 application. """ - conn = ctx.obj["conn"] + info = ctx.obj["info"] ctap2 = ctx.obj.get("ctap2") - info: Dict = {} - lines: List = [info] - if is_yk4_fips(ctx.obj["info"]): - info["FIPS Approved Mode"] = "Yes" if is_in_fips_mode(conn) else "No" - elif ctap2: + data: Dict = {} + lines: List = [data] + + if CAPABILITY.FIDO2 in info.fips_capable: + data["FIPS approved"] = CAPABILITY.FIDO2 in info.fips_approved + elif is_yk4_fips(info): + data["FIPS approved"] = is_in_fips_mode(ctx.obj["conn"]) + + if ctap2: client_pin = ClientPin(ctap2) # N.B. All YubiKeys with CTAP2 support PIN. if ctap2.info.options["clientPin"]: if ctap2.info.force_pin_change: @@ -115,42 +119,41 @@ def info(ctx): ) pin_retries, power_cycle = client_pin.get_pin_retries() if pin_retries: - info["PIN"] = f"{pin_retries} attempt(s) remaining" + data["PIN"] = f"{pin_retries} attempt(s) remaining" if power_cycle: lines.append( "PIN is temporarily blocked. " "Remove and re-insert the YubiKey to unblock." ) else: - info["PIN"] = "blocked" + data["PIN"] = "Blocked" else: - info["PIN"] = "not set" - info["Minimum PIN length"] = ctap2.info.min_pin_length + data["PIN"] = "Not set" + data["Minimum PIN length"] = ctap2.info.min_pin_length bio_enroll = ctap2.info.options.get("bioEnroll") if bio_enroll: uv_retries = client_pin.get_uv_retries() if uv_retries: - info["Fingerprints"] = f"registered, {uv_retries} attempt(s) remaining" + data["Fingerprints"] = f"Registered, {uv_retries} attempt(s) remaining" else: - info["Fingerprints"] = "registered, blocked until PIN is verified" + data["Fingerprints"] = "Registered, blocked until PIN is verified" elif bio_enroll is False: - info["Fingerprints"] = "not registered" + data["Fingerprints"] = "Not registered" always_uv = ctap2.info.options.get("alwaysUv") if always_uv is not None: - info["Always Require UV"] = "on" if always_uv else "off" + data["Always Require UV"] = "On" if always_uv else "Off" remaining_creds = ctap2.info.remaining_disc_creds if remaining_creds is not None: - info["Credential storage remaining"] = remaining_creds + data["Credential storage remaining"] = remaining_creds ep = ctap2.info.options.get("ep") if ep is not None: - info["Enterprise Attestation"] = "enabled" if ep else "disabled" - + data["Enterprise Attestation"] = "Enabled" if ep else "Disabled" else: - info["PIN"] = "not supported" + data["PIN"] = "Not supported" click.echo("\n".join(pretty_print(lines))) @@ -891,6 +894,10 @@ def toggle_always_uv(ctx, pin): if not options or "alwaysUv" not in options: raise CliFail("Always Require UV is not supported on this YubiKey.") + info = ctx.obj["info"] + if CAPABILITY.FIDO2 in info.fips_capable: + raise CliFail("Always Require UV can not be disabled on this YubiKey.") + config = _init_config(ctx, pin) config.toggle_always_uv() diff --git a/ykman/_cli/hsmauth.py b/ykman/_cli/hsmauth.py index 2239323e..5ccd8d01 100644 --- a/ykman/_cli/hsmauth.py +++ b/ykman/_cli/hsmauth.py @@ -31,8 +31,9 @@ InvalidPinError, ALGORITHM, MANAGEMENT_KEY_LEN, - DEFAULT_MANAGEMENT_KEY, + CREDENTIAL_PASSWORD_LEN, ) +from yubikit.management import CAPABILITY from yubikit.core.smartcard import ApduError, SW from ..util import parse_private_key, InvalidPasswordError @@ -61,7 +62,9 @@ logger = logging.getLogger(__name__) -def handle_credential_error(e: Exception, default_exception_msg): +def handle_credential_error( + e: Exception, default_exception_msg, target="Credential password" +): if isinstance(e, InvalidPinError): attempts = e.attempts_remaining if attempts: @@ -78,7 +81,7 @@ def handle_credential_error(e: Exception, default_exception_msg): elif e.sw == SW.SECURITY_CONDITION_NOT_SATISFIED: raise CliFail("The device was not touched.") elif e.sw == SW.CONDITIONS_NOT_SATISFIED: - raise CliFail("Credential password does not meet complexity requirement.") + raise CliFail(f"{target} does not meet complexity requirement.") raise CliFail(default_exception_msg) @@ -96,26 +99,31 @@ def _parse_algorithm(algorithm: ALGORITHM) -> str: return "Asymmetric" -def _parse_key(key, key_len, key_type): +def _parse_hex(hex): try: - key = bytes.fromhex(key) - except Exception: - ValueError(key) + return bytes.fromhex(hex) + except Exception as e: + raise ValueError(e) + + +def _parse_key(key, key_len, key_type): + key = _parse_hex(key) if len(key) != key_len: raise ValueError( - f"{key_type} must be exactly {key_len} bytes long " - f"({key_len * 2} hexadecimal digits) long" + f"must be exactly {key_len} bytes long ({key_len * 2} hexadecimal digits) " + "long" ) return key -def _parse_hex(hex): - try: - val = bytes.fromhex(hex) - return val - except Exception: - raise ValueError(hex) +def _parse_password(value, key_len, name): + encoded = value.encode() + if len(encoded) <= key_len: + return encoded.ljust(key_len, b"\0") + if len(encoded) == key_len * 2: + return _parse_hex(value) + raise ValueError(f"{name} must be at most 16 bytes") @click_callback() @@ -123,6 +131,16 @@ def click_parse_management_key(ctx, param, val): return _parse_key(val, MANAGEMENT_KEY_LEN, "Management key") +@click_callback() +def click_parse_management_password(ctx, param, val): + return _parse_password(val, MANAGEMENT_KEY_LEN, "Management password") + + +@click_callback() +def click_parse_credential_password(ctx, param, val): + return _parse_password(val, CREDENTIAL_PASSWORD_LEN, "Credential password") + + @click_callback() def click_parse_enc_key(ctx, param, val): return _parse_key(val, ALGORITHM.AES128_YUBICO_AUTHENTICATION.key_len, "ENC key") @@ -143,29 +161,36 @@ def click_parse_context(ctx, param, val): return _parse_hex(val) -def _prompt_management_key(prompt="Enter a management key [blank to use default key]"): - management_key = click_prompt( - prompt, default="", hide_input=True, show_default=False +def _prompt_management_key(prompt="Enter management password", confirm=False): + management_password = click_prompt( + prompt, + default="", + hide_input=True, + show_default=False, + confirmation_prompt=confirm, + ) + return _parse_password( + management_password, MANAGEMENT_KEY_LEN, "Management password" ) - if management_key == "": - return DEFAULT_MANAGEMENT_KEY - - return _parse_key(management_key, MANAGEMENT_KEY_LEN, "Management key") def _prompt_credential_password(prompt="Enter credential password"): credential_password = click_prompt( - prompt, default="", hide_input=True, show_default=False + prompt, + hide_input=True, + confirmation_prompt=True, ) - return credential_password + return _parse_password( + credential_password, CREDENTIAL_PASSWORD_LEN, "Credential password" + ) -def _prompt_symmetric_key(type): - symmetric_key = click_prompt(f"Enter {type}", default="", show_default=False) +def _prompt_symmetric_key(name): + symmetric_key = click_prompt(f"Enter {name}") return _parse_key( - symmetric_key, ALGORITHM.AES128_YUBICO_AUTHENTICATION.key_len, "ENC key" + symmetric_key, ALGORITHM.AES128_YUBICO_AUTHENTICATION.key_len, name ) @@ -174,16 +199,22 @@ def _fname(fobj): click_credential_password_option = click.option( - "-c", "--credential-password", help="password to protect credential" + "-c", + "--credential-password", + help="password to protect credential", + callback=click_parse_credential_password, ) click_management_key_option = click.option( "-m", + "--management-password", "--management-key", - help="the management key", - callback=click_parse_management_key, + "management_key", + help="the management password", + callback=click_parse_management_password, ) + click_touch_option = click.option( "-t", "--touch", is_flag=True, help="require touch on YubiKey to access credential" ) @@ -195,13 +226,16 @@ def _fname(fobj): def hsmauth(ctx): """ Manage the YubiHSM Auth application - - """ dev = ctx.obj["device"] conn = dev.open_connection(SmartCardConnection) ctx.call_on_close(conn.close) ctx.obj["session"] = HsmAuthSession(conn) + info = ctx.obj["info"] + ctx.obj["fips_unready"] = ( + CAPABILITY.HSMAUTH in info.fips_capable + and CAPABILITY.HSMAUTH not in info.fips_approved + ) @hsmauth.command() @@ -210,8 +244,13 @@ def info(ctx): """ Display general status of the YubiHSM Auth application. """ - info = get_hsmauth_info(ctx.obj["session"]) - click.echo("\n".join(pretty_print(info))) + info = ctx.obj["info"] + data = get_hsmauth_info(ctx.obj["session"]) + if CAPABILITY.HSMAUTH in info.fips_capable: + # This is a bit ugly as it makes assumptions about the structure of data + data["FIPS approved"] = CAPABILITY.HSMAUTH in info.fips_approved + + click.echo("\n".join(pretty_print(data))) @hsmauth.command() @@ -236,10 +275,7 @@ def reset(ctx, force): ctx.obj["session"].reset() click.echo("Success! All YubiHSM Auth data have been cleared from the YubiKey.") - click.echo( - "Your YubiKey now has the default Management Key" - f"({DEFAULT_MANAGEMENT_KEY.hex()})." - ) + click.echo("Your YubiKey now has an empty Management password.") @hsmauth.group() @@ -314,12 +350,17 @@ def generate(ctx, label, credential_password, management_key, touch): LABEL label for the YubiHSM Auth credential """ - if not credential_password: - credential_password = _prompt_credential_password() + if ctx.obj["fips_unready"]: + raise CliFail( + "YubiKey FIPS must be in FIPS approved mode prior to adding credentials" + ) - if not management_key: + if management_key is None: management_key = _prompt_management_key() + if credential_password is None: + credential_password = _prompt_credential_password() + session = ctx.obj["session"] try: @@ -352,12 +393,17 @@ def import_credential( LABEL label for the YubiHSM Auth credential PRIVATE-KEY file containing the private key (use '-' to use stdin) """ - if not credential_password: - credential_password = _prompt_credential_password() + if ctx.obj["fips_unready"]: + raise CliFail( + "YubiKey FIPS must be in FIPS approved mode prior to adding credentials" + ) - if not management_key: + if management_key is None: management_key = _prompt_management_key() + if credential_password is None: + credential_password = _prompt_credential_password() + session = ctx.obj["session"] data = private_key.read() @@ -458,12 +504,17 @@ def symmetric( LABEL label for the YubiHSM Auth credential """ - if not credential_password: - credential_password = _prompt_credential_password() + if ctx.obj["fips_unready"]: + raise CliFail( + "YubiKey FIPS must be in FIPS approved mode prior to adding credentials" + ) - if not management_key: + if management_key is None: management_key = _prompt_management_key() + if credential_password is None: + credential_password = _prompt_credential_password() + if generate and (enc_key or mac_key): ctx.fail("--enc-key and --mac-key cannot be combined with --generate") @@ -516,15 +567,22 @@ def derive(ctx, label, derivation_password, credential_password, management_key, LABEL label for the YubiHSM Auth credential """ - if not credential_password: - credential_password = _prompt_credential_password() + if ctx.obj["fips_unready"]: + raise CliFail( + "YubiKey FIPS must be in FIPS approved mode prior to adding credentials" + ) - if not management_key: + if management_key is None: management_key = _prompt_management_key() - if not derivation_password: + if credential_password is None: + credential_password = _prompt_credential_password() + + if derivation_password is None: derivation_password = click_prompt( - "Enter derivation password", default="", show_default=False + "Enter derivation password", + hide_input=True, + confirmation_prompt=True, ) session = ctx.obj["session"] @@ -554,7 +612,7 @@ def delete(ctx, label, management_key, force): LABEL a label to match a single credential (as shown in "list") """ - if not management_key: + if management_key is None: management_key = _prompt_management_key() force or click.confirm( @@ -579,13 +637,12 @@ def access(): """Manage Management Key for YubiHSM Auth""" -@access.command() +@access.command(hidden=True) @click.pass_context @click.option( "-m", "--management-key", help="current management key", - default=DEFAULT_MANAGEMENT_KEY, show_default=True, callback=click_parse_management_key, ) @@ -610,7 +667,7 @@ def change_management_key(ctx, management_key, new_management_key, generate): YubiHSM Auth credentials stored on the YubiKey. """ - if not management_key: + if management_key is None: management_key = _prompt_management_key( "Enter current management key [blank to use default key]" ) @@ -621,7 +678,7 @@ def change_management_key(ctx, management_key, new_management_key, generate): if new_management_key and generate: ctx.fail("Invalid options: --new-management-key conflicts with --generate") - if not new_management_key: + if new_management_key is None: if generate: new_management_key = generate_random_management_key() click.echo(f"Generated management key: {new_management_key.hex()}") @@ -647,5 +704,53 @@ def change_management_key(ctx, management_key, new_management_key, generate): session.put_management_key(management_key, new_management_key) except Exception as e: handle_credential_error( - e, default_exception_msg="Failed to change management key." + e, + default_exception_msg="Failed to change management key.", + target="Management key", + ) + + +@access.command() +@click.pass_context +@click.option( + "-m", + "--management-password", + "management_key", + help="current management password", + show_default=True, + callback=click_parse_management_password, +) +@click.option( + "-n", + "--new-management-password", + "new_management_key", + help="a new management password to set", + callback=click_parse_management_password, +) +def change_management_password(ctx, management_key, new_management_key): + """ + Change the management key. + + Allows you to change the management key which is required to add and delete + YubiHSM Auth credentials stored on the YubiKey. + """ + + if management_key is None: + management_key = _prompt_management_key( + "Enter your current management password", + ) + + if new_management_key is None: + new_management_key = _prompt_management_key( + "Enter a new management password", confirm=True + ) + + session = ctx.obj["session"] + try: + session.put_management_key(management_key, new_management_key) + except Exception as e: + handle_credential_error( + e, + default_exception_msg="Failed to change management password.", + target="Management password", ) diff --git a/ykman/_cli/info.py b/ykman/_cli/info.py index a5767e83..59a62bcd 100644 --- a/ykman/_cli/info.py +++ b/ykman/_cli/info.py @@ -34,7 +34,7 @@ from yubikit.oath import OathSession from yubikit.support import get_name -from .util import CliFail, is_yk4_fips, click_command +from .util import CliFail, is_yk4_fips, click_command, pretty_print from ..otp import is_in_fips_mode as otp_in_fips_mode from ..oath import is_in_fips_mode as oath_in_fips_mode from ..fido import is_in_fips_mode as ctap_in_fips_mode @@ -196,6 +196,12 @@ def info(ctx, check_fips): info.supported_capabilities, info.config.enabled_capabilities ) + if info.fips_capable: + click.echo() + click.echo("FIPS approved applications") + data = {c.display_name: c in info.fips_approved for c in info.fips_capable} + click.echo("\n".join(pretty_print(data))) + if check_fips: click.echo() if is_yk4_fips(info): diff --git a/ykman/_cli/oath.py b/ykman/_cli/oath.py index 4a1ada8e..b3273110 100644 --- a/ykman/_cli/oath.py +++ b/ykman/_cli/oath.py @@ -25,8 +25,6 @@ # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. -import click -import logging from .util import ( CliFail, click_force_option, @@ -39,6 +37,7 @@ prompt_timeout, EnumChoice, is_yk4_fips, + pretty_print, ) from yubikit.core.smartcard import ApduError, SW, SmartCardConnection from yubikit.oath import ( @@ -49,9 +48,13 @@ parse_b32_key, _format_cred_id, ) +from yubikit.management import CAPABILITY from ..oath import is_steam, calculate_steam, is_hidden, delete_broken_credential from ..settings import AppData +from typing import Dict, List, Any +import click +import logging logger = logging.getLogger(__name__) @@ -84,6 +87,11 @@ def oath(ctx): ctx.call_on_close(conn.close) ctx.obj["session"] = OathSession(conn) ctx.obj["oath_keys"] = AppData("oath_keys") + info = ctx.obj["info"] + ctx.obj["fips_unready"] = ( + CAPABILITY.OATH in info.fips_capable + and CAPABILITY.OATH not in info.fips_approved + ) @oath.command() @@ -93,16 +101,22 @@ def info(ctx): Display general status of the OATH application. """ session = ctx.obj["session"] - version = session.version - click.echo(f"OATH version: {version[0]}.{version[1]}.{version[2]}") - click.echo("Password protection: " + ("enabled" if session.locked else "disabled")) + info = ctx.obj["info"] + data: Dict[str, Any] = {"OATH version": "%d.%d.%d" % session.version} + lines: List[Any] = [data] + + if CAPABILITY.OATH in info.fips_capable: + # This is a bit ugly as it makes assumptions about the structure of data + data["FIPS approved"] = CAPABILITY.OATH in info.fips_approved + elif is_yk4_fips(info): + data["FIPS approved"] = session.locked + data["Password protection"] = "enabled" if session.locked else "disabled" keys = ctx.obj["oath_keys"] if session.locked and session.device_id in keys: - click.echo("The password for this YubiKey is remembered by ykman.") + lines.append("The password for this YubiKey is remembered by ykman.") - if is_yk4_fips(ctx.obj["info"]): - click.echo(f"FIPS Approved Mode: {'Yes' if session.locked else 'No'}") + click.echo("\n".join(pretty_print(lines))) @oath.command() @@ -226,8 +240,13 @@ def change(ctx, password, clear, new_password, remember): Allows you to set or change a password that will be required to access the OATH accounts stored on the YubiKey. """ - if clear and new_password: - ctx.fail("--clear cannot be combined with --new-password.") + if clear: + if new_password: + raise CliFail("--clear cannot be combined with --new-password.") + + info = ctx.obj["info"] + if CAPABILITY.OATH in info.fips_capable: + raise CliFail("Removing the password is not allowed on YubiKey FIPS.") _init_session(ctx, password, False, prompt="Enter the current password") @@ -453,6 +472,11 @@ def add( SECRET base32-encoded secret/key value provided by the server """ + if ctx.obj["fips_unready"]: + raise CliFail( + "YubiKey FIPS must be in FIPS approved mode prior to adding accounts" + ) + digits = int(digits) if not secret: @@ -480,7 +504,7 @@ def add( def click_parse_uri(ctx, param, val): try: return CredentialData.parse_uri(val) - except ValueError: + except (ValueError, KeyError): raise click.BadParameter("URI seems to have the wrong format.") @@ -498,6 +522,11 @@ def uri(ctx, data, touch, force, password, remember): Use a URI to add a new account to the YubiKey. """ + if ctx.obj["fips_unready"]: + raise CliFail( + "YubiKey FIPS must be in FIPS approved mode prior to adding accounts" + ) + if not data: while True: uri = click_prompt("Enter an OATH URI (otpauth://)") @@ -521,16 +550,16 @@ def _add_cred(ctx, data, touch, force): version = session.version if not (0 < len(data.name) <= 64): - ctx.fail("Name must be between 1 and 64 bytes.") + raise CliFail("Name must be between 1 and 64 bytes.") if len(data.secret) < 2: - ctx.fail("Secret must be at least 2 bytes.") + raise CliFail("Secret must be at least 2 bytes.") if touch and version < (4, 2, 6): raise CliFail("Require touch is not supported on this YubiKey.") if data.counter and data.oath_type != OATH_TYPE.HOTP: - ctx.fail("Counter only supported for HOTP accounts.") + raise CliFail("Counter only supported for HOTP accounts.") if data.hash_algorithm == HASH_ALGORITHM.SHA512 and ( version < (4, 3, 1) or is_yk4_fips(ctx.obj["info"]) diff --git a/ykman/_cli/openpgp.py b/ykman/_cli/openpgp.py index 08fdfa62..f29915a8 100644 --- a/ykman/_cli/openpgp.py +++ b/ykman/_cli/openpgp.py @@ -27,6 +27,7 @@ from yubikit.core.smartcard import ApduError, SW, SmartCardConnection from yubikit.openpgp import OpenPgpSession, UIF, PIN_POLICY, KEY_REF as _KEY_REF +from yubikit.management import CAPABILITY from ..util import parse_certificates, parse_private_key from ..openpgp import get_openpgp_info from .util import ( @@ -90,8 +91,12 @@ def info(ctx): """ Display general status of the OpenPGP application. """ - session = ctx.obj["session"] - click.echo("\n".join(pretty_print(get_openpgp_info(session)))) + info = ctx.obj["info"] + data = get_openpgp_info(ctx.obj["session"]) + if CAPABILITY.OPENPGP in info.fips_capable: + # This is a bit ugly as it makes assumptions about the structure of data + data["FIPS approved"] = CAPABILITY.OPENPGP in info.fips_approved + click.echo("\n".join(pretty_print(data))) @openpgp.command() diff --git a/ykman/_cli/piv.py b/ykman/_cli/piv.py index bb9c1a89..929eb454 100644 --- a/ykman/_cli/piv.py +++ b/ykman/_cli/piv.py @@ -199,6 +199,10 @@ def piv(ctx): session = PivSession(conn) ctx.obj["session"] = session ctx.obj["pivman_data"] = get_pivman_data(session) + info = ctx.obj["info"] + ctx.obj["fips_unready"] = ( + CAPABILITY.PIV in info.fips_capable and CAPABILITY.PIV not in info.fips_approved + ) @piv.command() @@ -207,8 +211,12 @@ def info(ctx): """ Display general status of the PIV application. """ - info = get_piv_info(ctx.obj["session"]) - click.echo("\n".join(pretty_print(info))) + info = ctx.obj["info"] + data = get_piv_info(ctx.obj["session"]) + if CAPABILITY.PIV in info.fips_capable: + # This is a bit ugly as it makes assumptions about the structure of data + data[0]["FIPS approved"] = CAPABILITY.PIV in info.fips_approved + click.echo("\n".join(pretty_print(data))) @piv.command() @@ -264,6 +272,16 @@ def set_pin_retries(ctx, management_key, pin, pin_retries, puk_retries, force): NOTE: This will reset the PIN and PUK to their factory defaults. """ session = ctx.obj["session"] + info = ctx.obj["info"] + if CAPABILITY.PIV in info.fips_capable: + if not ( + session.get_pin_metadata().default_value + and session.get_puk_metadata().default_value + ): + raise CliFail( + "Retry attempts must be set before PIN/PUK have been changed." + ) + _ensure_authenticated( ctx, pin, management_key, require_pin_and_key=True, no_prompt=force ) @@ -404,8 +422,6 @@ def change_puk(ctx, puk, new_puk): "--algorithm", help="management key algorithm", type=EnumChoice(MANAGEMENT_KEY_TYPE), - default=MANAGEMENT_KEY_TYPE.TDES.name, - show_default=True, ) @click.option( "-p", @@ -442,7 +458,16 @@ def change_management_key( A random key may be generated and stored on the YubiKey, protected by PIN. """ session = ctx.obj["session"] - pivman = ctx.obj["pivman_data"] + + if not algorithm: + try: + algorithm = session.get_management_key_metadata().key_type + except NotSupportedError: + algorithm = MANAGEMENT_KEY_TYPE.TDES + + info = ctx.obj["info"] + if CAPABILITY.PIV in info.fips_capable and algorithm in (MANAGEMENT_KEY_TYPE.TDES,): + raise CliFail(f"{algorithm.name} not supported on YubiKey FIPS.") pin_verified = _ensure_authenticated( ctx, @@ -455,13 +480,14 @@ def change_management_key( # Can't combine new key with generate. if new_management_key and generate: - ctx.fail("Invalid options: --new-management-key conflicts with --generate") + raise CliFail("Invalid options: --new-management-key conflicts with --generate") # Touch not supported on NEO. if touch and session.version < (4, 0, 0): raise CliFail("Require touch not supported on this YubiKey.") # If an old stored key needs to be cleared, the PIN is needed. + pivman = ctx.obj["pivman_data"] if not pin_verified and pivman.has_stored_key: if pin: _verify_pin(ctx, session, pivman, pin, no_prompt=force) @@ -479,7 +505,7 @@ def change_management_key( if not protect: click.echo(f"Generated management key: {new_management_key.hex()}") elif force: - ctx.fail( + raise CliFail( "New management key not given. Remove the --force " "flag, or set the --generate flag or the " "--new-management-key option." @@ -494,7 +520,7 @@ def change_management_key( ) ) except Exception: - ctx.fail("New management key has the wrong format.") + raise CliFail("New management key has the wrong format.") if len(new_management_key) != algorithm.key_len: raise CliFail( @@ -589,6 +615,12 @@ def generate_key( PUBLIC-KEY file containing the generated public key (use '-' to use stdout) """ + if ctx.obj["fips_unready"]: + raise CliFail( + "YubiKey FIPS must be in FIPS approved mode prior to key generation" + ) + _check_key_support_fips(ctx, algorithm, pin_policy) + session = ctx.obj["session"] _ensure_authenticated(ctx, pin, management_key) @@ -628,6 +660,10 @@ def import_key( SLOT PIV slot of the private key PRIVATE-KEY file containing the private key (use '-' to use stdin) """ + + if ctx.obj["fips_unready"]: + raise CliFail("YubiKey FIPS must be in FIPS approved mode prior to key import") + session = ctx.obj["session"] data = private_key.read() @@ -653,6 +689,10 @@ def import_key( continue break + _check_key_support_fips( + ctx, KEY_TYPE.from_public_key(private_key.public_key()), pin_policy + ) + _ensure_authenticated(ctx, pin, management_key) session.put_key(slot, private_key, pin_policy, touch_policy) @@ -1162,6 +1202,15 @@ def read_object(ctx, pin, object_id, output): session = ctx.obj["session"] pivman = ctx.obj["pivman_data"] + if ctx.obj["fips_unready"] and object_id in ( + OBJECT_ID.PRINTED, + OBJECT_ID.FINGERPRINTS, + OBJECT_ID.FACIAL, + OBJECT_ID.IRIS, + ): + raise CliFail( + "YubiKey FIPS must be in FIPS approved mode to export this object." + ) def do_read_object(retry=True): try: @@ -1234,7 +1283,7 @@ def generate_object(ctx, pin, management_key, object_id): elif OBJECT_ID.CAPABILITY == object_id: session.put_object(OBJECT_ID.CAPABILITY, generate_ccc()) else: - ctx.fail("Unsupported object ID for generate.") + raise CliFail("Unsupported object ID for generate.") def _prompt_management_key(prompt="Enter a management key [blank to use default key]"): @@ -1326,7 +1375,7 @@ def _verify_pin_if_needed(ctx, session, func, pin=None, no_prompt=False): def _authenticate(ctx, session, management_key, mgm_key_prompt, no_prompt=False): if not management_key: if no_prompt: - ctx.fail("Management key required.") + raise CliFail("Management key required.") else: if mgm_key_prompt is None: management_key = _prompt_management_key() @@ -1342,3 +1391,12 @@ def _authenticate(ctx, session, management_key, mgm_key_prompt, no_prompt=False) session.authenticate(key_type, management_key) except Exception: raise CliFail("Authentication with management key failed.") + + +def _check_key_support_fips(ctx, key_type, pin_policy): + info = ctx.obj["info"] + if CAPABILITY.PIV in info.fips_capable: + if key_type in (KEY_TYPE.RSA1024, KEY_TYPE.X25519): + raise CliFail(f"Key type {key_type.name} not supported on YubiKey FIPS") + if pin_policy in (PIN_POLICY.NEVER,): + raise CliFail(f"PIN policy {pin_policy.name} not supported on YubiKey FIPS") diff --git a/ykman/piv.py b/ykman/piv.py index f5911ae2..b335e9bc 100644 --- a/ykman/piv.py +++ b/ykman/piv.py @@ -231,7 +231,7 @@ def get_bytes(self) -> bytes: data += Tlv(0x82, self.salt) if self.pin_timestamp is not None: data += Tlv(0x83, struct.pack(">I", self.pin_timestamp)) - return Tlv(0x80, data) + return Tlv(0x80, data) if data else b"" class PivmanProtectedData: @@ -243,7 +243,7 @@ def get_bytes(self) -> bytes: data = b"" if self.key is not None: data += Tlv(0x89, self.key) - return Tlv(0x88, data) + return Tlv(0x88, data) if data else b"" def get_pivman_data(session: PivSession) -> PivmanData: @@ -296,6 +296,7 @@ def pivman_set_mgm_key( :param store_on_device: If set, the management key is stored on device. """ pivman = get_pivman_data(session) + pivman_old_bytes = pivman.get_bytes() pivman_prot = None if store_on_device or (not store_on_device and pivman.has_stored_key): @@ -318,8 +319,10 @@ def pivman_set_mgm_key( # Set flag for stored or not stored key. pivman.mgm_key_protected = store_on_device - # Update readable pivman data - session.put_object(OBJECT_ID_PIVMAN_DATA, pivman.get_bytes()) + # Update readable pivman data, if changed + pivman_bytes = pivman.get_bytes() + if pivman_old_bytes != pivman_bytes: + session.put_object(OBJECT_ID_PIVMAN_DATA, pivman_bytes) if pivman_prot is not None: if store_on_device: diff --git a/yubikit/core/__init__.py b/yubikit/core/__init__.py index 473af30d..a2117933 100644 --- a/yubikit/core/__init__.py +++ b/yubikit/core/__init__.py @@ -56,6 +56,9 @@ class Version(NamedTuple): def __str__(self): return "%d.%d.%d" % self + def __bool__(self): + return any(self) + @classmethod def from_bytes(cls, data: bytes) -> "Version": return cls(*data) diff --git a/yubikit/hsmauth.py b/yubikit/hsmauth.py index 8a98af11..acd3fc30 100644 --- a/yubikit/hsmauth.py +++ b/yubikit/hsmauth.py @@ -599,14 +599,25 @@ def calculate_session_keys_asymmetric( ) ) - def get_challenge(self, label: str) -> bytes: + def get_challenge( + self, label: str, credential_password: Union[bytes, str, None] = None + ) -> bytes: """Get the Host Challenge. - For symmetric credentials this is Host Challenge, a random - 8 byte value. For asymmetric credentials this is EPK-OCE. + For symmetric credentials this is Host Challenge, a random 8 byte value. + For asymmetric credentials this is EPK-OCE. :param label: The label of the credential. + :param credential_password: The password used to protect access to the + credential, needed for asymmetric credentials. """ require_version(self.version, (5, 6, 0)) - data = Tlv(TAG_LABEL, _parse_label(label)) + + data: bytes = Tlv(TAG_LABEL, _parse_label(label)) + + if credential_password is not None and self.version >= (5, 7, 1): + data += Tlv( + TAG_CREDENTIAL_PASSWORD, _parse_credential_password(credential_password) + ) + return self.protocol.send_apdu(0, INS_GET_CHALLENGE, 0, 0, data) diff --git a/yubikit/management.py b/yubikit/management.py index 5ba57ce5..6dd4c00a 100644 --- a/yubikit/management.py +++ b/yubikit/management.py @@ -74,6 +74,21 @@ def __str__(self): name = "|".join(c.name or str(c) for c in CAPABILITY if c in self) return f"{name}: {hex(self)}" + @classmethod + def _from_fips(cls, fips: int) -> "CAPABILITY": + c = CAPABILITY(0) + if fips & (1 << 0): + c |= CAPABILITY.FIDO2 + if fips & (1 << 1): + c |= CAPABILITY.PIV + if fips & (1 << 2): + c |= CAPABILITY.OPENPGP + if fips & (1 << 3): + c |= CAPABILITY.OATH + if fips & (1 << 4): + c |= CAPABILITY.HSMAUTH + return c + @property def display_name(self) -> str: if self == CAPABILITY.OTP: @@ -174,9 +189,13 @@ class DEVICE_FLAG(IntFlag): TAG_FREE_FORM = 0x11 TAG_HID_INIT_DELAY = 0x12 TAG_PART_NUMBER = 0x13 +TAG_FIPS_CAPABLE = 0x14 +TAG_FIPS_APPROVED = 0x15 TAG_PIN_COMPLEXITY = 0x16 TAG_NFC_RESTRICTED = 0x17 TAG_RESET_BLOCKED = 0x18 +TAG_FPS_VERSION = 0x20 +TAG_STM_VERSION = 0x21 @dataclass @@ -233,8 +252,13 @@ class DeviceInfo: is_locked: bool is_fips: bool = False is_sky: bool = False + part_name: bytes = b"" + fips_capable: CAPABILITY = CAPABILITY(0) + fips_approved: CAPABILITY = CAPABILITY(0) pin_complexity: bool = False reset_blocked: CAPABILITY = CAPABILITY(0) + fps_version: Optional[Version] = None + stm_version: Optional[Version] = None def has_transport(self, transport: TRANSPORT) -> bool: return transport in self.supported_capabilities @@ -277,8 +301,17 @@ def parse_tlvs( supported[TRANSPORT.NFC] = CAPABILITY(bytes2int(data[TAG_NFC_SUPPORTED])) enabled[TRANSPORT.NFC] = CAPABILITY(bytes2int(data[TAG_NFC_ENABLED])) nfc_restricted = data.get(TAG_NFC_RESTRICTED, b"\0") == b"\1" + part_name = data.get(TAG_PART_NUMBER, b"") + fips_capable = CAPABILITY._from_fips( + bytes2int(data.get(TAG_FIPS_CAPABLE, b"\0")) + ) + fips_approved = CAPABILITY._from_fips( + bytes2int(data.get(TAG_FIPS_APPROVED, b"\0")) + ) pin_complexity = data.get(TAG_PIN_COMPLEXITY, b"\0") == b"\1" reset_blocked = CAPABILITY(bytes2int(data.get(TAG_RESET_BLOCKED, b"\0"))) + fps_version = Version.from_bytes(data.get(TAG_FPS_VERSION, b"\0\0\0")) + stm_version = Version.from_bytes(data.get(TAG_STM_VERSION, b"\0\0\0")) return cls( DeviceConfig(enabled, auto_eject_to, chal_resp_to, flags, nfc_restricted), @@ -289,8 +322,13 @@ def parse_tlvs( locked, fips, sky, + part_name, + fips_capable, + fips_approved, pin_complexity, reset_blocked, + fps_version or None, + stm_version or None, ) diff --git a/yubikit/piv.py b/yubikit/piv.py index 32cd0414..d9d25050 100755 --- a/yubikit/piv.py +++ b/yubikit/piv.py @@ -63,6 +63,7 @@ from enum import Enum, IntEnum, unique from typing import Optional, Union, Type, cast +import warnings import logging import gzip import os @@ -422,7 +423,24 @@ def check_key_support( This method will return None if the key (with PIN and touch policies) is supported, or it will raise a NotSupportedError if it is not. + + THIS FUNCTION IS DEPRECATED! Use PivSession.check_key_support() instead. """ + warnings.warn( + "Deprecated: use PivSession.check_key_support() instead.", + DeprecationWarning, + ) + _do_check_key_support(version, key_type, pin_policy, touch_policy, generate) + + +def _do_check_key_support( + version: Version, + key_type: KEY_TYPE, + pin_policy: PIN_POLICY, + touch_policy: TOUCH_POLICY, + generate: bool = True, + fips_restrictions: bool = False, +) -> None: if version[0] == 0 and version > (0, 1, 3): return # Development build, skip version checks @@ -441,13 +459,11 @@ def check_key_support( raise NotSupportedError("RSA key generation not supported on this YubiKey") # FIPS - if (4, 4, 0) <= version < (4, 5, 0): - if key_type == KEY_TYPE.RSA1024: - raise NotSupportedError("RSA 1024 not supported on YubiKey FIPS (4 Series)") + if fips_restrictions or (4, 4, 0) <= version < (4, 5, 0): + if key_type in (KEY_TYPE.RSA1024, KEY_TYPE.X25519): + raise NotSupportedError("RSA 1024 not supported on YubiKey FIPS") if pin_policy == PIN_POLICY.NEVER: - raise NotSupportedError( - "PIN_POLICY.NEVER not allowed on YubiKey FIPS (4 Series)" - ) + raise NotSupportedError("PIN_POLICY.NEVER not allowed on YubiKey FIPS") # New key types if version < (5, 7, 0) and key_type in ( @@ -458,12 +474,6 @@ def check_key_support( ): raise NotSupportedError(f"{key_type} requires YubiKey 5.7 or later") - # TODO: Detect Bio capabilities - if version < () and pin_policy in (PIN_POLICY.MATCH_ONCE, PIN_POLICY.MATCH_ALWAYS): - raise NotSupportedError( - "Biometric match PIN policy requires YubiKey 5.6 or later" - ) - def _parse_device_public_key(key_type, encoded): data = Tlv.parse_dict(encoded) @@ -633,10 +643,24 @@ def verify_pin(self, pin: str) -> None: self._current_pin_retries = retries raise InvalidPinError(retries) - def verify_uv(self) -> bytes: + def verify_uv( + self, temporary_pin: bool = False, check_only: bool = False + ) -> Optional[bytes]: logger.debug("Verifying UV") + if temporary_pin and check_only: + raise ValueError( + "Cannot request temporary PIN when doing check-only verification" + ) + + if check_only: + data = b"" + elif temporary_pin: + data = Tlv(2) + else: + data = Tlv(3) + try: - return self.protocol.send_apdu(0, INS_VERIFY, 0, SLOT_OCC_AUTH) + response = self.protocol.send_apdu(0, INS_VERIFY, 0, SLOT_OCC_AUTH, data) except ApduError as e: if e.sw == SW.REFERENCE_DATA_NOT_FOUND: raise NotSupportedError( @@ -648,6 +672,7 @@ def verify_uv(self) -> bytes: raise InvalidPinError( retries, f"Fingerprint mismatch, {retries} attempts remaining" ) + return response if temporary_pin else None def verify_temporary_pin(self, pin: bytes) -> None: logger.debug("Verifying temporary PIN") @@ -1009,7 +1034,7 @@ def put_key( """ slot = SLOT(slot) key_type = KEY_TYPE.from_public_key(private_key.public_key()) - check_key_support(self.version, key_type, pin_policy, touch_policy, False) + self.check_key_support(key_type, pin_policy, touch_policy, False) ln = key_type.bit_len // 8 if key_type.algorithm == ALGORITHM.RSA: numbers = private_key.private_numbers() @@ -1065,7 +1090,7 @@ def generate_key( """ slot = SLOT(slot) key_type = KEY_TYPE(key_type) - check_key_support(self.version, key_type, pin_policy, touch_policy, True) + self.check_key_support(key_type, pin_policy, touch_policy, True) data: bytes = Tlv(TAG_GEN_ALGORITHM, int2bytes(key_type)) if pin_policy: data += Tlv(TAG_PIN_POLICY, int2bytes(pin_policy)) @@ -1175,3 +1200,34 @@ def _use_private_key(self, slot, key_type, message, exponentiation): if e.sw == SW.INCORRECT_PARAMETERS: raise e # TODO: Different error, No key? raise + + def check_key_support( + self, + key_type: KEY_TYPE, + pin_policy: PIN_POLICY, + touch_policy: TOUCH_POLICY, + generate: bool, + fips_restrictions: bool = False, + ) -> None: + """Check if a key type is supported by this YubiKey. + + This method will return None if the key (with PIN and touch policies) is + supported, or it will raise a NotSupportedError if it is not. + + Set the generate parameter to True to check if generating the key is supported + (in addition to importing). + + Set fips_restrictions to True to apply restrictions based on FIPS status. + """ + + _do_check_key_support( + self.version, + key_type, + pin_policy, + touch_policy, + generate, + fips_restrictions, + ) + + if pin_policy in (PIN_POLICY.MATCH_ONCE, PIN_POLICY.MATCH_ALWAYS): + self.get_bio_metadata()