Compare commits

...

95 Commits
1.0.0 ... main

Author SHA1 Message Date
Dain Nilsson edcb00bc32
Merge PR #218 2024-04-03 17:55:59 +02:00
Joost van Dijk 8b979313e9
allow http://localhost origins 2024-04-03 10:23:53 +02:00
Dain Nilsson ee9bf59783
Merge release branch 2024-03-13 09:37:36 +01:00
Dain Nilsson cfffe17e18
Bump version 2024-03-13 09:37:04 +01:00
Dain Nilsson 77893c2fd5
Set release date 2024-03-13 09:31:16 +01:00
Dain Nilsson 82f9d0765b
Prepare 1.1.3 2024-03-13 09:12:56 +01:00
Dain Nilsson 9240d6e53b
Merge PR #214 2024-03-12 16:53:05 +01:00
Dain Nilsson b5686b3faf
Bump pre-commit hooks and server example lockfile 2024-03-12 16:21:24 +01:00
Dain Nilsson 6d13bd7b91
Allow cryptography <= 45 (current + 2) 2024-03-12 16:19:39 +01:00
Dain Nilsson f1952aebb9
Add types to constructors of webauthn data objects (close #205) 2024-03-05 16:09:47 +01:00
Dain Nilsson de1be496fe
Update NEWS 2024-03-05 13:57:18 +01:00
Dain Nilsson 1be5c0b5f6
Fix run loop error on MacOS (close #210) 2024-03-05 13:56:18 +01:00
Dain Nilsson bccf760c73
Fix docstring (close #207) 2024-03-05 12:47:43 +01:00
Dain Nilsson 4c6f7b68a2
Merge PR #213 2024-03-04 12:42:29 +01:00
Dain Nilsson 7a16dc4913
Bump upload-artifact Action 2024-03-04 12:37:49 +01:00
Dain Nilsson 8253b96f8e
Drop support for Python 3.7 (EOL) 2024-03-04 12:36:37 +01:00
Dain Nilsson 98a57ab968
Merge PR #211 2024-03-04 12:15:32 +01:00
Dain Nilsson 34e354e508
Bump actions versions 2024-03-04 12:09:06 +01:00
Dain Nilsson 1c3e05f82d
Bump pre-commit hooks 2024-03-04 12:03:18 +01:00
Dain Nilsson 2ce01289d8
Remove old codeql workflow, add dependabot checks for Actions 2024-03-04 11:01:27 +01:00
Dain Nilsson 7179b439d3
Update python versions 2024-03-04 11:01:01 +01:00
Dain Nilsson 54569a37cc
Update public suffix list 2024-03-04 10:57:56 +01:00
Dain Nilsson f36cade5e9
Bump server example dependencies 2024-03-04 10:56:50 +01:00
Dain Nilsson 9d8e8d167d
Deprecate websafe_decode of bytes 2024-03-04 09:58:09 +01:00
Dain Nilsson a7f3c51aca
Allow passing string value 2024-03-04 09:57:37 +01:00
Dain Nilsson a5357dd35c
Merge PR #204 2024-02-28 09:05:53 +01:00
Dain Nilsson a5bd97c529
Merge PR #206 2024-02-28 09:02:48 +01:00
Pol Henarejos 4abb4a577f
Fix packing sub_cmd.
sub_cmd is an unsigned char, since VENDOR_PROTOTYPE is 0xff. If not, it cannot be encoded.

Signed-off-by: Pol Henarejos <pol.henarejos@cttc.es>
2023-11-02 22:22:34 +01:00
Alexandre Detiste 526e1ab7a0 remove last reference to six
Signed-off-by: Alexandre Detiste <alexandre.detiste@gmail.com>
2023-10-19 00:54:41 +02:00
Dain Nilsson 032dd8b853
Fix formatting. 2023-08-22 15:33:04 +02:00
Dain Nilsson 8c9f3f0200
Merge PR #202. 2023-08-22 15:08:09 +02:00
Dain Nilsson aa3c5cd8e8
Merge PR #199. 2023-08-22 15:06:59 +02:00
Pol Henarejos a40850adaf
Added ES256K to supported algorithms.
Signed-off-by: Pol Henarejos <pol.henarejos@cttc.es>
2023-08-22 13:33:12 +02:00
Pol Henarejos c8fd18d4b2
Added support for ES256K algorithm with secp256k1 curve.
Signed-off-by: Pol Henarejos <pol.henarejos@cttc.es>
2023-08-22 13:31:41 +02:00
Pol Henarejos 2d7e9e1610
Fix returned curve for ES384 and ES512.
Signed-off-by: Pol Henarejos <pol.henarejos@cttc.es>
2023-08-22 13:17:15 +02:00
Pol Henarejos e82f231da9
Fix curve check for ES384 and ES512 verify().
Signed-off-by: Pol Henarejos <pol.henarejos@cttc.es>
2023-08-18 13:47:55 +02:00
Dain Nilsson 963eae041a
Bump version. 2023-07-06 16:03:59 +02:00
Dain Nilsson be2e8904e8
Prepare 1.1.2. 2023-07-06 16:00:58 +02:00
Dain Nilsson 8067e90f89
Update public suffix list. 2023-07-06 16:00:57 +02:00
Dain Nilsson d7304fa49f
Fix typing issues. 2023-07-06 15:39:02 +02:00
Dain Nilsson 5575d5838c
Update pre-commit hooks. 2023-07-06 12:34:06 +02:00
Dain Nilsson ed9f50a117
Bump dependencies. 2023-07-06 11:59:04 +02:00
Dain Nilsson f523839dab
Merge PR #194. 2023-07-06 11:22:56 +02:00
Dain Nilsson c7ebd878c0
Merge PR #193. 2023-07-03 10:06:01 +02:00
Dain Nilsson 2d6c067689
Handle authenticators with UV that do not support ClientPin. 2023-06-26 12:37:45 +02:00
Dain Nilsson cbe72665e1
Fix handling of error codes in authenticatorSelection (fix #184). 2023-06-26 12:22:36 +02:00
Dain Nilsson 1143d471ef
Skip TPM attestation test if SHA1 is unsupported. 2023-04-21 15:33:42 +02:00
Dain Nilsson 54cee2216a
Bump version. 2023-04-05 13:52:07 +02:00
Dain Nilsson e7eb53a73e
Prepare 1.1.1. 2023-04-05 13:42:26 +02:00
Dain Nilsson c7f09de1b6
Update NEWS. 2023-04-05 11:49:55 +02:00
Dain Nilsson 50d0306a2d
Merge PR #174. 2023-04-05 11:49:01 +02:00
Dain Nilsson fc46e83f7c
Fix pre-commit issues. 2023-04-05 11:48:07 +02:00
Taylor R Campbell fb6f77c5e4
Add NetBSD HID support.
fix https://github.com/Yubico/python-fido2/issues/173
2023-04-05 11:38:36 +02:00
Dain Nilsson 5dffcaa838
Update public suffix list. 2023-04-05 11:03:53 +02:00
Dain Nilsson 46c095fc21
Add poetry.lock to .gitignore. 2023-04-05 11:01:39 +02:00
Dain Nilsson 737fd76a27
Increase range for supported cryptography versions.
Also bump dependencies in server example.
2023-04-05 10:59:55 +02:00
Dain Nilsson d52024ebef
Merge PR #165. 2022-12-06 09:24:03 +01:00
Fabian Kaczmarczyck 9c980040da
Fixes largeBlob to be consistent with CTAP2 2022-12-06 09:22:10 +01:00
Dain Nilsson 75977a9468
Merge PR #164. 2022-11-22 15:15:31 +01:00
Dain Nilsson c6c3a68da0
Add Python 3.11 to build matrix. 2022-11-22 15:07:22 +01:00
Dain Nilsson bcefd244e6
Update dependencies for server sample. 2022-11-22 14:52:58 +01:00
Dain Nilsson 4d1782880c
Fix pre-commit issues. 2022-11-22 14:52:37 +01:00
Dain Nilsson 60a309eea0
Fix pre-commit after flake8 moved to github. 2022-11-22 14:39:03 +01:00
Dain Nilsson 8fdf098520
Merge PR #163. 2022-11-22 14:33:33 +01:00
Jon Janzen 589e2f1c4b
Remove print statement in webauthn json 2022-11-21 21:03:58 -06:00
Dain Nilsson 667ff5588b
Merge PR #117. 2022-10-17 17:12:15 +02:00
Dain Nilsson b915870e72
Bump version. 2022-10-17 16:09:12 +02:00
Dain Nilsson 08e1c45c93
Skip pre-commit on Py3.7 for now.
See: https://github.com/PyCQA/flake8/issues/1701
This hopefully gets resolved in flake8 and can be re-enabled.
2022-10-17 15:36:35 +02:00
Dain Nilsson d3ec6174ae
Update pre-commit. 2022-10-17 15:30:30 +02:00
Dain Nilsson cfd7f6b0c0
Prepare 1.1.0. 2022-10-17 12:42:39 +02:00
Dain Nilsson 71a317b12d
Update server example. 2022-10-17 12:42:16 +02:00
Dain Nilsson 026c6a7f8d
Set version. 2022-10-17 12:41:58 +02:00
Dain Nilsson 8c00d04945
Update NEWS. 2022-09-20 13:09:53 +02:00
Dain Nilsson e21341312c
Add new flags: Backup eligibility and state. 2022-09-20 13:09:52 +02:00
Dain Nilsson fabb844bce
Add "hybrid" Authenticator Transport. 2022-09-20 09:43:57 +02:00
Dain Nilsson 791ef6eba9
Merge PR #150. 2022-09-20 09:40:49 +02:00
Dain Nilsson 27cd3dda54
Remove old unused code in example. 2022-09-12 14:37:29 +02:00
Dain Nilsson cd9e6cbd59
Fix BioEnroll.is_supported and use it in example.
The wrong key was being checked, preventing Authenticators implementing
only the Prototype bioEnrollment command from being considered supported.
2022-08-31 12:43:41 +02:00
Dain Nilsson 103df1b456
Raise more descriptive error on failure to parse data classes. 2022-08-24 09:18:28 +02:00
Dain Nilsson ae048d06ff
Add license header to file. 2022-08-24 09:08:33 +02:00
Dain Nilsson 15ec60a3c4
Update NEWS. 2022-08-10 16:11:19 +02:00
Dain Nilsson 4a07a4d004
Update server example to use JSON serialization. 2022-08-10 16:10:53 +02:00
Dain Nilsson 354672b9ce
Provide better support in Fido2Server for (de-)serializing JSON.
This allows the "options" returned by register_begin/authenticate_begin
to more easily be serialized to JSON, as well as supporting responses
deserialized from JSON to register_complete/authenticate_complete.

See: #146.
2022-08-10 15:45:39 +02:00
Dain Nilsson 709599f98c
Update tests for webauthn_json_mapping. 2022-08-10 15:43:58 +02:00
Dain Nilsson 8debc41942
Add support for WebAuthn data class JSON serialization.
See https://github.com/w3c/webauthn/issues/1683 for details.
2022-08-10 15:41:27 +02:00
Dain Nilsson f7e8c59649
Fix incorrect type hints in mds3. 2022-08-10 15:39:32 +02:00
Dain Nilsson fa5e9fcce5
More strict type checking of data class fields. 2022-08-10 15:38:46 +02:00
Dain Nilsson 19bc5ce15a
Clean up annotations. 2022-08-09 17:39:39 +02:00
Dain Nilsson b674c2b5ac
Fix crossOrigin case in CollectedClientData. 2022-06-27 15:13:41 +02:00
Dain Nilsson 5e40339a7c
Merge PR #143. 2022-06-27 14:28:54 +02:00
Michael Gmelin e1050b575b
Fix typo in comment 2022-06-27 14:28:39 +02:00
Dain Nilsson d8542e82de
Merge PR #142. 2022-06-27 14:07:40 +02:00
Markus Meissner 35db7f2f5c
adding py.typed to enable mypy, if used as library 2022-06-27 11:11:57 +02:00
Dain Nilsson b546c0d629
Bump version. 2022-06-08 09:59:01 +02:00
Jonathan Morrison 1769dc1982
Reference Yubico Linux Instructions 2021-05-11 11:45:16 -06:00
59 changed files with 4984 additions and 3952 deletions

10
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
groups:
github-actions:
patterns:
- "*"

View File

@ -9,7 +9,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python: ['3.7', '3.8', '3.9', '3.10', 'pypy3']
python: ['3.8', '3.9', '3.10', '3.11', '3.12', 'pypy3.9']
architecture: [x86, x64]
exclude:
- os: ubuntu-latest
@ -23,10 +23,10 @@ jobs:
name: ${{ matrix.os }} Py ${{ matrix.python }} ${{ matrix.architecture }}
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v1
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
architecture: ${{ matrix.architecture }}
@ -37,7 +37,7 @@ jobs:
poetry update
- name: Run pre-commit
if: "!startsWith(matrix.python, 'pypy')"
if: "!startsWith(matrix.python, 'pypy') && !startsWith(matrix.python, '3.7')"
run: |
python -m pip install pre-commit
pre-commit run --all-files
@ -51,10 +51,10 @@ jobs:
name: Build Python source .tar.gz
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v1
uses: actions/setup-python@v5
with:
python-version: 3.x
@ -64,7 +64,7 @@ jobs:
poetry build
- name: Upload source package
uses: actions/upload-artifact@v1
uses: actions/upload-artifact@v4
with:
name: fido2-python-sdist
path: dist

View File

@ -1,35 +0,0 @@
name: "Code scanning - action"
on:
push:
pull_request:
schedule:
- cron: '0 12 * * 4'
jobs:
CodeQL-Build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
- name: Autobuild
uses: github/codeql-action/autobuild@v1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

1
.gitignore vendored
View File

@ -8,6 +8,7 @@ dist/
.ropeproject/
ChangeLog
man/*.1
poetry.lock
# Unit test / coverage reports
htmlcov/

View File

@ -1,19 +1,19 @@
repos:
- repo: https://gitlab.com/pycqa/flake8
rev: 3.9.2
- repo: https://github.com/PyCQA/flake8
rev: 7.0.0
hooks:
- id: flake8
- repo: https://github.com/psf/black
rev: 22.3.0
rev: 24.2.0
hooks:
- id: black
- repo: https://github.com/PyCQA/bandit
rev: 1.7.4
rev: 1.7.8
hooks:
- id: bandit
exclude: ^tests/
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.942
rev: v1.9.0
hooks:
- id: mypy
files: ^fido2/

33
NEWS
View File

@ -1,3 +1,36 @@
* Version 1.1.3 (released 2024-03-13)
** Fix USB HID issue on MacOS that sometimes caused a pause while waiting for a
timeout.
** Fix argument to CredProp extension where an enum value was required instead of
also allowing a string.
** Fix parsing of some key types (ES384, ES512) causing signature verification to fail.
** Deprecation: Calling websafe_decode with a bytes argument instead of str.
This will raise a TypeError in the next major version of the library.
* Version 1.1.2 (released 2023-07-06)
** Fix ClientPin usage for Authenticators that do not support passing a PIN.
** Fix: Handle correct CTAP response codes in authenticatorSelection.
* Version 1.1.1 (released 2023-04-05)
** Add community provided support for NetBSD.
** Bugfix: Don't set length for largeBlob when offset is 0.
** Bugfix: Remove print statement in webauthn parsing.
* Version 1.1.0 (released 2022-10-17)
** Bugfix: Fix name of "crossOrigin" in CollectedClientData.create().
** Bugfix: Some incorrect type hints in the MDS3 classes were fixed.
** Stricter checking of dataclass field types.
** Add support for JSON-serialization of WebAuthn data classes.
This changes the objects dict representation to align with new additions in the
WebAuthn specification. As this may break compatibility, the new behavior
requires explicit opt-in until python-fido2 2.0 is released.
** Update server example to use JSON serialization.
** Server: Add support for passing RegistrationResponse/AuthenticationResponse (or
their deserialized JSON data) to register_complete/authenticate_complete.
** Add new "hybrid" AuthenticatorTransport.
** Add new AuthenticatorData flags, and use 2-letter names as in the WebAuthn spec
(long names are still available as aliases).
* Version 1.0.0 (released 2022-06-08)
** First stable release.

View File

@ -41,12 +41,13 @@ or the _COPYING.MPLv2_ file for the full license text.
=== Requirements
fido2 is compatible with Python 3.7 and later, and is tested on Windows,
MacOS, and Linux. Support for OpenBSD and FreeBSD is provided as-is and relies
on community contributions.
fido2 is compatible with Python 3.7 and later, and is tested on Windows, MacOS,
and Linux. Support for OpenBSD, FreeBSD, and NetBSD is provided as-is and
relies on community contributions.
=== Installation
fido2 is installable by running the following command:
pip install fido2
@ -61,6 +62,7 @@ requires running as Administrator. This library can still be used when running
as non-administrator, via the `fido.client.WindowsClient` class. An example of
this is included in the file `examples/credential.py`.
Under Linux you will need to add a Udev rule to be able to access the FIDO
device, or run as root. For example, the Udev rule may contain the following:
@ -71,12 +73,15 @@ KERNEL=="hidraw*", SUBSYSTEM=="hidraw", \
MODE="0664", GROUP="plugdev", ATTRS{idVendor}=="1050"
----
There may be a package already available for your distribution that does this
for you, see:
https://support.yubico.com/hc/en-us/articles/360013708900-Using-Your-U2F-YubiKey-with-Linux
Under FreeBSD you will either need to run as root or add rules for your device
to /etc/devd.conf, which can be automated by installing security/u2f-devd:
# pkg install u2f-devd
==== Dependencies
This project depends on Cryptography. For instructions on installing this
dependency, see https://cryptography.io/en/latest/installation/.

View File

@ -33,15 +33,20 @@
* Merge and delete the release branch, and push the tag:
$ git checkout master
$ git checkout main
$ git merge --ff release/x.y.z
$ git branch -d release/x.y.z
$ git push && git push --tags
$ git push origin :release/x.y.z
* Bump the version number by incrementing the PATCH version and appending -dev.0
in fido2/__init__.py and add a new entry (unreleased) to the NEWS file.
in pyproject.toml and fido2/__init__.py and add a new entry (unreleased) to the
NEWS file.
# pyproject.toml:
version = "x.y.q-dev.0"
# fido2/__init__.py:
__version__ = 'x.y.q-dev.0'
* Commit and push the change:

View File

@ -34,7 +34,6 @@ www.acs.com.hk/en/driver/100/acr122u-nfc-reader-with-sam-slot-proprietary/
"""
import time
import six
from fido2.utils import sha256
from fido2.ctap1 import CTAP1
@ -78,7 +77,7 @@ class Acr122uSamPcscDevice(CtapPcscDevice):
"""
# print('>> %s' % b2a_hex(apdu))
resp, sw1, sw2 = self._conn.transmit(list(six.iterbytes(apdu)), protocol)
resp, sw1, sw2 = self._conn.transmit(list(iter(apdu)), protocol)
response = bytes(bytearray(resp))
# print('<< [0x%04x] %s' % (sw1 * 0x100 + sw2, b2a_hex(response)))

View File

@ -35,6 +35,7 @@ Consider this highly experimental.
from fido2.hid import CtapHidDevice
from fido2.ctap2 import Ctap2, FPBioEnrollment, CaptureError
from fido2.ctap2.pin import ClientPin
from fido2.ctap2.bio import BioEnrollment
from getpass import getpass
import sys
@ -44,7 +45,7 @@ uv = "discouraged"
for dev in CtapHidDevice.list_devices():
try:
ctap = Ctap2(dev)
if "bioEnroll" in ctap.info.options:
if BioEnrollment.is_supported(ctap.info):
break
except Exception: # nosec
continue

View File

@ -56,8 +56,9 @@ class CliInteraction(UserInteraction):
return True
cli_interaction = CliInteraction()
clients = [
Fido2Client(d, "https://example.com", user_interaction=CliInteraction())
Fido2Client(d, "https://example.com", user_interaction=cli_interaction)
for d in devs
]

View File

@ -17,11 +17,13 @@ Once the environment has been created, you can run the server by running:
$ poetry run server
When the server is running, use a browser supporting WebAuthn and open
https://localhost:5000 to access the website.
http://localhost:5000 to access the website.
NOTE: As this server uses a self-signed certificate, you will get warnings in
your browser about the connection not being secure. This is expected, and you
can safely proceed to the site.
NOTE: Webauthn requires a secure context (HTTPS), which involves
obtaining a valid TLS certificate. However, most browsers also treat
http://localhost as a secure context. This example runs without TLS
as a demo, but otherwise you should always use HTTPS with a valid
certificate when using Webauthn.
=== Using the website
The site allows you to register a WebAuthn credential, and to authenticate it.

View File

@ -1,64 +1,170 @@
# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
[[package]]
name = "blinker"
version = "1.7.0"
description = "Fast, simple object-to-object and broadcast signaling"
optional = false
python-versions = ">=3.8"
files = [
{file = "blinker-1.7.0-py3-none-any.whl", hash = "sha256:c3f865d4d54db7abc53758a01601cf343fe55b84c1de4e3fa910e420b438d5b9"},
{file = "blinker-1.7.0.tar.gz", hash = "sha256:e6820ff6fa4e4d1d8e2747c2283749c3f547e4fee112b98555cdcdae32996182"},
]
[[package]]
name = "cffi"
version = "1.15.0"
version = "1.16.0"
description = "Foreign Function Interface for Python calling C code."
category = "main"
optional = false
python-versions = "*"
python-versions = ">=3.8"
files = [
{file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"},
{file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"},
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"},
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"},
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"},
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"},
{file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"},
{file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"},
{file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"},
{file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"},
{file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"},
{file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"},
{file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"},
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"},
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"},
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"},
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"},
{file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"},
{file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"},
{file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"},
{file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"},
{file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"},
{file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"},
{file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"},
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"},
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"},
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"},
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"},
{file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"},
{file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"},
{file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"},
{file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"},
{file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"},
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"},
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"},
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"},
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"},
{file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"},
{file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"},
{file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"},
{file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"},
{file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"},
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"},
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"},
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"},
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"},
{file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"},
{file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"},
{file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"},
{file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"},
{file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"},
{file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"},
]
[package.dependencies]
pycparser = "*"
[[package]]
name = "click"
version = "8.1.3"
version = "8.1.7"
description = "Composable command line interface toolkit"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
[[package]]
name = "colorama"
version = "0.4.4"
version = "0.4.6"
description = "Cross-platform colored terminal text."
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "cryptography"
version = "37.0.2"
version = "42.0.5"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
category = "main"
optional = false
python-versions = ">=3.6"
python-versions = ">=3.7"
files = [
{file = "cryptography-42.0.5-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16"},
{file = "cryptography-42.0.5-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec"},
{file = "cryptography-42.0.5-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb"},
{file = "cryptography-42.0.5-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4"},
{file = "cryptography-42.0.5-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278"},
{file = "cryptography-42.0.5-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7"},
{file = "cryptography-42.0.5-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee"},
{file = "cryptography-42.0.5-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1"},
{file = "cryptography-42.0.5-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d"},
{file = "cryptography-42.0.5-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da"},
{file = "cryptography-42.0.5-cp37-abi3-win32.whl", hash = "sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74"},
{file = "cryptography-42.0.5-cp37-abi3-win_amd64.whl", hash = "sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940"},
{file = "cryptography-42.0.5-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8"},
{file = "cryptography-42.0.5-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1"},
{file = "cryptography-42.0.5-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e"},
{file = "cryptography-42.0.5-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc"},
{file = "cryptography-42.0.5-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a"},
{file = "cryptography-42.0.5-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7"},
{file = "cryptography-42.0.5-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922"},
{file = "cryptography-42.0.5-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc"},
{file = "cryptography-42.0.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30"},
{file = "cryptography-42.0.5-cp39-abi3-win32.whl", hash = "sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413"},
{file = "cryptography-42.0.5-cp39-abi3-win_amd64.whl", hash = "sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400"},
{file = "cryptography-42.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8"},
{file = "cryptography-42.0.5-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2"},
{file = "cryptography-42.0.5-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c"},
{file = "cryptography-42.0.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576"},
{file = "cryptography-42.0.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6"},
{file = "cryptography-42.0.5-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e"},
{file = "cryptography-42.0.5-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac"},
{file = "cryptography-42.0.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd"},
{file = "cryptography-42.0.5.tar.gz", hash = "sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1"},
]
[package.dependencies]
cffi = ">=1.12"
cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
[package.extras]
docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"]
docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"]
pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"]
sdist = ["setuptools_rust (>=0.11.4)"]
docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"]
docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"]
nox = ["nox"]
pep8test = ["check-sdist", "click", "mypy", "ruff"]
sdist = ["build"]
ssh = ["bcrypt (>=3.1.5)"]
test = ["pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"]
test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
test-randomorder = ["pytest-randomly"]
[[package]]
name = "fido2"
version = "1.0.0"
version = "1.1.3-dev.0"
description = "FIDO2/WebAuthn library for implementing clients and servers."
category = "main"
optional = false
python-versions = "^3.7"
python-versions = "^3.8"
files = []
develop = false
[package.dependencies]
cryptography = ">=2.6, !=35, <40"
cryptography = ">=2.6, !=35, <45"
[package.extras]
pcsc = ["pyscard (>=1.9,<3)"]
@ -69,18 +175,22 @@ url = "../.."
[[package]]
name = "flask"
version = "2.1.2"
version = "2.3.3"
description = "A simple framework for building complex web applications."
category = "main"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.8"
files = [
{file = "flask-2.3.3-py3-none-any.whl", hash = "sha256:f69fcd559dc907ed196ab9df0e48471709175e696d6e698dd4dbe940f96ce66b"},
{file = "flask-2.3.3.tar.gz", hash = "sha256:09c347a92aa7ff4a8e7f3206795f30d826654baf38b873d0744cd571ca609efc"},
]
[package.dependencies]
click = ">=8.0"
blinker = ">=1.6.2"
click = ">=8.1.3"
importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""}
itsdangerous = ">=2.0"
Jinja2 = ">=3.0"
Werkzeug = ">=2.0"
itsdangerous = ">=2.1.2"
Jinja2 = ">=3.1.2"
Werkzeug = ">=2.3.7"
[package.extras]
async = ["asgiref (>=3.2)"]
@ -88,36 +198,44 @@ dotenv = ["python-dotenv"]
[[package]]
name = "importlib-metadata"
version = "4.11.4"
version = "7.0.2"
description = "Read metadata from Python packages"
category = "main"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.8"
files = [
{file = "importlib_metadata-7.0.2-py3-none-any.whl", hash = "sha256:f4bc4c0c070c490abf4ce96d715f68e95923320370efb66143df00199bb6c100"},
{file = "importlib_metadata-7.0.2.tar.gz", hash = "sha256:198f568f3230878cb1b44fbd7975f87906c22336dba2e4a7f05278c281fbd792"},
]
[package.dependencies]
typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
zipp = ">=0.5"
[package.extras]
docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"]
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
perf = ["ipython"]
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"]
testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"]
[[package]]
name = "itsdangerous"
version = "2.1.2"
description = "Safely pass data to untrusted environments and back."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"},
{file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"},
]
[[package]]
name = "jinja2"
version = "3.1.2"
version = "3.1.3"
description = "A very fast and expressive template engine."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"},
{file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"},
]
[package.dependencies]
MarkupSafe = ">=2.0"
@ -127,213 +245,117 @@ i18n = ["Babel (>=2.7)"]
[[package]]
name = "markupsafe"
version = "2.1.1"
version = "2.1.5"
description = "Safely add untrusted strings to HTML/XML markup."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
{file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"},
{file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"},
{file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"},
{file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"},
{file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"},
{file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"},
{file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"},
{file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"},
{file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"},
{file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"},
{file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"},
{file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"},
{file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"},
{file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"},
{file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"},
{file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"},
{file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"},
{file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"},
{file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"},
{file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"},
{file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"},
{file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"},
{file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"},
{file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"},
{file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"},
{file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"},
{file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"},
{file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"},
{file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"},
{file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"},
{file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"},
{file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"},
{file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"},
{file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"},
{file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"},
{file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"},
{file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"},
{file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"},
{file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"},
{file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"},
{file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"},
{file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"},
{file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"},
{file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"},
{file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"},
{file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"},
{file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"},
{file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"},
{file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"},
{file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"},
{file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"},
{file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"},
{file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"},
{file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"},
{file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"},
{file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"},
{file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"},
{file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"},
{file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"},
{file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"},
]
[[package]]
name = "pycparser"
version = "2.21"
description = "C parser in Python"
category = "main"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
[[package]]
name = "typing-extensions"
version = "4.2.0"
description = "Backported and Experimental Type Hints for Python 3.7+"
category = "main"
optional = false
python-versions = ">=3.7"
[[package]]
name = "werkzeug"
version = "2.1.2"
description = "The comprehensive WSGI web application library."
category = "main"
optional = false
python-versions = ">=3.7"
[package.extras]
watchdog = ["watchdog"]
[[package]]
name = "zipp"
version = "3.8.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
category = "main"
optional = false
python-versions = ">=3.7"
[package.extras]
docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"]
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"]
[metadata]
lock-version = "1.1"
python-versions = "^3.7"
content-hash = "d21c95e6be38b286b3aa30ae191c220faf68523b4fcfddc46e81c694952d7f73"
[metadata.files]
cffi = [
{file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"},
{file = "cffi-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0"},
{file = "cffi-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14"},
{file = "cffi-1.15.0-cp27-cp27m-win32.whl", hash = "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474"},
{file = "cffi-1.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6"},
{file = "cffi-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27"},
{file = "cffi-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023"},
{file = "cffi-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2"},
{file = "cffi-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e"},
{file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7"},
{file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3"},
{file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c"},
{file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962"},
{file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382"},
{file = "cffi-1.15.0-cp310-cp310-win32.whl", hash = "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55"},
{file = "cffi-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0"},
{file = "cffi-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e"},
{file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39"},
{file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc"},
{file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032"},
{file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8"},
{file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605"},
{file = "cffi-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e"},
{file = "cffi-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc"},
{file = "cffi-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636"},
{file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4"},
{file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997"},
{file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b"},
{file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2"},
{file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7"},
{file = "cffi-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66"},
{file = "cffi-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029"},
{file = "cffi-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880"},
{file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20"},
{file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024"},
{file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e"},
{file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728"},
{file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6"},
{file = "cffi-1.15.0-cp38-cp38-win32.whl", hash = "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c"},
{file = "cffi-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443"},
{file = "cffi-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a"},
{file = "cffi-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37"},
{file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a"},
{file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e"},
{file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"},
{file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df"},
{file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8"},
{file = "cffi-1.15.0-cp39-cp39-win32.whl", hash = "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a"},
{file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"},
{file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"},
]
click = [
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
]
colorama = [
{file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
{file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
]
cryptography = [
{file = "cryptography-37.0.2-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:ef15c2df7656763b4ff20a9bc4381d8352e6640cfeb95c2972c38ef508e75181"},
{file = "cryptography-37.0.2-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:3c81599befb4d4f3d7648ed3217e00d21a9341a9a688ecdd615ff72ffbed7336"},
{file = "cryptography-37.0.2-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2bd1096476aaac820426239ab534b636c77d71af66c547b9ddcd76eb9c79e004"},
{file = "cryptography-37.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:31fe38d14d2e5f787e0aecef831457da6cec68e0bb09a35835b0b44ae8b988fe"},
{file = "cryptography-37.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:093cb351031656d3ee2f4fa1be579a8c69c754cf874206be1d4cf3b542042804"},
{file = "cryptography-37.0.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59b281eab51e1b6b6afa525af2bd93c16d49358404f814fe2c2410058623928c"},
{file = "cryptography-37.0.2-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:0cc20f655157d4cfc7bada909dc5cc228211b075ba8407c46467f63597c78178"},
{file = "cryptography-37.0.2-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:f8ec91983e638a9bcd75b39f1396e5c0dc2330cbd9ce4accefe68717e6779e0a"},
{file = "cryptography-37.0.2-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:46f4c544f6557a2fefa7ac8ac7d1b17bf9b647bd20b16decc8fbcab7117fbc15"},
{file = "cryptography-37.0.2-cp36-abi3-win32.whl", hash = "sha256:731c8abd27693323b348518ed0e0705713a36d79fdbd969ad968fbef0979a7e0"},
{file = "cryptography-37.0.2-cp36-abi3-win_amd64.whl", hash = "sha256:471e0d70201c069f74c837983189949aa0d24bb2d751b57e26e3761f2f782b8d"},
{file = "cryptography-37.0.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a68254dd88021f24a68b613d8c51d5c5e74d735878b9e32cc0adf19d1f10aaf9"},
{file = "cryptography-37.0.2-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:a7d5137e556cc0ea418dca6186deabe9129cee318618eb1ffecbd35bee55ddc1"},
{file = "cryptography-37.0.2-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aeaba7b5e756ea52c8861c133c596afe93dd716cbcacae23b80bc238202dc023"},
{file = "cryptography-37.0.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95e590dd70642eb2079d280420a888190aa040ad20f19ec8c6e097e38aa29e06"},
{file = "cryptography-37.0.2-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:1b9362d34363f2c71b7853f6251219298124aa4cc2075ae2932e64c91a3e2717"},
{file = "cryptography-37.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e53258e69874a306fcecb88b7534d61820db8a98655662a3dd2ec7f1afd9132f"},
{file = "cryptography-37.0.2-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:1f3bfbd611db5cb58ca82f3deb35e83af34bb8cf06043fa61500157d50a70982"},
{file = "cryptography-37.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:419c57d7b63f5ec38b1199a9521d77d7d1754eb97827bbb773162073ccd8c8d4"},
{file = "cryptography-37.0.2-pp39-pypy39_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:dc26bb134452081859aa21d4990474ddb7e863aa39e60d1592800a8865a702de"},
{file = "cryptography-37.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:3b8398b3d0efc420e777c40c16764d6870bcef2eb383df9c6dbb9ffe12c64452"},
{file = "cryptography-37.0.2.tar.gz", hash = "sha256:f224ad253cc9cea7568f49077007d2263efa57396a2f2f78114066fd54b5c68e"},
]
fido2 = []
flask = [
{file = "Flask-2.1.2-py3-none-any.whl", hash = "sha256:fad5b446feb0d6db6aec0c3184d16a8c1f6c3e464b511649c8918a9be100b4fe"},
{file = "Flask-2.1.2.tar.gz", hash = "sha256:315ded2ddf8a6281567edb27393010fe3406188bafbfe65a3339d5787d89e477"},
]
importlib-metadata = [
{file = "importlib_metadata-4.11.4-py3-none-any.whl", hash = "sha256:c58c8eb8a762858f49e18436ff552e83914778e50e9d2f1660535ffb364552ec"},
{file = "importlib_metadata-4.11.4.tar.gz", hash = "sha256:5d26852efe48c0a32b0509ffbc583fda1a2266545a78d104a6f4aff3db17d700"},
]
itsdangerous = [
{file = "itsdangerous-2.1.2-py3-none-any.whl", hash = "sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44"},
{file = "itsdangerous-2.1.2.tar.gz", hash = "sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"},
]
jinja2 = [
{file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"},
{file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"},
]
markupsafe = [
{file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"},
{file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"},
{file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"},
{file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"},
{file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"},
{file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"},
{file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"},
{file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"},
{file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"},
{file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"},
{file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"},
{file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"},
{file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"},
{file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"},
{file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"},
{file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"},
{file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"},
{file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"},
{file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"},
{file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"},
{file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"},
{file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"},
{file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"},
{file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"},
{file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"},
{file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"},
{file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"},
{file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"},
{file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"},
{file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"},
{file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"},
{file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"},
]
pycparser = [
files = [
{file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
{file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
]
typing-extensions = [
{file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"},
{file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"},
[[package]]
name = "werkzeug"
version = "3.0.1"
description = "The comprehensive WSGI web application library."
optional = false
python-versions = ">=3.8"
files = [
{file = "werkzeug-3.0.1-py3-none-any.whl", hash = "sha256:90a285dc0e42ad56b34e696398b8122ee4c681833fb35b8334a095d82c56da10"},
{file = "werkzeug-3.0.1.tar.gz", hash = "sha256:507e811ecea72b18a404947aded4b3390e1db8f826b494d76550ef45bb3b1dcc"},
]
werkzeug = [
{file = "Werkzeug-2.1.2-py3-none-any.whl", hash = "sha256:72a4b735692dd3135217911cbeaa1be5fa3f62bffb8745c5215420a03dc55255"},
{file = "Werkzeug-2.1.2.tar.gz", hash = "sha256:1ce08e8093ed67d638d63879fd1ba3735817f7a80de3674d293f5984f25fb6e6"},
]
zipp = [
{file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"},
{file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"},
[package.dependencies]
MarkupSafe = ">=2.1.1"
[package.extras]
watchdog = ["watchdog (>=2.3)"]
[[package]]
name = "zipp"
version = "3.17.0"
description = "Backport of pathlib-compatible object wrapper for zip files"
optional = false
python-versions = ">=3.8"
files = [
{file = "zipp-3.17.0-py3-none-any.whl", hash = "sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31"},
{file = "zipp-3.17.0.tar.gz", hash = "sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0"},
]
[package.extras]
docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy (>=0.9.1)", "pytest-ruff"]
[metadata]
lock-version = "2.0"
python-versions = "^3.8"
content-hash = "2fce33bd11a195af8dd3d95f62169819676ed4bba09e10863a6e61caa33a74a8"

View File

@ -9,7 +9,7 @@ packages = [
]
[tool.poetry.dependencies]
python = "^3.7"
python = "^3.8"
Flask = "^2.0"
fido2 = {path = "../.."}
@ -21,4 +21,3 @@ build-backend = "poetry.core.masonry.api"
[tool.poetry.scripts]
server = "server.server:main"
server-u2f = "server.server_u2f:main"

View File

@ -31,19 +31,16 @@ to register and use a credential.
See the file README.adoc in this directory for details.
Navigate to https://localhost:5000 in a supported web browser.
Navigate to http://localhost:5000 in a supported web browser.
"""
from fido2.webauthn import (
CollectedClientData,
PublicKeyCredentialRpEntity,
AttestationObject,
AuthenticatorData,
)
from fido2.webauthn import PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity
from fido2.server import Fido2Server
from fido2 import cbor
from flask import Flask, session, request, redirect, abort
from flask import Flask, session, request, redirect, abort, jsonify
import os
import fido2.features
fido2.features.webauthn_json_mapping.enabled = True
app = Flask(__name__, static_url_path="")
@ -65,12 +62,12 @@ def index():
@app.route("/api/register/begin", methods=["POST"])
def register_begin():
registration_data, state = server.register_begin(
{
"id": b"user_id",
"name": "a_user",
"displayName": "A. User",
},
options, state = server.register_begin(
PublicKeyCredentialUserEntity(
id=b"user_id",
name="a_user",
display_name="A. User",
),
credentials,
user_verification="discouraged",
authenticator_attachment="cross-platform",
@ -78,24 +75,21 @@ def register_begin():
session["state"] = state
print("\n\n\n\n")
print(registration_data)
print(options)
print("\n\n\n\n")
return cbor.encode(registration_data)
return jsonify(dict(options))
@app.route("/api/register/complete", methods=["POST"])
def register_complete():
data = cbor.decode(request.get_data())
client_data = CollectedClientData(data["clientDataJSON"])
att_obj = AttestationObject(data["attestationObject"])
print("clientData", client_data)
print("AttestationObject:", att_obj)
auth_data = server.register_complete(session["state"], client_data, att_obj)
response = request.json
print("RegistrationResponse:", response)
auth_data = server.register_complete(session["state"], response)
credentials.append(auth_data.credential_data)
print("REGISTERED CREDENTIAL:", auth_data.credential_data)
return cbor.encode({"status": "OK"})
return jsonify({"status": "OK"})
@app.route("/api/authenticate/begin", methods=["POST"])
@ -103,9 +97,10 @@ def authenticate_begin():
if not credentials:
abort(404)
auth_data, state = server.authenticate_begin(credentials)
options, state = server.authenticate_begin(credentials)
session["state"] = state
return cbor.encode(auth_data)
return jsonify(dict(options))
@app.route("/api/authenticate/complete", methods=["POST"])
@ -113,29 +108,23 @@ def authenticate_complete():
if not credentials:
abort(404)
data = cbor.decode(request.get_data())
credential_id = data["credentialId"]
client_data = CollectedClientData(data["clientDataJSON"])
auth_data = AuthenticatorData(data["authenticatorData"])
signature = data["signature"]
print("clientData", client_data)
print("AuthenticatorData", auth_data)
response = request.json
print("AuthenticationResponse:", response)
server.authenticate_complete(
session.pop("state"),
credentials,
credential_id,
client_data,
auth_data,
signature,
response,
)
print("ASSERTION OK")
return cbor.encode({"status": "OK"})
return jsonify({"status": "OK"})
def main():
print(__doc__)
app.run(ssl_context="adhoc", debug=False)
# Note: using localhost without TLS, as some browsers do
# not allow Webauthn in case of TLS certificate errors.
# See https://lists.w3.org/Archives/Public/public-webauthn/2022Nov/0135.html
app.run(host="localhost", debug=False)
if __name__ == "__main__":

View File

@ -1,186 +0,0 @@
# Copyright (c) 2018 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.
"""
Example demo server to use a supported web browser to call the WebAuthn APIs
to register and use a credential.
See the file README.adoc in this directory for details.
Navigate to https://localhost:5000 in a supported web browser.
"""
from fido2.webauthn import (
CollectedClientData,
PublicKeyCredentialRpEntity,
AttestationObject,
AuthenticatorData,
)
from fido2.server import U2FFido2Server
from fido2.ctap1 import RegistrationData
from fido2.utils import sha256, websafe_encode, websafe_decode
from fido2 import cbor
from flask import Flask, session, request, redirect, abort
import os
app = Flask(__name__, static_url_path="")
app.secret_key = os.urandom(32) # Used for session.
rp = PublicKeyCredentialRpEntity(name="Demo server", id="localhost")
# By using the U2FFido2Server class, we can support existing credentials
# registered by the legacy u2f.register API for an appId.
server = U2FFido2Server("https://localhost:5000", rp)
# Registered credentials are stored globally, in memory only. Single user
# support, state is lost when the server terminates.
credentials = []
@app.route("/")
def index():
return redirect("/index-u2f.html")
@app.route("/api/register/begin", methods=["POST"])
def register_begin():
registration_data, state = server.register_begin(
{
"id": b"user_id",
"name": "a_user",
"displayName": "A. User",
},
credentials,
)
session["state"] = state
print("\n\n\n\n")
print(registration_data)
print("\n\n\n\n")
return cbor.encode(registration_data)
@app.route("/api/register/complete", methods=["POST"])
def register_complete():
data = cbor.decode(request.get_data())
client_data = CollectedClientData(data["clientDataJSON"])
att_obj = AttestationObject(data["attestationObject"])
print("clientData", client_data)
print("AttestationObject:", att_obj)
auth_data = server.register_complete(session["state"], client_data, att_obj)
credentials.append(auth_data.credential_data)
print("REGISTERED CREDENTIAL:", auth_data.credential_data)
return cbor.encode({"status": "OK"})
@app.route("/api/authenticate/begin", methods=["POST"])
def authenticate_begin():
if not credentials:
abort(404)
auth_data, state = server.authenticate_begin(credentials)
session["state"] = state
return cbor.encode(auth_data)
@app.route("/api/authenticate/complete", methods=["POST"])
def authenticate_complete():
if not credentials:
abort(404)
data = cbor.decode(request.get_data())
credential_id = data["credentialId"]
client_data = CollectedClientData(data["clientDataJSON"])
auth_data = AuthenticatorData(data["authenticatorData"])
signature = data["signature"]
print("clientData", client_data)
print("AuthenticatorData", auth_data)
server.authenticate_complete(
session.pop("state"),
credentials,
credential_id,
client_data,
auth_data,
signature,
)
print("ASSERTION OK")
return cbor.encode({"status": "OK"})
###############################################################################
# WARNING!
#
# The below functions allow the registration of legacy U2F credentials.
# This is provided FOR TESTING PURPOSES ONLY. New credentials should be
# registered using the WebAuthn APIs.
###############################################################################
@app.route("/api/u2f/begin", methods=["POST"])
def u2f_begin():
registration_data, state = server.register_begin(
{
"id": b"user_id",
"name": "a_user",
"displayName": "A. User",
},
credentials,
)
session["state"] = state
print("\n\n\n\n")
print(registration_data)
print("\n\n\n\n")
return cbor.encode(websafe_encode(registration_data["publicKey"]["challenge"]))
@app.route("/api/u2f/complete", methods=["POST"])
def u2f_complete():
data = cbor.decode(request.get_data())
reg_data = RegistrationData.from_b64(data["registrationData"])
print("clientData", websafe_decode(data["clientData"]))
print("U2F RegistrationData:", reg_data)
att_obj = AttestationObject.from_ctap1(sha256(b"https://localhost:5000"), reg_data)
print("AttestationObject:", att_obj)
auth_data = att_obj.auth_data
credentials.append(auth_data.credential_data)
print("REGISTERED U2F CREDENTIAL:", auth_data.credential_data)
return cbor.encode({"status": "OK"})
def main():
print(__doc__)
app.run(ssl_context="adhoc", debug=False)
if __name__ == "__main__":
main()

View File

@ -1,7 +1,40 @@
<html>
<head>
<title>Fido 2.0 webauthn demo</title>
<script src="/cbor.js"></script>
<script type="module">
import {
get,
parseRequestOptionsFromJSON,
} from '/webauthn-json.browser-ponyfill.js';
async function start() {
let request = await fetch('/api/authenticate/begin', {
method: 'POST',
});
if(!request.ok) {
throw new Error('No credential available to authenticate!');
}
let json = await request.json();
let options = parseRequestOptionsFromJSON(json);
let response = await get(options);
let result = await fetch('/api/authenticate/complete', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(response),
});
let stat = result.ok ? 'successful' : 'unsuccessful';
alert('Authentication ' + stat + ' More details in server log...');
window.location = '/';
}
window.start = start;
</script>
<style>
body { font-family: sans-serif; line-height: 1.5em; padding: 2em 10em; }
h1, h2 { color: #325F74; }
@ -23,36 +56,5 @@
<a href="/">Cancel</a>
</div>
<script>
function start() {
fetch('/api/authenticate/begin', {
method: 'POST',
}).then(function(response) {
if(response.ok) return response.arrayBuffer();
throw new Error('No credential available to authenticate!');
}).then(CBOR.decode).then(function(options) {
return navigator.credentials.get(options);
}).then(function(assertion) {
return fetch('/api/authenticate/complete', {
method: 'POST',
headers: {'Content-Type': 'application/cbor'},
body: CBOR.encode({
"credentialId": new Uint8Array(assertion.rawId),
"authenticatorData": new Uint8Array(assertion.response.authenticatorData),
"clientDataJSON": new Uint8Array(assertion.response.clientDataJSON),
"signature": new Uint8Array(assertion.response.signature)
})
})
}).then(function(response) {
var stat = response.ok ? 'successful' : 'unsuccessful';
alert('Authentication ' + stat + ' More details in server log...');
}, function(reason) {
alert(reason);
}).then(function() {
window.location = '/';
});
}
</script>
</body>
</html>

View File

@ -1,406 +0,0 @@
/*
* The MIT License (MIT)
*
* Copyright (c) 2014-2016 Patrick Gansterer <paroga@paroga.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
(function(global, undefined) { "use strict";
var POW_2_24 = 5.960464477539063e-8,
POW_2_32 = 4294967296,
POW_2_53 = 9007199254740992;
function encode(value) {
var data = new ArrayBuffer(256);
var dataView = new DataView(data);
var lastLength;
var offset = 0;
function prepareWrite(length) {
var newByteLength = data.byteLength;
var requiredLength = offset + length;
while (newByteLength < requiredLength)
newByteLength <<= 1;
if (newByteLength !== data.byteLength) {
var oldDataView = dataView;
data = new ArrayBuffer(newByteLength);
dataView = new DataView(data);
var uint32count = (offset + 3) >> 2;
for (var i = 0; i < uint32count; ++i)
dataView.setUint32(i << 2, oldDataView.getUint32(i << 2));
}
lastLength = length;
return dataView;
}
function commitWrite() {
offset += lastLength;
}
function writeFloat64(value) {
commitWrite(prepareWrite(8).setFloat64(offset, value));
}
function writeUint8(value) {
commitWrite(prepareWrite(1).setUint8(offset, value));
}
function writeUint8Array(value) {
var dataView = prepareWrite(value.length);
for (var i = 0; i < value.length; ++i)
dataView.setUint8(offset + i, value[i]);
commitWrite();
}
function writeUint16(value) {
commitWrite(prepareWrite(2).setUint16(offset, value));
}
function writeUint32(value) {
commitWrite(prepareWrite(4).setUint32(offset, value));
}
function writeUint64(value) {
var low = value % POW_2_32;
var high = (value - low) / POW_2_32;
var dataView = prepareWrite(8);
dataView.setUint32(offset, high);
dataView.setUint32(offset + 4, low);
commitWrite();
}
function writeTypeAndLength(type, length) {
if (length < 24) {
writeUint8(type << 5 | length);
} else if (length < 0x100) {
writeUint8(type << 5 | 24);
writeUint8(length);
} else if (length < 0x10000) {
writeUint8(type << 5 | 25);
writeUint16(length);
} else if (length < 0x100000000) {
writeUint8(type << 5 | 26);
writeUint32(length);
} else {
writeUint8(type << 5 | 27);
writeUint64(length);
}
}
function encodeItem(value) {
var i;
if (value === false)
return writeUint8(0xf4);
if (value === true)
return writeUint8(0xf5);
if (value === null)
return writeUint8(0xf6);
if (value === undefined)
return writeUint8(0xf7);
switch (typeof value) {
case "number":
if (Math.floor(value) === value) {
if (0 <= value && value <= POW_2_53)
return writeTypeAndLength(0, value);
if (-POW_2_53 <= value && value < 0)
return writeTypeAndLength(1, -(value + 1));
}
writeUint8(0xfb);
return writeFloat64(value);
case "string":
var utf8data = [];
for (i = 0; i < value.length; ++i) {
var charCode = value.charCodeAt(i);
if (charCode < 0x80) {
utf8data.push(charCode);
} else if (charCode < 0x800) {
utf8data.push(0xc0 | charCode >> 6);
utf8data.push(0x80 | charCode & 0x3f);
} else if (charCode < 0xd800) {
utf8data.push(0xe0 | charCode >> 12);
utf8data.push(0x80 | (charCode >> 6) & 0x3f);
utf8data.push(0x80 | charCode & 0x3f);
} else {
charCode = (charCode & 0x3ff) << 10;
charCode |= value.charCodeAt(++i) & 0x3ff;
charCode += 0x10000;
utf8data.push(0xf0 | charCode >> 18);
utf8data.push(0x80 | (charCode >> 12) & 0x3f);
utf8data.push(0x80 | (charCode >> 6) & 0x3f);
utf8data.push(0x80 | charCode & 0x3f);
}
}
writeTypeAndLength(3, utf8data.length);
return writeUint8Array(utf8data);
default:
var length;
if (Array.isArray(value)) {
length = value.length;
writeTypeAndLength(4, length);
for (i = 0; i < length; ++i)
encodeItem(value[i]);
} else if (value instanceof Uint8Array) {
writeTypeAndLength(2, value.length);
writeUint8Array(value);
} else {
var keys = Object.keys(value);
length = keys.length;
writeTypeAndLength(5, length);
for (i = 0; i < length; ++i) {
var key = keys[i];
encodeItem(key);
encodeItem(value[key]);
}
}
}
}
encodeItem(value);
if ("slice" in data)
return data.slice(0, offset);
var ret = new ArrayBuffer(offset);
var retView = new DataView(ret);
for (var i = 0; i < offset; ++i)
retView.setUint8(i, dataView.getUint8(i));
return ret;
}
function decode(data, tagger, simpleValue) {
var dataView = new DataView(data);
var offset = 0;
if (typeof tagger !== "function")
tagger = function(value) { return value; };
if (typeof simpleValue !== "function")
simpleValue = function() { return undefined; };
function commitRead(length, value) {
offset += length;
return value;
}
function readArrayBuffer(length) {
return commitRead(length, new Uint8Array(data, offset, length));
}
function readFloat16() {
var tempArrayBuffer = new ArrayBuffer(4);
var tempDataView = new DataView(tempArrayBuffer);
var value = readUint16();
var sign = value & 0x8000;
var exponent = value & 0x7c00;
var fraction = value & 0x03ff;
if (exponent === 0x7c00)
exponent = 0xff << 10;
else if (exponent !== 0)
exponent += (127 - 15) << 10;
else if (fraction !== 0)
return (sign ? -1 : 1) * fraction * POW_2_24;
tempDataView.setUint32(0, sign << 16 | exponent << 13 | fraction << 13);
return tempDataView.getFloat32(0);
}
function readFloat32() {
return commitRead(4, dataView.getFloat32(offset));
}
function readFloat64() {
return commitRead(8, dataView.getFloat64(offset));
}
function readUint8() {
return commitRead(1, dataView.getUint8(offset));
}
function readUint16() {
return commitRead(2, dataView.getUint16(offset));
}
function readUint32() {
return commitRead(4, dataView.getUint32(offset));
}
function readUint64() {
return readUint32() * POW_2_32 + readUint32();
}
function readBreak() {
if (dataView.getUint8(offset) !== 0xff)
return false;
offset += 1;
return true;
}
function readLength(additionalInformation) {
if (additionalInformation < 24)
return additionalInformation;
if (additionalInformation === 24)
return readUint8();
if (additionalInformation === 25)
return readUint16();
if (additionalInformation === 26)
return readUint32();
if (additionalInformation === 27)
return readUint64();
if (additionalInformation === 31)
return -1;
throw "Invalid length encoding";
}
function readIndefiniteStringLength(majorType) {
var initialByte = readUint8();
if (initialByte === 0xff)
return -1;
var length = readLength(initialByte & 0x1f);
if (length < 0 || (initialByte >> 5) !== majorType)
throw "Invalid indefinite length element";
return length;
}
function appendUtf16Data(utf16data, length) {
for (var i = 0; i < length; ++i) {
var value = readUint8();
if (value & 0x80) {
if (value < 0xe0) {
value = (value & 0x1f) << 6
| (readUint8() & 0x3f);
length -= 1;
} else if (value < 0xf0) {
value = (value & 0x0f) << 12
| (readUint8() & 0x3f) << 6
| (readUint8() & 0x3f);
length -= 2;
} else {
value = (value & 0x0f) << 18
| (readUint8() & 0x3f) << 12
| (readUint8() & 0x3f) << 6
| (readUint8() & 0x3f);
length -= 3;
}
}
if (value < 0x10000) {
utf16data.push(value);
} else {
value -= 0x10000;
utf16data.push(0xd800 | (value >> 10));
utf16data.push(0xdc00 | (value & 0x3ff));
}
}
}
function decodeItem() {
var initialByte = readUint8();
var majorType = initialByte >> 5;
var additionalInformation = initialByte & 0x1f;
var i;
var length;
if (majorType === 7) {
switch (additionalInformation) {
case 25:
return readFloat16();
case 26:
return readFloat32();
case 27:
return readFloat64();
}
}
length = readLength(additionalInformation);
if (length < 0 && (majorType < 2 || 6 < majorType))
throw "Invalid length";
switch (majorType) {
case 0:
return length;
case 1:
return -1 - length;
case 2:
if (length < 0) {
var elements = [];
var fullArrayLength = 0;
while ((length = readIndefiniteStringLength(majorType)) >= 0) {
fullArrayLength += length;
elements.push(readArrayBuffer(length));
}
var fullArray = new Uint8Array(fullArrayLength);
var fullArrayOffset = 0;
for (i = 0; i < elements.length; ++i) {
fullArray.set(elements[i], fullArrayOffset);
fullArrayOffset += elements[i].length;
}
return fullArray;
}
return readArrayBuffer(length);
case 3:
var utf16data = [];
if (length < 0) {
while ((length = readIndefiniteStringLength(majorType)) >= 0)
appendUtf16Data(utf16data, length);
} else
appendUtf16Data(utf16data, length);
return String.fromCharCode.apply(null, utf16data);
case 4:
var retArray;
if (length < 0) {
retArray = [];
while (!readBreak())
retArray.push(decodeItem());
} else {
retArray = new Array(length);
for (i = 0; i < length; ++i)
retArray[i] = decodeItem();
}
return retArray;
case 5:
var retObject = {};
for (i = 0; i < length || length < 0 && !readBreak(); ++i) {
var key = decodeItem();
retObject[key] = decodeItem();
}
return retObject;
case 6:
return tagger(decodeItem(), length);
case 7:
switch (length) {
case 20:
return false;
case 21:
return true;
case 22:
return null;
case 23:
return undefined;
default:
return simpleValue(length);
}
}
}
var ret = decodeItem();
if (offset !== data.byteLength)
throw "Remaining bytes";
return ret;
}
var obj = { encode: encode, decode: decode };
if (typeof define === "function" && define.amd)
define("cbor/cbor", obj);
else if (typeof module !== "undefined" && module.exports)
module.exports = obj;
else if (!global.CBOR)
global.CBOR = obj;
})(this);

View File

@ -1,27 +0,0 @@
<html>
<head>
<title>Fido 2.0 webauthn demo</title>
<script src="/cbor.js"></script>
<style>
body { font-family: sans-serif; line-height: 1.5em; padding: 2em 10em; }
h1, h2 { color: #325F74; }
a { color: #0080ac; font-weight: bold; text-decoration: none;}
a:hover { text-decoration: underline; }
</style>
</head>
<body>
<h1>WebAuthn demo using python-fido2</h1>
<p>This demo requires a browser supporting the WebAuthn API!</p>
<hr>
<h2>Available actions</h2>
<a href="/register.html">Register</a><br>
<a href="/authenticate.html">Authenticate</a><br>
<p>
To allow the testing of authenticating with legacy U2F credentials, you can
also register a U2F credential:
<a href="/u2f.html">Register U2F</a>
</p>
</body>
</html>

View File

@ -1,7 +1,6 @@
<html>
<head>
<title>Fido 2.0 webauthn demo</title>
<script src="/cbor.js"></script>
<style>
body { font-family: sans-serif; line-height: 1.5em; padding: 2em 10em; }
h1, h2 { color: #325F74; }

View File

@ -1,7 +1,37 @@
<html>
<head>
<title>Fido 2.0 webauthn demo</title>
<script src="/cbor.js"></script>
<script type="module">
import {
create,
parseCreationOptionsFromJSON,
} from '/webauthn-json.browser-ponyfill.js';
async function start() {
let request = await fetch('/api/register/begin', {
method: 'POST',
});
let json = await request.json();
let options = parseCreationOptionsFromJSON(json);
document.getElementById('initial').style.display = 'none';
document.getElementById('started').style.display = 'block';
let response = await create(options);
let result = await fetch('/api/register/complete', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(response),
});
let stat = result.ok ? 'successful' : 'unsuccessful';
alert('Registration ' + stat + ' More details in server log...');
window.location = '/';
}
window.start = start;
</script>
<style>
body { font-family: sans-serif; line-height: 1.5em; padding: 2em 10em; }
h1, h2 { color: #325F74; }
@ -23,36 +53,5 @@
<a href="/">Cancel</a>
</div>
<script>
function start() {
fetch('/api/register/begin', {
method: 'POST',
}).then(function(response) {
if(response.ok) return response.arrayBuffer();
throw new Error('Error getting registration data!');
}).then(CBOR.decode).then(function(options) {
document.getElementById('initial').style.display = 'none';
document.getElementById('started').style.display = 'block';
return navigator.credentials.create(options);
}).then(function(attestation) {
return fetch('/api/register/complete', {
method: 'POST',
headers: {'Content-Type': 'application/cbor'},
body: CBOR.encode({
"attestationObject": new Uint8Array(attestation.response.attestationObject),
"clientDataJSON": new Uint8Array(attestation.response.clientDataJSON),
})
});
}).then(function(response) {
var stat = response.ok ? 'successful' : 'unsuccessful';
alert('Registration ' + stat + ' More details in server log...');
}, function(reason) {
alert(reason);
}).then(function() {
window.location = '/';
});
}
</script>
</body>
</html>

View File

@ -1,748 +0,0 @@
//Copyright 2014-2015 Google Inc. All rights reserved.
//Use of this source code is governed by a BSD-style
//license that can be found in the LICENSE file or at
//https://developers.google.com/open-source/licenses/bsd
/**
* @fileoverview The U2F api.
*/
'use strict';
/**
* Namespace for the U2F api.
* @type {Object}
*/
var u2f = u2f || {};
/**
* FIDO U2F Javascript API Version
* @number
*/
var js_api_version;
/**
* The U2F extension id
* @const {string}
*/
// The Chrome packaged app extension ID.
// Uncomment this if you want to deploy a server instance that uses
// the package Chrome app and does not require installing the U2F Chrome extension.
u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd';
// The U2F Chrome extension ID.
// Uncomment this if you want to deploy a server instance that uses
// the U2F Chrome extension to authenticate.
// u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne';
/**
* Message types for messsages to/from the extension
* @const
* @enum {string}
*/
u2f.MessageTypes = {
'U2F_REGISTER_REQUEST': 'u2f_register_request',
'U2F_REGISTER_RESPONSE': 'u2f_register_response',
'U2F_SIGN_REQUEST': 'u2f_sign_request',
'U2F_SIGN_RESPONSE': 'u2f_sign_response',
'U2F_GET_API_VERSION_REQUEST': 'u2f_get_api_version_request',
'U2F_GET_API_VERSION_RESPONSE': 'u2f_get_api_version_response'
};
/**
* Response status codes
* @const
* @enum {number}
*/
u2f.ErrorCodes = {
'OK': 0,
'OTHER_ERROR': 1,
'BAD_REQUEST': 2,
'CONFIGURATION_UNSUPPORTED': 3,
'DEVICE_INELIGIBLE': 4,
'TIMEOUT': 5
};
/**
* A message for registration requests
* @typedef {{
* type: u2f.MessageTypes,
* appId: ?string,
* timeoutSeconds: ?number,
* requestId: ?number
* }}
*/
u2f.U2fRequest;
/**
* A message for registration responses
* @typedef {{
* type: u2f.MessageTypes,
* responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse),
* requestId: ?number
* }}
*/
u2f.U2fResponse;
/**
* An error object for responses
* @typedef {{
* errorCode: u2f.ErrorCodes,
* errorMessage: ?string
* }}
*/
u2f.Error;
/**
* Data object for a single sign request.
* @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC}}
*/
u2f.Transport;
/**
* Data object for a single sign request.
* @typedef {Array<u2f.Transport>}
*/
u2f.Transports;
/**
* Data object for a single sign request.
* @typedef {{
* version: string,
* challenge: string,
* keyHandle: string,
* appId: string
* }}
*/
u2f.SignRequest;
/**
* Data object for a sign response.
* @typedef {{
* keyHandle: string,
* signatureData: string,
* clientData: string
* }}
*/
u2f.SignResponse;
/**
* Data object for a registration request.
* @typedef {{
* version: string,
* challenge: string
* }}
*/
u2f.RegisterRequest;
/**
* Data object for a registration response.
* @typedef {{
* version: string,
* keyHandle: string,
* transports: Transports,
* appId: string
* }}
*/
u2f.RegisterResponse;
/**
* Data object for a registered key.
* @typedef {{
* version: string,
* keyHandle: string,
* transports: ?Transports,
* appId: ?string
* }}
*/
u2f.RegisteredKey;
/**
* Data object for a get API register response.
* @typedef {{
* js_api_version: number
* }}
*/
u2f.GetJsApiVersionResponse;
//Low level MessagePort API support
/**
* Sets up a MessagePort to the U2F extension using the
* available mechanisms.
* @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
*/
u2f.getMessagePort = function(callback) {
if (typeof chrome != 'undefined' && chrome.runtime) {
// The actual message here does not matter, but we need to get a reply
// for the callback to run. Thus, send an empty signature request
// in order to get a failure response.
var msg = {
type: u2f.MessageTypes.U2F_SIGN_REQUEST,
signRequests: []
};
chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() {
if (!chrome.runtime.lastError) {
// We are on a whitelisted origin and can talk directly
// with the extension.
u2f.getChromeRuntimePort_(callback);
} else {
// chrome.runtime was available, but we couldn't message
// the extension directly, use iframe
u2f.getIframePort_(callback);
}
});
} else if (u2f.isAndroidChrome_()) {
u2f.getAuthenticatorPort_(callback);
} else if (u2f.isIosChrome_()) {
u2f.getIosPort_(callback);
} else {
// chrome.runtime was not available at all, which is normal
// when this origin doesn't have access to any extensions.
u2f.getIframePort_(callback);
}
};
/**
* Detect chrome running on android based on the browser's useragent.
* @private
*/
u2f.isAndroidChrome_ = function() {
var userAgent = navigator.userAgent;
return userAgent.indexOf('Chrome') != -1 &&
userAgent.indexOf('Android') != -1;
};
/**
* Detect chrome running on iOS based on the browser's platform.
* @private
*/
u2f.isIosChrome_ = function() {
return ["iPhone", "iPad", "iPod"].indexOf(navigator.platform) > -1;
};
/**
* Connects directly to the extension via chrome.runtime.connect.
* @param {function(u2f.WrappedChromeRuntimePort_)} callback
* @private
*/
u2f.getChromeRuntimePort_ = function(callback) {
var port = chrome.runtime.connect(u2f.EXTENSION_ID,
{'includeTlsChannelId': true});
setTimeout(function() {
callback(new u2f.WrappedChromeRuntimePort_(port));
}, 0);
};
/**
* Return a 'port' abstraction to the Authenticator app.
* @param {function(u2f.WrappedAuthenticatorPort_)} callback
* @private
*/
u2f.getAuthenticatorPort_ = function(callback) {
setTimeout(function() {
callback(new u2f.WrappedAuthenticatorPort_());
}, 0);
};
/**
* Return a 'port' abstraction to the iOS client app.
* @param {function(u2f.WrappedIosPort_)} callback
* @private
*/
u2f.getIosPort_ = function(callback) {
setTimeout(function() {
callback(new u2f.WrappedIosPort_());
}, 0);
};
/**
* A wrapper for chrome.runtime.Port that is compatible with MessagePort.
* @param {Port} port
* @constructor
* @private
*/
u2f.WrappedChromeRuntimePort_ = function(port) {
this.port_ = port;
};
/**
* Format and return a sign request compliant with the JS API version supported by the extension.
* @param {Array<u2f.SignRequest>} signRequests
* @param {number} timeoutSeconds
* @param {number} reqId
* @return {Object}
*/
u2f.formatSignRequest_ =
function(appId, challenge, registeredKeys, timeoutSeconds, reqId) {
if (js_api_version === undefined || js_api_version < 1.1) {
// Adapt request to the 1.0 JS API
var signRequests = [];
for (var i = 0; i < registeredKeys.length; i++) {
signRequests[i] = {
version: registeredKeys[i].version,
challenge: challenge,
keyHandle: registeredKeys[i].keyHandle,
appId: appId
};
}
return {
type: u2f.MessageTypes.U2F_SIGN_REQUEST,
signRequests: signRequests,
timeoutSeconds: timeoutSeconds,
requestId: reqId
};
}
// JS 1.1 API
return {
type: u2f.MessageTypes.U2F_SIGN_REQUEST,
appId: appId,
challenge: challenge,
registeredKeys: registeredKeys,
timeoutSeconds: timeoutSeconds,
requestId: reqId
};
};
/**
* Format and return a register request compliant with the JS API version supported by the extension..
* @param {Array<u2f.SignRequest>} signRequests
* @param {Array<u2f.RegisterRequest>} signRequests
* @param {number} timeoutSeconds
* @param {number} reqId
* @return {Object}
*/
u2f.formatRegisterRequest_ =
function(appId, registeredKeys, registerRequests, timeoutSeconds, reqId) {
if (js_api_version === undefined || js_api_version < 1.1) {
// Adapt request to the 1.0 JS API
for (var i = 0; i < registerRequests.length; i++) {
registerRequests[i].appId = appId;
}
var signRequests = [];
for (var i = 0; i < registeredKeys.length; i++) {
signRequests[i] = {
version: registeredKeys[i].version,
challenge: registerRequests[0],
keyHandle: registeredKeys[i].keyHandle,
appId: appId
};
}
return {
type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
signRequests: signRequests,
registerRequests: registerRequests,
timeoutSeconds: timeoutSeconds,
requestId: reqId
};
}
// JS 1.1 API
return {
type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
appId: appId,
registerRequests: registerRequests,
registeredKeys: registeredKeys,
timeoutSeconds: timeoutSeconds,
requestId: reqId
};
};
/**
* Posts a message on the underlying channel.
* @param {Object} message
*/
u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) {
this.port_.postMessage(message);
};
/**
* Emulates the HTML 5 addEventListener interface. Works only for the
* onmessage event, which is hooked up to the chrome.runtime.Port.onMessage.
* @param {string} eventName
* @param {function({data: Object})} handler
*/
u2f.WrappedChromeRuntimePort_.prototype.addEventListener =
function(eventName, handler) {
var name = eventName.toLowerCase();
if (name == 'message' || name == 'onmessage') {
this.port_.onMessage.addListener(function(message) {
// Emulate a minimal MessageEvent object
handler({'data': message});
});
} else {
console.error('WrappedChromeRuntimePort only supports onMessage');
}
};
/**
* Wrap the Authenticator app with a MessagePort interface.
* @constructor
* @private
*/
u2f.WrappedAuthenticatorPort_ = function() {
this.requestId_ = -1;
this.requestObject_ = null;
}
/**
* Launch the Authenticator intent.
* @param {Object} message
*/
u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) {
var intentUrl =
u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ +
';S.request=' + encodeURIComponent(JSON.stringify(message)) +
';end';
document.location = intentUrl;
};
/**
* Tells what type of port this is.
* @return {String} port type
*/
u2f.WrappedAuthenticatorPort_.prototype.getPortType = function() {
return "WrappedAuthenticatorPort_";
};
/**
* Emulates the HTML 5 addEventListener interface.
* @param {string} eventName
* @param {function({data: Object})} handler
*/
u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function(eventName, handler) {
var name = eventName.toLowerCase();
if (name == 'message') {
var self = this;
/* Register a callback to that executes when
* chrome injects the response. */
window.addEventListener(
'message', self.onRequestUpdate_.bind(self, handler), false);
} else {
console.error('WrappedAuthenticatorPort only supports message');
}
};
/**
* Callback invoked when a response is received from the Authenticator.
* @param function({data: Object}) callback
* @param {Object} message message Object
*/
u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ =
function(callback, message) {
var messageObject = JSON.parse(message.data);
var intentUrl = messageObject['intentURL'];
var errorCode = messageObject['errorCode'];
var responseObject = null;
if (messageObject.hasOwnProperty('data')) {
responseObject = /** @type {Object} */ (
JSON.parse(messageObject['data']));
}
callback({'data': responseObject});
};
/**
* Base URL for intents to Authenticator.
* @const
* @private
*/
u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ =
'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE';
/**
* Wrap the iOS client app with a MessagePort interface.
* @constructor
* @private
*/
u2f.WrappedIosPort_ = function() {};
/**
* Launch the iOS client app request
* @param {Object} message
*/
u2f.WrappedIosPort_.prototype.postMessage = function(message) {
var str = JSON.stringify(message);
var url = "u2f://auth?" + encodeURI(str);
location.replace(url);
};
/**
* Tells what type of port this is.
* @return {String} port type
*/
u2f.WrappedIosPort_.prototype.getPortType = function() {
return "WrappedIosPort_";
};
/**
* Emulates the HTML 5 addEventListener interface.
* @param {string} eventName
* @param {function({data: Object})} handler
*/
u2f.WrappedIosPort_.prototype.addEventListener = function(eventName, handler) {
var name = eventName.toLowerCase();
if (name !== 'message') {
console.error('WrappedIosPort only supports message');
}
};
/**
* Sets up an embedded trampoline iframe, sourced from the extension.
* @param {function(MessagePort)} callback
* @private
*/
u2f.getIframePort_ = function(callback) {
// Create the iframe
var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID;
var iframe = document.createElement('iframe');
iframe.src = iframeOrigin + '/u2f-comms.html';
iframe.setAttribute('style', 'display:none');
document.body.appendChild(iframe);
var channel = new MessageChannel();
var ready = function(message) {
if (message.data == 'ready') {
channel.port1.removeEventListener('message', ready);
callback(channel.port1);
} else {
console.error('First event on iframe port was not "ready"');
}
};
channel.port1.addEventListener('message', ready);
channel.port1.start();
iframe.addEventListener('load', function() {
// Deliver the port to the iframe and initialize
iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]);
});
};
//High-level JS API
/**
* Default extension response timeout in seconds.
* @const
*/
u2f.EXTENSION_TIMEOUT_SEC = 30;
/**
* A singleton instance for a MessagePort to the extension.
* @type {MessagePort|u2f.WrappedChromeRuntimePort_}
* @private
*/
u2f.port_ = null;
/**
* Callbacks waiting for a port
* @type {Array<function((MessagePort|u2f.WrappedChromeRuntimePort_))>}
* @private
*/
u2f.waitingForPort_ = [];
/**
* A counter for requestIds.
* @type {number}
* @private
*/
u2f.reqCounter_ = 0;
/**
* A map from requestIds to client callbacks
* @type {Object.<number,(function((u2f.Error|u2f.RegisterResponse))
* |function((u2f.Error|u2f.SignResponse)))>}
* @private
*/
u2f.callbackMap_ = {};
/**
* Creates or retrieves the MessagePort singleton to use.
* @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
* @private
*/
u2f.getPortSingleton_ = function(callback) {
if (u2f.port_) {
callback(u2f.port_);
} else {
if (u2f.waitingForPort_.length == 0) {
u2f.getMessagePort(function(port) {
u2f.port_ = port;
u2f.port_.addEventListener('message',
/** @type {function(Event)} */ (u2f.responseHandler_));
// Careful, here be async callbacks. Maybe.
while (u2f.waitingForPort_.length)
u2f.waitingForPort_.shift()(u2f.port_);
});
}
u2f.waitingForPort_.push(callback);
}
};
/**
* Handles response messages from the extension.
* @param {MessageEvent.<u2f.Response>} message
* @private
*/
u2f.responseHandler_ = function(message) {
var response = message.data;
var reqId = response['requestId'];
if (!reqId || !u2f.callbackMap_[reqId]) {
console.error('Unknown or missing requestId in response.');
return;
}
var cb = u2f.callbackMap_[reqId];
delete u2f.callbackMap_[reqId];
cb(response['responseData']);
};
/**
* Dispatches an array of sign requests to available U2F tokens.
* If the JS API version supported by the extension is unknown, it first sends a
* message to the extension to find out the supported API version and then it sends
* the sign request.
* @param {string=} appId
* @param {string=} challenge
* @param {Array<u2f.RegisteredKey>} registeredKeys
* @param {function((u2f.Error|u2f.SignResponse))} callback
* @param {number=} opt_timeoutSeconds
*/
u2f.sign = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) {
if (js_api_version === undefined) {
// Send a message to get the extension to JS API version, then send the actual sign request.
u2f.getApiVersion(
function (response) {
js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version'];
console.log("Extension JS API Version: ", js_api_version);
u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds);
});
} else {
// We know the JS API version. Send the actual sign request in the supported API version.
u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds);
}
};
/**
* Dispatches an array of sign requests to available U2F tokens.
* @param {string=} appId
* @param {string=} challenge
* @param {Array<u2f.RegisteredKey>} registeredKeys
* @param {function((u2f.Error|u2f.SignResponse))} callback
* @param {number=} opt_timeoutSeconds
*/
u2f.sendSignRequest = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) {
u2f.getPortSingleton_(function(port) {
var reqId = ++u2f.reqCounter_;
u2f.callbackMap_[reqId] = callback;
var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
var req = u2f.formatSignRequest_(appId, challenge, registeredKeys, timeoutSeconds, reqId);
port.postMessage(req);
});
};
/**
* Dispatches register requests to available U2F tokens. An array of sign
* requests identifies already registered tokens.
* If the JS API version supported by the extension is unknown, it first sends a
* message to the extension to find out the supported API version and then it sends
* the register request.
* @param {string=} appId
* @param {Array<u2f.RegisterRequest>} registerRequests
* @param {Array<u2f.RegisteredKey>} registeredKeys
* @param {function((u2f.Error|u2f.RegisterResponse))} callback
* @param {number=} opt_timeoutSeconds
*/
u2f.register = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) {
if (js_api_version === undefined) {
// Send a message to get the extension to JS API version, then send the actual register request.
u2f.getApiVersion(
function (response) {
js_api_version = response['js_api_version'] === undefined ? 0: response['js_api_version'];
console.log("Extension JS API Version: ", js_api_version);
u2f.sendRegisterRequest(appId, registerRequests, registeredKeys,
callback, opt_timeoutSeconds);
});
} else {
// We know the JS API version. Send the actual register request in the supported API version.
u2f.sendRegisterRequest(appId, registerRequests, registeredKeys,
callback, opt_timeoutSeconds);
}
};
/**
* Dispatches register requests to available U2F tokens. An array of sign
* requests identifies already registered tokens.
* @param {string=} appId
* @param {Array<u2f.RegisterRequest>} registerRequests
* @param {Array<u2f.RegisteredKey>} registeredKeys
* @param {function((u2f.Error|u2f.RegisterResponse))} callback
* @param {number=} opt_timeoutSeconds
*/
u2f.sendRegisterRequest = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) {
u2f.getPortSingleton_(function(port) {
var reqId = ++u2f.reqCounter_;
u2f.callbackMap_[reqId] = callback;
var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
var req = u2f.formatRegisterRequest_(
appId, registeredKeys, registerRequests, timeoutSeconds, reqId);
port.postMessage(req);
});
};
/**
* Dispatches a message to the extension to find out the supported
* JS API version.
* If the user is on a mobile phone and is thus using Google Authenticator instead
* of the Chrome extension, don't send the request and simply return 0.
* @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback
* @param {number=} opt_timeoutSeconds
*/
u2f.getApiVersion = function(callback, opt_timeoutSeconds) {
u2f.getPortSingleton_(function(port) {
// If we are using Android Google Authenticator or iOS client app,
// do not fire an intent to ask which JS API version to use.
if (port.getPortType) {
var apiVersion;
switch (port.getPortType()) {
case 'WrappedIosPort_':
case 'WrappedAuthenticatorPort_':
apiVersion = 1.1;
break;
default:
apiVersion = 0;
break;
}
callback({ 'js_api_version': apiVersion });
return;
}
var reqId = ++u2f.reqCounter_;
u2f.callbackMap_[reqId] = callback;
var req = {
type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST,
timeoutSeconds: (typeof opt_timeoutSeconds !== 'undefined' ?
opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC),
requestId: reqId
};
port.postMessage(req);
});
};

