diff --git a/tests/conftest.py b/tests/conftest.py index 8828e91..dce2795 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -20,12 +20,13 @@ from http import client from fido2.hid import CtapHidDevice -from fido2.client import Fido2Client, UserInteraction, ClientError, _Ctap1ClientBackend +from fido2.client import Fido2Client, UserInteraction, ClientError, _Ctap1ClientBackend, DefaultClientDataCollector from fido2.attestation import FidoU2FAttestation from fido2.ctap2.pin import ClientPin from fido2.server import Fido2Server from fido2.ctap import CtapError -from fido2.webauthn import CollectedClientData, PublicKeyCredentialParameters, PublicKeyCredentialType +from fido2.webauthn import PublicKeyCredentialParameters, PublicKeyCredentialType, PublicKeyCredentialCreationOptions, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, AuthenticatorSelectionCriteria, UserVerificationRequirement, PublicKeyCredentialRequestOptions +from fido2.ctap2.extensions import HmacSecretExtension, LargeBlobExtension, CredBlobExtension, CredProtectExtension, MinPinLengthExtension, CredPropsExtension, ThirdPartyPaymentExtension from utils import * from fido2.cose import ES256 import sys @@ -70,11 +71,13 @@ class DeviceSelectCredential: pass class Device(): - def __init__(self, origin="https://example.com", user_interaction=CliInteraction(),uv="discouraged",rp={"id": "example.com", "name": "Example RP"}, attestation="direct"): + def __init__(self, origin="https://example.com", user_interaction=CliInteraction(), uv="discouraged", rp={"id": "example.com", "name": "Example RP"}, attestation="direct"): self.__user = None self.__set_client(origin=origin, user_interaction=user_interaction, uv=uv) self.__set_server(rp=rp, attestation=attestation) + def __verify_rp(rp_id, origin): + return True def __set_client(self, origin, user_interaction, uv): self.__uv = uv @@ -101,14 +104,23 @@ class Device(): sys.exit(1) # Set up a FIDO 2 client using the origin https://example.com - self.__client = Fido2Client(self.__dev, self.__origin, user_interaction=self.__user_interaction) + extensions = [ + HmacSecretExtension(allow_hmac_secret=True), + LargeBlobExtension(), + CredBlobExtension(), + CredProtectExtension(), + MinPinLengthExtension(), + CredPropsExtension(), + ThirdPartyPaymentExtension() + ] + self.__client = Fido2Client(self.__dev, client_data_collector=DefaultClientDataCollector(self.__origin, verify=Device.__verify_rp), user_interaction=self.__user_interaction, extensions=extensions) # Prefer UV if supported and configured if self.__client.info.options.get("uv") or self.__client.info.options.get("pinUvAuthToken"): self.__uv = "preferred" print("Authenticator supports User Verification") - self.__client1 = Fido2Client(self.__dev, self.__origin, user_interaction=self.__user_interaction) + self.__client1 = Fido2Client(self.__dev, client_data_collector=DefaultClientDataCollector(self.__origin, verify=Device.__verify_rp), user_interaction=self.__user_interaction) self.__client1._backend = _Ctap1ClientBackend(self.__dev, user_interaction=self.__user_interaction) self.ctap1 = self.__client1._backend.ctap1 @@ -117,7 +129,7 @@ class Device(): self.__attestation = attestation self.__server = Fido2Server(self.__rp, attestation=self.__attestation) self.__server.allowed_algorithms = [ - PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, p['alg']) + PublicKeyCredentialParameters(type=PublicKeyCredentialType.PUBLIC_KEY, alg=p['alg']) for p in self.__client._backend.info.algorithms ] @@ -216,9 +228,7 @@ class Device(): 'key_params':key_params}} def doMC(self, client_data=Ellipsis, rp=Ellipsis, user=Ellipsis, key_params=Ellipsis, exclude_list=None, extensions=None, rk=None, user_verification=None, enterprise_attestation=None, event=None, ctap1=False): - client_data = client_data if client_data is not Ellipsis else CollectedClientData.create( - type=CollectedClientData.TYPE.CREATE, origin=self.__origin, challenge=os.urandom(32) - ) + client_data = client_data if client_data is not Ellipsis else DefaultClientDataCollector(origin=self.__origin, verify=Device.__verify_rp) rp = rp if rp is not Ellipsis else self.__rp user = user if user is not Ellipsis else self.user() key_params = key_params if key_params is not Ellipsis else self.__server.allowed_algorithms @@ -226,22 +236,31 @@ class Device(): client = self.__client1 else: client = self.__client - result = client._backend.do_make_credential( - client_data=client_data, - rp=rp, - user=user, - key_params=key_params, - exclude_list=exclude_list, + options=PublicKeyCredentialCreationOptions( + rp=PublicKeyCredentialRpEntity.from_dict(rp), + user=PublicKeyCredentialUserEntity.from_dict(user), + pub_key_cred_params=key_params, + exclude_credentials=exclude_list, extensions=extensions, - rk=rk, - user_verification=user_verification, - enterprise_attestation=enterprise_attestation, + challenge=os.urandom(32), + authenticator_selection=AuthenticatorSelectionCriteria( + require_resident_key=rk, + user_verification=UserVerificationRequirement.REQUIRED if user_verification else UserVerificationRequirement.DISCOURAGED + ), + attestation=enterprise_attestation + ) + client_data, rp_id = client_data.collect_client_data(options=options) + result = client._backend.do_make_credential( + options=options, + client_data=client_data, + rp_id=rp_id, + enterprise_rpid_list=None, event=event ) - return {'res':result,'req':{'client_data':client_data, + return {'res':result.response,'req':{'client_data':client_data, 'rp':rp, 'user':user, - 'key_params':key_params}} + 'key_params':key_params},'client_extension_results':result.client_extension_results} def try_make_credential(self, options=None): if (options is None): @@ -267,14 +286,14 @@ class Device(): # Complete registration auth_data = self.__server.register_complete( - state, result.client_data, result.attestation_object + state=state, response=result ) credentials = [auth_data.credential_data] print("New credential created!") - print("CLIENT DATA:", result.client_data) - print("ATTESTATION OBJECT:", result.attestation_object) + print("CLIENT DATA:", result.response.client_data) + print("ATTESTATION OBJECT:", result.response.attestation_object) print() print("CREDENTIAL DATA:", auth_data.credential_data) @@ -294,17 +313,14 @@ class Device(): self.__server.authenticate_complete( state, credentials, - result.credential_id, - result.client_data, - result.authenticator_data, - result.signature, + result ) print("Credential authenticated!") - print("CLIENT DATA:", result.client_data) + print("CLIENT DATA:", result.response.client_data) print() - print("AUTH DATA:", result.authenticator_data) + print("AUTH DATA:", result.response.authenticator_data) def GA(self, rp_id=Ellipsis, client_data_hash=Ellipsis, allow_list=None, extensions=None, options=None, pin_uv_param=None, pin_uv_protocol=None): rp_id = rp_id if rp_id is not Ellipsis else self.__rp['id'] @@ -325,21 +341,31 @@ class Device(): return self.__client._backend.ctap2.get_next_assertion() def doGA(self, client_data=Ellipsis, rp_id=Ellipsis, allow_list=None, extensions=None, user_verification=None, event=None, ctap1=False, check_only=False): - client_data = client_data if client_data is not Ellipsis else CollectedClientData.create( - type=CollectedClientData.TYPE.GET, origin=self.__origin, challenge=os.urandom(32) - ) + client_data = client_data if client_data is not Ellipsis else DefaultClientDataCollector(origin=self.__origin, verify=Device.__verify_rp) + if (ctap1 is True): + client = self.__client1 + else: + client = self.__client + rp_id = rp_id if rp_id is not Ellipsis else self.__rp['id'] + options=PublicKeyCredentialRequestOptions( + challenge=os.urandom(32), + rp_id=rp_id, + allow_credentials=allow_list, + user_verification=UserVerificationRequirement.REQUIRED if user_verification else UserVerificationRequirement.DISCOURAGED, + extensions=extensions + ) + client_data, rp_id = client_data.collect_client_data(options=options) + if (ctap1 is True): client = self.__client1 else: client = self.__client try: result = client._backend.do_get_assertion( + options=options, client_data=client_data, rp_id=rp_id, - allow_list=allow_list, - extensions=extensions, - user_verification=user_verification, event=event ) except ClientError as e: @@ -347,11 +373,9 @@ class Device(): client_pin = ClientPin(self.__client._backend.ctap2) client_pin.set_pin(DEFAULT_PIN) result = client._backend.do_get_assertion( + options=options, client_data=client_data, rp_id=rp_id, - allow_list=allow_list, - extensions=extensions, - user_verification=user_verification, event=event ) else: @@ -416,8 +440,8 @@ def AuthRes(device, RegRes, *args): {"id": RegRes['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"} ], *args) aut_data = res['res'].get_response(0) - m = aut_data.authenticator_data.rp_id_hash + aut_data.authenticator_data.flags.to_bytes(1, 'big') + aut_data.authenticator_data.counter.to_bytes(4, 'big') + aut_data.client_data.hash - ES256(RegRes['res'].attestation_object.auth_data.credential_data.public_key).verify(m, aut_data.signature) + m = aut_data.response.authenticator_data.rp_id_hash + aut_data.response.authenticator_data.flags.to_bytes(1, 'big') + aut_data.response.authenticator_data.counter.to_bytes(4, 'big') + aut_data.response.client_data.hash + ES256(RegRes['res'].attestation_object.auth_data.credential_data.public_key).verify(m, aut_data.response.signature) return aut_data @pytest.fixture(scope="class") diff --git a/tests/docker/bullseye/Dockerfile b/tests/docker/bullseye/Dockerfile index f4e20f8..b261379 100644 --- a/tests/docker/bullseye/Dockerfile +++ b/tests/docker/bullseye/Dockerfile @@ -22,11 +22,7 @@ RUN apt install -y libccid \ cmake \ libfuse-dev \ && rm -rf /var/lib/apt/lists/* -RUN pip3 install pytest pycvc cryptography pyscard inputimeout -RUN git clone https://github.com/polhenarejos/python-fido2.git -WORKDIR /python-fido2 -RUN git checkout development -RUN pip3 install . +RUN pip3 install pytest pycvc cryptography pyscard inputimeout fido2==2.0.0 WORKDIR / RUN git clone https://github.com/frankmorgner/vsmartcard.git WORKDIR /vsmartcard/virtualsmartcard diff --git a/tests/docker/fido2/__init__.py b/tests/docker/fido2/__init__.py index 83359b2..1755c76 100644 --- a/tests/docker/fido2/__init__.py +++ b/tests/docker/fido2/__init__.py @@ -27,16 +27,17 @@ from __future__ import annotations -from .base import HidDescriptor -from ..ctap import CtapDevice, CtapError, STATUS -from ..utils import LOG_LEVEL_TRAFFIC -from threading import Event -from enum import IntEnum, IntFlag, unique -from typing import Tuple, Optional, Callable, Iterator +import logging +import os import struct import sys -import os -import logging +from enum import IntEnum, IntFlag, unique +from threading import Event +from typing import Callable, Iterator + +from ..ctap import STATUS, CtapDevice, CtapError +from ..utils import LOG_LEVEL_TRAFFIC +from .base import HidDescriptor logger = logging.getLogger(__name__) @@ -55,6 +56,7 @@ elif sys.platform.startswith("openbsd"): from . import openbsd as backend else: raise Exception("Unsupported platform") + from . import emulation as backend list_descriptors = backend.list_descriptors @@ -62,6 +64,10 @@ get_descriptor = backend.get_descriptor open_connection = backend.open_connection +class ConnectionFailure(Exception): + """The CTAP connection failed or returned an invalid response.""" + + @unique class CTAPHID(IntEnum): PING = 0x01 @@ -109,7 +115,7 @@ class CtapHidDevice(CtapDevice): response = self.call(CTAPHID.INIT, nonce) r_nonce, response = response[:8], response[8:] if r_nonce != nonce: - raise Exception("Wrong nonce") + raise ConnectionFailure("Wrong nonce") ( self._channel_id, self._u2fhid_version, @@ -129,7 +135,7 @@ class CtapHidDevice(CtapDevice): return self._u2fhid_version @property - def device_version(self) -> Tuple[int, int, int]: + def device_version(self) -> tuple[int, int, int]: """Device version number.""" return self._device_version @@ -139,12 +145,12 @@ class CtapHidDevice(CtapDevice): return self._capabilities @property - def product_name(self) -> Optional[str]: + def product_name(self) -> str | None: """Product name of device.""" return self.descriptor.product_name @property - def serial_number(self) -> Optional[str]: + def serial_number(self) -> str | None: """Serial number of device.""" return self.descriptor.serial_number @@ -159,10 +165,22 @@ class CtapHidDevice(CtapDevice): self, cmd: int, data: bytes = b"", - event: Optional[Event] = None, - on_keepalive: Optional[Callable[[int], None]] = None, + event: Event | None = None, + on_keepalive: Callable[[STATUS], None] | None = None, ) -> bytes: event = event or Event() + + while True: + try: + return self._do_call(cmd, data, event, on_keepalive) + except CtapError as e: + if e.code == CtapError.ERR.CHANNEL_BUSY: + if not event.wait(0.1): + logger.warning("CTAP channel busy, trying again...") + continue # Keep retrying on BUSY while not cancelled + raise + + def _do_call(self, cmd, data, event, on_keepalive): remaining = data seq = 0 @@ -194,7 +212,7 @@ class CtapHidDevice(CtapDevice): r_channel = struct.unpack_from(">I", recv)[0] recv = recv[4:] if r_channel != self._channel_id: - raise Exception("Wrong channel") + raise ConnectionFailure("Wrong channel") if not response: # Initialization packet r_cmd, r_len = struct.unpack_from(">BH", recv) @@ -202,13 +220,12 @@ class CtapHidDevice(CtapDevice): if r_cmd == TYPE_INIT | cmd: pass # first data packet elif r_cmd == TYPE_INIT | CTAPHID.KEEPALIVE: - ka_status = struct.unpack_from(">B", recv)[0] - logger.debug(f"Got keepalive status: {ka_status:02x}") + try: + ka_status = STATUS(struct.unpack_from(">B", recv)[0]) + logger.debug(f"Got keepalive status: {ka_status:02x}") + except ValueError: + raise ConnectionFailure("Invalid keepalive status") if on_keepalive and ka_status != last_ka: - try: - ka_status = STATUS(ka_status) - except ValueError: - pass # Unknown status value last_ka = ka_status on_keepalive(ka_status) continue @@ -220,7 +237,7 @@ class CtapHidDevice(CtapDevice): r_seq = struct.unpack_from(">B", recv)[0] recv = recv[1:] if r_seq != seq: - raise Exception("Wrong sequence number") + raise ConnectionFailure("Wrong sequence number") seq += 1 response += recv diff --git a/tests/pico-fido/test_020_register.py b/tests/pico-fido/test_020_register.py index 28c37e6..b43c4fe 100644 --- a/tests/pico-fido/test_020_register.py +++ b/tests/pico-fido/test_020_register.py @@ -20,8 +20,6 @@ from fido2.client import CtapError from fido2.cose import ES256, ES384, ES512, EdDSA -import fido2.features -fido2.features.webauthn_json_mapping.enabled = False from utils import ES256K import pytest @@ -51,13 +49,13 @@ def test_bad_type_cdh(device): def test_missing_user(device): with pytest.raises(CtapError) as e: - device.doMC(user=None) + device.MC(user=None) assert e.value.code == CtapError.ERR.MISSING_PARAMETER def test_bad_type_user_user(device): with pytest.raises(CtapError) as e: - device.doMC(user=b"12345678") + device.MC(user=b"12345678") def test_missing_rp(device): with pytest.raises(CtapError) as e: @@ -71,7 +69,7 @@ def test_bad_type_rp(device): def test_missing_pubKeyCredParams(device): with pytest.raises(CtapError) as e: - device.doMC(key_params=None) + device.MC(key_params=None) assert e.value.code == CtapError.ERR.MISSING_PARAMETER @@ -93,35 +91,23 @@ def test_bad_type_options(device): def test_bad_type_rp_name(device): with pytest.raises(CtapError) as e: - device.doMC(rp={"id": "test.org", "name": 8, "icon": "icon"}) + device.MC(rp={"id": "test.org", "name": 8, "icon": "icon"}) def test_bad_type_rp_id(device): with pytest.raises(CtapError) as e: - device.doMC(rp={"id": 8, "name": "name", "icon": "icon"}) - -def test_bad_type_rp_icon(device): - with pytest.raises(CtapError) as e: - device.doMC(rp={"id": "test.org", "name": "name", "icon": 8}) + device.MC(rp={"id": 8, "name": "name", "icon": "icon"}) def test_bad_type_user_name(device): with pytest.raises(CtapError) as e: - device.doMC(user={"id": b"user_id", "name": 8}) + device.MC(user={"id": b"user_id", "name": 8}) def test_bad_type_user_id(device): with pytest.raises(CtapError) as e: - device.doMC(user={"id": "user_id", "name": "name"}) + device.MC(user={"id": "user_id", "name": "name"}) def test_bad_type_user_displayName(device): with pytest.raises(CtapError) as e: - device.doMC(user={"id": "user_id", "name": "name", "displayName": 8}) - -def test_bad_type_user_icon(device): - with pytest.raises(CtapError) as e: - device.doMC(user={"id": "user_id", "name": "name", "icon": 8}) - -def test_bad_type_pubKeyCredParams(device): - with pytest.raises(CtapError) as e: - device.doMC(key_params=["wrong"]) + device.MC(user={"id": "user_id", "name": "name", "displayName": 8}) @pytest.mark.parametrize( "alg", [ES256.ALGORITHM, ES384.ALGORITHM, ES512.ALGORITHM, ES256K.ALGORITHM, EdDSA.ALGORITHM] @@ -132,13 +118,13 @@ def test_algorithms(device, info, alg): def test_missing_pubKeyCredParams_type(device): with pytest.raises(CtapError) as e: - device.doMC(key_params=[{"alg": ES256.ALGORITHM}]) + device.MC(key_params=[{"alg": ES256.ALGORITHM}]) assert e.value.code == CtapError.ERR.INVALID_CBOR def test_missing_pubKeyCredParams_alg(device): with pytest.raises(CtapError) as e: - device.doMC(key_params=[{"type": "public-key"}]) + device.MC(key_params=[{"type": "public-key"}]) assert e.value.code in [ CtapError.ERR.INVALID_CBOR, @@ -147,7 +133,7 @@ def test_missing_pubKeyCredParams_alg(device): def test_bad_type_pubKeyCredParams_alg(device): with pytest.raises(CtapError) as e: - device.doMC(key_params=[{"alg": "7", "type": "public-key"}]) + device.MC(key_params=[{"alg": "7", "type": "public-key"}]) assert e.value.code == CtapError.ERR.CBOR_UNEXPECTED_TYPE @@ -158,26 +144,26 @@ def test_unsupported_algorithm(device): assert e.value.code == CtapError.ERR.UNSUPPORTED_ALGORITHM def test_exclude_list(resetdevice): - resetdevice.doMC(exclude_list=[{"id": b"1234", "type": "rot13"}]) + resetdevice.MC(exclude_list=[{"id": b"1234", "type": "rot13"}]) def test_exclude_list2(resetdevice): - resetdevice.doMC(exclude_list=[{"id": b"1234", "type": "mangoPapayaCoconutNotAPublicKey"}]) + resetdevice.MC(exclude_list=[{"id": b"1234", "type": "mangoPapayaCoconutNotAPublicKey"}]) def test_bad_type_exclude_list(device): with pytest.raises(CtapError) as e: - device.doMC(exclude_list=["1234"]) + device.MC(exclude_list=["1234"]) def test_missing_exclude_list_type(device): with pytest.raises(CtapError) as e: - device.doMC(exclude_list=[{"id": b"1234"}]) + device.MC(exclude_list=[{"id": b"1234"}]) def test_missing_exclude_list_id(device): with pytest.raises(CtapError) as e: - device.doMC(exclude_list=[{"type": "public-key"}]) + device.MC(exclude_list=[{"type": "public-key"}]) def test_bad_type_exclude_list_id(device): with pytest.raises(CtapError) as e: - device.doMC(exclude_list=[{"type": "public-key", "id": "1234"}]) + device.MC(exclude_list=[{"type": "public-key", "id": "1234"}]) def test_bad_type_exclude_list_type(device): with pytest.raises(CtapError) as e: diff --git a/tests/pico-fido/test_021_authenticate.py b/tests/pico-fido/test_021_authenticate.py index 14260b7..96e5338 100644 --- a/tests/pico-fido/test_021_authenticate.py +++ b/tests/pico-fido/test_021_authenticate.py @@ -31,10 +31,10 @@ def test_authenticate(device): AUTRes = device.authenticate(credentials) def test_assertion_auth_data(GARes): - assert len(GARes['res'].get_response(0).authenticator_data) == 37 + assert len(GARes['res'].get_response(0).response.authenticator_data) == 37 def test_Check_that_AT_flag_is_not_set(GARes): - assert (GARes['res'].get_response(0).authenticator_data.flags & 0xF8) == 0 + assert (GARes['res'].get_response(0).response.authenticator_data.flags & 0xF8) == 0 def test_that_user_credential_and_numberOfCredentials_are_not_present(device, MCRes): res = device.GA(allow_list=[ @@ -63,8 +63,8 @@ def test_get_assertion_allow_list_filtering_and_buffering(device): """ Check that authenticator filters and stores items in allow list correctly """ allow_list = [] - rp1 = {"id": "rp1.com", "name": "rp1.com"} - rp2 = {"id": "rp2.com", "name": "rp2.com"} + rp1 = {"id": "example.com", "name": "rp1.com"} + rp2 = {"id": "example.com", "name": "rp2.com"} rp1_registrations = [] rp2_registrations = [] @@ -127,7 +127,7 @@ def test_mismatched_rp(device, GARes): rp_id += ".com" with pytest.raises(CtapError) as e: - device.doGA(rp_id=rp_id) + device.GA(rp_id=rp_id) assert e.value.code == CtapError.ERR.NO_CREDENTIALS def test_missing_rp(device): @@ -137,7 +137,7 @@ def test_missing_rp(device): def test_bad_rp(device): with pytest.raises(CtapError) as e: - device.doGA(rp_id={"id": {"type": "wrong"}}) + device.GA(rp_id={"id": {"type": "wrong"}}) def test_missing_cdh(device): with pytest.raises(CtapError) as e: @@ -150,11 +150,11 @@ def test_bad_cdh(device): def test_bad_allow_list(device): with pytest.raises(CtapError) as e: - device.doGA(allow_list={"type": "wrong"}) + device.GA(allow_list={"type": "wrong"}) def test_bad_allow_list_item(device, MCRes): with pytest.raises(CtapError) as e: - device.doGA(allow_list=["wrong"] + [ + device.GA(allow_list=["wrong"] + [ {"id": MCRes['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"} ] ) @@ -177,7 +177,7 @@ def test_option_up(device, info, GARes): assert res.auth_data.flags & (1 << 0) def test_allow_list_fake_item(device, MCRes): - device.doGA(allow_list=[{"type": "rot13", "id": b"1234"}] + device.GA(allow_list=[{"type": "rot13", "id": b"1234"}] + [ {"id": MCRes['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"} ], @@ -185,7 +185,7 @@ def test_allow_list_fake_item(device, MCRes): def test_allow_list_missing_field(device, MCRes): with pytest.raises(CtapError) as e: - device.doGA(allow_list=[{"id": b"1234"}] + [ + device.GA(allow_list=[{"id": b"1234"}] + [ {"id": MCRes['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"} ] ) @@ -200,7 +200,7 @@ def test_allow_list_field_wrong_type(device, MCRes): def test_allow_list_id_wrong_type(device, MCRes): with pytest.raises(CtapError) as e: - device.doGA(allow_list=[{"type": "public-key", "id": 42}] + device.GA(allow_list=[{"type": "public-key", "id": 42}] + [ {"id": MCRes['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"} ] @@ -208,7 +208,7 @@ def test_allow_list_id_wrong_type(device, MCRes): def test_allow_list_missing_id(device, MCRes): with pytest.raises(CtapError) as e: - device.doGA(allow_list=[{"type": "public-key"}] + [ + device.GA(allow_list=[{"type": "public-key"}] + [ {"id": MCRes['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"} ] ) diff --git a/tests/pico-fido/test_022_discoverable.py b/tests/pico-fido/test_022_discoverable.py index d1d3c3f..83f8c37 100644 --- a/tests/pico-fido/test_022_discoverable.py +++ b/tests/pico-fido/test_022_discoverable.py @@ -85,7 +85,7 @@ def test_multiple_rk_nodisplay(device, MCRes_DC): auths = [] regs = [] # Use unique RP to not collide with other credentials - rp = {"id": f"unique-{random.random()}.com", "name": "Example"} + rp = {"id": "example.com", "name": "Example"} for i in range(0, 3): res = device.doMC(rp=rp, rk=True, user=generate_random_user()) regs.append(res) @@ -116,7 +116,7 @@ def test_rk_maximum_size_nodisplay(device): auths = resGA.get_assertions() user_max_GA = auths[0] - print(auths) + for y in ("name", "displayName", "id"): if (y in user_max_GA): assert user_max_GA.user[y] == user_max[y] @@ -126,7 +126,7 @@ def test_rk_maximum_list_capacity_per_rp_nodisplay(info, device, MCRes_DC): """ Test maximum returned capacity of the RK for the given RP """ - + device.reset() # Try to determine from get_info, or default to 19. RK_CAPACITY_PER_RP = info.max_creds_in_list if not RK_CAPACITY_PER_RP: @@ -140,7 +140,7 @@ def test_rk_maximum_list_capacity_per_rp_nodisplay(info, device, MCRes_DC): return user # Use unique RP to not collide with other credentials from other tests. - rp = {"id": f"unique-{random.random()}.com", "name": "Example"} + rp = {"id": "example.com", "name": "Example"} # req = FidoRequest(MCRes_DC, options=None, user=get_user(), rp = rp) # res = device.sendGA(*req.toGA()) @@ -183,10 +183,10 @@ def test_rk_with_allowlist_of_different_rp(resetdevice): """ rk_rp = {"id": "rk-cred.org", "name": "Example"} - rk_res = resetdevice.doMC(rp = rk_rp, rk=True)['res'].attestation_object + rk_res = resetdevice.MC(rp = rk_rp, options={"rk":True})['res'] server_rp = {"id": "server-cred.com", "name": "Example"} - server_res = resetdevice.doMC(rp = server_rp, rk=True)['res'].attestation_object + server_res = resetdevice.MC(rp = server_rp, options={"rk":True})['res'] allow_list_with_different_rp_cred = [ { @@ -197,7 +197,7 @@ def test_rk_with_allowlist_of_different_rp(resetdevice): with pytest.raises(CtapError) as e: - res = resetdevice.doGA(rp_id = rk_rp['id'], allow_list = allow_list_with_different_rp_cred) + res = resetdevice.GA(rp_id = rk_rp['id'], allow_list = allow_list_with_different_rp_cred) assert e.value.code == CtapError.ERR.NO_CREDENTIALS @@ -208,10 +208,10 @@ def test_same_userId_overwrites_rk(resetdevice): rp = {"id": "overwrite.org", "name": "Example"} user = generate_random_user() - mc_res1 = resetdevice.doMC(rp = rp, rk=True, user = user) + mc_res1 = resetdevice.MC(rp = rp, options={"rk":True}, user = user) # Should overwrite the first credential. - mc_res2 = resetdevice.doMC(rp = rp, rk=True, user = user) + mc_res2 = resetdevice.MC(rp = rp, options={"rk":True}, user = user) ga_res = resetdevice.GA(rp_id=rp['id'])['res'] @@ -227,7 +227,7 @@ def test_larger_icon_than_128(device): user = generate_random_user() user['icon'] = 'https://www.w3.org/TR/webauthn/?icon=' + ("A" * 128) - device.doMC(rp = rp, rk=True, user = user) + device.MC(rp = rp, options={"rk":True}, user = user) def test_returned_credential(device): diff --git a/tests/pico-fido/test_031_blob.py b/tests/pico-fido/test_031_blob.py index f5d2a73..6aba82d 100644 --- a/tests/pico-fido/test_031_blob.py +++ b/tests/pico-fido/test_031_blob.py @@ -21,6 +21,7 @@ import pytest from fido2.ctap import CtapError from fido2.ctap2.pin import PinProtocolV2, ClientPin +from fido2.utils import websafe_decode from utils import verify import os @@ -46,22 +47,24 @@ def GACredBlob(device, MCCredBlob): @pytest.fixture(scope="function") def MCLBK(device): - res = device.doMC( + mc = device.doMC( rk=True, extensions={'largeBlob':{'support':'required'}} - )['res'] - return res + ) + res = mc['res'] + ext = mc['client_extension_results'] + return {'res': res, 'ext': ext} @pytest.fixture(scope="function") def GALBRead(device, MCLBK): res = device.doGA( allow_list=[ - {"id": MCLBK.attestation_object.auth_data.credential_data.credential_id, "type": "public-key"} + {"id": MCLBK['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"} ],extensions={'largeBlob':{'read': True}} ) assertions = res['res'].get_assertions() for a in assertions: - verify(MCLBK.attestation_object, a, res['req']['client_data'].hash) + verify(MCLBK['res'].attestation_object, a, res['req']['client_data'].hash) return res['res'] @pytest.fixture(scope="function") @@ -70,18 +73,19 @@ def GALBReadLBK(GALBRead): @pytest.fixture(scope="function") def GALBReadLB(GALBRead): + print(GALBRead.get_response(0)) return GALBRead.get_response(0) @pytest.fixture(scope="function") def GALBWrite(device, MCLBK): res = device.doGA( allow_list=[ - {"id": MCLBK.attestation_object.auth_data.credential_data.credential_id, "type": "public-key"} + {"id": MCLBK['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"} ],extensions={'largeBlob':{'write': LARGE_BLOB}} ) assertions = res['res'].get_assertions() for a in assertions: - verify(MCLBK.attestation_object, a, res['req']['client_data'].hash) + verify(MCLBK['res'].attestation_object, a, res['req']['client_data'].hash) return res['res'].get_response(0) def test_supports_credblob(info): @@ -136,15 +140,17 @@ def test_supports_largeblobs(info): assert info.max_large_blob is None or (info.max_large_blob > 1024) def test_get_largeblobkey_mc(MCLBK): - assert 'supported' in MCLBK.extension_results - assert MCLBK.extension_results['supported'] is True + assert 'largeBlob' in MCLBK['ext'] + assert 'supported' in MCLBK['ext']['largeBlob'] + assert MCLBK['ext']['largeBlob']['supported'] is True def test_get_largeblobkey_ga(GALBReadLBK): assert GALBReadLBK.large_blob_key is not None def test_get_largeblob_rw(GALBWrite, GALBReadLB): - assert 'written' in GALBWrite.extension_results - assert GALBWrite.extension_results['written'] is True + assert 'largeBlob' in GALBWrite.client_extension_results + assert 'written' in GALBWrite.client_extension_results['largeBlob'] + assert GALBWrite.client_extension_results['largeBlob']['written'] is True - assert 'blob' in GALBReadLB.extension_results - assert GALBReadLB.extension_results['blob'] == LARGE_BLOB + assert 'blob' in GALBReadLB.client_extension_results['largeBlob'] + assert websafe_decode(GALBReadLB.client_extension_results['largeBlob']['blob']) == LARGE_BLOB diff --git a/tests/pico-fido/test_033_credprotect.py b/tests/pico-fido/test_033_credprotect.py index 83e02c8..65b7d2d 100644 --- a/tests/pico-fido/test_033_credprotect.py +++ b/tests/pico-fido/test_033_credprotect.py @@ -83,7 +83,7 @@ def test_credprotect_optional_list_excluded(device, MCCredProtectOptionalList): ] with pytest.raises(CtapError) as e: - device.doMC(rk=True, extensions={'credentialProtectionPolicy': CredProtectExtension.POLICY.OPTIONAL_WITH_LIST}, exclude_list=exclude_list) + device.MC(options={'rk': True}, extensions={'credProtect': CredProtect.UserVerificationOptionalWithCredentialId}, exclude_list=exclude_list) assert e.value.code == CtapError.ERR.CREDENTIAL_EXCLUDED @@ -123,10 +123,10 @@ def test_credprotect_optional_and_list_works_no_uv(device, MCCredProtectOptional }, ] # works - res1 = device.doGA(allow_list=allow_list)['res'].get_assertions()[0] + res1 = device.doGA(allow_list=allow_list, user_verification=False)['res'].get_assertions()[0] assert res1.number_of_credentials in (None, 2) - results = device.doGA(allow_list=allow_list)['res'].get_assertions() + results = device.doGA(allow_list=allow_list, user_verification=False)['res'].get_assertions() # the required credProtect is not returned. for res in results: diff --git a/tests/pico-fido/test_035_hmac_secret.py b/tests/pico-fido/test_035_hmac_secret.py index ed2861c..ccd276c 100644 --- a/tests/pico-fido/test_035_hmac_secret.py +++ b/tests/pico-fido/test_035_hmac_secret.py @@ -24,6 +24,7 @@ from fido2.ctap2.extensions import HmacSecretExtension from fido2.utils import hmac_sha256 from fido2.ctap2.pin import PinProtocolV2 from fido2.webauthn import UserVerificationRequirement +from fido2.client import ClientError from utils import * salt1 = b"\xa5" * 32 @@ -38,10 +39,6 @@ def MCHmacSecret(resetdevice): res = resetdevice.doMC(extensions={"hmacCreateSecret": True},rk=True) return res['res'].attestation_object -@pytest.fixture(scope="class") -def hmac(resetdevice): - return HmacSecretExtension(resetdevice.client()._backend.ctap2, pin_protocol=PinProtocolV2()) - def test_hmac_secret_make_credential(MCHmacSecret): assert MCHmacSecret.auth_data.extensions assert "hmac-secret" in MCHmacSecret.auth_data.extensions @@ -55,51 +52,51 @@ def test_fake_extension(device): @pytest.mark.parametrize("salts", [(salt1,), (salt1, salt2)]) -def test_hmac_secret_entropy(device, MCHmacSecret, hmac, salts +def test_hmac_secret_entropy(device, MCHmacSecret, salts ): hout = {'salt1':salts[0]} if (len(salts) > 1): hout['salt2'] = salts[1] auth = device.doGA(extensions={"hmacGetSecret": hout})['res'].get_response(0) - ext = auth.extension_results + ext = auth.client_extension_results assert ext assert "hmacGetSecret" in ext - assert len(auth.authenticator_data.extensions['hmac-secret']) == len(salts) * 32 + 16 + assert len(auth.response.authenticator_data.extensions['hmac-secret']) == len(salts) * 32 + 16 - #print(shannon_entropy(auth.authenticator_data.extensions['hmac-secret'])) + #print(shannon_entropy(auth.response.authenticator_data.extensions['hmac-secret'])) if len(salts) == 1: - assert shannon_entropy(auth.authenticator_data.extensions['hmac-secret']) > 4.5 - assert shannon_entropy(ext["hmacGetSecret"]['output1']) > 4.5 + assert shannon_entropy(auth.response.authenticator_data.extensions['hmac-secret']) > 4.5 + assert shannon_entropy(ext.hmac_get_secret.output1) > 4.5 if len(salts) == 2: - assert shannon_entropy(auth.authenticator_data.extensions['hmac-secret']) > 5.4 - assert shannon_entropy(ext["hmacGetSecret"]['output1']) > 4.5 - assert shannon_entropy(ext["hmacGetSecret"]['output2']) > 4.5 + assert shannon_entropy(auth.response.authenticator_data.extensions['hmac-secret']) > 5.4 + assert shannon_entropy(ext.hmac_get_secret.output1) > 4.5 + assert shannon_entropy(ext.hmac_get_secret.output2) > 4.5 -def get_output(device, MCHmacSecret, hmac, salts): +def get_output(device, MCHmacSecret, salts): hout = {'salt1':salts[0]} if (len(salts) > 1): hout['salt2'] = salts[1] auth = device.doGA(extensions={"hmacGetSecret": hout})['res'].get_response(0) - ext = auth.extension_results + ext = auth.client_extension_results assert ext assert "hmacGetSecret" in ext - assert len(auth.authenticator_data.extensions['hmac-secret']) == len(salts) * 32 + 16 + assert len(auth.response.authenticator_data.extensions['hmac-secret']) == len(salts) * 32 + 16 if len(salts) == 2: - return ext["hmacGetSecret"]['output1'], ext["hmacGetSecret"]['output2'] + return ext.hmac_get_secret.output1, ext.hmac_get_secret.output2 else: - return ext["hmacGetSecret"]['output1'] + return ext.hmac_get_secret.output1 -def test_hmac_secret_sanity(device, MCHmacSecret, hmac): - output1 = get_output(device, MCHmacSecret, hmac, (salt1,)) +def test_hmac_secret_sanity(device, MCHmacSecret): + output1 = get_output(device, MCHmacSecret, (salt1,)) output12 = get_output( - device, MCHmacSecret, hmac, (salt1, salt2) + device, MCHmacSecret, (salt1, salt2) ) output21 = get_output( - device, MCHmacSecret, hmac, (salt2, salt1) + device, MCHmacSecret, (salt2, salt1) ) assert output12[0] == output1 @@ -107,60 +104,60 @@ def test_hmac_secret_sanity(device, MCHmacSecret, hmac): assert output21[0] == output12[1] assert output12[0] != output12[1] -def test_missing_keyAgreement(device, hmac): - hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salt3}}) +def test_missing_keyAgreement(device): with pytest.raises(CtapError): - device.GA(extensions={"hmac-secret": {2: hout[2], 3: hout[3]}}) + device.GA(extensions={"hmac-secret": {2: b'1234', 3: b'1234'}}) -def test_missing_saltAuth(device, hmac): - hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salt3}}) +def test_missing_saltAuth(device): with pytest.raises(CtapError) as e: - device.GA(extensions={"hmac-secret": {1: hout[1], 2: hout[2]}}) + device.GA(extensions={"hmac-secret": {2: b'1234'}}) assert e.value.code == CtapError.ERR.MISSING_PARAMETER -def test_missing_saltEnc(device, hmac): - hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salt3}}) +def test_missing_saltEnc(device,): with pytest.raises(CtapError) as e: - device.GA(extensions={"hmac-secret": {1: hout[1], 3: hout[3]}}) + device.GA(extensions={"hmac-secret": { 3: b'1234'}}) assert e.value.code == CtapError.ERR.MISSING_PARAMETER -def test_bad_auth(device, hmac, MCHmacSecret): +def test_bad_auth(device, MCHmacSecret): - hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salt3}}) - bad_auth = list(hout[3][:]) - bad_auth[len(bad_auth) // 2] = bad_auth[len(bad_auth) // 2] ^ 1 - bad_auth = bytes(bad_auth) + key_agreement = { + 1: 2, + 3: -25, # Per the spec, "although this is NOT the algorithm actually used" + -1: 1, + -2: b'\x00'*32, + -3: b'\x00'*32, + } with pytest.raises(CtapError) as e: - device.GA(extensions={"hmac-secret": {1: hout[1], 2: hout[2], 3: bad_auth, 4: 2}}) + device.GA(extensions={"hmac-secret": {1: key_agreement, 2: b'\x00'*80, 3: b'\x00'*32, 4: 2}}) assert e.value.code == CtapError.ERR.EXTENSION_FIRST @pytest.mark.parametrize("salts", [(salt4,), (salt4, salt5)]) -def test_invalid_salt_length(device, hmac, salts): - with pytest.raises(ValueError) as e: +def test_invalid_salt_length(device, salts): + with pytest.raises((CtapError,ClientError)) as e: if (len(salts) == 2): - hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salts[0],"salt2":salts[1]}}) + hout = {"salt1":salts[0],"salt2":salts[1]} else: - hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salts[0]}}) + hout = {"salt1":salts[0]} device.doGA(extensions={"hmacGetSecret": hout}) @pytest.mark.parametrize("salts", [(salt1,), (salt1, salt2)]) def test_get_next_assertion_has_extension( - device, hmac, salts + device, salts ): """ Check that get_next_assertion properly returns extension information for multiple accounts. """ if (len(salts) == 2): - hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salts[0],"salt2":salts[1]}}) + hout = {"salt1":salts[0],"salt2":salts[1]} else: - hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salts[0]}}) + hout = {"salt1":salts[0]} accounts = 3 regs = [] auths = [] - rp = {"id": f"example_salts_{len(salts)}.org", "name": "ExampleRP_2"} + rp = {"id": f"example.com", "name": "ExampleRP_2"} fixed_users = [generate_random_user() for _ in range(accounts)] for i in range(accounts): res = device.doMC(extensions={"hmacCreateSecret": True}, @@ -183,21 +180,19 @@ def test_get_next_assertion_has_extension( assert "hmac-secret" in ext assert isinstance(ext["hmac-secret"], bytes) assert len(ext["hmac-secret"]) == len(salts) * 32 + 16 - key = hmac.process_get_output(x) - -def test_hmac_secret_different_with_uv(device, MCHmacSecret, hmac): +def test_hmac_secret_different_with_uv(device, MCHmacSecret): salts = [salt1] if (len(salts) == 2): - hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salts[0],"salt2":salts[1]}}) + hout = {"salt1":salts[0],"salt2":salts[1]} else: - hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salts[0]}}) + hout = {"salt1":salts[0]} - auth_no_uv = device.GA(extensions={"hmac-secret": hout})['res'] - assert (auth_no_uv.auth_data.flags & (1 << 2)) == 0 + auth_no_uv = device.doGA(extensions={"hmacGetSecret": hout})['res'].get_response(0) + assert (auth_no_uv.response.authenticator_data.flags & (1 << 2)) == 0 - ext_no_uv = auth_no_uv.auth_data.extensions + ext_no_uv = auth_no_uv.response.authenticator_data.extensions assert ext_no_uv assert "hmac-secret" in ext_no_uv assert isinstance(ext_no_uv["hmac-secret"], bytes) @@ -209,11 +204,11 @@ def test_hmac_secret_different_with_uv(device, MCHmacSecret, hmac): hout['salt2'] = salts[1] auth_uv = device.doGA(extensions={"hmacGetSecret": hout}, user_verification=UserVerificationRequirement.REQUIRED)['res'].get_response(0) - assert auth_uv.authenticator_data.flags & (1 << 2) - ext_uv = auth_uv.extension_results + assert auth_uv.response.authenticator_data.flags & (1 << 2) + ext_uv = auth_uv.client_extension_results assert ext_uv assert "hmacGetSecret" in ext_uv - assert len(ext_uv["hmacGetSecret"]) == len(salts) + assert len([p for p in ext_uv["hmacGetSecret"] if len(ext_uv["hmacGetSecret"][p]) > 0]) == len(salts) # Now see if the hmac-secrets are different assert ext_no_uv["hmac-secret"][:32] != ext_uv["hmacGetSecret"]['output1'] diff --git a/tests/pico-fido/test_051_ctap1_interop.py b/tests/pico-fido/test_051_ctap1_interop.py index da7e244..d8d3eab 100644 --- a/tests/pico-fido/test_051_ctap1_interop.py +++ b/tests/pico-fido/test_051_ctap1_interop.py @@ -29,7 +29,7 @@ def test_authenticate_ctap1_through_ctap2(device, RegRes): res = device.doGA(ctap1=False, allow_list=[ {"id": RegRes['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"} ]) - assert res['res'].get_response(0).credential_id == RegRes['res'].attestation_object.auth_data.credential_data.credential_id + assert res['res'].get_response(0).raw_id == RegRes['res'].attestation_object.auth_data.credential_data.credential_id # Test FIDO2 register works with U2F auth