Adding first tests.

Signed-off-by: Pol Henarejos <pol.henarejos@cttc.es>
This commit is contained in:
Pol Henarejos
2022-09-30 12:06:43 +02:00
parent 4e94cbe40e
commit cc8d9e0741
4 changed files with 519 additions and 0 deletions

225
tests/conftest.py Normal file
View File

@@ -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

View File

@@ -0,0 +1,7 @@
def test_authenticate(device):
device.reset()
REGRes,AUTData = device.register()
credentials = [AUTData.credential_data]
AUTRes = device.authenticate(credentials)

View File

@@ -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

View File

@@ -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)