View File

@ -1,58 +0,0 @@
<html>
<head>
<title>Fido 2.0 webauthn demo</title>
<script src="/cbor.js"></script>
<script src="/u2f-api.js"></script>
<style>
body { font-family: sans-serif; line-height: 1.5em; padding: 2em 10em; }
h1, h2 { color: #325F74; }
a { color: #0080ac; font-weight: bold; text-decoration: none;}
a:hover { text-decoration: underline; }
</style>
</head>
<body>
<h1>WebAuthn demo using python-fido2</h1>
<p>This demo requires a browser supporting the WebAuthn API!</p>
<hr>
<h2>Register a U2F credential</h2>
<p>Touch your authenticator device now...</p>
<a href="/">Cancel</a>
<script>
fetch('/api/u2f/begin', {
method: 'POST',
}).then(function(response) {
if(response.ok) return response.arrayBuffer();
throw new Error('Error getting registration data!');
}).then(CBOR.decode).then(function(challenge) {
return new Promise(function(resolve, reject) {
u2f.register('https://localhost:5000', [{
challenge: challenge,
version: 'U2F_V2'
}], [], function(resp) {
if(resp.errorCode) {
reject(new Error('Error: ' + resp.errorCode));
} else {
resolve(resp);
}
});
});
}).then(function(attestation) {
return fetch('/api/u2f/complete', {
method: 'POST',
headers: {'Content-Type': 'application/cbor'},
body: CBOR.encode(attestation)
});
}).then(function(response) {
var stat = response.ok ? 'successful' : 'unsuccessful';
alert('Registration ' + stat + ' More details in server log...');
}, function(reason) {
alert(reason);
}).then(function() {
window.location = '/';
});
</script>
</body>
</html>

View File

@ -0,0 +1,193 @@
// src/webauthn-json/base64url.ts
function base64urlToBuffer(baseurl64String) {
const padding = "==".slice(0, (4 - baseurl64String.length % 4) % 4);
const base64String = baseurl64String.replace(/-/g, "+").replace(/_/g, "/") + padding;
const str = atob(base64String);
const buffer = new ArrayBuffer(str.length);
const byteView = new Uint8Array(buffer);
for (let i = 0; i < str.length; i++) {
byteView[i] = str.charCodeAt(i);
}
return buffer;
}
function bufferToBase64url(buffer) {
const byteView = new Uint8Array(buffer);
let str = "";
for (const charCode of byteView) {
str += String.fromCharCode(charCode);
}
const base64String = btoa(str);
const base64urlString = base64String.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
return base64urlString;
}
// src/webauthn-json/convert.ts
var copyValue = "copy";
var convertValue = "convert";
function convert(conversionFn, schema, input) {
if (schema === copyValue) {
return input;
}
if (schema === convertValue) {
return conversionFn(input);
}
if (schema instanceof Array) {
return input.map((v) => convert(conversionFn, schema[0], v));
}
if (schema instanceof Object) {
const output = {};
for (const [key, schemaField] of Object.entries(schema)) {
if (schemaField.derive) {
const v = schemaField.derive(input);
if (v !== void 0) {
input[key] = v;
}
}
if (!(key in input)) {
if (schemaField.required) {
throw new Error(`Missing key: ${key}`);
}
continue;
}
if (input[key] == null) {
output[key] = null;
continue;
}
output[key] = convert(conversionFn, schemaField.schema, input[key]);
}
return output;
}
}
function derived(schema, derive) {
return {
required: true,
schema,
derive
};
}
function required(schema) {
return {
required: true,
schema
};
}
function optional(schema) {
return {
required: false,
schema
};
}
// src/webauthn-json/basic/schema.ts
var publicKeyCredentialDescriptorSchema = {
type: required(copyValue),
id: required(convertValue),
transports: optional(copyValue)
};
var simplifiedExtensionsSchema = {
appid: optional(copyValue),
appidExclude: optional(copyValue),
credProps: optional(copyValue)
};
var simplifiedClientExtensionResultsSchema = {
appid: optional(copyValue),
appidExclude: optional(copyValue),
credProps: optional(copyValue)
};
var credentialCreationOptions = {
publicKey: required({
rp: required(copyValue),
user: required({
id: required(convertValue),
name: required(copyValue),
displayName: required(copyValue)
}),
challenge: required(convertValue),
pubKeyCredParams: required(copyValue),
timeout: optional(copyValue),
excludeCredentials: optional([publicKeyCredentialDescriptorSchema]),
authenticatorSelection: optional(copyValue),
attestation: optional(copyValue),
extensions: optional(simplifiedExtensionsSchema)
}),
signal: optional(copyValue)
};
var publicKeyCredentialWithAttestation = {
type: required(copyValue),
id: required(copyValue),
rawId: required(convertValue),
authenticatorAttachment: optional(copyValue),
response: required({
clientDataJSON: required(convertValue),
attestationObject: required(convertValue),
transports: derived(copyValue, (response) => {
var _a;
return ((_a = response.getTransports) == null ? void 0 : _a.call(response)) || [];
})
}),
clientExtensionResults: derived(simplifiedClientExtensionResultsSchema, (pkc) => pkc.getClientExtensionResults())
};
var credentialRequestOptions = {
mediation: optional(copyValue),
publicKey: required({
challenge: required(convertValue),
timeout: optional(copyValue),
rpId: optional(copyValue),
allowCredentials: optional([publicKeyCredentialDescriptorSchema]),
userVerification: optional(copyValue),
extensions: optional(simplifiedExtensionsSchema)
}),
signal: optional(copyValue)
};
var publicKeyCredentialWithAssertion = {
type: required(copyValue),
id: required(copyValue),
rawId: required(convertValue),
authenticatorAttachment: optional(copyValue),
response: required({
clientDataJSON: required(convertValue),
authenticatorData: required(convertValue),
signature: required(convertValue),
userHandle: required(convertValue)
}),
clientExtensionResults: derived(simplifiedClientExtensionResultsSchema, (pkc) => pkc.getClientExtensionResults())
};
// src/webauthn-json/basic/api.ts
function createRequestFromJSON(requestJSON) {
return convert(base64urlToBuffer, credentialCreationOptions, requestJSON);
}
function createResponseToJSON(credential) {
return convert(bufferToBase64url, publicKeyCredentialWithAttestation, credential);
}
function getRequestFromJSON(requestJSON) {
return convert(base64urlToBuffer, credentialRequestOptions, requestJSON);
}
function getResponseToJSON(credential) {
return convert(bufferToBase64url, publicKeyCredentialWithAssertion, credential);
}
// src/webauthn-json/basic/supported.ts
function supported() {
return !!(navigator.credentials && navigator.credentials.create && navigator.credentials.get && window.PublicKeyCredential);
}
// src/webauthn-json/browser-ponyfill.ts
async function create(options) {
const response = await navigator.credentials.create(options);
response.toJSON = () => createResponseToJSON(response);
return response;
}
async function get(options) {
const response = await navigator.credentials.get(options);
response.toJSON = () => getResponseToJSON(response);
return response;
}
export {
create,
get,
createRequestFromJSON as parseCreationOptionsFromJSON,
getRequestFromJSON as parseRequestOptionsFromJSON,
supported
};
//# sourceMappingURL=webauthn-json.browser-ponyfill.js.map

File diff suppressed because one or more lines are too long

View File

@ -104,7 +104,6 @@ else:
dev = next(CtapHidDevice.list_devices(), None)
if dev is not None:
print("Use USB HID channel.")
use_prompt = True
else:
try:
from fido2.pcsc import CtapPcscDevice
@ -141,9 +140,6 @@ create_options, state = server.register_begin(
)
# Create a credential
if use_prompt:
print("\nTouch your authenticator device now...\n")
result = client.make_credential(create_options["publicKey"])
# Complete registration

View File

@ -26,4 +26,4 @@
# POSSIBILITY OF SUCH DAMAGE.
__version__ = "1.0.0"
__version__ = "1.1.4-dev.0"

View File

@ -194,7 +194,7 @@ class AttestationVerifier(abc.ABC):
to verify the trust path from the attestation.
"""
def __init__(self, attestation_types: Sequence[Attestation] = None):
def __init__(self, attestation_types: Optional[Sequence[Attestation]] = None):
self._attestation_types = attestation_types or _default_attestations()
@abc.abstractmethod

View File

@ -396,7 +396,8 @@ _Unique = Union[Tpm2bPublicKeyRsa, TpmsEccPoint]
@dataclass
class TpmPublicFormat:
"""the public area structure is defined by [TPMv2-Part2] Section 12.2.4 (TPMT_PUBLIC)
"""the public area structure is defined by [TPMv2-Part2] Section 12.2.4
(TPMT_PUBLIC)
as:
TPMI_ALG_PUBLIC - type
TPMI_ALG_HASH - nameAlg

View File

@ -453,6 +453,7 @@ class _Ctap2ClientBackend(_ClientBackend):
if "FIDO_2_1" in self.info.versions:
self.ctap2.selection(event=event)
else:
# Selection not supported, make dummy credential instead
try:
self.ctap2.make_credential(
b"\0" * 32,
@ -463,7 +464,11 @@ class _Ctap2ClientBackend(_ClientBackend):
event=event,
)
except CtapError as e:
if e.code is CtapError.ERR.PIN_AUTH_INVALID:
if e.code in (
CtapError.ERR.PIN_NOT_SET,
CtapError.ERR.PIN_INVALID,
CtapError.ERR.PIN_AUTH_INVALID,
):
return
raise
@ -497,7 +502,7 @@ class _Ctap2ClientBackend(_ClientBackend):
):
# Prefer UV
if self.info.options.get("uv"):
if self.info.options.get("pinUvAuthToken"):
if ClientPin.is_token_supported(self.info):
if self.user_interaction.request_uv(permissions, rp_id):
return client_pin.get_uv_token(
permissions, rp_id, event, on_keepalive

View File

@ -98,7 +98,15 @@ class CoseKey(dict):
@staticmethod
def supported_algorithms() -> Sequence[int]:
"""Get a list of all supported algorithm identifiers"""
algs: Sequence[Type[CoseKey]] = [ES256, EdDSA, ES384, ES512, PS256, RS256]
algs: Sequence[Type[CoseKey]] = [
ES256,
EdDSA,
ES384,
ES512,
PS256,
RS256,
ES256K,
]
return [cls.ALGORITHM for cls in algs]
@ -150,7 +158,7 @@ class ES384(CoseKey):
_HASH_ALG = hashes.SHA384()
def verify(self, message, signature):
if self[-1] != 1:
if self[-1] != 2:
raise ValueError("Unsupported elliptic curve")
ec.EllipticCurvePublicNumbers(
bytes2int(self[-2]), bytes2int(self[-3]), ec.SECP384R1()
@ -165,7 +173,7 @@ class ES384(CoseKey):
{
1: 2,
3: cls.ALGORITHM,
-1: 1,
-1: 2,
-2: int2bytes(pn.x, 48),
-3: int2bytes(pn.y, 48),
}
@ -177,7 +185,7 @@ class ES512(CoseKey):
_HASH_ALG = hashes.SHA512()
def verify(self, message, signature):
if self[-1] != 1:
if self[-1] != 3:
raise ValueError("Unsupported elliptic curve")
ec.EllipticCurvePublicNumbers(
bytes2int(self[-2]), bytes2int(self[-3]), ec.SECP521R1()
@ -192,7 +200,7 @@ class ES512(CoseKey):
{
1: 2,
3: cls.ALGORITHM,
-1: 1,
-1: 3,
-2: int2bytes(pn.x, 64),
-3: int2bytes(pn.y, 64),
}
@ -271,3 +279,30 @@ class RS1(CoseKey):
def from_cryptography_key(cls, public_key):
pn = public_key.public_numbers()
return cls({1: 3, 3: cls.ALGORITHM, -1: int2bytes(pn.n), -2: int2bytes(pn.e)})
class ES256K(CoseKey):
ALGORITHM = -47
_HASH_ALG = hashes.SHA256()
def verify(self, message, signature):
if self[-1] != 8:
raise ValueError("Unsupported elliptic curve")
ec.EllipticCurvePublicNumbers(
bytes2int(self[-2]), bytes2int(self[-3]), ec.SECP256K1()
).public_key(default_backend()).verify(
signature, message, ec.ECDSA(self._HASH_ALG)
)
@classmethod
def from_cryptography_key(cls, public_key):
pn = public_key.public_numbers()
return cls(
{
1: 2,
3: cls.ALGORITHM,
-1: 8,
-2: int2bytes(pn.x, 32),
-3: int2bytes(pn.y, 32),
}
)

View File

@ -57,7 +57,7 @@ def args(*params) -> Dict[int, Any]:
class _CborDataObject(_DataClassMapping[int]):
@classmethod
def _get_field_key(cls, field: Field) -> int:
return fields(cls).index(field) + 1
return fields(cls).index(field) + 1 # type: ignore
@dataclass(eq=False, frozen=True)
@ -168,7 +168,7 @@ class AssertionResponse(_CborDataObject):
credential=credential,
auth_data=AuthenticatorData.create(
app_param,
authentication.user_presence & AuthenticatorData.FLAG.USER_PRESENT,
authentication.user_presence & AuthenticatorData.FLAG.UP,
authentication.counter,
),
signature=authentication.signature,
@ -353,6 +353,7 @@ class Ctap2:
:param options: Optional dict of options.
:param pin_uv_param: Optional PIN/UV auth parameter.
:param pin_uv_protocol: The version of PIN/UV protocol used, if any.
:param enterprise_attestation: Whether or not to request Enterprise Attestation.
:param event: Optional threading.Event used to cancel the request.
:param on_keepalive: Optional callback function to handle keep-alive
messages from the authenticator.

View File

@ -66,8 +66,9 @@ class BioEnrollment:
if "bioEnroll" in info.options:
return True
# We also support the Prototype command
if "FIDO_2_1_PRE" in info.versions and info.options.get(
"credentialMgmtPreview"
if (
"FIDO_2_1_PRE" in info.versions
and "userVerificationMgmtPreview" in info.options
):
return True
return False

View File

@ -166,7 +166,7 @@ class LargeBlobs:
self.ctap.large_blobs(
offset,
set=_set,
length=ln,
length=size if offset == 0 else None,
pin_uv_protocol=pin_uv_protocol,
pin_uv_param=pin_uv_param,
)

View File

@ -86,7 +86,7 @@ class Config:
msg = (
b"\xff" * 32
+ b"\x0d"
+ struct.pack("<b", sub_cmd)
+ struct.pack("<B", sub_cmd)
+ (cbor.encode(params) if params else b"")
)
pin_uv_protocol = self.pin_uv.protocol.VERSION
@ -113,7 +113,7 @@ class Config:
def set_min_pin_length(
self,
min_pin_length: Optional[int] = None,
rp_ids: List[str] = None,
rp_ids: Optional[List[str]] = None,
force_change_pin: bool = False,
) -> None:
"""Set the minimum PIN length allowed when setting/changing the PIN.

View File

@ -86,9 +86,7 @@ class CredentialManagement:
if info.options.get("credMgmt"):
return True
# We also support the Prototype command
if "FIDO_2_1_PRE" in info.versions and info.options.get(
"credentialMgmtPreview"
):
if "FIDO_2_1_PRE" in info.versions and "credentialMgmtPreview" in info.options:
return True
return False

View File

@ -238,7 +238,9 @@ class CredProtectExtension(Ctap2Extension):
def process_create_input(self, inputs):
policy = inputs.get("credentialProtectionPolicy")
if policy:
index = list(CredProtectExtension.POLICY).index(policy)
index = list(CredProtectExtension.POLICY).index(
CredProtectExtension.POLICY(policy)
)
enforce = inputs.get("enforceCredentialProtectionPolicy", False)
if enforce and not self.is_supported() and index > 0:
raise ValueError("Authenticator does not support Credential Protection")

View File

@ -251,12 +251,19 @@ class ClientPin:
@staticmethod
def is_supported(info):
"""Checks if ClientPin functionality is supported.
Note that the ClientPin function is still usable without support for client
PIN functionality, as UV token may still be supported.
"""
return "clientPin" in info.options
def __init__(self, ctap: Ctap2, protocol: Optional[PinProtocol] = None):
if not self.is_supported(ctap.info):
raise ValueError("Authenticator does not support ClientPin")
@staticmethod
def is_token_supported(info):
"""Checks if pinUvAuthToken is supported."""
return info.options.get("pinUvAuthToken") is True
def __init__(self, ctap: Ctap2, protocol: Optional[PinProtocol] = None):
self.ctap = ctap
if protocol is None:
for proto in ClientPin.PROTOCOLS:
@ -267,7 +274,6 @@ class ClientPin:
raise ValueError("No compatible PIN/UV protocols supported!")
else:
self.protocol = protocol
self._supports_permissions = ctap.info.options.get("pinUvAuthToken")
def _get_shared_secret(self):
resp = self.ctap.client_pin(
@ -290,12 +296,15 @@ class ClientPin:
:param permissions_rpid: The permissions RPID to associate with the token.
:return: A PIN/UV token.
"""
if not ClientPin.is_supported(self.ctap.info):
raise ValueError("Authenticator does not support get_pin_token")
key_agreement, shared_secret = self._get_shared_secret()
pin_hash = sha256(pin.encode())[:16]
pin_hash_enc = self.protocol.encrypt(shared_secret, pin_hash)
if self._supports_permissions and permissions:
if ClientPin.is_token_supported(self.ctap.info) and permissions:
cmd = ClientPin.CMD.GET_TOKEN_USING_PIN
else:
cmd = ClientPin.CMD.GET_TOKEN_USING_PIN_LEGACY
@ -335,7 +344,7 @@ class ClientPin:
consecutive keep-alive messages with the same status.
:return: A PIN/UV token.
"""
if not self.ctap.info.options.get("pinUvAuthToken"):
if not ClientPin.is_token_supported(self.ctap.info):
raise ValueError("Authenticator does not support get_uv_token")
key_agreement, shared_secret = self._get_shared_secret()
@ -387,6 +396,9 @@ class ClientPin:
:param pin: A PIN to set.
"""
if not ClientPin.is_supported(self.ctap.info):
raise ValueError("Authenticator does not support ClientPin")
key_agreement, shared_secret = self._get_shared_secret()
pin_enc = self.protocol.encrypt(shared_secret, _pad_pin(pin))
@ -409,6 +421,9 @@ class ClientPin:
:param old_pin: The currently set PIN.
:param new_pin: The new PIN to set.
"""
if not ClientPin.is_supported(self.ctap.info):
raise ValueError("Authenticator does not support ClientPin")
key_agreement, shared_secret = self._get_shared_secret()
pin_hash = sha256(old_pin.encode())[:16]

95
fido2/features.py Normal file
View File

@ -0,0 +1,95 @@
# Copyright (c) 2022 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.
from typing import Optional
import warnings
class FeatureNotEnabledError(Exception):
pass
class _Feature:
def __init__(self, name: str, desc: str):
self._enabled: Optional[bool] = None
self._name = name
self._desc = desc
@property
def enabled(self) -> bool:
self.warn()
return self._enabled is True
@enabled.setter
def enabled(self, value: bool) -> None:
if self._enabled is not None:
raise ValueError(
f"{self._name} has already been configured with {self._enabled}"
)
self._enabled = value
def require(self, state=True) -> None:
if self._enabled != state:
self.warn()
raise FeatureNotEnabledError(
f"Usage requires {self._name}.enabled = {state}"
)
def warn(self) -> None:
if self._enabled is None:
warnings.warn(
f"""Deprecated use of {self._name}.
You are using deprecated functionality which will change in the next major version of
python-fido2. You can opt-in to use the new functionality now by adding the following
to your code somewhere where it gets executed prior to using the affected functionality:
import fido2.features
fido2.features.{self._name}.enabled = True
To silence this warning but retain the current behavior, instead set enabled to False:
fido2.features.{self._name}.enabled = False
{self._desc}
""",
DeprecationWarning,
)
webauthn_json_mapping = _Feature(
"webauthn_json_mapping",
"""JSON values for WebAuthn data class Mapping interface.
This changes the keys and values used by the webauthn data classes when accessed using
the Mapping (dict) interface (eg. user_entity["id"] and the from_dict() methods) to be
JSON-friendly and align with the current draft of the next WebAuthn Level specification.
For the most part, this means that binary values (bytes) are represented as URL-safe
base64 encoded strings instead.
""",
)

View File

@ -49,6 +49,8 @@ elif sys.platform.startswith("darwin"):
from . import macos as backend
elif sys.platform.startswith("freebsd"):
from . import freebsd as backend
elif sys.platform.startswith("netbsd"):
from . import netbsd as backend
elif sys.platform.startswith("openbsd"):
from . import openbsd as backend
else:

View File

@ -24,7 +24,7 @@
# uhid(4) - Classic method, default option on
# FreeBSD 13.x and earlier
#
# uhid is available since FreeBSD 13 and can be activated by adding
# hidraw is available since FreeBSD 13 and can be activated by adding
# `hw.usb.usbhid.enable="1"` to `/boot/loader.conf`. The actual kernel
# module is loaded with `kldload hidraw`.
@ -126,7 +126,6 @@ def _read_descriptor(vid, pid, name, serial, path):
def _enumerate():
for uhid in glob.glob(devdir + "uhid?*"):
index = uhid[len(devdir) + len("uhid") :]
if not index.isdigit():
continue

View File

@ -317,13 +317,16 @@ class MacCtapHidConnection(CtapHidConnection):
raise OSError(f"Failed to write report to device: {result}")
def read_packet(self):
read_thread = threading.Thread(target=_dev_read_thread, args=(self,))
read_thread.start()
read_thread.join()
try:
return self.read_queue.get(False)
except Empty:
raise OSError("Failed reading a response")
read_thread = threading.Thread(target=_dev_read_thread, args=(self,))
read_thread.start()
read_thread.join()
try:
return self.read_queue.get(False)
except Empty:
raise OSError("Failed reading a response")
def get_int_property(dev, key):

173
fido2/hid/netbsd.py Normal file
View File

@ -0,0 +1,173 @@
# Copyright 2016 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Implements raw HID interface on NetBSD."""
from __future__ import absolute_import
import errno
import logging
import os
import select
import struct
import sys
from ctypes import (
Structure,
c_char,
c_int,
c_ubyte,
c_uint16,
c_uint32,
c_uint8,
)
from typing import Set
from . import base
# Don't typecheck this file on Windows
assert sys.platform != "win32" # nosec
from fcntl import ioctl # noqa: E402
logger = logging.getLogger(__name__)
USB_MAX_DEVNAMELEN = 16
USB_MAX_DEVNAMES = 4
USB_MAX_STRING_LEN = 128
USB_MAX_ENCODED_STRING_LEN = USB_MAX_STRING_LEN * 3
class usb_ctl_report_desc(Structure):
_fields_ = [
("ucrd_size", c_int),
("ucrd_data", c_ubyte * 1024),
]
class usb_device_info(Structure):
_fields_ = [
("udi_bus", c_uint8),
("udi_addr", c_uint8),
("udi_pad0", c_uint8 * 2),
("udi_cookie", c_uint32),
("udi_product", c_char * USB_MAX_ENCODED_STRING_LEN),
("udi_vendor", c_char * USB_MAX_ENCODED_STRING_LEN),
("udi_release", c_char * 8),
("udi_serial", c_char * USB_MAX_ENCODED_STRING_LEN),
("udi_productNo", c_uint16),
("udi_vendorNo", c_uint16),
("udi_releaseNo", c_uint16),
("udi_class", c_uint8),
("udi_subclass", c_uint8),
("udi_protocol", c_uint8),
("udi_config", c_uint8),
("udi_speed", c_uint8),
("udi_pad1", c_uint8),
("udi_power", c_int),
("udi_nports", c_int),
("udi_devnames", c_char * USB_MAX_DEVNAMES * USB_MAX_DEVNAMELEN),
("udi_ports", c_uint8 * 16),
]
USB_GET_DEVICE_INFO = 0x44F45570 # _IOR('U', 112, struct usb_device_info)
USB_GET_REPORT_DESC = 0x44045515 # _IOR('U', 21, struct usb_ctl_report_desc)
USB_HID_SET_RAW = 0x80046802 # _IOW('h', 2, int)
# Cache for continuously failing devices
# XXX not thread-safe
_failed_cache: Set[str] = set()
def list_descriptors():
stale = set(_failed_cache)
descriptors = []
for i in range(100):
path = "/dev/uhid%d" % (i,)
stale.discard(path)
try:
desc = get_descriptor(path)
except OSError as e:
if e.errno == errno.ENOENT:
break
if path not in _failed_cache:
logger.debug("Failed opening FIDO device %s", path, exc_info=True)
_failed_cache.add(path)
continue
except Exception:
if path not in _failed_cache:
logger.debug("Failed opening FIDO device %s", path, exc_info=True)
_failed_cache.add(path)
continue
descriptors.append(desc)
_failed_cache.difference_update(stale)
return descriptors
def get_descriptor(path):
fd = None
try:
fd = os.open(path, os.O_RDONLY | os.O_CLOEXEC)
devinfo = usb_device_info()
ioctl(fd, USB_GET_DEVICE_INFO, devinfo)
ucrd = usb_ctl_report_desc()
ioctl(fd, USB_GET_REPORT_DESC, ucrd)
report_desc = bytearray(ucrd.ucrd_data[: ucrd.ucrd_size])
maxin, maxout = base.parse_report_descriptor(report_desc)
vid = devinfo.udi_vendorNo
pid = devinfo.udi_productNo
try:
name = devinfo.udi_product.decode("utf-8")
except UnicodeDecodeError:
name = None
try:
serial = devinfo.udi_serial.decode("utf-8")
except UnicodeDecodeError:
serial = None
return base.HidDescriptor(path, vid, pid, maxin, maxout, name, serial)
finally:
if fd is not None:
os.close(fd)
def open_connection(descriptor):
return NetBSDCtapHidConnection(descriptor)
class NetBSDCtapHidConnection(base.FileCtapHidConnection):
def __init__(self, descriptor):
# XXX racy -- device can change identity now that it has been
# closed
super().__init__(descriptor)
try:
ioctl(self.handle, USB_HID_SET_RAW, struct.pack("@i", 1))
ping = bytearray(64)
ping[0:7] = bytearray([0xFF, 0xFF, 0xFF, 0xFF, 0x81, 0, 1])
for i in range(10):
self.write_packet(ping)
poll = select.poll()
poll.register(self.handle, select.POLLIN)
if poll.poll(100):
self.read_packet()
break
else:
raise Exception("u2f ping timeout")
except Exception:
self.close()
raise

View File

@ -199,10 +199,10 @@ class MetadataStatement(_CamelCaseDataObject):
user_verification_details: Sequence[Sequence[VerificationMethodDescriptor]] = field(
metadata=dict(serialize=lambda xss: [[dict(x) for x in xs] for xs in xss])
)
key_protection: int
matcher_protection: int
attachment_hint: int
tc_display: int
key_protection: Sequence[str]
matcher_protection: Sequence[str]
attachment_hint: Sequence[str]
tc_display: Sequence[str]
attestation_root_certificates: Sequence[bytes] = field(
metadata=dict(
deserialize=lambda xs: [b64decode(x) for x in xs],
@ -363,7 +363,7 @@ class MdsAttestationVerifier(AttestationVerifier):
blob: MetadataBlobPayload,
entry_filter: Optional[EntryFilter] = filter_revoked,
attestation_filter: Optional[LookupFilter] = filter_attestation_key_compromised,
attestation_types: Sequence[Attestation] = None,
attestation_types: Optional[Sequence[Attestation]] = None,
):
super().__init__(attestation_types)
self._attestation_filter = attestation_filter or (

File diff suppressed because it is too large Load Diff

0
fido2/py.typed Normal file
View File

View File

@ -61,9 +61,12 @@ def verify_rp_id(rp_id: str, origin: str) -> bool:
return False
url = urlparse(origin)
if url.scheme != "https":
return False
host = url.hostname
# Note that Webauthn requires a secure context, i.e. an origin with https scheme.
# However, most browsers also treat http://localhost as a secure context. See
# https://groups.google.com/a/chromium.org/g/blink-dev/c/RC9dSw-O3fE/m/E3_0XaT0BAAJ
if url.scheme != "https" and (url.scheme, host) != ("http", "localhost"):
return False
if host == rp_id:
return True
if host and host.endswith("." + rp_id) and rp_id not in suffixes:

View File

@ -47,13 +47,17 @@ from .webauthn import (
UserVerificationRequirement,
ResidentKeyRequirement,
AuthenticatorAttachment,
RegistrationResponse,
AuthenticationResponse,
CredentialCreationOptions,
CredentialRequestOptions,
)
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
from typing import Sequence, Mapping, Optional, Callable, Union, Tuple, Any, overload
import os
import logging
@ -105,9 +109,11 @@ def _wrap_credentials(
if creds is None:
return None
return [
to_descriptor(c)
if isinstance(c, AttestedCredentialData)
else PublicKeyCredentialDescriptor.from_dict(c)
(
to_descriptor(c)
if isinstance(c, AttestedCredentialData)
else PublicKeyCredentialDescriptor.from_dict(c)
)
for c in creds
]
@ -159,7 +165,7 @@ class Fido2Server:
authenticator_attachment: Optional[AuthenticatorAttachment] = None,
challenge: Optional[bytes] = None,
extensions=None,
) -> Tuple[Mapping[str, Any], Any]:
) -> Tuple[CredentialCreationOptions, Any]:
"""Return a PublicKeyCredentialCreationOptions registration object and
the internal state dictionary that needs to be passed as is to the
corresponding `register_complete` call.
@ -186,40 +192,54 @@ class Fido2Server:
)
return (
{
"publicKey": PublicKeyCredentialCreationOptions(
CredentialCreationOptions(
PublicKeyCredentialCreationOptions(
self.rp,
user,
challenge,
self.allowed_algorithms,
self.timeout,
descriptors,
AuthenticatorSelectionCriteria(
authenticator_attachment,
resident_key_requirement,
user_verification,
)
if any(
(
(
AuthenticatorSelectionCriteria(
authenticator_attachment,
resident_key_requirement,
user_verification,
)
)
else None,
if any(
(
authenticator_attachment,
resident_key_requirement,
user_verification,
)
)
else None
),
self.attestation,
extensions,
)
},
),
state,
)
@overload
def register_complete(
self,
state,
response: Union[RegistrationResponse, Mapping[str, Any]],
) -> AuthenticatorData:
pass
@overload
def register_complete(
self,
state,
client_data: CollectedClientData,
attestation_object: AttestationObject,
) -> AuthenticatorData:
pass
def register_complete(self, state, *args, **kwargs):
"""Verify the correctness of the registration data received from
the client.
@ -229,6 +249,24 @@ class Fido2Server:
:param attestation_object: The attestation object.
:return: The authenticator data
"""
response = None
if len(args) == 1 and not kwargs:
response = args[0]
elif set(kwargs) == {"response"} and not args:
response = kwargs["response"]
if response:
registration = RegistrationResponse.from_dict(response)
client_data = registration.response.client_data
attestation_object = registration.response.attestation_object
else:
names = ["client_data", "attestation_object"]
pos = dict(zip(names, args))
data = {**kwargs, **pos}
if set(kwargs) & set(pos) or set(data) != set(names):
raise TypeError("incorrect arguments passed to register_complete()")
client_data = data[names[0]]
attestation_object = data[names[1]]
if client_data.type != CollectedClientData.TYPE.CREATE:
raise ValueError("Incorrect type in CollectedClientData.")
if not self._verify(client_data.origin):
@ -274,7 +312,7 @@ class Fido2Server:
user_verification: Optional[UserVerificationRequirement] = None,
challenge: Optional[bytes] = None,
extensions=None,
) -> Tuple[Mapping[str, Any], Any]:
) -> Tuple[CredentialRequestOptions, Any]:
"""Return a PublicKeyCredentialRequestOptions assertion object and the internal
state dictionary that needs to be passed as is to the corresponding
`authenticate_complete` call.
@ -297,8 +335,8 @@ class Fido2Server:
)
return (
{
"publicKey": PublicKeyCredentialRequestOptions(
CredentialRequestOptions(
PublicKeyCredentialRequestOptions(
challenge,
self.timeout,
self.rp.id,
@ -306,10 +344,20 @@ class Fido2Server:
user_verification,
extensions,
)
},
),
state,
)
@overload
def authenticate_complete(
self,
state,
credentials: Sequence[AttestedCredentialData],
response: Union[AuthenticationResponse, Mapping[str, Any]],
) -> AttestedCredentialData:
pass
@overload
def authenticate_complete(
self,
state,
@ -319,6 +367,9 @@ class Fido2Server:
auth_data: AuthenticatorData,
signature: bytes,
) -> AttestedCredentialData:
pass
def authenticate_complete(self, state, credentials, *args, **kwargs):
"""Verify the correctness of the assertion data received from
the client.
@ -329,6 +380,29 @@ class Fido2Server:
:param client_data: The client data.
:param auth_data: The authenticator data.
:param signature: The signature provided by the client."""
response = None
if len(args) == 1 and not kwargs:
response = args[0]
elif set(kwargs) == {"response"} and not args:
response = kwargs["response"]
if response:
authentication = AuthenticationResponse.from_dict(response)
credential_id = authentication.id
client_data = authentication.response.client_data
auth_data = authentication.response.authenticator_data
signature = authentication.response.signature
else:
names = ["credential_id", "client_data", "auth_data", "signature"]
pos = dict(zip(names, args))
data = {**kwargs, **pos}
if set(kwargs) & set(pos) or set(data) != set(names):
raise TypeError("incorrect arguments passed to authenticate_complete()")
credential_id = data[names[0]]
client_data = data[names[1]]
auth_data = data[names[2]]
signature = data[names[3]]
if client_data.type != CollectedClientData.TYPE.GET:
raise ValueError("Incorrect type in CollectedClientData.")
if not self._verify(client_data.origin):
@ -376,9 +450,12 @@ def verify_app_id(app_id: str, origin: str) -> bool:
: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
# Note that FIDO U2F requires a secure context, i.e. an origin with https scheme.
# However, most browsers also treat http://localhost as a secure context. See
# https://groups.google.com/a/chromium.org/g/blink-dev/c/RC9dSw-O3fE/m/E3_0XaT0BAAJ
if url.scheme != "https" and (url.scheme, hostname) != ("http", "localhost"):
return False
if not hostname:
return False
return verify_rp_id(hostname, origin)

View File

@ -49,6 +49,7 @@ from typing import (
get_type_hints,
)
import struct
import warnings
__all__ = [
"websafe_encode",
@ -121,6 +122,13 @@ def websafe_decode(data: Union[str, bytes]) -> bytes:
"""
if isinstance(data, str):
data = data.encode("ascii")
else:
warnings.warn(
"Calling websafe_decode on a byte value is deprecated, "
"and will no longer be allowed starting in python-fido2 2.0",
DeprecationWarning,
)
data += b"=" * (-len(data) % 4)
return urlsafe_b64decode(data)
@ -175,30 +183,52 @@ def _parse_value(t, value):
t = t.__args__[0]
return [_parse_value(t, v) for v in value]
# Handle Mappings
if issubclass(getattr(t, "__origin__", object), Mapping) and isinstance(
value, Mapping
):
return value
# Check if type is already correct
try:
if issubclass(t, _DataClassMapping) and not isinstance(value, t):
# Recursively call from_dict for nested _DataClassMappings
return t.from_dict(value)
if not isinstance(value, t):
# Convert to enum values, other wrappers
return t(value)
if isinstance(value, t):
return value
except TypeError:
pass
return value
# Check for subclass of _DataClassMapping
try:
is_dataclass = issubclass(t, _DataClassMapping)
except TypeError:
is_dataclass = False
if is_dataclass:
# Recursively call from_dict for nested _DataClassMappings
return t.from_dict(value)
# Convert to enum values, other wrappers
return t(value)
_T = TypeVar("_T", bound=Hashable)
class _DataClassMapping(Mapping[_T, Any]):
# TODO: This requires Python 3.9, and fixes the tpye errors we now ignore
# __dataclass_fields__: ClassVar[Dict[str, Field[Any]]]
def __post_init__(self):
hints = get_type_hints(type(self))
for f in fields(self):
for f in fields(self): # type: ignore
value = getattr(self, f.name)
if value is None:
continue
value = _parse_value(hints[f.name], value)
try:
value = _parse_value(hints[f.name], value)
except (TypeError, KeyError, ValueError):
raise ValueError(
f"Error parsing field {f.name} for {self.__class__.__name__}"
)
object.__setattr__(self, f.name, value)
@classmethod
@ -207,7 +237,7 @@ class _DataClassMapping(Mapping[_T, Any]):
raise NotImplementedError()
def __getitem__(self, key):
for f in fields(self):
for f in fields(self): # type: ignore
if key == self._get_field_key(f):
value = getattr(self, f.name)
serialize = f.metadata.get("serialize")
@ -225,7 +255,7 @@ class _DataClassMapping(Mapping[_T, Any]):
def __iter__(self):
return (
self._get_field_key(f)
for f in fields(self)
for f in fields(self) # type: ignore
if getattr(self, f.name) is not None
)
@ -238,16 +268,22 @@ class _DataClassMapping(Mapping[_T, Any]):
return None
if isinstance(data, cls):
return data
if not isinstance(data, Mapping):
raise TypeError(
f"{cls.__name__}.from_dict called with non-Mapping data of type"
f"{type(data)}"
)
kwargs = {}
for f in fields(cls):
for f in fields(cls): # type: ignore
key = cls._get_field_key(f)
if key in data:
value = data[key]
deserialize = f.metadata.get("deserialize")
if deserialize:
value = deserialize(value)
kwargs[f.name] = value
if value is not None:
deserialize = f.metadata.get("deserialize")
if deserialize:
value = deserialize(value)
kwargs[f.name] = value
return cls(**kwargs)

View File

@ -36,6 +36,7 @@ from .utils import (
ByteBuffer,
_CamelCaseDataObject,
)
from .features import webauthn_json_mapping
from enum import Enum, EnumMeta, unique, IntFlag
from dataclasses import dataclass, field
from typing import Any, Mapping, Optional, Sequence, Tuple, Union, cast
@ -52,8 +53,8 @@ See the specification for a description and details on their usage.
class Aaguid(bytes):
def __init__(self, data):
if len(data) != 16:
def __init__(self, data: bytes):
if len(self) != 16:
raise ValueError("AAGUID must be 16 bytes")
def __bool__(self):
@ -83,7 +84,7 @@ class AttestedCredentialData(bytes):
credential_id: bytes
public_key: CoseKey
def __init__(self, _):
def __init__(self, _: bytes):
super().__init__()
parsed = AttestedCredentialData._parse(self)
@ -113,7 +114,7 @@ class AttestedCredentialData(bytes):
@classmethod
def create(
cls, aaguid: bytes, credential_id: bytes, public_key: CoseKey
) -> "AttestedCredentialData":
) -> AttestedCredentialData:
"""Create an AttestedCredentialData by providing its components.
:param aaguid: The AAGUID of the authenticator.
@ -129,7 +130,7 @@ class AttestedCredentialData(bytes):
)
@classmethod
def unpack_from(cls, data: bytes) -> Tuple["AttestedCredentialData", bytes]:
def unpack_from(cls, data: bytes) -> Tuple[AttestedCredentialData, bytes]:
"""Unpack an AttestedCredentialData from a byte string, returning it and
any remaining data.
@ -141,9 +142,7 @@ class AttestedCredentialData(bytes):
return cls.create(aaguid, cred_id, pub_key), rest
@classmethod
def from_ctap1(
cls, key_handle: bytes, public_key: bytes
) -> "AttestedCredentialData":
def from_ctap1(cls, key_handle: bytes, public_key: bytes) -> AttestedCredentialData:
"""Create an AttestatedCredentialData from a CTAP1 RegistrationData instance.
:param key_handle: The CTAP1 credential key_handle.
@ -169,15 +168,25 @@ class AuthenticatorData(bytes):
:ivar extensions: Authenticator extensions, if available.
"""
@unique
class FLAG(IntFlag):
"""Authenticator data flags
See https://www.w3.org/TR/webauthn/#sec-authenticator-data for details
"""
# Names used in WebAuthn
UP = 0x01
UV = 0x04
BE = 0x08
BS = 0x10
AT = 0x40
ED = 0x80
# Aliases (for historical purposes)
USER_PRESENT = 0x01
USER_VERIFIED = 0x04
BACKUP_ELIGIBILITY = 0x08
BACKUP_STATE = 0x10
ATTESTED = 0x40
EXTENSION_DATA = 0x80
@ -187,7 +196,7 @@ class AuthenticatorData(bytes):
credential_data: Optional[AttestedCredentialData]
extensions: Optional[Mapping]
def __init__(self, _):
def __init__(self, _: bytes):
super().__init__()
reader = ByteBuffer(self)
@ -196,13 +205,13 @@ class AuthenticatorData(bytes):
object.__setattr__(self, "counter", reader.unpack(">I"))
rest = reader.read()
if self.flags & AuthenticatorData.FLAG.ATTESTED:
if self.flags & AuthenticatorData.FLAG.AT:
credential_data, rest = AttestedCredentialData.unpack_from(rest)
else:
credential_data = None
object.__setattr__(self, "credential_data", credential_data)
if self.flags & AuthenticatorData.FLAG.EXTENSION_DATA:
if self.flags & AuthenticatorData.FLAG.ED:
extensions, rest = cbor.decode_from(rest)
else:
extensions = None
@ -241,36 +250,28 @@ class AuthenticatorData(bytes):
)
def is_user_present(self) -> bool:
"""Return true if the User Present flag is set.
:return: True if User Present is set, False otherwise.
:rtype: bool
"""
return bool(self.flags & AuthenticatorData.FLAG.USER_PRESENT)
"""Return true if the User Present flag is set."""
return bool(self.flags & AuthenticatorData.FLAG.UP)
def is_user_verified(self) -> bool:
"""Return true if the User Verified flag is set.
"""Return true if the User Verified flag is set."""
return bool(self.flags & AuthenticatorData.FLAG.UV)
:return: True if User Verified is set, False otherwise.
:rtype: bool
"""
return bool(self.flags & AuthenticatorData.FLAG.USER_VERIFIED)
def is_backup_eligible(self) -> bool:
"""Return true if the Backup Eligibility flag is set."""
return bool(self.flags & AuthenticatorData.FLAG.BE)
def is_backed_up(self) -> bool:
"""Return true if the Backup State flag is set."""
return bool(self.flags & AuthenticatorData.FLAG.BS)
def is_attested(self) -> bool:
"""Return true if the Attested credential data flag is set.
:return: True if Attested credential data is set, False otherwise.
:rtype: bool
"""
return bool(self.flags & AuthenticatorData.FLAG.ATTESTED)
"""Return true if the Attested credential data flag is set."""
return bool(self.flags & AuthenticatorData.FLAG.AT)
def has_extension_data(self) -> bool:
"""Return true if the Extenstion data flag is set.
:return: True if Extenstion data is set, False otherwise.
:rtype: bool
"""
return bool(self.flags & AuthenticatorData.FLAG.EXTENSION_DATA)
"""Return true if the Extenstion data flag is set."""
return bool(self.flags & AuthenticatorData.FLAG.ED)
@dataclass(init=False, frozen=True)
@ -287,7 +288,7 @@ class AttestationObject(bytes): # , Mapping[str, Any]):
auth_data: AuthenticatorData
att_stmt: Mapping[str, Any]
def __init__(self, _):
def __init__(self, _: bytes):
super().__init__()
data = cast(Mapping[str, Any], cbor.decode(bytes(self)))
@ -321,7 +322,7 @@ class AttestationObject(bytes): # , Mapping[str, Any]):
"fido-u2f",
AuthenticatorData.create(
app_param,
AuthenticatorData.FLAG.ATTESTED | AuthenticatorData.FLAG.USER_PRESENT,
AuthenticatorData.FLAG.AT | AuthenticatorData.FLAG.UP,
0,
AttestedCredentialData.from_ctap1(
registration.key_handle, registration.public_key
@ -343,7 +344,7 @@ class CollectedClientData(bytes):
origin: str
cross_origin: bool = False
def __init__(self, *args):
def __init__(self, _: bytes):
super().__init__()
data = json.loads(self.decode())
@ -371,7 +372,7 @@ class CollectedClientData(bytes):
"type": type,
"challenge": encoded_challenge,
"origin": origin,
"cross_origin": cross_origin,
"crossOrigin": cross_origin,
**kwargs,
},
separators=(",", ":"),
@ -408,6 +409,12 @@ class _StringEnum(str, Enum, metaclass=_StringEnumMeta):
"""
_b64_metadata = dict(
serialize=lambda x: websafe_encode(x) if webauthn_json_mapping.enabled else x,
deserialize=lambda x: websafe_decode(x) if webauthn_json_mapping.enabled else x,
)
@unique
class AttestationConveyancePreference(_StringEnum):
NONE = "none"
@ -441,6 +448,7 @@ class AuthenticatorTransport(_StringEnum):
USB = "usb"
NFC = "nfc"
BLE = "ble"
HYBRID = "hybrid"
INTERNAL = "internal"
@ -463,7 +471,7 @@ class PublicKeyCredentialRpEntity(_CamelCaseDataObject):
@dataclass(eq=False, frozen=True)
class PublicKeyCredentialUserEntity(_CamelCaseDataObject):
name: str
id: bytes
id: bytes = field(metadata=_b64_metadata)
display_name: Optional[str] = None
@ -483,7 +491,7 @@ class PublicKeyCredentialParameters(_CamelCaseDataObject):
@dataclass(eq=False, frozen=True)
class PublicKeyCredentialDescriptor(_CamelCaseDataObject):
type: PublicKeyCredentialType
id: bytes
id: bytes = field(metadata=_b64_metadata)
transports: Optional[Sequence[AuthenticatorTransport]] = None
@classmethod
@ -508,9 +516,11 @@ class AuthenticatorSelectionCriteria(_CamelCaseDataObject):
object.__setattr__(
self,
"resident_key",
ResidentKeyRequirement.REQUIRED
if self.require_resident_key
else ResidentKeyRequirement.DISCOURAGED,
(
ResidentKeyRequirement.REQUIRED
if self.require_resident_key
else ResidentKeyRequirement.DISCOURAGED
),
)
object.__setattr__(
self,
@ -523,7 +533,7 @@ class AuthenticatorSelectionCriteria(_CamelCaseDataObject):
class PublicKeyCredentialCreationOptions(_CamelCaseDataObject):
rp: PublicKeyCredentialRpEntity
user: PublicKeyCredentialUserEntity
challenge: bytes
challenge: bytes = field(metadata=_b64_metadata)
pub_key_cred_params: Sequence[PublicKeyCredentialParameters] = field(
metadata=dict(deserialize=PublicKeyCredentialParameters._deserialize_list),
)
@ -539,12 +549,12 @@ class PublicKeyCredentialCreationOptions(_CamelCaseDataObject):
@dataclass(eq=False, frozen=True)
class PublicKeyCredentialRequestOptions(_CamelCaseDataObject):
challenge: bytes
challenge: bytes = field(metadata=_b64_metadata)
timeout: Optional[int] = None
rp_id: Optional[str] = None
allow_credentials: Optional[Sequence[PublicKeyCredentialDescriptor]] = field(
default=None,
metadata={"deserialize": PublicKeyCredentialDescriptor._deserialize_list},
metadata=dict(deserialize=PublicKeyCredentialDescriptor._deserialize_list),
)
user_verification: Optional[UserVerificationRequirement] = None
extensions: Optional[Mapping[str, Any]] = None
@ -552,16 +562,88 @@ class PublicKeyCredentialRequestOptions(_CamelCaseDataObject):
@dataclass(eq=False, frozen=True)
class AuthenticatorAttestationResponse(_CamelCaseDataObject):
client_data: bytes
attestation_object: AttestationObject
client_data: CollectedClientData = field(
metadata=dict(
_b64_metadata,
name="clientDataJSON",
)
)
attestation_object: AttestationObject = field(metadata=_b64_metadata)
extension_results: Optional[Mapping[str, Any]] = None
def __getitem__(self, key):
if key == "clientData" and not webauthn_json_mapping.enabled:
return self.client_data
return super().__getitem__(key)
@classmethod
def from_dict(cls, data: Optional[Mapping[str, Any]]):
if data is not None and not webauthn_json_mapping.enabled:
value = dict(data)
value["clientDataJSON"] = value.pop("clientData", None)
data = value
return super().from_dict(data)
@dataclass(eq=False, frozen=True)
class AuthenticatorAssertionResponse(_CamelCaseDataObject):
client_data: bytes
authenticator_data: AuthenticatorData
signature: bytes
user_handle: bytes
credential_id: bytes
client_data: CollectedClientData = field(
metadata=dict(
_b64_metadata,
name="clientDataJSON",
)
)
authenticator_data: AuthenticatorData = field(metadata=_b64_metadata)
signature: bytes = field(metadata=_b64_metadata)
user_handle: Optional[bytes] = field(metadata=_b64_metadata, default=None)
credential_id: Optional[bytes] = field(metadata=_b64_metadata, default=None)
extension_results: Optional[Mapping[str, Any]] = None
def __getitem__(self, key):
if key == "clientData" and not webauthn_json_mapping.enabled:
return self.client_data
return super().__getitem__(key)
@classmethod
def from_dict(cls, data: Optional[Mapping[str, Any]]):
if data is not None and not webauthn_json_mapping.enabled:
value = dict(data)
value["clientDataJSON"] = value.pop("clientData", None)
data = value
return super().from_dict(data)
@dataclass(eq=False, frozen=True)
class RegistrationResponse(_CamelCaseDataObject):
id: bytes = field(metadata=_b64_metadata)
response: AuthenticatorAttestationResponse
authenticator_attachment: Optional[AuthenticatorAttachment] = None
client_extension_results: Optional[Mapping] = None
type: Optional[PublicKeyCredentialType] = None
def __post_init__(self):
webauthn_json_mapping.require()
super().__post_init__()
@dataclass(eq=False, frozen=True)
class AuthenticationResponse(_CamelCaseDataObject):
id: bytes = field(metadata=_b64_metadata)
response: AuthenticatorAssertionResponse
authenticator_attachment: Optional[AuthenticatorAttachment] = None
client_extension_results: Optional[Mapping] = None
type: Optional[PublicKeyCredentialType] = None
def __post_init__(self):
webauthn_json_mapping.require()
super().__post_init__()
@dataclass(eq=False, frozen=True)
class CredentialCreationOptions(_CamelCaseDataObject):
public_key: PublicKeyCredentialCreationOptions
@dataclass(eq=False, frozen=True)
class CredentialRequestOptions(_CamelCaseDataObject):
public_key: PublicKeyCredentialRequestOptions

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "fido2"
version = "1.0.0"
version = "1.1.4-dev.0"
description = "FIDO2/WebAuthn library for implementing clients and servers."
authors = ["Dain Nilsson <dain@yubico.com>"]
homepage = "https://github.com/Yubico/python-fido2"
@ -31,8 +31,8 @@ include = [
]
[tool.poetry.dependencies]
python = "^3.7"
cryptography = ">=2.6, !=35, <40"
python = "^3.8"
cryptography = ">=2.6, !=35, <45"
pyscard = {version = "^1.9 || ^2", optional = true}
[tool.poetry.extras]

View File

@ -0,0 +1,3 @@
import fido2.features
fido2.features.webauthn_json_mapping.enabled = True

View File

@ -41,6 +41,7 @@ from fido2.attestation import (
UnsupportedType,
verify_x509_chain,
)
from cryptography.exceptions import UnsupportedAlgorithm, _Reasons
import unittest
@ -222,6 +223,14 @@ ee18128ed50dd7a855e54d2459db005""".replace(
"057a0ecbe7e3e99e8926941614f6af078c802b110be89eb221d69be2e17a1ba4"
)
try:
res = attestation.verify(statement, auth_data, client_param)
except UnsupportedAlgorithm as e:
if e._reason is _Reasons.UNSUPPORTED_HASH:
self.skipTest(
"SHA1 signature verification not supported on this machine"
)
res = attestation.verify(statement, auth_data, client_param)
self.assertEqual(res.attestation_type, AttestationType.ATT_CA)
verify_x509_chain(res.trust_path)

View File

@ -160,7 +160,7 @@ class TestCborTestVectors(unittest.TestCase):
"""
def test_vectors(self):
for (data, value) in _TEST_VECTORS:
for data, value in _TEST_VECTORS:
try:
self.assertEqual(cbor.decode_from(bytes.fromhex(data)), (value, b""))
self.assertEqual(cbor.decode(bytes.fromhex(data)), value)

View File

@ -30,7 +30,7 @@
import unittest
from unittest import mock
from fido2 import cbor
from fido2.utils import sha256
from fido2.utils import sha256, websafe_encode
from fido2.hid import CAPABILITY
from fido2.ctap import CtapError
from fido2.ctap1 import RegistrationData
@ -42,6 +42,7 @@ from fido2.webauthn import (
CollectedClientData,
)
APP_ID = "https://foo.example.com"
REG_DATA = RegistrationData(
bytes.fromhex(
@ -50,7 +51,7 @@ REG_DATA = RegistrationData(
)
rp = {"id": "example.com", "name": "Example RP"}
user = {"id": b"user_id", "name": "A. User"}
user = {"id": websafe_encode(b"user_id"), "name": "A. User"}
challenge = b"Y2hhbGxlbmdl"
_INFO_NO_PIN = bytes.fromhex(
"a60182665532465f5632684649444f5f325f3002826375766d6b686d61632d7365637265740350f8a011f38c0a4d15800617111f9edc7d04a462726bf5627570f564706c6174f469636c69656e7450696ef4051904b0068101" # noqa E501

View File

@ -8,6 +8,7 @@ from fido2.webauthn import (
AttestedCredentialData,
AuthenticatorData,
)
from fido2.utils import websafe_encode
from .test_ctap2 import _ATT_CRED_DATA, _CRED_ID
from .utils import U2FDevice
@ -95,7 +96,7 @@ class TestFido2Server(unittest.TestCase):
challenge = b"1234567890123456"
request, state = server.register_begin(USER, challenge=challenge)
self.assertEqual(request["publicKey"]["challenge"], challenge)
self.assertEqual(request["publicKey"]["challenge"], websafe_encode(challenge))
def test_register_begin_custom_challenge_too_short(self):
rp = PublicKeyCredentialRpEntity("Example", "example.com")

View File

@ -37,8 +37,10 @@ from fido2.webauthn import (
PublicKeyCredentialCreationOptions,
PublicKeyCredentialRequestOptions,
)
from fido2.utils import websafe_encode
import unittest
import json
class TestAaguid(unittest.TestCase):
@ -101,6 +103,14 @@ class TestWebAuthnDataTypes(unittest.TestCase):
"https://demo.yubico.com",
)
o = CollectedClientData.create(
"webauthn.create",
"cdySOP-1JI4J_BpOeO9ut25rlZJueF16aO6auTTYAis",
"https://demo.yubico.com",
True,
)
assert o.cross_origin is True
def test_authenticator_selection_criteria(self):
o = AuthenticatorSelectionCriteria(
"platform", require_resident_key=True, user_verification="required"
@ -175,7 +185,12 @@ class TestWebAuthnDataTypes(unittest.TestCase):
def test_user_entity(self):
o = PublicKeyCredentialUserEntity("Example", b"user", display_name="Display")
self.assertEqual(
o, {"id": b"user", "name": "Example", "displayName": "Display"}
o,
{
"id": websafe_encode(b"user"),
"name": "Example",
"displayName": "Display",
},
)
self.assertEqual(o.id, b"user")
self.assertEqual(o.name, "Example")
@ -204,7 +219,9 @@ class TestWebAuthnDataTypes(unittest.TestCase):
def test_descriptor(self):
o = PublicKeyCredentialDescriptor("public-key", b"credential_id")
self.assertEqual(o, {"type": "public-key", "id": b"credential_id"})
self.assertEqual(
o, {"type": "public-key", "id": websafe_encode(b"credential_id")}
)
self.assertEqual(o.type, "public-key")
self.assertEqual(o.id, b"credential_id")
self.assertIsNone(o.transports)
@ -216,7 +233,7 @@ class TestWebAuthnDataTypes(unittest.TestCase):
o,
{
"type": "public-key",
"id": b"credential_id",
"id": websafe_encode(b"credential_id"),
"transports": ["usb", "nfc"],
},
)
@ -235,12 +252,12 @@ class TestWebAuthnDataTypes(unittest.TestCase):
def test_creation_options(self):
o = PublicKeyCredentialCreationOptions(
{"id": "example.com", "name": "Example"},
{"id": b"user_id", "name": "A. User"},
PublicKeyCredentialRpEntity(id="example.com", name="Example"),
PublicKeyCredentialUserEntity(id=b"user_id", name="A. User"),
b"request_challenge",
[{"type": "public-key", "alg": -7}],
10000,
[{"type": "public-key", "id": b"credential_id"}],
[{"type": "public-key", "id": websafe_encode(b"credential_id")}],
{
"authenticatorAttachment": "platform",
"residentKey": "required",
@ -249,15 +266,23 @@ class TestWebAuthnDataTypes(unittest.TestCase):
"direct",
)
self.assertEqual(o.rp, {"id": "example.com", "name": "Example"})
self.assertEqual(o.user, {"id": b"user_id", "name": "A. User"})
self.assertEqual(o.user, {"id": websafe_encode(b"user_id"), "name": "A. User"})
self.assertIsNone(o.extensions)
o = PublicKeyCredentialCreationOptions(
{"id": "example.com", "name": "Example"},
{"id": b"user_id", "name": "A. User"},
b"request_challenge",
[{"type": "public-key", "alg": -7}],
js = json.dumps(dict(o))
o2 = PublicKeyCredentialCreationOptions.from_dict(json.loads(js))
self.assertEqual(o, o2)
o = PublicKeyCredentialCreationOptions.from_dict(
{
"rp": {"id": "example.com", "name": "Example"},
"user": {"id": websafe_encode(b"user_id"), "name": "A. User"},
"challenge": websafe_encode(b"request_challenge"),
"pubKeyCredParams": [{"type": "public-key", "alg": -7}],
}
)
self.assertEqual(o.user.id, b"user_id")
self.assertEqual(o.challenge, b"request_challenge"),
self.assertIsNone(o.timeout)
self.assertIsNone(o.authenticator_selection)
self.assertIsNone(o.attestation)
@ -265,19 +290,24 @@ class TestWebAuthnDataTypes(unittest.TestCase):
self.assertIsNone(
PublicKeyCredentialCreationOptions(
{"id": "example.com", "name": "Example"},
{"id": b"user_id", "name": "A. User"},
{"id": websafe_encode(b"user_id"), "name": "A. User"},
b"request_challenge",
[{"type": "public-key", "alg": -7}],
attestation="invalid",
).attestation
)
js = json.dumps(dict(o))
o2 = PublicKeyCredentialCreationOptions.from_dict(json.loads(js))
self.assertEqual(o, o2)
def test_request_options(self):
o = PublicKeyCredentialRequestOptions(
b"request_challenge",
10000,
"example.com",
[{"type": "public-key", "id": b"credential_id"}],
[PublicKeyCredentialDescriptor(type="public-key", id=b"credential_id")],
"discouraged",
)
self.assertEqual(o.challenge, b"request_challenge")
@ -285,6 +315,10 @@ class TestWebAuthnDataTypes(unittest.TestCase):
self.assertEqual(o.timeout, 10000)
self.assertIsNone(o.extensions)
js = json.dumps(dict(o))
o2 = PublicKeyCredentialRequestOptions.from_dict(json.loads(js))
self.assertEqual(o, o2)
o = PublicKeyCredentialRequestOptions(b"request_challenge")
self.assertIsNone(o.timeout)
self.assertIsNone(o.rp_id)

View File

@ -0,0 +1,121 @@
# Copyright (c) 2019 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.
from fido2.webauthn import (
PublicKeyCredentialUserEntity,
PublicKeyCredentialCreationOptions,
PublicKeyCredentialRequestOptions,
)
from fido2.features import webauthn_json_mapping
import unittest
class TestLegacyMapping(unittest.TestCase):
@classmethod
def setUpClass(cls):
webauthn_json_mapping._enabled = False
@classmethod
def tearDownClass(cls):
webauthn_json_mapping._enabled = True
def test_user_entity(self):
o = PublicKeyCredentialUserEntity("Example", b"user", display_name="Display")
self.assertEqual(
o, {"id": b"user", "name": "Example", "displayName": "Display"}
)
self.assertEqual(o.id, b"user")
self.assertEqual(o.name, "Example")
self.assertEqual(o.display_name, "Display")
def test_creation_options(self):
o = PublicKeyCredentialCreationOptions(
{"id": "example.com", "name": "Example"},
{"id": b"user_id", "name": "A. User"},
b"request_challenge",
[{"type": "public-key", "alg": -7}],
10000,
[{"type": "public-key", "id": b"credential_id"}],
{
"authenticatorAttachment": "platform",
"residentKey": "required",
"userVerification": "required",
},
"direct",
)
self.assertEqual(o.rp, {"id": "example.com", "name": "Example"})
self.assertEqual(o.user, {"id": b"user_id", "name": "A. User"})
self.assertIsNone(o.extensions)
o2 = PublicKeyCredentialCreationOptions.from_dict(dict(o))
self.assertEqual(o, o2)
o = PublicKeyCredentialCreationOptions(
{"id": "example.com", "name": "Example"},
{"id": b"user_id", "name": "A. User"},
b"request_challenge",
[{"type": "public-key", "alg": -7}],
)
self.assertIsNone(o.timeout)
self.assertIsNone(o.authenticator_selection)
self.assertIsNone(o.attestation)
self.assertIsNone(
PublicKeyCredentialCreationOptions(
{"id": "example.com", "name": "Example"},
{"id": b"user_id", "name": "A. User"},
b"request_challenge",
[{"type": "public-key", "alg": -7}],
attestation="invalid",
).attestation
)
def test_request_options(self):
o = PublicKeyCredentialRequestOptions(
b"request_challenge",
10000,
"example.com",
[{"type": "public-key", "id": b"credential_id"}],
"discouraged",
)
self.assertEqual(o.challenge, b"request_challenge")
self.assertEqual(o.rp_id, "example.com")
self.assertEqual(o.timeout, 10000)
self.assertIsNone(o.extensions)
o = PublicKeyCredentialRequestOptions(b"request_challenge")
self.assertIsNone(o.timeout)
self.assertIsNone(o.rp_id)
self.assertIsNone(o.allow_credentials)
self.assertIsNone(o.user_verification)
self.assertIsNone(
PublicKeyCredentialRequestOptions(
b"request_challenge", user_verification="invalid"
).user_verification
)

View File

@ -38,7 +38,7 @@ class U2FDevice(object):
def sign(self, client_data):
authenticator_data = AuthenticatorData.create(
sha256(self.app_id), flags=AuthenticatorData.FLAG.USER_PRESENT, counter=0
sha256(self.app_id), flags=AuthenticatorData.FLAG.UP, counter=0
)
signature = self.priv_key.sign(