From 6f226001df789060e0e02f4497b3d9ae95a6ed42 Mon Sep 17 00:00:00 2001 From: Pol Henarejos Date: Mon, 3 Oct 2022 01:24:17 +0200 Subject: [PATCH] More test fixes. Signed-off-by: Pol Henarejos --- tests/conftest.py | 85 +++++++++++++-------- tests/pico-fido/test_authenticate.py | 42 +++++------ tests/pico-fido/test_discoverable.py | 107 +++++++++++---------------- tests/pico-fido/test_register.py | 6 +- 4 files changed, 124 insertions(+), 116 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 74f287c..acb0bb6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,7 +4,7 @@ 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 CollectedClientData +from fido2.webauthn import CollectedClientData, AttestedCredentialData from getpass import getpass import sys import pytest @@ -106,11 +106,15 @@ class Device(): self.__set_server(rp=self.__rp, attestation=self.__attestation) 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): + 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 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, + client_data_hash=client_data_hash, + rp=rp, + user=user, + key_params=key_params, exclude_list=exclude_list, extensions=extensions, options=options, @@ -118,17 +122,23 @@ class Device(): pin_uv_protocol=pin_uv_protocol, enterprise_attestation=enterprise_attestation ) - return att_obj + return {'res':att_obj,'req':{'client_data_hash':client_data_hash, + 'rp':rp, + 'user':user, + '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): - - result = self.__client._backend.do_make_credential( - client_data=client_data if client_data is not Ellipsis else 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 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, + ) + 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 + result = self.__client._backend.do_make_credential( + client_data=client_data, + rp=rp, + user=user, + key_params=key_params, exclude_list=exclude_list, extensions=extensions, rk=rk, @@ -136,7 +146,10 @@ class Device(): enterprise_attestation=enterprise_attestation, event=event ) - return result + return {'res':result,'req':{'client_data':client_data, + 'rp':rp, + 'user':user, + 'key_params':key_params}} def try_make_credential(self, options=None): if (options is None): @@ -202,32 +215,38 @@ class Device(): 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): + 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) 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), + rp_id=rp_id, + client_data_hash=client_data_hash, allow_list=allow_list, extensions=extensions, options=options, pin_uv_param=pin_uv_param, pin_uv_protocol=pin_uv_protocol ) - return att_obj + return {'res':att_obj,'req':{'rp_id':rp_id, + 'client_data_hash':client_data_hash}} 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( + 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'], + ) + rp_id = rp_id if rp_id is not Ellipsis else self.__rp['id'] + result = self.__client._backend.do_get_assertion( + client_data=client_data, + rp_id=rp_id, allow_list=allow_list, extensions=extensions, user_verification=user_verification, event=event ) - return result + return {'res':result,'req':{'client_data':client_data, + 'rp_id':rp_id}} @pytest.fixture(scope="session") @@ -241,7 +260,7 @@ def info(device): @pytest.fixture(scope="session") def MCRes(device, *args): - return device.doMC(*args).attestation_object + return device.doMC(*args) @pytest.fixture(scope="session") def resetdevice(device): @@ -250,18 +269,24 @@ def resetdevice(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"} + res = device.doGA(allow_list=[ + {"id": MCRes['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"} ], *args) - return r + credential_data = AttestedCredentialData(MCRes['res'].attestation_object.auth_data.credential_data) + assertions = res['res'].get_assertions() + for a in assertions: + a.verify(res['req']['client_data'].hash, credential_data.public_key) + return res @pytest.fixture(scope="session") def MCRes_DC(device, *args): - return device.doMC(rk=True, *args).attestation_object + return device.doMC(rk=True, *args) @pytest.fixture(scope="session") def GARes_DC(device, MCRes_DC, *args): - r = device.GA(allow_list=[ - {"id": MCRes_DC.auth_data.credential_data.credential_id, "type": "public-key"} + res = device.GA(allow_list=[ + {"id": MCRes_DC['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"} ], *args) - return r + credential_data = AttestedCredentialData(MCRes_DC['res'].attestation_object.auth_data.credential_data) + res['res'].verify(res['req']['client_data_hash'], credential_data.public_key) + return res diff --git a/tests/pico-fido/test_authenticate.py b/tests/pico-fido/test_authenticate.py index 090eeeb..a002004 100644 --- a/tests/pico-fido/test_authenticate.py +++ b/tests/pico-fido/test_authenticate.py @@ -10,17 +10,17 @@ def test_authenticate(device): AUTRes = device.authenticate(credentials) def test_assertion_auth_data(GARes): - assert len(GARes.get_response(0).authenticator_data) == 37 + assert len(GARes['res'].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 + assert (GARes['res'].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"} + {"id": MCRes['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"} ]) - assert res.user == None - assert res.number_of_credentials == None + assert res['res'].user == None + assert res['res'].number_of_credentials == None def test_empty_allowList(device): with pytest.raises(CtapError) as e: @@ -41,7 +41,7 @@ def test_get_assertion_allow_list_filtering_and_buffering(device): l1 = 4 for i in range(0, l1): - res = device.doMC(rp=rp1).attestation_object + res = device.doMC(rp=rp1)['res'].attestation_object rp1_registrations.append(res) allow_list.append({ "id": res.auth_data.credential_data.credential_id[:], @@ -50,7 +50,7 @@ def test_get_assertion_allow_list_filtering_and_buffering(device): l2 = 6 for i in range(0, l2): - res = device.doMC(rp=rp2).attestation_object + res = device.doMC(rp=rp2)['res'].attestation_object rp2_registrations.append(res) allow_list.append({ "id": res.auth_data.credential_data.credential_id[:], @@ -66,10 +66,10 @@ def test_get_assertion_allow_list_filtering_and_buffering(device): # cached. # Should authenticate to all credentials matching rp1 - rp1_assertions = device.doGA(rp_id=rp1['id'], allow_list=allow_list).get_assertions() + rp1_assertions = device.doGA(rp_id=rp1['id'], allow_list=allow_list)['res'].get_assertions() # Should authenticate to all credentials matching rp2 - rp2_assertions = device.doGA(rp_id=rp2['id'], allow_list=allow_list).get_assertions() + rp2_assertions = device.doGA(rp_id=rp2['id'], allow_list=allow_list)['res'].get_assertions() counts = ( len(rp1_assertions), @@ -80,14 +80,14 @@ def test_get_assertion_allow_list_filtering_and_buffering(device): def test_corrupt_credId(device, MCRes): # apply bit flip - badid = list(MCRes.auth_data.credential_data.credential_id[:]) + badid = list(MCRes['res'].attestation_object.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) + device.doGA(allow_list=allow_list)['res'] assert e.value.code == CtapError.ERR.NO_CREDENTIALS def test_mismatched_rp(device, GARes): @@ -124,38 +124,38 @@ def test_bad_allow_list(device): 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"} + {"id": MCRes['res'].attestation_object.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"} + {"id": MCRes['res'].attestation_object.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}) + res = device.doGA(options={"uv": True})['res'] 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}) + res = device.doGA(options={"up": True})['res'] 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"} + {"id": MCRes['res'].attestation_object.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"} + {"id": MCRes['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"} ] ) @@ -163,7 +163,7 @@ 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"} + {"id": MCRes['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"} ] ) @@ -171,20 +171,20 @@ 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"} + {"id": MCRes['res'].attestation_object.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"} + {"id": MCRes['res'].attestation_object.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"} + {"id": MCRes['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"} ]) def test_credential_resets(device, MCRes, GARes): diff --git a/tests/pico-fido/test_discoverable.py b/tests/pico-fido/test_discoverable.py index 9d4cc68..682e5d7 100644 --- a/tests/pico-fido/test_discoverable.py +++ b/tests/pico-fido/test_discoverable.py @@ -46,11 +46,11 @@ def generate_user_maximum(): @pytest.mark.parametrize("do_reboot", [False, True]) def test_user_info_returned_when_using_allowlist(device, MCRes_DC, GARes_DC, do_reboot): - assert "id" in GARes_DC.user.keys() + assert "id" in GARes_DC['res'].user.keys() allow_list = [ { - "id": MCRes_DC.auth_data.credential_data.credential_id[:], + "id": MCRes_DC['res'].attestation_object.auth_data.credential_data.credential_id[:], "type": "public-key", } ] @@ -58,23 +58,23 @@ def test_user_info_returned_when_using_allowlist(device, MCRes_DC, GARes_DC, do_ if do_reboot: device.reboot() - ga_res = device.GA(allow_list=allow_list) + ga_res = device.GA(allow_list=allow_list)['res'] - assert device.user()["id"] == ga_res.user["id"] + assert MCRes_DC["req"]["user"]["id"] == ga_res.user["id"] def test_with_allow_list_after_reset(device, MCRes_DC, GARes_DC): - assert "id" in GARes_DC.user.keys() + assert "id" in GARes_DC['res'].user.keys() allow_list = [ { - "id": MCRes_DC.auth_data.credential_data.credential_id[:], + "id": MCRes_DC['res'].attestation_object.auth_data.credential_data.credential_id[:], "type": "public-key", } ] - ga_res = device.GA(allow_list=allow_list) + ga_res = device.GA(allow_list=allow_list)['res'] - assert device.user()["id"] == ga_res.user["id"] + assert MCRes_DC["req"]["user"]["id"] == ga_res.user["id"] device.reset() @@ -91,16 +91,16 @@ def test_resident_key_auth(MCRes_DC, GARes_DC): pass def test_user_info_returned(device, MCRes_DC, GARes_DC): - assert "id" in GARes_DC.user.keys() + assert "id" in GARes_DC['res'].user.keys() assert ( - MCRes_DC.auth_data.credential_data.credential_id - == GARes_DC.credential["id"] + MCRes_DC['res'].attestation_object.auth_data.credential_data.credential_id + == GARes_DC['res'].credential["id"] ) - assert device.user()["id"] == GARes_DC.user["id"] - if not GARes_DC.number_of_credentials: - assert "id" in GARes_DC.user.keys() and len(GARes_DC.user.keys()) == 1 + assert MCRes_DC["req"]["user"]["id"] == GARes_DC['res'].user["id"] + if not GARes_DC['res'].number_of_credentials: + assert "id" in GARes_DC['res'].user.keys() and len(GARes_DC['res'].user.keys()) == 1 else: - assert device.user() == GARes_DC.user + assert MCRes_DC["req"]["user"] == GARes_DC['res'].user def test_multiple_rk_nodisplay(device, MCRes_DC): @@ -113,7 +113,7 @@ def test_multiple_rk_nodisplay(device, MCRes_DC): regs.append(res) # time.sleep(2) - res = device.doGA(rp_id=rp['id']) + res = device.doGA(rp_id=rp['id'])['res'] auths = res.get_assertions() assert len(regs) == 3 @@ -125,21 +125,20 @@ def test_multiple_rk_nodisplay(device, MCRes_DC): print("FAIL: %s was not in user: " % y, x.user) -def test_rk_maximum_size_nodisplay(device, MCRes_DC): +def test_rk_maximum_size_nodisplay(device): """ Check the lengths of the fields according to the FIDO2 spec https://github.com/solokeys/solo/issues/158#issue-426613303 https://www.w3.org/TR/webauthn/#dom-publickeycredentialuserentity-displayname """ - auths = [] + device.reset() user_max = generate_user_maximum() - print(user_max) - resMC = device.doMC(user=user_max) - resGA = device.doGA() + resMC = device.doMC(user=user_max, rk=True) + resGA = device.doGA()['res'] auths = resGA.get_assertions() user_max_GA = auths[0] - + print(auths) for y in ("name", "displayName", "id"): assert user_max_GA.user[y] == user_max[y] @@ -172,20 +171,18 @@ def test_rk_maximum_list_capacity_per_rp_nodisplay(info, device, MCRes_DC): regs = [MCRes_DC] RK_to_generate = RK_CAPACITY_PER_RP - current_credentials_count for i in range(RK_to_generate): - req = FidoRequest(MCRes_DC, user=get_user(), rp = rp) - res = device.sendMC(*req.toMC()) + res = device.doMC(user=get_user(), rp=rp, rk=True)['res'].attestation_object regs.append(res) - req = FidoRequest(MCRes_DC, options=None, user=generate_user_maximum(), rp = rp) - res = device.sendGA(*req.toGA()) + res = device.GA(rp_id = rp['id'])['res'] assert res.number_of_credentials == RK_CAPACITY_PER_RP auths.append(res) for i in range(RK_CAPACITY_PER_RP - 1): - auths.append(device.ctap2.get_next_assertion()) + auths.append(device.GNA()) with pytest.raises(CtapError) as e: - device.ctap2.get_next_assertion() + device.GNA() auths = auths[::-1][-RK_to_generate:] regs = regs[-RK_to_generate:] @@ -193,29 +190,24 @@ def test_rk_maximum_list_capacity_per_rp_nodisplay(info, device, MCRes_DC): assert len(auths) == len(users) - if MCRes_DC.request.pin_protocol: - for x, u in zip(auths, users): - for y in ("name", "icon", "displayName", "id"): - assert y in x.user.keys() - assert x.user[y] == u[y] + for x, u in zip(auths, users): + for y in ("name", "displayName", "id"): + assert y in x.user.keys() + assert x.user[y] == u[y] assert len(auths) == len(regs) - for x, y in zip(regs, auths): - verify(x, y, req.cdh) -def test_rk_with_allowlist_of_different_rp(resetDevice): +def test_rk_with_allowlist_of_different_rp(resetdevice): """ Test that a rk credential is not found when using an allowList item for a different RP """ rk_rp = {"id": "rk-cred.org", "name": "Example"} - rk_req = FidoRequest(rp = rk_rp, options={"rk": True}) - rk_res = resetDevice.sendMC(*rk_req.toMC()) + rk_res = resetdevice.doMC(rp = rk_rp, rk=True)['res'].attestation_object server_rp = {"id": "server-cred.com", "name": "Example"} - server_req = FidoRequest(rp = server_rp) - server_res = resetDevice.sendMC(*server_req.toMC()) + server_res = resetdevice.doMC(rp = server_rp, rk=True)['res'].attestation_object allow_list_with_different_rp_cred = [ { @@ -224,43 +216,39 @@ def test_rk_with_allowlist_of_different_rp(resetDevice): } ] - test_req = FidoRequest(rp = rk_rp, allow_list = allow_list_with_different_rp_cred) with pytest.raises(CtapError) as e: - res = resetDevice.sendGA(*test_req.toGA()) + res = resetdevice.doGA(rp_id = rk_rp['id'], allow_list = allow_list_with_different_rp_cred) assert e.value.code == CtapError.ERR.NO_CREDENTIALS -def test_same_userId_overwrites_rk(resetDevice): +def test_same_userId_overwrites_rk(resetdevice): """ A make credential request with a UserId & Rp that is the same as an existing one should overwrite. """ rp = {"id": "overwrite.org", "name": "Example"} - user = generate_user() + user = generate_random_user() - req = FidoRequest(rp = rp, options={"rk": True}, user = user) - mc_res1 = resetDevice.sendMC(*req.toMC()) + mc_res1 = resetdevice.doMC(rp = rp, rk=True, user = user) # Should overwrite the first credential. - mc_res2 = resetDevice.sendMC(*req.toMC()) + mc_res2 = resetdevice.doMC(rp = rp, rk=True, user = user) - ga_res = resetDevice.sendGA(*req.toGA()) + ga_res = resetdevice.GA(rp_id=rp['id'])['res'] # If there's only one credential, this is None assert ga_res.number_of_credentials == None - verify(mc_res2, ga_res, req.cdh) def test_larger_icon_than_128(device): """ Test it works if we give an icon value larger than 128 bytes """ rp = {"id": "overwrite.org", "name": "Example"} - user = generate_user() + user = generate_random_user() user['icon'] = 'https://www.w3.org/TR/webauthn/?icon=' + ("A" * 128) - req = FidoRequest(rp = rp, options={"rk": True}, user = user) - device.sendMC(*req.toMC()) + device.doMC(rp = rp, rk=True, user = user) def test_returned_credential(device): @@ -269,29 +257,24 @@ def test_returned_credential(device): only 1 will get returned. """ device.reset() - pin = '12345' - device.client.pin_protocol.set_pin(pin) - req = FidoRequest(pin = pin, options={"rk": True}) regs = [] allow_list = [] for i in range(0, 2): - req = FidoRequest(req, user = { + res = device.doMC(rk=True, user = { "id": b'123456' + bytes([i]), "name": f'Test User {i}', "displayName": f'Test User display {i}' - }) - res = device.sendMC(*req.toMC()) - setattr(res, "request", req) + })['res'].attestation_object regs.append(res) allow_list.append({"id": res.auth_data.credential_data.credential_id[:], "type": "public-key"}) print('allow_list: ' , allow_list) - ga_req = FidoRequest(pin = pin, allow_list=allow_list) - ga_res = device.sendGA(*ga_req.toGA()) + ga_res = device.GA(allow_list=allow_list)['res'] + print(ga_res) # No other credentials should be returned with pytest.raises(CtapError) as e: - device.ctap2.get_next_assertion() + device.GNA() # the returned credential should have user id in it print(ga_res) diff --git a/tests/pico-fido/test_register.py b/tests/pico-fido/test_register.py index ab84319..0c104c6 100644 --- a/tests/pico-fido/test_register.py +++ b/tests/pico-fido/test_register.py @@ -11,10 +11,10 @@ def test_make_credential(): pass def test_attestation_format(MCRes): - assert MCRes.fmt in ["packed", "tpm", "android-key", "adroid-safetynet"] + assert MCRes['res'].attestation_object.fmt in ["packed", "tpm", "android-key", "adroid-safetynet"] def test_authdata_length(MCRes): - assert len(MCRes.auth_data) >= 77 + assert len(MCRes['res'].attestation_object.auth_data) >= 77 def test_missing_cdh(device): with pytest.raises(CtapError) as e: @@ -152,7 +152,7 @@ def test_bad_type_exclude_list_type(device): device.doMC(exclude_list=[{"type": b"public-key", "id": b"1234"}]) def test_exclude_list_excluded(device): - res = device.doMC().attestation_object + res = device.doMC()['res'].attestation_object with pytest.raises(CtapError) as e: device.doMC(exclude_list=[ {"id": res.auth_data.credential_data.credential_id, "type": "public-key"}