Upgrade tests to python-fido2 v2.0.0

Signed-off-by: Pol Henarejos <pol.henarejos@cttc.es>
This commit is contained in:
Pol Henarejos
2025-08-29 01:20:31 +02:00
parent d30ebde4f0
commit fdf97f5469
10 changed files with 217 additions and 193 deletions

View File

@@ -20,12 +20,13 @@
from http import client
from fido2.hid import CtapHidDevice
from fido2.client import Fido2Client, UserInteraction, ClientError, _Ctap1ClientBackend
from fido2.client import Fido2Client, UserInteraction, ClientError, _Ctap1ClientBackend, DefaultClientDataCollector
from fido2.attestation import FidoU2FAttestation
from fido2.ctap2.pin import ClientPin
from fido2.server import Fido2Server
from fido2.ctap import CtapError
from fido2.webauthn import CollectedClientData, PublicKeyCredentialParameters, PublicKeyCredentialType
from fido2.webauthn import PublicKeyCredentialParameters, PublicKeyCredentialType, PublicKeyCredentialCreationOptions, PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity, AuthenticatorSelectionCriteria, UserVerificationRequirement, PublicKeyCredentialRequestOptions
from fido2.ctap2.extensions import HmacSecretExtension, LargeBlobExtension, CredBlobExtension, CredProtectExtension, MinPinLengthExtension, CredPropsExtension, ThirdPartyPaymentExtension
from utils import *
from fido2.cose import ES256
import sys
@@ -70,11 +71,13 @@ class DeviceSelectCredential:
pass
class Device():
def __init__(self, origin="https://example.com", user_interaction=CliInteraction(),uv="discouraged",rp={"id": "example.com", "name": "Example RP"}, attestation="direct"):
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 __verify_rp(rp_id, origin):
return True
def __set_client(self, origin, user_interaction, uv):
self.__uv = uv
@@ -101,14 +104,23 @@ class Device():
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)
extensions = [
HmacSecretExtension(allow_hmac_secret=True),
LargeBlobExtension(),
CredBlobExtension(),
CredProtectExtension(),
MinPinLengthExtension(),
CredPropsExtension(),
ThirdPartyPaymentExtension()
]
self.__client = Fido2Client(self.__dev, client_data_collector=DefaultClientDataCollector(self.__origin, verify=Device.__verify_rp), user_interaction=self.__user_interaction, extensions=extensions)
# 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")
self.__client1 = Fido2Client(self.__dev, self.__origin, user_interaction=self.__user_interaction)
self.__client1 = Fido2Client(self.__dev, client_data_collector=DefaultClientDataCollector(self.__origin, verify=Device.__verify_rp), user_interaction=self.__user_interaction)
self.__client1._backend = _Ctap1ClientBackend(self.__dev, user_interaction=self.__user_interaction)
self.ctap1 = self.__client1._backend.ctap1
@@ -117,7 +129,7 @@ class Device():
self.__attestation = attestation
self.__server = Fido2Server(self.__rp, attestation=self.__attestation)
self.__server.allowed_algorithms = [
PublicKeyCredentialParameters(PublicKeyCredentialType.PUBLIC_KEY, p['alg'])
PublicKeyCredentialParameters(type=PublicKeyCredentialType.PUBLIC_KEY, alg=p['alg'])
for p in self.__client._backend.info.algorithms
]
@@ -216,9 +228,7 @@ class Device():
'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, ctap1=False):
client_data = client_data if client_data is not Ellipsis else CollectedClientData.create(
type=CollectedClientData.TYPE.CREATE, origin=self.__origin, challenge=os.urandom(32)
)
client_data = client_data if client_data is not Ellipsis else DefaultClientDataCollector(origin=self.__origin, verify=Device.__verify_rp)
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
@@ -226,22 +236,31 @@ class Device():
client = self.__client1
else:
client = self.__client
result = client._backend.do_make_credential(
client_data=client_data,
rp=rp,
user=user,
key_params=key_params,
exclude_list=exclude_list,
options=PublicKeyCredentialCreationOptions(
rp=PublicKeyCredentialRpEntity.from_dict(rp),
user=PublicKeyCredentialUserEntity.from_dict(user),
pub_key_cred_params=key_params,
exclude_credentials=exclude_list,
extensions=extensions,
rk=rk,
user_verification=user_verification,
enterprise_attestation=enterprise_attestation,
challenge=os.urandom(32),
authenticator_selection=AuthenticatorSelectionCriteria(
require_resident_key=rk,
user_verification=UserVerificationRequirement.REQUIRED if user_verification else UserVerificationRequirement.DISCOURAGED
),
attestation=enterprise_attestation
)
client_data, rp_id = client_data.collect_client_data(options=options)
result = client._backend.do_make_credential(
options=options,
client_data=client_data,
rp_id=rp_id,
enterprise_rpid_list=None,
event=event
)
return {'res':result,'req':{'client_data':client_data,
return {'res':result.response,'req':{'client_data':client_data,
'rp':rp,
'user':user,
'key_params':key_params}}
'key_params':key_params},'client_extension_results':result.client_extension_results}
def try_make_credential(self, options=None):
if (options is None):
@@ -267,14 +286,14 @@ class Device():
# Complete registration
auth_data = self.__server.register_complete(
state, result.client_data, result.attestation_object
state=state, response=result
)
credentials = [auth_data.credential_data]
print("New credential created!")
print("CLIENT DATA:", result.client_data)
print("ATTESTATION OBJECT:", result.attestation_object)
print("CLIENT DATA:", result.response.client_data)
print("ATTESTATION OBJECT:", result.response.attestation_object)
print()
print("CREDENTIAL DATA:", auth_data.credential_data)
@@ -294,17 +313,14 @@ class Device():
self.__server.authenticate_complete(
state,
credentials,
result.credential_id,
result.client_data,
result.authenticator_data,
result.signature,
result
)
print("Credential authenticated!")
print("CLIENT DATA:", result.client_data)
print("CLIENT DATA:", result.response.client_data)
print()
print("AUTH DATA:", result.authenticator_data)
print("AUTH DATA:", result.response.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']
@@ -325,21 +341,31 @@ class Device():
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, ctap1=False, check_only=False):
client_data = client_data if client_data is not Ellipsis else CollectedClientData.create(
type=CollectedClientData.TYPE.GET, origin=self.__origin, challenge=os.urandom(32)
)
client_data = client_data if client_data is not Ellipsis else DefaultClientDataCollector(origin=self.__origin, verify=Device.__verify_rp)
if (ctap1 is True):
client = self.__client1
else:
client = self.__client
rp_id = rp_id if rp_id is not Ellipsis else self.__rp['id']
options=PublicKeyCredentialRequestOptions(
challenge=os.urandom(32),
rp_id=rp_id,
allow_credentials=allow_list,
user_verification=UserVerificationRequirement.REQUIRED if user_verification else UserVerificationRequirement.DISCOURAGED,
extensions=extensions
)
client_data, rp_id = client_data.collect_client_data(options=options)
if (ctap1 is True):
client = self.__client1
else:
client = self.__client
try:
result = client._backend.do_get_assertion(
options=options,
client_data=client_data,
rp_id=rp_id,
allow_list=allow_list,
extensions=extensions,
user_verification=user_verification,
event=event
)
except ClientError as e:
@@ -347,11 +373,9 @@ class Device():
client_pin = ClientPin(self.__client._backend.ctap2)
client_pin.set_pin(DEFAULT_PIN)
result = client._backend.do_get_assertion(
options=options,
client_data=client_data,
rp_id=rp_id,
allow_list=allow_list,
extensions=extensions,
user_verification=user_verification,
event=event
)
else:
@@ -416,8 +440,8 @@ def AuthRes(device, RegRes, *args):
{"id": RegRes['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"}
], *args)
aut_data = res['res'].get_response(0)
m = aut_data.authenticator_data.rp_id_hash + aut_data.authenticator_data.flags.to_bytes(1, 'big') + aut_data.authenticator_data.counter.to_bytes(4, 'big') + aut_data.client_data.hash
ES256(RegRes['res'].attestation_object.auth_data.credential_data.public_key).verify(m, aut_data.signature)
m = aut_data.response.authenticator_data.rp_id_hash + aut_data.response.authenticator_data.flags.to_bytes(1, 'big') + aut_data.response.authenticator_data.counter.to_bytes(4, 'big') + aut_data.response.client_data.hash
ES256(RegRes['res'].attestation_object.auth_data.credential_data.public_key).verify(m, aut_data.response.signature)
return aut_data
@pytest.fixture(scope="class")

View File

@@ -22,11 +22,7 @@ RUN apt install -y libccid \
cmake \
libfuse-dev \
&& rm -rf /var/lib/apt/lists/*
RUN pip3 install pytest pycvc cryptography pyscard inputimeout
RUN git clone https://github.com/polhenarejos/python-fido2.git
WORKDIR /python-fido2
RUN git checkout development
RUN pip3 install .
RUN pip3 install pytest pycvc cryptography pyscard inputimeout fido2==2.0.0
WORKDIR /
RUN git clone https://github.com/frankmorgner/vsmartcard.git
WORKDIR /vsmartcard/virtualsmartcard

View File

@@ -27,16 +27,17 @@
from __future__ import annotations
from .base import HidDescriptor
from ..ctap import CtapDevice, CtapError, STATUS
from ..utils import LOG_LEVEL_TRAFFIC
from threading import Event
from enum import IntEnum, IntFlag, unique
from typing import Tuple, Optional, Callable, Iterator
import logging
import os
import struct
import sys
import os
import logging
from enum import IntEnum, IntFlag, unique
from threading import Event
from typing import Callable, Iterator
from ..ctap import STATUS, CtapDevice, CtapError
from ..utils import LOG_LEVEL_TRAFFIC
from .base import HidDescriptor
logger = logging.getLogger(__name__)
@@ -55,6 +56,7 @@ elif sys.platform.startswith("openbsd"):
from . import openbsd as backend
else:
raise Exception("Unsupported platform")
from . import emulation as backend
list_descriptors = backend.list_descriptors
@@ -62,6 +64,10 @@ get_descriptor = backend.get_descriptor
open_connection = backend.open_connection
class ConnectionFailure(Exception):
"""The CTAP connection failed or returned an invalid response."""
@unique
class CTAPHID(IntEnum):
PING = 0x01
@@ -109,7 +115,7 @@ class CtapHidDevice(CtapDevice):
response = self.call(CTAPHID.INIT, nonce)
r_nonce, response = response[:8], response[8:]
if r_nonce != nonce:
raise Exception("Wrong nonce")
raise ConnectionFailure("Wrong nonce")
(
self._channel_id,
self._u2fhid_version,
@@ -129,7 +135,7 @@ class CtapHidDevice(CtapDevice):
return self._u2fhid_version
@property
def device_version(self) -> Tuple[int, int, int]:
def device_version(self) -> tuple[int, int, int]:
"""Device version number."""
return self._device_version
@@ -139,12 +145,12 @@ class CtapHidDevice(CtapDevice):
return self._capabilities
@property
def product_name(self) -> Optional[str]:
def product_name(self) -> str | None:
"""Product name of device."""
return self.descriptor.product_name
@property
def serial_number(self) -> Optional[str]:
def serial_number(self) -> str | None:
"""Serial number of device."""
return self.descriptor.serial_number
@@ -159,10 +165,22 @@ class CtapHidDevice(CtapDevice):
self,
cmd: int,
data: bytes = b"",
event: Optional[Event] = None,
on_keepalive: Optional[Callable[[int], None]] = None,
event: Event | None = None,
on_keepalive: Callable[[STATUS], None] | None = None,
) -> bytes:
event = event or Event()
while True:
try:
return self._do_call(cmd, data, event, on_keepalive)
except CtapError as e:
if e.code == CtapError.ERR.CHANNEL_BUSY:
if not event.wait(0.1):
logger.warning("CTAP channel busy, trying again...")
continue # Keep retrying on BUSY while not cancelled
raise
def _do_call(self, cmd, data, event, on_keepalive):
remaining = data
seq = 0
@@ -194,7 +212,7 @@ class CtapHidDevice(CtapDevice):
r_channel = struct.unpack_from(">I", recv)[0]
recv = recv[4:]
if r_channel != self._channel_id:
raise Exception("Wrong channel")
raise ConnectionFailure("Wrong channel")
if not response: # Initialization packet
r_cmd, r_len = struct.unpack_from(">BH", recv)
@@ -202,13 +220,12 @@ class CtapHidDevice(CtapDevice):
if r_cmd == TYPE_INIT | cmd:
pass # first data packet
elif r_cmd == TYPE_INIT | CTAPHID.KEEPALIVE:
ka_status = struct.unpack_from(">B", recv)[0]
logger.debug(f"Got keepalive status: {ka_status:02x}")
try:
ka_status = STATUS(struct.unpack_from(">B", recv)[0])
logger.debug(f"Got keepalive status: {ka_status:02x}")
except ValueError:
raise ConnectionFailure("Invalid keepalive status")
if on_keepalive and ka_status != last_ka:
try:
ka_status = STATUS(ka_status)
except ValueError:
pass # Unknown status value
last_ka = ka_status
on_keepalive(ka_status)
continue
@@ -220,7 +237,7 @@ class CtapHidDevice(CtapDevice):
r_seq = struct.unpack_from(">B", recv)[0]
recv = recv[1:]
if r_seq != seq:
raise Exception("Wrong sequence number")
raise ConnectionFailure("Wrong sequence number")
seq += 1
response += recv

View File

@@ -20,8 +20,6 @@
from fido2.client import CtapError
from fido2.cose import ES256, ES384, ES512, EdDSA
import fido2.features
fido2.features.webauthn_json_mapping.enabled = False
from utils import ES256K
import pytest
@@ -51,13 +49,13 @@ def test_bad_type_cdh(device):
def test_missing_user(device):
with pytest.raises(CtapError) as e:
device.doMC(user=None)
device.MC(user=None)
assert e.value.code == CtapError.ERR.MISSING_PARAMETER
def test_bad_type_user_user(device):
with pytest.raises(CtapError) as e:
device.doMC(user=b"12345678")
device.MC(user=b"12345678")
def test_missing_rp(device):
with pytest.raises(CtapError) as e:
@@ -71,7 +69,7 @@ def test_bad_type_rp(device):
def test_missing_pubKeyCredParams(device):
with pytest.raises(CtapError) as e:
device.doMC(key_params=None)
device.MC(key_params=None)
assert e.value.code == CtapError.ERR.MISSING_PARAMETER
@@ -93,35 +91,23 @@ def test_bad_type_options(device):
def test_bad_type_rp_name(device):
with pytest.raises(CtapError) as e:
device.doMC(rp={"id": "test.org", "name": 8, "icon": "icon"})
device.MC(rp={"id": "test.org", "name": 8, "icon": "icon"})
def test_bad_type_rp_id(device):
with pytest.raises(CtapError) as e:
device.doMC(rp={"id": 8, "name": "name", "icon": "icon"})
def test_bad_type_rp_icon(device):
with pytest.raises(CtapError) as e:
device.doMC(rp={"id": "test.org", "name": "name", "icon": 8})
device.MC(rp={"id": 8, "name": "name", "icon": "icon"})
def test_bad_type_user_name(device):
with pytest.raises(CtapError) as e:
device.doMC(user={"id": b"user_id", "name": 8})
device.MC(user={"id": b"user_id", "name": 8})
def test_bad_type_user_id(device):
with pytest.raises(CtapError) as e:
device.doMC(user={"id": "user_id", "name": "name"})
device.MC(user={"id": "user_id", "name": "name"})
def test_bad_type_user_displayName(device):
with pytest.raises(CtapError) as e:
device.doMC(user={"id": "user_id", "name": "name", "displayName": 8})
def test_bad_type_user_icon(device):
with pytest.raises(CtapError) as e:
device.doMC(user={"id": "user_id", "name": "name", "icon": 8})
def test_bad_type_pubKeyCredParams(device):
with pytest.raises(CtapError) as e:
device.doMC(key_params=["wrong"])
device.MC(user={"id": "user_id", "name": "name", "displayName": 8})
@pytest.mark.parametrize(
"alg", [ES256.ALGORITHM, ES384.ALGORITHM, ES512.ALGORITHM, ES256K.ALGORITHM, EdDSA.ALGORITHM]
@@ -132,13 +118,13 @@ def test_algorithms(device, info, alg):
def test_missing_pubKeyCredParams_type(device):
with pytest.raises(CtapError) as e:
device.doMC(key_params=[{"alg": ES256.ALGORITHM}])
device.MC(key_params=[{"alg": ES256.ALGORITHM}])
assert e.value.code == CtapError.ERR.INVALID_CBOR
def test_missing_pubKeyCredParams_alg(device):
with pytest.raises(CtapError) as e:
device.doMC(key_params=[{"type": "public-key"}])
device.MC(key_params=[{"type": "public-key"}])
assert e.value.code in [
CtapError.ERR.INVALID_CBOR,
@@ -147,7 +133,7 @@ def test_missing_pubKeyCredParams_alg(device):
def test_bad_type_pubKeyCredParams_alg(device):
with pytest.raises(CtapError) as e:
device.doMC(key_params=[{"alg": "7", "type": "public-key"}])
device.MC(key_params=[{"alg": "7", "type": "public-key"}])
assert e.value.code == CtapError.ERR.CBOR_UNEXPECTED_TYPE
@@ -158,26 +144,26 @@ def test_unsupported_algorithm(device):
assert e.value.code == CtapError.ERR.UNSUPPORTED_ALGORITHM
def test_exclude_list(resetdevice):
resetdevice.doMC(exclude_list=[{"id": b"1234", "type": "rot13"}])
resetdevice.MC(exclude_list=[{"id": b"1234", "type": "rot13"}])
def test_exclude_list2(resetdevice):
resetdevice.doMC(exclude_list=[{"id": b"1234", "type": "mangoPapayaCoconutNotAPublicKey"}])
resetdevice.MC(exclude_list=[{"id": b"1234", "type": "mangoPapayaCoconutNotAPublicKey"}])
def test_bad_type_exclude_list(device):
with pytest.raises(CtapError) as e:
device.doMC(exclude_list=["1234"])
device.MC(exclude_list=["1234"])
def test_missing_exclude_list_type(device):
with pytest.raises(CtapError) as e:
device.doMC(exclude_list=[{"id": b"1234"}])
device.MC(exclude_list=[{"id": b"1234"}])
def test_missing_exclude_list_id(device):
with pytest.raises(CtapError) as e:
device.doMC(exclude_list=[{"type": "public-key"}])
device.MC(exclude_list=[{"type": "public-key"}])
def test_bad_type_exclude_list_id(device):
with pytest.raises(CtapError) as e:
device.doMC(exclude_list=[{"type": "public-key", "id": "1234"}])
device.MC(exclude_list=[{"type": "public-key", "id": "1234"}])
def test_bad_type_exclude_list_type(device):
with pytest.raises(CtapError) as e:

View File

@@ -31,10 +31,10 @@ def test_authenticate(device):
AUTRes = device.authenticate(credentials)
def test_assertion_auth_data(GARes):
assert len(GARes['res'].get_response(0).authenticator_data) == 37
assert len(GARes['res'].get_response(0).response.authenticator_data) == 37
def test_Check_that_AT_flag_is_not_set(GARes):
assert (GARes['res'].get_response(0).authenticator_data.flags & 0xF8) == 0
assert (GARes['res'].get_response(0).response.authenticator_data.flags & 0xF8) == 0
def test_that_user_credential_and_numberOfCredentials_are_not_present(device, MCRes):
res = device.GA(allow_list=[
@@ -63,8 +63,8 @@ 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 = {"id": "example.com", "name": "rp1.com"}
rp2 = {"id": "example.com", "name": "rp2.com"}
rp1_registrations = []
rp2_registrations = []
@@ -127,7 +127,7 @@ def test_mismatched_rp(device, GARes):
rp_id += ".com"
with pytest.raises(CtapError) as e:
device.doGA(rp_id=rp_id)
device.GA(rp_id=rp_id)
assert e.value.code == CtapError.ERR.NO_CREDENTIALS
def test_missing_rp(device):
@@ -137,7 +137,7 @@ def test_missing_rp(device):
def test_bad_rp(device):
with pytest.raises(CtapError) as e:
device.doGA(rp_id={"id": {"type": "wrong"}})
device.GA(rp_id={"id": {"type": "wrong"}})
def test_missing_cdh(device):
with pytest.raises(CtapError) as e:
@@ -150,11 +150,11 @@ def test_bad_cdh(device):
def test_bad_allow_list(device):
with pytest.raises(CtapError) as e:
device.doGA(allow_list={"type": "wrong"})
device.GA(allow_list={"type": "wrong"})
def test_bad_allow_list_item(device, MCRes):
with pytest.raises(CtapError) as e:
device.doGA(allow_list=["wrong"] + [
device.GA(allow_list=["wrong"] + [
{"id": MCRes['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"}
]
)
@@ -177,7 +177,7 @@ def test_option_up(device, info, GARes):
assert res.auth_data.flags & (1 << 0)
def test_allow_list_fake_item(device, MCRes):
device.doGA(allow_list=[{"type": "rot13", "id": b"1234"}]
device.GA(allow_list=[{"type": "rot13", "id": b"1234"}]
+ [
{"id": MCRes['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"}
],
@@ -185,7 +185,7 @@ def test_allow_list_fake_item(device, MCRes):
def test_allow_list_missing_field(device, MCRes):
with pytest.raises(CtapError) as e:
device.doGA(allow_list=[{"id": b"1234"}] + [
device.GA(allow_list=[{"id": b"1234"}] + [
{"id": MCRes['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"}
]
)
@@ -200,7 +200,7 @@ def test_allow_list_field_wrong_type(device, MCRes):
def test_allow_list_id_wrong_type(device, MCRes):
with pytest.raises(CtapError) as e:
device.doGA(allow_list=[{"type": "public-key", "id": 42}]
device.GA(allow_list=[{"type": "public-key", "id": 42}]
+ [
{"id": MCRes['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"}
]
@@ -208,7 +208,7 @@ def test_allow_list_id_wrong_type(device, MCRes):
def test_allow_list_missing_id(device, MCRes):
with pytest.raises(CtapError) as e:
device.doGA(allow_list=[{"type": "public-key"}] + [
device.GA(allow_list=[{"type": "public-key"}] + [
{"id": MCRes['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"}
]
)

View File

@@ -85,7 +85,7 @@ def test_multiple_rk_nodisplay(device, MCRes_DC):
auths = []
regs = []
# Use unique RP to not collide with other credentials
rp = {"id": f"unique-{random.random()}.com", "name": "Example"}
rp = {"id": "example.com", "name": "Example"}
for i in range(0, 3):
res = device.doMC(rp=rp, rk=True, user=generate_random_user())
regs.append(res)
@@ -116,7 +116,7 @@ def test_rk_maximum_size_nodisplay(device):
auths = resGA.get_assertions()
user_max_GA = auths[0]
print(auths)
for y in ("name", "displayName", "id"):
if (y in user_max_GA):
assert user_max_GA.user[y] == user_max[y]
@@ -126,7 +126,7 @@ def test_rk_maximum_list_capacity_per_rp_nodisplay(info, device, MCRes_DC):
"""
Test maximum returned capacity of the RK for the given RP
"""
device.reset()
# Try to determine from get_info, or default to 19.
RK_CAPACITY_PER_RP = info.max_creds_in_list
if not RK_CAPACITY_PER_RP:
@@ -140,7 +140,7 @@ def test_rk_maximum_list_capacity_per_rp_nodisplay(info, device, MCRes_DC):
return user
# Use unique RP to not collide with other credentials from other tests.
rp = {"id": f"unique-{random.random()}.com", "name": "Example"}
rp = {"id": "example.com", "name": "Example"}
# req = FidoRequest(MCRes_DC, options=None, user=get_user(), rp = rp)
# res = device.sendGA(*req.toGA())
@@ -183,10 +183,10 @@ def test_rk_with_allowlist_of_different_rp(resetdevice):
"""
rk_rp = {"id": "rk-cred.org", "name": "Example"}
rk_res = resetdevice.doMC(rp = rk_rp, rk=True)['res'].attestation_object
rk_res = resetdevice.MC(rp = rk_rp, options={"rk":True})['res']
server_rp = {"id": "server-cred.com", "name": "Example"}
server_res = resetdevice.doMC(rp = server_rp, rk=True)['res'].attestation_object
server_res = resetdevice.MC(rp = server_rp, options={"rk":True})['res']
allow_list_with_different_rp_cred = [
{
@@ -197,7 +197,7 @@ def test_rk_with_allowlist_of_different_rp(resetdevice):
with pytest.raises(CtapError) as e:
res = resetdevice.doGA(rp_id = rk_rp['id'], allow_list = allow_list_with_different_rp_cred)
res = resetdevice.GA(rp_id = rk_rp['id'], allow_list = allow_list_with_different_rp_cred)
assert e.value.code == CtapError.ERR.NO_CREDENTIALS
@@ -208,10 +208,10 @@ def test_same_userId_overwrites_rk(resetdevice):
rp = {"id": "overwrite.org", "name": "Example"}
user = generate_random_user()
mc_res1 = resetdevice.doMC(rp = rp, rk=True, user = user)
mc_res1 = resetdevice.MC(rp = rp, options={"rk":True}, user = user)
# Should overwrite the first credential.
mc_res2 = resetdevice.doMC(rp = rp, rk=True, user = user)
mc_res2 = resetdevice.MC(rp = rp, options={"rk":True}, user = user)
ga_res = resetdevice.GA(rp_id=rp['id'])['res']
@@ -227,7 +227,7 @@ def test_larger_icon_than_128(device):
user = generate_random_user()
user['icon'] = 'https://www.w3.org/TR/webauthn/?icon=' + ("A" * 128)
device.doMC(rp = rp, rk=True, user = user)
device.MC(rp = rp, options={"rk":True}, user = user)
def test_returned_credential(device):

View File

@@ -21,6 +21,7 @@
import pytest
from fido2.ctap import CtapError
from fido2.ctap2.pin import PinProtocolV2, ClientPin
from fido2.utils import websafe_decode
from utils import verify
import os
@@ -46,22 +47,24 @@ def GACredBlob(device, MCCredBlob):
@pytest.fixture(scope="function")
def MCLBK(device):
res = device.doMC(
mc = device.doMC(
rk=True,
extensions={'largeBlob':{'support':'required'}}
)['res']
return res
)
res = mc['res']
ext = mc['client_extension_results']
return {'res': res, 'ext': ext}
@pytest.fixture(scope="function")
def GALBRead(device, MCLBK):
res = device.doGA(
allow_list=[
{"id": MCLBK.attestation_object.auth_data.credential_data.credential_id, "type": "public-key"}
{"id": MCLBK['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"}
],extensions={'largeBlob':{'read': True}}
)
assertions = res['res'].get_assertions()
for a in assertions:
verify(MCLBK.attestation_object, a, res['req']['client_data'].hash)
verify(MCLBK['res'].attestation_object, a, res['req']['client_data'].hash)
return res['res']
@pytest.fixture(scope="function")
@@ -70,18 +73,19 @@ def GALBReadLBK(GALBRead):
@pytest.fixture(scope="function")
def GALBReadLB(GALBRead):
print(GALBRead.get_response(0))
return GALBRead.get_response(0)
@pytest.fixture(scope="function")
def GALBWrite(device, MCLBK):
res = device.doGA(
allow_list=[
{"id": MCLBK.attestation_object.auth_data.credential_data.credential_id, "type": "public-key"}
{"id": MCLBK['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"}
],extensions={'largeBlob':{'write': LARGE_BLOB}}
)
assertions = res['res'].get_assertions()
for a in assertions:
verify(MCLBK.attestation_object, a, res['req']['client_data'].hash)
verify(MCLBK['res'].attestation_object, a, res['req']['client_data'].hash)
return res['res'].get_response(0)
def test_supports_credblob(info):
@@ -136,15 +140,17 @@ def test_supports_largeblobs(info):
assert info.max_large_blob is None or (info.max_large_blob > 1024)
def test_get_largeblobkey_mc(MCLBK):
assert 'supported' in MCLBK.extension_results
assert MCLBK.extension_results['supported'] is True
assert 'largeBlob' in MCLBK['ext']
assert 'supported' in MCLBK['ext']['largeBlob']
assert MCLBK['ext']['largeBlob']['supported'] is True
def test_get_largeblobkey_ga(GALBReadLBK):
assert GALBReadLBK.large_blob_key is not None
def test_get_largeblob_rw(GALBWrite, GALBReadLB):
assert 'written' in GALBWrite.extension_results
assert GALBWrite.extension_results['written'] is True
assert 'largeBlob' in GALBWrite.client_extension_results
assert 'written' in GALBWrite.client_extension_results['largeBlob']
assert GALBWrite.client_extension_results['largeBlob']['written'] is True
assert 'blob' in GALBReadLB.extension_results
assert GALBReadLB.extension_results['blob'] == LARGE_BLOB
assert 'blob' in GALBReadLB.client_extension_results['largeBlob']
assert websafe_decode(GALBReadLB.client_extension_results['largeBlob']['blob']) == LARGE_BLOB

View File

@@ -83,7 +83,7 @@ def test_credprotect_optional_list_excluded(device, MCCredProtectOptionalList):
]
with pytest.raises(CtapError) as e:
device.doMC(rk=True, extensions={'credentialProtectionPolicy': CredProtectExtension.POLICY.OPTIONAL_WITH_LIST}, exclude_list=exclude_list)
device.MC(options={'rk': True}, extensions={'credProtect': CredProtect.UserVerificationOptionalWithCredentialId}, exclude_list=exclude_list)
assert e.value.code == CtapError.ERR.CREDENTIAL_EXCLUDED
@@ -123,10 +123,10 @@ def test_credprotect_optional_and_list_works_no_uv(device, MCCredProtectOptional
},
]
# works
res1 = device.doGA(allow_list=allow_list)['res'].get_assertions()[0]
res1 = device.doGA(allow_list=allow_list, user_verification=False)['res'].get_assertions()[0]
assert res1.number_of_credentials in (None, 2)
results = device.doGA(allow_list=allow_list)['res'].get_assertions()
results = device.doGA(allow_list=allow_list, user_verification=False)['res'].get_assertions()
# the required credProtect is not returned.
for res in results:

View File

@@ -24,6 +24,7 @@ from fido2.ctap2.extensions import HmacSecretExtension
from fido2.utils import hmac_sha256
from fido2.ctap2.pin import PinProtocolV2
from fido2.webauthn import UserVerificationRequirement
from fido2.client import ClientError
from utils import *
salt1 = b"\xa5" * 32
@@ -38,10 +39,6 @@ def MCHmacSecret(resetdevice):
res = resetdevice.doMC(extensions={"hmacCreateSecret": True},rk=True)
return res['res'].attestation_object
@pytest.fixture(scope="class")
def hmac(resetdevice):
return HmacSecretExtension(resetdevice.client()._backend.ctap2, pin_protocol=PinProtocolV2())
def test_hmac_secret_make_credential(MCHmacSecret):
assert MCHmacSecret.auth_data.extensions
assert "hmac-secret" in MCHmacSecret.auth_data.extensions
@@ -55,51 +52,51 @@ def test_fake_extension(device):
@pytest.mark.parametrize("salts", [(salt1,), (salt1, salt2)])
def test_hmac_secret_entropy(device, MCHmacSecret, hmac, salts
def test_hmac_secret_entropy(device, MCHmacSecret, salts
):
hout = {'salt1':salts[0]}
if (len(salts) > 1):
hout['salt2'] = salts[1]
auth = device.doGA(extensions={"hmacGetSecret": hout})['res'].get_response(0)
ext = auth.extension_results
ext = auth.client_extension_results
assert ext
assert "hmacGetSecret" in ext
assert len(auth.authenticator_data.extensions['hmac-secret']) == len(salts) * 32 + 16
assert len(auth.response.authenticator_data.extensions['hmac-secret']) == len(salts) * 32 + 16
#print(shannon_entropy(auth.authenticator_data.extensions['hmac-secret']))
#print(shannon_entropy(auth.response.authenticator_data.extensions['hmac-secret']))
if len(salts) == 1:
assert shannon_entropy(auth.authenticator_data.extensions['hmac-secret']) > 4.5
assert shannon_entropy(ext["hmacGetSecret"]['output1']) > 4.5
assert shannon_entropy(auth.response.authenticator_data.extensions['hmac-secret']) > 4.5
assert shannon_entropy(ext.hmac_get_secret.output1) > 4.5
if len(salts) == 2:
assert shannon_entropy(auth.authenticator_data.extensions['hmac-secret']) > 5.4
assert shannon_entropy(ext["hmacGetSecret"]['output1']) > 4.5
assert shannon_entropy(ext["hmacGetSecret"]['output2']) > 4.5
assert shannon_entropy(auth.response.authenticator_data.extensions['hmac-secret']) > 5.4
assert shannon_entropy(ext.hmac_get_secret.output1) > 4.5
assert shannon_entropy(ext.hmac_get_secret.output2) > 4.5
def get_output(device, MCHmacSecret, hmac, salts):
def get_output(device, MCHmacSecret, salts):
hout = {'salt1':salts[0]}
if (len(salts) > 1):
hout['salt2'] = salts[1]
auth = device.doGA(extensions={"hmacGetSecret": hout})['res'].get_response(0)
ext = auth.extension_results
ext = auth.client_extension_results
assert ext
assert "hmacGetSecret" in ext
assert len(auth.authenticator_data.extensions['hmac-secret']) == len(salts) * 32 + 16
assert len(auth.response.authenticator_data.extensions['hmac-secret']) == len(salts) * 32 + 16
if len(salts) == 2:
return ext["hmacGetSecret"]['output1'], ext["hmacGetSecret"]['output2']
return ext.hmac_get_secret.output1, ext.hmac_get_secret.output2
else:
return ext["hmacGetSecret"]['output1']
return ext.hmac_get_secret.output1
def test_hmac_secret_sanity(device, MCHmacSecret, hmac):
output1 = get_output(device, MCHmacSecret, hmac, (salt1,))
def test_hmac_secret_sanity(device, MCHmacSecret):
output1 = get_output(device, MCHmacSecret, (salt1,))
output12 = get_output(
device, MCHmacSecret, hmac, (salt1, salt2)
device, MCHmacSecret, (salt1, salt2)
)
output21 = get_output(
device, MCHmacSecret, hmac, (salt2, salt1)
device, MCHmacSecret, (salt2, salt1)
)
assert output12[0] == output1
@@ -107,60 +104,60 @@ def test_hmac_secret_sanity(device, MCHmacSecret, hmac):
assert output21[0] == output12[1]
assert output12[0] != output12[1]
def test_missing_keyAgreement(device, hmac):
hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salt3}})
def test_missing_keyAgreement(device):
with pytest.raises(CtapError):
device.GA(extensions={"hmac-secret": {2: hout[2], 3: hout[3]}})
device.GA(extensions={"hmac-secret": {2: b'1234', 3: b'1234'}})
def test_missing_saltAuth(device, hmac):
hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salt3}})
def test_missing_saltAuth(device):
with pytest.raises(CtapError) as e:
device.GA(extensions={"hmac-secret": {1: hout[1], 2: hout[2]}})
device.GA(extensions={"hmac-secret": {2: b'1234'}})
assert e.value.code == CtapError.ERR.MISSING_PARAMETER
def test_missing_saltEnc(device, hmac):
hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salt3}})
def test_missing_saltEnc(device,):
with pytest.raises(CtapError) as e:
device.GA(extensions={"hmac-secret": {1: hout[1], 3: hout[3]}})
device.GA(extensions={"hmac-secret": { 3: b'1234'}})
assert e.value.code == CtapError.ERR.MISSING_PARAMETER
def test_bad_auth(device, hmac, MCHmacSecret):
def test_bad_auth(device, MCHmacSecret):
hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salt3}})
bad_auth = list(hout[3][:])
bad_auth[len(bad_auth) // 2] = bad_auth[len(bad_auth) // 2] ^ 1
bad_auth = bytes(bad_auth)
key_agreement = {
1: 2,
3: -25, # Per the spec, "although this is NOT the algorithm actually used"
-1: 1,
-2: b'\x00'*32,
-3: b'\x00'*32,
}
with pytest.raises(CtapError) as e:
device.GA(extensions={"hmac-secret": {1: hout[1], 2: hout[2], 3: bad_auth, 4: 2}})
device.GA(extensions={"hmac-secret": {1: key_agreement, 2: b'\x00'*80, 3: b'\x00'*32, 4: 2}})
assert e.value.code == CtapError.ERR.EXTENSION_FIRST
@pytest.mark.parametrize("salts", [(salt4,), (salt4, salt5)])
def test_invalid_salt_length(device, hmac, salts):
with pytest.raises(ValueError) as e:
def test_invalid_salt_length(device, salts):
with pytest.raises((CtapError,ClientError)) as e:
if (len(salts) == 2):
hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salts[0],"salt2":salts[1]}})
hout = {"salt1":salts[0],"salt2":salts[1]}
else:
hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salts[0]}})
hout = {"salt1":salts[0]}
device.doGA(extensions={"hmacGetSecret": hout})
@pytest.mark.parametrize("salts", [(salt1,), (salt1, salt2)])
def test_get_next_assertion_has_extension(
device, hmac, salts
device, salts
):
""" Check that get_next_assertion properly returns extension information for multiple accounts. """
if (len(salts) == 2):
hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salts[0],"salt2":salts[1]}})
hout = {"salt1":salts[0],"salt2":salts[1]}
else:
hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salts[0]}})
hout = {"salt1":salts[0]}
accounts = 3
regs = []
auths = []
rp = {"id": f"example_salts_{len(salts)}.org", "name": "ExampleRP_2"}
rp = {"id": f"example.com", "name": "ExampleRP_2"}
fixed_users = [generate_random_user() for _ in range(accounts)]
for i in range(accounts):
res = device.doMC(extensions={"hmacCreateSecret": True},
@@ -183,21 +180,19 @@ def test_get_next_assertion_has_extension(
assert "hmac-secret" in ext
assert isinstance(ext["hmac-secret"], bytes)
assert len(ext["hmac-secret"]) == len(salts) * 32 + 16
key = hmac.process_get_output(x)
def test_hmac_secret_different_with_uv(device, MCHmacSecret, hmac):
def test_hmac_secret_different_with_uv(device, MCHmacSecret):
salts = [salt1]
if (len(salts) == 2):
hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salts[0],"salt2":salts[1]}})
hout = {"salt1":salts[0],"salt2":salts[1]}
else:
hout = hmac.process_get_input({"hmacGetSecret":{"salt1":salts[0]}})
hout = {"salt1":salts[0]}
auth_no_uv = device.GA(extensions={"hmac-secret": hout})['res']
assert (auth_no_uv.auth_data.flags & (1 << 2)) == 0
auth_no_uv = device.doGA(extensions={"hmacGetSecret": hout})['res'].get_response(0)
assert (auth_no_uv.response.authenticator_data.flags & (1 << 2)) == 0
ext_no_uv = auth_no_uv.auth_data.extensions
ext_no_uv = auth_no_uv.response.authenticator_data.extensions
assert ext_no_uv
assert "hmac-secret" in ext_no_uv
assert isinstance(ext_no_uv["hmac-secret"], bytes)
@@ -209,11 +204,11 @@ def test_hmac_secret_different_with_uv(device, MCHmacSecret, hmac):
hout['salt2'] = salts[1]
auth_uv = device.doGA(extensions={"hmacGetSecret": hout}, user_verification=UserVerificationRequirement.REQUIRED)['res'].get_response(0)
assert auth_uv.authenticator_data.flags & (1 << 2)
ext_uv = auth_uv.extension_results
assert auth_uv.response.authenticator_data.flags & (1 << 2)
ext_uv = auth_uv.client_extension_results
assert ext_uv
assert "hmacGetSecret" in ext_uv
assert len(ext_uv["hmacGetSecret"]) == len(salts)
assert len([p for p in ext_uv["hmacGetSecret"] if len(ext_uv["hmacGetSecret"][p]) > 0]) == len(salts)
# Now see if the hmac-secrets are different
assert ext_no_uv["hmac-secret"][:32] != ext_uv["hmacGetSecret"]['output1']

View File

@@ -29,7 +29,7 @@ def test_authenticate_ctap1_through_ctap2(device, RegRes):
res = device.doGA(ctap1=False, allow_list=[
{"id": RegRes['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"}
])
assert res['res'].get_response(0).credential_id == RegRes['res'].attestation_object.auth_data.credential_data.credential_id
assert res['res'].get_response(0).raw_id == RegRes['res'].attestation_object.auth_data.credential_data.credential_id
# Test FIDO2 register works with U2F auth