diff --git a/tests/conftest.py b/tests/conftest.py index 188c614..ff9dcf4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,24 +4,11 @@ from fido2.client import Fido2Client, WindowsClient, UserInteraction, ClientErro from fido2.ctap2.pin import ClientPin from fido2.server import Fido2Server from fido2.ctap import CtapError -from fido2.webauthn import ( - Aaguid, - AttestationObject, - CollectedClientData, - PublicKeyCredentialCreationOptions, - PublicKeyCredentialRequestOptions, - AuthenticatorSelectionCriteria, - UserVerificationRequirement, - AuthenticatorAttestationResponse, - AuthenticatorAssertionResponse, - AttestationConveyancePreference, -) +from fido2.webauthn import CollectedClientData from getpass import getpass import sys -import ctypes import pytest import os -import inspect DEFAULT_PIN='12345678' @@ -95,6 +82,13 @@ class Device(): self.__user = user return self.__user + def rp(self, rp=None): + if (self.__rp is None): + self.__rp = {"id": "example.com", "name": "Example RP"} + if (rp is not None): + self.__rp = rp + return self.__rp + def reset(self): print("Resetting Authenticator...") try: @@ -125,15 +119,15 @@ class Device(): ) return att_obj - def doMC(self, client_data=None, rp=None, user=None, key_params=None, exclude_list=None, extensions=None, rk=None, user_verification=None, enterprise_attestation=None, event=None): + 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): result = self.__client._backend.do_make_credential( - client_data=client_data or CollectedClientData.create( + client_data=client_data if client_data is not Ellipsis else CollectedClientData.create( type=CollectedClientData.TYPE.CREATE, origin=self.__origin, challenge=os.urandom(32) ), - rp=rp or self.__rp, - user=user or self.user(), - key_params=key_params or self.__server.allowed_algorithms, + 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, exclude_list=exclude_list, extensions=extensions, rk=rk, @@ -160,7 +154,7 @@ class Device(): def register(self, uv=None): # Prepare parameters for makeCredential create_options, state = self.__server.register_begin( - self.user(), user_verification=self.__uv, authenticator_attachment="cross-platform" + self.user(), user_verification=uv or self.__uv, authenticator_attachment="cross-platform" ) # Create a credential result = self.try_make_credential(create_options) @@ -206,6 +200,35 @@ class Device(): print() print("AUTH DATA:", result.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): + att_obj = self.__client._backend.ctap2.get_assertion( + rp_id=rp_id if rp_id is not Ellipsis else self.__rp['id'], + client_data_hash=client_data_hash if client_data_hash is not Ellipsis else os.urandom(32), + allow_list=allow_list, + extensions=extensions, + options=options, + pin_uv_param=pin_uv_param, + pin_uv_protocol=pin_uv_protocol + ) + return att_obj + + def GNA(self): + 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): + result = self.__client._backend.do_get_assertion( + client_data=client_data if client_data is not Ellipsis else CollectedClientData.create( + type=CollectedClientData.TYPE.CREATE, origin=self.__origin, challenge=os.urandom(32) + ), + rp_id=rp_id if rp_id is not Ellipsis else self.__rp['id'], + allow_list=allow_list, + extensions=extensions, + user_verification=user_verification, + event=event + ) + return result + + @pytest.fixture(scope="session") def device(): dev = Device() @@ -223,3 +246,10 @@ def MCRes(device, *args): def resetdevice(device): device.reset() return device + +@pytest.fixture(scope="session") +def GARes(device, MCRes, *args): + r = device.doGA(allow_list=[ + {"id": MCRes.auth_data.credential_data.credential_id, "type": "public-key"} + ], *args) + return r diff --git a/tests/pico-fido/test_authenticate.py b/tests/pico-fido/test_authenticate.py index 6fb932c..1e5d23c 100644 --- a/tests/pico-fido/test_authenticate.py +++ b/tests/pico-fido/test_authenticate.py @@ -1,3 +1,6 @@ +from fido2.utils import sha256 +from fido2.client import CtapError +import pytest def test_authenticate(device): device.reset() @@ -5,3 +8,182 @@ def test_authenticate(device): credentials = [AUTData.credential_data] AUTRes = device.authenticate(credentials) + +def test_assertion_auth_data(GARes): + assert len(GARes.get_response(0).authenticator_data) == 37 + +def test_Check_that_AT_flag_is_not_set(GARes): + assert (GARes.get_response(0).authenticator_data.flags & 0xF8) == 0 + +def test_that_user_credential_and_numberOfCredentials_are_not_present(device, MCRes): + res = device.GA(allow_list=[ + {"id": MCRes.auth_data.credential_data.credential_id, "type": "public-key"} + ]) + assert res.user == None + assert res.number_of_credentials == None + +def test_empty_allowList(device): + with pytest.raises(CtapError) as e: + device.doGA(allow_list=[]) + assert e.value.code == CtapError.ERR.NO_CREDENTIALS + +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_registrations = [] + rp2_registrations = [] + rp1_assertions = [] + rp2_assertions = [] + + l1 = 4 + for i in range(0, l1): + res = device.doMC(rp=rp1).attestation_object + rp1_registrations.append(res) + allow_list.append({ + "id": res.auth_data.credential_data.credential_id[:], + "type": "public-key", + }) + + l2 = 6 + for i in range(0, l2): + res = device.doMC(rp=rp2).attestation_object + rp2_registrations.append(res) + allow_list.append({ + "id": res.auth_data.credential_data.credential_id[:], + "type": "public-key", + }) + + # CTAP 2.1: If allowlist is passed, only one (any) applicable + # credential signs, and numberOfCredentials = None is returned. + # + # + # CTAP 2.0: Expects the authenticator to return the total number + # even when allowlist is passed (and hence keep the credential IDs + # cached. + + # Should authenticate to all credentials matching rp1 + rp1_assertions = device.doGA(rp_id=rp1['id'], allow_list=allow_list).get_assertions() + + # Should authenticate to all credentials matching rp2 + rp2_assertions = device.doGA(rp_id=rp2['id'], allow_list=allow_list).get_assertions() + + counts = ( + len(rp1_assertions), + len(rp2_assertions) + ) + + assert counts in [(None, None), (l1, l2)] + +def test_corrupt_credId(device, MCRes): + # apply bit flip + badid = list(MCRes.auth_data.credential_data.credential_id[:]) + badid[len(badid) // 2] = badid[len(badid) // 2] ^ 1 + badid = bytes(badid) + + allow_list = [{"id": badid, "type": "public-key"}] + + with pytest.raises(CtapError) as e: + device.doGA(allow_list=allow_list) + assert e.value.code == CtapError.ERR.NO_CREDENTIALS + +def test_mismatched_rp(device, GARes): + rp_id = device.rp()['id'] + rp_id += ".com" + + with pytest.raises(CtapError) as e: + device.doGA(rp_id=rp_id) + assert e.value.code == CtapError.ERR.NO_CREDENTIALS + +def test_missing_rp(device): + with pytest.raises(CtapError) as e: + device.doGA(rp_id=None) + assert e.value.code == CtapError.ERR.MISSING_PARAMETER + +def test_bad_rp(device): + + with pytest.raises(CtapError) as e: + device.doGA(rp_id={"id": {"type": "wrong"}}) + +def test_missing_cdh(device): + with pytest.raises(CtapError) as e: + device.GA(client_data_hash=None) + assert e.value.code == CtapError.ERR.MISSING_PARAMETER + +def test_bad_cdh(device): + with pytest.raises(CtapError) as e: + device.GA(client_data_hash={"type": "wrong"}) + +def test_bad_allow_list(device): + with pytest.raises(CtapError) as e: + device.doGA(allow_list={"type": "wrong"}) + +def test_bad_allow_list_item(device, MCRes): + with pytest.raises(CtapError) as e: + device.doGA(allow_list=["wrong"] + [ + {"id": MCRes.auth_data.credential_data.credential_id, "type": "public-key"} + ] + ) + +def test_unknown_option(device, MCRes): + device.GA(options={"unknown": True}, allow_list=[ + {"id": MCRes.auth_data.credential_data.credential_id, "type": "public-key"} + ]) + +def test_option_uv(device, info, GARes): + if "uv" in info.options: + if info.options["uv"]: + res = device.doGA(options={"uv": True}) + assert res.auth_data.flags & (1 << 2) + +def test_option_up(device, info, GARes): + if "up" in info.options: + if info.options["up"]: + res = device.doGA(options={"up": True}) + assert res.auth_data.flags & (1 << 0) + +def test_allow_list_fake_item(device, MCRes): + device.doGA(allow_list=[{"type": "rot13", "id": b"1234"}] + + [ + {"id": MCRes.auth_data.credential_data.credential_id, "type": "public-key"} + ], + ) + +def test_allow_list_missing_field(device, MCRes): + with pytest.raises(CtapError) as e: + device.doGA(allow_list=[{"id": b"1234"}] + [ + {"id": MCRes.auth_data.credential_data.credential_id, "type": "public-key"} + ] + ) + +def test_allow_list_field_wrong_type(device, MCRes): + with pytest.raises(CtapError) as e: + device.doGA(allow_list=[{"type": b"public-key", "id": b"1234"}] + + [ + {"id": MCRes.auth_data.credential_data.credential_id, "type": "public-key"} + ] + ) + +def test_allow_list_id_wrong_type(device, MCRes): + with pytest.raises(CtapError) as e: + device.doGA(allow_list=[{"type": "public-key", "id": 42}] + + [ + {"id": MCRes.auth_data.credential_data.credential_id, "type": "public-key"} + ] + ) + +def test_allow_list_missing_id(device, MCRes): + with pytest.raises(CtapError) as e: + device.doGA(allow_list=[{"type": "public-key"}] + [ + {"id": MCRes.auth_data.credential_data.credential_id, "type": "public-key"} + ] + ) + +def test_user_presence_option_false(device, MCRes): + res = device.GA(options={"up": False}, allow_list=[ + {"id": MCRes.auth_data.credential_data.credential_id, "type": "public-key"} + ]) + diff --git a/tests/pico-fido/test_register.py b/tests/pico-fido/test_register.py index b0d4cca..ab84319 100644 --- a/tests/pico-fido/test_register.py +++ b/tests/pico-fido/test_register.py @@ -10,10 +10,10 @@ def test_register(device): def test_make_credential(): pass -def test_attestation_format( MCRes): +def test_attestation_format(MCRes): assert MCRes.fmt in ["packed", "tpm", "android-key", "adroid-safetynet"] -def test_authdata_length( MCRes): +def test_authdata_length(MCRes): assert len(MCRes.auth_data) >= 77 def test_missing_cdh(device): @@ -26,15 +26,15 @@ def test_bad_type_cdh(device): with pytest.raises(CtapError) as e: device.MC(client_data_hash=b'\xff') -def test_missing_user(device, MCRes): +def test_missing_user(device): with pytest.raises(CtapError) as e: - device.MC(user=None) + device.doMC(user=None) assert e.value.code == CtapError.ERR.MISSING_PARAMETER def test_bad_type_user_user(device): with pytest.raises(CtapError) as e: - device.MC(user=b"12345678") + device.doMC(user=b"12345678") def test_missing_rp(device): with pytest.raises(CtapError) as e: @@ -48,7 +48,7 @@ def test_bad_type_rp(device): def test_missing_pubKeyCredParams(device): with pytest.raises(CtapError) as e: - device.MC(key_params=None) + device.doMC(key_params=None) assert e.value.code == CtapError.ERR.MISSING_PARAMETER @@ -70,45 +70,45 @@ def test_bad_type_options(device): def test_bad_type_rp_name(device): with pytest.raises(CtapError) as e: - device.MC(rp={"id": "test.org", "name": 8, "icon": "icon"}) + device.doMC(rp={"id": "test.org", "name": 8, "icon": "icon"}) def test_bad_type_rp_id(device): with pytest.raises(CtapError) as e: - device.MC(rp={"id": 8, "name": "name", "icon": "icon"}) + device.doMC(rp={"id": 8, "name": "name", "icon": "icon"}) def test_bad_type_rp_icon(device): with pytest.raises(CtapError) as e: - device.MC(rp={"id": "test.org", "name": "name", "icon": 8}) + device.doMC(rp={"id": "test.org", "name": "name", "icon": 8}) def test_bad_type_user_name(device): with pytest.raises(CtapError) as e: - device.MC(user={"id": b"user_id", "name": 8}) + device.doMC(user={"id": b"user_id", "name": 8}) def test_bad_type_user_id(device): with pytest.raises(CtapError) as e: - device.MC(user={"id": "user_id", "name": "name"}) + device.doMC(user={"id": "user_id", "name": "name"}) def test_bad_type_user_displayName(device): with pytest.raises(CtapError) as e: - device.MC(user={"id": "user_id", "name": "name", "displayName": 8}) + device.doMC(user={"id": "user_id", "name": "name", "displayName": 8}) def test_bad_type_user_icon(device): with pytest.raises(CtapError) as e: - device.MC(user={"id": "user_id", "name": "name", "icon": 8}) + device.doMC(user={"id": "user_id", "name": "name", "icon": 8}) def test_bad_type_pubKeyCredParams(device): with pytest.raises(CtapError) as e: - device.MC(key_params=["wrong"]) + device.doMC(key_params=["wrong"]) def test_missing_pubKeyCredParams_type(device): with pytest.raises(CtapError) as e: - device.MC(key_params=[{"alg": ES256.ALGORITHM}]) + device.doMC(key_params=[{"alg": ES256.ALGORITHM}]) assert e.value.code == CtapError.ERR.MISSING_PARAMETER def test_missing_pubKeyCredParams_alg(device): with pytest.raises(CtapError) as e: - device.MC(key_params=[{"type": "public-key"}]) + device.doMC(key_params=[{"type": "public-key"}]) assert e.value.code in [ CtapError.ERR.MISSING_PARAMETER, @@ -117,43 +117,46 @@ def test_missing_pubKeyCredParams_alg(device): def test_bad_type_pubKeyCredParams_alg(device): with pytest.raises(CtapError) as e: - device.MC(key_params=[{"alg": "7", "type": "public-key"}]) + device.doMC(key_params=[{"alg": "7", "type": "public-key"}]) def test_unsupported_algorithm(device): with pytest.raises(CtapError) as e: - device.MC(key_params=[{"alg": 1337, "type": "public-key"}]) + device.doMC(key_params=[{"alg": 1337, "type": "public-key"}]) assert e.value.code == CtapError.ERR.UNSUPPORTED_ALGORITHM def test_exclude_list(resetdevice): - resetdevice.MC(exclude_list=[{"id": b"1234", "type": "rot13"}]) + resetdevice.doMC(exclude_list=[{"id": b"1234", "type": "rot13"}]) def test_exclude_list2(resetdevice): - resetdevice.MC(exclude_list=[{"id": b"1234", "type": "mangoPapayaCoconutNotAPublicKey"}]) + resetdevice.doMC(exclude_list=[{"id": b"1234", "type": "mangoPapayaCoconutNotAPublicKey"}]) def test_bad_type_exclude_list(device): with pytest.raises(CtapError) as e: - device.MC(exclude_list=["1234"]) + device.doMC(exclude_list=["1234"]) def test_missing_exclude_list_type(device): with pytest.raises(CtapError) as e: - device.MC(exclude_list=[{"id": b"1234"}]) + device.doMC(exclude_list=[{"id": b"1234"}]) def test_missing_exclude_list_id(device): with pytest.raises(CtapError) as e: - device.MC(exclude_list=[{"type": "public-key"}]) + device.doMC(exclude_list=[{"type": "public-key"}]) def test_bad_type_exclude_list_id(device): with pytest.raises(CtapError) as e: - device.MC(exclude_list=[{"type": "public-key", "id": "1234"}]) + device.doMC(exclude_list=[{"type": "public-key", "id": "1234"}]) def test_bad_type_exclude_list_type(device): with pytest.raises(CtapError) as e: - device.MC(exclude_list=[{"type": b"public-key", "id": b"1234"}]) + device.doMC(exclude_list=[{"type": b"public-key", "id": b"1234"}]) -def test_exclude_list_excluded(device, MCRes, GARes): +def test_exclude_list_excluded(device): + res = device.doMC().attestation_object with pytest.raises(CtapError) as e: - device.MC(exclude_list=GARes.request.allow_list) + device.doMC(exclude_list=[ + {"id": res.auth_data.credential_data.credential_id, "type": "public-key"} + ]) assert e.value.code == CtapError.ERR.CREDENTIAL_EXCLUDED