From 5eead78917afb2f0db10d5b146bee5286dca5c83 Mon Sep 17 00:00:00 2001 From: Dain Nilsson Date: Wed, 6 Nov 2024 12:10:37 +0100 Subject: [PATCH] Clean up examples and extract common code --- examples/cred_blob.py | 55 +++------------- examples/credential.py | 54 +++------------- examples/exampleutils.py | 97 +++++++++++++++++++++++++++++ examples/hmac_secret.py | 21 +------ examples/large_blobs.py | 63 ++----------------- examples/prf.py | 53 ++-------------- examples/resident_key.py | 57 +++-------------- examples/verify_attestation.py | 55 ++-------------- examples/verify_attestation_mds3.py | 54 ++-------------- fido2/win_api.py | 2 + 10 files changed, 142 insertions(+), 369 deletions(-) create mode 100644 examples/exampleutils.py diff --git a/examples/cred_blob.py b/examples/cred_blob.py index 04d60bf1..6de845f7 100644 --- a/examples/cred_blob.py +++ b/examples/cred_blob.py @@ -29,62 +29,21 @@ Connects to the first FIDO device found which supports the CredBlob extension, creates a new credential for it with the extension enabled, and stores some data. """ -from fido2.hid import CtapHidDevice -from fido2.client import Fido2Client, WindowsClient, UserInteraction from fido2.server import Fido2Server -from getpass import getpass -import ctypes +from exampleutils import get_client import sys import os -try: - from fido2.pcsc import CtapPcscDevice -except ImportError: - CtapPcscDevice = None - -def enumerate_devices(): - for dev in CtapHidDevice.list_devices(): - yield dev - if CtapPcscDevice: - for dev in CtapPcscDevice.list_devices(): - yield dev - - -# Handle user interaction -class CliInteraction(UserInteraction): - def prompt_up(self): - print("\nTouch your authenticator device now...\n") - - def request_pin(self, permissions, rd_id): - return getpass("Enter PIN: ") - - def request_uv(self, permissions, rd_id): - print("User Verification required.") - return True +# Locate a suitable FIDO authenticator +client = get_client(lambda client: "credBlob" in client.info.extensions) +# Prefer UV token if supported uv = "discouraged" - -if WindowsClient.is_available() and not ctypes.windll.shell32.IsUserAnAdmin(): - # Use the Windows WebAuthn API if available, and we're not running as admin - client = WindowsClient("https://example.com") -else: - # Locate a device - for dev in enumerate_devices(): - client = Fido2Client( - dev, "https://example.com", user_interaction=CliInteraction() - ) - if "credBlob" in client.info.extensions: - break - else: - print("No Authenticator with the CredBlob extension found!") - sys.exit(1) - - # Prefer UV token if supported - if client.info.options.get("uv") or client.info.options.get("bioEnroll"): - uv = "preferred" - print("Authenticator is configured for User Verification") +if client.info.options.get("uv") or client.info.options.get("bioEnroll"): + uv = "preferred" + print("Authenticator is configured for User Verification") server = Fido2Server({"id": "example.com", "name": "Example RP"}) diff --git a/examples/credential.py b/examples/credential.py index 8fef2118..daf33354 100644 --- a/examples/credential.py +++ b/examples/credential.py @@ -31,57 +31,19 @@ This works with both FIDO 2.0 devices as well as with U2F devices. On Windows, the native WebAuthn API will be used. """ -from fido2.hid import CtapHidDevice -from fido2.client import Fido2Client, WindowsClient, UserInteraction from fido2.server import Fido2Server -from getpass import getpass -import sys -import ctypes +from exampleutils import get_client +# Locate a suitable FIDO authenticator +client = get_client() -# Handle user interaction -class CliInteraction(UserInteraction): - def prompt_up(self): - print("\nTouch your authenticator device now...\n") - def request_pin(self, permissions, rd_id): - return getpass("Enter PIN: ") - - def request_uv(self, permissions, rd_id): - print("User Verification required.") - return True - - -uv = "discouraged" - -if WindowsClient.is_available() and not ctypes.windll.shell32.IsUserAnAdmin(): - # Use the Windows WebAuthn API if available, and we're not running as admin - client = WindowsClient("https://example.com") +# Prefer UV if supported and configured +if client.info.options.get("uv") or client.info.options.get("bioEnroll"): + uv = "preferred" + print("Authenticator supports User Verification") else: - # Locate a device - dev = next(CtapHidDevice.list_devices(), None) - if dev is not None: - print("Use USB HID channel.") - else: - try: - from fido2.pcsc import CtapPcscDevice - - dev = next(CtapPcscDevice.list_devices(), None) - print("Use NFC channel.") - except Exception as e: - print("NFC channel search error:", e) - - if not dev: - print("No FIDO device found") - sys.exit(1) - - # Set up a FIDO 2 client using the origin https://example.com - client = Fido2Client(dev, "https://example.com", user_interaction=CliInteraction()) - - # Prefer UV if supported and configured - if client.info.options.get("uv") or client.info.options.get("bioEnroll"): - uv = "preferred" - print("Authenticator supports User Verification") + uv = "discouraged" server = Fido2Server({"id": "example.com", "name": "Example RP"}, attestation="direct") diff --git a/examples/exampleutils.py b/examples/exampleutils.py new file mode 100644 index 00000000..bf921ce0 --- /dev/null +++ b/examples/exampleutils.py @@ -0,0 +1,97 @@ +# Copyright (c) 2024 Yubico AB +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or +# without modification, are permitted provided that the following +# conditions are met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided +# with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +""" +Utilities for common functionality used by several examples in this directory. +""" + +from fido2.hid import CtapHidDevice +from fido2.client import Fido2Client, WindowsClient, UserInteraction +from getpass import getpass +import ctypes + + +try: + from fido2.pcsc import CtapPcscDevice +except ImportError: + CtapPcscDevice = None + + +# Handle user interaction via CLI prompts +class CliInteraction(UserInteraction): + def __init__(self): + self._pin = None + + def prompt_up(self): + print("\nTouch your authenticator device now...\n") + + def request_pin(self, permissions, rd_id): + if not self._pin: + self._pin = getpass("Enter PIN: ") + return self._pin + + def request_uv(self, permissions, rd_id): + print("User Verification required.") + return True + + +def enumerate_devices(): + for dev in CtapHidDevice.list_devices(): + yield dev + if CtapPcscDevice: + for dev in CtapPcscDevice.list_devices(): + yield dev + + +def get_client(predicate=None, **kwargs): + """Locate a CTAP device suitable for use. + + If running on Windows as non-admin, the predicate check will be skipped and + a webauthn.dll based client will be returned. + + Extra kwargs will be passed to the constructor of Fido2Client. + """ + if WindowsClient.is_available() and not ctypes.windll.shell32.IsUserAnAdmin(): + # Use the Windows WebAuthn API if available, and we're not running as admin + return WindowsClient("https://example.com") + + user_interaction = kwargs.pop("user_interaction", None) or CliInteraction() + + # Locate a device + for dev in enumerate_devices(): + # Set up a FIDO 2 client using the origin https://example.com + client = Fido2Client( + dev, + "https://example.com", + user_interaction=user_interaction, + **kwargs, + ) + # Check if it is suitable for use + if predicate is None or predicate(client): + return client + else: + raise ValueError("No suitable Authenticator found!") diff --git a/examples/hmac_secret.py b/examples/hmac_secret.py index 559d18f6..1c164c2c 100644 --- a/examples/hmac_secret.py +++ b/examples/hmac_secret.py @@ -36,10 +36,10 @@ """ from fido2.hid import CtapHidDevice from fido2.server import Fido2Server -from fido2.client import Fido2Client, WindowsClient, UserInteraction +from fido2.client import Fido2Client, WindowsClient from fido2.ctap2.extensions import HmacSecretExtension +from exampleutils import CliInteraction from functools import partial -from getpass import getpass import ctypes import sys import os @@ -58,28 +58,13 @@ def enumerate_devices(): yield dev -# Handle user interaction -class CliInteraction(UserInteraction): - def prompt_up(self): - print("\nTouch your authenticator device now...\n") - - def request_pin(self, permissions, rd_id): - return getpass("Enter PIN: ") - - def request_uv(self, permissions, rd_id): - print("User Verification required.") - return True - - uv = "discouraged" -rk = "discouraged" if WindowsClient.is_available() and not ctypes.windll.shell32.IsUserAnAdmin(): # Use the Windows WebAuthn API if available, and we're not running as admin # By default only the PRF extension is allowed, we need to explicitly # configure the client to allow hmac-secret client = WindowsClient("https://example.com", allow_hmac_secret=True) - rk = "required" # Windows requires resident key for hmac-secret else: # Locate a device for dev in enumerate_devices(): @@ -103,7 +88,7 @@ def request_uv(self, permissions, rd_id): # Prepare parameters for makeCredential create_options, state = server.register_begin( user, - resident_key_requirement=rk, + resident_key_requirement="discouraged", user_verification=uv, authenticator_attachment="cross-platform", ) diff --git a/examples/large_blobs.py b/examples/large_blobs.py index f4c55af4..eb510bbc 100644 --- a/examples/large_blobs.py +++ b/examples/large_blobs.py @@ -31,66 +31,19 @@ This works with both FIDO 2.0 devices as well as with U2F devices. On Windows, the native WebAuthn API will be used. """ -from fido2.hid import CtapHidDevice -from fido2.client import Fido2Client, WindowsClient, UserInteraction from fido2.server import Fido2Server -from getpass import getpass -import ctypes +from exampleutils import get_client import sys -try: - from fido2.pcsc import CtapPcscDevice -except ImportError: - CtapPcscDevice = None - - -def enumerate_devices(): - for dev in CtapHidDevice.list_devices(): - yield dev - if CtapPcscDevice: - for dev in CtapPcscDevice.list_devices(): - yield dev - - -# Handle user interaction -class CliInteraction(UserInteraction): - def prompt_up(self): - print("\nTouch your authenticator device now...\n") - - def request_pin(self, permissions, rd_id): - return getpass("Enter PIN: ") - - def request_uv(self, permissions, rd_id): - print("User Verification required.") - return True +# Locate a suitable FIDO authenticator +client = get_client(lambda client: "largeBlobKey" in client.info.extensions) +# LargeBlob requires UV if is it configured uv = "discouraged" - -# Locate a device -if WindowsClient.is_available() and not ctypes.windll.shell32.IsUserAnAdmin(): - # Use the Windows WebAuthn API if available, and we're not running as admin - client = WindowsClient("https://example.com") -else: - for dev in enumerate_devices(): - client = Fido2Client( - dev, "https://example.com", user_interaction=CliInteraction() - ) - if "largeBlobKey" in client.info.extensions: - break - else: - print("No Authenticator with the largeBlobKey extension found!") - sys.exit(1) - - if not client.info.options.get("largeBlobs"): - print("Authenticator does not support large blobs!") - sys.exit(1) - - # Prefer UV token if supported - if client.info.options.get("uv") or client.info.options.get("bioEnroll"): - uv = "preferred" - print("Authenticator is configured for User Verification") +if client.info.options.get("clientPin"): + uv = "required" server = Fido2Server({"id": "example.com", "name": "Example RP"}) @@ -127,10 +80,6 @@ def request_uv(self, permissions, rd_id): print("Credential created! Writing a blob...") -# If UV is configured, it is required -if auth_data.is_user_verified(): - uv = "required" - # Prepare parameters for getAssertion request_options, state = server.authenticate_begin(user_verification=uv) diff --git a/examples/prf.py b/examples/prf.py index 4447d5dc..26f21448 100644 --- a/examples/prf.py +++ b/examples/prf.py @@ -30,62 +30,17 @@ creates a new credential for it with the extension enabled, and uses it to derive two separate secrets. """ -from fido2.hid import CtapHidDevice from fido2.server import Fido2Server -from fido2.client import Fido2Client, WindowsClient, UserInteraction from fido2.utils import websafe_encode -from getpass import getpass -import ctypes +from exampleutils import get_client import sys import os -try: - from fido2.pcsc import CtapPcscDevice -except ImportError: - CtapPcscDevice = None - - -def enumerate_devices(): - for dev in CtapHidDevice.list_devices(): - yield dev - if CtapPcscDevice: - for dev in CtapPcscDevice.list_devices(): - yield dev - - -# Handle user interaction -class CliInteraction(UserInteraction): - def prompt_up(self): - print("\nTouch your authenticator device now...\n") - - def request_pin(self, permissions, rd_id): - return getpass("Enter PIN: ") - - def request_uv(self, permissions, rd_id): - print("User Verification required.") - return True +# Locate a suitable FIDO authenticator +client = get_client(lambda client: "hmac-secret" in client.info.extensions) uv = "discouraged" -rk = "discouraged" - -if WindowsClient.is_available() and not ctypes.windll.shell32.IsUserAnAdmin(): - # Use the Windows WebAuthn API if available, and we're not running as admin - client = WindowsClient("https://example.com") - rk = "required" # Windows requires resident key for hmac-secret -else: - # Locate a device - for dev in enumerate_devices(): - client = Fido2Client( - dev, - "https://example.com", - user_interaction=CliInteraction(), - ) - if "hmac-secret" in client.info.extensions: - break - else: - print("No Authenticator with the PRF extension found!") - sys.exit(1) server = Fido2Server({"id": "example.com", "name": "Example RP"}, attestation="none") user = {"id": b"user_id", "name": "A. User"} @@ -93,7 +48,7 @@ def request_uv(self, permissions, rd_id): # Prepare parameters for makeCredential create_options, state = server.register_begin( user, - resident_key_requirement=rk, + resident_key_requirement="discouraged", user_verification=uv, authenticator_attachment="cross-platform", ) diff --git a/examples/resident_key.py b/examples/resident_key.py index 2634a22d..ede5722c 100644 --- a/examples/resident_key.py +++ b/examples/resident_key.py @@ -31,61 +31,18 @@ This works with both FIDO 2.0 devices as well as with U2F devices. On Windows, the native WebAuthn API will be used. """ -from fido2.hid import CtapHidDevice -from fido2.client import Fido2Client, WindowsClient, UserInteraction from fido2.server import Fido2Server -from getpass import getpass -import sys -import ctypes +from exampleutils import get_client -try: - from fido2.pcsc import CtapPcscDevice -except ImportError: - CtapPcscDevice = None - - -def enumerate_devices(): - for dev in CtapHidDevice.list_devices(): - yield dev - if CtapPcscDevice: - for dev in CtapPcscDevice.list_devices(): - yield dev - - -# Handle user interaction -class CliInteraction(UserInteraction): - def prompt_up(self): - print("\nTouch your authenticator device now...\n") - - def request_pin(self, permissions, rd_id): - return getpass("Enter PIN: ") - - def request_uv(self, permissions, rd_id): - print("User Verification required.") - return True +# Locate a suitable FIDO authenticator +client = get_client(lambda client: client.info.options.get("rk")) +# Prefer UV if supported and configured uv = "discouraged" - -if WindowsClient.is_available() and not ctypes.windll.shell32.IsUserAnAdmin(): - # Use the Windows WebAuthn API if available, and we're not running as admin - client = WindowsClient("https://example.com") -else: - # Locate a device - for dev in enumerate_devices(): - client = Fido2Client( - dev, "https://example.com", user_interaction=CliInteraction() - ) - if client.info.options.get("rk"): - break - else: - print("No Authenticator with support for resident key found!") - sys.exit(1) - - # Prefer UV if supported and configured - if client.info.options.get("uv") or client.info.options.get("bioEnroll"): - uv = "preferred" - print("Authenticator is configured for User Verification") +if client.info.options.get("uv") or client.info.options.get("bioEnroll"): + uv = "preferred" + print("Authenticator is configured for User Verification") server = Fido2Server({"id": "example.com", "name": "Example RP"}, attestation="direct") diff --git a/examples/verify_attestation.py b/examples/verify_attestation.py index 51a60eef..0f5ed3f5 100644 --- a/examples/verify_attestation.py +++ b/examples/verify_attestation.py @@ -34,14 +34,10 @@ Yubico FIDO root CA (this will only work for Yubico devices). On Windows, the native WebAuthn API will be used. """ -from fido2.hid import CtapHidDevice -from fido2.client import Fido2Client, WindowsClient, UserInteraction from fido2.server import Fido2Server from fido2.attestation import AttestationVerifier +from exampleutils import get_client from base64 import b64decode -from getpass import getpass -import sys -import ctypes # Official Yubico root CA for FIDO Authenticators @@ -80,51 +76,8 @@ def ca_lookup(self, result, auth_data): return YUBICO_CA -uv = "discouraged" - - -# Handle user interaction -class CliInteraction(UserInteraction): - def prompt_up(self): - print("\nTouch your authenticator device now...\n") - - def request_pin(self, permissions, rd_id): - return getpass("Enter PIN: ") - - def request_uv(self, permissions, rd_id): - print("User Verification required.") - return True - - -if WindowsClient.is_available() and not ctypes.windll.shell32.IsUserAnAdmin(): - # Use the Windows WebAuthn API if available, and we're not running as admin - client = WindowsClient("https://example.com") -else: - # Locate a device - dev = next(CtapHidDevice.list_devices(), None) - if dev is not None: - print("Use USB HID channel.") - else: - try: - from fido2.pcsc import CtapPcscDevice - - dev = next(CtapPcscDevice.list_devices(), None) - print("Use NFC channel.") - except Exception as e: - print("NFC channel search error:", e) - - if not dev: - print("No FIDO device found") - sys.exit(1) - - # Set up a FIDO 2 client using the origin https://example.com - client = Fido2Client(dev, "https://example.com", user_interaction=CliInteraction()) - - # Prefer UV if supported - if client.info.options.get("uv"): - uv = "preferred" - print("Authenticator supports User Verification") - +# Locate a suitable FIDO authenticator +client = get_client() server = Fido2Server( {"id": "example.com", "name": "Example RP"}, @@ -136,7 +89,7 @@ def request_uv(self, permissions, rd_id): # Prepare parameters for makeCredential create_options, state = server.register_begin( - user, user_verification=uv, authenticator_attachment="cross-platform" + user, user_verification="discouraged", authenticator_attachment="cross-platform" ) # Create a credential diff --git a/examples/verify_attestation_mds3.py b/examples/verify_attestation_mds3.py index 5700072a..e192dbce 100644 --- a/examples/verify_attestation_mds3.py +++ b/examples/verify_attestation_mds3.py @@ -38,15 +38,12 @@ NOTE: You need to retrieve a MDS3 blob to run this example. See https://fidoalliance.org/metadata/ for more info. """ -from fido2.hid import CtapHidDevice -from fido2.client import Fido2Client, WindowsClient, UserInteraction from fido2.server import Fido2Server from fido2.attestation import UntrustedAttestation from fido2.mds3 import parse_blob, MdsAttestationVerifier +from exampleutils import get_client from base64 import b64decode -from getpass import getpass import sys -import ctypes # Load the root CA used to sign the Metadata Statement blob ca = b64decode( @@ -87,51 +84,8 @@ # We could optionally pass a filter function to only allow specific authenticators. mds = MdsAttestationVerifier(metadata) -uv = "discouraged" - - -# Handle user interaction -class CliInteraction(UserInteraction): - def prompt_up(self): - print("\nTouch your authenticator device now...\n") - - def request_pin(self, permissions, rd_id): - return getpass("Enter PIN: ") - - def request_uv(self, permissions, rd_id): - print("User Verification required.") - return True - - -if WindowsClient.is_available() and not ctypes.windll.shell32.IsUserAnAdmin(): - # Use the Windows WebAuthn API if available, and we're not running as admin - client = WindowsClient("https://example.com") -else: - # Locate a device - dev = next(CtapHidDevice.list_devices(), None) - if dev is not None: - print("Use USB HID channel.") - else: - try: - from fido2.pcsc import CtapPcscDevice - - dev = next(CtapPcscDevice.list_devices(), None) - print("Use NFC channel.") - except Exception as e: - print("NFC channel search error:", e) - - if not dev: - print("No FIDO device found") - sys.exit(1) - - # Set up a FIDO 2 client using the origin https://example.com - client = Fido2Client(dev, "https://example.com", user_interaction=CliInteraction()) - - # Prefer UV if supported - if client.info.options.get("uv"): - uv = "preferred" - print("Authenticator supports User Verification") - +# Locate a suitable FIDO authenticator +client = get_client() # The MDS verifier is passed to the server to verify that new credentials registered # exist in the MDS blob, else the registration will fail. @@ -145,7 +99,7 @@ def request_uv(self, permissions, rd_id): # Prepare parameters for makeCredential create_options, state = server.register_begin( - user, user_verification=uv, authenticator_attachment="cross-platform" + user, user_verification="discouraged", authenticator_attachment="cross-platform" ) # Create a credential diff --git a/fido2/win_api.py b/fido2/win_api.py index 29668b4e..57fb154e 100644 --- a/fido2/win_api.py +++ b/fido2/win_api.py @@ -1040,9 +1040,11 @@ def make_credential( if extensions.get("minPinLength", True): win_extensions.append(WebAuthNExtension("minPinLength", BOOL(True))) if "prf" in extensions: + resident_key = True # Windows requires resident key for hmac-secret enable_prf = True win_extensions.append(WebAuthNExtension("hmac-secret", BOOL(True))) elif "hmacCreateSecret" in extensions and self._allow_hmac_secret: + resident_key = True # Windows requires resident key for hmac-secret win_extensions.append(WebAuthNExtension("hmac-secret", BOOL(True))) if event: