Adding authentication tests.

Signed-off-by: Pol Henarejos <pol.henarejos@cttc.es>
This commit is contained in:
Pol Henarejos
2022-10-02 00:28:13 +02:00
parent cb4827688b
commit 47ea749454
3 changed files with 262 additions and 47 deletions

View File

@@ -4,24 +4,11 @@ from fido2.client import Fido2Client, WindowsClient, UserInteraction, ClientErro
from fido2.ctap2.pin import ClientPin from fido2.ctap2.pin import ClientPin
from fido2.server import Fido2Server from fido2.server import Fido2Server
from fido2.ctap import CtapError from fido2.ctap import CtapError
from fido2.webauthn import ( from fido2.webauthn import CollectedClientData
Aaguid,
AttestationObject,
CollectedClientData,
PublicKeyCredentialCreationOptions,
PublicKeyCredentialRequestOptions,
AuthenticatorSelectionCriteria,
UserVerificationRequirement,
AuthenticatorAttestationResponse,
AuthenticatorAssertionResponse,
AttestationConveyancePreference,
)
from getpass import getpass from getpass import getpass
import sys import sys
import ctypes
import pytest import pytest
import os import os
import inspect
DEFAULT_PIN='12345678' DEFAULT_PIN='12345678'
@@ -95,6 +82,13 @@ class Device():
self.__user = user self.__user = user
return self.__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): def reset(self):
print("Resetting Authenticator...") print("Resetting Authenticator...")
try: try:
@@ -125,15 +119,15 @@ class Device():
) )
return att_obj 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( 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) type=CollectedClientData.TYPE.CREATE, origin=self.__origin, challenge=os.urandom(32)
), ),
rp=rp or self.__rp, rp=rp if rp is not Ellipsis else self.__rp,
user=user or self.user(), user=user if user is not Ellipsis else self.user(),
key_params=key_params or self.__server.allowed_algorithms, key_params=key_params if key_params is not Ellipsis else self.__server.allowed_algorithms,
exclude_list=exclude_list, exclude_list=exclude_list,
extensions=extensions, extensions=extensions,
rk=rk, rk=rk,
@@ -160,7 +154,7 @@ class Device():
def register(self, uv=None): def register(self, uv=None):
# Prepare parameters for makeCredential # Prepare parameters for makeCredential
create_options, state = self.__server.register_begin( 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 # Create a credential
result = self.try_make_credential(create_options) result = self.try_make_credential(create_options)
@@ -206,6 +200,35 @@ class Device():
print() print()
print("AUTH DATA:", result.authenticator_data) 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") @pytest.fixture(scope="session")
def device(): def device():
dev = Device() dev = Device()
@@ -223,3 +246,10 @@ def MCRes(device, *args):
def resetdevice(device): def resetdevice(device):
device.reset() device.reset()
return device 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

View File

@@ -1,3 +1,6 @@
from fido2.utils import sha256
from fido2.client import CtapError
import pytest
def test_authenticate(device): def test_authenticate(device):
device.reset() device.reset()
@@ -5,3 +8,182 @@ def test_authenticate(device):
credentials = [AUTData.credential_data] credentials = [AUTData.credential_data]
AUTRes = device.authenticate(credentials) 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.
# <https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#:~:text=If%20the%20allowList%20parameter%20is%20present%3A,Go%20to%20Step%2013>
#
# 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"}
])

View File

@@ -10,10 +10,10 @@ def test_register(device):
def test_make_credential(): def test_make_credential():
pass pass
def test_attestation_format( MCRes): def test_attestation_format(MCRes):
assert MCRes.fmt in ["packed", "tpm", "android-key", "adroid-safetynet"] 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 assert len(MCRes.auth_data) >= 77
def test_missing_cdh(device): def test_missing_cdh(device):
@@ -26,15 +26,15 @@ def test_bad_type_cdh(device):
with pytest.raises(CtapError) as e: with pytest.raises(CtapError) as e:
device.MC(client_data_hash=b'\xff') device.MC(client_data_hash=b'\xff')
def test_missing_user(device, MCRes): def test_missing_user(device):
with pytest.raises(CtapError) as e: with pytest.raises(CtapError) as e:
device.MC(user=None) device.doMC(user=None)
assert e.value.code == CtapError.ERR.MISSING_PARAMETER assert e.value.code == CtapError.ERR.MISSING_PARAMETER
def test_bad_type_user_user(device): def test_bad_type_user_user(device):
with pytest.raises(CtapError) as e: with pytest.raises(CtapError) as e:
device.MC(user=b"12345678") device.doMC(user=b"12345678")
def test_missing_rp(device): def test_missing_rp(device):
with pytest.raises(CtapError) as e: with pytest.raises(CtapError) as e:
@@ -48,7 +48,7 @@ def test_bad_type_rp(device):
def test_missing_pubKeyCredParams(device): def test_missing_pubKeyCredParams(device):
with pytest.raises(CtapError) as e: with pytest.raises(CtapError) as e:
device.MC(key_params=None) device.doMC(key_params=None)
assert e.value.code == CtapError.ERR.MISSING_PARAMETER 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): def test_bad_type_rp_name(device):
with pytest.raises(CtapError) as e: 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): def test_bad_type_rp_id(device):
with pytest.raises(CtapError) as e: 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): def test_bad_type_rp_icon(device):
with pytest.raises(CtapError) as e: 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): def test_bad_type_user_name(device):
with pytest.raises(CtapError) as e: 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): def test_bad_type_user_id(device):
with pytest.raises(CtapError) as e: 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): def test_bad_type_user_displayName(device):
with pytest.raises(CtapError) as e: 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): def test_bad_type_user_icon(device):
with pytest.raises(CtapError) as e: 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): def test_bad_type_pubKeyCredParams(device):
with pytest.raises(CtapError) as e: with pytest.raises(CtapError) as e:
device.MC(key_params=["wrong"]) device.doMC(key_params=["wrong"])
def test_missing_pubKeyCredParams_type(device): def test_missing_pubKeyCredParams_type(device):
with pytest.raises(CtapError) as e: 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 assert e.value.code == CtapError.ERR.MISSING_PARAMETER
def test_missing_pubKeyCredParams_alg(device): def test_missing_pubKeyCredParams_alg(device):
with pytest.raises(CtapError) as e: 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 [ assert e.value.code in [
CtapError.ERR.MISSING_PARAMETER, CtapError.ERR.MISSING_PARAMETER,
@@ -117,43 +117,46 @@ def test_missing_pubKeyCredParams_alg(device):
def test_bad_type_pubKeyCredParams_alg(device): def test_bad_type_pubKeyCredParams_alg(device):
with pytest.raises(CtapError) as e: 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): def test_unsupported_algorithm(device):
with pytest.raises(CtapError) as e: 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 assert e.value.code == CtapError.ERR.UNSUPPORTED_ALGORITHM
def test_exclude_list(resetdevice): 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): 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): def test_bad_type_exclude_list(device):
with pytest.raises(CtapError) as e: with pytest.raises(CtapError) as e:
device.MC(exclude_list=["1234"]) device.doMC(exclude_list=["1234"])
def test_missing_exclude_list_type(device): def test_missing_exclude_list_type(device):
with pytest.raises(CtapError) as e: 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): def test_missing_exclude_list_id(device):
with pytest.raises(CtapError) as e: 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): def test_bad_type_exclude_list_id(device):
with pytest.raises(CtapError) as e: 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): def test_bad_type_exclude_list_type(device):
with pytest.raises(CtapError) as e: 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: 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 assert e.value.code == CtapError.ERR.CREDENTIAL_EXCLUDED