Skip to content

Commit

Permalink
Add client tests
Browse files Browse the repository at this point in the history
  • Loading branch information
dainnilsson committed Jan 27, 2025
1 parent 5e242a8 commit f2e5a91
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 13 deletions.
28 changes: 26 additions & 2 deletions tests/device/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,33 @@ def __init__(self, capmanager):

def print(self, *messages):
with self.capmanager.global_and_fixture_disabled():
print("")
for m in messages:
print(m)

def touch(self):
self.print("👉 Touch the Authenticator")

def insert(self, nfc=False):
self.print(
"♻️ "
+ (
"Place the Authenticator on the NFC reader"
if nfc
else "Connect the Authenticator"
)
)

def remove(self, nfc=False):
self.print(
"🚫 "
+ (
"Remove the Authenticator from the NFC reader"
if nfc
else "Disconnect the Authenticator"
)
)


# Handle user interaction
class CliInteraction(UserInteraction):
Expand All @@ -21,11 +45,11 @@ def __init__(self, printer, pin=TEST_PIN):
self.pin = pin

def prompt_up(self):
self.printer.print("\nTouch your authenticator device now...\n")
self.printer.touch()

def request_pin(self, permissions, rd_id):
return self.pin

def request_uv(self, permissions, rd_id):
self.printer.print("\nUser Verification required.")
self.printer.print("User Verification required.")
return True
27 changes: 16 additions & 11 deletions tests/device/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@ def __init__(self, printer, reader_name):
self.printer = printer

self.printer.print(
"⚠️ Tests will now run against a connected FIDO authenticator. ⚠️ ",
"",
"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!",
"Tests will now run against a connected FIDO authenticator.",
"You may be prompted to interact with the authenticator throughout these "
"tests.",
"",
"THESE TESTS ARE DESTRUCTIVE!",
" ☠️ WARNING! THESE TESTS ARE DESTRUCTIVE! ☠️ ",
"ANY CREDENTIALS ON THIS AUTHENTICATOR WILL BE PERMANENTLY DELETED!",
"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!",
)

if reader_name:
Expand All @@ -51,6 +51,11 @@ def __init__(self, printer, reader_name):
if self.has_ctap2():
options = Ctap2(self.device).info.options
if options.get("clientPin") or options.get("uv"):
self.printer.print(
"As a precaution, these tests will not run on an authenticator "
"which is configured with any form of UV. Factory reset the "
"authenticator prior to running tests against it."
)
pytest.exit("Authenticator must be in a newly-reset state!")

self.setup()
Expand Down Expand Up @@ -79,7 +84,7 @@ def select(descriptor):
exc_info=True,
)

self.printer.print("Insert and touch the authenticator to test...")
self.printer.touch()

descriptors: set[str | bytes] = set()
threads: list[Thread] = []
Expand Down Expand Up @@ -124,7 +129,7 @@ def _connect():
connection = ExclusiveConnectCardConnection(reader.createConnection())
return CtapPcscDevice(connection, reader.name)

self.printer.print("Remove Authenticator from the NFC reader now...")
self.printer.remove(nfc=True)
removed = False
while not event.wait(0.5):
try:
Expand All @@ -138,7 +143,7 @@ def _connect():
pass # Expected, ignore
except (NoCardException, KeyError):
if not removed:
self.printer.print("Place Authenticator on the NFC reader now...")
self.printer.insert(nfc=True)
removed = True

raise Exception("Failed to (re-)connect to Authenticator")
Expand Down Expand Up @@ -168,7 +173,7 @@ def on_keepalive(status):
if status != prompted[0]:
prompted[0] = status
if status == 2:
self.printer.print("Touch the Authenticator now...")
self.printer.touch()

return on_keepalive

Expand All @@ -186,12 +191,12 @@ def _reconnect_usb(self):
info = Ctap2(self._dev).info
logger.debug(f"Reconnect over USB: {dev_path}")

self.printer.print("", "Disconnect the Authenticator now...")
self.printer.remove()
ds = {d.path for d in list_descriptors()}
while dev_path in ds:
event.wait(0.5)
ds = {d.path for d in list_descriptors()}
self.printer.print("Re-insert the Authenticator now...")
self.printer.insert()
ds2 = ds
while True:
event.wait(0.5)
Expand All @@ -214,7 +219,7 @@ def reconnect(self):
return self._dev

def factory_reset(self, setup=False):
self.printer.print("", "PERFORMING FACTORY RESET!")
self.printer.print("⚠️ PERFORMING FACTORY RESET! ⚠️ ")

self.reconnect()

Expand Down
101 changes: 101 additions & 0 deletions tests/device/test_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
from fido2.ctap2.pin import ClientPin
from fido2.ctap2.credman import CredentialManagement
from fido2.server import Fido2Server, to_descriptor
from fido2.client import ClientError
from . import TEST_PIN

import pytest
import os


rp = {"id": "example.com", "name": "Example RP"}
user = {"id": b"user_id", "name": "A. User"}
server = Fido2Server(rp)


@pytest.fixture(scope="module")
def excluded_match(dev_manager):
if dev_manager.has_ctap2():
return "CREDENTIAL_EXCLUDED"
return "DEVICE_INELIGIBLE"


@pytest.fixture(scope="module")
def credential(dev_manager):
create_options, state = server.register_begin(user)
result = dev_manager.client.make_credential(create_options.public_key)
auth_data = server.register_complete(state, result)
return auth_data.credential_data


@pytest.fixture(scope="module")
def discoverable_credential(dev_manager):
if not dev_manager.has_ctap2():
pytest.skip("Authenticator does not support CTAP 2")

create_options, state = server.register_begin(
user,
resident_key_requirement="required",
)
result = dev_manager.client.make_credential(create_options.public_key)
auth_data = server.register_complete(state, result)
yield auth_data.credential_data

# Delete credential via credman, or factory reset
if CredentialManagement.is_supported(dev_manager.info):
client_pin = ClientPin(dev_manager.ctap2)
token = client_pin.get_pin_token(TEST_PIN, ClientPin.PERMISSION.CREDENTIAL_MGMT)
credman = CredentialManagement(dev_manager.ctap2, client_pin.protocol, token)
cred_id = {"id": auth_data.credential_data.credential_id, "type": "public-key"}
credman.delete_cred(cred_id)
else:
dev_manager.factory_reset(setup=True)


def test_exclude_credentials_single(credential, client, excluded_match):
create_options, state = server.register_begin(user, [credential])
with pytest.raises(ClientError, match=excluded_match):
client.make_credential(create_options.public_key)


def test_exclude_credentials_multiple(credential, client, excluded_match):
exclude = [{"id": os.urandom(32), "type": "public-key"} for _ in range(5)]
exclude.insert(3, to_descriptor(credential))
create_options, state = server.register_begin(user, exclude)
with pytest.raises(ClientError, match=excluded_match):
client.make_credential(create_options.public_key)


def test_exclude_credentials_others(credential, client):
exclude = [{"id": os.urandom(32), "type": "public-key"} for _ in range(5)]
create_options, state = server.register_begin(user, exclude)
client.make_credential(create_options.public_key)


def test_allow_credentials_empty(discoverable_credential, client):
request_options, state = server.authenticate_begin()
result = client.get_assertion(request_options.public_key).get_response(0)
server.authenticate_complete(state, [discoverable_credential], result)


def test_allow_credentials_single(credential, client):
credentials = [credential]
request_options, state = server.authenticate_begin(credentials)
result = client.get_assertion(request_options.public_key).get_response(0)
server.authenticate_complete(state, credentials, result)


def test_allow_credentials_multiple(credential, client):
credentials = [credential]
allow = [{"id": os.urandom(32), "type": "public-key"} for _ in range(5)]
allow.insert(3, to_descriptor(credential))
request_options, state = server.authenticate_begin(allow)
result = client.get_assertion(request_options.public_key).get_response(0)
server.authenticate_complete(state, credentials, result)


def test_allow_credentials_ineligible(credential, client):
allow = [{"id": os.urandom(32), "type": "public-key"} for _ in range(5)]
request_options, state = server.authenticate_begin(allow)
with pytest.raises(ClientError, match="DEVICE_INELIGIBLE"):
client.get_assertion(request_options.public_key)

0 comments on commit f2e5a91

Please sign in to comment.