diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..188c614 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,225 @@ +from http import client +from fido2.hid import CtapHidDevice +from fido2.client import Fido2Client, WindowsClient, UserInteraction, ClientError +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 getpass import getpass +import sys +import ctypes +import pytest +import os +import inspect + +DEFAULT_PIN='12345678' + +class CliInteraction(UserInteraction): + def prompt_up(self): + print("\nTouch your authenticator device now...\n") + + def request_pin(self, permissions, rd_id): + return DEFAULT_PIN + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + +class DeviceSelectCredential: + def __init__(self, number): + pass + + def __call__(self, status): + pass + +class Device(): + 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 __set_client(self, origin, user_interaction, uv): + self.__uv = uv + self.__dev = None + self.__origin = origin + self.__user_interaction = user_interaction + + # Locate a device + self.__dev = next(CtapHidDevice.list_devices(), None) + if self.__dev is not None: + print("Use USB HID channel.") + else: + try: + from fido2.pcsc import CtapPcscDevice + + self.__dev = next(CtapPcscDevice.list_devices(), None) + print("Use NFC channel.") + except Exception as e: + print("NFC channel search error:", e) + + if not self.__dev: + print("No FIDO device found") + 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) + + # 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") + + def __set_server(self, rp, attestation): + self.__rp = rp + self.__attestation = attestation + self.__server = Fido2Server(self.__rp, attestation=self.__attestation) + + def client(self): + return self.__client + + def user(self, user=None): + if (self.__user is None): + self.__user = {"id": b"user_id", "name": "A. User"} + if (user is not None): + self.__user = user + return self.__user + + def reset(self): + print("Resetting Authenticator...") + try: + self.__client._backend.ctap2.reset(on_keepalive=DeviceSelectCredential(1)) + except CtapError: + # Some authenticators need a power cycle + print("Need to power cycle authentictor to reset..") + self.reboot() + self.__client._backend.ctap2.reset(on_keepalive=DeviceSelectCredential(1)) + + def reboot(self): + print("Please reboot authenticator and hit enter") + input() + self.__setup_client(self.__origin, self.__user_interaction, self.__uv) + + def MC(self, client_data_hash=Ellipsis, rp=Ellipsis, user=Ellipsis, key_params=Ellipsis, exclude_list=None, extensions=None, options=None, pin_uv_param=None, pin_uv_protocol=None, enterprise_attestation=None): + att_obj = self.__client._backend.ctap2.make_credential( + client_data_hash=client_data_hash if client_data_hash is not Ellipsis else os.urandom(32), + 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, + options=options, + pin_uv_param=pin_uv_param, + pin_uv_protocol=pin_uv_protocol, + enterprise_attestation=enterprise_attestation + ) + 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): + + result = self.__client._backend.do_make_credential( + client_data=client_data or 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, + exclude_list=exclude_list, + extensions=extensions, + rk=rk, + user_verification=user_verification, + enterprise_attestation=enterprise_attestation, + event=event + ) + return result + + def try_make_credential(self, options=None): + if (options is None): + options, _ = self.__server.register_begin( + self.user(), user_verification=self.__uv, authenticator_attachment="cross-platform" + ) + try: + result = self.__client.make_credential(options["publicKey"]) + except ClientError as e: + if (e.code == ClientError.ERR.CONFIGURATION_UNSUPPORTED): + client_pin = ClientPin(self.__client._backend.ctap2) + client_pin.set_pin(DEFAULT_PIN) + result = self.__client.make_credential(options["publicKey"]) + return result + + 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" + ) + # Create a credential + result = self.try_make_credential(create_options) + + # Complete registration + auth_data = self.__server.register_complete( + state, result.client_data, result.attestation_object + ) + credentials = [auth_data.credential_data] + + print("New credential created!") + + print("CLIENT DATA:", result.client_data) + print("ATTESTATION OBJECT:", result.attestation_object) + print() + print("CREDENTIAL DATA:", auth_data.credential_data) + + return (result, auth_data) + + def authenticate(self, credentials): + # Prepare parameters for getAssertion + request_options, state = self.__server.authenticate_begin(credentials, user_verification=self.__uv) + + # Authenticate the credential + result = self.__client.get_assertion(request_options["publicKey"]) + + # Only one cred in allowCredentials, only one response. + result = result.get_response(0) + + # Complete authenticator + self.__server.authenticate_complete( + state, + credentials, + result.credential_id, + result.client_data, + result.authenticator_data, + result.signature, + ) + + print("Credential authenticated!") + + print("CLIENT DATA:", result.client_data) + print() + print("AUTH DATA:", result.authenticator_data) + +@pytest.fixture(scope="session") +def device(): + dev = Device() + return dev + +@pytest.fixture(scope="session") +def info(device): + return device.client()._backend.info + +@pytest.fixture(scope="session") +def MCRes(device, *args): + return device.doMC(*args).attestation_object + +@pytest.fixture(scope="session") +def resetdevice(device): + device.reset() + return device diff --git a/tests/pico-fido/test_authenticate.py b/tests/pico-fido/test_authenticate.py new file mode 100644 index 0000000..6fb932c --- /dev/null +++ b/tests/pico-fido/test_authenticate.py @@ -0,0 +1,7 @@ + +def test_authenticate(device): + device.reset() + REGRes,AUTData = device.register() + + credentials = [AUTData.credential_data] + AUTRes = device.authenticate(credentials) diff --git a/tests/pico-fido/test_getinfo.py b/tests/pico-fido/test_getinfo.py new file mode 100644 index 0000000..8a4a8be --- /dev/null +++ b/tests/pico-fido/test_getinfo.py @@ -0,0 +1,27 @@ +import pytest +from fido2.client import CtapError + + +def test_getinfo(device): + pass + + +def test_get_info_version(info): + assert "FIDO_2_0" in info.versions + + +def test_Check_pin_protocols_field(info): + if len(info.pin_uv_protocols): + assert sum(info.pin_uv_protocols) > 0 + + +def test_Check_options_field(info): + for x in info.options: + assert info.options[x] in [True, False] + + +def test_Check_up_option(device, info): + if "up" not in info.options or info.options["up"]: + with pytest.raises(CtapError) as e: + device.MC(options={"up": True}) + assert e.value.code == CtapError.ERR.INVALID_OPTION diff --git a/tests/pico-fido/test_register.py b/tests/pico-fido/test_register.py new file mode 100644 index 0000000..0031caa --- /dev/null +++ b/tests/pico-fido/test_register.py @@ -0,0 +1,260 @@ +from fido2.client import CtapError +import pytest + + +def test_register(device): + device.reset() + REGRes,AUTData = device.register() + +def test_make_credential(device, MCRes): + pass + +def test_attestation_format(device, MCRes): + assert MCRes.fmt in ["packed", "tpm", "android-key", "adroid-safetynet"] + +def test_authdata_length(device, MCRes): + assert len(MCRes.auth_data) >= 77 + +def test_missing_cdh(device, MCRes): + with pytest.raises(CtapError) as e: + device.MC(client_data_hash=None) + + assert e.value.code == CtapError.ERR.MISSING_PARAMETER + +def test_bad_type_cdh(device, MCRes): + with pytest.raises(CtapError) as e: + device.MC(client_data_hash=b'\xff') + +def test_missing_user(device, MCRes): + with pytest.raises(CtapError) as e: + device.MC(user=None) + + assert e.value.code == CtapError.ERR.MISSING_PARAMETER + +def test_bad_type_user_user(device, MCRes): + with pytest.raises(CtapError) as e: + device.MC(user=b"12345678") + +def test_missing_rp(device, MCRes): + req = FidoRequest(MCRes, rp=None) + + with pytest.raises(CtapError) as e: + device.sendMC(*req.toMC()) + + assert e.value.code == CtapError.ERR.MISSING_PARAMETER + +def test_bad_type_rp(device, MCRes): + req = FidoRequest(MCRes, rp=b"1234abcdef") + + with pytest.raises(CtapError) as e: + device.sendMC(*req.toMC()) + +def test_missing_pubKeyCredParams(device, MCRes): + req = FidoRequest(MCRes, key_params=None) + + with pytest.raises(CtapError) as e: + device.sendMC(*req.toMC()) + + assert e.value.code == CtapError.ERR.MISSING_PARAMETER + +def test_bad_type_pubKeyCredParams(device, MCRes): + req = FidoRequest(MCRes, key_params=b"1234a") + + with pytest.raises(CtapError) as e: + device.sendMC(*req.toMC()) + +def test_bad_type_excludeList(device, MCRes): + req = FidoRequest(MCRes, exclude_list=8) + + with pytest.raises(CtapError) as e: + device.sendMC(*req.toMC()) + +def test_bad_type_extensions(device, MCRes): + req = FidoRequest(MCRes, extensions=8) + + with pytest.raises(CtapError) as e: + device.sendMC(*req.toMC()) + +def test_bad_type_options(device, MCRes): + req = FidoRequest(MCRes, options=8) + + with pytest.raises(CtapError) as e: + device.sendMC(*req.toMC()) + +def test_bad_type_rp_name(device, MCRes): + req = FidoRequest(MCRes, rp={"id": "test.org", "name": 8, "icon": "icon"}) + + with pytest.raises(CtapError) as e: + device.sendMC(*req.toMC()) + +def test_bad_type_rp_id(device, MCRes): + req = FidoRequest(MCRes, rp={"id": 8, "name": "name", "icon": "icon"}) + + with pytest.raises(CtapError) as e: + device.sendMC(*req.toMC()) + +def test_bad_type_rp_icon(device, MCRes): + req = FidoRequest(MCRes, rp={"id": "test.org", "name": "name", "icon": 8}) + + with pytest.raises(CtapError) as e: + device.sendMC(*req.toMC()) + +def test_bad_type_user_name(device, MCRes): + req = FidoRequest(MCRes, user={"id": b"user_id", "name": 8}) + + with pytest.raises(CtapError) as e: + device.sendMC(*req.toMC()) + +def test_bad_type_user_id(device, MCRes): + req = FidoRequest(MCRes, user={"id": "user_id", "name": "name"}) + + with pytest.raises(CtapError) as e: + device.sendMC(*req.toMC()) + +def test_bad_type_user_displayName(device, MCRes): + req = FidoRequest( + MCRes, user={"id": "user_id", "name": "name", "displayName": 8} + ) + + with pytest.raises(CtapError) as e: + device.sendMC(*req.toMC()) + +def test_bad_type_user_icon(device, MCRes): + req = FidoRequest(MCRes, user={"id": "user_id", "name": "name", "icon": 8}) + + with pytest.raises(CtapError) as e: + device.sendMC(*req.toMC()) + +def test_bad_type_pubKeyCredParams(device, MCRes): + req = FidoRequest(MCRes, key_params=["wrong"]) + + with pytest.raises(CtapError) as e: + device.sendMC(*req.toMC()) + +def test_missing_pubKeyCredParams_type(device, MCRes): + req = FidoRequest(MCRes, key_params=[{"alg": ES256.ALGORITHM}]) + + with pytest.raises(CtapError) as e: + device.sendMC(*req.toMC()) + + assert e.value.code == CtapError.ERR.MISSING_PARAMETER + +def test_missing_pubKeyCredParams_alg(device, MCRes): + req = FidoRequest(MCRes, key_params=[{"type": "public-key"}]) + + with pytest.raises(CtapError) as e: + device.sendMC(*req.toMC()) + + assert e.value.code in [ + CtapError.ERR.MISSING_PARAMETER, + CtapError.ERR.UNSUPPORTED_ALGORITHM, + ] + +def test_bad_type_pubKeyCredParams_alg(device, MCRes): + req = FidoRequest(MCRes, key_params=[{"alg": "7", "type": "public-key"}]) + + with pytest.raises(CtapError) as e: + device.sendMC(*req.toMC()) + +def test_unsupported_algorithm(device, MCRes): + req = FidoRequest(MCRes, key_params=[{"alg": 1337, "type": "public-key"}]) + + with pytest.raises(CtapError) as e: + device.sendMC(*req.toMC()) + + assert e.value.code == CtapError.ERR.UNSUPPORTED_ALGORITHM + +def test_exclude_list(device, MCRes): + req = FidoRequest(MCRes, exclude_list=[{"id": b"1234", "type": "rot13"}]) + + device.sendMC(*req.toMC()) + +def test_exclude_list2(device, MCRes): + req = FidoRequest( + MCRes, + exclude_list=[{"id": b"1234", "type": "mangoPapayaCoconutNotAPublicKey"}], + ) + + device.sendMC(*req.toMC()) + +def test_bad_type_exclude_list(device, MCRes): + req = FidoRequest(MCRes, exclude_list=["1234"]) + + with pytest.raises(CtapError) as e: + device.sendMC(*req.toMC()) + +def test_missing_exclude_list_type(device, MCRes): + req = FidoRequest(MCRes, exclude_list=[{"id": b"1234"}]) + + with pytest.raises(CtapError) as e: + device.sendMC(*req.toMC()) + +def test_missing_exclude_list_id(device, MCRes): + req = FidoRequest(MCRes, exclude_list=[{"type": "public-key"}]) + + with pytest.raises(CtapError) as e: + device.sendMC(*req.toMC()) + +def test_bad_type_exclude_list_id(device, MCRes): + req = FidoRequest(MCRes, exclude_list=[{"type": "public-key", "id": "1234"}]) + + with pytest.raises(CtapError) as e: + device.sendMC(*req.toMC()) + +def test_bad_type_exclude_list_type(device, MCRes): + req = FidoRequest(MCRes, exclude_list=[{"type": b"public-key", "id": b"1234"}]) + + with pytest.raises(CtapError) as e: + device.sendMC(*req.toMC()) + +def test_exclude_list_excluded(device, MCRes, GARes): + req = FidoRequest(MCRes, exclude_list=GARes.request.allow_list) + + with pytest.raises(CtapError) as e: + device.sendMC(*req.toMC()) + + assert e.value.code == CtapError.ERR.CREDENTIAL_EXCLUDED + +def test_unknown_option(device, MCRes): + req = FidoRequest(MCRes, options={"unknown": False}) + print("MC", req.toMC()) + device.sendMC(*req.toMC()) + +def test_eddsa(device): + mc_req = FidoRequest( + key_params=[{"type": "public-key", "alg": EdDSA.ALGORITHM}] + ) + try: + mc_res = device.sendMC(*mc_req.toMC()) + except CtapError as e: + if e.code == CtapError.ERR.UNSUPPORTED_ALGORITHM: + print("ed25519 is not supported. Skip this test.") + return + + setattr(mc_res, "request", mc_req) + + allow_list = [ + { + "id": mc_res.auth_data.credential_data.credential_id[:], + "type": "public-key", + } + ] + + ga_req = FidoRequest(allow_list=allow_list) + ga_res = device.sendGA(*ga_req.toGA()) + setattr(ga_res, "request", ga_req) + + try: + verify(mc_res, ga_res) + except: + # Print out extra details on failure + from binascii import hexlify + + print("authdata", hexlify(ga_res.auth_data)) + print("cdh", hexlify(ga_res.request.cdh)) + print("sig", hexlify(ga_res.signature)) + from fido2.ctap2 import AttestedCredentialData + + credential_data = AttestedCredentialData(mc_res.auth_data.credential_data) + print("public key:", hexlify(credential_data.public_key[-2])) + verify(mc_res, ga_res)