Drop legacy U2F code.

- Remove U2fClient implementation.
- Replace fido2.client.ClientData with fido2.webauthn.CollectedClientData.
- Move verify_app_id from fido2.rpid to fido2.client.
This commit is contained in:
Dain Nilsson 2022-04-26 16:31:05 +02:00
parent 77b7fb6990
commit 452a02dd06
No known key found for this signature in database
GPG Key ID: F04367096FBA95E8
8 changed files with 235 additions and 576 deletions

View File

@ -36,6 +36,7 @@ from .ctap2.extensions import Ctap2Extension
from .webauthn import (
Aaguid,
AttestationObject,
CollectedClientData,
PublicKeyCredentialCreationOptions,
PublicKeyCredentialRequestOptions,
AuthenticatorSelectionCriteria,
@ -44,16 +45,15 @@ from .webauthn import (
AuthenticatorAssertionResponse,
)
from .cose import ES256
from .rpid import verify_rp_id, verify_app_id
from .utils import sha256, websafe_decode, websafe_encode
from enum import Enum, IntEnum, unique
from .rpid import verify_rp_id
from .utils import sha256
from enum import IntEnum, unique
from urllib.parse import urlparse
from dataclasses import replace
from threading import Timer, Event
from typing import (
Type,
Any,
Union,
Callable,
Optional,
Mapping,
@ -61,7 +61,6 @@ from typing import (
)
import abc
import json
import platform
import inspect
import logging
@ -69,41 +68,6 @@ import logging
logger = logging.getLogger(__name__)
class ClientData(bytes):
def __init__(self, _):
super().__init__()
self._data = json.loads(self.decode())
def get(self, key: str) -> Any:
return self._data[key]
@property
def challenge(self) -> bytes:
return websafe_decode(self.get("challenge"))
@property
def b64(self) -> str:
return websafe_encode(self)
@property
def hash(self) -> bytes:
return sha256(self)
@classmethod
def build(cls, **kwargs) -> ClientData:
return cls(json.dumps(kwargs).encode())
@classmethod
def from_b64(cls, data: Union[str, bytes]) -> ClientData:
return cls(websafe_decode(data))
def __repr__(self):
return self.decode()
def __str__(self):
return self.decode()
class ClientError(Exception):
@unique
class ERR(IntEnum):
@ -191,141 +155,6 @@ def _call_polling(poll_delay, event, on_keepalive, func, *args, **kwargs):
raise ClientError.ERR.TIMEOUT()
@unique
class U2F_TYPE(str, Enum):
REGISTER = "navigator.id.finishEnrollment"
SIGN = "navigator.id.getAssertion"
class U2fClient:
"""U2F-like client implementation.
The client allows registration and authentication of U2F credentials against
an Authenticator using CTAP 1. Prefer using Fido2Client if possible.
:param device: CtapDevice to use.
:param str origin: The origin to use.
:param verify: Function to verify an APP ID for a given origin.
"""
def __init__(
self,
device: CtapDevice,
origin: str,
verify: Callable[[str, str], bool] = verify_app_id,
):
self.poll_delay = 0.25
self.ctap = Ctap1(device)
self.origin = origin
self._verify = verify
def _verify_app_id(self, app_id):
try:
if self._verify(app_id, self.origin):
return
except Exception: # nosec
pass # Fall through to ClientError
raise ClientError.ERR.BAD_REQUEST()
def register(
self,
app_id: str,
register_requests: Sequence[Mapping[str, str]],
registered_keys: Sequence[Mapping[str, Any]],
event: Optional[Event] = None,
on_keepalive: Optional[Callable[[int], None]] = None,
) -> Mapping[str, str]:
self._verify_app_id(app_id)
version = self.ctap.get_version()
dummy_param = b"\0" * 32
for key in registered_keys:
if key["version"] != version:
continue
key_app_id = key.get("appId", app_id)
app_param = sha256(key_app_id.encode())
self._verify_app_id(key_app_id)
key_handle = websafe_decode(key["keyHandle"])
try:
self.ctap.authenticate(dummy_param, app_param, key_handle, True)
raise ClientError.ERR.DEVICE_INELIGIBLE() # Bad response
except ApduError as e:
if e.code == APDU.USE_NOT_SATISFIED:
raise ClientError.ERR.DEVICE_INELIGIBLE()
except CtapError as e:
raise _ctap2client_err(e)
for request in register_requests:
if request["version"] == version:
challenge = request["challenge"]
break
else:
raise ClientError.ERR.DEVICE_INELIGIBLE()
client_data = ClientData.build(
typ=U2F_TYPE.REGISTER, challenge=challenge, origin=self.origin
)
app_param = sha256(app_id.encode())
reg_data = _call_polling(
self.poll_delay,
event,
on_keepalive,
self.ctap.register,
client_data.hash,
app_param,
)
return {"registrationData": reg_data.b64, "clientData": client_data.b64}
def sign(
self,
app_id: str,
challenge: bytes,
registered_keys: Sequence[Mapping[str, Any]],
event: Optional[Event] = None,
on_keepalive: Optional[Callable[[int], None]] = None,
) -> Mapping[str, str]:
client_data = ClientData.build(
typ=U2F_TYPE.SIGN, challenge=challenge, origin=self.origin
)
version = self.ctap.get_version()
for key in registered_keys:
if key["version"] == version:
key_app_id = key.get("appId", app_id)
self._verify_app_id(key_app_id)
key_handle = websafe_decode(key["keyHandle"])
app_param = sha256(key_app_id.encode())
try:
signature_data = _call_polling(
self.poll_delay,
event,
on_keepalive,
self.ctap.authenticate,
client_data.hash,
app_param,
key_handle,
)
break
except ClientError: # nosec
pass # Ignore and try next key
else:
raise ClientError.ERR.DEVICE_INELIGIBLE()
return {
"clientData": client_data.b64,
"signatureData": signature_data.b64,
"keyHandle": key["keyHandle"],
}
@unique
class WEBAUTHN_TYPE(str, Enum):
MAKE_CREDENTIAL = "webauthn.create"
GET_ASSERTION = "webauthn.get"
class _BaseClient:
def __init__(self, origin: str, verify: Callable[[str, str], bool]):
self.origin = origin
@ -339,12 +168,12 @@ class _BaseClient:
pass # Fall through to ClientError
raise ClientError.ERR.BAD_REQUEST()
def _build_client_data(self, typ, challenge, extensions={}):
return ClientData.build(
def _build_client_data(self, typ, challenge):
print(typ, self.origin, challenge)
return CollectedClientData.create(
type=typ,
origin=self.origin,
challenge=websafe_encode(challenge),
clientExtensions=extensions,
challenge=challenge,
)
@ -356,7 +185,7 @@ class AssertionSelection:
"""
def __init__(
self, client_data: ClientData, assertions: Sequence[AssertionResponse]
self, client_data: CollectedClientData, assertions: Sequence[AssertionResponse]
):
self._client_data = client_data
self._assertions = assertions
@ -580,7 +409,7 @@ class _Ctap1ClientBackend(_ClientBackend):
class _Ctap2ClientAssertionSelection(AssertionSelection):
def __init__(
self,
client_data: ClientData,
client_data: CollectedClientData,
assertions: Sequence[AssertionResponse],
extensions: Sequence[Ctap2Extension],
pin_token: Optional[str],
@ -690,7 +519,7 @@ class _Ctap2ClientBackend(_ClientBackend):
def _get_auth_params(
self, client_data, rp_id, user_verification, permissions, event, on_keepalive
):
mc = client_data.get("type") == WEBAUTHN_TYPE.MAKE_CREDENTIAL
mc = client_data.type == CollectedClientData.TYPE.CREATE
self.info = self.ctap2.get_info() # Make sure we have "fresh" info
pin_protocol = None
@ -937,7 +766,7 @@ class Fido2Client(WebAuthnClient, _BaseClient):
self._verify_rp_id(rp.id)
client_data = self._build_client_data(
WEBAUTHN_TYPE.MAKE_CREDENTIAL, options.challenge
CollectedClientData.TYPE.CREATE, options.challenge
)
selection = options.authenticator_selection or AuthenticatorSelectionCriteria()
@ -982,7 +811,7 @@ class Fido2Client(WebAuthnClient, _BaseClient):
self._verify_rp_id(options.rp_id)
client_data = self._build_client_data(
WEBAUTHN_TYPE.GET_ASSERTION, options.challenge
CollectedClientData.TYPE.GET, options.challenge
)
try:
@ -1057,7 +886,7 @@ class WindowsClient(WebAuthnClient, _BaseClient):
self._verify_rp_id(options.rp.id)
client_data = self._build_client_data(
WEBAUTHN_TYPE.MAKE_CREDENTIAL, options.challenge
CollectedClientData.TYPE.CREATE, options.challenge
)
selection = options.authenticator_selection or AuthenticatorSelectionCriteria()
@ -1104,7 +933,7 @@ class WindowsClient(WebAuthnClient, _BaseClient):
self._verify_rp_id(options.rp_id)
client_data = self._build_client_data(
WEBAUTHN_TYPE.GET_ASSERTION, options.challenge
CollectedClientData.TYPE.GET, options.challenge
)
try:

View File

@ -69,19 +69,3 @@ def verify_rp_id(rp_id: str, origin: str) -> bool:
if host and host.endswith("." + rp_id) and rp_id not in suffixes:
return True
return False
def verify_app_id(app_id: str, origin: str) -> bool:
"""Checks if a FIDO U2F App ID is usable for a given origin.
:param app_id: The App ID to validate.
:param origin: The origin of the request.
:return: True if the App ID is usable by the origin, False if not.
"""
url = urlparse(app_id)
if url.scheme != "https":
return False
hostname = url.hostname
if not hostname:
return False
return verify_rp_id(hostname, origin)

View File

@ -27,11 +27,11 @@
from __future__ import annotations
from .rpid import verify_rp_id, verify_app_id
from .rpid import verify_rp_id
from .cose import CoseKey
from .client import WEBAUTHN_TYPE, ClientData
from .utils import websafe_encode, websafe_decode
from .webauthn import (
CollectedClientData,
AuthenticatorData,
AttestationObject,
AttestedCredentialData,
@ -52,6 +52,7 @@ from .webauthn import (
from cryptography.hazmat.primitives import constant_time
from cryptography.exceptions import InvalidSignature as _InvalidSignature
from dataclasses import replace
from urllib.parse import urlparse
from typing import Sequence, Mapping, Optional, Callable, Union, Tuple, Any
import os
@ -214,7 +215,10 @@ class Fido2Server:
)
def register_complete(
self, state, client_data: ClientData, attestation_object: AttestationObject
self,
state,
client_data: CollectedClientData,
attestation_object: AttestationObject,
) -> AuthenticatorData:
"""Verify the correctness of the registration data received from
the client.
@ -225,10 +229,10 @@ class Fido2Server:
:param attestation_object: The attestation object.
:return: The authenticator data
"""
if client_data.get("type") != WEBAUTHN_TYPE.MAKE_CREDENTIAL:
raise ValueError("Incorrect type in ClientData.")
if not self._verify(client_data.get("origin")):
raise ValueError("Invalid origin in ClientData.")
if client_data.type != CollectedClientData.TYPE.CREATE:
raise ValueError("Incorrect type in CollectedClientData.")
if not self._verify(client_data.origin):
raise ValueError("Invalid origin in CollectedClientData.")
if not constant_time.bytes_eq(
websafe_decode(state["challenge"]), client_data.challenge
):
@ -311,7 +315,7 @@ class Fido2Server:
state,
credentials: Sequence[AttestedCredentialData],
credential_id: bytes,
client_data: ClientData,
client_data: CollectedClientData,
auth_data: AuthenticatorData,
signature: bytes,
) -> AttestedCredentialData:
@ -325,10 +329,10 @@ class Fido2Server:
:param client_data: The client data.
:param auth_data: The authenticator data.
:param signature: The signature provided by the client."""
if client_data.get("type") != WEBAUTHN_TYPE.GET_ASSERTION:
raise ValueError("Incorrect type in ClientData.")
if not self._verify(client_data.get("origin")):
raise ValueError("Invalid origin in ClientData.")
if client_data.type != CollectedClientData.TYPE.GET:
raise ValueError("Incorrect type in CollectedClientData.")
if not self._verify(client_data.origin):
raise ValueError("Invalid origin in CollectedClientData.")
if websafe_decode(state["challenge"]) != client_data.challenge:
raise ValueError("Wrong challenge in response.")
if not constant_time.bytes_eq(self.rp.id_hash, auth_data.rp_id_hash):
@ -364,6 +368,22 @@ class Fido2Server:
}
def verify_app_id(app_id: str, origin: str) -> bool:
"""Checks if a FIDO U2F App ID is usable for a given origin.
:param app_id: The App ID to validate.
:param origin: The origin of the request.
:return: True if the App ID is usable by the origin, False if not.
"""
url = urlparse(app_id)
if url.scheme != "https":
return False
hostname = url.hostname
if not hostname:
return False
return verify_rp_id(hostname, origin)
class U2FFido2Server(Fido2Server):
"""Fido2Server which can be used with existing U2F credentials.
@ -373,7 +393,7 @@ class U2FFido2Server(Fido2Server):
:param app_id: The appId which was used for U2F registration.
:param verify_u2f_origin: (optional) Alternative function to validate an
origin for U2F credentials..
origin for U2F credentials.
For other parameters, see Fido2Server.
"""

View File

@ -29,11 +29,18 @@ from __future__ import annotations
from . import cbor
from .cose import CoseKey, ES256
from .utils import sha256, ByteBuffer, _CamelCaseDataObject
from .utils import (
sha256,
websafe_decode,
websafe_encode,
ByteBuffer,
_CamelCaseDataObject,
)
from enum import Enum, EnumMeta, unique, IntFlag
from dataclasses import dataclass, field
from typing import Any, Mapping, Optional, Sequence, Tuple, cast
from typing import Any, Mapping, Optional, Sequence, Tuple, Union, cast
import struct
import json
"""
Data classes based on the W3C WebAuthn specification (https://www.w3.org/TR/webauthn/).
@ -324,6 +331,65 @@ class AttestationObject(bytes): # , Mapping[str, Any]):
)
@dataclass(init=False, frozen=True)
class CollectedClientData(bytes):
@unique
class TYPE(str, Enum):
CREATE = "webauthn.create"
GET = "webauthn.get"
type: str
challenge: bytes
origin: str
cross_origin: bool = False
def __init__(self, *args):
super().__init__()
data = json.loads(self.decode())
object.__setattr__(self, "type", data["type"])
object.__setattr__(self, "challenge", websafe_decode(data["challenge"]))
object.__setattr__(self, "origin", data["origin"])
object.__setattr__(self, "cross_origin", data.get("crossOrigin", False))
@classmethod
def create(
cls,
type: str,
challenge: Union[bytes, str],
origin: str,
cross_origin: bool = False,
**kwargs,
) -> CollectedClientData:
if isinstance(challenge, bytes):
encoded_challenge = websafe_encode(challenge)
else:
encoded_challenge = challenge
return cls(
json.dumps(
{
"type": type,
"challenge": encoded_challenge,
"origin": origin,
"cross_origin": cross_origin,
**kwargs,
},
separators=(",", ":"),
).encode()
)
def __str__(self): # Override default implementation from bytes.
return repr(self)
@property
def b64(self) -> str:
return websafe_encode(self)
@property
def hash(self) -> bytes:
return sha256(self)
class _StringEnumMeta(EnumMeta):
def _get_value(cls, value):
return None

View File

@ -28,50 +28,19 @@
# POSSIBILITY OF SUCH DAMAGE.
import unittest
import json
from unittest import mock
from threading import Event, Timer
from fido2 import cbor
from fido2.utils import sha256, websafe_decode
from fido2.utils import sha256
from fido2.hid import CAPABILITY
from fido2.ctap import CtapError
from fido2.ctap1 import ApduError, APDU, RegistrationData, SignatureData
from fido2.ctap1 import RegistrationData
from fido2.ctap2 import Info, AttestationResponse
from fido2.client import ClientData, U2fClient, ClientError, Fido2Client
from fido2.webauthn import PublicKeyCredentialCreationOptions, AttestationObject
class TestClientData(unittest.TestCase):
def test_client_data(self):
client_data = ClientData(
b'{"typ":"navigator.id.finishEnrollment","challenge":"vqrS6WXDe1JUs5_c3i4-LkKIHRr-3XVb3azuA5TifHo","cid_pubkey":{"kty":"EC","crv":"P-256","x":"HzQwlfXX7Q4S5MtCCnZUNBw3RMzPO9tOyWjBqRl4tJ8","y":"XVguGFLIZx1fXg3wNqfdbn75hi4-_7-BxhMljw42Ht4"},"origin":"http://example.com"}' # noqa E501
)
self.assertEqual(
client_data.hash,
bytes.fromhex(
"4142d21c00d94ffb9d504ada8f99b721f4b191ae4e37ca0140f696b6983cfacb"
),
)
self.assertEqual(client_data.get("origin"), "http://example.com")
self.assertEqual(client_data, ClientData.from_b64(client_data.b64))
self.assertEqual(
json.loads(client_data),
{
"typ": "navigator.id.finishEnrollment",
"challenge": "vqrS6WXDe1JUs5_c3i4-LkKIHRr-3XVb3azuA5TifHo",
"cid_pubkey": {
"kty": "EC",
"crv": "P-256",
"x": "HzQwlfXX7Q4S5MtCCnZUNBw3RMzPO9tOyWjBqRl4tJ8",
"y": "XVguGFLIZx1fXg3wNqfdbn75hi4-_7-BxhMljw42Ht4",
},
"origin": "http://example.com",
},
)
from fido2.client import ClientError, Fido2Client
from fido2.webauthn import (
PublicKeyCredentialCreationOptions,
AttestationObject,
CollectedClientData,
)
APP_ID = "https://foo.example.com"
REG_DATA = RegistrationData(
@ -79,240 +48,6 @@ REG_DATA = RegistrationData(
"0504b174bc49c7ca254b70d2e5c207cee9cf174820ebd77ea3c65508c26da51b657c1cc6b952f8621697936482da0a6d3d3826a59095daf6cd7c03e2e60385d2f6d9402a552dfdb7477ed65fd84133f86196010b2215b57da75d315b7b9e8fe2e3925a6019551bab61d16591659cbaf00b4950f7abfe6660e2e006f76868b772d70c253082013c3081e4a003020102020a47901280001155957352300a06082a8648ce3d0403023017311530130603550403130c476e756262792050696c6f74301e170d3132303831343138323933325a170d3133303831343138323933325a3031312f302d0603550403132650696c6f74476e756262792d302e342e312d34373930313238303030313135353935373335323059301306072a8648ce3d020106082a8648ce3d030107034200048d617e65c9508e64bcc5673ac82a6799da3c1446682c258c463fffdf58dfd2fa3e6c378b53d795c4a4dffb4199edd7862f23abaf0203b4b8911ba0569994e101300a06082a8648ce3d0403020347003044022060cdb6061e9c22262d1aac1d96d8c70829b2366531dda268832cb836bcd30dfa0220631b1459f09e6330055722c8d89b7f48883b9089b88d60d1d9795902b30410df304502201471899bcc3987e62e8202c9b39c33c19033f7340352dba80fcab017db9230e402210082677d673d891933ade6f617e5dbde2e247e70423fd5ad7804a6d3d3961ef871" # noqa E501
)
)
SIG_DATA = SignatureData(
bytes.fromhex(
"0100000001304402204b5f0cd17534cedd8c34ee09570ef542a353df4436030ce43d406de870b847780220267bb998fac9b7266eb60e7cb0b5eabdfd5ba9614f53c7b22272ec10047a923f" # noqa E501
)
)
class TestU2fClient(unittest.TestCase):
def test_register_wrong_app_id(self):
client = U2fClient(None, APP_ID)
try:
client.register(
"https://bar.example.com",
[{"version": "U2F_V2", "challenge": "foobar"}],
[],
)
self.fail("register did not raise error")
except ClientError as e:
self.assertEqual(e.code, ClientError.ERR.BAD_REQUEST)
def test_register_unsupported_version(self):
client = U2fClient(None, APP_ID)
client.ctap = mock.MagicMock()
client.ctap.get_version.return_value = "U2F_XXX"
try:
client.register(APP_ID, [{"version": "U2F_V2", "challenge": "foobar"}], [])
self.fail("register did not raise error")
except ClientError as e:
self.assertEqual(e.code, ClientError.ERR.DEVICE_INELIGIBLE)
client.ctap.get_version.assert_called_with()
def test_register_existing_key(self):
client = U2fClient(None, APP_ID)
client.ctap = mock.MagicMock()
client.ctap.get_version.return_value = "U2F_V2"
client.ctap.authenticate.side_effect = ApduError(APDU.USE_NOT_SATISFIED)
try:
client.register(
APP_ID,
[{"version": "U2F_V2", "challenge": "foobar"}],
[{"version": "U2F_V2", "keyHandle": "a2V5"}],
)
self.fail("register did not raise error")
except ClientError as e:
self.assertEqual(e.code, ClientError.ERR.DEVICE_INELIGIBLE)
client.ctap.get_version.assert_called_with()
client.ctap.authenticate.assert_called_once()
# Check keyHandle
self.assertEqual(client.ctap.authenticate.call_args[0][2], b"key")
# Ensure check-only was set
self.assertTrue(client.ctap.authenticate.call_args[0][3])
def test_register(self):
client = U2fClient(None, APP_ID)
client.ctap = mock.MagicMock()
client.ctap.get_version.return_value = "U2F_V2"
client.ctap.authenticate.side_effect = ApduError(APDU.WRONG_DATA)
client.ctap.register.return_value = REG_DATA
resp = client.register(
APP_ID,
[{"version": "U2F_V2", "challenge": "foobar"}],
[{"version": "U2F_V2", "keyHandle": "a2V5"}],
)
client.ctap.get_version.assert_called_with()
client.ctap.authenticate.assert_called_once()
client.ctap.register.assert_called_once()
client_param, app_param = client.ctap.register.call_args[0]
self.assertEqual(sha256(websafe_decode(resp["clientData"])), client_param)
self.assertEqual(websafe_decode(resp["registrationData"]), REG_DATA)
self.assertEqual(sha256(APP_ID.encode()), app_param)
def test_register_await_timeout(self):
client = U2fClient(None, APP_ID)
client.ctap = mock.MagicMock()
client.ctap.get_version.return_value = "U2F_V2"
client.ctap.authenticate.side_effect = ApduError(APDU.WRONG_DATA)
client.ctap.register.side_effect = ApduError(APDU.USE_NOT_SATISFIED)
client.poll_delay = 0.01
event = Event()
timer = Timer(0.1, event.set)
timer.start()
try:
client.register(
APP_ID,
[{"version": "U2F_V2", "challenge": "foobar"}],
[{"version": "U2F_V2", "keyHandle": "a2V5"}],
event=event,
)
except ClientError as e:
self.assertEqual(e.code, ClientError.ERR.TIMEOUT)
def test_register_await_touch(self):
client = U2fClient(None, APP_ID)
client.ctap = mock.MagicMock()
client.ctap.get_version.return_value = "U2F_V2"
client.ctap.authenticate.side_effect = ApduError(APDU.WRONG_DATA)
client.ctap.register.side_effect = [
ApduError(APDU.USE_NOT_SATISFIED),
ApduError(APDU.USE_NOT_SATISFIED),
ApduError(APDU.USE_NOT_SATISFIED),
ApduError(APDU.USE_NOT_SATISFIED),
REG_DATA,
]
event = Event()
event.wait = mock.MagicMock()
resp = client.register(
APP_ID,
[{"version": "U2F_V2", "challenge": "foobar"}],
[{"version": "U2F_V2", "keyHandle": "a2V5"}],
event=event,
)
event.wait.assert_called()
client.ctap.get_version.assert_called_with()
client.ctap.authenticate.assert_called_once()
client.ctap.register.assert_called()
client_param, app_param = client.ctap.register.call_args[0]
self.assertEqual(sha256(websafe_decode(resp["clientData"])), client_param)
self.assertEqual(websafe_decode(resp["registrationData"]), REG_DATA)
self.assertEqual(sha256(APP_ID.encode()), app_param)
def test_sign_wrong_app_id(self):
client = U2fClient(None, APP_ID)
client.ctap = mock.MagicMock()
client.ctap.get_version.return_value = "U2F_V2"
try:
client.sign(
"http://foo.example.com",
"challenge",
[{"version": "U2F_V2", "keyHandle": "a2V5"}],
)
self.fail("sign did not raise error")
except ClientError as e:
self.assertEqual(e.code, ClientError.ERR.BAD_REQUEST)
def test_sign_unsupported_version(self):
client = U2fClient(None, APP_ID)
client.ctap = mock.MagicMock()
client.ctap.get_version.return_value = "U2F_XXX"
try:
client.sign(
APP_ID, "challenge", [{"version": "U2F_V2", "keyHandle": "a2V5"}]
)
self.fail("sign did not raise error")
except ClientError as e:
self.assertEqual(e.code, ClientError.ERR.DEVICE_INELIGIBLE)
client.ctap.get_version.assert_called_with()
def test_sign_missing_key(self):
client = U2fClient(None, APP_ID)
client.ctap = mock.MagicMock()
client.ctap.get_version.return_value = "U2F_V2"
client.ctap.authenticate.side_effect = ApduError(APDU.WRONG_DATA)
try:
client.sign(
APP_ID, "challenge", [{"version": "U2F_V2", "keyHandle": "a2V5"}]
)
self.fail("sign did not raise error")
except ClientError as e:
self.assertEqual(e.code, ClientError.ERR.DEVICE_INELIGIBLE)
client.ctap.get_version.assert_called_with()
client.ctap.authenticate.assert_called_once()
_, app_param, key_handle = client.ctap.authenticate.call_args[0]
self.assertEqual(app_param, sha256(APP_ID.encode()))
self.assertEqual(key_handle, b"key")
def test_sign(self):
client = U2fClient(None, APP_ID)
client.ctap = mock.MagicMock()
client.ctap.get_version.return_value = "U2F_V2"
client.ctap.authenticate.return_value = SIG_DATA
resp = client.sign(
APP_ID, "challenge", [{"version": "U2F_V2", "keyHandle": "a2V5"}]
)
client.ctap.get_version.assert_called_with()
client.ctap.authenticate.assert_called_once()
client_param, app_param, key_handle = client.ctap.authenticate.call_args[0]
self.assertEqual(client_param, sha256(websafe_decode(resp["clientData"])))
self.assertEqual(app_param, sha256(APP_ID.encode()))
self.assertEqual(key_handle, b"key")
self.assertEqual(websafe_decode(resp["signatureData"]), SIG_DATA)
def test_sign_await_touch(self):
client = U2fClient(None, APP_ID)
client.ctap = mock.MagicMock()
client.ctap.get_version.return_value = "U2F_V2"
client.ctap.authenticate.side_effect = [
ApduError(APDU.USE_NOT_SATISFIED),
ApduError(APDU.USE_NOT_SATISFIED),
ApduError(APDU.USE_NOT_SATISFIED),
ApduError(APDU.USE_NOT_SATISFIED),
SIG_DATA,
]
event = Event()
event.wait = mock.MagicMock()
resp = client.sign(
APP_ID,
"challenge",
[{"version": "U2F_V2", "keyHandle": "a2V5"}],
event=event,
)
event.wait.assert_called()
client.ctap.get_version.assert_called_with()
client.ctap.authenticate.assert_called()
client_param, app_param, key_handle = client.ctap.authenticate.call_args[0]
self.assertEqual(client_param, sha256(websafe_decode(resp["clientData"])))
self.assertEqual(app_param, sha256(APP_ID.encode()))
self.assertEqual(key_handle, b"key")
self.assertEqual(websafe_decode(resp["signatureData"]), SIG_DATA)
rp = {"id": "example.com", "name": "Example RP"}
user = {"id": b"user_id", "name": "A. User"}
@ -406,7 +141,7 @@ class TestFido2Client(unittest.TestCase):
)
self.assertIsInstance(response.attestation_object, AttestationObject)
self.assertIsInstance(response.client_data, ClientData)
self.assertIsInstance(response.client_data, CollectedClientData)
ctap2.make_credential.assert_called_with(
response.client_data.hash,
@ -422,8 +157,8 @@ class TestFido2Client(unittest.TestCase):
mock.ANY,
)
self.assertEqual(response.client_data.get("origin"), APP_ID)
self.assertEqual(response.client_data.get("type"), "webauthn.create")
self.assertEqual(response.client_data.origin, APP_ID)
self.assertEqual(response.client_data.type, "webauthn.create")
self.assertEqual(response.client_data.challenge, challenge)
def test_make_credential_ctap1(self):
@ -443,15 +178,15 @@ class TestFido2Client(unittest.TestCase):
)
self.assertIsInstance(response.attestation_object, AttestationObject)
self.assertIsInstance(response.client_data, ClientData)
self.assertIsInstance(response.client_data, CollectedClientData)
client_data = response.client_data
ctap1_mock.register.assert_called_with(
client_data.hash, sha256(rp["id"].encode())
)
self.assertEqual(client_data.get("origin"), APP_ID)
self.assertEqual(client_data.get("type"), "webauthn.create")
self.assertEqual(client_data.origin, APP_ID)
self.assertEqual(client_data.type, "webauthn.create")
self.assertEqual(client_data.challenge, challenge)
self.assertEqual(response.attestation_object.fmt, "fido-u2f")

View File

@ -27,61 +27,10 @@
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from fido2.rpid import verify_app_id, verify_rp_id
from fido2.rpid import verify_rp_id
import unittest
class TestAppId(unittest.TestCase):
def test_valid_ids(self):
self.assertTrue(
verify_app_id("https://example.com", "https://register.example.com")
)
self.assertTrue(
verify_app_id("https://example.com", "https://fido.example.com")
)
self.assertTrue(
verify_app_id("https://example.com", "https://www.example.com:444")
)
self.assertTrue(
verify_app_id(
"https://companyA.hosting.example.com",
"https://fido.companyA.hosting.example.com",
)
)
self.assertTrue(
verify_app_id(
"https://companyA.hosting.example.com",
"https://xyz.companyA.hosting.example.com",
)
)
def test_invalid_ids(self):
self.assertFalse(verify_app_id("https://example.com", "http://example.com"))
self.assertFalse(verify_app_id("https://example.com", "http://www.example.com"))
self.assertFalse(
verify_app_id("https://example.com", "https://example-test.com")
)
self.assertFalse(
verify_app_id(
"https://companyA.hosting.example.com", "https://register.example.com"
)
)
self.assertFalse(
verify_app_id(
"https://companyA.hosting.example.com",
"https://companyB.hosting.example.com",
)
)
def test_effective_tld_names(self):
self.assertFalse(
verify_app_id("https://appspot.com", "https://foo.appspot.com")
)
self.assertFalse(verify_app_id("https://co.uk", "https://example.co.uk"))
class TestRpId(unittest.TestCase):
def test_valid_ids(self):
self.assertTrue(verify_rp_id("example.com", "https://register.example.com"))

View File

@ -1,9 +1,8 @@
import json
import unittest
from fido2.client import WEBAUTHN_TYPE, ClientData
from fido2.server import Fido2Server, U2FFido2Server
from fido2.server import Fido2Server, U2FFido2Server, verify_app_id
from fido2.webauthn import (
CollectedClientData,
PublicKeyCredentialRpEntity,
UserVerificationRequirement,
AttestedCredentialData,
@ -14,6 +13,57 @@ from .test_ctap2 import _ATT_CRED_DATA, _CRED_ID
from .utils import U2FDevice
class TestAppId(unittest.TestCase):
def test_valid_ids(self):
self.assertTrue(
verify_app_id("https://example.com", "https://register.example.com")
)
self.assertTrue(
verify_app_id("https://example.com", "https://fido.example.com")
)
self.assertTrue(
verify_app_id("https://example.com", "https://www.example.com:444")
)
self.assertTrue(
verify_app_id(
"https://companyA.hosting.example.com",
"https://fido.companyA.hosting.example.com",
)
)
self.assertTrue(
verify_app_id(
"https://companyA.hosting.example.com",
"https://xyz.companyA.hosting.example.com",
)
)
def test_invalid_ids(self):
self.assertFalse(verify_app_id("https://example.com", "http://example.com"))
self.assertFalse(verify_app_id("https://example.com", "http://www.example.com"))
self.assertFalse(
verify_app_id("https://example.com", "https://example-test.com")
)
self.assertFalse(
verify_app_id(
"https://companyA.hosting.example.com", "https://register.example.com"
)
)
self.assertFalse(
verify_app_id(
"https://companyA.hosting.example.com",
"https://companyB.hosting.example.com",
)
)
def test_effective_tld_names(self):
self.assertFalse(
verify_app_id("https://appspot.com", "https://foo.appspot.com")
)
self.assertFalse(verify_app_id("https://co.uk", "https://example.co.uk"))
class TestPublicKeyCredentialRpEntity(unittest.TestCase):
def test_id_hash(self):
rp = PublicKeyCredentialRpEntity("Example", "example.com")
@ -63,12 +113,11 @@ class TestFido2Server(unittest.TestCase):
"challenge": "GAZPACHO!",
"user_verification": UserVerificationRequirement.PREFERRED,
}
client_data_dict = {
"challenge": "GAZPACHO!",
"origin": "https://example.com",
"type": WEBAUTHN_TYPE.GET_ASSERTION,
}
client_data = ClientData(json.dumps(client_data_dict).encode("utf-8"))
client_data = CollectedClientData.create(
CollectedClientData.TYPE.GET,
"GAZPACHO!",
"https://example.com",
)
_AUTH_DATA = bytes.fromhex(
"A379A6F6EEAFB9A55E378C118034E2751E682FAB9F2D30AB13D2125586CE1947010000001D"
)
@ -93,12 +142,11 @@ class TestU2FFido2Server(unittest.TestCase):
"challenge": "GAZPACHO!",
"user_verification": UserVerificationRequirement.PREFERRED,
}
client_data_dict = {
"challenge": "GAZPACHO!",
"origin": "https://example.com",
"type": WEBAUTHN_TYPE.GET_ASSERTION,
}
client_data = ClientData(json.dumps(client_data_dict).encode("utf-8"))
client_data = CollectedClientData.create(
CollectedClientData.TYPE.GET,
"GAZPACHO!",
"https://example.com",
)
param = b"TOMATO GIVES "
@ -130,12 +178,12 @@ class TestU2FFido2Server(unittest.TestCase):
"challenge": "GAZPACHO!",
"user_verification": UserVerificationRequirement.PREFERRED,
}
client_data_dict = {
"challenge": "GAZPACHO!",
"origin": "https://oauth.example.com",
"type": WEBAUTHN_TYPE.GET_ASSERTION,
}
client_data = ClientData(json.dumps(client_data_dict).encode("utf-8"))
client_data = CollectedClientData.create(
CollectedClientData.TYPE.GET,
"GAZPACHO!",
"https://oauth.example.com",
)
param = b"TOMATO GIVES "
@ -153,16 +201,17 @@ class TestU2FFido2Server(unittest.TestCase):
)
# Now with something not whitelisted
client_data_dict = {
"challenge": "GAZPACHO!",
"origin": "https://publicthingy.example.com",
"type": WEBAUTHN_TYPE.GET_ASSERTION,
}
client_data = ClientData(json.dumps(client_data_dict).encode("utf-8"))
client_data = CollectedClientData.create(
CollectedClientData.TYPE.GET,
"GAZPACHO!",
"https://publicthingy.example.com",
)
authenticator_data, signature = device.sign(client_data)
with self.assertRaisesRegex(ValueError, "Invalid origin in ClientData."):
with self.assertRaisesRegex(
ValueError, "Invalid origin in CollectedClientData."
):
server.authenticate_complete(
state,
[auth_data],

View File

@ -28,6 +28,7 @@
from fido2.webauthn import (
Aaguid,
AuthenticatorSelectionCriteria,
CollectedClientData,
ResidentKeyRequirement,
PublicKeyCredentialRpEntity,
PublicKeyCredentialUserEntity,
@ -74,6 +75,32 @@ class TestAaguid(unittest.TestCase):
class TestWebAuthnDataTypes(unittest.TestCase):
def test_collected_client_data(self):
o = CollectedClientData(
b'{"type":"webauthn.create","challenge":"cdySOP-1JI4J_BpOeO9ut25rlZJueF16aO6auTTYAis","origin":"https://demo.yubico.com","crossOrigin":false}' # noqa
)
assert o.type == "webauthn.create"
assert o.origin == "https://demo.yubico.com"
assert o.challenge == bytes.fromhex(
"71dc9238ffb5248e09fc1a4e78ef6eb76e6b95926e785d7a68ee9ab934d8022b"
)
assert o.cross_origin is False
assert (
o.b64
== "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiY2R5U09QLTFKSTRKX0JwT2VPOXV0MjVybFpKdWVGMTZhTzZhdVRUWUFpcyIsIm9yaWdpbiI6Imh0dHBzOi8vZGVtby55dWJpY28uY29tIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ" # noqa
)
assert o.hash == bytes.fromhex(
"8b20a0b904b4747aacae71d55bf60b4eb2583f7e639f55f40baac23c2600c178"
)
assert o == CollectedClientData.create(
"webauthn.create",
"cdySOP-1JI4J_BpOeO9ut25rlZJueF16aO6auTTYAis",
"https://demo.yubico.com",
)
def test_authenticator_selection_criteria(self):
o = AuthenticatorSelectionCriteria(
"platform", require_resident_key=True, user_verification="required"