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 http import client
from fido2.hid import CtapHidDevice 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.attestation import FidoU2FAttestation
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 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 utils import *
from fido2.cose import ES256 from fido2.cose import ES256
import sys import sys
@@ -75,6 +76,8 @@ class Device():
self.__set_client(origin=origin, user_interaction=user_interaction, uv=uv) self.__set_client(origin=origin, user_interaction=user_interaction, uv=uv)
self.__set_server(rp=rp, attestation=attestation) self.__set_server(rp=rp, attestation=attestation)
def __verify_rp(rp_id, origin):
return True
def __set_client(self, origin, user_interaction, uv): def __set_client(self, origin, user_interaction, uv):
self.__uv = uv self.__uv = uv
@@ -101,14 +104,23 @@ class Device():
sys.exit(1) sys.exit(1)
# Set up a FIDO 2 client using the origin https://example.com # 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 # Prefer UV if supported and configured
if self.__client.info.options.get("uv") or self.__client.info.options.get("pinUvAuthToken"): if self.__client.info.options.get("uv") or self.__client.info.options.get("pinUvAuthToken"):
self.__uv = "preferred" self.__uv = "preferred"
print("Authenticator supports User Verification") 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.__client1._backend = _Ctap1ClientBackend(self.__dev, user_interaction=self.__user_interaction)
self.ctap1 = self.__client1._backend.ctap1 self.ctap1 = self.__client1._backend.ctap1
@@ -117,7 +129,7 @@ class Device():
self.__attestation = attestation self.__attestation = attestation
self.__server = Fido2Server(self.__rp, attestation=self.__attestation) self.__server = Fido2Server(self.__rp, attestation=self.__attestation)
self.__server.allowed_algorithms = [ 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 for p in self.__client._backend.info.algorithms
] ]
@@ -216,9 +228,7 @@ class Device():
'key_params':key_params}} '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): 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( client_data = client_data if client_data is not Ellipsis else DefaultClientDataCollector(origin=self.__origin, verify=Device.__verify_rp)
type=CollectedClientData.TYPE.CREATE, origin=self.__origin, challenge=os.urandom(32)
)
rp = rp if rp is not Ellipsis else self.__rp rp = rp if rp is not Ellipsis else self.__rp
user = user if user is not Ellipsis else self.user() 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 key_params = key_params if key_params is not Ellipsis else self.__server.allowed_algorithms
@@ -226,22 +236,31 @@ class Device():
client = self.__client1 client = self.__client1
else: else:
client = self.__client client = self.__client
result = client._backend.do_make_credential( options=PublicKeyCredentialCreationOptions(
client_data=client_data, rp=PublicKeyCredentialRpEntity.from_dict(rp),
rp=rp, user=PublicKeyCredentialUserEntity.from_dict(user),
user=user, pub_key_cred_params=key_params,
key_params=key_params, exclude_credentials=exclude_list,
exclude_list=exclude_list,
extensions=extensions, extensions=extensions,
rk=rk, challenge=os.urandom(32),
user_verification=user_verification, authenticator_selection=AuthenticatorSelectionCriteria(
enterprise_attestation=enterprise_attestation, 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 event=event
) )
return {'res':result,'req':{'client_data':client_data, return {'res':result.response,'req':{'client_data':client_data,
'rp':rp, 'rp':rp,
'user':user, 'user':user,
'key_params':key_params}} 'key_params':key_params},'client_extension_results':result.client_extension_results}
def try_make_credential(self, options=None): def try_make_credential(self, options=None):
if (options is None): if (options is None):
@@ -267,14 +286,14 @@ class Device():
# Complete registration # Complete registration
auth_data = self.__server.register_complete( auth_data = self.__server.register_complete(
state, result.client_data, result.attestation_object state=state, response=result
) )
credentials = [auth_data.credential_data] credentials = [auth_data.credential_data]
print("New credential created!") print("New credential created!")
print("CLIENT DATA:", result.client_data) print("CLIENT DATA:", result.response.client_data)
print("ATTESTATION OBJECT:", result.attestation_object) print("ATTESTATION OBJECT:", result.response.attestation_object)
print() print()
print("CREDENTIAL DATA:", auth_data.credential_data) print("CREDENTIAL DATA:", auth_data.credential_data)
@@ -294,17 +313,14 @@ class Device():
self.__server.authenticate_complete( self.__server.authenticate_complete(
state, state,
credentials, credentials,
result.credential_id, result
result.client_data,
result.authenticator_data,
result.signature,
) )
print("Credential authenticated!") print("Credential authenticated!")
print("CLIENT DATA:", result.client_data) print("CLIENT DATA:", result.response.client_data)
print() 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): 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'] 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() 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): 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( client_data = client_data if client_data is not Ellipsis else DefaultClientDataCollector(origin=self.__origin, verify=Device.__verify_rp)
type=CollectedClientData.TYPE.GET, origin=self.__origin, challenge=os.urandom(32) 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'] 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): if (ctap1 is True):
client = self.__client1 client = self.__client1
else: else:
client = self.__client client = self.__client
try: try:
result = client._backend.do_get_assertion( result = client._backend.do_get_assertion(
options=options,
client_data=client_data, client_data=client_data,
rp_id=rp_id, rp_id=rp_id,
allow_list=allow_list,
extensions=extensions,
user_verification=user_verification,
event=event event=event
) )
except ClientError as e: except ClientError as e:
@@ -347,11 +373,9 @@ class Device():
client_pin = ClientPin(self.__client._backend.ctap2) client_pin = ClientPin(self.__client._backend.ctap2)
client_pin.set_pin(DEFAULT_PIN) client_pin.set_pin(DEFAULT_PIN)
result = client._backend.do_get_assertion( result = client._backend.do_get_assertion(
options=options,
client_data=client_data, client_data=client_data,
rp_id=rp_id, rp_id=rp_id,
allow_list=allow_list,
extensions=extensions,
user_verification=user_verification,
event=event event=event
) )
else: else:
@@ -416,8 +440,8 @@ def AuthRes(device, RegRes, *args):
{"id": RegRes['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"} {"id": RegRes['res'].attestation_object.auth_data.credential_data.credential_id, "type": "public-key"}
], *args) ], *args)
aut_data = res['res'].get_response(0) 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 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.signature) ES256(RegRes['res'].attestation_object.auth_data.credential_data.public_key).verify(m, aut_data.response.signature)
return aut_data return aut_data
@pytest.fixture(scope="class") @pytest.fixture(scope="class")

