mirror of https://github.com/Yubico/python-fido2
Compare commits
95 Commits
Author | SHA1 | Date |
---|---|---|
Dain Nilsson | edcb00bc32 | |
Joost van Dijk | 8b979313e9 | |
Dain Nilsson | ee9bf59783 | |
Dain Nilsson | cfffe17e18 | |
Dain Nilsson | 77893c2fd5 | |
Dain Nilsson | 82f9d0765b | |
Dain Nilsson | 9240d6e53b | |
Dain Nilsson | b5686b3faf | |
Dain Nilsson | 6d13bd7b91 | |
Dain Nilsson | f1952aebb9 | |
Dain Nilsson | de1be496fe | |
Dain Nilsson | 1be5c0b5f6 | |
Dain Nilsson | bccf760c73 | |
Dain Nilsson | 4c6f7b68a2 | |
Dain Nilsson | 7a16dc4913 | |
Dain Nilsson | 8253b96f8e | |
Dain Nilsson | 98a57ab968 | |
Dain Nilsson | 34e354e508 | |
Dain Nilsson | 1c3e05f82d | |
Dain Nilsson | 2ce01289d8 | |
Dain Nilsson | 7179b439d3 | |
Dain Nilsson | 54569a37cc | |
Dain Nilsson | f36cade5e9 | |
Dain Nilsson | 9d8e8d167d | |
Dain Nilsson | a7f3c51aca | |
Dain Nilsson | a5357dd35c | |
Dain Nilsson | a5bd97c529 | |
Pol Henarejos | 4abb4a577f | |
Alexandre Detiste | 526e1ab7a0 | |
Dain Nilsson | 032dd8b853 | |
Dain Nilsson | 8c9f3f0200 | |
Dain Nilsson | aa3c5cd8e8 | |
Pol Henarejos | a40850adaf | |
Pol Henarejos | c8fd18d4b2 | |
Pol Henarejos | 2d7e9e1610 | |
Pol Henarejos | e82f231da9 | |
Dain Nilsson | 963eae041a | |
Dain Nilsson | be2e8904e8 | |
Dain Nilsson | 8067e90f89 | |
Dain Nilsson | d7304fa49f | |
Dain Nilsson | 5575d5838c | |
Dain Nilsson | ed9f50a117 | |
Dain Nilsson | f523839dab | |
Dain Nilsson | c7ebd878c0 | |
Dain Nilsson | 2d6c067689 | |
Dain Nilsson | cbe72665e1 | |
Dain Nilsson | 1143d471ef | |
Dain Nilsson | 54cee2216a | |
Dain Nilsson | e7eb53a73e | |
Dain Nilsson | c7f09de1b6 | |
Dain Nilsson | 50d0306a2d | |
Dain Nilsson | fc46e83f7c | |
Taylor R Campbell | fb6f77c5e4 | |
Dain Nilsson | 5dffcaa838 | |
Dain Nilsson | 46c095fc21 | |
Dain Nilsson | 737fd76a27 | |
Dain Nilsson | d52024ebef | |
Fabian Kaczmarczyck | 9c980040da | |
Dain Nilsson | 75977a9468 | |
Dain Nilsson | c6c3a68da0 | |
Dain Nilsson | bcefd244e6 | |
Dain Nilsson | 4d1782880c | |
Dain Nilsson | 60a309eea0 | |
Dain Nilsson | 8fdf098520 | |
Jon Janzen | 589e2f1c4b | |
Dain Nilsson | 667ff5588b | |
Dain Nilsson | b915870e72 | |
Dain Nilsson | 08e1c45c93 | |
Dain Nilsson | d3ec6174ae | |
Dain Nilsson | cfd7f6b0c0 | |
Dain Nilsson | 71a317b12d | |
Dain Nilsson | 026c6a7f8d | |
Dain Nilsson | 8c00d04945 | |
Dain Nilsson | e21341312c | |
Dain Nilsson | fabb844bce | |
Dain Nilsson | 791ef6eba9 | |
Dain Nilsson | 27cd3dda54 | |
Dain Nilsson | cd9e6cbd59 | |
Dain Nilsson | 103df1b456 | |
Dain Nilsson | ae048d06ff | |
Dain Nilsson | 15ec60a3c4 | |
Dain Nilsson | 4a07a4d004 | |
Dain Nilsson | 354672b9ce | |
Dain Nilsson | 709599f98c | |
Dain Nilsson | 8debc41942 | |
Dain Nilsson | f7e8c59649 | |
Dain Nilsson | fa5e9fcce5 | |
Dain Nilsson | 19bc5ce15a | |
Dain Nilsson | b674c2b5ac | |
Dain Nilsson | 5e40339a7c | |
Michael Gmelin | e1050b575b | |
Dain Nilsson | d8542e82de | |
Markus Meissner | 35db7f2f5c | |
Dain Nilsson | b546c0d629 | |
Jonathan Morrison | 1769dc1982 |
|
@ -0,0 +1,10 @@
|
|||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
groups:
|
||||
github-actions:
|
||||
patterns:
|
||||
- "*"
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -8,6 +8,7 @@ dist/
|
|||
.ropeproject/
|
||||
ChangeLog
|
||||
man/*.1
|
||||
poetry.lock
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
|
|
|
@ -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
33
NEWS
|
@ -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.
|
||||
|
||||
|
|
13
README.adoc
13
README.adoc
|
@ -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/.
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)))
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
]
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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__":
|
||||
|
|
|
@ -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()
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
|
@ -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>
|
|
@ -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; }
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
};
|
|
@ -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>
|
|
@ -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
|
@ -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
|
||||
|
|
|
@ -26,4 +26,4 @@
|
|||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
|
||||
__version__ = "1.0.0"
|
||||
__version__ = "1.1.4-dev.0"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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.
|
||||
""",
|
||||
)
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
|
@ -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
|
@ -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:
|
||||
|
|
123
fido2/server.py
123
fido2/server.py
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
import fido2.features
|
||||
|
||||
fido2.features.webauthn_json_mapping.enabled = True
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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(
|
||||
|
|
Loading…
Reference in New Issue