View File

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

View File

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

View File

@@ -20,8 +20,6 @@
from fido2.client import CtapError from fido2.client import CtapError
from fido2.cose import ES256, ES384, ES512, EdDSA from fido2.cose import ES256, ES384, ES512, EdDSA
import fido2.features
fido2.features.webauthn_json_mapping.enabled = False
from utils import ES256K from utils import ES256K
import pytest import pytest
@@ -51,13 +49,13 @@ def test_bad_type_cdh(device):
def test_missing_user(device): def test_missing_user(device):
with pytest.raises(CtapError) as e: with pytest.raises(CtapError) as e:
device.doMC(user=None) device.MC(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.doMC(user=b"12345678") device.MC(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:
@@ -71,7 +69,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.doMC(key_params=None) device.MC(key_params=None)
assert e.value.code == CtapError.ERR.MISSING_PARAMETER 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): def test_bad_type_rp_name(device):
with pytest.raises(CtapError) as e: 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): def test_bad_type_rp_id(device):
with pytest.raises(CtapError) as e: with pytest.raises(CtapError) as e:
device.doMC(rp={"id": 8, "name": "name", "icon": "icon"}) device.MC(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})
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.doMC(user={"id": b"user_id", "name": 8}) device.MC(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.doMC(user={"id": "user_id", "name": "name"}) device.MC(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.doMC(user={"id": "user_id", "name": "name", "displayName": 8}) device.MC(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"])
@pytest.mark.parametrize( @pytest.mark.parametrize(
"alg", [ES256.ALGORITHM, ES384.ALGORITHM, ES512.ALGORITHM, ES256K.ALGORITHM, EdDSA.ALGORITHM] "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): def test_missing_pubKeyCredParams_type(device):
with pytest.raises(CtapError) as e: 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 assert e.value.code == CtapError.ERR.INVALID_CBOR
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.doMC(key_params=[{"type": "public-key"}]) device.MC(key_params=[{"type": "public-key"}])
assert e.value.code in [ assert e.value.code in [
CtapError.ERR.INVALID_CBOR, CtapError.ERR.INVALID_CBOR,
@@ -147,7 +133,7 @@ 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.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 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 assert e.value.code == CtapError.ERR.UNSUPPORTED_ALGORITHM
def test_exclude_list(resetdevice): 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): 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): def test_bad_type_exclude_list(device):
with pytest.raises(CtapError) as e: with pytest.raises(CtapError) as e:
device.doMC(exclude_list=["1234"]) device.MC(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.doMC(exclude_list=[{"id": b"1234"}]) device.MC(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.doMC(exclude_list=[{"type": "public-key"}]) device.MC(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.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): def test_bad_type_exclude_list_type(device):
with pytest.raises(CtapError) as e: with pytest.raises(CtapError) as e:

View File

@@ -31,10 +31,10 @@ def test_authenticate(device):
AUTRes = device.authenticate(credentials) AUTRes = device.authenticate(credentials)
def test_assertion_auth_data(GARes): 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): 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): def test_that_user_credential_and_numberOfCredentials_are_not_present(device, MCRes):
res = device.GA(allow_list=[ 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 """ """ Check that authenticator filters and stores items in allow list correctly """
allow_list = [] allow_list = []
rp1 = {"id": "rp1.com", "name": "rp1.com"} rp1 = {"id": "example.com", "name": "rp1.com"}
rp2 = {"id": "rp2.com", "name": "rp2.com"} rp2 = {"id": "example.com", "name": "rp2.com"}
rp1_registrations = [] rp1_registrations = []
rp2_registrations = [] rp2_registrations = []
@@ -127,7 +127,7 @@ def test_mismatched_rp(device, GARes):
rp_id += ".com" rp_id += ".com"
with pytest.raises(CtapError) as e: 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 assert e.value.code == CtapError.ERR.NO_CREDENTIALS
def test_missing_rp(device): def test_missing_rp(device):
@@ -137,7 +137,7 @@ def test_missing_rp(device):
def test_bad_rp(device): def test_bad_rp(device):
with pytest.raises(CtapError) as e: 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): def test_missing_cdh(device):
with pytest.raises(CtapError) as e: with pytest.raises(CtapError) as e:
@@ -150,11 +150,11 @@ def test_bad_cdh(device):
def test_bad_allow_list(device): def test_bad_allow_list(device):
with pytest.raises(CtapError) as e: 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): def test_bad_allow_list_item(device, MCRes):
with pytest.raises(CtapError) as e: 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"} {"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) assert res.auth_data.flags & (1 << 0)
def test_allow_list_fake_item(device, MCRes): 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"} {"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): def test_allow_list_missing_field(device, MCRes):
with pytest.raises(CtapError) as e: 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"} {"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): def test_allow_list_id_wrong_type(device, MCRes):
with pytest.raises(CtapError) as e: 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"} {"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): def test_allow_list_missing_id(device, MCRes):
with pytest.raises(CtapError) as e: 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"} {"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 = [] auths = []
regs = [] regs = []
# Use unique RP to not collide with other credentials # 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): for i in range(0, 3):
res = device.doMC(rp=rp, rk=True, user=generate_random_user()) res = device.doMC(rp=rp, rk=True, user=generate_random_user())
regs.append(res) regs.append(res)
@@ -116,7 +116,7 @@ def test_rk_maximum_size_nodisplay(device):
auths = resGA.get_assertions() auths = resGA.get_assertions()
user_max_GA = auths[0] user_max_GA = auths[0]
print(auths)
for y in ("name", "displayName", "id"): for y in ("name", "displayName", "id"):
if (y in user_max_GA): if (y in user_max_GA):
assert user_max_GA.user[y] == user_max[y] 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 Test maximum returned capacity of the RK for the given RP
""" """
device.reset()
# Try to determine from get_info, or default to 19. # Try to determine from get_info, or default to 19.
RK_CAPACITY_PER_RP = info.max_creds_in_list RK_CAPACITY_PER_RP = info.max_creds_in_list
if not RK_CAPACITY_PER_RP: 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 return user
# Use unique RP to not collide with other credentials from other tests. # 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) # req = FidoRequest(MCRes_DC, options=None, user=get_user(), rp = rp)
# res = device.sendGA(*req.toGA()) # 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_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_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 = [ 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: 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 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"} rp = {"id": "overwrite.org", "name": "Example"}
user = generate_random_user() 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. # 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'] 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 = generate_random_user()
user['icon'] = 'https://www.w3.org/TR/webauthn/?icon=' + ("A" * 128) 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): def test_returned_credential(device):

View File

@@ -21,6 +21,7 @@
import pytest import pytest
from fido2.ctap import CtapError from fido2.ctap import CtapError
from fido2.ctap2.pin import PinProtocolV2, ClientPin from fido2.ctap2.pin import PinProtocolV2, ClientPin
from fido2.utils import websafe_decode
from utils import verify from utils import verify
import os import os
@@ -46,22 +47,24 @@ def GACredBlob(device, MCCredBlob):
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def MCLBK(device): def MCLBK(device):
res = device.doMC( mc = device.doMC(
rk=True, rk=True,
extensions={'largeBlob':{'support':'required'}} extensions={'largeBlob':{'support':'required'}}
)['res'] )
return res res = mc['res']
ext = mc['client_extension_results']
return {'res': res, 'ext': ext}
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def GALBRead(device, MCLBK): def GALBRead(device, MCLBK):
res = device.doGA( res = device.doGA(
allow_list=[ 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}} ],extensions={'largeBlob':{'read': True}}
) )
assertions = res['res'].get_assertions() assertions = res['res'].get_assertions()
for a in 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'] return res['res']
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
@@ -70,18 +73,19 @@ def GALBReadLBK(GALBRead):
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def GALBReadLB(GALBRead): def GALBReadLB(GALBRead):
print(GALBRead.get_response(0))
return GALBRead.get_response(0) return GALBRead.get_response(0)
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def GALBWrite(device, MCLBK): def GALBWrite(device, MCLBK):
res = device.doGA( res = device.doGA(
allow_list=[ 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}} ],extensions={'largeBlob':{'write': LARGE_BLOB}}
) )
assertions = res['res'].get_assertions() assertions = res['res'].get_assertions()
for a in 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) return res['res'].get_response(0)
def test_supports_credblob(info): 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) assert info.max_large_blob is None or (info.max_large_blob > 1024)
def test_get_largeblobkey_mc(MCLBK): def test_get_largeblobkey_mc(MCLBK):
assert 'supported' in MCLBK.extension_results assert 'largeBlob' in MCLBK['ext']
assert MCLBK.extension_results['supported'] is True assert 'supported' in MCLBK['ext']['largeBlob']
assert MCLBK['ext']['largeBlob']['supported'] is True
def test_get_largeblobkey_ga(GALBReadLBK): def test_get_largeblobkey_ga(GALBReadLBK):
assert GALBReadLBK.large_blob_key is not None assert GALBReadLBK.large_blob_key is not None
def test_get_largeblob_rw(GALBWrite, GALBReadLB): def test_get_largeblob_rw(GALBWrite, GALBReadLB):
assert 'written' in GALBWrite.extension_results assert 'largeBlob' in GALBWrite.client_extension_results
assert GALBWrite.extension_results['written'] is True assert 'written' in GALBWrite.client_extension_results['largeBlob']
assert GALBWrite.client_extension_results['largeBlob']['written'] is True
assert 'blob' in GALBReadLB.extension_results assert 'blob' in GALBReadLB.client_extension_results['largeBlob']
assert GALBReadLB.extension_results['blob'] == LARGE_BLOB 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: 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 assert e.value.code == CtapError.ERR.CREDENTIAL_EXCLUDED
@@ -123,10 +123,10 @@ def test_credprotect_optional_and_list_works_no_uv(device, MCCredProtectOptional
}, },
] ]
# works # 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) 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. # the required credProtect is not returned.
for res in results: for res in results:

View File

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