fido2-2.2.1/COPYING 0000644 0000000 0000000 00000002430 15220526030 010472 0 ustar 00 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:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* 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
OWNER 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.
fido2-2.2.1/COPYING.APLv2 0000644 0000000 0000000 00000026136 15220526030 011366 0 ustar 00
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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.
fido2-2.2.1/COPYING.MPLv2 0000644 0000000 0000000 00000040526 15220526030 011401 0 ustar 00 Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.
fido2-2.2.1/NEWS 0000644 0000000 0000000 00000030112 15220526030 010134 0 ustar 00 * Version 2.2.1 (released 2026-06-29)
** Server example: Migrate build tool from poetry to uv.
** Fix: Correctly format att_obj in previewSign.
* Version 2.2.0 (released 2026-04-15)
** Restrict DLL search paths (YSA-2026-01).
** Add support for experimental previewSign extension:
https://yubicolabs.github.io/webauthn-sign-extension/4/#sctn-sign-extension
** Add support for PSL wildcard and exception rules to RP validation.
** Fix: WindowsClient hmac_secret extension raising Null pointer access error.
** Fix: TPM attestation certificate Subject field validation.
* Version 2.1.1 (released 2026-01-19)
** Fix: Platform detection in fido2.hid module for BSD's.
* Version 2.1.0 (released 2026-01-14)
** CTAP 2.3 support:
*** Add new GetInfo fields: enc_cred_store_state.
*** Add support for pinComplexityPolicy extension.
*** Add thirdPartyPayment bit to credman.
*** Check support for config subcommands.
** WebAuthn:
*** Allow UserEntity without 'name' field for improved spec compliance.
*** Update MDS3 dataclasses with new fields.
** Fido2Client:
*** Fallback to PIN after UV_BLOCKED error.
*** Improve preflight handling when message exceeds maximum size.
** WindowsClient:
*** Fix: Parse 'credentialProtectionPolicy' properly.
*** Update win_api.py from latest webauthn.h.
*** Add support for hmac-secret-mc extension.
*** Add support for hints.
** Development:
*** Switch from Poetry to uv for project management.
*** Add pyright and ty for improved type checking.
*** Replace bandit and flake8 with ruff for linting.
* Version 2.0.0 (released 2025-05-20)
** See also the migration guide: doc/Migration_1-2.adoc.
** Python 3.10 or later is now required.
** WebAuthn dataclasses have been updated to align with the WebAuthn Level 3
Working Draft. Constructors now require keyword arguments (`kwargs_only=True`),
and serialization to/from dictionaries is compatible with standardized JSON
formats.
** The `features.webauthn_json_mapping` flag has been removed, as its
behavior (standardized JSON mapping) is now default.
** `Fido2Client` and `WindowsClient` constructors now accept a
`ClientDataCollector` instance instead of `origin` and `verify` parameters.
** `WindowsClient` has been relocated to `fido2.client.windows`. Importing this
class on non-Windows platforms will now raise an `ImportError`.
** `Fido2Client` methods now return `RegistrationResponse` and
`AuthenticationResponse` objects, instead of raw attestation/assertion data.
** CTAP2/WebAuthn extension handling has been redesigned. `Fido2Client` now
expects a list of `Ctap2Extension` instances. Default behavior includes
extensions commonly supported by browsers.
** The `fido2.cbor` module's `load_x` and `dump_x` functions have been made
private (renamed with a leading underscore) and should not be used directly.
** Previously deprecated functions and APIs have been removed.
** The `__version__` attribute in `fido2/__init__.py` has been removed. Use
`importlib.metadata.version('fido2')` to get the package version.
** Add new GetInfo fields based on CTAP 2.2.
** Add support for Persistent PinUvAuthToken and encIdentifier.
** Add support for hmac-secret-mc and thirdPartyPayments exensions.
** Update COSE algorithm types.
** Building the library now requires Poetry version 2.0 or later.
* Version 1.2.0 (released 2024-11-27)
** Improved extension handling:
Several new extensions are now supported, both for Fido2Client and WindowsClient.
Extension APIs have been redesigned, and old APIs have been deprecated, slated for
removal in version 2.0.
*** Disable hmac-secret extension by default, preferring prf.
** Improved (de-)serialization of dataclasses to/from JSON-friendly dicts.
** Fido2Client:
*** Support allowCredentials/excludeCredentials of arbitrary length.
*** Handle PUAT_REQUIRED by re-attempting with PIN/UV.
** Allow localhost (and subdomains) to use http:// in RP ID verification by default.
** NFC: Support for Authenticators that return SW=61XX on SELECT.
** USB: Improve connection recovery and use more specific exceptions for errors.
** Fix: Handle residentKey=preferred properly.
** Fix: Handle Authentictors that do not pass extensions in GetInfo.
* 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.
* Version 1.0.0rc1 (released 2022-05-02)
** Release Candidate 1 of first stable release.
** Require Python 3.7 or later.
** APIs have updated to align with WebAuthn level 2.
** Several CTAP 2.1 features have been implemented.
* Version 0.9.3 (released 2021-11-09)
** Bugfix: Linux - Don't fail device discovery when hidraw doesn't support
HIDIOCGRAWUNIQ (Linux kernels before 5.6).
* Version 0.9.2 (released 2021-10-14)
** Support the latest Windows webauthn.h API (included in Windows 11).
** Add product name and serial number to HidDescriptors.
** Remove the need for the uhid-freebsd dependency on FreeBSD.
* Version 0.9.1 (released 2021-02-03)
** Add new CTAP error codes and improve handling of unknown codes.
* Version 0.9.0 (released 2021-01-20)
** Server: Attestation is now done in two parts (to align better with the spec):
First, type-specific validation is done to provide a trust chain.
Second, validation of the trust chain is done.
** Client: API changes to better support extensions.
*** Fido2Client can be configured with Ctap2Extensions to support.
*** Client.make_credential now returns a AuthenticatorAttestationResponse,
which holds the AttestationObject and ClientData, as well as any client
extension results for the credential.
*** Client.get_assertion now returns an AssertionSelection object, which is
used to select between multiple assertions, resulting in an
AuthenticatorAssertionResponse, which holds the ClientData, assertion
values, as well as any client extension results for the assertion.
** Renames: The CTAP1 and CTAP2 classes have been renamed to Ctap1 and Ctap2,
respectively. The old names currently work, but will be removed in the
future.
** ClientPin: The ClientPin API has been restructured to support multiple PIN
protocols, UV tokens, and token permissions.
** CTAP 2.1 PRE: Several new features have been added for CTAP 2.1, including
Credential Management, Bio Enrollment, Large Blobs, and Authenticator Config.
** HID: The platform specific HID code has been revamped and cleaned up.
* Version 0.8.1 (released 2019-11-25)
** Bugfix: WindowsClient.make_credential error when resident key requirement is
unspecified.
* Version 0.8.0 (released 2019-11-25)
** New fido2.webauthn classes modeled after the W3C WebAuthn spec introduced.
** CTAP2 send_cbor/make_credential/get_assertion and U2fClient
request/authenticate `timeout` arguments replaced with `event` used to
cancel a request.
** Fido2Client:
*** make_credential/get_assertion now take WebAuthn options objects.
*** timeout is now provided in ms in WebAuthn options objects. Event based
cancelation also available by passing an Event.
** Fido2Server:
*** ATTESTATION, USER_VERIFICATION, and AUTHENTICATOR_ATTACHMENT enums
have been replaced with fido2.webauthn classes.
*** RelyingParty has been replaced with PublicKeyCredentialRpEntity, and
name is no longer optional.
*** Options returned by register_begin/authenticate_begin now omit unspecified
values if they are optional, instead of filling in default values.
*** Fido2Server.allowed_algorithms now contains a list of
PublicKeyCredentialParameters instead of algorithm identifiers.
*** Fido2Server.timeout is now in ms and of type int.
** Support native WebAuthn API on Windows through WindowsClient.
* Version 0.7.3 (released 2019-10-24)
** Bugfix: Workaround for size of int on Python 2 on Windows.
* Version 0.7.2 (released 2019-10-24)
** Support for the TPM attestation format.
** Allow passing custom challenges to register/authenticate in Fido2Server.
** Bugfix: CTAP2 CANCEL command response handling fixed.
** Bugfix: Fido2Client fix handling of empty allow_list.
** Bugfix: Fix typo in CTAP2.get_assertions() causing it to fail.
* Version 0.7.1 (released 2019-09-20)
** Support for FreeBSD.
** Enforce canonical CBOR on Authenticator responses by default.
** PCSC: Support extended APDUs.
** Server: Verify that UP flag is set.
** U2FFido2Server: Implement AppID exclusion extension.
** U2FFido2Server: Allow custom U2F facet verification.
** Bugfix: U2FFido2Server.authenticate_complete now returns the result.
* Version 0.7.0 (released 2019-06-17)
** Add support for NFC devices using PCSC.
** Add support for the hmac-secret Authenticator extension.
** Honor max credential ID length and number of credentials to Authenticator.
** Add close() method to CTAP devices to explicitly release their resources.
* Version 0.6.0 (released 2019-05-10)
** Don't fail if CTAP2 Info contains unknown fields.
** Replace cbor loads/dumps functions with encode/decode/decode_from.
** Server: Add support for AuthenticatorAttachment.
** Server: Add support for more key algorithms.
** Client: Expose CTAP2 Info object as Fido2Client.info.
* Version 0.5.0 (released 2018-12-21)
** Changes to server classes, some backwards breaking.
** Add ability to authenticate U2F credentials by using the appid extension.
** Make verification of attestation more explicit.
** Add support for Android SafetyNet attestation.
** Make it easier to work with U2F/CTAP1 data formats.
* Version 0.4.0 (released 2018-09-27)
** Add classes for implementing a server.
** Various small changes, some affecting backwards compatibility.
* Version 0.3.0 (released 2018-04-13)
** Add conversion between string/int keys for AttestationObject.
** Replace internal Exceptions with built-in types.
** Bugfix: Don't use TimeoutError which isn't available on Python 2.
* Version 0.2.2 (released 2018-04-11)
** Bugfix: Better handling of unplugged devices on MacOS and avoid leaking threads.
* Version 0.2.1 (released 2018-04-10)
** Add server example.
** Parse AttestationObjects that use string keys (Webauthn).
** Fix bug in handling packets with the wrong channel id.
* Version 0.2.0 (released 2018-04-05)
** Changed name of project to python-fido2 to better reflect its scope.
** Added attestation and assertion verification methods.
** A lot of name changes, moved classes, etc.
** New example for multi-device use.
* Version 0.1.0 (released 2018-03-16)
** First beta release.
fido2-2.2.1/README.adoc 0000644 0000000 0000000 00000010432 15220526030 011225 0 ustar 00 == python-fido2
image:https://github.com/Yubico/python-fido2/workflows/build/badge.svg["Github actions build", link="https://github.com/Yubico/python-fido2/actions"]
Provides library functionality for communicating with a FIDO device over USB as
well as verifying attestation and assertion signatures.
NOTE: Version 2.0 is now released. For help with migration from version 1.x, see
link:doc/Migration_1-2.adoc[the migration guide].
This library aims to support the FIDO U2F and FIDO 2 protocols for
communicating with a USB authenticator via the Client-to-Authenticator Protocol
(CTAP 1 and 2). In addition to this low-level device access, classes defined in
the `fido2.client` and `fido2.server` modules implement higher level operations
which are useful when interfacing with an Authenticator, or when implementing
WebAuthn support for a Relying Party.
For usage, see the `examples/` directory and
link:https://developers.yubico.com/python-fido2/API_Documentation/[API documentation].
=== References
These links related to WebAuthn and FIDO2 can help you get started:
* Yubico WebAuthn/FIDO2 guide: https://developers.yubico.com/FIDO2/
* W3C WebAuthn specification: https://www.w3.org/TR/webauthn/
* FIDO specifications: https://fidoalliance.org/specifications/download/
=== License
This project, with the exception of the files mentioned below, is licensed
under the BSD 2-clause license.
See the _COPYING_ file for the full license text.
This project contains source code from pyu2f (https://github.com/google/pyu2f)
which is licensed under the Apache License, version 2.0.
These files are located in `fido2/hid/`.
See http://www.apache.org/licenses/LICENSE-2.0,
or the _COPYING.APLv2_ file for the full license text.
This project also bundles the public suffix list (https://publicsuffix.org)
which is licensed under the Mozilla Public License, version 2.0.
This file is stored as `fido2/public_suffix_list.dat`.
See https://mozilla.org/MPL/2.0/,
or the _COPYING.MPLv2_ file for the full license text.
=== Requirements
fido2 is compatible with Python 3.10 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
To install the dependencies required for communication with NFC authenticators,
instead use:
pip install fido2[pcsc]
Under Windows 10 (1903 or later) access to FIDO devices is restricted and
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:
----
#Udev rule for allowing HID access to Yubico devices for FIDO support.
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/.
NFC support is optionally available via PC/SC, using the pyscard library. For
instructions on installing this dependency, see
https://github.com/LudovicRousseau/pyscard/blob/master/INSTALL.md.
=== Development
For development of the library we use https://docs.astral.sh/uv/[uv].
Follow the uv Getting Started guide to install and configure it.
We also use https://pre-commit.com/[pre-commit] to run some scans on the code
prior to committing.
==== Running tests
While some tests can run on their own, most require a connected FIDO2 device to run.
WARNING: These tests are destructive, and will factory reset the device under test.
As a safety precaution, the tests will only run on a device that is in a newly reset
state, as far as the test runner can tell.
uv run pytest
fido2-2.2.1/examples/acr122u.py 0000644 0000000 0000000 00000004465 15220526030 013020 0 ustar 00 import time
from fido2.pcsc import CtapPcscDevice
class Acr122uPcscDevice(object):
def __init__(self, pcsc_device):
self.pcsc = pcsc_device
def reader_version(self):
"""
Get reader's version from reader
:return: string. Reader's version
"""
try:
result, sw1, sw2 = self.pcsc.apdu_exchange(b"\xff\x00\x48\x00\x00")
if len(result) > 0:
str_result = result + bytes([sw1]) + bytes([sw2])
str_result = str_result.decode("utf-8")
return str_result
except Exception as e:
print("Get version error:", e)
return "n/a"
def led_control(
self,
red=False,
green=False,
blink_count=0,
red_end_blink=False,
green_end_blink=False,
):
"""
Reader's led control
:param red: boolean. red led on
:param green: boolean. green let on
:param blink_count: int. if needs to blink value > 0. blinks count
:param red_end_blink: boolean.
state of red led at the end of blinking
:param green_end_blink: boolean.
state of green led at the end of blinking
:return:
"""
try:
if blink_count > 0:
cbyte = (
0b00001100
+ (0b01 if red_end_blink else 0b00)
+ (0b10 if green_end_blink else 0b00)
)
cbyte |= (0b01000000 if red else 0b00000000) + (
0b10000000 if green else 0b00000000
)
else:
cbyte = 0b00001100 + (0b01 if red else 0b00) + (0b10 if green else 0b00)
apdu = (
b"\xff\x00\x40"
+ bytes([cbyte & 0xFF])
+ b"\4"
+ b"\5\3"
+ bytes([blink_count])
+ b"\0"
)
self.pcsc.apdu_exchange(apdu)
except Exception as e:
print("LED control error:", e)
dev = next(CtapPcscDevice.list_devices())
print("CONNECT: %s" % dev)
pcsc_device = Acr122uPcscDevice(dev)
pcsc_device.led_control(False, True, 0)
print("version: %s" % pcsc_device.reader_version())
pcsc_device.led_control(True, False, 0)
time.sleep(1)
pcsc_device.led_control(False, True, 3)
fido2-2.2.1/examples/acr122usam.py 0000644 0000000 0000000 00000024611 15220526030 013514 0 ustar 00 # Copyright (c) 2019 Yubico AB
# Copyright (c) 2019 Oleg Moiseenko
# 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.
"""
Sample work with reader:
ACR-122U-SAM or touchatag
drivers and manual link:
www.acs.com.hk/en/driver/100/acr122u-nfc-reader-with-sam-slot-proprietary/
"""
import time
from smartcard.Exceptions import CardConnectionException
from fido2.ctap1 import CTAP1
from fido2.pcsc import CtapPcscDevice
from fido2.utils import sha256
class Acr122uSamPcscDevice(CtapPcscDevice):
def __init__(self, connection, name):
self.ats = b""
self.vparity = False
self.max_block_len = 29
try:
super().__init__(connection, name)
except (CardConnectionException, ValueError):
pass
except Exception as e:
print(e.__class__)
# setup reader
if not self.set_auto_iso14443_4_activation():
raise Exception("Set automatic iso-14443-4 activation error")
if not self.set_default_retry_timeout():
raise Exception("Set default retry timeout error")
self.ats = self.get_ats()
if self.ats == b"":
raise Exception("No card in field")
self._select()
def apdu_plain(self, apdu, protocol=None):
"""Exchange data with reader.
:param apdu: byte string. data to exchange with card
:param protocol: protocol to exchange with card. usually set by default
:return: byte string. response from card
"""
# print('>> %s' % b2a_hex(apdu))
resp, sw1, sw2 = self._conn.transmit(list(iter(apdu)), protocol)
response = bytes(bytearray(resp))
# print('<< [0x%04x] %s' % (sw1 * 0x100 + sw2, b2a_hex(response)))
return response, sw1, sw2
def pseudo_apdu_ex(self, apdu, protocol=None):
req = b"\xff\x00\x00\x00" + bytes([len(apdu) & 0xFF]) + apdu
resp, sw1, sw2 = self.apdu_plain(req, protocol)
if sw1 != 0x61:
return resp, sw1, sw2
return self.apdu_plain(b"\xff\xc0\x00\x00" + bytes([sw2]), protocol)
# override base method
# commands in PN 532 User manual (UM0701-02)
# page 178. 7.4.5 DEP chaining mechanism
# page 136. 7.3.9 InCommunicateThru
# chaining ISO 14443-4:2001
# page 20. 7.5.2 Chaining
def apdu_exchange(self, apdu, protocol=None):
all_response = b""
alen = 0
while True:
vapdu = apdu[alen : alen + self.max_block_len]
# input chaining
chaining = alen + len(vapdu) < len(apdu)
vb = 0x02 | (0x01 if self.vparity else 0x00) | (0x10 if chaining else 0x00)
# 7.3.9 InCommunicateThru
resp, sw1, sw2 = self.pseudo_apdu_ex(
b"\xd4\x42" + bytes([vb]) + vapdu, protocol
)
self.vparity = not self.vparity
if len(resp) > 2 and resp[2] > 0:
print("Error: 0x%02x" % resp[2])
return b"", 0x6F, resp[2]
if sw1 != 0x90 or len(resp) < 3 or resp[0] != 0xD5 or resp[1] != 0x43:
return b"", 0x67, 0x00
alen += len(vapdu)
if not chaining:
break
if len(resp) > 3:
if resp[3] & 0x10 == 0:
return resp[4:-2], resp[-2], resp[-1]
else:
if resp[3] != 0xF2:
all_response = resp[4:]
else:
return b"", 0x90, 0x00
while True:
if len(resp) > 3 and resp[3] == 0xF2:
# WTX
answer = resp[3:5]
else:
# ACK
answer = bytes([0xA2 | (0x01 if self.vparity else 0x00)])
self.vparity = not self.vparity
# 7.3.9 InCommunicateThru
resp, sw1, sw2 = self.pseudo_apdu_ex(b"\xd4\x42" + answer, protocol)
if len(resp) > 2 and resp[2] > 0:
print("Error: 0x%02x" % resp[2])
return b"", 0x6F, resp[2]
if sw1 != 0x90 or len(resp) < 3 or resp[0] != 0xD5 or resp[1] != 0x43:
return b"", 0x67, 0x00
response_chaining = len(resp) > 3 and resp[3] & 0x10 != 0
# if I block
if len(resp) > 3 and resp[3] & 0xE0 == 0x00:
all_response += resp[4:]
if not response_chaining:
break
return all_response[:-2], resp[-2], resp[-1]
def get_ats(self, verbose=False):
self.field_reset()
self.ats = b""
resp, sw1, sw2 = self.pseudo_apdu_ex(b"\xd4\x4a\x01\x00")
if sw1 == 0x90 and len(resp) > 8 and resp[2] > 0x00:
if verbose:
print("ATQA 0x%02x%02x" % (resp[4], resp[5]))
print("SAK 0x%02x" % resp[6])
uid_len = resp[7]
if verbose:
print("UID [%d] %s" % (uid_len, resp[8 : 8 + uid_len].hex()))
self.ats = resp[8 + uid_len :]
if verbose:
print("ATS [%d] %s" % (len(self.ats), self.ats.hex()))
self.vparity = False
return self.ats
return b""
def set_default_retry_timeout(self):
result, sw1, sw2 = self.pseudo_apdu_ex(b"\xd4\x32\x05\x00\x00\x00")
if sw1 != 0x90 or sw2 != 0x00 or result != b"\xd5\x33":
print("set default retry time error")
return False
# 14443 timeout. UM0701-02 PN432 user manual. page 101.
# RFU, fATR_RES_Timeout, fRetryTimeout
# 0b 102ms, 0c - 204ms, 0d - 409ms, 0f - 1.6s
result, sw1, sw2 = self.pseudo_apdu_ex(b"\xd4\x32\x02\x00\x0c\x0f")
if sw1 != 0x90 or sw2 != 0x00 or result != b"\xd5\x33":
print("set fRetryTimeout error")
return False
return True
def set_auto_iso14443_4_activation(self, activate=True):
result, sw1, sw2 = self.pseudo_apdu_ex(
b"\xd4\x12" + bytes([0x34 if activate else 0x24])
)
if sw1 != 0x90 or sw2 != 0x00 or result != b"\xd5\x13":
print("set automatic iso-14443-4 activation error")
return False
return True
def field_control(self, field_on=True):
result, sw1, sw2 = self.pseudo_apdu_ex(
b"\xd4\x32\x01" + bytes([0x01 if field_on else 0x00])
)
if sw1 != 0x90 or sw2 != 0x00 or result != b"\xd5\x33":
print("set field state error")
return False
return True
def field_reset(self):
self.led_control(True, False)
result = self.field_control(False)
time.sleep(0.2)
result |= self.field_control(True)
self.led_control()
return result
def reader_version(self):
"""
Get reader's version from reader
:return: string. Reader's version
"""
try:
result, sw1, sw2 = self.apdu_plain(b"\xff\x00\x48\x00\x00")
if len(result) > 0:
str_result = result + bytes([sw1]) + bytes([sw2])
str_result = str_result.decode("utf-8")
return str_result
except Exception as e:
print("Get version error:", e)
return "n/a"
def led_control(
self,
red=False,
green=False,
blink_count=0,
red_end_blink=False,
green_end_blink=False,
):
"""
Reader's led control
:param red: boolean. red led on
:param green: boolean. green let on
:param blink_count: int. if needs to blink value > 0. blinks count
:param red_end_blink: boolean.
state of red led at the end of blinking
:param green_end_blink: boolean.
state of green led at the end of blinking
:return:
"""
try:
if blink_count > 0:
cbyte = (
0b00001100
+ (0b01 if red_end_blink else 0b00)
+ (0b10 if green_end_blink else 0b00)
)
cbyte |= (0b01000000 if red else 0b00000000) + (
0b10000000 if green else 0b00000000
)
else:
cbyte = 0b00001100 + (0b01 if red else 0b00) + (0b10 if green else 0b00)
apdu = (
b"\xff\x00\x40"
+ bytes([cbyte & 0xFF])
+ b"\4"
+ b"\5\3"
+ bytes([blink_count])
+ b"\0"
)
self.apdu_plain(apdu)
except Exception as e:
print("LED control error:", e)
dev = next(Acr122uSamPcscDevice.list_devices())
print("CONNECT: %s" % dev)
print("version: %s" % dev.reader_version())
print("atr: %s" % dev.get_atr().hex())
print("ats: %s" % dev.ats.hex())
# uncomment if you want to see parameters from card's selection
# dev.get_ats(True)
# dev._select()
dev.led_control(False, True, 0)
chal = sha256(b"AAA")
appid = sha256(b"BBB")
ctap1 = CTAP1(dev)
print("ctap1 version:", ctap1.get_version())
reg = ctap1.register(chal, appid)
print("u2f register:", reg)
reg.verify(appid, chal)
print("Register message verify OK")
auth = ctap1.authenticate(chal, appid, reg.key_handle)
print("u2f authenticate: ", auth)
res = auth.verify(appid, chal, reg.public_key)
print("Authenticate message verify OK")
dev.led_control()
fido2-2.2.1/examples/acr1252u.py 0000644 0000000 0000000 00000013045 15220526030 013077 0 ustar 00 import time
from fido2.pcsc import CtapPcscDevice
# control codes:
# 3225264 - magic number!!!
# 0x42000000 + 3500 - cross platform way
C_CODE = 3225264
class Acr1252uPcscDevice(object):
def __init__(self, pcsc_device):
self.pcsc = pcsc_device
def reader_version(self):
try:
res = self.pcsc.control_exchange(C_CODE, b"\xe0\x00\x00\x18\x00")
if len(res) > 0 and res.find(b"\xe1\x00\x00\x00") == 0:
reslen = res[4]
if reslen == len(res) - 5:
strres = res[5 : 5 + reslen].decode("utf-8")
return strres
except Exception as e:
print("Get version error:", e)
return "n/a"
def reader_serial_number(self):
try:
res = self.pcsc.control_exchange(C_CODE, b"\xe0\x00\x00\x33\x00")
if len(res) > 0 and res.find(b"\xe1\x00\x00\x00") == 0:
reslen = res[4]
if reslen == len(res) - 5:
strres = res[5 : 5 + reslen].decode("utf-8")
return strres
except Exception as e:
print("Get serial number error:", e)
return "n/a"
def led_control(self, red=False, green=False):
try:
cbyte = (0b01 if red else 0b00) + (0b10 if green else 0b00)
result = self.pcsc.control_exchange(
C_CODE, b"\xe0\x00\x00\x29\x01" + bytes([cbyte])
)
if len(result) > 0 and result.find(b"\xe1\x00\x00\x00") == 0:
result_length = result[4]
if result_length == 1:
ex_red = bool(result[5] & 0b01)
ex_green = bool(result[5] & 0b10)
return True, ex_red, ex_green
except Exception as e:
print("LED control error:", e)
return False, False, False
def led_status(self):
try:
result = self.pcsc.control_exchange(C_CODE, b"\xe0\x00\x00\x29\x00")
if len(result) > 0 and result.find(b"\xe1\x00\x00\x00") == 0:
result_length = result[4]
if result_length == 1:
ex_red = bool(result[5] & 0b01)
ex_green = bool(result[5] & 0b10)
return True, ex_red, ex_green
except Exception as e:
print("LED status error:", e)
return False, False, False
def get_polling_settings(self):
try:
res = self.pcsc.control_exchange(C_CODE, b"\xe0\x00\x00\x23\x00")
if len(res) > 0 and res.find(b"\xe1\x00\x00\x00") == 0:
reslen = res[4]
if reslen == 1:
return True, res[5]
except Exception as e:
print("Get polling settings error:", e)
return False, 0
def set_polling_settings(self, settings):
try:
res = self.pcsc.control_exchange(
C_CODE, b"\xe0\x00\x00\x23\x01" + bytes([settings & 0xFF])
)
if len(res) > 0 and res.find(b"\xe1\x00\x00\x00") == 0:
reslen = res[4]
if reslen == 1:
return True, res[5]
except Exception as e:
print("Set polling settings error:", e)
return False, 0
def get_picc_operation_parameter(self):
try:
res = self.pcsc.control_exchange(C_CODE, b"\xe0\x00\x00\x20\x00")
if len(res) > 0 and res.find(b"\xe1\x00\x00\x00") == 0:
reslen = res[4]
if reslen == 1:
return True, res[5]
except Exception as e:
print("Get PICC Operating Parameter error:", e)
return False, 0
def set_picc_operation_parameter(self, param):
try:
res = self.pcsc.control_exchange(
C_CODE, b"\xe0\x00\x00\x20\x01" + bytes([param])
)
if len(res) > 0 and res.find(b"\xe1\x00\x00\x00") == 0:
reslen = res[4]
if reslen == 1:
return True, res[5]
except Exception as e:
print("Set PICC Operating Parameter error:", e)
return False, 0
dev = next(CtapPcscDevice.list_devices())
print("CONNECT: %s" % dev)
pcsc_device = Acr1252uPcscDevice(dev)
if pcsc_device is not None:
print("version: %s" % pcsc_device.reader_version())
print("serial number: %s" % pcsc_device.reader_serial_number())
print("")
result, settings = pcsc_device.set_polling_settings(0x8B)
print("write polling settings: %r 0x%x" % (result, settings))
result, settings = pcsc_device.get_polling_settings()
print("polling settings: %r 0x%x" % (result, settings))
set_desc = [
[0, "Auto PICC Polling"],
[1, "Turn off Antenna Field if no PICC is found"],
[2, "Turn off Antenna Field if the PICC is inactive"],
[3, "Activate the PICC when detected"],
[7, "Enforce ISO 14443-A Part 4"],
]
for x in set_desc:
print(x[1], "on" if settings & (1 << x[0]) else "off")
interval_desc = [250, 500, 1000, 2500]
print("PICC Poll Interval for PICC", interval_desc[(settings >> 4) & 0b11], "ms")
print("")
print(
"PICC operation parameter: %r 0x%x" % pcsc_device.get_picc_operation_parameter()
)
print("")
result, red, green = pcsc_device.led_control(True, False)
print("led control result:", result, "red:", red, "green:", green)
result, red, green = pcsc_device.led_status()
print("led state result:", result, "red:", red, "green:", green)
time.sleep(1)
pcsc_device.led_control(False, False)
fido2-2.2.1/examples/bio_enrollment.py 0000644 0000000 0000000 00000005634 15220526030 014650 0 ustar 00 # Copyright (c) 2020 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.
"""
Connects to the first FIDO device found over USB, and attempts to enroll a new
fingerprint. This requires that a PIN is already set.
NOTE: This uses a draft bio enrollment specification which is not yet final.
Consider this highly experimental.
"""
import sys
from getpass import getpass
from fido2.ctap2 import CaptureError, Ctap2, FPBioEnrollment
from fido2.ctap2.bio import BioEnrollment
from fido2.ctap2.pin import ClientPin
from fido2.hid import CtapHidDevice
pin = None
uv = "discouraged"
for dev in CtapHidDevice.list_devices():
try:
ctap = Ctap2(dev)
if BioEnrollment.is_supported(ctap.info):
break
except Exception: # noqa: S112
continue
else:
print("No Authenticator supporting bioEnroll found")
sys.exit(1)
if not ctap.info.options.get("clientPin"):
print("PIN not set for the device!")
sys.exit(1)
# Authenticate with PIN
print("Preparing to enroll a new fingerprint.")
pin = getpass("Please enter PIN: ")
client_pin = ClientPin(ctap)
pin_token = client_pin.get_pin_token(pin, ClientPin.PERMISSION.BIO_ENROLL)
bio = FPBioEnrollment(ctap, client_pin.protocol, pin_token)
print(bio.enumerate_enrollments())
# Start enrollment
enroller = bio.enroll()
template_id = None
while template_id is None:
print("Press your fingerprint against the sensor now...")
try:
template_id = enroller.capture()
print(enroller.remaining, "more scans needed.")
except CaptureError as e:
print(e)
bio.set_name(template_id, "Example")
print("Fingerprint registered successfully with ID:", template_id)
fido2-2.2.1/examples/cred_blob.py 0000644 0000000 0000000 00000006615 15220526030 013553 0 ustar 00 # Copyright (c) 2020 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.
"""
Connects to the first FIDO device found which supports the CredBlob extension,
creates a new credential for it with the extension enabled, and stores some data.
"""
import os
import sys
from exampleutils import get_client
from fido2.server import Fido2Server
# Locate a suitable FIDO authenticator
client, info = get_client(lambda info: "credBlob" in info.extensions)
# Prefer UV token if supported
uv = "discouraged"
if info and (info.options.get("uv") or info.options.get("bioEnroll")):
uv = "preferred"
print("Authenticator is configured for User Verification")
server = Fido2Server({"id": "example.com", "name": "Example RP"})
user = {"id": b"user_id", "name": "A. User"}
# Prepare parameters for makeCredential
create_options, state = server.register_begin(
user,
resident_key_requirement="required",
user_verification=uv,
authenticator_attachment="cross-platform",
)
# Add CredBlob extension, attach data
blob = os.urandom(32) # 32 random bytes
# Create a credential
result = client.make_credential(
{
**create_options["publicKey"],
"extensions": {"credBlob": blob},
}
)
# Complete registration
auth_data = server.register_complete(state, result)
credentials = [auth_data.credential_data]
# CredBlob result:
if not auth_data.extensions.get("credBlob"):
print("Credential was registered, but credBlob was NOT saved.")
sys.exit(1)
print("New credential created, with the CredBlob extension.")
# Prepare parameters for getAssertion
request_options, state = server.authenticate_begin()
# Authenticate the credential
# Only one cred in allowCredentials, only one response.
result = client.get_assertion(
{
**request_options["publicKey"],
"extensions": {"getCredBlob": True},
}
).get_response(0)
blob_res = result.response.authenticator_data.extensions.get("credBlob")
if blob == blob_res:
print("Authenticated, got correct blob:", blob.hex())
else:
print(
"Authenticated, got incorrect blob! (was %s, expected %s)"
% (blob_res.hex(), blob.hex())
)
sys.exit(1)
fido2-2.2.1/examples/credential.py 0000644 0000000 0000000 00000006331 15220526030 013745 0 ustar 00 # 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.
"""
Connects to the first FIDO device found (starts from USB, then looks into NFC),
creates a new credential for it, and authenticates the credential.
This works with both FIDO 2.0 devices as well as with U2F devices.
On Windows, the native WebAuthn API will be used.
"""
from exampleutils import get_client
from fido2.server import Fido2Server
# Locate a suitable FIDO authenticator
client, info = get_client()
# Prefer UV if supported and configured
if info and (info.options.get("uv") or info.options.get("bioEnroll")):
uv = "preferred"
print("Authenticator supports User Verification")
else:
uv = "discouraged"
server = Fido2Server({"id": "example.com", "name": "Example RP"}, attestation="direct")
user = {"id": b"user_id", "name": "A. User"}
# Prepare parameters for makeCredential
create_options, state = server.register_begin(
user, user_verification=uv, authenticator_attachment="cross-platform"
)
# Create a credential
result = client.make_credential(create_options["publicKey"])
# Complete registration
auth_data = server.register_complete(state, result)
credentials = [auth_data.credential_data]
print("New credential created!")
response = result.response
print("CLIENT DATA:", response.client_data)
print("ATTESTATION OBJECT:", response.attestation_object)
print()
print("CREDENTIAL DATA:", auth_data.credential_data)
# Prepare parameters for getAssertion
request_options, state = server.authenticate_begin(credentials, user_verification=uv)
# Authenticate the credential
results = client.get_assertion(request_options["publicKey"])
# Only one cred in allowCredentials, only one response.
result = results.get_response(0)
# Complete authenticator
server.authenticate_complete(state, credentials, result)
print("Credential authenticated!")
response = result.response
print("CLIENT DATA:", response.client_data)
print()
print("AUTH DATA:", response.authenticator_data)
fido2-2.2.1/examples/exampleutils.py 0000644 0000000 0000000 00000007360 15220526030 014352 0 ustar 00 # Copyright (c) 2024 Yubico AB
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
"""
Utilities for common functionality used by several examples in this directory.
"""
import ctypes
from getpass import getpass
from fido2.client import DefaultClientDataCollector, Fido2Client, UserInteraction
from fido2.hid import CtapHidDevice
# Support NFC devices if we can
try:
from fido2.pcsc import CtapPcscDevice
except ImportError:
CtapPcscDevice = None
# Use the Windows WebAuthn API if available, and we're not running as admin
try:
from fido2.client.windows import WindowsClient
use_winclient = (
WindowsClient.is_available() and not ctypes.windll.shell32.IsUserAnAdmin()
)
except Exception:
use_winclient = False
# Handle user interaction via CLI prompts
class CliInteraction(UserInteraction):
def __init__(self):
self._pin = None
def prompt_up(self):
print("\nTouch your authenticator device now...\n")
def request_pin(self, permissions, rd_id):
if not self._pin:
self._pin = getpass("Enter PIN: ")
return self._pin
def request_uv(self, permissions, rd_id):
print("User Verification required.")
return True
def enumerate_devices():
for dev in CtapHidDevice.list_devices():
yield dev
if CtapPcscDevice:
for dev in CtapPcscDevice.list_devices():
yield dev
def get_client(predicate=None, **kwargs):
"""Locate a CTAP device suitable for use.
If running on Windows as non-admin, the predicate check will be skipped and
a webauthn.dll based client will be returned.
Extra kwargs will be passed to the constructor of Fido2Client.
The client will be returned, with the CTAP2 Info, if available.
"""
client_data_collector = DefaultClientDataCollector("https://example.com")
if use_winclient:
return WindowsClient(client_data_collector), None
user_interaction = kwargs.pop("user_interaction", None) or CliInteraction()
# Locate a device
for dev in enumerate_devices():
# Set up a FIDO 2 client using the origin https://example.com
client = Fido2Client(
dev,
client_data_collector=client_data_collector,
user_interaction=user_interaction,
**kwargs,
)
# Check if it is suitable for use
if predicate is None or predicate(client.info):
return client, client.info
else:
raise ValueError("No suitable Authenticator found!")
fido2-2.2.1/examples/get_info.py 0000644 0000000 0000000 00000004704 15220526030 013427 0 ustar 00 # 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.
"""
Connects to each attached FIDO device, and:
1. If the device supports CBOR commands, perform a getInfo command.
2. If the device supports WINK, perform the wink command.
"""
from fido2.ctap2 import Ctap2
from fido2.hid import CAPABILITY, CtapHidDevice
try:
from fido2.pcsc import CtapPcscDevice
except ImportError:
CtapPcscDevice = None
def enumerate_devices():
for dev in CtapHidDevice.list_devices():
yield dev
if CtapPcscDevice:
for dev in CtapPcscDevice.list_devices():
yield dev
for dev in enumerate_devices():
print("CONNECT: %s" % dev)
print("Product name: %s" % dev.product_name)
print("Serial number: %s" % dev.serial_number)
print("CTAPHID protocol version: %d" % dev.version)
if dev.capabilities & CAPABILITY.CBOR:
ctap2 = Ctap2(dev)
info = ctap2.get_info()
print("DEVICE INFO: %s" % info)
else:
print("Device does not support CBOR")
if dev.capabilities & CAPABILITY.WINK:
dev.wink()
print("WINK sent!")
else:
print("Device does not support WINK")
dev.close()
fido2-2.2.1/examples/hmac_secret.py 0000644 0000000 0000000 00000012651 15220526030 014112 0 ustar 00 # 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.
"""
Connects to the first FIDO device found which supports the HmacSecret extension,
creates a new credential for it with the extension enabled, and uses it to
derive two separate secrets.
NOTE: This extension is not enabled by default as direct access to the extension
is now allowed in a browser setting. See also prf.py for an example which uses
the PRF extension which is enabled by default.
"""
import ctypes
import os
import sys
from exampleutils import CliInteraction, enumerate_devices
from fido2.client import DefaultClientDataCollector, Fido2Client
from fido2.ctap2.extensions import HmacSecretExtension
from fido2.server import Fido2Server
# Use the Windows WebAuthn API if available, and we're not running as admin
try:
from fido2.client.windows import WindowsClient
use_winclient = (
WindowsClient.is_available() and not ctypes.windll.shell32.IsUserAnAdmin()
)
except ImportError:
use_winclient = False
uv = "discouraged"
client_data_collector = DefaultClientDataCollector("https://example.com")
if use_winclient:
# Use the Windows WebAuthn API if available, and we're not running as admin
# By default only the PRF extension is allowed, we need to explicitly
# configure the client to allow hmac-secret
client = WindowsClient(client_data_collector, allow_hmac_secret=True)
else:
# Locate a device
for dev in enumerate_devices():
client = Fido2Client(
dev,
client_data_collector=client_data_collector,
user_interaction=CliInteraction(),
# By default only the PRF extension is allowed, we need to explicitly
# configure the client to allow hmac-secret
extensions=[HmacSecretExtension(allow_hmac_secret=True)],
)
if "hmac-secret" in client.info.extensions:
break
else:
print("No Authenticator with the HmacSecret extension found!")
sys.exit(1)
server = Fido2Server({"id": "example.com", "name": "Example RP"}, attestation="none")
user = {"id": b"user_id", "name": "A. User"}
# Prepare parameters for makeCredential
create_options, state = server.register_begin(
user,
resident_key_requirement="discouraged",
user_verification=uv,
authenticator_attachment="cross-platform",
)
# Create a credential
result = client.make_credential(
{
**create_options["publicKey"],
"extensions": {"hmacCreateSecret": True},
}
)
# Complete registration
auth_data = server.register_complete(state, result)
credentials = [auth_data.credential_data]
# HmacSecret result:
if result.client_extension_results.get("hmacCreateSecret"):
print("New credential created, with HmacSecret")
else:
# This fails on Windows, but we might still be able to use hmac-secret even if
# the credential wasn't made with it, so keep going
print("Failed to create credential with HmacSecret, it might not work")
# Generate a salt for HmacSecret:
salt = os.urandom(32)
print("Authenticate with salt:", salt.hex())
# Prepare parameters for getAssertion
request_options, state = server.authenticate_begin(credentials, user_verification=uv)
# Authenticate the credential
result = client.get_assertion(
{
**request_options["publicKey"],
"extensions": {"hmacGetSecret": {"salt1": salt}},
}
)
# Only one cred in allowCredentials, only one response.
result = result.get_response(0)
output1 = result.client_extension_results.hmac_get_secret.output1
print("Authenticated, secret:", output1.hex())
# Authenticate again, using two salts to generate two secrets:
# Generate a second salt for HmacSecret:
salt2 = os.urandom(32)
print("Authenticate with second salt:", salt2.hex())
# The first salt is reused, which should result in the same secret.
result = client.get_assertion(
{
**request_options["publicKey"],
"extensions": {"hmacGetSecret": {"salt1": salt, "salt2": salt2}},
}
)
# Only one cred in allowCredentials, only one response.
result = result.get_response(0)
output = result.client_extension_results.hmac_get_secret
print("Old secret:", output.output1.hex())
print("New secret:", output.output2.hex())
fido2-2.2.1/examples/large_blobs.py 0000644 0000000 0000000 00000010010 15220526030 014073 0 ustar 00 # 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.
"""
Connects to the first FIDO device found (starts from USB, then looks into NFC),
creates a new credential for it, and authenticates the credential.
This works with both FIDO 2.0 devices as well as with U2F devices.
On Windows, the native WebAuthn API will be used.
"""
import sys
from exampleutils import get_client
from fido2.server import Fido2Server
from fido2.utils import websafe_decode, websafe_encode
# Locate a suitable FIDO authenticator
client, info = get_client(lambda info: "largeBlobKey" in info.extensions)
# LargeBlob requires UV if it is configured
uv = "discouraged"
if info and info.options.get("clientPin"):
uv = "required"
server = Fido2Server({"id": "example.com", "name": "Example RP"})
user = {"id": b"user_id", "name": "A. User"}
# Prepare parameters for makeCredential
create_options, state = server.register_begin(
user,
resident_key_requirement="required",
user_verification=uv,
authenticator_attachment="cross-platform",
)
print("Creating a credential with LargeBlob support...")
# Create a credential
result = client.make_credential(
{
**create_options["publicKey"],
# Enable largeBlob
"extensions": {"largeBlob": {"support": "required"}},
}
)
# Complete registration
auth_data = server.register_complete(state, result)
credentials = [auth_data.credential_data]
if auth_data.is_user_verified():
# The WindowsClient doesn't know about authenticator config until now
uv = "required"
if not result.client_extension_results.get("largeBlob", {}).get("supported"):
print("Credential does not support largeBlob, failure!")
sys.exit(1)
print("Credential created! Writing a blob...")
# Prepare parameters for getAssertion
request_options, state = server.authenticate_begin(credentials, user_verification=uv)
# Authenticate the credential
selection = client.get_assertion(
{
**request_options["publicKey"],
# Write a large blob
"extensions": {
"largeBlob": {"write": websafe_encode(b"Here is some data to store!")}
},
}
)
# Only one cred in allowCredentials, only one response.
result = selection.get_response(0)
if not result.client_extension_results.get("largeBlob", {}).get("written"):
print("Failed to write blob!")
sys.exit(1)
print("Blob written! Reading back the blob...")
# Authenticate the credential
selection = client.get_assertion(
{
**request_options["publicKey"],
# Read the blob
"extensions": {"largeBlob": {"read": True}},
}
)
# Only one cred in allowCredentials, only one response.
result = selection.get_response(0)
print(
"Read blob:", websafe_decode(result.client_extension_results["largeBlob"]["blob"])
)
fido2-2.2.1/examples/multi_device.py 0000644 0000000 0000000 00000007234 15220526030 014307 0 ustar 00 # 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.
"""
Connects to each FIDO device found, and causes them all to blink until the user
triggers one to select it. A new credential is created for that authenticator,
and the operation is cancelled for the others.
"""
import sys
from getpass import getpass
from threading import Event, Thread
from fido2.client import (
ClientError,
DefaultClientDataCollector,
Fido2Client,
UserInteraction,
)
from fido2.hid import CtapHidDevice
# Locate a device
devs = list(CtapHidDevice.list_devices())
if not devs:
print("No FIDO device found")
sys.exit(1)
# Handle user interaction
class CliInteraction(UserInteraction):
def prompt_up(self):
print("\nTouch your authenticator device now...\n")
def request_pin(self, permissions, rd_id):
return getpass("Enter PIN: ")
def request_uv(self, permissions, rd_id):
print("User Verification required.")
return True
cli_interaction = CliInteraction()
clients = [
Fido2Client(
d,
client_data_collector=DefaultClientDataCollector("https://example.com"),
user_interaction=cli_interaction,
)
for d in devs
]
# Prepare parameters for makeCredential
rp = {"id": "example.com", "name": "Example RP"}
user = {"id": b"user_id", "name": "A. User"}
challenge = b"Y2hhbGxlbmdl"
cancel = Event()
selected = None
def select(client):
global selected
try:
client.selection(cancel)
selected = client
except ClientError as e:
if e.code != ClientError.ERR.TIMEOUT:
raise
else:
return
cancel.set()
print("\nTouch the authenticator you wish to use...\n")
threads = []
for client in clients:
t = Thread(target=select, args=(client,))
threads.append(t)
t.start()
for t in threads:
t.join()
if cancel.is_set():
print("Authenticator selected, making credential...")
result = selected.make_credential(
{
"rp": rp,
"user": user,
"challenge": challenge,
"pubKeyCredParams": [{"type": "public-key", "alg": -7}],
},
)
print("New credential created!")
response = result.response
print("ATTESTATION OBJECT:", response.attestation_object)
print()
print("CREDENTIAL DATA:", response.attestation_object.auth_data.credential_data)
else:
print("Operation timed out!")
fido2-2.2.1/examples/prf.py 0000644 0000000 0000000 00000011253 15220526030 012421 0 ustar 00 # Copyright (c) 2024 Yubico AB
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
"""
Connects to the first FIDO device found which supports the PRF extension,
creates a new credential for it with the extension enabled, and uses it to
derive two separate secrets.
"""
import os
from exampleutils import get_client
from fido2.server import Fido2Server
from fido2.utils import websafe_encode
# Locate a suitable FIDO authenticator
client, _ = get_client(lambda info: "hmac-secret" in info.extensions)
uv = "discouraged"
server = Fido2Server({"id": "example.com", "name": "Example RP"}, attestation="none")
user = {"id": b"user_id", "name": "A. User"}
# Prepare parameters for makeCredential
create_options, state = server.register_begin(
user,
resident_key_requirement="discouraged",
user_verification=uv,
authenticator_attachment="cross-platform",
)
# Generate salts for PRF:
salt = websafe_encode(os.urandom(32))
salt2 = websafe_encode(os.urandom(32))
# Create a credential
result = client.make_credential(
{
**create_options["publicKey"],
"extensions": {"prf": {"eval": {"first": salt, "second": salt2}}},
}
)
# Complete registration
auth_data = server.register_complete(state, result)
credential = auth_data.credential_data
# PRF result:
if result.client_extension_results.get("prf", {}).get("enabled"):
print("New credential created, with PRF")
else:
# This fails on Windows, but we might still be able to use prf even if
# the credential wasn't made with it, so keep going
print("Failed to create credential with PRF, it might not work")
# If created with UV, keep using UV
if auth_data.is_user_verified():
uv = "required"
print("First salt:", salt)
# Prepare parameters for getAssertion
credentials = [credential]
request_options, state = server.authenticate_begin(credentials, user_verification=uv)
prf_results = result.client_extension_results.get("prf", {}).get("results")
if prf_results:
output1 = prf_results["first"]
print("Credential created, with salt. Secret:", output1)
else:
# Authenticate the credential
result = client.get_assertion(
{
**request_options["publicKey"],
"extensions": {"prf": {"eval": {"first": salt}}},
}
)
# Only one cred in allowCredentials, only one response.
response = result.get_response(0)
output1 = response.client_extension_results["prf"]["results"]["first"]
print("Authenticated, with salt. Secret:", output1)
# Authenticate again, using two salts to generate two secrets.
# This time we will use evalByCredential, which can be used if there are multiple
# credentials which use different salts. Here it is not needed, but provided for
# completeness of the example.
print("Authenticate with second salt:", salt2)
# The first salt is reused, which should result in the same secret.
result = client.get_assertion(
{
**request_options["publicKey"],
"extensions": {
"prf": {
"evalByCredential": {
websafe_encode(credential.credential_id): {
"first": salt,
"second": salt2,
}
}
}
},
}
)
# Only one cred in allowCredentials, only one response.
response = result.get_response(0)
output = response.client_extension_results["prf"]["results"]
print("Old secret:", output["first"])
print("New secret:", output["second"])
fido2-2.2.1/examples/resident_key.py 0000644 0000000 0000000 00000007320 15220526030 014317 0 ustar 00 # 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.
"""
Connects to the first FIDO device found (starts from USB, then looks into NFC),
creates a new credential for it, and authenticates the credential.
This works with both FIDO 2.0 devices as well as with U2F devices.
On Windows, the native WebAuthn API will be used.
"""
from exampleutils import get_client
from fido2.server import Fido2Server
# Locate a suitable FIDO authenticator
client, info = get_client(lambda info: info.options.get("rk"))
# Prefer UV if supported and configured
uv = "discouraged"
if info and (info.options.get("uv") or info.options.get("bioEnroll")):
uv = "preferred"
print("Authenticator is configured for User Verification")
server = Fido2Server({"id": "example.com", "name": "Example RP"}, attestation="direct")
user = {"id": b"user_id", "name": "A. User"}
# Prepare parameters for makeCredential
create_options, state = server.register_begin(
user,
resident_key_requirement="required",
user_verification=uv,
authenticator_attachment="cross-platform",
)
# Create a credential
result = client.make_credential(
{
**create_options["publicKey"],
# This extension isn't needed, but can be used to verify that the created
# credential uses resident key
"extensions": {"credProps": True},
}
)
# Complete registration
auth_data = server.register_complete(state, result)
credentials = [auth_data.credential_data]
print("New credential created!")
response = result.response
print("CLIENT DATA:", response.client_data)
print("ATTESTATION OBJECT:", response.attestation_object)
print()
print("CREDENTIAL DATA:", auth_data.credential_data)
print()
print("Credential Properties:", result.client_extension_results.get("credProps"))
# credProps:
cred_props = result.client_extension_results.get("credProps")
print("CredProps", cred_props)
# Prepare parameters for getAssertion
request_options, state = server.authenticate_begin(user_verification=uv)
# Authenticate the credential
selection = client.get_assertion(request_options["publicKey"])
result = selection.get_response(0) # There may be multiple responses, get the first.
# Complete authenticator
server.authenticate_complete(state, credentials, result)
print("Credential authenticated!")
response = result.response
print("USER ID:", response.user_handle)
print("CLIENT DATA:", response.client_data)
print()
print("AUTHENTICATOR DATA:", response.authenticator_data)
fido2-2.2.1/examples/server/README.adoc 0000644 0000000 0000000 00000003274 15220526030 014357 0 ustar 00 == WebAuthn Server Example
This example shows a minimal website that uses python-fido2 to implement
WebAuthn credential registration, and use.
=== Running
To run this sample, you will need `uv`. For instructions on installing
`uv`, see https://docs.astral.sh/uv/.
Run the following command in the `examples/server` directory to set up the
example:
$ uv sync
Once the environment has been created, you can run the server by running:
$ uv run server
When the server is running, use a browser supporting WebAuthn and open
http://localhost:5000 to access the website.
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.
Credentials are only stored in memory, and stopping the server will cause it to
"forget" any registered credentials.
==== Registration
1. Click on the `Register` link to begin credential registration.
2. If not already inserted, insert your U2F/FIDO2 Authenticator now.
3. Touch the button to activate the Authenticator.
4. A popup will indicate whether the registration was successful. Click `OK`.
==== Authentication
NOTE: You must register a credential prior to authentication.
1. Click on the `Authenticate` link to begin authentication.
2. If not already inserted, insert your U2F/FIDO2 Authenticator now.
3. Touch the button to activate the Authenticator.
4. A popup will indicate whether the authentication was successful. Click `OK`.
fido2-2.2.1/examples/server/pyproject.toml 0000644 0000000 0000000 00000001030 15220526030 015472 0 ustar 00 [project]
name = "fido2-example-server"
version = "0.1.0"
description = "Example server for python-fido2"
authors = [
{ name = "Dain Nilsson", email = "dain@yubico.com" }
]
requires-python = ">=3.10, <4"
license = "Apache-2.0"
dependencies = [
"Flask>=2.0,<3",
"fido2",
]
[tool.uv.sources]
fido2 = { path = "../..", editable = true }
[tool.poetry]
packages = [
{ include = "server" },
]
[build-system]
requires = ["poetry-core>=2.0"]
build-backend = "poetry.core.masonry.api"
[project.scripts]
server = "server.server:main"
fido2-2.2.1/examples/server/server/__init__.py 0000644 0000000 0000000 00000000000 15220526030 016171 0 ustar 00 fido2-2.2.1/examples/server/server/server.py 0000644 0000000 0000000 00000007731 15220526030 015762 0 ustar 00 # 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 http://localhost:5000 in a supported web browser.
"""
import os
from flask import Flask, abort, jsonify, redirect, request, session
from fido2.server import Fido2Server
from fido2.webauthn import PublicKeyCredentialRpEntity, PublicKeyCredentialUserEntity
app = Flask(__name__, static_url_path="")
app.secret_key = os.urandom(32) # Used for session.
rp = PublicKeyCredentialRpEntity(name="Demo server", id="localhost")
server = Fido2Server(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.html")
@app.route("/api/register/begin", methods=["POST"])
def register_begin():
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",
)
session["state"] = state
print("\n\n\n\n")
print(dict(options))
print("\n\n\n\n")
return jsonify(dict(options))
@app.route("/api/register/complete", methods=["POST"])
def register_complete():
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 jsonify({"status": "OK"})
@app.route("/api/authenticate/begin", methods=["POST"])
def authenticate_begin():
if not credentials:
abort(404)
options, state = server.authenticate_begin(credentials)
session["state"] = state
return jsonify(dict(options))
@app.route("/api/authenticate/complete", methods=["POST"])
def authenticate_complete():
if not credentials:
abort(404)
response = request.json
print("AuthenticationResponse:", response)
server.authenticate_complete(
session.pop("state"),
credentials,
response,
)
print("ASSERTION OK")
return jsonify({"status": "OK"})
def main():
print(__doc__)
# 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__":
main()
fido2-2.2.1/examples/server/server/static/authenticate.html 0000644 0000000 0000000 00000003106 15220526030 020725 0 ustar 00
Fido 2.0 webauthn demo
WebAuthn demo using python-fido2
This demo requires a browser supporting the WebAuthn API!
fido2-2.2.1/examples/server/server/static/webauthn-json.browser-ponyfill.js 0000644 0000000 0000000 00000013372 15220526030 024025 0 ustar 00 // 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
fido2-2.2.1/examples/server/server/static/webauthn-json.browser-ponyfill.js.map 0000644 0000000 0000000 00000033044 15220526030 024577 0 ustar 00 {
"version": 3,
"sources": ["../../src/webauthn-json/base64url.ts", "../../src/webauthn-json/convert.ts", "../../src/webauthn-json/basic/schema.ts", "../../src/webauthn-json/basic/api.ts", "../../src/webauthn-json/basic/supported.ts", "../../src/webauthn-json/browser-ponyfill.ts"],
"sourcesContent": ["export type Base64urlString = string;\n\nexport function base64urlToBuffer(\n baseurl64String: Base64urlString,\n): ArrayBuffer {\n // Base64url to Base64\n const padding = \"==\".slice(0, (4 - (baseurl64String.length % 4)) % 4);\n const base64String =\n baseurl64String.replace(/-/g, \"+\").replace(/_/g, \"/\") + padding;\n\n // Base64 to binary string\n const str = atob(base64String);\n\n // Binary string to buffer\n const buffer = new ArrayBuffer(str.length);\n const byteView = new Uint8Array(buffer);\n for (let i = 0; i < str.length; i++) {\n byteView[i] = str.charCodeAt(i);\n }\n return buffer;\n}\n\nexport function bufferToBase64url(buffer: ArrayBuffer): Base64urlString {\n // Buffer to binary string\n const byteView = new Uint8Array(buffer);\n let str = \"\";\n for (const charCode of byteView) {\n str += String.fromCharCode(charCode);\n }\n\n // Binary string to base64\n const base64String = btoa(str);\n\n // Base64 to base64url\n // We assume that the base64url string is well-formed.\n const base64urlString = base64String\n .replace(/\\+/g, \"-\")\n .replace(/\\//g, \"_\")\n .replace(/=/g, \"\");\n return base64urlString;\n}\n", "// We export these values in order so that they can be used to deduplicate\n// schema definitions in minified JS code.\n\nimport { Schema, SchemaProperty } from \"./schema-format\";\n\n// TODO: Parcel isn't deduplicating these values.\nexport const copyValue = \"copy\";\nexport const convertValue = \"convert\";\n\nexport function convert(\n conversionFn: (v: From) => To,\n schema: Schema,\n input: any,\n): any {\n if (schema === copyValue) {\n return input;\n }\n if (schema === convertValue) {\n return conversionFn(input);\n }\n if (schema instanceof Array) {\n return input.map((v: any) => convert(conversionFn, schema[0], v));\n }\n if (schema instanceof Object) {\n const output: any = {};\n for (const [key, schemaField] of Object.entries(schema)) {\n if (schemaField.derive) {\n const v = schemaField.derive(input);\n if (v !== undefined) {\n input[key] = v;\n }\n }\n\n if (!(key in input)) {\n if (schemaField.required) {\n throw new Error(`Missing key: ${key}`);\n }\n continue;\n }\n // Fields can be null (rather than missing or `undefined`), e.g. the\n // `userHandle` field of the `AuthenticatorAssertionResponse`:\n // https://www.w3.org/TR/webauthn/#iface-authenticatorassertionresponse\n if (input[key] == null) {\n output[key] = null;\n continue;\n }\n output[key] = convert(\n conversionFn,\n schemaField.schema,\n input[key],\n );\n }\n return output;\n }\n}\n\nexport function derived(\n schema: Schema,\n derive: (v: any) => any,\n): SchemaProperty {\n return {\n required: true,\n schema,\n derive,\n };\n}\n\nexport function required(schema: Schema): SchemaProperty {\n return {\n required: true,\n schema,\n };\n}\n\nexport function optional(schema: Schema): SchemaProperty {\n return {\n required: false,\n schema,\n };\n}\n", "import { Schema } from \"../schema-format\";\nimport {\n convertValue as convert,\n copyValue as copy,\n derived,\n optional,\n required,\n} from \"../convert\";\n\n// Shared by `create()` and `get()`.\n\nconst publicKeyCredentialDescriptorSchema: Schema = {\n type: required(copy),\n id: required(convert),\n transports: optional(copy),\n};\n\nconst simplifiedExtensionsSchema: Schema = {\n appid: optional(copy),\n appidExclude: optional(copy),\n credProps: optional(copy),\n};\n\nconst simplifiedClientExtensionResultsSchema = {\n appid: optional(copy),\n appidExclude: optional(copy),\n credProps: optional(copy),\n};\n\n// `navigator.create()` request\n\nexport const credentialCreationOptions: Schema = {\n publicKey: required({\n rp: required(copy),\n user: required({\n id: required(convert),\n name: required(copy),\n displayName: required(copy),\n }),\n\n challenge: required(convert),\n pubKeyCredParams: required(copy),\n\n timeout: optional(copy),\n excludeCredentials: optional([publicKeyCredentialDescriptorSchema]),\n authenticatorSelection: optional(copy),\n attestation: optional(copy),\n extensions: optional(simplifiedExtensionsSchema),\n }),\n signal: optional(copy),\n};\n\n// `navigator.create()` response\n\nexport const publicKeyCredentialWithAttestation: Schema = {\n type: required(copy),\n id: required(copy),\n rawId: required(convert),\n authenticatorAttachment: optional(copy),\n response: required({\n clientDataJSON: required(convert),\n attestationObject: required(convert),\n transports: derived(\n copy,\n (response: any) => response.getTransports?.() || [],\n ),\n }),\n clientExtensionResults: derived(\n simplifiedClientExtensionResultsSchema,\n (pkc: PublicKeyCredential) => pkc.getClientExtensionResults(),\n ),\n};\n\n// `navigator.get()` request\n\nexport const credentialRequestOptions: Schema = {\n mediation: optional(copy),\n publicKey: required({\n challenge: required(convert),\n timeout: optional(copy),\n rpId: optional(copy),\n allowCredentials: optional([publicKeyCredentialDescriptorSchema]),\n userVerification: optional(copy),\n extensions: optional(simplifiedExtensionsSchema),\n }),\n signal: optional(copy),\n};\n\n// `navigator.get()` response\n\nexport const publicKeyCredentialWithAssertion: Schema = {\n type: required(copy),\n id: required(copy),\n rawId: required(convert),\n authenticatorAttachment: optional(copy),\n response: required({\n clientDataJSON: required(convert),\n authenticatorData: required(convert),\n signature: required(convert),\n userHandle: required(convert),\n }),\n clientExtensionResults: derived(\n simplifiedClientExtensionResultsSchema,\n (pkc: PublicKeyCredential) => pkc.getClientExtensionResults(),\n ),\n};\n\nexport const schema: { [s: string]: Schema } = {\n credentialCreationOptions,\n publicKeyCredentialWithAttestation,\n credentialRequestOptions,\n publicKeyCredentialWithAssertion,\n};\n", "import { base64urlToBuffer, bufferToBase64url } from \"../base64url\";\nimport { convert } from \"../convert\";\nimport {\n CredentialCreationOptionsJSON,\n CredentialRequestOptionsJSON,\n PublicKeyCredentialWithAssertionJSON,\n PublicKeyCredentialWithAttestationJSON,\n} from \"./json\";\nimport {\n credentialCreationOptions,\n credentialRequestOptions,\n publicKeyCredentialWithAssertion,\n publicKeyCredentialWithAttestation,\n} from \"./schema\";\n\nexport function createRequestFromJSON(\n requestJSON: CredentialCreationOptionsJSON,\n): CredentialCreationOptions {\n return convert(base64urlToBuffer, credentialCreationOptions, requestJSON);\n}\n\nexport function createResponseToJSON(\n credential: PublicKeyCredential,\n): PublicKeyCredentialWithAttestationJSON {\n return convert(\n bufferToBase64url,\n publicKeyCredentialWithAttestation,\n credential,\n );\n}\n\nexport async function create(\n requestJSON: CredentialCreationOptionsJSON,\n): Promise {\n const credential = (await navigator.credentials.create(\n createRequestFromJSON(requestJSON),\n )) as PublicKeyCredential;\n return createResponseToJSON(credential);\n}\n\nexport function getRequestFromJSON(\n requestJSON: CredentialRequestOptionsJSON,\n): CredentialRequestOptions {\n return convert(base64urlToBuffer, credentialRequestOptions, requestJSON);\n}\n\nexport function getResponseToJSON(\n credential: PublicKeyCredential,\n): PublicKeyCredentialWithAssertionJSON {\n return convert(\n bufferToBase64url,\n publicKeyCredentialWithAssertion,\n credential,\n );\n}\n\nexport async function get(\n requestJSON: CredentialRequestOptionsJSON,\n): Promise {\n const credential = (await navigator.credentials.get(\n getRequestFromJSON(requestJSON),\n )) as PublicKeyCredential;\n return getResponseToJSON(credential);\n}\n\ndeclare global {\n interface Window {\n PublicKeyCredential: PublicKeyCredential | undefined;\n }\n}\n", "// This function does a simple check to test for the credential management API\n// functions we need, and an indication of public key credential authentication\n// support.\n// https://developers.google.com/web/updates/2018/03/webauthn-credential-management\n\nexport function supported(): boolean {\n return !!(\n navigator.credentials &&\n navigator.credentials.create &&\n navigator.credentials.get &&\n window.PublicKeyCredential\n );\n}\n", "import {\n createRequestFromJSON as parseCreationOptionsFromJSON,\n createResponseToJSON,\n getRequestFromJSON as parseRequestOptionsFromJSON,\n getResponseToJSON,\n} from \"./basic/api\";\nimport { supported } from \"./basic/supported\";\n\nimport {\n CredentialCreationOptionsJSON,\n CredentialRequestOptionsJSON,\n PublicKeyCredentialWithAssertionJSON as AuthenticationResponseJSON,\n PublicKeyCredentialWithAttestationJSON as RegistrationResponseJSON,\n} from \"./basic/json\";\n\nexport { parseCreationOptionsFromJSON, parseRequestOptionsFromJSON, supported };\nexport type {\n CredentialCreationOptionsJSON,\n CredentialRequestOptionsJSON,\n AuthenticationResponseJSON,\n RegistrationResponseJSON,\n};\n\nexport interface RegistrationPublicKeyCredential extends PublicKeyCredential {\n toJSON(): RegistrationResponseJSON;\n}\n\nexport async function create(\n options: CredentialCreationOptions,\n): Promise {\n const response = (await navigator.credentials.create(\n options,\n )) as RegistrationPublicKeyCredential;\n response.toJSON = () => createResponseToJSON(response);\n return response;\n}\n\nexport interface AuthenticationPublicKeyCredential extends PublicKeyCredential {\n toJSON(): AuthenticationResponseJSON;\n}\n\nexport async function get(\n options: CredentialRequestOptions,\n): Promise {\n const response = (await navigator.credentials.get(\n options,\n )) as AuthenticationPublicKeyCredential;\n response.toJSON = () => getResponseToJSON(response);\n return response;\n}\n"],
"mappings": ";AAEO,2BACL,iBACa;AAEb,QAAM,UAAU,KAAK,MAAM,GAAI,KAAK,gBAAgB,SAAS,KAAM;AACnE,QAAM,eACJ,gBAAgB,QAAQ,MAAM,KAAK,QAAQ,MAAM,OAAO;AAG1D,QAAM,MAAM,KAAK;AAGjB,QAAM,SAAS,IAAI,YAAY,IAAI;AACnC,QAAM,WAAW,IAAI,WAAW;AAChC,WAAS,IAAI,GAAG,IAAI,IAAI,QAAQ,KAAK;AACnC,aAAS,KAAK,IAAI,WAAW;AAAA;AAE/B,SAAO;AAAA;AAGF,2BAA2B,QAAsC;AAEtE,QAAM,WAAW,IAAI,WAAW;AAChC,MAAI,MAAM;AACV,aAAW,YAAY,UAAU;AAC/B,WAAO,OAAO,aAAa;AAAA;AAI7B,QAAM,eAAe,KAAK;AAI1B,QAAM,kBAAkB,aACrB,QAAQ,OAAO,KACf,QAAQ,OAAO,KACf,QAAQ,MAAM;AACjB,SAAO;AAAA;;;ACjCF,IAAM,YAAY;AAClB,IAAM,eAAe;AAErB,iBACL,cACA,QACA,OACK;AACL,MAAI,WAAW,WAAW;AACxB,WAAO;AAAA;AAET,MAAI,WAAW,cAAc;AAC3B,WAAO,aAAa;AAAA;AAEtB,MAAI,kBAAkB,OAAO;AAC3B,WAAO,MAAM,IAAI,CAAC,MAAW,QAAkB,cAAc,OAAO,IAAI;AAAA;AAE1E,MAAI,kBAAkB,QAAQ;AAC5B,UAAM,SAAc;AACpB,eAAW,CAAC,KAAK,gBAAgB,OAAO,QAAQ,SAAS;AACvD,UAAI,YAAY,QAAQ;AACtB,cAAM,IAAI,YAAY,OAAO;AAC7B,YAAI,MAAM,QAAW;AACnB,gBAAM,OAAO;AAAA;AAAA;AAIjB,UAAI,CAAE,QAAO,QAAQ;AACnB,YAAI,YAAY,UAAU;AACxB,gBAAM,IAAI,MAAM,gBAAgB;AAAA;AAElC;AAAA;AAKF,UAAI,MAAM,QAAQ,MAAM;AACtB,eAAO,OAAO;AACd;AAAA;AAEF,aAAO,OAAO,QACZ,cACA,YAAY,QACZ,MAAM;AAAA;AAGV,WAAO;AAAA;AAAA;AAIJ,iBACL,QACA,QACgB;AAChB,SAAO;AAAA,IACL,UAAU;AAAA,IACV;AAAA,IACA;AAAA;AAAA;AAIG,kBAAkB,QAAgC;AACvD,SAAO;AAAA,IACL,UAAU;AAAA,IACV;AAAA;AAAA;AAIG,kBAAkB,QAAgC;AACvD,SAAO;AAAA,IACL,UAAU;AAAA,IACV;AAAA;AAAA;;;AClEJ,IAAM,sCAA8C;AAAA,EAClD,MAAM,SAAS;AAAA,EACf,IAAI,SAAS;AAAA,EACb,YAAY,SAAS;AAAA;AAGvB,IAAM,6BAAqC;AAAA,EACzC,OAAO,SAAS;AAAA,EAChB,cAAc,SAAS;AAAA,EACvB,WAAW,SAAS;AAAA;AAGtB,IAAM,yCAAyC;AAAA,EAC7C,OAAO,SAAS;AAAA,EAChB,cAAc,SAAS;AAAA,EACvB,WAAW,SAAS;AAAA;AAKf,IAAM,4BAAoC;AAAA,EAC/C,WAAW,SAAS;AAAA,IAClB,IAAI,SAAS;AAAA,IACb,MAAM,SAAS;AAAA,MACb,IAAI,SAAS;AAAA,MACb,MAAM,SAAS;AAAA,MACf,aAAa,SAAS;AAAA;AAAA,IAGxB,WAAW,SAAS;AAAA,IACpB,kBAAkB,SAAS;AAAA,IAE3B,SAAS,SAAS;AAAA,IAClB,oBAAoB,SAAS,CAAC;AAAA,IAC9B,wBAAwB,SAAS;AAAA,IACjC,aAAa,SAAS;AAAA,IACtB,YAAY,SAAS;AAAA;AAAA,EAEvB,QAAQ,SAAS;AAAA;AAKZ,IAAM,qCAA6C;AAAA,EACxD,MAAM,SAAS;AAAA,EACf,IAAI,SAAS;AAAA,EACb,OAAO,SAAS;AAAA,EAChB,yBAAyB,SAAS;AAAA,EAClC,UAAU,SAAS;AAAA,IACjB,gBAAgB,SAAS;AAAA,IACzB,mBAAmB,SAAS;AAAA,IAC5B,YAAY,QACV,WACA,CAAC,aAAe;AAhEtB;AAgEyB,6BAAS,kBAAT,sCAA8B;AAAA;AAAA;AAAA,EAGrD,wBAAwB,QACtB,wCACA,CAAC,QAA6B,IAAI;AAAA;AAM/B,IAAM,2BAAmC;AAAA,EAC9C,WAAW,SAAS;AAAA,EACpB,WAAW,SAAS;AAAA,IAClB,WAAW,SAAS;AAAA,IACpB,SAAS,SAAS;AAAA,IAClB,MAAM,SAAS;AAAA,IACf,kBAAkB,SAAS,CAAC;AAAA,IAC5B,kBAAkB,SAAS;AAAA,IAC3B,YAAY,SAAS;AAAA;AAAA,EAEvB,QAAQ,SAAS;AAAA;AAKZ,IAAM,mCAA2C;AAAA,EACtD,MAAM,SAAS;AAAA,EACf,IAAI,SAAS;AAAA,EACb,OAAO,SAAS;AAAA,EAChB,yBAAyB,SAAS;AAAA,EAClC,UAAU,SAAS;AAAA,IACjB,gBAAgB,SAAS;AAAA,IACzB,mBAAmB,SAAS;AAAA,IAC5B,WAAW,SAAS;AAAA,IACpB,YAAY,SAAS;AAAA;AAAA,EAEvB,wBAAwB,QACtB,wCACA,CAAC,QAA6B,IAAI;AAAA;;;ACxF/B,+BACL,aAC2B;AAC3B,SAAO,QAAQ,mBAAmB,2BAA2B;AAAA;AAGxD,8BACL,YACwC;AACxC,SAAO,QACL,mBACA,oCACA;AAAA;AAaG,4BACL,aAC0B;AAC1B,SAAO,QAAQ,mBAAmB,0BAA0B;AAAA;AAGvD,2BACL,YACsC;AACtC,SAAO,QACL,mBACA,kCACA;AAAA;;;AC/CG,qBAA8B;AACnC,SAAO,CAAC,CACN,WAAU,eACV,UAAU,YAAY,UACtB,UAAU,YAAY,OACtB,OAAO;AAAA;;;ACiBX,sBACE,SAC0C;AAC1C,QAAM,WAAY,MAAM,UAAU,YAAY,OAC5C;AAEF,WAAS,SAAS,MAAM,qBAAqB;AAC7C,SAAO;AAAA;AAOT,mBACE,SAC4C;AAC5C,QAAM,WAAY,MAAM,UAAU,YAAY,IAC5C;AAEF,WAAS,SAAS,MAAM,kBAAkB;AAC1C,SAAO;AAAA;",
"names": []
}
fido2-2.2.1/examples/server/uv.lock 0000644 0000000 0000000 00000212702 15220526030 014074 0 ustar 00 version = 1
revision = 3
requires-python = ">=3.10, <4"
[[package]]
name = "blinker"
version = "1.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" },
]
[[package]]
name = "cffi"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" },
{ url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" },
{ url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" },
{ url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" },
{ url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" },
{ url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" },
{ url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" },
{ url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" },
{ url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" },
{ url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" },
{ url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" },
{ url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" },
{ url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" },
{ url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" },
{ url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" },
{ url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" },
{ url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" },
{ url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" },
{ url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" },
{ url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" },
{ url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" },
{ url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" },
{ url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" },
{ url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" },
{ url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" },
{ url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
{ url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
{ url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
{ url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
{ url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
{ url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
{ url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
{ url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
{ url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
{ url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
{ url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
{ url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
{ url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
{ url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
{ url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
{ url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
{ url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
{ url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
{ url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
{ url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
{ url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
{ url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
{ url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
{ url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
{ url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
{ url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
{ url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
{ url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
{ url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
{ url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
{ url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
{ url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
{ url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
{ url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
{ url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
{ url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
{ url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
{ url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
]
[[package]]
name = "click"
version = "8.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "cryptography"
version = "49.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" },
{ url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" },
{ url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" },
{ url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" },
{ url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" },
{ url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" },
{ url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" },
{ url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" },
{ url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" },
{ url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" },
{ url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" },
{ url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" },
{ url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" },
{ url = "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892, upload-time = "2026-06-12T20:02:10.735Z" },
{ url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" },
{ url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" },
{ url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" },
{ url = "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" },
{ url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" },
{ url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" },
{ url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" },
{ url = "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" },
{ url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" },
{ url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" },
{ url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" },
{ url = "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153, upload-time = "2026-06-12T20:01:39.059Z" },
{ url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" },
{ url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" },
{ url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" },
{ url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" },
{ url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" },
{ url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" },
{ url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" },
{ url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" },
{ url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" },
{ url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" },
{ url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" },
{ url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" },
{ url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" },
{ url = "https://files.pythonhosted.org/packages/63/d3/4a83af35d65e3fad632c926fad684c193ea4398569ccb0bbbc7fe8f5dc9a/cryptography-49.0.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b", size = 3993685, upload-time = "2026-06-12T20:02:14.883Z" },
{ url = "https://files.pythonhosted.org/packages/d6/a7/f9dac0ab7f80368c56993a7bf638ef9935f825c91902798481fac0898138/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838", size = 4676239, upload-time = "2026-06-12T20:02:28.793Z" },
{ url = "https://files.pythonhosted.org/packages/d7/70/2ba3769dd0ae167e2f33dfa9592d45db6ff9a61d62ca1a5b3d1bdd09068f/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5", size = 4715584, upload-time = "2026-06-12T20:01:27.495Z" },
{ url = "https://files.pythonhosted.org/packages/94/64/2923570ac1c0bd3a737aa366ac3abbbbde273042308b8cde95e2364a6e6a/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615", size = 4675885, upload-time = "2026-06-12T20:01:55.49Z" },
{ url = "https://files.pythonhosted.org/packages/ab/f8/614dc7e051418cfe53d55173c1e24c6b0085e89996fe90508c2fdf769aef/cryptography-49.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6", size = 4715449, upload-time = "2026-06-12T20:02:05.469Z" },
{ url = "https://files.pythonhosted.org/packages/aa/50/a9caea39ad19c431c1a3f8a31114df65b260cdfe67786b6c7e7c040c4c44/cryptography-49.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6", size = 3783731, upload-time = "2026-06-12T20:02:43.319Z" },
]
[[package]]
name = "fido2"
version = "2.2.1.dev0"
source = { editable = "../../" }
dependencies = [
{ name = "cryptography" },
]
[package.metadata]
requires-dist = [
{ name = "cryptography", specifier = ">=2.6,!=35,<52" },
{ name = "pyscard", marker = "extra == 'pcsc'", specifier = ">=1.9,<3" },
]
provides-extras = ["pcsc"]
[package.metadata.requires-dev]
dev = [
{ name = "pytest", specifier = ">=9.0,<10" },
{ name = "sphinx", marker = "python_full_version >= '3.11'", specifier = ">=9.0,<10" },
{ name = "sphinx-autoapi", specifier = ">=3.3.3,<4" },
{ name = "sphinx-rtd-theme", specifier = ">=3,<4" },
]
[[package]]
name = "fido2-example-server"
version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "fido2" },
{ name = "flask" },
]
[package.metadata]
requires-dist = [
{ name = "fido2", editable = "../../" },
{ name = "flask", specifier = ">=2.0,<3" },
]
[[package]]
name = "flask"
version = "2.3.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "blinker" },
{ name = "click" },
{ name = "itsdangerous" },
{ name = "jinja2" },
{ name = "werkzeug" },
]
sdist = { url = "https://files.pythonhosted.org/packages/46/b7/4ace17e37abd9c21715dea5ee11774a25e404c486a7893fa18e764326ead/flask-2.3.3.tar.gz", hash = "sha256:09c347a92aa7ff4a8e7f3206795f30d826654baf38b873d0744cd571ca609efc", size = 672756, upload-time = "2023-08-21T19:52:35.012Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/56/26f0be8adc2b4257df20c1c4260ddd0aa396cf8e75d90ab2f7ff99bc34f9/flask-2.3.3-py3-none-any.whl", hash = "sha256:f69fcd559dc907ed196ab9df0e48471709175e696d6e698dd4dbe940f96ce66b", size = 96112, upload-time = "2023-08-21T19:52:33.115Z" },
]
[[package]]
name = "itsdangerous"
version = "2.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" },
]
[[package]]
name = "jinja2"
version = "3.1.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" },
]
[[package]]
name = "markupsafe"
version = "3.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" },
{ url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" },
{ url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" },
{ url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" },
{ url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" },
{ url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" },
{ url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" },
{ url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" },
{ url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" },
{ url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" },
{ url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" },
{ url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" },
{ url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" },
{ url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" },
{ url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" },
{ url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" },
{ url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" },
{ url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" },
{ url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" },
{ url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" },
{ url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" },
{ url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" },
{ url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" },
{ url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" },
{ url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" },
{ url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" },
{ url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" },
{ url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" },
{ url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" },
{ url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" },
{ url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" },
{ url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" },
{ url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" },
{ url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
{ url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
{ url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
{ url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
{ url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
{ url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
{ url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
{ url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
{ url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
{ url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
{ url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
{ url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
{ url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
{ url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
{ url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
{ url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
{ url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
{ url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
{ url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
{ url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
{ url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
{ url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
{ url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
{ url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
{ url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
{ url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
{ url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
{ url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
{ url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
{ url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
{ url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
{ url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
{ url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
{ url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
{ url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
{ url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
{ url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
{ url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
{ url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
{ url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
{ url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
{ url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
{ url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
{ url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
]
[[package]]
name = "pycparser"
version = "3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
]
[[package]]
name = "werkzeug"
version = "3.1.8"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/dd/b2/381be8cfdee792dd117872481b6e378f85c957dd7c5bca38897b08f765fd/werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44", size = 875852, upload-time = "2026-04-02T18:49:14.268Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/93/8c/2e650f2afeb7ee576912636c23ddb621c91ac6a98e66dc8d29c3c69446e1/werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50", size = 226459, upload-time = "2026-04-02T18:49:12.72Z" },
]
fido2-2.2.1/examples/sign_arkg.py 0000644 0000000 0000000 00000011570 15220526030 013600 0 ustar 00 # Copyright (c) 2024 Yubico AB
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
"""
Connects to the first FIDO device found which supports the sign extension,
creates a new credential for it with the extension enabled, and uses it to
derive two separate secrets.
"""
import sys
from exampleutils import get_client
from fido2 import cbor
from fido2.cose import ESP256_SPLIT_ARKG_PLACEHOLDER, CoseKey
from fido2.ctap2.extensions import PreviewSignExtension
from fido2.server import Fido2Server
from fido2.utils import sha256, websafe_decode, websafe_encode
uv = "discouraged"
# Locate a suitable FIDO authenticator
client, info = get_client(
lambda info: PreviewSignExtension.NAME in info.extensions,
extensions=[PreviewSignExtension()],
)
server = Fido2Server({"id": "example.com", "name": "Example RP"}, attestation="none")
user = {"id": b"user_id", "name": "A. User"}
# Prepare parameters for makeCredential
create_options, state = server.register_begin(
user,
resident_key_requirement="discouraged",
user_verification=uv,
authenticator_attachment="cross-platform",
)
# Create a credential
result = client.make_credential(
{
**create_options["publicKey"],
"extensions": {
PreviewSignExtension.NAME: {
"generateKey": {"algorithms": [ESP256_SPLIT_ARKG_PLACEHOLDER]}
}
},
}
)
# Complete registration
auth_data = server.register_complete(state, result)
credentials = [auth_data.credential_data]
print("New credential created, with the sign extension.")
# sign result:
sign_result = result.client_extension_results.previewSign
print("CREATE sign result", sign_result)
sign_key = sign_result.generated_key
if not sign_key:
print(
"Failed to create credential with sign extension",
result.client_extension_results,
)
sys.exit(1)
# Extension output contains master public key
pk = CoseKey.parse(
cbor.decode(websafe_decode(sign_key["publicKey"]))
) # COSE key in bytes
print("public key", pk)
# Master public key contains blinding and KEM keys
# ARKG derive_public_key uses these
print("Blinding public key", pk.pk_bl)
print("KEM public key", pk.pk_kem)
# Arbitrary bytestring used for ctx, ikm
ctx = b"my-ctx-here"
ikm = b"my-ikm-here"
# Derived public key to verify with, and kh to send to Authenticator
pk2, args = pk.derive_public_key(ikm, ctx)
print("Derived public key", pk2)
print("COSE_Sign_Args for derived key", args)
# Prepare a message to sign
message = b"New message"
ph_data = sha256(message)
# Prepare parameters for getAssertion
request_options, state = server.authenticate_begin(credentials, user_verification=uv)
# Authenticate the credential
result = client.get_assertion(
{
**request_options["publicKey"],
# Add extension outputs. We have only 1 credential in allowCredentials
"extensions": {
PreviewSignExtension.NAME: {
"signByCredential": {
websafe_encode(credentials[0].credential_id): {
"keyHandle": sign_key.key_handle,
"tbs": ph_data,
"additionalArgs": cbor.encode(args),
},
},
}
},
}
)
# Only one cred in allowCredentials, only one response.
result = result.get_response(0)
sign_result = result.client_extension_results[PreviewSignExtension.NAME]
print("GET sign result", sign_result)
# Response contains a signature over message
signature = sign_result.get("signature")
print("Test verify signature", signature)
pk2.verify(message, websafe_decode(signature))
print("Signature verified with derived public key!")
fido2-2.2.1/examples/u2f_nfc.py 0000644 0000000 0000000 00000001742 15220526030 013156 0 ustar 00 import sys
from fido2.ctap1 import Ctap1
from fido2.pcsc import CtapPcscDevice
from fido2.utils import sha256
dev = next(CtapPcscDevice.list_devices(), None)
if not dev:
print("No NFC u2f device found")
sys.exit(1)
chal = sha256(b"AAA")
appid = sha256(b"BBB")
ctap1 = Ctap1(dev)
print("version:", ctap1.get_version())
# True - make extended APDU and send it to key
# ISO 7816-3:2006. page 33, 12.1.3 Decoding conventions for command APDUs
# ISO 7816-3:2006. page 34, 12.2 Command-response pair transmission by T=0
# False - make group of short (less than 255 bytes length) APDU
# and send them to key. ISO 7816-3:2005, page 9, 5.1.1.1 Command chaining
dev.use_ext_apdu = False
reg = ctap1.register(chal, appid)
print("register:", reg)
reg.verify(appid, chal)
print("Register message verify OK")
auth = ctap1.authenticate(chal, appid, reg.key_handle)
print("authenticate result: ", auth)
res = auth.verify(appid, chal, reg.public_key)
print("Authenticate message verify OK")
fido2-2.2.1/examples/verify_attestation.py 0000644 0000000 0000000 00000010316 15220526030 015554 0 ustar 00 # Copyright (c) 2021 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.
"""
This example shows how to use an AttestationVerifier to only allow credentials signed
by a specific CA.
It connects to the first FIDO device found (starts from USB, then looks into NFC),
creates a new credential for it, and verifies that attestation is signed by the
Yubico FIDO root CA (this will only work for Yubico devices).
On Windows, the native WebAuthn API will be used.
"""
from base64 import b64decode
from exampleutils import get_client
from fido2.attestation import AttestationVerifier
from fido2.server import Fido2Server
# Official Yubico root CA for FIDO Authenticators
YUBICO_CA = b64decode(
"""
MIIDHjCCAgagAwIBAgIEG0BT9zANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZ
dWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAw
MDBaGA8yMDUwMDkwNDAwMDAwMFowLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290
IENBIFNlcmlhbCA0NTcyMDA2MzEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQC/jwYuhBVlqaiYWEMsrWFisgJ+PtM91eSrpI4TK7U53mwCIawSDHy8vUmk
5N2KAj9abvT9NP5SMS1hQi3usxoYGonXQgfO6ZXyUA9a+KAkqdFnBnlyugSeCOep
8EdZFfsaRFtMjkwz5Gcz2Py4vIYvCdMHPtwaz0bVuzneueIEz6TnQjE63Rdt2zbw
nebwTG5ZybeWSwbzy+BJ34ZHcUhPAY89yJQXuE0IzMZFcEBbPNRbWECRKgjq//qT
9nmDOFVlSRCt2wiqPSzluwn+v+suQEBsUjTGMEd25tKXXTkNW21wIWbxeSyUoTXw
LvGS6xlwQSgNpk2qXYwf8iXg7VWZAgMBAAGjQjBAMB0GA1UdDgQWBBQgIvz0bNGJ
hjgpToksyKpP9xv9oDAPBgNVHRMECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBBjAN
BgkqhkiG9w0BAQsFAAOCAQEAjvjuOMDSa+JXFCLyBKsycXtBVZsJ4Ue3LbaEsPY4
MYN/hIQ5ZM5p7EjfcnMG4CtYkNsfNHc0AhBLdq45rnT87q/6O3vUEtNMafbhU6kt
hX7Y+9XFN9NpmYxr+ekVY5xOxi8h9JDIgoMP4VB1uS0aunL1IGqrNooL9mmFnL2k
LVVee6/VR6C5+KSTCMCWppMuJIZII2v9o4dkoZ8Y7QRjQlLfYzd3qGtKbw7xaF1U
sG/5xUb/Btwb2X2g4InpiB/yt/3CpQXpiWX/K4mBvUKiGn05ZsqeY1gx4g0xLBqc
U9psmyPzK+Vsgw2jeRQ5JlKDyqE0hebfC1tvFu0CCrJFcw==
"""
)
class YubicoAttestationVerifier(AttestationVerifier):
"""Example implementation of an AttestationVerifier.
This simple example will attempt to verify all trust paths using the Yubico CA.
A real implementation can use the information in the attestation result, or the
authenticator data, to determine which CA should be used to verify the path.
"""
def ca_lookup(self, result, auth_data):
return YUBICO_CA
# Locate a suitable FIDO authenticator
client, _ = get_client()
server = Fido2Server(
{"id": "example.com", "name": "Example RP"},
attestation="direct",
verify_attestation=YubicoAttestationVerifier(),
)
user = {"id": b"user_id", "name": "A. User"}
# Prepare parameters for makeCredential
create_options, state = server.register_begin(
user, user_verification="discouraged", authenticator_attachment="cross-platform"
)
# Create a credential
result = client.make_credential(create_options["publicKey"])
# Complete registration
auth_data = server.register_complete(state, result)
credentials = [auth_data.credential_data]
print("New credential created, attestation verified!")
print("Yubico device AAGUID:", auth_data.credential_data.aaguid.hex())
fido2-2.2.1/examples/verify_attestation_mds3.py 0000644 0000000 0000000 00000011724 15220526030 016506 0 ustar 00 # Copyright (c) 2021 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.
"""
This example shows how to use the FIDO MDS to only allow authenticators for which
metadata is available.
It connects to the first FIDO device found (starts from USB, then looks into NFC),
creates a new credential for it, and verifies that attestation is correctly signed
and valid according to its metadata statement.
On Windows, the native WebAuthn API will be used.
NOTE: You need to retrieve a MDS3 blob to run this example.
See https://fidoalliance.org/metadata/ for more info.
"""
import sys
from base64 import b64decode
from exampleutils import get_client
from fido2.attestation import UntrustedAttestation
from fido2.mds3 import MdsAttestationVerifier, parse_blob
from fido2.server import Fido2Server
# Load the root CA used to sign the Metadata Statement blob
ca = b64decode(
"""
MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G
A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp
Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4
MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG
A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI
hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8
RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT
gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm
KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd
QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ
XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw
DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o
LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU
RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp
jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK
6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX
mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs
Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH
WD9f"""
)
# Parse the MDS3 blob
if len(sys.argv) != 2:
print("This example requires a FIDO MDS3 metadata blob, which you can get here:")
print("https://fidoalliance.org/metadata/")
print()
print("USAGE: python verify_attestation_mds3.py blob.jwt")
sys.exit(1)
with open(sys.argv[1], "rb") as f:
metadata = parse_blob(f.read(), ca)
# The verifier is used to query for data in the blob and to verify attestation.
# We could optionally pass a filter function to only allow specific authenticators.
mds = MdsAttestationVerifier(metadata)
# Locate a suitable FIDO authenticator
client, _ = get_client()
# The MDS verifier is passed to the server to verify that new credentials registered
# exist in the MDS blob, else the registration will fail.
server = Fido2Server(
{"id": "example.com", "name": "Example RP"},
attestation="direct",
verify_attestation=mds,
)
user = {"id": b"user_id", "name": "A. User"}
# Prepare parameters for makeCredential
create_options, state = server.register_begin(
user, user_verification="discouraged", authenticator_attachment="cross-platform"
)
# Create a credential
result = client.make_credential(create_options["publicKey"])
# Complete registration
try:
auth_data = server.register_complete(state, result)
print("Registration completed")
# mds can also be used to get the metadata for the Authenticator,
# regardless of if it was used to verify the attestation or not:
response = result.response
entry = mds.find_entry(response.attestation_object, response.client_data.hash)
print("Authenticator description:", entry.metadata_statement.description)
except UntrustedAttestation:
print("Authenticator metadata not found")
fido2-2.2.1/fido2/__init__.py 0000644 0000000 0000000 00000002533 15220526030 012557 0 ustar 00 # Copyright (c) 2013 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.
fido2-2.2.1/fido2/arkg.py 0000644 0000000 0000000 00000034557 15220526030 011757 0 ustar 00 # Copyright (c) 2024 Yubico AB
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import struct
from dataclasses import dataclass
from typing import Sequence, Tuple
from cryptography.hazmat.primitives.asymmetric.ec import (
ECDH,
EllipticCurve,
EllipticCurvePrivateKey,
EllipticCurvePublicKey,
EllipticCurvePublicNumbers,
derive_private_key,
)
from cryptography.hazmat.primitives.hashes import Hash, HashAlgorithm
from cryptography.hazmat.primitives.hmac import HMAC
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat
from .utils import bytes2int
"""
WARNING: This module and these algorithms are currently in DRAFT.
They are to be considered experimental, and NOT part of the stable API of this library.
This module implements the Asynchronous Remote Key Generation (ARKG) algorithm,
as specified in https://www.ietf.org/archive/id/draft-bradleylundberg-cfrg-arkg-08.html
"""
@dataclass(frozen=True)
class _CurveParams:
"""Weierstrass curve parameters needed for EC point addition."""
prime: int
a: int
_CURVE_PARAMS: dict[str, _CurveParams] = {
"secp256r1": _CurveParams(
prime=0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF,
a=0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC,
),
}
def _params_for_curve(curve: EllipticCurve) -> _CurveParams:
"""Look up curve parameters, or raise ValueError for unsupported curves."""
try:
return _CURVE_PARAMS[curve.name]
except KeyError:
raise ValueError(f"Unsupported curve: {curve.name}")
def _strxor(a: bytes, b: bytes) -> bytes:
return bytes(a[i] ^ b[i] for i in range(len(a)))
def _point_add(
p1: EllipticCurvePublicKey, p2: EllipticCurvePublicKey
) -> EllipticCurvePublicKey:
"""Add two EC points. Both points must be on the same supported curve."""
if p1.curve.name != p2.curve.name:
raise ValueError("Cannot add points from different curves")
params = _params_for_curve(p1.curve)
n1 = p1.public_numbers()
n2 = p2.public_numbers()
x1, y1 = n1.x, n1.y
x2, y2 = n2.x, n2.y
p = params.prime
if x1 == x2 and y1 == y2:
lam = (3 * x1 * x1 + params.a) * pow(2 * y1, -1, p) % p
else:
lam = (y2 - y1) * pow(x2 - x1, -1, p) % p
x3 = (lam * lam - x1 - x2) % p
y3 = (lam * (x1 - x3) - y1) % p
return EllipticCurvePublicNumbers(x3, y3, p1.curve).public_key()
@dataclass
class _HTF:
"""
hash_to_field(msg, count)
Parameters:
- DST, a domain separation tag (see Section 3.1).
- F, a finite field of characteristic p and order q = p^m.
- p, the characteristic of F (see immediately above).
- m, the extension degree of F, m >= 1 (see immediately above).
- L = ceil((ceil(log2(p)) + k) / 8), where k is the security
parameter of the suite (e.g., k = 128).
- expand_message, a function that expands a byte string and
domain separation tag into a uniformly random byte string
(see Section 5.3).
"""
DST: bytes
p: int
# m: int - is always 1
L: int
Hash: HashAlgorithm
# expand_message is always xmd
def expand_message_xmd(self, msg: bytes, len_in_bytes: int):
"""
expand_message_xmd(msg, DST, len_in_bytes)
Parameters:
- H, a hash function (see requirements above).
- b_in_bytes, b / 8 for b the output size of H in bits.
For example, for b = 256, b_in_bytes = 32.
- s_in_bytes, the input block size of H, measured in bytes (see
discussion above). For example, for SHA-256, s_in_bytes = 64.
Input:
- msg, a byte string.
- DST, a byte string of at most 255 bytes.
See below for information on using longer DSTs.
- len_in_bytes, the length of the requested output in bytes,
not greater than the lesser of (255 * b_in_bytes) or 2^16-1.
Output:
- uniform_bytes, a byte string.
Steps:
1. ell = ceil(len_in_bytes / b_in_bytes)
2. ABORT if ell > 255 or len_in_bytes > 65535 or len(DST) > 255
3. DST_prime = DST || I2OSP(len(DST), 1)
4. Z_pad = I2OSP(0, s_in_bytes)
5. l_i_b_str = I2OSP(len_in_bytes, 2)
6. msg_prime = Z_pad || msg || l_i_b_str || I2OSP(0, 1) || DST_prime
7. b_0 = H(msg_prime)
8. b_1 = H(b_0 || I2OSP(1, 1) || DST_prime)
9. for i in (2, ..., ell):
10. b_i = H(strxor(b_0, b_(i - 1)) || I2OSP(i, 1) || DST_prime)
11. uniform_bytes = b_1 || ... || b_ell
12. return substr(uniform_bytes, 0, len_in_bytes)
"""
b_in_bytes = self.Hash.digest_size
ell = -(-len_in_bytes // b_in_bytes)
if ell > 255 or len_in_bytes > 65535 or len(self.DST) > 255:
raise ValueError("Invalid size of input/output")
dst_prime = self.DST + struct.pack(">B", len(self.DST))
assert self.Hash.block_size is not None # noqa: S101
z_pad = b"\x00" * self.Hash.block_size
l_i_b_str = struct.pack(">H", len_in_bytes)
msg_prime = z_pad + msg + l_i_b_str + b"\x00" + dst_prime
d = Hash(self.Hash)
d.update(msg_prime)
b_0 = d.finalize()
b_xor = b_0
uniform_bytes = bytearray()
for i in range(1, ell + 1):
d = Hash(self.Hash)
d.update(b_xor + struct.pack(">B", i) + dst_prime)
b_i = d.finalize()
uniform_bytes.extend(b_i)
b_xor = _strxor(b_0, b_i)
return bytes(uniform_bytes[:len_in_bytes])
def hash_to_field(self, msg: bytes, count: int) -> Sequence[int]:
"""
hash_to_field(msg, count)
Input:
- msg, a byte string containing the message to hash.
- count, the number of elements of F to output.
Output:
- (u_0, ..., u_(count - 1)), a list of field elements.
Steps:
1. len_in_bytes = count * m * L
2. uniform_bytes = expand_message(msg, DST, len_in_bytes)
3. for i in (0, ..., count - 1):
4. for j in (0, ..., m - 1):
5. elm_offset = L * (j + i * m)
6. tv = substr(uniform_bytes, elm_offset, L)
7. e_j = OS2IP(tv) mod p
8. u_i = (e_0, ..., e_(m - 1))
9. return (u_0, ..., u_(count - 1))
"""
# Only implemented for m = 1
uniform_bytes = self.expand_message_xmd(msg, count * self.L)
elements = list()
for i in range(count):
offset = self.L * i
tv = uniform_bytes[offset : offset + self.L]
e_j = bytes2int(tv) % self.p
elements.append(e_j)
return elements
@dataclass
class _BL:
crv: EllipticCurve
Hash: HashAlgorithm
DST_ext: bytes
def prf(self, ikm_tau: bytes, ctx: bytes) -> int:
"""
BL-PRF(ikm_tau, ctx) -> tau
DST_tau = 'ARKG-BL-EC.' || DST_ext || ctx
tau = hash_to_field(tau, 1) with the parameters:
DST: DST_tau
F: GF(N), the scalar field
of the prime order subgroup of crv
p: N
m: 1
L: The L defined in hash-to-crv-suite
expand_message: The expand_message function
defined in hash-to-crv-suite
"""
dst_tau = b"ARKG-BL-EC." + self.DST_ext + ctx
htf = _HTF(dst_tau, self.crv.group_order, 48, self.Hash)
tau = htf.hash_to_field(ikm_tau, 1)[0]
return tau
def blind_public_key(
self, pk: EllipticCurvePublicKey, tau: int
) -> EllipticCurvePublicKey:
"""
BL-Blind-Public-Key(pk, tau) -> pk_tau
pk_tau = pk + tau * G
"""
tau_g = derive_private_key(tau, self.crv).public_key()
return _point_add(pk, tau_g)
@dataclass
class _KEM:
crv: EllipticCurve
Hash: HashAlgorithm
DST_ext: bytes
def sub_kem_derive_key_pair(self, ikm: bytes) -> EllipticCurvePrivateKey:
"""
Sub-Kem-Derive-Key-Pair(ikm) -> (pk, sk)
sk = hash_to_field(ikm, 1) with the parameters:
DST: 'ARKG-KEM-ECDH-KG.' || DST_ext
F: GF(N), the scalar field
of the prime order subgroup of crv
p: N
m: 1
L: The L defined in hash-to-crv-suite
expand_message: The expand_message function
defined in hash-to-crv-suite
pk = sk * G
"""
htf = _HTF(
b"ARKG-KEM-ECDH-KG." + self.DST_ext,
self.crv.group_order,
48,
self.Hash,
)
sk = htf.hash_to_field(ikm, 1)[0]
return derive_private_key(sk, self.crv)
def sub_kem_encaps(
self, pk: EllipticCurvePublicKey, ikm: bytes, ctx: bytes
) -> Tuple[bytes, bytes]:
"""
ECDH(pk, sk) represents the compact output of ECDH [RFC6090]
using public key (curve point) pk and private key (exponent) sk.
G is the generator of the prime order subgroup of crv.
N is the order of G.
Sub-Kem-Encaps(pk, ikm, ctx) -> (k, c)
(pk', sk') = Sub-Kem-Derive-Key-Pair(ikm)
k = ECDH(pk, sk')
c = Elliptic-Curve-Point-to-Octet-String(pk')
"""
sk_prime = self.sub_kem_derive_key_pair(ikm)
pk_prime = sk_prime.public_key()
k = sk_prime.exchange(ECDH(), pk)
c = pk_prime.public_bytes(Encoding.X962, PublicFormat.UncompressedPoint)
return k, c
def encaps(
self, pk: EllipticCurvePublicKey, ikm: bytes, ctx: bytes
) -> Tuple[bytes, bytes]:
"""
KEM-Encaps(pk, ikm, ctx) -> (k, c)
ctx_sub = 'ARKG-KEM-HMAC.' || DST_ext || ctx
(k', c') = Sub-Kem-Encaps(pk, ikm, ctx_sub)
prk = HKDF-Extract with the arguments:
Hash: Hash
salt: not set
IKM: k'
mk = HKDF-Expand with the arguments:
Hash: Hash
PRK: prk
info: 'ARKG-KEM-HMAC-mac.' || DST_ext || ctx
L: L
t = HMAC-Hash-128(K=mk, text=c')
k = HKDF-Expand with the arguments:
Hash: Hash
PRK: prk
info: 'ARKG-KEM-HMAC-shared.' || DST_ext || ctx
L: The length of k' in octets.
c = t || c'
"""
ctx_sub = b"ARKG-KEM-HMAC." + self.DST_ext + ctx
k_prime, c_prime = self.sub_kem_encaps(pk, ikm, ctx_sub)
mk = HKDF(
self.Hash,
self.Hash.digest_size,
None,
b"ARKG-KEM-HMAC-mac." + self.DST_ext + ctx,
).derive(k_prime)
hmac = HMAC(mk, self.Hash)
hmac.update(c_prime)
t = hmac.finalize()[:16] # Truncate to 128-bit
k = HKDF(
self.Hash,
len(k_prime),
None,
b"ARKG-KEM-HMAC-shared." + self.DST_ext + ctx,
).derive(k_prime)
c = t + c_prime
return k, c
@dataclass
class _ARKG:
"""
ARKG instance parameters:
BL A key blinding scheme.
KEM A key encapsulation mechanism.
"""
bl: _BL
kem: _KEM
def derive_public_key(
self,
pk_bl: EllipticCurvePublicKey,
pk_kem: EllipticCurvePublicKey,
ikm: bytes,
ctx: bytes,
) -> Tuple[EllipticCurvePublicKey, bytes]:
"""
ARKG-Derive-Public-Key((pk_bl, pk_kem), ikm, ctx) -> (pk', kh)
ARKG instance parameters:
BL A key blinding scheme.
KEM A key encapsulation mechanism.
Inputs:
pk_bl A key blinding public key.
pk_kem A key encapsulation public key.
ikm Input entropy for KEM encapsulation.
ctx An octet string of length at most 64,
containing optional context and
application specific information
(can be a zero-length string).
Output:
pk' A blinded public key.
kh A key handle for deriving the blinded
private key sk' corresponding to pk'.
The output (pk', kh) is calculated as follows:
if LEN(ctx) > 64:
Abort with an error.
ctx' = I2OSP(LEN(ctx), 1) || ctx
ctx_bl = 'ARKG-Derive-Key-BL.' || ctx'
ctx_kem = 'ARKG-Derive-Key-KEM.' || ctx'
(ikm_tau, c) = KEM-Encaps(pk_kem, ikm, ctx_kem)
tau = BL-PRF(ikm_tau, ctx_bl)
pk' = BL-Blind-Public-Key(pk_bl, tau)
kh = c
"""
if len(ctx) > 64:
raise ValueError("Context too long, should be at most 64 bytes")
ctx_prime = struct.pack(">B", len(ctx)) + ctx
ctx_bl = b"ARKG-Derive-Key-BL." + ctx_prime
ctx_kem = b"ARKG-Derive-Key-KEM." + ctx_prime
ikm_tau, c = self.kem.encaps(pk_kem, ikm, ctx_kem)
tau = self.bl.prf(ikm_tau, ctx_bl)
pk_prime = self.bl.blind_public_key(pk_bl, tau)
kh = c
return pk_prime, kh
fido2-2.2.1/fido2/attestation/__init__.py 0000644 0000000 0000000 00000003571 15220526030 015121 0 ustar 00 # Copyright (c) 2020 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 .android import AndroidSafetynetAttestation # noqa: F401
from .apple import AppleAttestation # noqa: F401
from .base import ( # noqa: F401
Attestation,
AttestationResult,
AttestationType,
AttestationVerifier,
InvalidData,
InvalidSignature,
NoneAttestation,
UnsupportedAttestation,
UnsupportedType,
UntrustedAttestation,
verify_x509_chain,
)
from .packed import PackedAttestation # noqa: F401
from .tpm import TpmAttestation # noqa: F401
from .u2f import FidoU2FAttestation # noqa: F401
fido2-2.2.1/fido2/attestation/android.py 0000644 0000000 0000000 00000006025 15220526030 014777 0 ustar 00 # 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.
from __future__ import annotations
import json
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.constant_time import bytes_eq
from ..cose import CoseKey
from ..utils import sha256, websafe_decode
from .base import (
Attestation,
AttestationResult,
AttestationType,
InvalidData,
catch_builtins,
)
class AndroidSafetynetAttestation(Attestation):
FORMAT = "android-safetynet"
def __init__(self, allow_rooted: bool = False):
self.allow_rooted = allow_rooted
@catch_builtins
def verify(self, statement, auth_data, client_data_hash):
jwt = statement["response"]
header, payload, sig = (websafe_decode(x) for x in jwt.split(b"."))
data = json.loads(payload.decode("utf8"))
if not self.allow_rooted and data["ctsProfileMatch"] is not True:
raise InvalidData("ctsProfileMatch must be true!")
expected_nonce = sha256(auth_data + client_data_hash)
if not bytes_eq(expected_nonce, websafe_decode(data["nonce"])):
raise InvalidData("Nonce does not match!")
data = json.loads(header.decode("utf8"))
x5c = [websafe_decode(x) for x in data["x5c"]]
cert = x509.load_der_x509_certificate(x5c[0], default_backend())
cn = cert.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)
if cn[0].value != "attest.android.com":
raise InvalidData("Certificate not issued to attest.android.com!")
CoseKey.for_name(data["alg"]).from_cryptography_key(cert.public_key()).verify(
jwt.rsplit(b".", 1)[0], sig
)
return AttestationResult(AttestationType.BASIC, x5c)
fido2-2.2.1/fido2/attestation/apple.py 0000644 0000000 0000000 00000004556 15220526030 014467 0 ustar 00 # Copyright (c) 2020 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 __future__ import annotations
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.constant_time import bytes_eq
from ..utils import sha256
from .base import (
Attestation,
AttestationResult,
AttestationType,
InvalidData,
catch_builtins,
)
OID_APPLE = x509.ObjectIdentifier("1.2.840.113635.100.8.2")
class AppleAttestation(Attestation):
FORMAT = "apple"
@catch_builtins
def verify(self, statement, auth_data, client_data_hash):
x5c = statement["x5c"]
expected_nonce = sha256(auth_data + client_data_hash)
cert = x509.load_der_x509_certificate(x5c[0], default_backend())
ext = cert.extensions.get_extension_for_oid(OID_APPLE)
# Sequence of single element of octet string
ext_nonce = ext.value.public_bytes()[6:]
if not bytes_eq(expected_nonce, ext_nonce):
raise InvalidData("Nonce does not match!")
return AttestationResult(AttestationType.ANON_CA, x5c)
fido2-2.2.1/fido2/attestation/base.py 0000644 0000000 0000000 00000021105 15220526030 014265 0 ustar 00 # 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.
from __future__ import annotations
import abc
from dataclasses import dataclass
from enum import IntEnum, unique
from functools import wraps
from typing import Any, Mapping, Sequence, cast
from cryptography import x509
from cryptography.exceptions import InvalidSignature as _InvalidSignature
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import ec, padding, rsa
from ..webauthn import AttestationObject, AuthenticatorData
class InvalidAttestation(Exception):
"""Base exception for attestation-related errors."""
class InvalidData(InvalidAttestation):
"""Attestation contains invalid data."""
class InvalidSignature(InvalidAttestation):
"""The signature of the attestation could not be verified."""
class UntrustedAttestation(InvalidAttestation):
"""The CA of the attestation is not trusted."""
class UnsupportedType(InvalidAttestation):
"""The attestation format is not supported."""
def __init__(self, auth_data, fmt=None):
super().__init__(
f'Attestation format "{fmt}" is not supported'
if fmt
else "This attestation format is not supported!"
)
self.auth_data = auth_data
self.fmt = fmt
@unique
class AttestationType(IntEnum):
"""Supported attestation types."""
BASIC = 1
SELF = 2
ATT_CA = 3
ANON_CA = 4
NONE = 0
@dataclass
class AttestationResult:
"""The result of verifying an attestation."""
attestation_type: AttestationType
trust_path: list[bytes]
def catch_builtins(f):
"""Utility decoractor to wrap common exceptions related to InvalidData."""
@wraps(f)
def inner(*args, **kwargs):
try:
return f(*args, **kwargs)
except (ValueError, KeyError, IndexError) as e:
raise InvalidData(e)
return inner
@catch_builtins
def verify_x509_chain(chain: list[bytes]) -> None:
"""Verifies a chain of certificates.
Checks that the first item in the chain is signed by the next, and so on.
The first item is the leaf, the last is the root.
"""
certs = [x509.load_der_x509_certificate(der, default_backend()) for der in chain]
cert = certs.pop(0)
while certs:
child = cert
cert = certs.pop(0)
pub = cert.public_key()
try:
if isinstance(pub, rsa.RSAPublicKey):
assert child.signature_hash_algorithm is not None # noqa: S101
pub.verify(
child.signature,
child.tbs_certificate_bytes,
padding.PKCS1v15(),
child.signature_hash_algorithm,
)
elif isinstance(pub, ec.EllipticCurvePublicKey):
assert child.signature_hash_algorithm is not None # noqa: S101
pub.verify(
child.signature,
child.tbs_certificate_bytes,
ec.ECDSA(child.signature_hash_algorithm),
)
else:
raise ValueError("Unsupported signature key type")
except _InvalidSignature:
raise InvalidSignature()
class Attestation(abc.ABC):
"""Implements verification of a specific attestation type."""
@abc.abstractmethod
def verify(
self,
statement: Mapping[str, Any],
auth_data: AuthenticatorData,
client_data_hash: bytes,
) -> AttestationResult:
"""Verifies attestation statement.
:return: An AttestationResult if successful.
"""
@staticmethod
def for_type(fmt: str) -> type[Attestation]:
"""Get an Attestation subclass type for the given format."""
for cls in Attestation.__subclasses__():
if getattr(cls, "FORMAT", None) == fmt:
return cls
class TypedUnsupportedAttestation(UnsupportedAttestation):
def __init__(self):
super().__init__(fmt)
return TypedUnsupportedAttestation
class UnsupportedAttestation(Attestation):
def __init__(self, fmt=None):
self.fmt = fmt
def verify(self, statement, auth_data, client_data_hash):
raise UnsupportedType(auth_data, self.fmt)
class NoneAttestation(Attestation):
FORMAT = "none"
def verify(self, statement, auth_data, client_data_hash):
if statement != {}:
raise InvalidData("None Attestation requires empty statement.")
return AttestationResult(AttestationType.NONE, [])
def _validate_cert_common(cert):
if cert.version != x509.Version.v3:
raise InvalidData("Attestation certificate must use version 3!")
try:
bc = cert.extensions.get_extension_for_class(x509.BasicConstraints)
if bc.value.ca:
raise InvalidData("Attestation certificate must have CA=false!")
except x509.ExtensionNotFound:
raise InvalidData("Attestation certificate must have Basic Constraints!")
def _default_attestations():
return [
cast(type[Any], cls)()
for cls in Attestation.__subclasses__()
if getattr(cls, "FORMAT", "none") != "none"
]
class AttestationVerifier(abc.ABC):
"""Base class for verifying attestation.
Override the ca_lookup method to provide a trusted root certificate used
to verify the trust path from the attestation.
"""
def __init__(self, attestation_types: Sequence[Attestation] | None = None):
self._attestation_types = attestation_types or _default_attestations()
@abc.abstractmethod
def ca_lookup(
self, attestation_result: AttestationResult, auth_data: AuthenticatorData
) -> bytes | None:
"""Lookup a CA certificate to be used to verify a trust path.
:param attestation_result: The result of the attestation
:param auth_data: The AuthenticatorData from the registration
"""
raise NotImplementedError()
def verify_attestation(
self, attestation_object: AttestationObject, client_data_hash: bytes
) -> None:
"""Verify attestation.
:param attestation_object: dict containing attestation data.
:param client_data_hash: SHA256 hash of the ClientData bytes.
"""
att_verifier: Attestation = UnsupportedAttestation(attestation_object.fmt)
for at in self._attestation_types:
if getattr(at, "FORMAT", None) == attestation_object.fmt:
att_verifier = at
break
# An unsupported format causes an exception to be thrown, which
# includes the auth_data. The caller may choose to handle this case
# and allow the registration.
result = att_verifier.verify(
attestation_object.att_stmt,
attestation_object.auth_data,
client_data_hash,
)
# Lookup CA to use for trust path verification
ca = self.ca_lookup(result, attestation_object.auth_data)
if not ca:
raise UntrustedAttestation("No root found for Authenticator")
# Validate the trust chain
try:
verify_x509_chain(result.trust_path + [ca])
except InvalidSignature as e:
raise UntrustedAttestation(e)
def __call__(self, *args):
"""Allows passing an instance to Fido2Server as verify_attestation"""
self.verify_attestation(*args)
fido2-2.2.1/fido2/attestation/packed.py 0000644 0000000 0000000 00000010342 15220526030 014603 0 ustar 00 # 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.
from __future__ import annotations
from cryptography import x509
from cryptography.exceptions import InvalidSignature as _InvalidSignature
from cryptography.hazmat.backends import default_backend
from ..cose import CoseKey
from .base import (
Attestation,
AttestationResult,
AttestationType,
InvalidData,
InvalidSignature,
_validate_cert_common,
catch_builtins,
)
OID_AAGUID = x509.ObjectIdentifier("1.3.6.1.4.1.45724.1.1.4")
def _validate_packed_cert(cert, aaguid):
# https://www.w3.org/TR/webauthn/#packed-attestation-cert-requirements
_validate_cert_common(cert)
c = cert.subject.get_attributes_for_oid(x509.NameOID.COUNTRY_NAME)
if not c:
raise InvalidData("Subject must have C set!")
o = cert.subject.get_attributes_for_oid(x509.NameOID.ORGANIZATION_NAME)
if not o:
raise InvalidData("Subject must have O set!")
ous = cert.subject.get_attributes_for_oid(x509.NameOID.ORGANIZATIONAL_UNIT_NAME)
if not ous:
raise InvalidData('Subject must have OU = "Authenticator Attestation"!')
ou = ous[0]
if ou.value != "Authenticator Attestation":
raise InvalidData('Subject must have OU = "Authenticator Attestation"!')
cn = cert.subject.get_attributes_for_oid(x509.NameOID.COMMON_NAME)
if not cn:
raise InvalidData("Subject must have CN set!")
try:
ext = cert.extensions.get_extension_for_oid(OID_AAGUID)
if ext.critical:
raise InvalidData("AAGUID extension must not be marked as critical")
ext_aaguid = ext.value.value[2:]
if ext_aaguid != aaguid:
raise InvalidData(
"AAGUID in Authenticator data does not match attestation certificate!"
)
except x509.ExtensionNotFound:
pass # If missing, ignore
class PackedAttestation(Attestation):
FORMAT = "packed"
@catch_builtins
def verify(self, statement, auth_data, client_data_hash):
if "ecdaaKeyId" in statement:
raise NotImplementedError("ECDAA not implemented")
alg = statement["alg"]
x5c = statement.get("x5c")
assert auth_data.credential_data is not None # noqa: S101
if x5c:
cert = x509.load_der_x509_certificate(x5c[0], default_backend())
_validate_packed_cert(cert, auth_data.credential_data.aaguid)
pub_key = CoseKey.for_alg(alg).from_cryptography_key(cert.public_key())
att_type = AttestationType.BASIC
else:
pub_key = CoseKey.parse(auth_data.credential_data.public_key)
if pub_key.ALGORITHM != alg:
raise InvalidData("Wrong algorithm of public key!")
att_type = AttestationType.SELF
try:
pub_key.verify(auth_data + client_data_hash, statement["sig"])
return AttestationResult(att_type, x5c or [])
except _InvalidSignature:
raise InvalidSignature()
fido2-2.2.1/fido2/attestation/tpm.py 0000644 0000000 0000000 00000046102 15220526030 014157 0 ustar 00 # -*- coding: utf-8 -*-
# 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 __future__ import annotations
import struct
from dataclasses import dataclass
from enum import IntEnum, unique
from typing import TypeAlias, cast
from cryptography import x509
from cryptography.exceptions import InvalidSignature as _InvalidSignature
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec, rsa
from ..cose import CoseKey
from ..utils import ByteBuffer, bytes2int
from .base import (
Attestation,
AttestationResult,
AttestationType,
InvalidData,
InvalidSignature,
_validate_cert_common,
catch_builtins,
)
TPM_ALG_NULL = 0x0010
OID_AIK_CERTIFICATE = x509.ObjectIdentifier("2.23.133.8.3")
@unique
class TpmRsaScheme(IntEnum):
RSASSA = 0x0014
RSAPSS = 0x0016
OAEP = 0x0017
RSAES = 0x0015
@unique
class TpmAlgAsym(IntEnum):
RSA = 0x0001
ECC = 0x0023
@unique
class TpmAlgHash(IntEnum):
SHA1 = 0x0004
SHA256 = 0x000B
SHA384 = 0x000C
SHA512 = 0x000D
def _hash_alg(self) -> hashes.HashAlgorithm:
if self == TpmAlgHash.SHA1:
return hashes.SHA1() # noqa: S303
elif self == TpmAlgHash.SHA256:
return hashes.SHA256()
elif self == TpmAlgHash.SHA384:
return hashes.SHA384()
elif self == TpmAlgHash.SHA512:
return hashes.SHA512()
raise NotImplementedError(f"_hash_alg is not implemented for {self!r}")
@dataclass
class TpmsCertifyInfo:
name: bytes
qualified_name: bytes
TPM_GENERATED_VALUE = b"\xffTCG"
TPM_ST_ATTEST_CERTIFY = b"\x80\x17"
@dataclass
class TpmAttestationFormat:
"""the signature data is defined by [TPMv2-Part2] Section 10.12.8 (TPMS_ATTEST)
as:
TPM_GENERATED_VALUE (0xff544347 aka "\xffTCG")
TPMI_ST_ATTEST - always TPM_ST_ATTEST_CERTIFY (0x8017)
because signing procedure defines it should call TPM_Certify
[TPMv2-Part3] Section 18.2
TPM2B_NAME
size (uint16)
name (size long)
TPM2B_DATA
size (uint16)
name (size long)
TPMS_CLOCK_INFO
clock (uint64)
resetCount (uint32)
restartCount (uint32)
safe (byte) 1 yes, 0 no
firmwareVersion uint64
attested TPMS_CERTIFY_INFO (because TPM_ST_ATTEST_CERTIFY)
name TPM2B_NAME
qualified_name TPM2B_NAME
See:
https://www.trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf
https://www.trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-3-Commands-01.38.pdf
"""
name: bytes
data: bytes
clock_info: tuple[int, int, int, bool]
firmware_version: int
attested: TpmsCertifyInfo
@classmethod
def parse(cls, data: bytes) -> TpmAttestationFormat:
reader = ByteBuffer(data)
generated_value = reader.read(4)
# Verify that magic is set to TPM_GENERATED_VALUE.
# see https://w3c.github.io/webauthn/#sctn-tpm-attestation
# verification procedure
if generated_value != TPM_GENERATED_VALUE:
raise ValueError("generated value field is invalid")
# Verify that type is set to TPM_ST_ATTEST_CERTIFY.
# see https://w3c.github.io/webauthn/#sctn-tpm-attestation
# verification procedure
tpmi_st_attest = reader.read(2)
if tpmi_st_attest != TPM_ST_ATTEST_CERTIFY:
raise ValueError("tpmi_st_attest field is invalid")
try:
name = reader.read(reader.unpack("!H"))
data = reader.read(reader.unpack("!H"))
clock = reader.unpack("!Q")
reset_count = reader.unpack("!L")
restart_count = reader.unpack("!L")
safe_value = reader.unpack("B")
if safe_value not in (0, 1):
raise ValueError(f"invalid value 0x{safe_value:x} for boolean")
safe = safe_value == 1
firmware_version = reader.unpack("!Q")
attested_name = reader.read(reader.unpack("!H"))
attested_qualified_name = reader.read(reader.unpack("!H"))
except struct.error as e:
raise ValueError(e)
return cls(
name=name,
data=data,
clock_info=(clock, reset_count, restart_count, safe),
firmware_version=firmware_version,
attested=TpmsCertifyInfo(
name=attested_name, qualified_name=attested_qualified_name
),
)
@dataclass
class TpmsRsaParms:
"""Parse TPMS_RSA_PARMS struct
See:
https://www.trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf
section 12.2.3.5
"""
symmetric: int
scheme: int
key_bits: int
exponent: int
@classmethod
def parse(cls, reader, attributes):
symmetric = reader.unpack("!H")
restricted_decryption = attributes & (
ATTRIBUTES.RESTRICTED | ATTRIBUTES.DECRYPT
)
is_restricted_decryption_key = restricted_decryption == (
ATTRIBUTES.DECRYPT | ATTRIBUTES.RESTRICTED
)
if not is_restricted_decryption_key and symmetric != TPM_ALG_NULL:
# if the key is not a restricted decryption key, this field
# shall be set to TPM_ALG_NULL.
raise ValueError("symmetric is expected to be NULL")
# Otherwise should be set to a supported symmetric algorithm, keysize and mode
# TODO(baloo): Should we have non-null value here, do we expect more data?
scheme = reader.unpack("!H")
restricted_sign = attributes & (ATTRIBUTES.RESTRICTED | ATTRIBUTES.SIGN_ENCRYPT)
is_unrestricted_signing_key = restricted_sign == ATTRIBUTES.SIGN_ENCRYPT
if is_unrestricted_signing_key and scheme not in (
TPM_ALG_NULL,
TpmRsaScheme.RSASSA,
TpmRsaScheme.RSAPSS,
):
raise ValueError(
"key is an unrestricted signing key, scheme is "
"expected to be TPM_ALG_RSAPSS, TPM_ALG_RSASSA, "
"or TPM_ALG_NULL"
)
is_restricted_signing_key = restricted_sign == (
ATTRIBUTES.RESTRICTED | ATTRIBUTES.SIGN_ENCRYPT
)
if is_restricted_signing_key and scheme not in (
TpmRsaScheme.RSASSA,
TpmRsaScheme.RSAPSS,
):
raise ValueError(
"key is a restricted signing key, scheme is "
"expected to be TPM_ALG_RSAPSS, or TPM_ALG_RSASSA"
)
is_unrestricted_decryption_key = restricted_decryption == ATTRIBUTES.DECRYPT
if is_unrestricted_decryption_key and scheme not in (
TpmRsaScheme.OAEP,
TpmRsaScheme.RSAES,
TPM_ALG_NULL,
):
raise ValueError(
"key is an unrestricted decryption key, scheme is "
"expected to be TPM_ALG_RSAES, TPM_ALG_OAEP, or "
"TPM_ALG_NULL"
)
if is_restricted_decryption_key and scheme not in (TPM_ALG_NULL,):
raise ValueError(
"key is an restricted decryption key, scheme is "
"expected to be TPM_ALG_NULL"
)
key_bits = reader.unpack("!H")
exponent = reader.unpack("!L")
if exponent == 0:
# When zero, indicates that the exponent is the default of 2^16 + 1
exponent = (2**16) + 1
return cls(symmetric, scheme, key_bits, exponent)
class Tpm2bPublicKeyRsa(bytes):
@classmethod
def parse(cls, reader: ByteBuffer) -> Tpm2bPublicKeyRsa:
return cls(reader.read(reader.unpack("!H")))
@unique
class TpmEccCurve(IntEnum):
"""TPM_ECC_CURVE
https://www.trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf
section 6.4
"""
NONE = 0x0000
NIST_P192 = 0x0001
NIST_P224 = 0x0002
NIST_P256 = 0x0003
NIST_P384 = 0x0004
NIST_P521 = 0x0005
BN_P256 = 0x0010
BN_P638 = 0x0011
SM2_P256 = 0x0020
def to_curve(self) -> ec.EllipticCurve:
if self == TpmEccCurve.NONE:
raise ValueError("No such curve")
elif self == TpmEccCurve.NIST_P192:
return ec.SECP192R1()
elif self == TpmEccCurve.NIST_P224:
return ec.SECP224R1()
elif self == TpmEccCurve.NIST_P256:
return ec.SECP256R1()
elif self == TpmEccCurve.NIST_P384:
return ec.SECP384R1()
elif self == TpmEccCurve.NIST_P521:
return ec.SECP521R1()
raise ValueError("curve is not supported", self)
@unique
class TpmiAlgKdf(IntEnum):
"""TPMI_ALG_KDF
https://www.trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf
section 9.28
"""
NULL = TPM_ALG_NULL
KDF1_SP800_56A = 0x0020
KDF2 = 0x0021
KDF1_SP800_108 = 0x0022
@dataclass
class TpmsEccParms:
symmetric: int
scheme: int
curve_id: TpmEccCurve
kdf: TpmiAlgKdf
@classmethod
def parse(cls, reader: ByteBuffer) -> TpmsEccParms:
symmetric = reader.unpack("!H")
scheme = reader.unpack("!H")
if symmetric != TPM_ALG_NULL:
raise ValueError("symmetric is expected to be NULL")
if scheme != TPM_ALG_NULL:
raise ValueError("scheme is expected to be NULL")
curve_id = TpmEccCurve(reader.unpack("!H"))
kdf_scheme = TpmiAlgKdf(reader.unpack("!H"))
return cls(symmetric, scheme, curve_id, kdf_scheme)
@dataclass
class TpmsEccPoint:
"""TPMS_ECC_POINT
https://www.trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf
Section 11.2.5.2
"""
x: bytes
y: bytes
@classmethod
def parse(cls, reader: ByteBuffer) -> TpmsEccPoint:
x = reader.read(reader.unpack("!H"))
y = reader.read(reader.unpack("!H"))
return cls(x, y)
@unique
class ATTRIBUTES(IntEnum):
"""Object attributes
see section 8.3
https://www.trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf
"""
FIXED_TPM = 1 << 1
ST_CLEAR = 1 << 2
FIXED_PARENT = 1 << 4
SENSITIVE_DATA_ORIGIN = 1 << 5
USER_WITH_AUTH = 1 << 6
ADMIN_WITH_POLICY = 1 << 7
NO_DA = 1 << 10
ENCRYPTED_DUPLICATION = 1 << 11
RESTRICTED = 1 << 16
DECRYPT = 1 << 17
SIGN_ENCRYPT = 1 << 18
SHALL_BE_ZERO = (
(1 << 0) # 0 Reserved
| (1 << 3) # 3 Reserved
| (0x3 << 8) # 9:8 Reserved
| (0xF << 12) # 15:12 Reserved
| ((0xFFFFFFFF << 19) & (2**32 - 1)) # 31:19 Reserved
)
_PublicKey: TypeAlias = rsa.RSAPublicKey | ec.EllipticCurvePublicKey
_Parameters = TpmsRsaParms | TpmsEccParms
_Unique = Tpm2bPublicKeyRsa | TpmsEccPoint
@dataclass
class TpmPublicFormat:
"""the public area structure is defined by [TPMv2-Part2] Section 12.2.4
(TPMT_PUBLIC)
as:
TPMI_ALG_PUBLIC - type
TPMI_ALG_HASH - nameAlg
or + to indicate TPM_ALG_NULL
TPMA_OBJECT - objectAttributes
TPM2B_DIGEST - authPolicy
TPMU_PUBLIC_PARMS - type parameters
TPMU_PUBLIC_ID - uniq
See:
https://www.trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf
"""
sign_alg: TpmAlgAsym
name_alg: TpmAlgHash
attributes: int
auth_policy: bytes
parameters: _Parameters
unique: _Unique
data: bytes
@classmethod
def parse(cls, data: bytes) -> TpmPublicFormat:
reader = ByteBuffer(data)
sign_alg = TpmAlgAsym(reader.unpack("!H"))
name_alg = TpmAlgHash(reader.unpack("!H"))
attributes = reader.unpack("!L")
if attributes & ATTRIBUTES.SHALL_BE_ZERO != 0:
raise ValueError(f"attributes is not formated correctly: 0x{attributes:x}")
auth_policy = reader.read(reader.unpack("!H"))
if sign_alg == TpmAlgAsym.RSA:
parameters: _Parameters = TpmsRsaParms.parse(reader, attributes)
unique: _Unique = Tpm2bPublicKeyRsa.parse(reader)
elif sign_alg == TpmAlgAsym.ECC:
parameters = TpmsEccParms.parse(reader)
unique = TpmsEccPoint.parse(reader)
else:
raise NotImplementedError(f"sign alg {sign_alg:x} is not supported")
rest = reader.read()
if len(rest) != 0:
raise ValueError("there should not be any data left in buffer")
return cls(
sign_alg, name_alg, attributes, auth_policy, parameters, unique, data
)
def public_key(self) -> _PublicKey:
if self.sign_alg == TpmAlgAsym.RSA:
exponent = cast(TpmsRsaParms, self.parameters).exponent
modulus = bytes2int(cast(Tpm2bPublicKeyRsa, self.unique))
return rsa.RSAPublicNumbers(exponent, modulus).public_key(default_backend())
elif self.sign_alg == TpmAlgAsym.ECC:
unique = cast(TpmsEccPoint, self.unique)
return ec.EllipticCurvePublicNumbers(
bytes2int(unique.x),
bytes2int(unique.y),
cast(TpmsEccParms, self.parameters).curve_id.to_curve(),
).public_key(default_backend())
raise NotImplementedError(f"public_key not implemented for {self.sign_alg!r}")
def name(self) -> bytes:
"""
Computing Entity Names
see:
https://www.trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-1-Architecture-01.38.pdf
section 16 Names
Name ≔ nameAlg || HnameAlg (handle→nvPublicArea)
where
nameAlg algorithm used to compute Name
HnameAlg hash using the nameAlg parameter in the NV Index location
associated with handle
nvPublicArea contents of the TPMS_NV_PUBLIC associated with handle
"""
output = struct.pack("!H", self.name_alg)
digest = hashes.Hash(self.name_alg._hash_alg(), backend=default_backend())
digest.update(self.data)
output += digest.finalize()
return output
def _validate_tpm_cert(cert):
# https://www.w3.org/TR/webauthn/#tpm-cert-requirements
_validate_cert_common(cert)
if len(cert.subject) > 0:
raise InvalidData("Certificate should not have Subject")
s = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName)
if not s:
raise InvalidData("Certificate should have SubjectAlternativeName")
ext = cert.extensions.get_extension_for_class(x509.ExtendedKeyUsage)
has_aik = [x == OID_AIK_CERTIFICATE for x in ext.value]
if True not in has_aik:
raise InvalidData(
'Extended key usage MUST contain the "joint-iso-itu-t(2) '
"internationalorganizations(23) 133 tcg-kp(8) "
'tcg-kp-AIKCertificate(3)" OID.'
)
class TpmAttestation(Attestation):
FORMAT = "tpm"
@catch_builtins
def verify(self, statement, auth_data, client_data_hash):
if "ecdaaKeyId" in statement:
raise NotImplementedError("ECDAA not implemented")
alg = statement["alg"]
x5c = statement["x5c"]
cert_info = statement["certInfo"]
cert = x509.load_der_x509_certificate(x5c[0], default_backend())
_validate_tpm_cert(cert)
pub_key = CoseKey.for_alg(alg).from_cryptography_key(cert.public_key())
try:
pub_area = TpmPublicFormat.parse(statement["pubArea"])
except Exception as e:
raise InvalidData("unable to parse pubArea", e)
# Verify that the public key specified by the parameters and unique
# fields of pubArea is identical to the credentialPublicKey in the
# attestedCredentialData in authenticatorData.
assert auth_data.credential_data is not None # noqa: S101
if (
auth_data.credential_data.public_key.from_cryptography_key(
pub_area.public_key()
)
!= auth_data.credential_data.public_key
):
raise InvalidSignature(
"attestation pubArea does not match attestedCredentialData"
)
try:
# TpmAttestationFormat.parse is reponsible for:
# Verify that magic is set to TPM_GENERATED_VALUE.
# Verify that type is set to TPM_ST_ATTEST_CERTIFY.
tpm = TpmAttestationFormat.parse(cert_info)
# Verify that extraData is set to the hash of attToBeSigned
# using the hash algorithm employed in "alg".
att_to_be_signed = auth_data + client_data_hash
hash_alg = pub_key._HASH_ALG # type: ignore
digest = hashes.Hash(hash_alg, backend=default_backend())
digest.update(att_to_be_signed)
data = digest.finalize()
if tpm.data != data:
raise InvalidSignature(
"attestation does not sign for authData and ClientData"
)
# Verify that attested contains a TPMS_CERTIFY_INFO structure as
# specified in [TPMv2-Part2] section 10.12.3, whose name field
# contains a valid Name for pubArea, as computed using the
# algorithm in the nameAlg field of pubArea using the procedure
# specified in [TPMv2-Part1] section 16.
# [TPMv2-Part2]:
# https://www.trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-2-Structures-01.38.pdf
# [TPMv2-Part1]:
# https://www.trustedcomputinggroup.org/wp-content/uploads/TPM-Rev-2.0-Part-1-Architecture-01.38.pdf
if tpm.attested.name != pub_area.name():
raise InvalidData(
"TPMS_CERTIFY_INFO does not include a valid name for pubArea"
)
pub_key.verify(cert_info, statement["sig"])
return AttestationResult(AttestationType.ATT_CA, x5c)
except _InvalidSignature:
raise InvalidSignature("signature of certInfo does not match")
fido2-2.2.1/fido2/attestation/u2f.py 0000644 0000000 0000000 00000005312 15220526030 014051 0 ustar 00 # 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.
from __future__ import annotations
from cryptography import x509
from cryptography.exceptions import InvalidSignature as _InvalidSignature
from cryptography.hazmat.backends import default_backend
from ..cose import ES256
from .base import (
Attestation,
AttestationResult,
AttestationType,
InvalidSignature,
catch_builtins,
)
class FidoU2FAttestation(Attestation):
FORMAT = "fido-u2f"
@catch_builtins
def verify(self, statement, auth_data, client_data_hash):
cd = auth_data.credential_data
assert cd is not None # noqa: S101
pk = b"\x04" + cd.public_key[-2] + cd.public_key[-3]
x5c = statement["x5c"]
FidoU2FAttestation.verify_signature(
auth_data.rp_id_hash,
client_data_hash,
cd.credential_id,
pk,
x5c[0],
statement["sig"],
)
return AttestationResult(AttestationType.BASIC, x5c)
@staticmethod
def verify_signature(
app_param, client_param, key_handle, public_key, cert_bytes, signature
):
m = b"\0" + app_param + client_param + key_handle + public_key
cert = x509.load_der_x509_certificate(cert_bytes, default_backend())
try:
ES256.from_cryptography_key(cert.public_key()).verify(m, signature)
except _InvalidSignature:
raise InvalidSignature()
fido2-2.2.1/fido2/cbor.py 0000644 0000000 0000000 00000013332 15220526030 011744 0 ustar 00 # 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.
"""
Minimal CBOR implementation supporting a subset of functionality and types
required for FIDO 2 CTAP.
Use the :func:`encode`, :func:`decode` and :func:`decode_from` functions to encode
and decode objects to/from CBOR.
"""
from __future__ import annotations
import struct
from types import UnionType
from typing import Any, Callable, Mapping, Sequence, TypeAlias
CborType: TypeAlias = int | bool | str | bytes | Sequence[Any] | Mapping[Any, Any]
# TODO: Requires Python 3.12, replace with collections.abc.Buffer
Buffer: TypeAlias = bytes | bytearray | memoryview
def _dump_int(data: int, mt: int = 0) -> bytes:
if data < 0:
mt = 1
data = -1 - data
mt = mt << 5
fmt: str
args: tuple[int, ...]
if data <= 23:
fmt, args = ">B", (mt | data,)
elif data <= 0xFF:
fmt, args = ">BB", (mt | 24, data)
elif data <= 0xFFFF:
fmt, args = ">BH", (mt | 25, data)
elif data <= 0xFFFFFFFF:
fmt, args = ">BI", (mt | 26, data)
else:
fmt, args = ">BQ", (mt | 27, data)
return struct.pack(fmt, *args)
def _dump_bool(data: bool) -> bytes:
return b"\xf5" if data else b"\xf4"
def _dump_list(data: Sequence[CborType]) -> bytes:
return _dump_int(len(data), mt=4) + b"".join([encode(x) for x in data])
def _sort_keys(entry):
key = entry[0]
return key[0], len(key), key
def _dump_dict(data: Mapping[CborType, CborType]) -> bytes:
items = [(encode(k), encode(v)) for k, v in data.items()]
items.sort(key=_sort_keys)
return _dump_int(len(items), mt=5) + b"".join([k + v for (k, v) in items])
def _dump_bytes(data: bytes) -> bytes:
return _dump_int(len(data), mt=2) + data
def _dump_text(data: str) -> bytes:
data_bytes = data.encode("utf8")
return _dump_int(len(data_bytes), mt=3) + data_bytes
_SERIALIZERS: Sequence[tuple[type | UnionType, Callable[[Any], bytes]]] = [
(bool, _dump_bool),
(int, _dump_int),
(str, _dump_text),
(Buffer, _dump_bytes),
(Mapping, _dump_dict),
(Sequence, _dump_list),
]
def _load_int(ai: int, data: bytes) -> tuple[int, bytes]:
if ai < 24:
return ai, data
elif ai == 24:
return data[0], data[1:]
elif ai == 25:
return struct.unpack_from(">H", data)[0], data[2:]
elif ai == 26:
return struct.unpack_from(">I", data)[0], data[4:]
elif ai == 27:
return struct.unpack_from(">Q", data)[0], data[8:]
raise ValueError("Invalid additional information")
def _load_nint(ai: int, data: bytes) -> tuple[int, bytes]:
val, rest = _load_int(ai, data)
return -1 - val, rest
def _load_bool(ai: int, data: bytes) -> tuple[bool, bytes]:
return ai == 21, data
def _load_bytes(ai: int, data: bytes) -> tuple[bytes, bytes]:
ln, data = _load_int(ai, data)
return data[:ln], data[ln:]
def _load_text(ai: int, data: bytes) -> tuple[str, bytes]:
enc, rest = _load_bytes(ai, data)
return enc.decode("utf8"), rest
def _load_array(ai: int, data: bytes) -> tuple[Sequence[CborType], bytes]:
ln, data = _load_int(ai, data)
values = []
for i in range(ln):
val, data = decode_from(data)
values.append(val)
return values, data
def _load_map(ai: int, data: bytes) -> tuple[Mapping[CborType, CborType], bytes]:
ln, data = _load_int(ai, data)
values = {}
for i in range(ln):
k, data = decode_from(data)
v, data = decode_from(data)
values[k] = v
return values, data
_DESERIALIZERS = {
0: _load_int,
1: _load_nint,
2: _load_bytes,
3: _load_text,
4: _load_array,
5: _load_map,
7: _load_bool,
}
def encode(data: CborType) -> bytes:
"""Encodes data to a CBOR byte string."""
for k, v in _SERIALIZERS:
if isinstance(data, k):
return v(data)
raise ValueError(f"Unsupported value: {data!r}")
def decode_from(data: bytes) -> tuple[Any, bytes]:
"""Decodes a CBOR-encoded value from the start of a byte string.
Additional data after a valid CBOR object is returned as well.
:return: The decoded object, and any remaining data."""
fb = data[0]
return _DESERIALIZERS[fb >> 5](fb & 0b11111, data[1:])
def decode(data) -> CborType:
"""Decodes data from a CBOR-encoded byte string.
Also validates that no extra data follows the encoded object.
"""
value, rest = decode_from(data)
if rest != b"":
raise ValueError("Extraneous data")
return value
fido2-2.2.1/fido2/client/__init__.py 0000644 0000000 0000000 00000116724 15220526030 014045 0 ustar 00 # 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.
from __future__ import annotations
import abc
import logging
from dataclasses import replace
from enum import IntEnum, unique
from threading import Event, Timer
from typing import Any, Callable, Mapping, Sequence, overload
from urllib.parse import urlparse
from ..cose import ES256
from ..ctap import CtapDevice, CtapError
from ..ctap1 import APDU, ApduError, Ctap1
from ..ctap2 import AssertionResponse, Ctap2, Info
from ..ctap2.extensions import (
_DEFAULT_EXTENSIONS,
AuthenticationExtensionProcessor,
Ctap2Extension,
RegistrationExtensionProcessor,
)
from ..ctap2.pin import ClientPin, PinProtocol
from ..hid import STATUS
from ..rpid import verify_rp_id
from ..utils import sha256
from ..webauthn import (
Aaguid,
AttestationConveyancePreference,
AttestationObject,
AuthenticationExtensionsClientOutputs,
AuthenticationResponse,
AuthenticatorAssertionResponse,
AuthenticatorAttachment,
AuthenticatorAttestationResponse,
AuthenticatorSelectionCriteria,
CollectedClientData,
PublicKeyCredentialCreationOptions,
PublicKeyCredentialDescriptor,
PublicKeyCredentialRequestOptions,
PublicKeyCredentialType,
RegistrationResponse,
ResidentKeyRequirement,
UserVerificationRequirement,
_as_cbor,
)
logger = logging.getLogger(__name__)
class ClientError(Exception):
"""Base error raised by clients."""
@unique
class ERR(IntEnum):
"""Error codes for ClientError."""
OTHER_ERROR = 1
BAD_REQUEST = 2
CONFIGURATION_UNSUPPORTED = 3
DEVICE_INELIGIBLE = 4
TIMEOUT = 5
def __call__(self, cause=None):
return ClientError(self, cause)
def __init__(self, code, cause=None):
self.code = ClientError.ERR(code)
self.cause = cause
def __repr__(self):
r = "Client error: {0} - {0.name}".format(self.code)
if self.cause:
r += f" (cause: {self.cause})"
return r
def _ctap2client_err(e, err_cls=ClientError):
if e.code in [CtapError.ERR.CREDENTIAL_EXCLUDED, CtapError.ERR.NO_CREDENTIALS]:
ce = ClientError.ERR.DEVICE_INELIGIBLE
elif e.code in [
CtapError.ERR.KEEPALIVE_CANCEL,
CtapError.ERR.ACTION_TIMEOUT,
CtapError.ERR.USER_ACTION_TIMEOUT,
]:
ce = ClientError.ERR.TIMEOUT
elif e.code in [
CtapError.ERR.UNSUPPORTED_ALGORITHM,
CtapError.ERR.UNSUPPORTED_OPTION,
CtapError.ERR.KEY_STORE_FULL,
]:
ce = ClientError.ERR.CONFIGURATION_UNSUPPORTED
elif e.code in [
CtapError.ERR.INVALID_COMMAND,
CtapError.ERR.CBOR_UNEXPECTED_TYPE,
CtapError.ERR.INVALID_CBOR,
CtapError.ERR.MISSING_PARAMETER,
CtapError.ERR.INVALID_OPTION,
CtapError.ERR.PUAT_REQUIRED,
CtapError.ERR.PIN_INVALID,
CtapError.ERR.PIN_BLOCKED,
CtapError.ERR.PIN_NOT_SET,
CtapError.ERR.PIN_POLICY_VIOLATION,
CtapError.ERR.PIN_TOKEN_EXPIRED,
CtapError.ERR.PIN_AUTH_INVALID,
CtapError.ERR.PIN_AUTH_BLOCKED,
CtapError.ERR.REQUEST_TOO_LARGE,
CtapError.ERR.OPERATION_DENIED,
]:
ce = ClientError.ERR.BAD_REQUEST
else:
ce = ClientError.ERR.OTHER_ERROR
return err_cls(ce, e)
class PinRequiredError(ClientError):
"""Raised when a call cannot be completed without providing PIN."""
def __init__(
self, code=ClientError.ERR.BAD_REQUEST, cause="PIN required but not provided"
):
super().__init__(code, cause)
def _call_polling(poll_delay, event, on_keepalive, func, *args, **kwargs):
event = event or Event()
while not event.is_set():
try:
return func(*args, **kwargs)
except ApduError as e:
if e.code == APDU.USE_NOT_SATISFIED:
if on_keepalive:
on_keepalive(STATUS.UPNEEDED)
on_keepalive = None
event.wait(poll_delay)
else:
raise ClientError.ERR.OTHER_ERROR(e)
except CtapError as e:
raise _ctap2client_err(e)
raise ClientError.ERR.TIMEOUT()
class AssertionSelection:
"""GetAssertion result holding one or more assertions.
Since multiple assertions may be retured by Fido2Client.get_assertion, this result
is returned which can be used to select a specific response to get.
"""
def __init__(
self,
client_data: CollectedClientData,
assertions: Sequence[AssertionResponse],
extension_results: Mapping[str, Any] = {},
):
self._client_data = client_data
self._assertions = assertions
self._extension_results = extension_results
def get_assertions(self) -> Sequence[AssertionResponse]:
"""Get the raw AssertionResponses available to inspect before selecting one."""
return self._assertions
def _get_extension_results(self, assertion: AssertionResponse) -> Mapping[str, Any]:
return self._extension_results
def get_response(self, index: int) -> AuthenticationResponse:
"""Get a single response."""
assertion = self._assertions[index]
return AuthenticationResponse(
raw_id=assertion.credential["id"],
response=AuthenticatorAssertionResponse(
client_data=self._client_data,
authenticator_data=assertion.auth_data,
signature=assertion.signature,
user_handle=assertion.user["id"] if assertion.user else None,
),
authenticator_attachment=AuthenticatorAttachment.CROSS_PLATFORM,
client_extension_results=AuthenticationExtensionsClientOutputs(
self._get_extension_results(assertion)
),
type=PublicKeyCredentialType.PUBLIC_KEY,
)
class WebAuthnClient(abc.ABC):
"""Base class for a WebAuthn client, supporting registration and authentication."""
@abc.abstractmethod
def make_credential(
self,
options: PublicKeyCredentialCreationOptions,
event: Event | None = None,
) -> RegistrationResponse:
"""Creates a credential.
:param options: PublicKeyCredentialCreationOptions data.
:param threading.Event event: (optional) Signal to abort the operation.
"""
raise NotImplementedError()
@abc.abstractmethod
def get_assertion(
self,
options: PublicKeyCredentialRequestOptions,
event: Event | None = None,
) -> AssertionSelection:
"""Get an assertion.
:param options: PublicKeyCredentialRequestOptions data.
:param threading.Event event: (optional) Signal to abort the operation.
"""
raise NotImplementedError()
class UserInteraction:
"""Provides user interaction to the Client.
Users of Fido2Client should subclass this to implement asking the user to perform
specific actions, such as entering a PIN or touching their"""
def prompt_up(self) -> None:
"""Called when the authenticator is awaiting a user presence check."""
logger.info("User Presence check required.")
def request_pin(
self, permissions: ClientPin.PERMISSION, rp_id: str | None
) -> str | None:
"""Called when the client requires a PIN from the user.
Should return a PIN, or None/Empty to cancel."""
logger.info("PIN requested, but UserInteraction does not support it.")
return None
def request_uv(self, permissions: ClientPin.PERMISSION, rp_id: str | None) -> bool:
"""Called when the client is about to request UV from the user.
Should return True if allowed, or False to cancel."""
logger.info("User Verification requested.")
return True
class ClientDataCollector(abc.ABC):
"""Provides client data and logic to the Client.
Users should subclass this to implement custom behavior for determining the origin,
validating the RP ID, and providing client data for a request.
"""
@abc.abstractmethod
def collect_client_data(
self,
options: PublicKeyCredentialCreationOptions | PublicKeyCredentialRequestOptions,
) -> tuple[CollectedClientData, str]:
"""Called when the client is preparing a request.
Should return a CollectedClientData object with the client data for the request,
as well as the RP ID of the credential.
"""
class DefaultClientDataCollector(ClientDataCollector):
"""Default implementation of ClientDataProvider.
This implementation uses a fixed origin, it can be subclassed to customize specific
behavior.
"""
def __init__(self, origin: str, verify: Callable[[str, str], bool] = verify_rp_id):
self._origin = origin
self._verify = verify
def get_rp_id(
self,
options: PublicKeyCredentialCreationOptions | PublicKeyCredentialRequestOptions,
origin: str,
) -> str:
"""Get the RP ID for the given options and origin."""
if isinstance(options, PublicKeyCredentialCreationOptions):
rp_id = options.rp.id
elif isinstance(options, PublicKeyCredentialRequestOptions):
rp_id = options.rp_id
else:
raise ValueError("Invalid options type.")
if rp_id is None:
url = urlparse(origin)
if url.scheme != "https" or not url.netloc:
raise ClientError.ERR.BAD_REQUEST(
"RP ID required for non-https origin."
)
return url.netloc
else:
return rp_id
def verify_rp_id(self, rp_id: str, origin: str) -> None:
"""Verify the RP ID for the given origin."""
try:
if self._verify(rp_id, origin):
return
except Exception: # noqa: S110
pass # Fall through to ClientError
raise ClientError.ERR.BAD_REQUEST()
def get_request_type(self, options) -> str:
"""Get the request type for the given options."""
if isinstance(options, PublicKeyCredentialCreationOptions):
return CollectedClientData.TYPE.CREATE
elif isinstance(options, PublicKeyCredentialRequestOptions):
return CollectedClientData.TYPE.GET
else:
raise ValueError("Invalid options type.")
def collect_client_data(self, options):
# Get the effective RP ID from the request options, falling back to the origin
rp_id = self.get_rp_id(options, self._origin)
# Validate that the RP ID is valid for the given origin
self.verify_rp_id(rp_id, self._origin)
# Construct the client data
return (
CollectedClientData.create(
type=self.get_request_type(options),
origin=self._origin,
challenge=options.challenge,
),
rp_id,
)
def _user_keepalive(user_interaction):
def on_keepalive(status):
if status == STATUS.UPNEEDED: # Waiting for touch
user_interaction.prompt_up()
return on_keepalive
class _ClientBackend(abc.ABC):
info: Info
@abc.abstractmethod
def selection(self, event: Event | None) -> None:
raise NotImplementedError()
@abc.abstractmethod
def do_make_credential(
self,
options: PublicKeyCredentialCreationOptions,
client_data: CollectedClientData,
rp_id: str,
enterprise_rpid_list: Sequence[str] | None,
event: Event,
) -> RegistrationResponse:
raise NotImplementedError()
@abc.abstractmethod
def do_get_assertion(
self,
options: PublicKeyCredentialRequestOptions,
client_data: CollectedClientData,
rp_id: str,
event: Event,
) -> AssertionSelection:
raise NotImplementedError()
class _Ctap1ClientBackend(_ClientBackend):
def __init__(self, device: CtapDevice, user_interaction: UserInteraction):
self.ctap1 = Ctap1(device)
self.info = Info(versions=["U2F_V2"], extensions=[], aaguid=Aaguid.NONE)
self._poll_delay = 0.25
self._on_keepalive = _user_keepalive(user_interaction)
def selection(self, event):
_call_polling(
self._poll_delay,
event,
None,
self.ctap1.register,
b"\0" * 32,
b"\0" * 32,
)
def do_make_credential(
self,
options,
client_data,
rp_id,
enterprise_rpid_list,
event,
):
key_params = options.pub_key_cred_params
exclude_list = options.exclude_credentials
selection = options.authenticator_selection or AuthenticatorSelectionCriteria()
rk = selection.require_resident_key
user_verification = selection.user_verification
if (
rk
or user_verification == UserVerificationRequirement.REQUIRED
or ES256.ALGORITHM not in [p.alg for p in key_params]
or options.attestation == AttestationConveyancePreference.ENTERPRISE
):
raise CtapError(CtapError.ERR.UNSUPPORTED_OPTION)
app_param = sha256(rp_id.encode())
dummy_param = b"\0" * 32
for cred in exclude_list or []:
key_handle = cred.id
try:
self.ctap1.authenticate(dummy_param, app_param, key_handle, True)
raise ClientError.ERR.OTHER_ERROR() # Shouldn't happen
except ApduError as e:
if e.code == APDU.USE_NOT_SATISFIED:
_call_polling(
self._poll_delay,
event,
self._on_keepalive,
self.ctap1.register,
dummy_param,
dummy_param,
)
raise ClientError.ERR.DEVICE_INELIGIBLE()
att_obj = AttestationObject.from_ctap1(
app_param,
_call_polling(
self._poll_delay,
event,
self._on_keepalive,
self.ctap1.register,
client_data.hash,
app_param,
),
)
credential = att_obj.auth_data.credential_data
assert credential is not None # noqa: S101
return RegistrationResponse(
raw_id=credential.credential_id,
response=AuthenticatorAttestationResponse(
client_data=client_data, attestation_object=att_obj
),
authenticator_attachment=AuthenticatorAttachment.CROSS_PLATFORM,
client_extension_results=AuthenticationExtensionsClientOutputs({}),
type=PublicKeyCredentialType.PUBLIC_KEY,
)
def do_get_assertion(
self,
options,
client_data,
rp_id,
event,
):
allow_list = options.allow_credentials
user_verification = options.user_verification
if user_verification == UserVerificationRequirement.REQUIRED or not allow_list:
raise CtapError(CtapError.ERR.UNSUPPORTED_OPTION)
app_param = sha256(rp_id.encode())
client_param = client_data.hash
for cred in allow_list:
try:
auth_resp = _call_polling(
self._poll_delay,
event,
self._on_keepalive,
self.ctap1.authenticate,
client_param,
app_param,
cred.id,
)
assertions = [
AssertionResponse.from_ctap1(app_param, _as_cbor(cred), auth_resp)
]
return AssertionSelection(client_data, assertions)
except ClientError as e:
if e.code == ClientError.ERR.TIMEOUT:
raise # Other errors are ignored so we move to the next.
raise ClientError.ERR.DEVICE_INELIGIBLE()
class _Ctap2ClientAssertionSelection(AssertionSelection):
def __init__(
self,
client_data: CollectedClientData,
assertions: Sequence[AssertionResponse],
extensions: Sequence[AuthenticationExtensionProcessor],
pin_token: bytes | None,
):
super().__init__(client_data, assertions)
self._extensions = extensions
self._pin_token = pin_token
def _get_extension_results(self, assertion):
# Process extension outputs
extension_outputs = {}
try:
for ext in self._extensions:
output = ext.prepare_outputs(assertion, self._pin_token)
if output:
extension_outputs.update(output)
except ValueError as e:
raise ClientError.ERR.CONFIGURATION_UNSUPPORTED(e)
return extension_outputs
@overload
def _cbor_list(values: Sequence) -> list: ...
@overload
def _cbor_list(values: None) -> None: ...
def _cbor_list(values):
if not values:
return None
return [_as_cbor(v) for v in values]
class _Ctap2ClientBackend(_ClientBackend):
def __init__(
self,
device: CtapDevice,
user_interaction: UserInteraction,
extensions: Sequence[Ctap2Extension],
):
self.ctap2 = Ctap2(device)
self.info = self.ctap2.info
self._extensions = extensions
self.user_interaction = user_interaction
def _filter_creds(
self, rp_id, cred_list, pin_protocol, pin_token, event, on_keepalive
):
# Use fresh info
info = self.ctap2.get_info()
# Filter out credential IDs which are too long
max_len = info.max_cred_id_length
if max_len:
cred_list = [c for c in cred_list if len(c.id) <= max_len]
client_data_hash = b"\0" * 32
if pin_token:
pin_auth = pin_protocol.authenticate(pin_token, client_data_hash)
version = pin_protocol.VERSION
else:
pin_auth = None
version = None
max_creds = info.max_creds_in_list or 1
while cred_list:
chunk = cred_list[:max_creds]
try:
assertions = self.ctap2.get_assertions(
rp_id,
client_data_hash,
_cbor_list(chunk),
None,
{"up": False},
pin_auth,
version,
event=event,
on_keepalive=on_keepalive,
)
if len(chunk) == 1:
# Credential ID might be omitted from assertions
return chunk[0]
else:
return PublicKeyCredentialDescriptor(**assertions[0].credential)
except CtapError as e:
match e.code:
case CtapError.ERR.REQUEST_TOO_LARGE if max_creds > 1:
# Message is too large, try smaller chunks
max_creds -= 1
case CtapError.ERR.NO_CREDENTIALS:
# All creds in chunk are discarded
cred_list = cred_list[max_creds:]
case _:
raise
# No matches found
return None
def selection(self, event):
if "FIDO_2_1" in self.ctap2.info.versions:
self.ctap2.selection(event=event)
else:
# Selection not supported, make dummy credential instead
try:
self.ctap2.make_credential(
b"\0" * 32,
{"id": "example.com", "name": "example.com"},
{"id": b"dummy", "name": "dummy"},
[{"type": "public-key", "alg": -7}],
pin_uv_param=b"",
event=event,
)
except CtapError as e:
if e.code in (
CtapError.ERR.PIN_NOT_SET,
CtapError.ERR.PIN_INVALID,
CtapError.ERR.PIN_AUTH_INVALID,
):
return
raise
def _should_use_uv(self, info, user_verification, permissions):
uv_supported = any(k in info.options for k in ("uv", "clientPin", "bioEnroll"))
uv_configured = any(
info.options.get(k) for k in ("uv", "clientPin", "bioEnroll")
)
mc = ClientPin.PERMISSION.MAKE_CREDENTIAL & permissions != 0
additional_perms = permissions & ~(
ClientPin.PERMISSION.MAKE_CREDENTIAL | ClientPin.PERMISSION.GET_ASSERTION
)
if (
user_verification == UserVerificationRequirement.REQUIRED
or (
user_verification in (UserVerificationRequirement.PREFERRED, None)
and uv_supported
)
or info.options.get("alwaysUv")
):
if not uv_configured:
raise ClientError.ERR.CONFIGURATION_UNSUPPORTED(
"User verification not configured/supported"
)
return True
elif mc and uv_configured and not info.options.get("makeCredUvNotRqd"):
return True
elif uv_configured and additional_perms:
return True
return False
def _get_token(
self,
info,
client_pin,
permissions,
rp_id,
event,
on_keepalive,
allow_internal_uv,
allow_uv,
):
# Prefer UV
if allow_uv and info.options.get("uv"):
if ClientPin.is_token_supported(info):
if self.user_interaction.request_uv(permissions, rp_id):
return client_pin.get_uv_token(
permissions, rp_id, event, on_keepalive
)
elif allow_internal_uv:
if self.user_interaction.request_uv(permissions, rp_id):
return None # No token, use uv=True
# PIN if UV not supported/allowed.
if info.options.get("clientPin"):
pin = self.user_interaction.request_pin(permissions, rp_id)
if pin:
return client_pin.get_pin_token(pin, permissions, rp_id)
raise PinRequiredError()
# Client PIN not configured.
raise ClientError.ERR.CONFIGURATION_UNSUPPORTED(
"User verification not configured"
)
def _get_auth_params(
self,
pin_protocol,
rp_id,
user_verification,
permissions,
allow_uv,
event,
on_keepalive,
):
info = self.ctap2.get_info()
pin_token = None
internal_uv = False
if self._should_use_uv(info, user_verification, permissions):
client_pin = ClientPin(self.ctap2, pin_protocol)
allow_internal_uv = (
permissions
& ~(
ClientPin.PERMISSION.MAKE_CREDENTIAL
| ClientPin.PERMISSION.GET_ASSERTION
)
== 0
)
pin_token = self._get_token(
info,
client_pin,
permissions,
rp_id,
event,
on_keepalive,
allow_internal_uv,
allow_uv,
)
if not pin_token:
internal_uv = True
return pin_token, internal_uv
def do_make_credential(
self,
options,
client_data,
rp_id,
enterprise_rpid_list,
event,
):
user = options.user
key_params = options.pub_key_cred_params
exclude_list = options.exclude_credentials
selection = options.authenticator_selection or AuthenticatorSelectionCriteria()
user_verification = selection.user_verification
on_keepalive = _user_keepalive(self.user_interaction)
info = self.ctap2.get_info()
# Handle enterprise attestation
enterprise_attestation = None
if options.attestation == AttestationConveyancePreference.ENTERPRISE:
if info.options.get("ep"):
if enterprise_rpid_list is not None:
# Platform facilitated
if rp_id in enterprise_rpid_list:
enterprise_attestation = 2
else:
# Vendor facilitated
enterprise_attestation = 1
# Negotiate PIN/UV protocol version
for proto in ClientPin.PROTOCOLS:
if proto.VERSION in info.pin_uv_protocols:
pin_protocol: PinProtocol | None = proto()
break
else:
pin_protocol = None
used_extensions: list[RegistrationExtensionProcessor] = []
allow_uv = True
def _do_make():
# Gather UV permissions
permissions = ClientPin.PERMISSION.MAKE_CREDENTIAL
if exclude_list:
# We need this for filtering the exclude_list
permissions |= ClientPin.PERMISSION.GET_ASSERTION
# Initialize extensions and add extension permissions
used_extensions.clear()
for e in self._extensions:
ext = e.make_credential(self.ctap2, options, pin_protocol)
if ext:
used_extensions.append(ext)
permissions |= ext.permissions
# Handle auth
pin_token, internal_uv = self._get_auth_params(
pin_protocol,
rp_id,
user_verification,
permissions,
allow_uv,
event,
on_keepalive,
)
if exclude_list:
exclude_cred = self._filter_creds(
rp_id, exclude_list, pin_protocol, pin_token, event, on_keepalive
)
# We know the request will fail if exclude_cred is not None here
# BUT DO NOT FAIL EARLY! We still need to prompt for UP, so we keep
# processing the request
else:
exclude_cred = None
# Process extensions
extension_inputs = {}
try:
for ext in used_extensions:
auth_input = ext.prepare_inputs(pin_token)
if auth_input:
extension_inputs.update(auth_input)
except ValueError as e:
raise ClientError.ERR.CONFIGURATION_UNSUPPORTED(e)
can_rk = info.options.get("rk")
rk = selection.resident_key == ResidentKeyRequirement.REQUIRED or (
selection.resident_key == ResidentKeyRequirement.PREFERRED and can_rk
)
if not (rk or internal_uv):
opts = None
else:
opts = {}
if rk:
if not can_rk:
raise ClientError.ERR.CONFIGURATION_UNSUPPORTED(
"Resident key not supported"
)
opts["rk"] = True
if internal_uv:
opts["uv"] = True
# Calculate pin_auth
client_data_hash = client_data.hash
if pin_protocol and pin_token:
pin_auth: tuple[bytes | None, int | None] = (
pin_protocol.authenticate(pin_token, client_data_hash),
pin_protocol.VERSION,
)
else:
pin_auth = (None, None)
# Perform make credential
return (
self.ctap2.make_credential(
client_data_hash,
_as_cbor(replace(options.rp, id=rp_id)),
_as_cbor(user),
_cbor_list(key_params),
[_as_cbor(exclude_cred)] if exclude_cred else None,
extension_inputs or None,
opts,
*pin_auth,
enterprise_attestation,
event=event,
on_keepalive=on_keepalive,
),
pin_token,
)
dev = self.ctap2.device
reconnected = False
while True:
try:
att_resp, pin_token = _do_make()
break
except CtapError as e:
# The Authenticator may still require UV, try again
if (
e.code == CtapError.ERR.PUAT_REQUIRED
and user_verification == UserVerificationRequirement.DISCOURAGED
):
user_verification = UserVerificationRequirement.REQUIRED
continue
# UV may be blocked, try again (once) with PIN
if e.code == CtapError.ERR.UV_BLOCKED and allow_uv:
allow_uv = False
continue
# NFC may require reconnect
connect = getattr(dev, "connect", None)
if (
e.code == CtapError.ERR.PIN_AUTH_BLOCKED
and connect
and not reconnected
):
dev.close()
connect()
reconnected = True # We only want to try this once
continue
raise
# Process extension outputs
extension_outputs = {}
try:
for ext in used_extensions:
output = ext.prepare_outputs(att_resp, pin_token)
if output is not None:
extension_outputs.update(output)
except ValueError as e:
raise ClientError.ERR.CONFIGURATION_UNSUPPORTED(e)
att_obj = AttestationObject.create(
att_resp.fmt, att_resp.auth_data, att_resp.att_stmt
)
credential = att_obj.auth_data.credential_data
assert credential is not None # noqa: S101
return RegistrationResponse(
raw_id=credential.credential_id,
response=AuthenticatorAttestationResponse(
client_data=client_data, attestation_object=att_obj
),
authenticator_attachment=AuthenticatorAttachment.CROSS_PLATFORM,
client_extension_results=AuthenticationExtensionsClientOutputs(
extension_outputs
),
type=PublicKeyCredentialType.PUBLIC_KEY,
)
def do_get_assertion(
self,
options,
client_data,
rp_id,
event,
):
rp_id = options.rp_id
allow_list = options.allow_credentials
user_verification = options.user_verification
on_keepalive = _user_keepalive(self.user_interaction)
# Negotiate PIN/UV protocol version
for proto in ClientPin.PROTOCOLS:
if proto.VERSION in self.info.pin_uv_protocols:
pin_protocol: PinProtocol | None = proto()
break
else:
pin_protocol = None
allow_uv = True
def _do_auth():
# Gather UV permissions
permissions = ClientPin.PERMISSION.GET_ASSERTION
# Initialize extensions and add extension permissions
used_extensions = []
for e in self._extensions:
ext = e.get_assertion(self.ctap2, options, pin_protocol)
if ext:
used_extensions.append(ext)
permissions |= ext.permissions
# Handle auth
pin_token, internal_uv = self._get_auth_params(
pin_protocol,
rp_id,
user_verification,
permissions,
allow_uv,
event,
on_keepalive,
)
if allow_list:
selected_cred = self._filter_creds(
rp_id, allow_list, pin_protocol, pin_token, event, on_keepalive
)
# We know the request will fail if selected_cred is None here
# BUT DO NOT FAIL EARLY! We still need to prompt for UP, so we keep
# processing the request
else:
selected_cred = None
# Process extensions
extension_inputs = {}
try:
for ext in used_extensions:
inputs = ext.prepare_inputs(selected_cred, pin_token)
if inputs:
extension_inputs.update(inputs)
except ValueError as e:
raise ClientError.ERR.CONFIGURATION_UNSUPPORTED(e)
opts = {"uv": True} if internal_uv else None
# Calculate pin_auth
client_data_hash = client_data.hash
if pin_protocol and pin_token:
pin_auth: tuple[bytes | None, int | None] = (
pin_protocol.authenticate(pin_token, client_data_hash),
pin_protocol.VERSION,
)
else:
pin_auth = (None, None)
if allow_list and not selected_cred:
# We still need to send a dummy value if there was an allow_list
# but no matches were found:
selected_cred = PublicKeyCredentialDescriptor(
type=allow_list[0].type, id=b"\0"
)
# Perform get assertion
assertions = self.ctap2.get_assertions(
rp_id,
client_data_hash,
[_as_cbor(selected_cred)] if selected_cred else None,
extension_inputs or None,
opts,
*pin_auth,
event=event,
on_keepalive=on_keepalive,
)
return _Ctap2ClientAssertionSelection(
client_data, assertions, used_extensions, pin_token
)
dev = self.ctap2.device
reconnected = False
while True:
try:
return _do_auth()
except CtapError as e:
# The Authenticator may still require UV, try again
if (
e.code == CtapError.ERR.PUAT_REQUIRED
and user_verification == UserVerificationRequirement.DISCOURAGED
):
user_verification = UserVerificationRequirement.REQUIRED
continue
# UV may be blocked, try again (once) with PIN
if e.code == CtapError.ERR.UV_BLOCKED and allow_uv:
allow_uv = False
continue
# NFC may require reconnect
connect = getattr(dev, "connect", None)
if (
e.code == CtapError.ERR.PIN_AUTH_BLOCKED
and connect
and not reconnected
):
dev.close()
connect()
reconnected = True # We only want to try this once
continue
raise
class Fido2Client(WebAuthnClient):
"""WebAuthn-like client implementation.
The client allows registration and authentication of WebAuthn credentials against
an Authenticator using CTAP (1 or 2).
:param device: CtapDevice to use.
:param str origin: The origin to use.
:param verify: Function to verify an RP ID for a given origin.
"""
def __init__(
self,
device: CtapDevice,
client_data_collector: ClientDataCollector,
user_interaction: UserInteraction = UserInteraction(),
extensions: Sequence[Ctap2Extension] = _DEFAULT_EXTENSIONS,
):
self._client_data_collector = client_data_collector
# TODO: Decide how to configure this list.
self._enterprise_rpid_list: Sequence[str] | None = None
try:
self._backend: _ClientBackend = _Ctap2ClientBackend(
device, user_interaction, extensions
)
except (ValueError, CtapError):
self._backend = _Ctap1ClientBackend(device, user_interaction)
@property
def info(self) -> Info:
return self._backend.info
def selection(self, event: Event | None = None) -> None:
try:
self._backend.selection(event)
except CtapError as e:
raise _ctap2client_err(e)
def make_credential(
self,
options: PublicKeyCredentialCreationOptions,
event: Event | None = None,
) -> RegistrationResponse:
"""Creates a credential.
:param options: PublicKeyCredentialCreationOptions data.
:param threading.Event event: (optional) Signal to abort the operation.
"""
options = PublicKeyCredentialCreationOptions.from_dict(options)
event = event or Event()
if options.timeout:
timer = Timer(options.timeout / 1000, event.set)
timer.daemon = True
timer.start()
else:
timer = None
# Gather client data, RP ID from client
client_data, rp_id = self._client_data_collector.collect_client_data(options)
logger.debug(f"Register a new credential for RP ID: {rp_id}")
try:
return self._backend.do_make_credential(
options,
client_data,
rp_id,
self._enterprise_rpid_list,
event,
)
except CtapError as e:
raise _ctap2client_err(e)
finally:
if timer:
timer.cancel()
def get_assertion(
self,
options: PublicKeyCredentialRequestOptions,
event: Event | None = None,
) -> AssertionSelection:
"""Get an assertion.
:param options: PublicKeyCredentialRequestOptions data.
:param threading.Event event: (optional) Signal to abort the operation.
"""
options = PublicKeyCredentialRequestOptions.from_dict(options)
event = event or Event()
if options.timeout:
timer = Timer(options.timeout / 1000, event.set)
timer.daemon = True
timer.start()
else:
timer = None
# Gather client data, RP ID from client
client_data, rp_id = self._client_data_collector.collect_client_data(options)
logger.debug(f"Assert a credential for RP ID: {rp_id}")
try:
return self._backend.do_get_assertion(
options,
client_data,
rp_id,
event,
)
except CtapError as e:
raise _ctap2client_err(e)
finally:
if timer:
timer.cancel()
fido2-2.2.1/fido2/client/win_api.py 0000644 0000000 0000000 00000076175 15220526030 013741 0 ustar 00 # Copyright (c) 2019 Onica Group LLC.
# Modified work Copyright 2019 Yubico.
# 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.
"""
Structs based on Microsoft's WebAuthN API.
https://github.com/microsoft/webauthn
Definitions taken from https://github.com/microsoft/webauthn/blob/master/webauthn.h
"""
# With the ctypes.Structure a lot of the property names
# will be invalid, and when creating the __init__ methods
# we do not need to call super() for the Structure class
#
# pylint: disable=invalid-name, super-init-not-called, too-few-public-methods
from __future__ import annotations
import ctypes
from ctypes.wintypes import BOOL, DWORD, HWND, LONG, LPCWSTR, WORD
from enum import IntEnum, unique
from typing import Any, Mapping, Sequence, cast
# Not implemented: Platform credentials support, listing of built-in authenticators
LOAD_LIBRARY_SEARCH_SYSTEM32 = 0x00000800
PBYTE = ctypes.POINTER(ctypes.c_ubyte) # Different from wintypes.PBYTE, which is signed
PCWSTR = ctypes.c_wchar_p
PVOID = ctypes.c_void_p
class BytesProperty:
"""Property for structs storing byte arrays as DWORD + PBYTE.
Allows for easy reading/writing to struct fields using Python bytes objects.
"""
def __init__(self, name: str):
self.cbName = "cb" + name
self.pbName = "pb" + name
def __get__(self, instance, owner):
return bytes(
bytearray(getattr(instance, self.pbName)[: getattr(instance, self.cbName)])
)
def __set__(self, instance, value: bytes | None):
ln = len(value) if value else 0
buffer = ctypes.create_string_buffer(value) if value else 0
setattr(instance, self.cbName, ln)
setattr(instance, self.pbName, ctypes.cast(buffer, PBYTE))
class GUID(ctypes.Structure):
"""GUID Type in C++."""
_fields_ = [
("Data1", ctypes.c_ulong),
("Data2", ctypes.c_ushort),
("Data3", ctypes.c_ushort),
("Data4", ctypes.c_ubyte * 8),
]
def __str__(self):
return "{%08X-%04X-%04X-%04X-%012X}" % (
self.Data1,
self.Data2,
self.Data3,
self.Data4[0] * 256 + self.Data4[1],
self.Data4[2] * (256**5)
+ self.Data4[3] * (256**4)
+ self.Data4[4] * (256**3)
+ self.Data4[5] * (256**2)
+ self.Data4[6] * 256
+ self.Data4[7],
)
class _FromString:
@classmethod
def from_string(cls, value: str):
return getattr(cls, value.upper().replace("-", "_"))
@unique
class WebAuthNUserVerificationRequirement(_FromString, IntEnum):
"""Maps to WEBAUTHN_USER_VERIFICATION_REQUIREMENT_*."""
ANY = 0
REQUIRED = 1
PREFERRED = 2
DISCOURAGED = 3
@unique
class WebAuthNAttestationConveyancePreference(_FromString, IntEnum):
"""Maps to WEBAUTHN_ATTESTATION_CONVEYANCE_PREFERENCE_*."""
ANY = 0
NONE = 1
INDIRECT = 2
DIRECT = 3
@unique
class WebAuthNAuthenticatorAttachment(_FromString, IntEnum):
"""Maps to WEBAUTHN_AUTHENTICATOR_ATTACHMENT_*."""
ANY = 0
PLATFORM = 1
CROSS_PLATFORM = 2
CROSS_PLATFORM_U2F_V2 = 3
@unique
class WebAuthNCTAPTransport(_FromString, IntEnum):
"""Maps to WEBAUTHN_CTAP_TRANSPORT_*."""
ANY = 0x00000000
USB = 0x00000001
NFC = 0x00000002
BLE = 0x00000004
TEST = 0x00000008
INTERNAL = 0x00000010
HYBRID = 0x00000020
SMART_CARD = 0x00000040
FLAGS_MASK = 0x0000007F
@unique
class WebAuthNEnterpriseAttestation(_FromString, IntEnum):
"""Maps to WEBAUTHN_ENTERPRISE_ATTESTATION_*."""
NONE = 0
VENDOR_FACILITATED = 1
PLATFORM_MANAGED = 2
@unique
class WebAuthNLargeBlobSupport(_FromString, IntEnum):
"""Maps to WEBAUTHN_LARGE_BLOB_SUPPORT_*."""
NONE = 0
REQUIRED = 1
PREFERRED = 2
@unique
class WebAuthNLargeBlobOperation(_FromString, IntEnum):
"""Maps to WEBAUTHN_LARGE_BLOB_OPERATION_*."""
NONE = 0
GET = 1
SET = 2
DELETE = 3
@unique
class WebAuthNUserVerification(_FromString, IntEnum):
"""Maps to WEBAUTHN_USER_VERIFICATION_*."""
ANY = 0
OPTIONAL = 1
OPTIONAL_WITH_CREDENTIAL_ID_LIST = 2
REQUIRED = 3
class WebAuthNCoseCredentialParameter(ctypes.Structure):
"""Maps to WEBAUTHN_COSE_CREDENTIAL_PARAMETER Struct.
:param cred_params: Dict of Credential parameters.
"""
_fields_ = [
("dwVersion", DWORD),
("pwszCredentialType", LPCWSTR),
("lAlg", LONG),
]
def __init__(self, cred_params: Mapping[str, Any]):
self.dwVersion = get_version(self.__class__.__name__)
self.pwszCredentialType = cred_params["type"]
self.lAlg = cred_params["alg"]
class WebAuthNCoseCredentialParameters(ctypes.Structure):
"""Maps to WEBAUTHN_COSE_CREDENTIAL_PARAMETERS Struct.
:param params: List of Credential parameter dicts.
"""
_fields_ = [
("cCredentialParameters", DWORD),
("pCredentialParameters", ctypes.POINTER(WebAuthNCoseCredentialParameter)),
]
def __init__(self, params: Sequence[Mapping[str, Any]]):
self.cCredentialParameters = len(params)
self.pCredentialParameters = (WebAuthNCoseCredentialParameter * len(params))(
*(WebAuthNCoseCredentialParameter(param) for param in params)
)
class WebAuthNClientData(ctypes.Structure):
"""Maps to WEBAUTHN_CLIENT_DATA Struct.
:param client_data_json: ClientData serialized as JSON bytes.
"""
_fields_ = [
("dwVersion", DWORD),
("cbClientDataJSON", DWORD),
("pbClientDataJSON", PBYTE),
("pwszHashAlgId", LPCWSTR),
]
client_data_json = BytesProperty("ClientDataJSON")
def __init__(self, client_data_json: bytes):
self.dwVersion = get_version(self.__class__.__name__)
self.client_data_json = client_data_json
self.pwszHashAlgId = "SHA-256"
class WebAuthNRpEntityInformation(ctypes.Structure):
"""Maps to WEBAUTHN_RP_ENTITY_INFORMATION Struct.
:param rp: Dict of RP information.
"""
_fields_ = [
("dwVersion", DWORD),
("pwszId", PCWSTR),
("pwszName", PCWSTR),
("pwszIcon", PCWSTR),
]
def __init__(self, rp: Mapping[str, Any]):
self.dwVersion = get_version(self.__class__.__name__)
self.pwszId = rp["id"]
self.pwszName = rp["name"]
self.pwszIcon = rp.get("icon")
class WebAuthNUserEntityInformation(ctypes.Structure):
"""Maps to WEBAUTHN_USER_ENTITY_INFORMATION Struct.
:param user: Dict of User information.
"""
_fields_ = [
("dwVersion", DWORD),
("cbId", DWORD),
("pbId", PBYTE),
("pwszName", PCWSTR),
("pwszIcon", PCWSTR),
("pwszDisplayName", PCWSTR),
]
id = BytesProperty("Id")
def __init__(self, user: Mapping[str, Any]):
self.dwVersion = get_version(self.__class__.__name__)
self.id = user["id"]
self.pwszName = user["name"]
self.pwszIcon = user.get("icon")
self.pwszDisplayName = user.get("displayName")
class WebAuthNCredentialEx(ctypes.Structure):
"""Maps to WEBAUTHN_CREDENTIAL_EX Struct.
:param cred: Dict of Credential Descriptor data.
"""
_fields_ = [
("dwVersion", DWORD),
("cbId", DWORD),
("pbId", PBYTE),
("pwszCredentialType", LPCWSTR),
("dwTransports", DWORD),
]
id = BytesProperty("Id")
def __init__(self, cred: Mapping[str, Any]):
self.dwVersion = get_version(self.__class__.__name__)
self.id = cred["id"]
self.pwszCredentialType = cred["type"]
self.dwTransports = WebAuthNCTAPTransport[cred.get("transport", "ANY")]
class WebAuthNCredentialList(ctypes.Structure):
"""Maps to WEBAUTHN_CREDENTIAL_LIST Struct.
:param credentials: List of dict of Credential Descriptor data.
"""
_fields_ = [
("cCredentials", DWORD),
("ppCredentials", ctypes.POINTER(ctypes.POINTER(WebAuthNCredentialEx))),
]
def __init__(self, credentials: Sequence[Mapping[str, Any]]):
self.cCredentials = len(credentials)
self.ppCredentials = (ctypes.POINTER(WebAuthNCredentialEx) * len(credentials))(
*(ctypes.pointer(WebAuthNCredentialEx(cred)) for cred in credentials)
)
class WebAuthNHmacSecretSalt(ctypes.Structure):
_fields_ = [
("cbFirst", DWORD),
("pbFirst", PBYTE),
("cbSecond", DWORD),
("pbSecond", PBYTE),
]
first = BytesProperty("First")
second = BytesProperty("Second")
def __init__(self, first: bytes, second: bytes | None = None):
self.first = first
self.second = second
class WebAuthNCredWithHmacSecretSalt(ctypes.Structure):
_fields_ = [
("cbCredID", DWORD),
("pbCredID", PBYTE),
("pHmacSecretSalt", ctypes.POINTER(WebAuthNHmacSecretSalt)),
]
cred_id = BytesProperty("CredID")
def __init__(self, cred_id: bytes, salt: WebAuthNHmacSecretSalt):
self.cred_id = cred_id
self.pHmacSecretSalt = ctypes.pointer(salt)
class WebAuthNHmacSecretSaltValues(ctypes.Structure):
_fields_ = [
("pGlobalHmacSalt", ctypes.POINTER(WebAuthNHmacSecretSalt)),
("cCredWithHmacSecretSaltList", DWORD),
("pCredWithHmacSecretSaltList", ctypes.POINTER(WebAuthNCredWithHmacSecretSalt)),
]
def __init__(
self,
global_salt: WebAuthNHmacSecretSalt | None,
credential_salts: Sequence[WebAuthNCredWithHmacSecretSalt] = [],
):
if global_salt:
self.pGlobalHmacSalt = ctypes.pointer(global_salt)
self.cCredWithHmacSecretSaltList = len(credential_salts)
self.pCredWithHmacSecretSaltList = (
WebAuthNCredWithHmacSecretSalt * len(credential_salts)
)(*credential_salts)
class WebAuthNCredProtectExtensionIn(ctypes.Structure):
"""Maps to WEBAUTHN_CRED_PROTECT_EXTENSION_IN Struct."""
_fields_ = [
("dwCredProtect", DWORD),
("bRequireCredProtect", BOOL),
]
def __init__(
self, cred_protect: WebAuthNUserVerification, require_cred_protect: bool
):
self.dwCredProtect = cred_protect
self.bRequireCredProtect = require_cred_protect
class WebAuthNCredBlobExtension(ctypes.Structure):
_fields_ = [
("cbCredBlob", DWORD),
("pbCredBlob", PBYTE),
]
cred_blob = BytesProperty("CredBlob")
def __init__(self, blob: bytes):
self.cred_blob = blob
class WebAuthNExtension(ctypes.Structure):
"""Maps to WEBAUTHN_EXTENSION Struct."""
_fields_ = [
("pwszExtensionIdentifier", LPCWSTR),
("cbExtension", DWORD),
("pvExtension", PVOID),
]
def __init__(self, identifier: str, value: Any):
self.pwszExtensionIdentifier = identifier
self.cbExtension = ctypes.sizeof(value)
self.pvExtension = ctypes.cast(ctypes.pointer(value), PVOID)
class WebAuthNExtensions(ctypes.Structure):
"""Maps to WEBAUTHN_EXTENSIONS Struct."""
_fields_ = [
("cExtensions", DWORD),
("pExtensions", ctypes.POINTER(WebAuthNExtension)),
]
def __init__(self, extensions: Sequence[WebAuthNExtension]):
self.cExtensions = len(extensions)
self.pExtensions = (WebAuthNExtension * len(extensions))(*extensions)
class WebAuthNCredential(ctypes.Structure):
"""Maps to WEBAUTHN_CREDENTIAL Struct.
:param cred: Dict of Credential Descriptor data.
"""
_fields_ = [
("dwVersion", DWORD),
("cbId", DWORD),
("pbId", PBYTE),
("pwszCredentialType", LPCWSTR),
]
id = BytesProperty("Id")
def __init__(self, cred: Mapping[str, Any]):
self.id = cred["id"]
self.pwszCredentialType = cred["type"]
class WebAuthNCredentials(ctypes.Structure):
"""Maps to WEBAUTHN_CREDENTIALS Struct.
:param credentials: List of dict of Credential Descriptor data.
"""
_fields_ = [
("cCredentials", DWORD),
("pCredentials", ctypes.POINTER(WebAuthNCredential)),
]
def __init__(self, credentials: Sequence[Mapping[str, Any]]):
self.cCredentials = len(credentials)
self.pCredentials = (WebAuthNCredential * len(credentials))(
*(WebAuthNCredential(cred) for cred in credentials)
)
class CtapCborHybridStorageLinkedData(ctypes.Structure):
"""Maps to CTAPCBOR_HYBRID_STORAGE_LINKED_DATA Struct."""
_fields_ = [
("dwVersion", DWORD),
("cbContactId", DWORD),
("pbContactId", PBYTE),
("cbLinkId", DWORD),
("pbLinkId", PBYTE),
("cbLinkSecret", DWORD),
("pbLinkSecret", PBYTE),
("cbPublicKey", DWORD),
("pbPublicKey", PBYTE),
("pwszAuthenticatorName", PCWSTR),
("wEncodedTunnelServerDomain", WORD),
] # TODO
contact_id = BytesProperty("ContactId")
link_id = BytesProperty("LinkId")
link_secret = BytesProperty("LinkSecret")
public_key = BytesProperty("PublicKey")
class WebAuthNGetAssertionOptions(ctypes.Structure):
"""Maps to WEBAUTHN_AUTHENTICATOR_GET_ASSERTION_OPTIONS Struct.
:param timeout: Time that the operation is expected to complete within.
This is used as guidance, and can be overridden by the platform.
:param attachment: Platform vs Cross-Platform
Authenticators.
:param uv_requirement: User Verification Requirement.
:param credentials: Allowed Credentials List.
"""
_fields_ = [
("dwVersion", DWORD),
("dwTimeoutMilliseconds", DWORD),
("CredentialList", WebAuthNCredentials),
("Extensions", WebAuthNExtensions),
("dwAuthenticatorAttachment", DWORD),
("dwUserVerificationRequirement", DWORD),
("dwFlags", DWORD),
# Version 2 additions
("pwszU2fAppId", PCWSTR),
("pbU2fAppId", ctypes.POINTER(BOOL)),
# Version 3 additions
("pCancellationId", ctypes.POINTER(GUID)),
# Version 4 additions
("pAllowCredentialList", ctypes.POINTER(WebAuthNCredentialList)),
# Version 5 additions
("dwCredLargeBlobOperation", DWORD),
("cbCredLargeBlob", DWORD),
("pbCredLargeBlob", PBYTE),
# Version 6 additions
("pHmacSecretSaltValues", ctypes.POINTER(WebAuthNHmacSecretSaltValues)),
("bBrowserInPrivateMode", BOOL),
# Version 7 additions
("pLinkedDevice", ctypes.POINTER(CtapCborHybridStorageLinkedData)),
("bAutoFill", BOOL),
("cbJsonExt", DWORD),
("pbJsonExt", PBYTE),
# Version 8 additions
("cCredentialHints", DWORD),
("ppwszCredentialHints", ctypes.POINTER(PCWSTR)),
# Version 9 additions
("pwszRemoteWebOrigin", PCWSTR),
("cbPublicKeyCredentialRequestOptionsJSON", DWORD),
("pbPublicKeyCredentialRequestOptionsJSON", PBYTE),
("cbAuthenticatorId", DWORD),
("pbAuthenticatorId", PBYTE),
]
cred_large_blob = BytesProperty("CredLargeBlob")
json_ext = BytesProperty("JsonExt")
public_key_credential_request_options_json = BytesProperty(
"PublicKeyCredentialRequestOptionsJSON"
)
authenticator_id = BytesProperty("AuthenticatorId")
def __init__(
self,
timeout: int = 0,
attachment: int = WebAuthNAuthenticatorAttachment.ANY,
uv_requirement: int = WebAuthNUserVerificationRequirement.DISCOURAGED,
credentials: Sequence[Mapping[str, Any]] = [],
cancellationId: GUID | None = None,
cred_large_blob_operation: int = WebAuthNLargeBlobOperation.NONE,
cred_large_blob: bytes | None = None,
hmac_secret_salts: WebAuthNHmacSecretSaltValues | None = None,
extensions: Sequence[WebAuthNExtension] = [],
flags: int = 0,
u2f_appid: str | None = None,
u2f_appid_used: BOOL | None = None,
credential_hints: Sequence[str] | None = None,
remote_web_origin: str | None = None,
public_key_credential_request_options_json: bytes | None = None,
authenticator_id: bytes | None = None,
):
self.dwVersion = get_version(self.__class__.__name__)
self.dwTimeoutMilliseconds = timeout
self.dwAuthenticatorAttachment = attachment
self.dwUserVerificationRequirement = uv_requirement
self.dwFlags = flags
if extensions:
self.Extensions = WebAuthNExtensions(extensions)
if self.dwVersion >= 2:
self.pwszU2fAppId = u2f_appid
if u2f_appid_used is not None:
self.pbU2fAppId = ctypes.pointer(u2f_appid_used)
if self.dwVersion >= 3 and cancellationId:
self.pCancellationId = ctypes.pointer(cancellationId)
if self.dwVersion >= 4:
clist = WebAuthNCredentialList(credentials)
self.pAllowCredentialList = ctypes.pointer(clist)
else:
self.CredentialList = WebAuthNCredentials(credentials)
if self.dwVersion >= 5:
self.dwCredLargeBlobOperation = cred_large_blob_operation
self.cred_large_blob = cred_large_blob
if self.dwVersion >= 6 and hmac_secret_salts:
self.pHmacSecretSaltValues = ctypes.pointer(hmac_secret_salts)
if self.dwVersion >= 8 and credential_hints:
self.cCredentialHints = len(credential_hints)
# Keep array alive by storing on instance
self._credential_hints_array = (PCWSTR * len(credential_hints))(
*credential_hints
)
self.ppwszCredentialHints = self._credential_hints_array
if self.dwVersion >= 9:
self.pwszRemoteWebOrigin = remote_web_origin
self.public_key_credential_request_options_json = (
public_key_credential_request_options_json
)
self.authenticator_id = authenticator_id
class WebAuthNAssertion(ctypes.Structure):
"""Maps to WEBAUTHN_ASSERTION Struct."""
_fields_ = [
("dwVersion", DWORD),
("cbAuthenticatorData", DWORD),
("pbAuthenticatorData", PBYTE),
("cbSignature", DWORD),
("pbSignature", PBYTE),
("Credential", WebAuthNCredential),
("cbUserId", DWORD),
("pbUserId", PBYTE),
# Version 2 additions
("Extensions", WebAuthNExtensions),
("cbCredLargeBlob", DWORD),
("pbCredLargeBlob", PBYTE),
("dwCredLargeBlobStatus", DWORD),
# Version 3 additions
("pHmacSecret", ctypes.POINTER(WebAuthNHmacSecretSalt)),
# Version 4 additions
("dwUsedTransports", DWORD),
# Version 5 additions
("cbUnsignedExtensionOutputs", DWORD),
("pbUnsignedExtensionOutputs", PBYTE),
# Version 6 additions
("cbClientDataJSON", DWORD),
("pbClientDataJSON", PBYTE),
("cbAuthenticationResponseJSON", DWORD),
("pbAuthenticationResponseJSON", PBYTE),
]
auth_data = BytesProperty("AuthenticatorData")
signature = BytesProperty("Signature")
user_id = BytesProperty("UserId")
cred_large_blob = BytesProperty("CredLargeBlob")
unsigned_extension_outputs = BytesProperty("UnsignedExtensionOutputs")
client_data_json = BytesProperty("ClientDataJSON")
authentication_response_json = BytesProperty("AuthenticationResponseJSON")
def __del__(self):
WEBAUTHN.WebAuthNFreeAssertion(ctypes.byref(self))
class WebAuthNMakeCredentialOptions(ctypes.Structure):
"""maps to WEBAUTHN_AUTHENTICATOR_MAKE_CREDENTIAL_OPTIONS Struct.
:param timeout: Time that the operation is expected to complete within.This
is used as guidance, and can be overridden by the platform.
:param require_resident_key: Require key to be resident or not.
:param attachment: Platform vs Cross-Platform
Authenticators.
:param user_verification_requirement: User
Verification Requirement.
:param attestation_convoyence:
Attestation Conveyance Preference.
:param credentials: Credentials used for exclusion.
"""
_fields_ = [
("dwVersion", DWORD),
("dwTimeoutMilliseconds", DWORD),
("CredentialList", WebAuthNCredentials),
("Extensions", WebAuthNExtensions),
("dwAuthenticatorAttachment", DWORD),
("bRequireResidentKey", BOOL),
("dwUserVerificationRequirement", DWORD),
("dwAttestationConveyancePreference", DWORD),
("dwFlags", DWORD),
# Version 2 additions
("pCancellationId", ctypes.POINTER(GUID)),
# Version 3 additions
("pExcludeCredentialList", ctypes.POINTER(WebAuthNCredentialList)),
# Version 4 additions
("dwEnterpriseAttestation", DWORD),
("dwLargeBlobSupport", DWORD),
("bPreferResidentKey", BOOL),
# Version 5 additions
("bBrowserInPrivateMode", BOOL),
# Version 6 additions
("bEnablePrf", BOOL),
# Version 7 additions
("pLinkedDevice", ctypes.POINTER(CtapCborHybridStorageLinkedData)),
("cbJsonExt", DWORD),
("pbJsonExt", PBYTE),
# Version 8 additions
("pPRFGlobalEval", ctypes.POINTER(WebAuthNHmacSecretSalt)),
("cCredentialHints", DWORD),
("ppwszCredentialHints", ctypes.POINTER(PCWSTR)),
("bThirdPartyPayment", BOOL),
# Version 9 additions
("pwszRemoteWebOrigin", PCWSTR),
("cbPublicKeyCredentialCreationOptionsJSON", DWORD),
("pbPublicKeyCredentialCreationOptionsJSON", PBYTE),
("cbAuthenticatorId", DWORD),
("pbAuthenticatorId", PBYTE),
]
json_ext = BytesProperty("JsonExt")
public_key_credential_creation_options_json = BytesProperty(
"PublicKeyCredentialCreationOptionsJSON"
)
authenticator_id = BytesProperty("AuthenticatorId")
def __init__(
self,
timeout: int = 0,
require_resident_key: bool = False,
attachment: int = WebAuthNAuthenticatorAttachment.ANY,
uv_requirement: int = WebAuthNUserVerificationRequirement.DISCOURAGED,
attestation_convoyence: int = WebAuthNAttestationConveyancePreference.ANY,
credentials: Sequence[Mapping[str, Any]] = [],
cancellationId: GUID | None = None,
enterprise_attestation: int = WebAuthNEnterpriseAttestation.NONE,
large_blob_support: int = WebAuthNLargeBlobSupport.NONE,
prefer_resident_key: bool = False,
enable_prf: bool = False,
extensions: Sequence[WebAuthNExtension] = [],
prf_global_eval: WebAuthNHmacSecretSalt | None = None,
credential_hints: Sequence[str] | None = None,
third_party_payment: bool = False,
remote_web_origin: str | None = None,
public_key_credential_creation_options_json: bytes | None = None,
authenticator_id: bytes | None = None,
):
self.dwVersion = get_version(self.__class__.__name__)
self.dwTimeoutMilliseconds = timeout
self.bRequireResidentKey = require_resident_key
self.dwAuthenticatorAttachment = attachment
self.dwUserVerificationRequirement = uv_requirement
self.dwAttestationConveyancePreference = attestation_convoyence
if extensions:
self.Extensions = WebAuthNExtensions(extensions)
if self.dwVersion >= 2 and cancellationId:
self.pCancellationId = ctypes.pointer(cancellationId)
if self.dwVersion >= 3:
self.pExcludeCredentialList = ctypes.pointer(
WebAuthNCredentialList(credentials)
)
else:
self.CredentialList = WebAuthNCredentials(credentials)
if self.dwVersion >= 4:
self.dwEnterpriseAttestation = enterprise_attestation
self.dwLargeBlobSupport = large_blob_support
self.bPreferResidentKey = prefer_resident_key
if self.dwVersion >= 6:
self.bEnablePrf = enable_prf
if self.dwVersion >= 8:
if prf_global_eval is not None:
self.pPRFGlobalEval = ctypes.pointer(prf_global_eval)
if credential_hints:
self.cCredentialHints = len(credential_hints)
self._credential_hints_array = (PCWSTR * len(credential_hints))(
*credential_hints
)
self.ppwszCredentialHints = self._credential_hints_array
self.bThirdPartyPayment = third_party_payment
if self.dwVersion >= 9:
self.pwszRemoteWebOrigin = remote_web_origin
self.public_key_credential_creation_options_json = (
public_key_credential_creation_options_json
)
self.authenticator_id = authenticator_id
class WebAuthNCredentialAttestation(ctypes.Structure):
"""Maps to WEBAUTHN_CREDENTIAL_ATTESTATION Struct."""
_fields_ = [
("dwVersion", DWORD),
("pwszFormatType", LPCWSTR),
("cbAuthenticatorData", DWORD),
("pbAuthenticatorData", PBYTE),
("cbAttestation", DWORD),
("pbAttestation", PBYTE),
("dwAttestationDecodeType", DWORD),
("pvAttestationDecode", PBYTE),
("cbAttestationObject", DWORD),
("pbAttestationObject", PBYTE),
("cbCredentialId", DWORD),
("pbCredentialId", PBYTE),
# Version 2 additions
("Extensions", WebAuthNExtensions),
# Version 3 additions
("dwUsedTransport", DWORD),
# Version 4 additions
("bEpAtt", BOOL),
("bLargeBlobSupported", BOOL),
("bResidentKey", BOOL),
# Version 5 additions
("bPrfEnabled", BOOL),
# Version 6 additions
("cbUnsignedExtensionOutputs", DWORD),
("pbUnsignedExtensionOutputs", PBYTE),
# Version 7 additions
("pHmacSecret", ctypes.POINTER(WebAuthNHmacSecretSalt)),
("bThirdPartyPayment", BOOL),
# Version 8 additions
("dwTransports", DWORD),
("cbClientDataJSON", DWORD),
("pbClientDataJSON", PBYTE),
("cbRegistrationResponseJSON", DWORD),
("pbRegistrationResponseJSON", PBYTE),
]
auth_data = BytesProperty("AuthenticatorData")
attestation = BytesProperty("Attestation")
attestation_object = BytesProperty("AttestationObject")
credential_id = BytesProperty("CredentialId")
unsigned_extension_outputs = BytesProperty("UnsignedExtensionOutputs")
client_data_json = BytesProperty("ClientDataJSON")
registration_response_json = BytesProperty("RegistrationResponseJSON")
def __del__(self):
WEBAUTHN.WebAuthNFreeCredentialAttestation(ctypes.byref(self))
HRESULT = cast(Any, ctypes).HRESULT
WEBAUTHN = cast(Any, ctypes).WinDLL("webauthn", winmode=LOAD_LIBRARY_SEARCH_SYSTEM32)
WEBAUTHN_API_VERSION = WEBAUTHN.WebAuthNGetApiVersionNumber()
WEBAUTHN.WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable.argtypes = [
ctypes.POINTER(ctypes.c_bool)
]
WEBAUTHN.WebAuthNIsUserVerifyingPlatformAuthenticatorAvailable.restype = HRESULT
WEBAUTHN.WebAuthNAuthenticatorMakeCredential.argtypes = [
HWND,
ctypes.POINTER(WebAuthNRpEntityInformation),
ctypes.POINTER(WebAuthNUserEntityInformation),
ctypes.POINTER(WebAuthNCoseCredentialParameters),
ctypes.POINTER(WebAuthNClientData),
ctypes.POINTER(WebAuthNMakeCredentialOptions),
ctypes.POINTER(ctypes.POINTER(WebAuthNCredentialAttestation)),
]
WEBAUTHN.WebAuthNAuthenticatorMakeCredential.restype = HRESULT
WEBAUTHN.WebAuthNAuthenticatorGetAssertion.argtypes = [
HWND,
LPCWSTR,
ctypes.POINTER(WebAuthNClientData),
ctypes.POINTER(WebAuthNGetAssertionOptions),
ctypes.POINTER(ctypes.POINTER(WebAuthNAssertion)),
]
WEBAUTHN.WebAuthNAuthenticatorGetAssertion.restype = HRESULT
WEBAUTHN.WebAuthNFreeCredentialAttestation.argtypes = [
ctypes.POINTER(WebAuthNCredentialAttestation)
]
WEBAUTHN.WebAuthNFreeAssertion.argtypes = [ctypes.POINTER(WebAuthNAssertion)]
WEBAUTHN.WebAuthNGetCancellationId.argtypes = [ctypes.POINTER(GUID)]
WEBAUTHN.WebAuthNGetCancellationId.restype = HRESULT
WEBAUTHN.WebAuthNCancelCurrentOperation.argtypes = [ctypes.POINTER(GUID)]
WEBAUTHN.WebAuthNCancelCurrentOperation.restype = HRESULT
WEBAUTHN.WebAuthNGetErrorName.argtypes = [HRESULT]
WEBAUTHN.WebAuthNGetErrorName.restype = PCWSTR
WEBAUTHN_STRUCT_VERSIONS: Mapping[int, Mapping[str, int]] = {
1: {
"WebAuthNRpEntityInformation": 1,
"WebAuthNUserEntityInformation": 1,
"WebAuthNClientData": 1,
"WebAuthNCoseCredentialParameter": 1,
"WebAuthNCredential": 1,
"WebAuthNCredentialEx": 1,
"WebAuthNMakeCredentialOptions": 3,
"WebAuthNGetAssertionOptions": 4,
"WebAuthNCommonAttestation": 1,
"WebAuthNCredentialAttestation": 3,
"WebAuthNAssertion": 1,
},
2: {},
3: {
"WebAuthNMakeCredentialOptions": 4,
"WebAuthNGetAssertionOptions": 5,
"WebAuthNCredentialAttestation": 4,
"WebAuthNAssertion": 2,
},
4: {
"WebAuthNMakeCredentialOptions": 5,
"WebAuthNGetAssertionOptions": 6,
"WebAuthNAssertion": 3,
"WebAuthNCredentialDetails": 1, # Not implemented
},
5: {
"WebAuthNCredentialDetails": 2,
},
6: {
"WebAuthNMakeCredentialOptions": 6,
"WebAuthNCredentialAttestation": 5,
"WebAuthNAssertion": 4,
},
7: {
"WebAuthNMakeCredentialOptions": 7,
"WebAuthNGetAssertionOptions": 7,
"WebAuthNCredentialAttestation": 6,
"WebAuthNAssertion": 5,
},
8: {
"WebAuthNMakeCredentialOptions": 8,
"WebAuthNCredentialDetails": 3,
"WebAuthNCredentialAttestation": 7,
"WebAuthNGetAssertionOptions": 8,
},
9: {
"WebAuthNMakeCredentialOptions": 9,
"WebAuthNGetAssertionOptions": 9,
"WebAuthNAssertion": 6,
"WebAuthNCredentialDetails": 4,
"WebAuthNCredentialAttestation": 8,
"WebAuthNAuthenticatorDetails": 1, # Not implemented
},
}
def get_version(class_name: str) -> int:
"""Get version of struct.
:param str class_name: Struct class name.
:returns: Version of Struct to use.
:rtype: int
"""
for api_version in range(WEBAUTHN_API_VERSION, 0, -1):
if (
api_version in WEBAUTHN_STRUCT_VERSIONS
and class_name in WEBAUTHN_STRUCT_VERSIONS[api_version]
):
return WEBAUTHN_STRUCT_VERSIONS[api_version][class_name]
raise ValueError("Unknown class name")
fido2-2.2.1/fido2/client/windows.py 0000644 0000000 0000000 00000047430 15220526030 013775 0 ustar 00 # 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.
from __future__ import annotations
import ctypes
import logging
from threading import Thread
from typing import Any, Mapping, Sequence, cast
from ..ctap2 import AssertionResponse
from ..ctap2.extensions import (
AuthenticatorExtensionsLargeBlobInputs,
AuthenticatorExtensionsLargeBlobOutputs,
AuthenticatorExtensionsPRFInputs,
AuthenticatorExtensionsPRFOutputs,
CredentialPropertiesOutput,
CredProtectExtension,
HMACGetSecretInput,
HMACGetSecretOutput,
)
from ..utils import _JsonDataObject, websafe_decode
from ..webauthn import (
AttestationConveyancePreference,
AttestationObject,
AuthenticationExtensionsClientOutputs,
AuthenticatorAttachment,
AuthenticatorAttestationResponse,
AuthenticatorData,
AuthenticatorSelectionCriteria,
PublicKeyCredentialCreationOptions,
PublicKeyCredentialRequestOptions,
PublicKeyCredentialType,
RegistrationResponse,
ResidentKeyRequirement,
_as_cbor,
)
from . import (
AssertionSelection,
ClientDataCollector,
ClientError,
WebAuthnClient,
_cbor_list,
)
from .win_api import (
BOOL,
GUID,
WEBAUTHN,
WEBAUTHN_API_VERSION,
WebAuthNAssertion,
WebAuthNAttestationConveyancePreference,
WebAuthNAuthenticatorAttachment,
WebAuthNClientData,
WebAuthNCoseCredentialParameters,
WebAuthNCredBlobExtension,
WebAuthNCredentialAttestation,
WebAuthNCredProtectExtensionIn,
WebAuthNCredWithHmacSecretSalt,
WebAuthNEnterpriseAttestation,
WebAuthNExtension,
WebAuthNGetAssertionOptions,
WebAuthNHmacSecretSalt,
WebAuthNHmacSecretSaltValues,
WebAuthNLargeBlobOperation,
WebAuthNLargeBlobSupport,
WebAuthNMakeCredentialOptions,
WebAuthNRpEntityInformation,
WebAuthNUserEntityInformation,
WebAuthNUserVerification,
WebAuthNUserVerificationRequirement,
)
logger = logging.getLogger(__name__)
LOAD_LIBRARY_SEARCH_SYSTEM32 = 0x00000800
user32 = cast(Any, ctypes).WinDLL("user32", winmode=LOAD_LIBRARY_SEARCH_SYSTEM32)
_extension_output_types: dict[str, type[_JsonDataObject]] = {
"hmacGetSecret": HMACGetSecretOutput,
"prf": AuthenticatorExtensionsPRFOutputs,
"largeBlob": AuthenticatorExtensionsLargeBlobOutputs,
"credProps": CredentialPropertiesOutput,
}
def _wrap_ext(key, value):
if key in _extension_output_types:
return _extension_output_types[key].from_dict(value)
return value
class CancelThread(Thread):
def __init__(self, event):
super().__init__()
self.daemon = True
self._completed = False
self.event = event
self.guid = GUID()
WEBAUTHN.WebAuthNGetCancellationId(ctypes.byref(self.guid))
def run(self):
self.event.wait()
if not self._completed:
WEBAUTHN.WebAuthNCancelCurrentOperation(ctypes.byref(self.guid))
def complete(self):
self._completed = True
self.event.set()
self.join()
class WindowsClient(WebAuthnClient):
"""Fido2Client-like class using the Windows WebAuthn API.
Note: This class only works on Windows 10 19H1 or later. This is also when Windows
started restricting access to FIDO devices, causing the standard client classes to
require admin priveleges to run (unlike this one).
:param str origin: The origin to use.
:param verify: Function to verify an RP ID for a given origin.
:param ctypes.wintypes.HWND handle: (optional) Window reference to use.
"""
def __init__(
self,
client_data_collector: ClientDataCollector,
handle=None,
allow_hmac_secret=False,
):
self.handle = handle or user32.GetForegroundWindow()
self._client_data_collector = client_data_collector
self._allow_hmac_secret = allow_hmac_secret
# TODO: Decide how to configure this list.
self._enterprise_rpid_list: Sequence[str] | None = None
@staticmethod
def is_available() -> bool:
return WEBAUTHN_API_VERSION > 0
def make_credential(self, options, event=None):
"""Create a credential using Windows WebAuthN APIs.
:param options: PublicKeyCredentialCreationOptions data.
:param threading.Event event: (optional) Signal to abort the operation.
"""
options = PublicKeyCredentialCreationOptions.from_dict(options)
# Gather client data, RP ID from client
client_data, rp_id = self._client_data_collector.collect_client_data(options)
logger.debug(f"Register a new credential for RP ID: {rp_id}")
selection = options.authenticator_selection or AuthenticatorSelectionCriteria()
resident_key = selection.resident_key or ResidentKeyRequirement.DISCOURAGED
enterprise_attestation = WebAuthNEnterpriseAttestation.NONE
if options.attestation == AttestationConveyancePreference.ENTERPRISE:
attestation = WebAuthNAttestationConveyancePreference.ANY
if self._enterprise_rpid_list is not None:
# Platform facilitated
if options.rp.id in self._enterprise_rpid_list:
enterprise_attestation = (
WebAuthNEnterpriseAttestation.PLATFORM_MANAGED
)
else:
# Vendor facilitated
enterprise_attestation = (
WebAuthNEnterpriseAttestation.VENDOR_FACILITATED
)
else:
attestation = WebAuthNAttestationConveyancePreference.from_string(
options.attestation or "none"
)
win_extensions = []
large_blob_support = WebAuthNLargeBlobSupport.NONE
enable_prf = False
hmac_salts = None
if options.extensions:
if "credentialProtectionPolicy" in options.extensions:
win_extensions.append(
WebAuthNExtension(
"credProtect",
WebAuthNCredProtectExtensionIn(
WebAuthNUserVerification(
CredProtectExtension.POLICY.str2int(
options.extensions["credentialProtectionPolicy"]
)
),
options.extensions.get(
"enforceCredentialProtectionPolicy", False
),
),
)
)
if "credBlob" in options.extensions:
win_extensions.append(
WebAuthNExtension(
"credBlob",
WebAuthNCredBlobExtension(options.extensions["credBlob"]),
)
)
if "largeBlob" in options.extensions:
large_blob_support = WebAuthNLargeBlobSupport.from_string(
options.extensions["largeBlob"].get("support", "none")
)
if options.extensions.get("minPinLength", True):
win_extensions.append(WebAuthNExtension("minPinLength", BOOL(True)))
prf = AuthenticatorExtensionsPRFInputs.from_dict(
cast(Mapping | None, options.extensions.get("prf"))
)
if prf:
enable_prf = True
win_extensions.append(WebAuthNExtension("hmac-secret", BOOL(True)))
if prf.eval:
hmac_salts = WebAuthNHmacSecretSalt(prf.eval.first, prf.eval.second)
elif "hmacCreateSecret" in options.extensions and self._allow_hmac_secret:
win_extensions.append(WebAuthNExtension("hmac-secret", BOOL(True)))
hmac_get_secret = HMACGetSecretInput.from_dict(
cast(Mapping | None, options.extensions.get("hmacGetSecret"))
)
if hmac_get_secret:
hmac_salts = WebAuthNHmacSecretSalt(
hmac_get_secret.salt1, hmac_get_secret.salt2
)
if event:
timer = CancelThread(event)
timer.start()
else:
timer = None
attestation_pointer = ctypes.POINTER(WebAuthNCredentialAttestation)()
try:
WEBAUTHN.WebAuthNAuthenticatorMakeCredential(
self.handle,
ctypes.byref(WebAuthNRpEntityInformation(_as_cbor(options.rp))),
ctypes.byref(WebAuthNUserEntityInformation(_as_cbor(options.user))),
ctypes.byref(
WebAuthNCoseCredentialParameters(
_cbor_list(options.pub_key_cred_params)
)
),
ctypes.byref(WebAuthNClientData(client_data)),
ctypes.byref(
WebAuthNMakeCredentialOptions(
options.timeout or 0,
resident_key == ResidentKeyRequirement.REQUIRED,
WebAuthNAuthenticatorAttachment.from_string(
selection.authenticator_attachment or "any"
),
WebAuthNUserVerificationRequirement.from_string(
selection.user_verification or "discouraged"
),
attestation,
_cbor_list(options.exclude_credentials) or [],
timer.guid if timer else None,
enterprise_attestation,
large_blob_support,
resident_key == ResidentKeyRequirement.PREFERRED,
enable_prf,
win_extensions,
hmac_salts,
options.hints,
)
),
ctypes.byref(attestation_pointer),
)
except OSError as e:
raise ClientError.ERR.OTHER_ERROR(e)
if timer:
# TODO: Avoid setting event?
timer.complete()
obj = attestation_pointer.contents
att_obj = AttestationObject(obj.attestation_object)
extension_outputs: dict[str, Any] = {}
if options.extensions:
extensions_out = att_obj.auth_data.extensions or {}
if obj.dwVersion >= 4 and options.extensions.get("credProps"):
extension_outputs["credProps"] = {"rk": bool(obj.bResidentKey)}
if "hmac-secret" in extensions_out:
if obj.dwVersion >= 7 and obj.pHmacSecret:
secret = obj.pHmacSecret.contents
secrets = (secret.first, secret.second)
else:
secrets = None
if enable_prf:
extension_outputs["prf"] = {
"enabled": extensions_out["hmac-secret"]
}
if secrets:
results = {"first": secrets[0]}
if secrets[1]:
results["second"] = secrets[1]
extension_outputs["prf"]["results"] = results
else:
extension_outputs["hmacCreateSecret"] = extensions_out[
"hmac-secret"
]
if secrets:
results = {"output1": secrets[0]}
if secrets[1]:
results["output2"] = secrets[1]
extension_outputs["hmacGetSecret"] = results
if obj.dwVersion >= 4 and "largeBlob" in options.extensions:
extension_outputs["largeBlob"] = {
"supported": bool(obj.bLargeBlobSupported)
}
logger.info("New credential registered")
credential = att_obj.auth_data.credential_data
assert credential is not None # noqa: S101
return RegistrationResponse(
raw_id=credential.credential_id,
response=AuthenticatorAttestationResponse(
client_data=client_data, attestation_object=att_obj
),
authenticator_attachment=AuthenticatorAttachment.CROSS_PLATFORM,
client_extension_results=AuthenticationExtensionsClientOutputs(
{k: _wrap_ext(k, v) for k, v in extension_outputs.items()}
),
type=PublicKeyCredentialType.PUBLIC_KEY,
)
def get_assertion(self, options, event=None):
"""Get assertion using Windows WebAuthN APIs.
:param options: PublicKeyCredentialRequestOptions data.
:param threading.Event event: (optional) Signal to abort the operation.
"""
options = PublicKeyCredentialRequestOptions.from_dict(options)
# Gather client data, RP ID from client
client_data, rp_id = self._client_data_collector.collect_client_data(options)
logger.debug(f"Assert a credential for RP ID: {rp_id}")
attachment = WebAuthNAuthenticatorAttachment.ANY
for hint in options.hints or []:
match hint:
case "security-key":
attachment = WebAuthNAuthenticatorAttachment.CROSS_PLATFORM
case "client-device":
attachment = WebAuthNAuthenticatorAttachment.PLATFORM
case _:
continue
break
flags = 0
large_blob = None
large_blob_operation = WebAuthNLargeBlobOperation.NONE
hmac_secret_salts = None
win_extensions = []
u2f_appid = None
u2f_appid_used = BOOL(False)
if options.extensions:
if options.extensions.get("appid"):
u2f_appid = options.extensions["appid"]
if options.extensions.get("getCredBlob"):
win_extensions.append(WebAuthNExtension("credBlob", BOOL(True)))
lg_blob = AuthenticatorExtensionsLargeBlobInputs.from_dict(
cast(Mapping | None, options.extensions.get("largeBlob"))
)
if lg_blob:
if lg_blob.read:
large_blob_operation = WebAuthNLargeBlobOperation.GET
else:
large_blob = lg_blob.write
large_blob_operation = WebAuthNLargeBlobOperation.SET
prf = AuthenticatorExtensionsPRFInputs.from_dict(
cast(Mapping | None, options.extensions.get("prf"))
)
if prf:
cred_salts = prf.eval_by_credential or {}
hmac_secret_salts = WebAuthNHmacSecretSaltValues(
(
WebAuthNHmacSecretSalt(prf.eval.first, prf.eval.second)
if prf.eval
else None
),
[
WebAuthNCredWithHmacSecretSalt(
websafe_decode(cred_id),
WebAuthNHmacSecretSalt(salts.first, salts.second),
)
for cred_id, salts in cred_salts.items()
],
)
elif "hmacGetSecret" in options.extensions and self._allow_hmac_secret:
flags |= 0x00100000
salts = HMACGetSecretInput.from_dict(
cast(Mapping, options.extensions["hmacGetSecret"])
)
hmac_secret_salts = WebAuthNHmacSecretSaltValues(
WebAuthNHmacSecretSalt(salts.salt1, salts.salt2)
)
if event:
timer = CancelThread(event)
timer.start()
else:
timer = None
assertion_pointer = ctypes.POINTER(WebAuthNAssertion)()
try:
WEBAUTHN.WebAuthNAuthenticatorGetAssertion(
self.handle,
options.rp_id,
ctypes.byref(WebAuthNClientData(client_data)),
ctypes.byref(
WebAuthNGetAssertionOptions(
options.timeout or 0,
attachment,
WebAuthNUserVerificationRequirement.from_string(
options.user_verification or "discouraged"
),
_cbor_list(options.allow_credentials) or [],
timer.guid if timer else None,
large_blob_operation,
large_blob,
hmac_secret_salts,
win_extensions,
flags,
u2f_appid,
u2f_appid_used,
options.hints,
)
),
ctypes.byref(assertion_pointer),
)
except OSError as e:
raise ClientError.ERR.OTHER_ERROR(e)
if timer:
# TODO: Avoid setting event?
timer.complete()
obj = assertion_pointer.contents
auth_data = AuthenticatorData(obj.auth_data)
extension_outputs: dict[str, Any] = {}
if obj.dwVersion >= 2 and u2f_appid:
extension_outputs["appid"] = bool(u2f_appid_used.value)
if options.extensions:
if obj.dwVersion >= 3 and obj.pHmacSecret:
secret = obj.pHmacSecret.contents
if "prf" in options.extensions:
result = {"first": secret.first}
if secret.second:
result["second"] = secret.second
extension_outputs["prf"] = {"results": result}
else:
result = {"output1": secret.first}
if secret.second:
result["output2"] = secret.second
extension_outputs["hmacGetSecret"] = result
if obj.dwVersion >= 2 and obj.dwCredLargeBlobStatus != 0:
if options.extensions["largeBlob"].get("read", False):
extension_outputs["largeBlob"] = {"blob": obj.cred_large_blob}
else:
extension_outputs["largeBlob"] = {
"written": obj.dwCredLargeBlobStatus == 1
}
credential = {
"type": obj.Credential.pwszCredentialType,
"id": obj.Credential.id,
}
return AssertionSelection(
client_data,
[
AssertionResponse(
credential=credential,
auth_data=auth_data,
signature=obj.signature,
user={"id": obj.user_id} if obj.user_id else None,
)
],
{k: _wrap_ext(k, v) for k, v in extension_outputs.items()},
)
fido2-2.2.1/fido2/cose.py 0000644 0000000 0000000 00000041574 15220526030 011761 0 ustar 00 # 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.
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Mapping, Sequence, Tuple, TypeVar
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec, ed448, ed25519, padding, rsa
from . import arkg
from .utils import bytes2int, int2bytes
if TYPE_CHECKING:
# This type isn't available on cryptography <40.
from cryptography.hazmat.primitives.asymmetric.types import PublicKeyTypes
_backend = default_backend()
_mldsa = hasattr(_backend, "mldsa_supported") and _backend.mldsa_supported()
class CoseKey(dict):
"""A COSE formatted public key.
:param _: The COSE key paramters.
:cvar ALGORITHM: COSE algorithm identifier.
"""
ALGORITHM: int = None # type: ignore
def verify(self, message: bytes, signature: bytes) -> None:
"""Validates a digital signature over a given message.
:param message: The message which was signed.
:param signature: The signature to check.
"""
raise NotImplementedError("Signature verification not supported.")
@classmethod
def from_cryptography_key(
cls: type[T_CoseKey], public_key: PublicKeyTypes
) -> T_CoseKey:
"""Converts a PublicKey object from Cryptography into a COSE key.
:param public_key: Either an EC or RSA public key.
:return: A CoseKey.
"""
raise NotImplementedError("Creation from cryptography not supported.")
@staticmethod
def for_alg(alg: int) -> type[CoseKey]:
"""Get a subclass of CoseKey corresponding to an algorithm identifier.
:param alg: The COSE identifier of the algorithm.
:return: A CoseKey.
"""
def find_subclass(base_cls: type[CoseKey]) -> type[CoseKey] | None:
for cls in base_cls.__subclasses__():
if cls.ALGORITHM == alg:
return cls
subresult = find_subclass(cls)
if subresult:
return subresult
return None
return find_subclass(CoseKey) or UnsupportedKey
@staticmethod
def for_name(name: str) -> type[CoseKey]:
"""Get a subclass of CoseKey corresponding to an algorithm identifier.
:param alg: The COSE identifier of the algorithm.
:return: A CoseKey.
"""
def find_subclass(base_cls: type[CoseKey]) -> type[CoseKey] | None:
for cls in base_cls.__subclasses__():
if cls.__name__ == name:
return cls
subresult = find_subclass(cls)
if subresult:
return subresult
return None
return find_subclass(CoseKey) or UnsupportedKey
@staticmethod
def parse(cose: Mapping[int, Any]) -> CoseKey:
"""Create a CoseKey from a dict"""
alg = cose.get(3)
if not alg:
raise ValueError("COSE alg identifier must be provided.")
return CoseKey.for_alg(alg)(cose)
@staticmethod
def supported_algorithms() -> Sequence[int]:
"""Get a list of all supported algorithm identifiers"""
algs: list[type[CoseKey]] = [
ES256,
EdDSA,
ES384,
ES512,
PS256,
RS256,
ES256K,
]
if _mldsa:
algs.extend([MLDSA44, MLDSA65, MLDSA87])
return [cls.ALGORITHM for cls in algs]
T_CoseKey = TypeVar("T_CoseKey", bound=CoseKey)
class UnsupportedKey(CoseKey):
"""A COSE key with an unsupported algorithm."""
class ES256(CoseKey):
ALGORITHM = -7
_HASH_ALG = hashes.SHA256()
def verify(self, message, signature):
if self[-1] != 1:
raise ValueError("Unsupported elliptic curve")
ec.EllipticCurvePublicNumbers(
bytes2int(self[-2]), bytes2int(self[-3]), ec.SECP256R1()
).public_key(_backend).verify(signature, message, ec.ECDSA(self._HASH_ALG))
@classmethod
def from_cryptography_key(cls, public_key):
assert isinstance(public_key, ec.EllipticCurvePublicKey) # noqa: S101
pn = public_key.public_numbers()
return cls(
{
1: 2,
3: cls.ALGORITHM,
-1: 1,
-2: int2bytes(pn.x, 32),
-3: int2bytes(pn.y, 32),
}
)
@classmethod
def from_ctap1(cls, data):
"""Creates an ES256 key from a CTAP1 formatted public key byte string.
:param data: A 65 byte SECP256R1 public key.
:return: A ES256 key.
"""
return cls({1: 2, 3: cls.ALGORITHM, -1: 1, -2: data[1:33], -3: data[33:65]})
class ESP256(ES256):
# See: https://www.ietf.org/archive/id/draft-ietf-jose-fully-specified-algorithms-10.html#name-elliptic-curve-digital-sign # noqa:E501
ALGORITHM = -9
class ES384(CoseKey):
ALGORITHM = -35
_HASH_ALG = hashes.SHA384()
def verify(self, message, signature):
if self[-1] != 2:
raise ValueError("Unsupported elliptic curve")
ec.EllipticCurvePublicNumbers(
bytes2int(self[-2]), bytes2int(self[-3]), ec.SECP384R1()
).public_key(_backend).verify(signature, message, ec.ECDSA(self._HASH_ALG))
@classmethod
def from_cryptography_key(cls, public_key):
assert isinstance(public_key, ec.EllipticCurvePublicKey) # noqa: S101
pn = public_key.public_numbers()
return cls(
{
1: 2,
3: cls.ALGORITHM,
-1: 2,
-2: int2bytes(pn.x, 48),
-3: int2bytes(pn.y, 48),
}
)
class ESP384(ES384):
# See: https://www.ietf.org/archive/id/draft-ietf-jose-fully-specified-algorithms-12.html#name-elliptic-curve-digital-sign # noqa:E501
ALGORITHM = -51
class ES512(CoseKey):
ALGORITHM = -36
_HASH_ALG = hashes.SHA512()
def verify(self, message, signature):
if self[-1] != 3:
raise ValueError("Unsupported elliptic curve")
ec.EllipticCurvePublicNumbers(
bytes2int(self[-2]), bytes2int(self[-3]), ec.SECP521R1()
).public_key(_backend).verify(signature, message, ec.ECDSA(self._HASH_ALG))
@classmethod
def from_cryptography_key(cls, public_key):
assert isinstance(public_key, ec.EllipticCurvePublicKey) # noqa: S101
pn = public_key.public_numbers()
return cls(
{
1: 2,
3: cls.ALGORITHM,
-1: 3,
-2: int2bytes(pn.x, 66),
-3: int2bytes(pn.y, 66),
}
)
class ESP512(ES512):
# See: https://www.ietf.org/archive/id/draft-ietf-jose-fully-specified-algorithms-12.html#name-elliptic-curve-digital-sign # noqa:E501
ALGORITHM = -52
class RS256(CoseKey):
ALGORITHM = -257
_HASH_ALG = hashes.SHA256()
def verify(self, message, signature):
rsa.RSAPublicNumbers(bytes2int(self[-2]), bytes2int(self[-1])).public_key(
_backend
).verify(signature, message, padding.PKCS1v15(), self._HASH_ALG)
@classmethod
def from_cryptography_key(cls, public_key):
assert isinstance(public_key, rsa.RSAPublicKey) # noqa: S101
pn = public_key.public_numbers()
return cls({1: 3, 3: cls.ALGORITHM, -1: int2bytes(pn.n), -2: int2bytes(pn.e)})
class PS256(CoseKey):
ALGORITHM = -37
_HASH_ALG = hashes.SHA256()
def verify(self, message, signature):
rsa.RSAPublicNumbers(bytes2int(self[-2]), bytes2int(self[-1])).public_key(
_backend
).verify(
signature,
message,
padding.PSS(
mgf=padding.MGF1(self._HASH_ALG), salt_length=padding.PSS.MAX_LENGTH
),
self._HASH_ALG,
)
@classmethod
def from_cryptography_key(cls, public_key):
assert isinstance(public_key, rsa.RSAPublicKey) # noqa: S101
pn = public_key.public_numbers()
return cls({1: 3, 3: cls.ALGORITHM, -1: int2bytes(pn.n), -2: int2bytes(pn.e)})
class EdDSA(CoseKey):
ALGORITHM = -8
def verify(self, message, signature):
if self[-1] != 6:
raise ValueError("Unsupported elliptic curve")
ed25519.Ed25519PublicKey.from_public_bytes(self[-2]).verify(signature, message)
@classmethod
def from_cryptography_key(cls, public_key):
assert isinstance(public_key, ed25519.Ed25519PublicKey) # noqa: S101
return cls(
{
1: 1,
3: cls.ALGORITHM,
-1: 6,
-2: public_key.public_bytes(
serialization.Encoding.Raw, serialization.PublicFormat.Raw
),
}
)
class Ed25519(EdDSA):
# See: https://www.ietf.org/archive/id/draft-ietf-jose-fully-specified-algorithms-12.html#name-edwards-curve-digital-signa # noqa:E501
ALGORITHM = -19
class Ed448(CoseKey):
# See: https://www.ietf.org/archive/id/draft-ietf-jose-fully-specified-algorithms-12.html#name-edwards-curve-digital-signa # noqa:E501
ALGORITHM = -53
def verify(self, message, signature):
if self[-1] != 7:
raise ValueError("Unsupported elliptic curve")
ed448.Ed448PublicKey.from_public_bytes(self[-2]).verify(signature, message)
@classmethod
def from_cryptography_key(cls, public_key):
assert isinstance(public_key, ed448.Ed448PublicKey) # noqa: S101
return cls(
{
1: 1,
3: cls.ALGORITHM,
-1: 7,
-2: public_key.public_bytes(
serialization.Encoding.Raw, serialization.PublicFormat.Raw
),
}
)
class RS1(CoseKey):
ALGORITHM = -65535
_HASH_ALG = hashes.SHA1() # noqa: S303
def verify(self, message, signature):
rsa.RSAPublicNumbers(bytes2int(self[-2]), bytes2int(self[-1])).public_key(
_backend
).verify(signature, message, padding.PKCS1v15(), self._HASH_ALG)
@classmethod
def from_cryptography_key(cls, public_key):
assert isinstance(public_key, rsa.RSAPublicKey) # noqa: S101
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(_backend).verify(signature, message, ec.ECDSA(self._HASH_ALG))
@classmethod
def from_cryptography_key(cls, public_key):
assert isinstance(public_key, ec.EllipticCurvePublicKey) # noqa: S101
pn = public_key.public_numbers()
return cls(
{
1: 2,
3: cls.ALGORITHM,
-1: 8,
-2: int2bytes(pn.x, 32),
-3: int2bytes(pn.y, 32),
}
)
def _cose2key(cose: CoseKey, curve: ec.EllipticCurve) -> ec.EllipticCurvePublicKey:
return ec.EllipticCurvePublicNumbers(
bytes2int(cose[-2]), bytes2int(cose[-3]), curve
).public_key()
# ESP256-split using private key derived by ARKG-P256
# https://www.ietf.org/archive/id/draft-bradleylundberg-cfrg-arkg-10.html#section-8.3
ESP256_SPLIT_ARKG_PLACEHOLDER = -65539 # placeholder value
class ARKG_P256_PLACEHOLDER(CoseKey):
"""
https://www.ietf.org/archive/id/draft-bradleylundberg-cfrg-arkg-10.html#ARKG-P256
The identifier ARKG-P256 represents the following ARKG instance:
BL: Elliptic curve addition as described in Section 3.1 with the parameters:
crv: The NIST curve secp256r1 [SEC2].
hash-to-crv-suite: P256_XMD:SHA-256_SSWU_RO_ [RFC9380].
DST_ext: 'ARKG-P256'.
KEM: ECDH as described in Section 3.3 with the parameters:
crv: The NIST curve secp256r1 [SEC2].
Hash: SHA-256 [FIPS 180-4].
hash-to-crv-suite: P256_XMD:SHA-256_SSWU_RO_ [RFC9380].
DST_ext: 'ARKG-P256'.
"""
ALGORITHM = -65700 # placeholder value
_CRV = ec.SECP256R1()
_ARKG = arkg._ARKG(
bl=arkg._BL(crv=_CRV, Hash=hashes.SHA256(), DST_ext=b"ARKG-P256"),
kem=arkg._KEM(crv=_CRV, Hash=hashes.SHA256(), DST_ext=b"ARKG-ECDH.ARKG-P256"),
)
@property
def pk_bl(self) -> CoseKey:
return CoseKey.parse(self[-1])
@property
def pk_kem(self) -> CoseKey:
return CoseKey.parse(self[-2])
def derive_public_key(self, ikm: bytes, ctx: bytes) -> Tuple[CoseKey, Mapping]:
pk, kh = self._ARKG.derive_public_key(
_cose2key(self.pk_bl, self._CRV),
_cose2key(self.pk_kem, self._CRV),
ikm,
ctx,
)
point_enc = pk.public_bytes(
serialization.Encoding.X962, serialization.PublicFormat.UncompressedPoint
)
ln = (len(point_enc) - 1) // 2
pk_cose = ESP256(
{
1: 2, # kty: EC
3: self[-3], # alg: derived key alg (key -3) from seed (value -9)
-1: 1,
-2: point_enc[1 : ln + 1], # x-coordinate
-3: point_enc[1 + ln :], # y-coordinate
}
)
# COSE_Sign_Args: https://www.ietf.org/archive/id/draft-bradleylundberg-cfrg-arkg-10.html#name-cose-signing-arguments
args = {
3: ESP256_SPLIT_ARKG_PLACEHOLDER,
-1: kh,
-2: ctx,
}
return pk_cose, args
# MLDSA support was added in cryptography 47.0.0, and requires a supported backend
if _mldsa:
from cryptography.hazmat.primitives.asymmetric import mldsa
class MLDSA44(CoseKey):
ALGORITHM = -48
def verify(self, message, signature):
if self[1] != 7:
raise ValueError("Invalid key type")
pk = mldsa.MLDSA44PublicKey.from_public_bytes(self[-1])
pk.verify(signature, message)
@classmethod
def from_cryptography_key(cls, public_key):
assert isinstance(public_key, mldsa.MLDSA44PublicKey) # noqa: S101
return cls(
{
1: 7,
3: cls.ALGORITHM,
-1: public_key.public_bytes_raw(),
}
)
class MLDSA65(CoseKey):
ALGORITHM = -49
def verify(self, message, signature):
if self[1] != 7:
raise ValueError("Invalid key type")
pk = mldsa.MLDSA65PublicKey.from_public_bytes(self[-1])
pk.verify(signature, message)
@classmethod
def from_cryptography_key(cls, public_key):
assert isinstance(public_key, mldsa.MLDSA65PublicKey) # noqa: S101
return cls(
{
1: 7,
3: cls.ALGORITHM,
-1: public_key.public_bytes_raw(),
}
)
class MLDSA87(CoseKey):
ALGORITHM = -50
def verify(self, message, signature):
if self[1] != 7:
raise ValueError("Invalid key type")
pk = mldsa.MLDSA87PublicKey.from_public_bytes(self[-1])
pk.verify(signature, message)
@classmethod
def from_cryptography_key(cls, public_key):
assert isinstance(public_key, mldsa.MLDSA87PublicKey) # noqa: S101
return cls(
{
1: 7,
3: cls.ALGORITHM,
-1: public_key.public_bytes_raw(),
}
)
fido2-2.2.1/fido2/ctap.py 0000644 0000000 0000000 00000013357 15220526030 011755 0 ustar 00 # 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.
from __future__ import annotations
import abc
from enum import IntEnum, unique
from threading import Event
from typing import Callable, Iterator, cast
@unique
class STATUS(IntEnum):
"""Status code for CTAP keep-alive message."""
PROCESSING = 1
UPNEEDED = 2
class CtapDevice(abc.ABC):
"""
CTAP-capable device.
Subclasses of this should implement :func:`call`, as well as :func:`list_devices`,
which should return a generator over discoverable devices.
"""
@property
@abc.abstractmethod
def capabilities(self) -> int:
"""Get device capabilities"""
@abc.abstractmethod
def call(
self,
cmd: int,
data: bytes = b"",
event: Event | None = None,
on_keepalive: Callable[[STATUS], None] | None = None,
) -> bytes:
"""Sends a command to the authenticator, and reads the response.
:param cmd: The integer value of the command.
:param data: The payload of the command.
:param event: An optional threading.Event which can be used to cancel
the invocation.
:param on_keepalive: An optional callback to handle keep-alive messages
from the authenticator. The function is only called once for
consecutive keep-alive messages with the same status.
:return: The response from the authenticator.
"""
def close(self) -> None:
"""Close the device, releasing any held resources."""
def __enter__(self):
return self
def __exit__(self, typ, value, traceback):
self.close()
@classmethod
@abc.abstractmethod
def list_devices(cls) -> Iterator[CtapDevice]:
"""Generates instances of cls for discoverable devices."""
class CtapError(Exception):
"""Error returned from the Authenticator when a command fails."""
class UNKNOWN_ERR(int):
"""CTAP error status code that is not recognized."""
name = "UNKNOWN_ERR"
@property
def value(self) -> int:
return int(self)
def __repr__(self):
return "" % self
def __str__(self):
return f"0x{self:02X} - UNKNOWN"
@unique
class ERR(IntEnum):
"""CTAP status codes.
https://fidoalliance.org/specs/fido-v2.1-rd-20201208/fido-client-to-authenticator-protocol-v2.1-rd-20201208.html#error-responses
"""
SUCCESS = 0x00
INVALID_COMMAND = 0x01
INVALID_PARAMETER = 0x02
INVALID_LENGTH = 0x03
INVALID_SEQ = 0x04
TIMEOUT = 0x05
CHANNEL_BUSY = 0x06
LOCK_REQUIRED = 0x0A
INVALID_CHANNEL = 0x0B
CBOR_UNEXPECTED_TYPE = 0x11
INVALID_CBOR = 0x12
MISSING_PARAMETER = 0x14
LIMIT_EXCEEDED = 0x15
# UNSUPPORTED_EXTENSION = 0x16 # No longer in spec
FP_DATABASE_FULL = 0x17
LARGE_BLOB_STORAGE_FULL = 0x18
CREDENTIAL_EXCLUDED = 0x19
PROCESSING = 0x21
INVALID_CREDENTIAL = 0x22
USER_ACTION_PENDING = 0x23
OPERATION_PENDING = 0x24
NO_OPERATIONS = 0x25
UNSUPPORTED_ALGORITHM = 0x26
OPERATION_DENIED = 0x27
KEY_STORE_FULL = 0x28
# NOT_BUSY = 0x29 # No longer in spec
# NO_OPERATION_PENDING = 0x2A # No longer in spec
UNSUPPORTED_OPTION = 0x2B
INVALID_OPTION = 0x2C
KEEPALIVE_CANCEL = 0x2D
NO_CREDENTIALS = 0x2E
USER_ACTION_TIMEOUT = 0x2F
NOT_ALLOWED = 0x30
PIN_INVALID = 0x31
PIN_BLOCKED = 0x32
PIN_AUTH_INVALID = 0x33
PIN_AUTH_BLOCKED = 0x34
PIN_NOT_SET = 0x35
PUAT_REQUIRED = 0x36
PIN_POLICY_VIOLATION = 0x37
PIN_TOKEN_EXPIRED = 0x38
REQUEST_TOO_LARGE = 0x39
ACTION_TIMEOUT = 0x3A
UP_REQUIRED = 0x3B
UV_BLOCKED = 0x3C
INTEGRITY_FAILURE = 0x3D
INVALID_SUBCOMMAND = 0x3E
UV_INVALID = 0x3F
UNAUTHORIZED_PERMISSION = 0x40
OTHER = 0x7F
SPEC_LAST = 0xDF
EXTENSION_FIRST = 0xE0
EXTENSION_LAST = 0xEF
VENDOR_FIRST = 0xF0
VENDOR_LAST = 0xFF
def __str__(self):
return f"0x{self.value:02X} - {self.name}"
def __init__(self, code: int):
try:
self.code = CtapError.ERR(code)
except ValueError:
self.code = cast(CtapError.ERR, CtapError.UNKNOWN_ERR(code))
super().__init__(f"CTAP error: {self.code}")
fido2-2.2.1/fido2/ctap1.py 0000644 0000000 0000000 00000021736 15220526030 012036 0 ustar 00 # Copyright (c) 2013 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 __future__ import annotations
import struct
from dataclasses import dataclass
from enum import IntEnum, unique
from .attestation import FidoU2FAttestation
from .cose import ES256
from .ctap import CtapDevice
from .hid import CTAPHID
from .utils import ByteBuffer, bytes2int, websafe_decode, websafe_encode
@unique
class APDU(IntEnum):
"""APDU response codes."""
OK = 0x9000
USE_NOT_SATISFIED = 0x6985
WRONG_DATA = 0x6A80
class ApduError(Exception):
"""An Exception thrown when a response APDU doesn't have an OK (0x9000)
status.
:param code: APDU response code.
:param data: APDU response body.
"""
def __init__(self, code: int, data: bytes = b""):
self.code = code
self.data = data
def __repr__(self):
return f"APDU error: 0x{self.code:04X} {len(self.data):d} bytes of data"
@dataclass(init=False)
class RegistrationData(bytes):
"""Binary response data for a CTAP1 registration.
:param _: The binary contents of the response data.
:ivar public_key: Binary representation of the credential public key.
:ivar key_handle: Binary key handle of the credential.
:ivar certificate: Attestation certificate of the authenticator, DER
encoded.
:ivar signature: Attestation signature.
"""
public_key: bytes
key_handle: bytes
certificate: bytes
signature: bytes
def __init__(self, _: bytes):
super().__init__()
reader = ByteBuffer(self)
if reader.unpack("B") != 0x05:
raise ValueError("Reserved byte != 0x05")
self.public_key = reader.read(65)
self.key_handle = reader.read(reader.unpack("B"))
cert_buf = reader.read(2) # Tag and first length byte
cert_len = cert_buf[1]
if cert_len > 0x80: # Multi-byte length
n_bytes = cert_len - 0x80
len_bytes = reader.read(n_bytes)
cert_buf += len_bytes
cert_len = bytes2int(len_bytes)
self.certificate = cert_buf + reader.read(cert_len)
self.signature = reader.read()
@property
def b64(self) -> str:
"""Websafe base64 encoded string of the RegistrationData."""
return websafe_encode(self)
def verify(self, app_param: bytes, client_param: bytes) -> None:
"""Verify the included signature with regard to the given app and client
params.
:param app_param: SHA256 hash of the app ID used for the request.
:param client_param: SHA256 hash of the ClientData used for the request.
"""
FidoU2FAttestation.verify_signature(
app_param,
client_param,
self.key_handle,
self.public_key,
self.certificate,
self.signature,
)
@classmethod
def from_b64(cls, data: str) -> RegistrationData:
"""Parse a RegistrationData from a websafe base64 encoded string.
:param data: Websafe base64 encoded string.
:return: The decoded and parsed RegistrationData.
"""
return cls(websafe_decode(data))
@dataclass(init=False)
class SignatureData(bytes):
"""Binary response data for a CTAP1 authentication.
:param _: The binary contents of the response data.
:ivar user_presence: User presence byte.
:ivar counter: Signature counter.
:ivar signature: Cryptographic signature.
"""
user_presence: int
counter: int
signature: bytes
def __init__(self, _: bytes):
super().__init__()
reader = ByteBuffer(self)
self.user_presence = reader.unpack("B")
self.counter = reader.unpack(">I")
self.signature = reader.read()
@property
def b64(self) -> str:
"""str: Websafe base64 encoded string of the SignatureData."""
return websafe_encode(self)
def verify(self, app_param: bytes, client_param: bytes, public_key: bytes) -> None:
"""Verify the included signature with regard to the given app and client
params, using the given public key.
:param app_param: SHA256 hash of the app ID used for the request.
:param client_param: SHA256 hash of the ClientData used for the request.
:param public_key: Binary representation of the credential public key.
"""
m = app_param + self[:5] + client_param
ES256.from_ctap1(public_key).verify(m, self.signature)
@classmethod
def from_b64(cls, data: str) -> SignatureData:
"""Parse a SignatureData from a websafe base64 encoded string.
:param data: Websafe base64 encoded string.
:return: The decoded and parsed SignatureData.
"""
return cls(websafe_decode(data))
class Ctap1:
"""Implementation of the CTAP1 specification.
:param device: A CtapHidDevice handle supporting CTAP1.
"""
@unique
class INS(IntEnum):
REGISTER = 0x01
AUTHENTICATE = 0x02
VERSION = 0x03
def __init__(self, device: CtapDevice):
self.device = device
def send_apdu(
self, cla: int = 0, ins: int = 0, p1: int = 0, p2: int = 0, data: bytes = b""
) -> bytes:
"""Packs and sends an APDU for use in CTAP1 commands.
This is a low-level method mainly used internally. Avoid calling it
directly if possible, and use the get_version, register, and
authenticate methods if possible instead.
:param cla: The CLA parameter of the request.
:param ins: The INS parameter of the request.
:param p1: The P1 parameter of the request.
:param p2: The P2 parameter of the request.
:param data: The body of the request.
:return: The response APDU data of a successful request.
:raise: ApduError
"""
apdu = struct.pack(">BBBBBH", cla, ins, p1, p2, 0, len(data)) + data + b"\0\0"
response = self.device.call(CTAPHID.MSG, apdu)
status = struct.unpack(">H", response[-2:])[0]
data = response[:-2]
if status != APDU.OK:
raise ApduError(status, data)
return data
def get_version(self) -> str:
"""Get the U2F version implemented by the authenticator.
The only version specified is "U2F_V2".
:return: A U2F version string.
"""
return self.send_apdu(ins=Ctap1.INS.VERSION).decode()
def register(self, client_param: bytes, app_param: bytes) -> RegistrationData:
"""Register a new U2F credential.
:param client_param: SHA256 hash of the ClientData used for the request.
:param app_param: SHA256 hash of the app ID used for the request.
:return: The registration response from the authenticator.
"""
data = client_param + app_param
response = self.send_apdu(ins=Ctap1.INS.REGISTER, data=data)
return RegistrationData(response)
def authenticate(
self,
client_param: bytes,
app_param: bytes,
key_handle: bytes,
check_only: bool = False,
) -> SignatureData:
"""Authenticate a previously registered credential.
:param client_param: SHA256 hash of the ClientData used for the request.
:param app_param: SHA256 hash of the app ID used for the request.
:param key_handle: The binary key handle of the credential.
:param check_only: True to send a "check-only" request, which is used to
determine if a key handle is known.
:return: The authentication response from the authenticator.
"""
data = (
client_param + app_param + struct.pack(">B", len(key_handle)) + key_handle
)
p1 = 0x07 if check_only else 0x03
response = self.send_apdu(ins=Ctap1.INS.AUTHENTICATE, p1=p1, data=data)
return SignatureData(response)
fido2-2.2.1/fido2/ctap2/__init__.py 0000644 0000000 0000000 00000003262 15220526030 013570 0 ustar 00 # Copyright (c) 2020 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 .base import ( # noqa
Ctap2,
Info,
AttestationResponse,
AssertionResponse,
)
from .pin import ClientPin, PinProtocolV1, PinProtocolV2 # noqa
from .credman import CredentialManagement # noqa
from .bio import FPBioEnrollment, CaptureError # noqa
from .blob import LargeBlobs # noqa
from .config import Config # noqa
fido2-2.2.1/fido2/ctap2/base.py 0000644 0000000 0000000 00000060016 15220526030 012743 0 ustar 00 # 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.
from __future__ import annotations
import logging
import struct
from dataclasses import Field, dataclass, field, fields
from enum import IntEnum, unique
from threading import Event
from typing import Any, Callable, Mapping, cast
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from .. import cbor
from ..cose import CoseKey
from ..ctap import CtapDevice, CtapError
from ..hid import CAPABILITY, CTAPHID
from ..utils import _DataClassMapping
from ..webauthn import Aaguid, AuthenticatorData
logger = logging.getLogger(__name__)
def args(*params) -> dict[int, Any]:
"""Constructs a dict from a list of arguments for sending a CBOR command.
None elements will be omitted.
:param params: Arguments, in order, to add to the command.
:return: The input parameters as a dict.
"""
return {i: v for i, v in enumerate(params, 1) if v is not None}
class _CborDataObject(_DataClassMapping[int]):
@classmethod
def _get_field_key(cls, field: Field) -> int:
return fields(cls).index(field) + 1
@dataclass(eq=False, frozen=True)
class Info(_CborDataObject):
"""Binary CBOR encoded response data returned by the CTAP2 GET_INFO command.
See:
https://fidoalliance.org/specs/fido-v2.2-rd-20230321/fido-client-to-authenticator-protocol-v2.2-rd-20230321.html#authenticatorGetInfo
for descriptions of these fields.
Note that while many fields are optional when returned by the authenticator,
this dataclass uses defaults to represent a missing value such as empty lists
or dicts, or the integer value 0. These are used rather than None for omitted
values as long as this can be done without introducing any practical ambiguity.
This also means that several of these fields may have a 0 value even if the
specification states that they should be non-zero when returned from the
authenticator.
:param _: The binary content of the Info data.
"""
versions: list[str]
extensions: list[str] = field(default_factory=list)
aaguid: Aaguid = Aaguid.NONE
options: dict[str, bool] = field(default_factory=dict)
max_msg_size: int = 1024
pin_uv_protocols: list[int] = field(default_factory=list)
max_creds_in_list: int = 0
max_cred_id_length: int = 0
transports: list[str] = field(default_factory=list)
algorithms: list[dict[str, Any]] = field(default_factory=list)
max_large_blob: int = 0
force_pin_change: bool = False
min_pin_length: int = 4
firmware_version: int = 0
max_cred_blob_length: int = 0
max_rpids_for_min_pin: int = 0
preferred_platform_uv_attempts: int = 0
uv_modality: int = 0
certifications: dict = field(default_factory=dict)
remaining_disc_creds: int | None = None
vendor_prototype_config_commands: list[int] = field(default_factory=list)
attestation_formats: list[str] = field(default_factory=lambda: ["packed"])
uv_count_since_pin: int | None = None
long_touch_for_reset: bool = False
enc_identifier: bytes | None = None
transports_for_reset: list[str] = field(default_factory=list)
pin_complexity_policy: bool | None = None
pin_complexity_policy_url: bytes | None = None
max_pin_length: int = 63
enc_cred_store_state: bytes | None = None
authenticator_config_commands: list[int] | None = None
def _decrypt(
self, encrypted: bytes | None, info: bytes, pin_token: bytes
) -> bytes | None:
if not encrypted:
return None
iv, ct = encrypted[:16], encrypted[16:]
be = default_backend()
secret = HKDF(
algorithm=hashes.SHA256(),
length=16,
salt=b"\0" * 32,
info=info,
backend=be,
).derive(pin_token)
dec = Cipher(algorithms.AES(secret), modes.CBC(iv), be).decryptor()
return dec.update(ct) + dec.finalize()
def get_identifier(self, pin_token: bytes) -> bytes | None:
"""Decrypt the device identifier using a persistent PUAT."""
return self._decrypt(self.enc_identifier, b"encIdentifier", pin_token)
def get_cred_store_state(self, pin_token: bytes) -> bytes | None:
"""Decrypt the credential store state using a persistent PUAT."""
return self._decrypt(self.enc_cred_store_state, b"encCredStoreState", pin_token)
@dataclass(eq=False, frozen=True)
class AttestationResponse(_CborDataObject):
"""Binary CBOR encoded attestation object.
:param _: The binary representation of the attestation object.
:ivar fmt: The type of attestation used.
:ivar auth_data: The attested authenticator data.
:ivar att_stmt: The attestation statement.
:ivar ep_att: Whether an enterprise attestation was returned for this credential.
:ivar large_blob_key: The largeBlobKey for the credential, if requested.
:ivar unsigned_extension_outputs: Any unsigned outputs of extensions.
"""
fmt: str
auth_data: AuthenticatorData
att_stmt: dict[str, Any]
ep_att: bool | None = None
large_blob_key: bytes | None = None
unsigned_extension_outputs: dict[str, Any] = field(default_factory=dict)
@dataclass(eq=False, frozen=True)
class AssertionResponse(_CborDataObject):
"""Binary CBOR encoded assertion response.
:param _: The binary representation of the assertion response.
:ivar credential: The credential used for the assertion.
:ivar auth_data: The authenticator data part of the response.
:ivar signature: The digital signature of the assertion.
:ivar user: The user data of the credential.
:ivar number_of_credentials: The total number of responses available
(only set for the first response, if > 1).
"""
credential: Mapping[str, Any]
auth_data: AuthenticatorData
signature: bytes
user: dict[str, Any] | None = None
number_of_credentials: int | None = None
user_selected: bool | None = None
large_blob_key: bytes | None = None
def verify(self, client_param: bytes, public_key: CoseKey):
"""Verify the digital signature of the response with regard to the
client_param, using the given public key.
:param client_param: SHA256 hash of the ClientData used for the request.
:param public_key: The public key of the credential, to verify.
"""
public_key.verify(self.auth_data + client_param, self.signature)
@classmethod
def from_ctap1(
cls, app_param: bytes, credential: Mapping[str, Any], authentication
) -> "AssertionResponse":
"""Create an AssertionResponse from a CTAP1 SignatureData instance.
:param app_param: SHA256 hash of the RP ID used for the CTAP1 request.
:param credential: Credential used for the CTAP1 request (from the
allowList).
:param authentication: The CTAP1 signature data.
:return: The assertion response.
"""
return cls(
credential=credential,
auth_data=AuthenticatorData.create(
app_param,
authentication.user_presence & AuthenticatorData.FLAG.UP,
authentication.counter,
),
signature=authentication.signature,
)
class Ctap2:
"""Implementation of the CTAP2 specification.
:param device: A CtapHidDevice handle supporting CTAP2.
:param strict_cbor: Validate that CBOR returned from the Authenticator is
canonical, defaults to True.
"""
@unique
class CMD(IntEnum):
MAKE_CREDENTIAL = 0x01
GET_ASSERTION = 0x02
GET_INFO = 0x04
CLIENT_PIN = 0x06
RESET = 0x07
GET_NEXT_ASSERTION = 0x08
BIO_ENROLLMENT = 0x09
CREDENTIAL_MGMT = 0x0A
SELECTION = 0x0B
LARGE_BLOBS = 0x0C
CONFIG = 0x0D
BIO_ENROLLMENT_PRE = 0x40
CREDENTIAL_MGMT_PRE = 0x41
def __init__(self, device: CtapDevice, strict_cbor: bool = True):
if not device.capabilities & CAPABILITY.CBOR:
raise ValueError("Device does not support CTAP2.")
self.device = device
self._strict_cbor = strict_cbor
self._max_msg_size = 1024 # For initial get_info call
self._info = self.get_info()
self._max_msg_size = self._info.max_msg_size
@property
def info(self) -> Info:
"""Get a cached Info object which can be used to determine capabilities.
:rtype: Info
:return: The response of calling GetAuthenticatorInfo.
"""
return self._info
def send_cbor(
self,
cmd: int,
data: Mapping[int, Any] | None = None,
*,
event: Event | None = None,
on_keepalive: Callable[[int], None] | None = None,
) -> Mapping[int, Any]:
"""Sends a CBOR message to the device, and waits for a response.
:param cmd: The command byte of the request.
:param data: The payload to send (to be CBOR encoded).
:param event: Optional threading.Event used to cancel the request.
:param on_keepalive: Optional function called when keep-alive is sent by
the authenticator.
"""
request = struct.pack(">B", cmd)
if data is not None:
request += cbor.encode(data)
if len(request) > self._max_msg_size:
raise CtapError(CtapError.ERR.REQUEST_TOO_LARGE)
response = self.device.call(CTAPHID.CBOR, request, event, on_keepalive)
status = response[0]
if status != 0x00:
raise CtapError(status)
enc = response[1:]
if not enc:
return {}
decoded = cbor.decode(enc)
if self._strict_cbor:
expected = cbor.encode(decoded)
if expected != enc:
raise ValueError(
"Non-canonical CBOR from Authenticator.\n"
f"Got: {enc.hex()}\nExpected: {expected.hex()}"
)
if isinstance(decoded, Mapping):
return cast(Mapping[int, Any], decoded)
raise TypeError("Decoded value of wrong type")
def get_info(self) -> Info:
"""CTAP2 getInfo command.
:return: Information about the authenticator.
"""
return Info.from_dict(self.send_cbor(Ctap2.CMD.GET_INFO))
def client_pin(
self,
pin_uv_protocol: int,
sub_cmd: int,
key_agreement: Mapping[int, Any] | None = None,
pin_uv_param: bytes | None = None,
new_pin_enc: bytes | None = None,
pin_hash_enc: bytes | None = None,
permissions: int | None = None,
permissions_rpid: str | None = None,
*,
event: Event | None = None,
on_keepalive: Callable[[int], None] | None = None,
) -> Mapping[int, Any]:
"""CTAP2 clientPin command, used for various PIN operations.
This method is not intended to be called directly. It is intended to be used by
an instance of the PinProtocolV1 class.
:param pin_uv_protocol: The PIN/UV protocol version to use.
:param sub_cmd: A clientPin sub command.
:param key_agreement: The keyAgreement parameter.
:param pin_uv_param: The pinAuth parameter.
:param new_pin_enc: The newPinEnc parameter.
:param pin_hash_enc: The pinHashEnc parameter.
:param permissions: The permissions parameter.
:param permissions_rpid: The permissions RPID parameter.
:param event: Optional threading.Event used to cancel the request.
:param on_keepalive: Optional callback function to handle keep-alive
messages from the authenticator.
:return: The response of the command, decoded.
"""
return self.send_cbor(
Ctap2.CMD.CLIENT_PIN,
args(
pin_uv_protocol,
sub_cmd,
key_agreement,
pin_uv_param,
new_pin_enc,
pin_hash_enc,
None,
None,
permissions,
permissions_rpid,
),
event=event,
on_keepalive=on_keepalive,
)
def reset(
self,
*,
event: Event | None = None,
on_keepalive: Callable[[int], None] | None = None,
) -> None:
"""CTAP2 reset command, erases all credentials and PIN.
:param event: Optional threading.Event object used to cancel the request.
:param on_keepalive: Optional callback function to handle keep-alive
messages from the authenticator.
"""
self.send_cbor(Ctap2.CMD.RESET, event=event, on_keepalive=on_keepalive)
logger.info("Reset completed - All data erased")
def make_credential(
self,
client_data_hash: bytes,
rp: Mapping[str, Any],
user: Mapping[str, Any],
key_params: list[Mapping[str, Any]],
exclude_list: list[Mapping[str, Any]] | None = None,
extensions: Mapping[str, Any] | None = None,
options: Mapping[str, Any] | None = None,
pin_uv_param: bytes | None = None,
pin_uv_protocol: int | None = None,
enterprise_attestation: int | None = None,
*,
event: Event | None = None,
on_keepalive: Callable[[int], None] | None = None,
) -> AttestationResponse:
"""CTAP2 makeCredential operation.
:param client_data_hash: SHA256 hash of the ClientData.
:param rp: PublicKeyCredentialRpEntity parameters.
:param user: PublicKeyCredentialUserEntity parameters.
:param key_params: List of acceptable credential types.
:param exclude_list: Optional list of PublicKeyCredentialDescriptors.
:param extensions: Optional dict of extensions.
: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.
:return: The new credential.
"""
logger.debug("Calling CTAP2 make_credential")
return AttestationResponse.from_dict(
self.send_cbor(
Ctap2.CMD.MAKE_CREDENTIAL,
args(
client_data_hash,
rp,
user,
key_params,
exclude_list,
extensions,
options,
pin_uv_param,
pin_uv_protocol,
enterprise_attestation,
),
event=event,
on_keepalive=on_keepalive,
)
)
def get_assertion(
self,
rp_id: str,
client_data_hash: bytes,
allow_list: list[Mapping[str, Any]] | None = None,
extensions: Mapping[str, Any] | None = None,
options: Mapping[str, Any] | None = None,
pin_uv_param: bytes | None = None,
pin_uv_protocol: int | None = None,
*,
event: Event | None = None,
on_keepalive: Callable[[int], None] | None = None,
) -> AssertionResponse:
"""CTAP2 getAssertion command.
:param rp_id: The RP ID of the credential.
:param client_data_hash: SHA256 hash of the ClientData used.
:param allow_list: Optional list of PublicKeyCredentialDescriptors.
:param extensions: Optional dict of extensions.
: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 event: Optional threading.Event used to cancel the request.
:param on_keepalive: Optional callback function to handle keep-alive messages
from the authenticator.
:return: The new assertion.
"""
logger.debug("Calling CTAP2 get_assertion")
return AssertionResponse.from_dict(
self.send_cbor(
Ctap2.CMD.GET_ASSERTION,
args(
rp_id,
client_data_hash,
allow_list,
extensions,
options,
pin_uv_param,
pin_uv_protocol,
),
event=event,
on_keepalive=on_keepalive,
)
)
def get_next_assertion(self) -> AssertionResponse:
"""CTAP2 getNextAssertion command.
:return: The next available assertion response.
"""
return AssertionResponse.from_dict(self.send_cbor(Ctap2.CMD.GET_NEXT_ASSERTION))
def get_assertions(self, *args, **kwargs) -> list[AssertionResponse]:
"""Convenience method to get list of assertions.
See get_assertion and get_next_assertion for details.
"""
first = self.get_assertion(*args, **kwargs)
rest = [
self.get_next_assertion()
for _ in range(1, first.number_of_credentials or 1)
]
return [first] + rest
def credential_mgmt(
self,
sub_cmd: int,
sub_cmd_params: Mapping[int, Any] | None = None,
pin_uv_protocol: int | None = None,
pin_uv_param: bytes | None = None,
) -> Mapping[int, Any]:
"""CTAP2 credentialManagement command, used to manage resident
credentials.
NOTE: This implements the current draft version of the CTAP2 specification and
should be considered highly experimental.
This method is not intended to be called directly. It is intended to be used by
an instance of the CredentialManagement class.
:param sub_cmd: A CredentialManagement sub command.
:param sub_cmd_params: Sub command specific parameters.
:param pin_uv_protocol: PIN/UV auth protocol version used.
:param pin_uv_param: PIN/UV Auth parameter.
"""
if "credMgmt" in self.info.options:
cmd = Ctap2.CMD.CREDENTIAL_MGMT
elif "credentialMgmtPreview" in self.info.options:
cmd = Ctap2.CMD.CREDENTIAL_MGMT_PRE
else:
raise ValueError(
"Credential Management not supported by this Authenticator"
)
return self.send_cbor(
cmd,
args(sub_cmd, sub_cmd_params, pin_uv_protocol, pin_uv_param),
)
def bio_enrollment(
self,
modality: int | None = None,
sub_cmd: int | None = None,
sub_cmd_params: Mapping[int, Any] | None = None,
pin_uv_protocol: int | None = None,
pin_uv_param: bytes | None = None,
get_modality: bool | None = None,
*,
event: Event | None = None,
on_keepalive: Callable[[int], None] | None = None,
) -> Mapping[int, Any]:
"""CTAP2 bio enrollment command. Used to provision/enumerate/delete bio
enrollments in the authenticator.
NOTE: This implements the current draft version of the CTAP2 specification and
should be considered highly experimental.
This method is not intended to be called directly. It is intended to be used by
an instance of the BioEnrollment class.
:param modality: The user verification modality being used.
:param sub_cmd: A BioEnrollment sub command.
:param sub_cmd_params: Sub command specific parameters.
:param pin_uv_protocol: PIN/UV protocol version used.
:param pin_uv_param: PIN/UV auth param.
:param get_modality: Get the user verification type modality.
"""
if "bioEnroll" in self.info.options:
cmd = Ctap2.CMD.BIO_ENROLLMENT
elif "userVerificationMgmtPreview" in self.info.options:
cmd = Ctap2.CMD.BIO_ENROLLMENT_PRE
else:
raise ValueError("Authenticator does not support Bio Enroll")
return self.send_cbor(
cmd,
args(
modality,
sub_cmd,
sub_cmd_params,
pin_uv_protocol,
pin_uv_param,
get_modality,
),
event=event,
on_keepalive=on_keepalive,
)
def selection(
self,
*,
event: Event | None = None,
on_keepalive: Callable[[int], None] | None = None,
) -> None:
"""CTAP2 authenticator selection command.
This command allows the platform to let a user select a certain authenticator
by asking for user presence.
:param event: Optional threading.Event used to cancel the request.
:param on_keepalive: Optional callback function to handle keep-alive messages
from the authenticator.
"""
self.send_cbor(Ctap2.CMD.SELECTION, event=event, on_keepalive=on_keepalive)
def large_blobs(
self,
offset: int,
get: int | None = None,
set: bytes | None = None,
length: int | None = None,
pin_uv_param: bytes | None = None,
pin_uv_protocol: int | None = None,
) -> Mapping[int, Any]:
"""CTAP2 authenticator large blobs command.
This command is used to read and write the large blob array.
This method is not intended to be called directly. It is intended to be used by
an instance of the LargeBlobs class.
:param offset: The offset of where to start reading/writing data.
:param get: Optional (max) length of data to read.
:param set: Optional data to write.
:param length: Length of the payload in set.
:param pin_uv_protocol: PIN/UV protocol version used.
:param pin_uv_param: PIN/UV auth param.
"""
return self.send_cbor(
Ctap2.CMD.LARGE_BLOBS,
args(get, set, offset, length, pin_uv_param, pin_uv_protocol),
)
def config(
self,
sub_cmd: int,
sub_cmd_params: Mapping[int, Any] | None = None,
pin_uv_protocol: int | None = None,
pin_uv_param: bytes | None = None,
) -> Mapping[int, Any]:
"""CTAP2 authenticator config command.
This command is used to configure various authenticator features through the
use of its subcommands.
This method is not intended to be called directly. It is intended to be used by
an instance of the Config class.
:param sub_cmd: A Config sub command.
:param sub_cmd_params: Sub command specific parameters.
:param pin_uv_protocol: PIN/UV auth protocol version used.
:param pin_uv_param: PIN/UV Auth parameter.
"""
return self.send_cbor(
Ctap2.CMD.CONFIG,
args(sub_cmd, sub_cmd_params, pin_uv_protocol, pin_uv_param),
)
fido2-2.2.1/fido2/ctap2/bio.py 0000644 0000000 0000000 00000030653 15220526030 012606 0 ustar 00 # Copyright (c) 2020 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 __future__ import annotations
import logging
import struct
from enum import IntEnum, unique
from threading import Event
from typing import Any, Callable, Mapping
from .. import cbor
from ..ctap import CtapError
from .base import Ctap2, Info
from .pin import PinProtocol
logger = logging.getLogger(__name__)
class BioEnrollment:
@unique
class RESULT(IntEnum):
MODALITY = 0x01
FINGERPRINT_KIND = 0x02
MAX_SAMPLES_REQUIRED = 0x03
TEMPLATE_ID = 0x04
LAST_SAMPLE_STATUS = 0x05
REMAINING_SAMPLES = 0x06
TEMPLATE_INFOS = 0x07
MAX_TEMPLATE_FRIENDLY_NAME = 0x08
@unique
class TEMPLATE_INFO(IntEnum):
ID = 0x01
NAME = 0x02
@unique
class MODALITY(IntEnum):
FINGERPRINT = 0x01
@staticmethod
def is_supported(info: Info) -> bool:
if "bioEnroll" in info.options:
return True
# We also support the Prototype command
if (
"FIDO_2_1_PRE" in info.versions
and "userVerificationMgmtPreview" in info.options
):
return True
return False
def __init__(self, ctap: Ctap2, modality: MODALITY):
if not self.is_supported(ctap.info):
raise ValueError("Authenticator does not support BioEnroll")
self.ctap = ctap
self.modality = self.get_modality()
if modality != self.modality:
raise ValueError(f"Device does not support {modality:s}")
def get_modality(self) -> int:
"""Get bio modality.
:return: The type of modality supported by the authenticator.
"""
return self.ctap.bio_enrollment(get_modality=True)[
BioEnrollment.RESULT.MODALITY
]
class CaptureError(Exception):
def __init__(self, code: int):
self.code = code
super().__init__(f"Fingerprint capture error: {code}")
class FPEnrollmentContext:
"""Helper object to perform fingerprint enrollment.
:param bio: An instance of FPBioEnrollment.
:param timeout: Optional timeout for fingerprint captures (ms).
:ivar remaining: The number of (estimated) remaining samples needed.
:ivar template_id: The ID of the new template (only available after the initial
sample has been captured).
"""
def __init__(self, bio: "FPBioEnrollment", timeout: int | None = None):
self._bio = bio
self.timeout = timeout
self.template_id: bytes | None = None
self.remaining: int | None = None
def capture(
self,
event: Event | None = None,
on_keepalive: Callable[[int], None] | None = None,
) -> bytes | None:
"""Capture a fingerprint sample.
This call will block for up to timeout milliseconds (or indefinitely, if
timeout not specified) waiting for the user to scan their fingerprint to
collect one sample.
:return: None, if more samples are needed, or the template ID if enrollment is
completed.
"""
if self.template_id is None:
self.template_id, status, self.remaining = self._bio.enroll_begin(
self.timeout, event, on_keepalive
)
else:
status, self.remaining = self._bio.enroll_capture_next(
self.template_id, self.timeout, event, on_keepalive
)
if status != FPBioEnrollment.FEEDBACK.FP_GOOD:
raise CaptureError(status)
if self.remaining == 0:
return self.template_id
return None
def cancel(self) -> None:
"""Cancels ongoing enrollment."""
self._bio.enroll_cancel()
self.template_id = None
class FPBioEnrollment(BioEnrollment):
"""Implementation of the bio enrollment API.
NOTE: The get_fingerprint_sensor_info method does not require authentication, and
can be used by setting pin_uv_protocol and pin_uv_token to None.
:param ctap: An instance of a CTAP2 object.
:param pin_uv_protocol: The PIN/UV protocol version used.
:param pin_uv_token: A valid PIN/UV Auth Token for the current CTAP session.
"""
@unique
class CMD(IntEnum):
ENROLL_BEGIN = 0x01
ENROLL_CAPTURE_NEXT = 0x02
ENROLL_CANCEL = 0x03
ENUMERATE_ENROLLMENTS = 0x04
SET_NAME = 0x05
REMOVE_ENROLLMENT = 0x06
GET_SENSOR_INFO = 0x07
@unique
class PARAM(IntEnum):
TEMPLATE_ID = 0x01
TEMPLATE_NAME = 0x02
TIMEOUT_MS = 0x03
@unique
class FEEDBACK(IntEnum):
FP_GOOD = 0x00
FP_TOO_HIGH = 0x01
FP_TOO_LOW = 0x02
FP_TOO_LEFT = 0x03
FP_TOO_RIGHT = 0x04
FP_TOO_FAST = 0x05
FP_TOO_SLOW = 0x06
FP_POOR_QUALITY = 0x07
FP_TOO_SKEWED = 0x08
FP_TOO_SHORT = 0x09
FP_MERGE_FAILURE = 0x0A
FP_EXISTS = 0x0B
FP_DATABASE_FULL = 0x0C
NO_USER_ACTIVITY = 0x0D
NO_UP_TRANSITION = 0x0E
def __str__(self):
return f"0x{self.value:02X} - {self.name}"
def __init__(self, ctap: Ctap2, pin_uv_protocol: PinProtocol, pin_uv_token: bytes):
super().__init__(ctap, BioEnrollment.MODALITY.FINGERPRINT)
self.pin_uv_protocol = pin_uv_protocol
self.pin_uv_token = pin_uv_token
def _call(self, sub_cmd, params=None, auth=True, event=None, on_keepalive=None):
kwargs: dict[str, Any] = {
"modality": self.modality,
"sub_cmd": sub_cmd,
"sub_cmd_params": params,
"event": event,
"on_keepalive": on_keepalive,
}
if auth:
msg = struct.pack(">BB", self.modality, sub_cmd)
if params is not None:
msg += cbor.encode(params)
kwargs["pin_uv_protocol"] = self.pin_uv_protocol.VERSION
kwargs["pin_uv_param"] = self.pin_uv_protocol.authenticate(
self.pin_uv_token, msg
)
return self.ctap.bio_enrollment(**kwargs)
def get_fingerprint_sensor_info(self) -> Mapping[int, Any]:
"""Get fingerprint sensor info.
:return: A dict containing FINGERPRINT_KIND and MAX_SAMPLES_REQUIRES.
"""
return self._call(FPBioEnrollment.CMD.GET_SENSOR_INFO, auth=False)
def enroll_begin(
self,
timeout: int | None = None,
event: Event | None = None,
on_keepalive: Callable[[int], None] | None = None,
) -> tuple[bytes, FPBioEnrollment.FEEDBACK, int]:
"""Start fingerprint enrollment.
Starts the process of enrolling a new fingerprint, and will wait for the user
to scan their fingerprint once to provide an initial sample.
:param timeout: Optional timeout in milliseconds.
:return: A tuple containing the new template ID, the sample status, and the
number of samples remaining to complete the enrollment.
"""
logger.debug(f"Starting fingerprint enrollment (timeout={timeout})")
result = self._call(
FPBioEnrollment.CMD.ENROLL_BEGIN,
(
{FPBioEnrollment.PARAM.TIMEOUT_MS: timeout}
if timeout is not None
else None
),
event=event,
on_keepalive=on_keepalive,
)
logger.debug(f"Sample capture result: {result}")
return (
result[BioEnrollment.RESULT.TEMPLATE_ID],
FPBioEnrollment.FEEDBACK(result[BioEnrollment.RESULT.LAST_SAMPLE_STATUS]),
result[BioEnrollment.RESULT.REMAINING_SAMPLES],
)
def enroll_capture_next(
self,
template_id: bytes,
timeout: int | None = None,
event: Event | None = None,
on_keepalive: Callable[[int], None] | None = None,
) -> tuple[FPBioEnrollment.FEEDBACK, int]:
"""Continue fingerprint enrollment.
Continues enrolling a new fingerprint and will wait for the user to scan their
fingerpring once to provide a new sample.
Once the number of samples remaining is 0, the enrollment is completed.
:param template_id: The template ID returned by a call to `enroll_begin`.
:param timeout: Optional timeout in milliseconds.
:return: A tuple containing the sample status, and the number of samples
remaining to complete the enrollment.
"""
logger.debug(f"Capturing next sample with (timeout={timeout})")
params: dict[int, Any] = {FPBioEnrollment.PARAM.TEMPLATE_ID: template_id}
if timeout is not None:
params[FPBioEnrollment.PARAM.TIMEOUT_MS] = timeout
result = self._call(
FPBioEnrollment.CMD.ENROLL_CAPTURE_NEXT,
params,
event=event,
on_keepalive=on_keepalive,
)
logger.debug(f"Sample capture result: {result}")
return (
FPBioEnrollment.FEEDBACK(result[BioEnrollment.RESULT.LAST_SAMPLE_STATUS]),
result[BioEnrollment.RESULT.REMAINING_SAMPLES],
)
def enroll_cancel(self) -> None:
"""Cancel any ongoing fingerprint enrollment."""
logger.debug("Cancelling fingerprint enrollment.")
self._call(FPBioEnrollment.CMD.ENROLL_CANCEL, auth=False)
def enroll(self, timeout: int | None = None) -> FPEnrollmentContext:
"""Convenience wrapper for doing fingerprint enrollment.
See FPEnrollmentContext for details.
:return: An initialized FPEnrollmentContext.
"""
return FPEnrollmentContext(self, timeout)
def enumerate_enrollments(self) -> Mapping[bytes, str | None]:
"""Get a dict of enrolled fingerprint templates which maps template ID's to
their friendly names.
:return: A dict of enrolled template_id -> name pairs.
"""
try:
return {
t[BioEnrollment.TEMPLATE_INFO.ID]: t[BioEnrollment.TEMPLATE_INFO.NAME]
for t in self._call(FPBioEnrollment.CMD.ENUMERATE_ENROLLMENTS)[
BioEnrollment.RESULT.TEMPLATE_INFOS
]
}
except CtapError as e:
if e.code == CtapError.ERR.INVALID_OPTION:
return {}
raise
def set_name(self, template_id: bytes, name: str) -> None:
"""Set/Change the friendly name of a previously enrolled fingerprint template.
:param template_id: The ID of the template to change.
:param name: A friendly name to give the template.
"""
logger.debug(f"Changing name of template: {template_id.hex()} to {name}")
self._call(
FPBioEnrollment.CMD.SET_NAME,
{
BioEnrollment.TEMPLATE_INFO.ID: template_id,
BioEnrollment.TEMPLATE_INFO.NAME: name,
},
)
logger.info("Fingerprint template renamed")
def remove_enrollment(self, template_id: bytes) -> None:
"""Remove a previously enrolled fingerprint template.
:param template_id: The Id of the template to remove.
"""
logger.debug(f"Deleting template: {template_id.hex()}")
self._call(
FPBioEnrollment.CMD.REMOVE_ENROLLMENT,
{BioEnrollment.TEMPLATE_INFO.ID: template_id},
)
logger.info("Fingerprint template deleted")
fido2-2.2.1/fido2/ctap2/blob.py 0000644 0000000 0000000 00000016302 15220526030 012746 0 ustar 00 # Copyright (c) 2020 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 __future__ import annotations
import os
import struct
import zlib
from typing import Any, Mapping, Sequence, cast
from cryptography.exceptions import InvalidTag
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from .. import cbor
from ..utils import sha256
from .base import Ctap2, Info
from .pin import PinProtocol, _PinUv
def _compress(data):
o = zlib.compressobj(wbits=-zlib.MAX_WBITS)
return o.compress(data) + o.flush()
def _decompress(data):
o = zlib.decompressobj(wbits=-zlib.MAX_WBITS)
return o.decompress(data) + o.flush()
def _lb_ad(orig_size):
return b"blob" + struct.pack(" bool:
return info.options.get("largeBlobs") is True
def __init__(
self,
ctap: Ctap2,
pin_uv_protocol: PinProtocol | None = None,
pin_uv_token: bytes | None = None,
):
if not self.is_supported(ctap.info):
raise ValueError("Authenticator does not support LargeBlobs")
self.ctap = ctap
self.max_fragment_length = self.ctap.info.max_msg_size - 64
self.pin_uv = (
_PinUv(pin_uv_protocol, pin_uv_token)
if pin_uv_protocol and pin_uv_token
else None
)
def read_blob_array(self) -> Sequence[Mapping[int, Any]]:
"""Gets the entire contents of the Large Blobs array.
:return: The CBOR decoded list of Large Blobs.
"""
offset = 0
buf = b""
while True:
fragment = self.ctap.large_blobs(offset, get=self.max_fragment_length)[1]
buf += fragment
if len(fragment) < self.max_fragment_length:
break
offset += self.max_fragment_length
data, check = buf[:-16], buf[-16:]
if check != sha256(data)[:-16]:
return []
return cast(Sequence[Mapping[int, Any]], cbor.decode(data))
def write_blob_array(self, blob_array: Sequence[Mapping[int, Any]]) -> None:
"""Writes the entire Large Blobs array.
:param blob_array: A list to write to the Authenticator.
"""
if not isinstance(blob_array, list):
raise TypeError("large-blob array must be a list")
data = cbor.encode(blob_array)
data += sha256(data)[:16]
offset = 0
size = len(data)
while offset < size:
ln = min(size - offset, self.max_fragment_length)
_set = data[offset : offset + ln]
if self.pin_uv:
msg = (
b"\xff" * 32
+ b"\x0c\x00"
+ struct.pack(" bytes | None:
"""Gets the Large Blob stored for a single credential.
:param large_blob_key: The largeBlobKey for the credential, or None.
:returns: The decrypted and deflated value stored for the credential.
"""
for entry in self.read_blob_array():
try:
compressed, orig_size = _lb_unpack(large_blob_key, entry)
decompressed = _decompress(compressed)
if len(decompressed) == orig_size:
return decompressed
except (ValueError, zlib.error):
continue
return None
def put_blob(self, large_blob_key: bytes, data: bytes | None) -> None:
"""Stores a Large Blob for a single credential.
Any existing entries for the same credential will be replaced.
:param large_blob_key: The largeBlobKey for the credential.
:param data: The data to compress, encrypt and store.
"""
modified = data is not None
entries = []
for entry in self.read_blob_array():
try:
_lb_unpack(large_blob_key, entry)
modified = True
except ValueError:
entries.append(entry)
if data is not None:
entries.append(_lb_pack(large_blob_key, data))
if modified:
self.write_blob_array(entries)
def delete_blob(self, large_blob_key: bytes) -> None:
"""Deletes any Large Blob(s) stored for a single credential.
:param large_blob_key: The largeBlobKey for the credential.
"""
self.put_blob(large_blob_key, None)
fido2-2.2.1/fido2/ctap2/config.py 0000644 0000000 0000000 00000012416 15220526030 013277 0 ustar 00 # Copyright (c) 2020 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 __future__ import annotations
import struct
from enum import IntEnum, unique
from typing import Any
from .. import cbor
from .base import Ctap2, Info
from .pin import PinProtocol, _PinUv
class Config:
"""Implementation of the CTAP2.1 Authenticator Config API.
:param ctap: An instance of a CTAP2 object.
:param pin_uv_protocol: An instance of a PinUvAuthProtocol.
:param pin_uv_token: A valid PIN/UV Auth Token for the current CTAP session.
"""
@unique
class CMD(IntEnum):
ENABLE_ENTERPRISE_ATT = 0x01
TOGGLE_ALWAYS_UV = 0x02
SET_MIN_PIN_LENGTH = 0x03
VENDOR_PROTOTYPE = 0xFF
@unique
class PARAM(IntEnum):
NEW_MIN_PIN_LENGTH = 0x01
MIN_PIN_LENGTH_RPIDS = 0x02
FORCE_CHANGE_PIN = 0x03
PIN_COMPLEXITY_POLICY = 0x04
@staticmethod
def is_supported(info: Info) -> bool:
return info.options.get("authnrCfg") is True
def __init__(
self,
ctap: Ctap2,
pin_uv_protocol: PinProtocol | None = None,
pin_uv_token: bytes | None = None,
):
if not self.is_supported(ctap.info):
raise ValueError("Authenticator does not support Config")
self.ctap = ctap
self.pin_uv = (
_PinUv(pin_uv_protocol, pin_uv_token)
if pin_uv_protocol and pin_uv_token
else None
)
self._subcommands = self.ctap.info.authenticator_config_commands
def _call(self, sub_cmd, params=None):
if self._subcommands is not None and sub_cmd not in self._subcommands:
raise ValueError(f"Config command {sub_cmd} not supported by Authenticator")
if self.pin_uv:
msg = b"\xff" * 32 + b"\x0d" + struct.pack(" None:
"""Enables Enterprise Attestation.
If already enabled, this command is ignored.
"""
self._call(Config.CMD.ENABLE_ENTERPRISE_ATT)
def toggle_always_uv(self) -> None:
"""Toggle the alwaysUV setting.
When true, the Authenticator always requires UV for credential assertion.
"""
self._call(Config.CMD.TOGGLE_ALWAYS_UV)
def set_min_pin_length(
self,
min_pin_length: int | None = None,
rp_ids: list[str] | None = None,
force_change_pin: bool = False,
pin_complexity_policy: bool = False,
) -> None:
"""Set the minimum PIN length allowed when setting/changing the PIN.
:param min_pin_length: The minimum PIN length the Authenticator should allow.
:param rp_ids: A list of RP IDs which should be allowed to get the current
minimum PIN length.
:param force_change_pin: True if the Authenticator should enforce changing the
PIN before the next use.
:param pin_complexity_policy: True if the Authenticator should enforce an
additional PIN complexity policy beyond minPINLength.
"""
params: dict[int, Any] = {Config.PARAM.FORCE_CHANGE_PIN: force_change_pin}
if min_pin_length is not None:
params[Config.PARAM.NEW_MIN_PIN_LENGTH] = min_pin_length
if rp_ids is not None:
params[Config.PARAM.MIN_PIN_LENGTH_RPIDS] = rp_ids
if pin_complexity_policy:
if self.ctap.info.pin_complexity_policy is None:
raise ValueError(
"Authenticator does not support setting PIN complexity policy"
)
params[Config.PARAM.PIN_COMPLEXITY_POLICY] = True
self._call(Config.CMD.SET_MIN_PIN_LENGTH, params)
fido2-2.2.1/fido2/ctap2/credman.py 0000644 0000000 0000000 00000022573 15220526030 013450 0 ustar 00 # Copyright (c) 2020 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 __future__ import annotations
import logging
import struct
from enum import IntEnum, unique
from typing import Any, Mapping, Sequence
from .. import cbor
from ..ctap import CtapError
from ..webauthn import (
PublicKeyCredentialDescriptor,
PublicKeyCredentialUserEntity,
_as_cbor,
)
from .base import Ctap2, Info
from .pin import PinProtocol, _PinUv
logger = logging.getLogger(__name__)
class CredentialManagement:
"""Implementation of a draft specification of the Credential Management API.
WARNING: This specification is not final and this class is likely to change.
:param ctap: An instance of a CTAP2 object.
:param pin_uv_protocol: An instance of a PinUvAuthProtocol.
:param pin_uv_token: A valid PIN/UV Auth Token for the current CTAP session.
"""
@unique
class CMD(IntEnum):
GET_CREDS_METADATA = 0x01
ENUMERATE_RPS_BEGIN = 0x02
ENUMERATE_RPS_NEXT = 0x03
ENUMERATE_CREDS_BEGIN = 0x04
ENUMERATE_CREDS_NEXT = 0x05
DELETE_CREDENTIAL = 0x06
UPDATE_USER_INFO = 0x07
@unique
class PARAM(IntEnum):
RP_ID_HASH = 0x01
CREDENTIAL_ID = 0x02
USER = 0x03
@unique
class RESULT(IntEnum):
EXISTING_CRED_COUNT = 0x01
MAX_REMAINING_COUNT = 0x02
RP = 0x03
RP_ID_HASH = 0x04
TOTAL_RPS = 0x05
USER = 0x06
CREDENTIAL_ID = 0x07
PUBLIC_KEY = 0x08
TOTAL_CREDENTIALS = 0x09
CRED_PROTECT = 0x0A
LARGE_BLOB_KEY = 0x0B
THIRD_PARTY_PAYMENT = 0x0C
@staticmethod
def is_supported(info: Info) -> bool:
if info.options.get("credMgmt"):
return True
# We also support the Prototype command
if "FIDO_2_1_PRE" in info.versions and "credentialMgmtPreview" in info.options:
return True
return False
@staticmethod
def is_update_supported(info: Info) -> bool:
# Not supported in credentialMgmtPreview
return bool(info.options.get("credMgmt"))
@staticmethod
def is_readonly_supported(info: Info) -> bool:
return bool(info.options.get("perCredMgmtRO"))
def __init__(
self,
ctap: Ctap2,
pin_uv_protocol: PinProtocol,
pin_uv_token: bytes,
):
if not self.is_supported(ctap.info):
raise ValueError("Authenticator does not support Credential Management")
self.ctap = ctap
self.pin_uv = _PinUv(pin_uv_protocol, pin_uv_token)
def _call(self, sub_cmd, params=None, auth=True):
kwargs: dict[str, Any] = {"sub_cmd": sub_cmd, "sub_cmd_params": params}
if auth:
msg = struct.pack(">B", sub_cmd)
if params is not None:
msg += cbor.encode(params)
kwargs["pin_uv_protocol"] = self.pin_uv.protocol.VERSION
kwargs["pin_uv_param"] = self.pin_uv.protocol.authenticate(
self.pin_uv.token, msg
)
return self.ctap.credential_mgmt(**kwargs)
def get_metadata(self) -> Mapping[int, Any]:
"""Get credentials metadata.
This returns the existing resident credentials count, and the max
possible number of remaining resident credentials (the actual number of
remaining credentials may depend on algorithm choice, etc).
:return: A dict containing EXISTING_CRED_COUNT, and MAX_REMAINING_COUNT.
"""
return self._call(CredentialManagement.CMD.GET_CREDS_METADATA)
def enumerate_rps_begin(self) -> Mapping[int, Any]:
"""Start enumeration of RP entities of resident credentials.
This will begin enumeration of stored RP entities, returning the first
entity, as well as a count of the total number of entities stored.
:return: A dict containing RP, RP_ID_HASH, and TOTAL_RPS.
"""
return self._call(CredentialManagement.CMD.ENUMERATE_RPS_BEGIN)
def enumerate_rps_next(self) -> Mapping[int, Any]:
"""Get the next RP entity stored.
This continues enumeration of stored RP entities, returning the next
entity.
:return: A dict containing RP, and RP_ID_HASH.
"""
return self._call(CredentialManagement.CMD.ENUMERATE_RPS_NEXT, auth=False)
def enumerate_rps(self) -> Sequence[Mapping[int, Any]]:
"""Convenience method to enumerate all RPs.
See enumerate_rps_begin and enumerate_rps_next for details.
"""
try:
first = self.enumerate_rps_begin()
except CtapError as e:
if e.code == CtapError.ERR.NO_CREDENTIALS:
return []
raise # Other error
n_rps = first[CredentialManagement.RESULT.TOTAL_RPS]
if n_rps == 0:
return []
rest = [self.enumerate_rps_next() for _ in range(1, n_rps)]
return [first] + rest
def enumerate_creds_begin(self, rp_id_hash: bytes) -> Mapping[int, Any]:
"""Start enumeration of resident credentials.
This will begin enumeration of resident credentials for a given RP,
returning the first credential, as well as a count of the total number
of resident credentials stored for the given RP.
:param rp_id_hash: SHA256 hash of the RP ID.
:return: A dict containing USER, CREDENTIAL_ID, PUBLIC_KEY, and
TOTAL_CREDENTIALS.
"""
return self._call(
CredentialManagement.CMD.ENUMERATE_CREDS_BEGIN,
{CredentialManagement.PARAM.RP_ID_HASH: rp_id_hash},
)
def enumerate_creds_next(self) -> Mapping[int, Any]:
"""Get the next resident credential stored.
This continues enumeration of resident credentials, returning the next
credential.
:return: A dict containing USER, CREDENTIAL_ID, and PUBLIC_KEY.
"""
return self._call(CredentialManagement.CMD.ENUMERATE_CREDS_NEXT, auth=False)
def enumerate_creds(self, *args, **kwargs) -> Sequence[Mapping[int, Any]]:
"""Convenience method to enumerate all resident credentials for an RP.
See enumerate_creds_begin and enumerate_creds_next for details.
"""
try:
first = self.enumerate_creds_begin(*args, **kwargs)
except CtapError as e:
if e.code == CtapError.ERR.NO_CREDENTIALS:
return []
raise # Other error
rest = [
self.enumerate_creds_next()
for _ in range(
1, first.get(CredentialManagement.RESULT.TOTAL_CREDENTIALS, 1)
)
]
return [first] + rest
def delete_cred(self, cred_id: PublicKeyCredentialDescriptor) -> None:
"""Delete a resident credential.
:param cred_id: The PublicKeyCredentialDescriptor of the credential to delete.
"""
cred_id = PublicKeyCredentialDescriptor.from_dict(cred_id)
logger.debug(f"Deleting credential with ID: {cred_id}")
self._call(
CredentialManagement.CMD.DELETE_CREDENTIAL,
{CredentialManagement.PARAM.CREDENTIAL_ID: _as_cbor(cred_id)},
)
logger.info("Credential deleted")
def update_user_info(
self,
cred_id: PublicKeyCredentialDescriptor,
user_info: PublicKeyCredentialUserEntity,
) -> None:
"""Update the user entity of a resident key.
:param cred_id: The PublicKeyCredentialDescriptor of the credential to update.
:param user_info: The user info update.
"""
if not CredentialManagement.is_update_supported(self.ctap.info):
raise ValueError("Authenticator does not support update_user_info")
cred_id = PublicKeyCredentialDescriptor.from_dict(cred_id)
user_info = PublicKeyCredentialUserEntity.from_dict(user_info)
logger.debug(f"Updating credential: {cred_id} with user info: {user_info}")
self._call(
CredentialManagement.CMD.UPDATE_USER_INFO,
{
CredentialManagement.PARAM.CREDENTIAL_ID: _as_cbor(cred_id),
CredentialManagement.PARAM.USER: _as_cbor(user_info),
},
)
logger.info("Credential user info updated")
fido2-2.2.1/fido2/ctap2/extensions.py 0000644 0000000 0000000 00000072051 15220526030 014232 0 ustar 00 # Copyright (c) 2020 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 __future__ import annotations
import abc
from dataclasses import dataclass
from enum import Enum, unique
from typing import Any, Mapping, Sequence, cast
from .. import cbor
from ..utils import _JsonDataObject, sha256, websafe_encode
from ..webauthn import (
AttestationObject,
AuthenticatorSelectionCriteria,
PublicKeyCredentialCreationOptions,
PublicKeyCredentialDescriptor,
PublicKeyCredentialRequestOptions,
ResidentKeyRequirement,
UserVerificationRequirement,
)
from .base import AssertionResponse, AttestationResponse, Ctap2
from .blob import LargeBlobs
from .pin import ClientPin, PinProtocol
class ExtensionProcessor(abc.ABC):
"""Base class for CTAP2 extension processing.
See: :class:`RegistrationExtensionProcessor` and
:class:`AuthenticationExtensionProcessor`.
"""
def __init__(
self,
permissions: ClientPin.PERMISSION = ClientPin.PERMISSION(0),
inputs: dict[str, Any] | None = None,
outputs: dict[str, Any] | None = None,
):
self.permissions = permissions
self._inputs = inputs
self._outputs = outputs
class RegistrationExtensionProcessor(ExtensionProcessor):
"""Processing state for a CTAP2 extension, for single use.
The ExtensionProcessor holds state and logic for client processing of an extension,
for a registration (MakeCredential) call.
:param permissions: PinUvAuthToken permissions required by the extension.
:param inputs: Default authenticator inputs, if prepare_inputs is not overridden.
:param outputs: Default client outputs, if prepare_outputs is not overridden.
"""
def prepare_inputs(self, pin_token: bytes | None) -> dict[str, Any] | None:
"Prepare authenticator extension inputs, to be passed to the Authenenticator."
return self._inputs
def prepare_outputs(
self,
response: AttestationResponse,
pin_token: bytes | None,
) -> dict[str, Any] | None:
"Prepare client extension outputs, to be returned to the caller."
return self._outputs
class AuthenticationExtensionProcessor(ExtensionProcessor):
"""Processing state for a CTAP2 extension, for single use.
The ExtensionProcessor holds state and logic for client processing of an extension,
for an authentication (GetAssertion) call.
:param permissions: PinUvAuthToken permissions required by the extension.
:param inputs: Default authenticator inputs, if prepare_inputs is not overridden.
:param outputs: Default client outputs, if prepare_outputs is not overridden.
"""
def prepare_inputs(
self,
selected: PublicKeyCredentialDescriptor | None,
pin_token: bytes | None,
) -> dict[str, Any] | None:
"Prepare authenticator extension inputs, to be passed to the Authenenticator."
return self._inputs
def prepare_outputs(
self,
response: AssertionResponse,
pin_token: bytes | None,
) -> dict[str, Any] | None:
"Prepare client extension outputs, to be returned to the caller."
return self._outputs
class Ctap2Extension(abc.ABC):
"""Base class for CTAP2 extensions.
As of python-fido2 1.2 these instances can be used for multiple requests and
should be invoked via the make_credential and get_assertion methods.
Subclasses are instantiated for a single request, if the Authenticator supports
the extension.
"""
@abc.abstractmethod
def is_supported(self, ctap: Ctap2) -> bool:
"""Whether or not the extension is supported by the authenticator."""
def make_credential(
self,
ctap: Ctap2,
options: PublicKeyCredentialCreationOptions,
pin_protocol: PinProtocol | None,
) -> RegistrationExtensionProcessor | None:
"""Start client extension processing for registration."""
return None
def get_assertion(
self,
ctap: Ctap2,
options: PublicKeyCredentialRequestOptions,
pin_protocol: PinProtocol | None,
) -> AuthenticationExtensionProcessor | None:
"""Start client extension processing for authentication."""
return None
@dataclass(eq=False, frozen=True)
class HMACGetSecretInput(_JsonDataObject):
"""Client inputs for hmac-secret."""
salt1: bytes
salt2: bytes | None = None
@dataclass(eq=False, frozen=True)
class HMACGetSecretOutput(_JsonDataObject):
"""Client outputs for hmac-secret."""
output1: bytes
output2: bytes | None = None
def _prf_salt(secret):
return sha256(b"WebAuthn PRF\0" + secret)
@dataclass(eq=False, frozen=True)
class AuthenticatorExtensionsPRFValues(_JsonDataObject):
"""Salt values for use with prf."""
first: bytes
second: bytes | None = None
@dataclass(eq=False, frozen=True)
class AuthenticatorExtensionsPRFInputs(_JsonDataObject):
"""Client inputs for prf."""
eval: AuthenticatorExtensionsPRFValues | None = None
eval_by_credential: Mapping[str, AuthenticatorExtensionsPRFValues] | None = None
@dataclass(eq=False, frozen=True)
class AuthenticatorExtensionsPRFOutputs(_JsonDataObject):
"""Client outputs for prf."""
enabled: bool | None = None
results: AuthenticatorExtensionsPRFValues | None = None
def _hmac_prepare_salts(allow_list, selected, prf, hmac):
if prf:
secrets = prf.eval
by_creds = prf.eval_by_credential
if by_creds:
# Make sure all keys are valid IDs from allow_credentials
if not allow_list:
raise ValueError("evalByCredentials requires allowCredentials")
ids = {websafe_encode(c.id) for c in allow_list}
if not ids.issuperset(by_creds):
raise ValueError("evalByCredentials contains invalid key")
if selected:
key = websafe_encode(selected.id)
if key in by_creds:
secrets = by_creds[key]
if not secrets:
return
salts = (
_prf_salt(secrets.first),
(_prf_salt(secrets.second) if secrets.second is not None else b""),
)
elif hmac:
salts = hmac.salt1, hmac.salt2 or b""
else:
return
if not (
len(salts[0]) == HmacSecretExtension.SALT_LEN
and (not salts[1] or len(salts[1]) == HmacSecretExtension.SALT_LEN)
):
raise ValueError("Invalid salt length")
return salts
def _hmac_format_outputs(enabled, decrypted, prf):
output1 = decrypted[: HmacSecretExtension.SALT_LEN] if decrypted else None
output2 = decrypted[HmacSecretExtension.SALT_LEN :] if decrypted else None
if prf:
result = AuthenticatorExtensionsPRFOutputs(
enabled=enabled,
results=(
AuthenticatorExtensionsPRFValues(output1, output2) if output1 else None
),
)
# If result has no content, don't add an entry for it
return {"prf": result} if result else None
else:
outputs = {}
if enabled is not None:
outputs["hmacCreateSecret"] = enabled
if output1:
outputs["hmacGetSecret"] = HMACGetSecretOutput(output1, output2)
return outputs or None
class HmacSecretExtension(Ctap2Extension):
"""
Implements the Pseudo-random function (prf) and the hmac-secret CTAP2 extensions.
The hmac-secret extension is not directly available to clients by default, instead
the prf extension is used.
https://www.w3.org/TR/webauthn-3/#prf-extension
https://fidoalliance.org/specs/fido-v2.1-rd-20201208/fido-client-to-authenticator-protocol-v2.1-rd-20201208.html#sctn-hmac-secret-extension
:param allow_hmac_secret: Set to True to allow hmac-secret, in addition to prf.
"""
NAME = "hmac-secret"
MC_NAME = "hmac-secret-mc"
SALT_LEN = 32
def __init__(self, allow_hmac_secret=False):
self._allow_hmac_secret = allow_hmac_secret
def is_supported(self, ctap):
return self.NAME in ctap.info.extensions
def make_credential(self, ctap, options, pin_protocol):
c_inputs = options.extensions or {}
prf = c_inputs.get("prf") is not None
hmac = self._allow_hmac_secret and c_inputs.get("hmacCreateSecret") is True
if pin_protocol and self.is_supported(ctap) and (prf or hmac):
inputs: dict[str, Any] = {HmacSecretExtension.NAME: True}
shared_secret = None
if self.MC_NAME in ctap.info.extensions:
prf_salts = AuthenticatorExtensionsPRFInputs.from_dict(
cast(Mapping | None, c_inputs.get("prf"))
)
hmac_salts = (
HMACGetSecretInput.from_dict(
cast(Mapping | None, c_inputs.get("hmacGetSecret"))
)
if hmac
else None
)
salts = _hmac_prepare_salts(None, None, prf_salts, hmac_salts)
if salts:
client_pin = ClientPin(ctap, pin_protocol)
key_agreement, shared_secret = client_pin._get_shared_secret()
salt_enc = pin_protocol.encrypt(shared_secret, salts[0] + salts[1])
salt_auth = pin_protocol.authenticate(shared_secret, salt_enc)
inputs[HmacSecretExtension.MC_NAME] = {
1: key_agreement,
2: salt_enc,
3: salt_auth,
4: pin_protocol.VERSION,
}
class Processor(RegistrationExtensionProcessor):
def prepare_inputs(self, pin_token):
return inputs
def prepare_outputs(self, response, pin_token):
extensions = response.auth_data.extensions or {}
enabled = extensions.get(HmacSecretExtension.NAME, False)
value = extensions.get(HmacSecretExtension.MC_NAME)
decrypted = (
pin_protocol.decrypt(shared_secret, value)
if value and shared_secret
else None
)
return _hmac_format_outputs(enabled, decrypted, prf)
return Processor()
def get_assertion(self, ctap, options, pin_protocol):
inputs = options.extensions or {}
prf = AuthenticatorExtensionsPRFInputs.from_dict(
cast(Mapping | None, inputs.get("prf"))
)
hmac = (
HMACGetSecretInput.from_dict(
cast(Mapping | None, inputs.get("hmacGetSecret"))
)
if self._allow_hmac_secret
else None
)
if pin_protocol and self.is_supported(ctap) and (prf or hmac):
client_pin = ClientPin(ctap, pin_protocol)
key_agreement, shared_secret = client_pin._get_shared_secret()
class Processing(AuthenticationExtensionProcessor):
def prepare_inputs(self, selected, pin_token):
salts = _hmac_prepare_salts(
options.allow_credentials, selected, prf, hmac
)
if not salts:
return
salt_enc = pin_protocol.encrypt(shared_secret, salts[0] + salts[1])
salt_auth = pin_protocol.authenticate(shared_secret, salt_enc)
return {
HmacSecretExtension.NAME: {
1: key_agreement,
2: salt_enc,
3: salt_auth,
4: pin_protocol.VERSION,
}
}
def prepare_outputs(self, response, pin_token):
extensions = response.auth_data.extensions or {}
value = extensions.get(HmacSecretExtension.NAME)
decrypted = (
pin_protocol.decrypt(shared_secret, value) if value else None
)
return _hmac_format_outputs(None, decrypted, prf)
return Processing()
@dataclass(eq=False, frozen=True)
class AuthenticatorExtensionsLargeBlobInputs(_JsonDataObject):
"""Client inputs for largeBlob."""
support: str | None = None
read: bool | None = None
write: bytes | None = None
@dataclass(eq=False, frozen=True)
class AuthenticatorExtensionsLargeBlobOutputs(_JsonDataObject):
"""Client outputs for largeBlob."""
supported: bool | None = None
blob: bytes | None = None
written: bool | None = None
class LargeBlobExtension(Ctap2Extension):
"""
Implements the Large Blob storage (largeBlob) WebAuthn extension.
https://www.w3.org/TR/webauthn-3/#sctn-large-blob-extension
"""
NAME = "largeBlobKey"
def is_supported(self, ctap):
return self.NAME in ctap.info.extensions and ctap.info.options.get(
"largeBlobs", False
)
def make_credential(self, ctap, options, pin_protocol):
inputs = options.extensions or {}
data = AuthenticatorExtensionsLargeBlobInputs.from_dict(
cast(Mapping | None, inputs.get("largeBlob"))
)
if data:
if data.read or data.write:
raise ValueError("Invalid set of parameters")
if data.support == "required" and not self.is_supported(ctap):
raise ValueError("Authenticator does not support large blob storage")
class Processor(RegistrationExtensionProcessor):
def prepare_inputs(self, pin_token):
return {LargeBlobExtension.NAME: True}
def prepare_outputs(self, response, pin_token):
return {
"largeBlob": AuthenticatorExtensionsLargeBlobOutputs(
supported=response.large_blob_key is not None
)
}
return Processor()
def get_assertion(self, ctap, options, pin_protocol):
inputs = options.extensions or {}
data = AuthenticatorExtensionsLargeBlobInputs.from_dict(
cast(Mapping | None, inputs.get("largeBlob"))
)
if data:
if data.support or (data.read and data.write):
raise ValueError("Invalid set of parameters")
if not self.is_supported(ctap):
raise ValueError("Authenticator does not support large blob storage")
class Processor(AuthenticationExtensionProcessor):
def prepare_outputs(self, response, pin_token):
assert data is not None # noqa: S101 needed for mypy
blob_key = response.large_blob_key
if blob_key:
if data.read:
large_blobs = LargeBlobs(ctap)
blob = large_blobs.get_blob(blob_key)
return {
"largeBlob": AuthenticatorExtensionsLargeBlobOutputs(
blob=blob
)
}
elif data.write:
large_blobs = LargeBlobs(ctap, pin_protocol, pin_token)
large_blobs.put_blob(blob_key, data.write)
return {
"largeBlob": AuthenticatorExtensionsLargeBlobOutputs(
written=True
)
}
return Processor(
(
ClientPin.PERMISSION.LARGE_BLOB_WRITE
if data.write
else ClientPin.PERMISSION(0)
),
inputs={LargeBlobExtension.NAME: True},
)
class CredBlobExtension(Ctap2Extension):
"""
Implements the Credential Blob (credBlob) CTAP2 extension.
https://fidoalliance.org/specs/fido-v2.1-rd-20201208/fido-client-to-authenticator-protocol-v2.1-rd-20201208.html#sctn-credBlob-extension
"""
NAME = "credBlob"
def is_supported(self, ctap):
return self.NAME in ctap.info.extensions
def make_credential(self, ctap, options, pin_protocol):
inputs = options.extensions or {}
if self.is_supported(ctap):
blob = inputs.get("credBlob")
assert ctap.info.max_cred_blob_length is not None # noqa: S101
if blob and len(blob) <= ctap.info.max_cred_blob_length:
return RegistrationExtensionProcessor(inputs={self.NAME: blob})
def get_assertion(self, ctap, options, pin_protocol):
inputs = options.extensions or {}
if self.is_supported(ctap) and inputs.get("getCredBlob") is True:
return AuthenticationExtensionProcessor(inputs={self.NAME: True})
class CredProtectExtension(Ctap2Extension):
"""
Implements the Credential Protection CTAP2 extension.
https://fidoalliance.org/specs/fido-v2.1-rd-20201208/fido-client-to-authenticator-protocol-v2.1-rd-20201208.html#sctn-credProtect-extension
"""
@unique
class POLICY(Enum):
OPTIONAL = "userVerificationOptional"
OPTIONAL_WITH_LIST = "userVerificationOptionalWithCredentialIDList"
REQUIRED = "userVerificationRequired"
@classmethod
def str2int(cls, policy: str) -> int:
return list(cls).index(cls(policy)) + 1
NAME = "credProtect"
def is_supported(self, ctap):
return self.NAME in ctap.info.extensions
def make_credential(self, ctap, options, pin_protocol):
inputs = options.extensions or {}
policy = inputs.get("credentialProtectionPolicy")
if policy:
index = CredProtectExtension.POLICY.str2int(policy)
enforce = inputs.get("enforceCredentialProtectionPolicy", False)
if enforce and not self.is_supported(ctap) and index > 1:
raise ValueError("Authenticator does not support Credential Protection")
return RegistrationExtensionProcessor(inputs={self.NAME: index})
class MinPinLengthExtension(Ctap2Extension):
"""
Implements the Minimum PIN Length (minPinLength) CTAP2 extension.
https://fidoalliance.org/specs/fido-v2.1-rd-20201208/fido-client-to-authenticator-protocol-v2.1-rd-20201208.html#sctn-minpinlength-extension
"""
NAME = "minPinLength"
def is_supported(self, ctap):
# NB: There is no key in the extensions field.
return "setMinPINLength" in ctap.info.options
def make_credential(self, ctap, options, pin_protocol):
inputs = options.extensions or {}
if self.is_supported(ctap) and inputs.get(self.NAME) is True:
return RegistrationExtensionProcessor(inputs={self.NAME: True})
class PinComplexityPolicyExtension(Ctap2Extension):
"""
Implements the PIN Complexity Policy (pinComplexityPolicy) CTAP2 extension.
https://fidoalliance.org/specs/fido-v2.1-rd-20201208/fido-client-to-authenticator-protocol-v2.1-rd-20201208.html#sctn-minpinlength-extension
"""
NAME = "pinComplexityPolicy"
def is_supported(self, ctap):
return self.NAME in ctap.info.extensions
def make_credential(self, ctap, options, pin_protocol):
inputs = options.extensions or {}
if self.is_supported(ctap) and inputs.get(self.NAME) is True:
return RegistrationExtensionProcessor(inputs={self.NAME: True})
@dataclass(eq=False, frozen=True)
class CredentialPropertiesOutput(_JsonDataObject):
"""Client outputs for credProps."""
rk: bool | None = None
class CredPropsExtension(Ctap2Extension):
"""
Implements the Credential Properties (credProps) WebAuthn extension.
https://www.w3.org/TR/webauthn-3/#sctn-authenticator-credential-properties-extension
"""
NAME = "credProps"
def is_supported(self, ctap):
# NB: There is no key in the extensions field.
return True
def make_credential(self, ctap, options, pin_protocol):
inputs = options.extensions or {}
if inputs.get(self.NAME) is True:
selection = (
options.authenticator_selection or AuthenticatorSelectionCriteria()
)
rk = selection.resident_key == ResidentKeyRequirement.REQUIRED or (
selection.resident_key == ResidentKeyRequirement.PREFERRED
and ctap.info.options.get("rk")
)
return RegistrationExtensionProcessor(
outputs={self.NAME: CredentialPropertiesOutput(rk=rk)}
)
@dataclass(eq=False, frozen=True)
class PaymentCurrencyAmount(_JsonDataObject):
currency: str
value: str
@dataclass(eq=False, frozen=True)
class PaymentCredentialInstrument(_JsonDataObject):
display_name: str
icon: str
icon_must_be_shown: bool = True
@dataclass(eq=False, frozen=True)
class AuthenticationExtensionsPaymentInputs(_JsonDataObject):
"""Client inputs for payment."""
is_payment: bool | None = None
rp_id: str | None = None
top_origin: str | None = None
payee_name: str | None = None
payee_origin: str | None = None
total: PaymentCurrencyAmount | None = None
instrument: PaymentCredentialInstrument | None = None
class ThirdPartyPaymentExtension(Ctap2Extension):
"""
Implements the Third Party Payment (thirdPartyPayment) CTAP2.2 extension.
https://fidoalliance.org/specs/fido-v2.2-ps-20250228/fido-client-to-authenticator-protocol-v2.2-ps-20250228.html#sctn-thirdPartyPayment-extension
Note that most of the processing for the WebAuthn extension needs to be done by the
client, see:
https://www.w3.org/TR/secure-payment-confirmation/#sctn-collectedclientpaymentdata-dictionary
As such, this extension is not included in the default extensions list, and should
not be used without a client that supports the WebAuthn payment extension.
"""
NAME = "thirdPartyPayment"
def is_supported(self, ctap):
return self.NAME in ctap.info.extensions
def make_credential(self, ctap, options, pin_protocol):
inputs = options.extensions or {}
data = AuthenticationExtensionsPaymentInputs.from_dict(
cast(Mapping | None, inputs.get("payment"))
)
if self.is_supported(ctap) and data and data.is_payment:
return RegistrationExtensionProcessor(inputs={self.NAME: True})
def get_assertion(self, ctap, options, pin_protocol):
inputs = options.extensions or {}
data = AuthenticationExtensionsPaymentInputs.from_dict(
cast(Mapping | None, inputs.get("payment"))
)
if self.is_supported(ctap) and data and data.is_payment:
return AuthenticationExtensionProcessor(inputs={self.NAME: True})
@dataclass(eq=False, frozen=True)
class _SignGenerateKeyInputs(_JsonDataObject):
algorithms: Sequence[int]
@dataclass(eq=False, frozen=True)
class _SignSignInputs(_JsonDataObject):
key_handle: bytes
tbs: bytes
additional_args: bytes | None = None
@dataclass(eq=False, frozen=True)
class _SignInputs(_JsonDataObject):
generate_key: _SignGenerateKeyInputs | None = None
sign_by_credential: Mapping[str, _SignSignInputs] | None = None
@dataclass(eq=False, frozen=True)
class _SignGeneratedKey(_JsonDataObject):
key_handle: bytes
public_key: bytes
algorithm: int
attestation_object: AttestationObject
@dataclass(eq=False, frozen=True)
class _SignOutputs(_JsonDataObject):
generated_key: _SignGeneratedKey | None = None
signature: bytes | None = None
class PreviewSignExtension(Ctap2Extension):
"""
WARNING: This extension is currently in DRAFT.
It is to be considered experimental, and NOT part of the stable API of this library.
Implements version 4-SNAPSHOT-2025-08-21T15:00 of the sign CTAP2 extension.
https://github.com/yubicolabs/webauthn-sign-extension
"""
NAME = "previewSign"
def is_supported(self, ctap):
return self.NAME in ctap.info.extensions
def make_credential(self, ctap, options, pin_protocol):
inputs = options.extensions or {}
data = _SignInputs.from_dict(inputs.get(self.NAME))
if not data or not self.is_supported(ctap):
return
if data.sign_by_credential or not data.generate_key:
raise ValueError("Invalid inputs")
class Processor(RegistrationExtensionProcessor):
def prepare_inputs(self, pin_token):
gk = data.generate_key
selection = (
options.authenticator_selection or AuthenticatorSelectionCriteria()
)
flags = (
0b101
if selection.user_verification
== UserVerificationRequirement.REQUIRED
else 0b001
)
outputs = {3: gk.algorithms, 4: flags}
return {PreviewSignExtension.NAME: outputs}
def prepare_outputs(self, response, pin_token):
extensions = response.auth_data.extensions or {}
data = extensions.get(PreviewSignExtension.NAME)
if not data:
return None
att_obj_bytes = response.unsigned_extension_outputs[
PreviewSignExtension.NAME
][7]
att_resp = AttestationResponse.from_dict(
cast(Mapping, cbor.decode(att_obj_bytes))
)
att_obj = AttestationObject.create(
att_resp.fmt, att_resp.auth_data, att_resp.att_stmt
)
cred_data = att_obj.auth_data.credential_data
assert cred_data is not None # noqa: S101
return {
PreviewSignExtension.NAME: _SignOutputs(
generated_key=_SignGeneratedKey(
key_handle=cred_data.credential_id,
public_key=cbor.encode(cred_data.public_key),
algorithm=data.get(3),
attestation_object=att_obj,
),
)
}
return Processor()
def get_assertion(self, ctap, options, pin_protocol):
inputs = options.extensions or {}
data = _SignInputs.from_dict(inputs.get(self.NAME))
if not data or not self.is_supported(ctap):
return
if not data.sign_by_credential or data.generate_key:
raise ValueError("Invalid inputs")
class Processor(AuthenticationExtensionProcessor):
def prepare_inputs(self, selected, pin_token):
by_creds = data.sign_by_credential
# Make sure all keys are valid IDs from allow_credentials
allow_list = options.allow_credentials
if not allow_list or not selected:
raise ValueError("sign requires allowCredentials")
ids = {websafe_encode(c.id) for c in allow_list}
if ids.difference(by_creds):
raise ValueError("keyHandleByCredential is not valid")
cred_inputs = by_creds[websafe_encode(selected.id)]
authenticator_input = {
2: cred_inputs.key_handle,
6: cred_inputs.tbs,
}
if cred_inputs.additional_args:
authenticator_input[7] = cred_inputs.additional_args
return {PreviewSignExtension.NAME: authenticator_input}
def prepare_outputs(self, response, pin_token):
extensions = response.auth_data.extensions or {}
data = extensions.get(PreviewSignExtension.NAME)
if data:
return {PreviewSignExtension.NAME: _SignOutputs(signature=data[6])}
return Processor()
_DEFAULT_EXTENSIONS = [
HmacSecretExtension(),
LargeBlobExtension(),
CredBlobExtension(),
CredProtectExtension(),
MinPinLengthExtension(),
CredPropsExtension(),
]
fido2-2.2.1/fido2/ctap2/pin.py 0000644 0000000 0000000 00000036227 15220526030 012626 0 ustar 00 # 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.
from __future__ import annotations
import abc
import logging
import os
from dataclasses import dataclass
from enum import IntEnum, IntFlag, unique
from threading import Event
from typing import Any, Callable, ClassVar, Mapping
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from ..cose import CoseKey
from ..utils import bytes2int, hmac_sha256, int2bytes, sha256
from .base import Ctap2
logger = logging.getLogger(__name__)
def _pad_pin(pin: str) -> bytes:
if not isinstance(pin, str):
raise ValueError(f"PIN of wrong type, expecting {str}")
if len(pin) < 4:
raise ValueError("PIN must be >= 4 characters")
pin_padded = pin.encode().ljust(64, b"\0")
pin_padded += b"\0" * (-(len(pin_padded) - 16) % 16)
if len(pin_padded) > 255:
raise ValueError("PIN must be <= 255 bytes")
return pin_padded
class PinProtocol(abc.ABC):
VERSION: ClassVar[int]
@abc.abstractmethod
def encapsulate(self, peer_cose_key: CoseKey) -> tuple[Mapping[int, Any], bytes]:
"""Generates an encapsulation of the public key.
Returns the message to transmit and the shared secret.
"""
@abc.abstractmethod
def encrypt(self, key: bytes, plaintext: bytes) -> bytes:
"""Encrypts data"""
@abc.abstractmethod
def decrypt(self, key: bytes, ciphertext: bytes) -> bytes:
"""Decrypts encrypted data"""
@abc.abstractmethod
def authenticate(self, key: bytes, message: bytes) -> bytes:
"""Computes a MAC of the given message."""
@abc.abstractmethod
def validate_token(self, token: bytes) -> bytes:
"""Validates that a token is well-formed.
Returns the token, or if invalid, raises a ValueError.
"""
@dataclass
class _PinUv:
protocol: PinProtocol
token: bytes
class PinProtocolV1(PinProtocol):
"""Implementation of the CTAP2 PIN/UV protocol v1.
:param ctap: An instance of a CTAP2 object.
:cvar VERSION: The version number of the PIV/UV protocol.
:cvar IV: An all-zero IV used for some cryptographic operations.
"""
VERSION = 1
IV = b"\x00" * 16
def kdf(self, z: bytes) -> bytes:
return sha256(z)
def encapsulate(self, peer_cose_key):
be = default_backend()
sk = ec.generate_private_key(ec.SECP256R1(), be)
pn = sk.public_key().public_numbers()
key_agreement = {
1: 2,
3: -25, # Per the spec, "although this is NOT the algorithm actually used"
-1: 1,
-2: int2bytes(pn.x, 32),
-3: int2bytes(pn.y, 32),
}
x = bytes2int(peer_cose_key[-2])
y = bytes2int(peer_cose_key[-3])
pk = ec.EllipticCurvePublicNumbers(x, y, ec.SECP256R1()).public_key(be)
shared_secret = self.kdf(sk.exchange(ec.ECDH(), pk)) # x-coordinate, 32b
return key_agreement, shared_secret
def _get_cipher_v1(self, secret):
be = default_backend()
return Cipher(algorithms.AES(secret), modes.CBC(PinProtocolV1.IV), be)
def encrypt(self, key, plaintext):
cipher = self._get_cipher_v1(key)
enc = cipher.encryptor()
return enc.update(plaintext) + enc.finalize()
def decrypt(self, key, ciphertext):
cipher = self._get_cipher_v1(key)
dec = cipher.decryptor()
return dec.update(ciphertext) + dec.finalize()
def authenticate(self, key, message):
return hmac_sha256(key, message)[:16]
def validate_token(self, token):
if len(token) not in (16, 32):
raise ValueError("PIN/UV token must be 16 or 32 bytes")
return token
class PinProtocolV2(PinProtocolV1):
"""Implementation of the CTAP2 PIN/UV protocol v2.
:param ctap: An instance of a CTAP2 object.
:cvar VERSION: The version number of the PIV/UV protocol.
:cvar IV: An all-zero IV used for some cryptographic operations.
"""
VERSION = 2
HKDF_SALT = b"\x00" * 32
HKDF_INFO_HMAC = b"CTAP2 HMAC key"
HKDF_INFO_AES = b"CTAP2 AES key"
def kdf(self, z):
be = default_backend()
hmac_key = HKDF(
algorithm=hashes.SHA256(),
length=32,
salt=PinProtocolV2.HKDF_SALT,
info=PinProtocolV2.HKDF_INFO_HMAC,
backend=be,
).derive(z)
aes_key = HKDF(
algorithm=hashes.SHA256(),
length=32,
salt=PinProtocolV2.HKDF_SALT,
info=PinProtocolV2.HKDF_INFO_AES,
backend=be,
).derive(z)
return hmac_key + aes_key
def _get_cipher_v2(self, secret, iv):
be = default_backend()
return Cipher(algorithms.AES(secret), modes.CBC(iv), be)
def encrypt(self, key, plaintext):
aes_key = key[32:]
iv = os.urandom(16)
cipher = self._get_cipher_v2(aes_key, iv)
enc = cipher.encryptor()
return iv + enc.update(plaintext) + enc.finalize()
def decrypt(self, key, ciphertext):
aes_key = key[32:]
iv, ciphertext = ciphertext[:16], ciphertext[16:]
cipher = self._get_cipher_v2(aes_key, iv)
dec = cipher.decryptor()
return dec.update(ciphertext) + dec.finalize()
def authenticate(self, key, message):
hmac_key = key[:32]
return hmac_sha256(hmac_key, message)
def validate_token(self, token):
if len(token) != 32:
raise ValueError("PIN/UV token must be 32 bytes")
return token
class ClientPin:
"""Implementation of the CTAP2 Client PIN API.
:param ctap: An instance of a CTAP2 object.
:param protocol: An optional instance of a PinUvAuthProtocol object. If None is
provided then the latest protocol supported by both library and Authenticator
will be used.
"""
PROTOCOLS = [PinProtocolV2, PinProtocolV1]
@unique
class CMD(IntEnum):
GET_PIN_RETRIES = 0x01
GET_KEY_AGREEMENT = 0x02
SET_PIN = 0x03
CHANGE_PIN = 0x04
GET_TOKEN_USING_PIN_LEGACY = 0x05
GET_TOKEN_USING_UV = 0x06
GET_UV_RETRIES = 0x07
GET_TOKEN_USING_PIN = 0x09
@unique
class RESULT(IntEnum):
KEY_AGREEMENT = 0x01
PIN_UV_TOKEN = 0x02
PIN_RETRIES = 0x03
POWER_CYCLE_STATE = 0x04
UV_RETRIES = 0x05
@unique
class PERMISSION(IntFlag):
MAKE_CREDENTIAL = 0x01
GET_ASSERTION = 0x02
CREDENTIAL_MGMT = 0x04
BIO_ENROLL = 0x08
LARGE_BLOB_WRITE = 0x10
AUTHENTICATOR_CFG = 0x20
PERSISTENT_CREDENTIAL_MGMT = 0x40
@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
@staticmethod
def is_token_supported(info):
"""Checks if pinUvAuthToken is supported."""
return info.options.get("pinUvAuthToken") is True
def __init__(self, ctap: Ctap2, protocol: PinProtocol | None = None):
self.ctap = ctap
if protocol is None:
for proto in ClientPin.PROTOCOLS:
if proto.VERSION in ctap.info.pin_uv_protocols:
self.protocol: PinProtocol = proto()
break
else:
raise ValueError("No compatible PIN/UV protocols supported!")
else:
self.protocol = protocol
def _get_shared_secret(self):
resp = self.ctap.client_pin(
self.protocol.VERSION, ClientPin.CMD.GET_KEY_AGREEMENT
)
pk = resp[ClientPin.RESULT.KEY_AGREEMENT]
return self.protocol.encapsulate(pk)
def get_pin_token(
self,
pin: str,
permissions: ClientPin.PERMISSION | None = None,
permissions_rpid: str | None = None,
) -> bytes:
"""Get a PIN/UV token from the authenticator using PIN.
:param pin: The PIN of the authenticator.
:param permissions: The permissions to associate with the token.
: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 ClientPin.is_token_supported(self.ctap.info) and permissions is not None:
cmd = ClientPin.CMD.GET_TOKEN_USING_PIN
else:
cmd = ClientPin.CMD.GET_TOKEN_USING_PIN_LEGACY
# Ignore permissions if not supported
permissions = None
permissions_rpid = None
resp = self.ctap.client_pin(
self.protocol.VERSION,
cmd,
key_agreement=key_agreement,
pin_hash_enc=pin_hash_enc,
permissions=permissions,
permissions_rpid=permissions_rpid,
)
pin_token_enc = resp[ClientPin.RESULT.PIN_UV_TOKEN]
logger.debug(f"Got PIN token for permissions: {permissions}")
return self.protocol.validate_token(
self.protocol.decrypt(shared_secret, pin_token_enc)
)
def get_uv_token(
self,
permissions: ClientPin.PERMISSION | None = None,
permissions_rpid: str | None = None,
event: Event | None = None,
on_keepalive: Callable[[int], None] | None = None,
) -> bytes:
"""Get a PIN/UV token from the authenticator using built-in UV.
:param permissions: The permissions to associate with the token.
:param permissions_rpid: The permissions RPID to associate with the token.
:param event: An optional threading.Event which can be used to cancel
the invocation.
:param on_keepalive: An optional callback to handle keep-alive messages
from the authenticator. The function is only called once for
consecutive keep-alive messages with the same status.
:return: A PIN/UV token.
"""
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()
resp = self.ctap.client_pin(
self.protocol.VERSION,
ClientPin.CMD.GET_TOKEN_USING_UV,
key_agreement=key_agreement,
permissions=permissions,
permissions_rpid=permissions_rpid,
event=event,
on_keepalive=on_keepalive,
)
pin_token_enc = resp[ClientPin.RESULT.PIN_UV_TOKEN]
logger.debug(f"Got UV token for permissions: {permissions}")
return self.protocol.validate_token(
self.protocol.decrypt(shared_secret, pin_token_enc)
)
def get_pin_retries(self) -> tuple[int, int | None]:
"""Get the number of PIN retries remaining.
:return: A tuple of the number of PIN attempts remaining until the
authenticator is locked, and the power cycle state, if available.
"""
resp = self.ctap.client_pin(
self.protocol.VERSION, ClientPin.CMD.GET_PIN_RETRIES
)
return (
resp[ClientPin.RESULT.PIN_RETRIES],
resp.get(ClientPin.RESULT.POWER_CYCLE_STATE),
)
def get_uv_retries(self) -> int:
"""Get the number of UV retries remaining.
:return: A tuple of the number of UV attempts remaining until the
authenticator is locked, and the power cycle state, if available.
"""
resp = self.ctap.client_pin(self.protocol.VERSION, ClientPin.CMD.GET_UV_RETRIES)
return resp[ClientPin.RESULT.UV_RETRIES]
def set_pin(self, pin: str) -> None:
"""Set the PIN of the autenticator.
This only works when no PIN is set. To change the PIN when set, use
change_pin.
: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))
pin_uv_param = self.protocol.authenticate(shared_secret, pin_enc)
self.ctap.client_pin(
self.protocol.VERSION,
ClientPin.CMD.SET_PIN,
key_agreement=key_agreement,
new_pin_enc=pin_enc,
pin_uv_param=pin_uv_param,
)
logger.info("PIN has been set")
def change_pin(self, old_pin: str, new_pin: str) -> None:
"""Change the PIN of the authenticator.
This only works when a PIN is already set. If no PIN is set, use
set_pin.
: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]
pin_hash_enc = self.protocol.encrypt(shared_secret, pin_hash)
new_pin_enc = self.protocol.encrypt(shared_secret, _pad_pin(new_pin))
pin_uv_param = self.protocol.authenticate(
shared_secret, new_pin_enc + pin_hash_enc
)
self.ctap.client_pin(
self.protocol.VERSION,
ClientPin.CMD.CHANGE_PIN,
key_agreement=key_agreement,
pin_hash_enc=pin_hash_enc,
new_pin_enc=new_pin_enc,
pin_uv_param=pin_uv_param,
)
logger.info("PIN has been changed")
fido2-2.2.1/fido2/features.py 0000644 0000000 0000000 00000005471 15220526030 012642 0 ustar 00 # 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.
import warnings
class FeatureNotEnabledError(Exception):
pass
class _Feature:
def __init__(self, name: str, desc: str):
self._enabled: bool | None = 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,
)
fido2-2.2.1/fido2/hid/__init__.py 0000644 0000000 0000000 00000022320 15220526030 013317 0 ustar 00 # Copyright (c) 2020 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 __future__ import annotations
import logging
import os
import struct
import sys
from enum import IntEnum, IntFlag, unique
from threading import Event
from typing import Callable, Iterator
from ..ctap import STATUS, CtapDevice, CtapError
from ..utils import LOG_LEVEL_TRAFFIC
from .base import CtapHidConnection, HidDescriptor
logger = logging.getLogger(__name__)
if sys.platform == "linux":
from . import linux as backend
elif sys.platform == "win32":
from . import windows as backend
elif sys.platform == "darwin":
from . import macos as backend
# The following have version numbers at the end
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:
raise Exception("Unsupported platform")
list_descriptors = backend.list_descriptors
get_descriptor = backend.get_descriptor
open_connection = backend.open_connection
class ConnectionFailure(Exception):
"""The CTAP connection failed or returned an invalid response."""
@unique
class CTAPHID(IntEnum):
PING = 0x01
MSG = 0x03
LOCK = 0x04
INIT = 0x06
WINK = 0x08
CBOR = 0x10
CANCEL = 0x11
ERROR = 0x3F
KEEPALIVE = 0x3B
VENDOR_FIRST = 0x40
@unique
class CAPABILITY(IntFlag):
WINK = 0x01
LOCK = 0x02 # Not used
CBOR = 0x04
NMSG = 0x08
def supported(self, flags: CAPABILITY) -> bool:
return bool(flags & self)
TYPE_INIT = 0x80
class CtapHidDevice(CtapDevice):
"""
CtapDevice implementation using the HID transport.
:cvar descriptor: Device descriptor.
"""
def __init__(self, descriptor: HidDescriptor, connection: CtapHidConnection):
self.descriptor = descriptor
self._packet_size = descriptor.report_size_out
self._connection = connection
nonce = os.urandom(8)
self._channel_id = 0xFFFFFFFF
response = self.call(CTAPHID.INIT, nonce)
r_nonce, response = response[:8], response[8:]
if r_nonce != nonce:
raise ConnectionFailure("Wrong nonce")
(
self._channel_id,
self._u2fhid_version,
v1,
v2,
v3,
self._capabilities,
) = struct.unpack_from(">IBBBBB", response)
self._device_version = (v1, v2, v3)
def __repr__(self):
return f"CtapHidDevice({self.descriptor.path!r})"
@property
def version(self) -> int:
"""CTAP HID protocol version."""
return self._u2fhid_version
@property
def device_version(self) -> tuple[int, int, int]:
"""Device version number."""
return self._device_version
@property
def capabilities(self) -> int:
"""Capabilities supported by the device."""
return self._capabilities
@property
def product_name(self) -> str | None:
"""Product name of device."""
return self.descriptor.product_name
@property
def serial_number(self) -> str | None:
"""Serial number of device."""
return self.descriptor.serial_number
def _send_cancel(self):
packet = struct.pack(">IB", self._channel_id, TYPE_INIT | CTAPHID.CANCEL).ljust(
self._packet_size, b"\0"
)
logger.log(LOG_LEVEL_TRAFFIC, "SEND: %s", packet.hex())
self._connection.write_packet(packet)
def call(
self,
cmd: int,
data: bytes = b"",
event: Event | None = None,
on_keepalive: Callable[[STATUS], None] | None = None,
) -> bytes:
event = event or Event()
while True:
try:
return self._do_call(cmd, data, event, on_keepalive)
except CtapError as e:
if e.code == CtapError.ERR.CHANNEL_BUSY:
if not event.wait(0.1):
logger.warning("CTAP channel busy, trying again...")
continue # Keep retrying on BUSY while not cancelled
raise
def _do_call(self, cmd, data, event, on_keepalive):
remaining = data
seq = 0
# Send request
header = struct.pack(">IBH", self._channel_id, TYPE_INIT | cmd, len(remaining))
while remaining or seq == 0:
size = min(len(remaining), self._packet_size - len(header))
body, remaining = remaining[:size], remaining[size:]
packet = header + body
logger.log(LOG_LEVEL_TRAFFIC, "SEND: %s", packet.hex())
self._connection.write_packet(packet.ljust(self._packet_size, b"\0"))
header = struct.pack(">IB", self._channel_id, 0x7F & seq)
seq += 1
try:
# Read response
seq = 0
r_len = 0
response = b""
last_ka = None
while True:
if event.is_set():
# Cancel
logger.debug("Sending cancel...")
self._send_cancel()
recv = self._connection.read_packet()
logger.log(LOG_LEVEL_TRAFFIC, "RECV: %s", recv.hex())
r_channel = struct.unpack_from(">I", recv)[0]
recv = recv[4:]
if r_channel != self._channel_id:
raise ConnectionFailure("Wrong channel")
if not response: # Initialization packet
r_cmd, r_len = struct.unpack_from(">BH", recv)
recv = recv[3:]
if r_cmd == TYPE_INIT | cmd:
pass # first data packet
elif r_cmd == TYPE_INIT | CTAPHID.KEEPALIVE:
try:
ka_status = STATUS(struct.unpack_from(">B", recv)[0])
logger.debug(f"Got keepalive status: {ka_status:02x}")
except ValueError:
raise ConnectionFailure("Invalid keepalive status")
if on_keepalive and ka_status != last_ka:
last_ka = ka_status
on_keepalive(ka_status)
continue
elif r_cmd == TYPE_INIT | CTAPHID.ERROR:
raise CtapError(struct.unpack_from(">B", recv)[0])
else:
raise CtapError(CtapError.ERR.INVALID_COMMAND)
else: # Continuation packet
r_seq = struct.unpack_from(">B", recv)[0]
recv = recv[1:]
if r_seq != seq & 0x7F:
raise ConnectionFailure("Wrong sequence number")
seq += 1
response += recv
if len(response) >= r_len:
break
return response[:r_len]
except KeyboardInterrupt:
logger.debug("Keyboard interrupt, cancelling...")
self._send_cancel()
raise
def wink(self) -> None:
"""Causes the authenticator to blink."""
self.call(CTAPHID.WINK)
def ping(self, msg: bytes = b"Hello FIDO") -> bytes:
"""Sends data to the authenticator, which echoes it back.
:param msg: The data to send.
:return: The response from the authenticator.
"""
return self.call(CTAPHID.PING, msg)
def lock(self, lock_time: int = 10) -> None:
"""Locks the channel."""
self.call(CTAPHID.LOCK, struct.pack(">B", lock_time))
def close(self) -> None:
self._connection.close()
@classmethod
def list_devices(cls) -> Iterator[CtapHidDevice]:
for d in list_descriptors():
yield cls(d, open_connection(d))
def list_devices() -> Iterator[CtapHidDevice]:
return CtapHidDevice.list_devices()
def open_device(path) -> CtapHidDevice:
descriptor = get_descriptor(path)
return CtapHidDevice(descriptor, open_connection(descriptor))
fido2-2.2.1/fido2/hid/base.py 0000644 0000000 0000000 00000010640 15220526030 012474 0 ustar 00 # Copyright (c) 2020 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 __future__ import annotations
import abc
import os
import struct
from dataclasses import dataclass
FIDO_USAGE_PAGE = 0xF1D0
FIDO_USAGE = 0x1
@dataclass
class HidDescriptor:
path: str | bytes
vid: int
pid: int
report_size_in: int
report_size_out: int
product_name: str | None
serial_number: str | None
class CtapHidConnection(abc.ABC):
@abc.abstractmethod
def read_packet(self) -> bytes:
"""Reads a CTAP HID packet"""
@abc.abstractmethod
def write_packet(self, data: bytes) -> None:
"""Writes a CTAP HID packet"""
@abc.abstractmethod
def close(self) -> None:
"""Closes the connection"""
class FileCtapHidConnection(CtapHidConnection):
"""Basic CtapHidConnection implementation which uses a path to a file descriptor"""
def __init__(self, descriptor):
self.handle = os.open(descriptor.path, os.O_RDWR)
self.descriptor = descriptor
def close(self):
os.close(self.handle)
def write_packet(self, data):
if os.write(self.handle, data) != len(data):
raise OSError("failed to write entire packet")
def read_packet(self):
return os.read(self.handle, self.descriptor.report_size_in)
REPORT_DESCRIPTOR_KEY_MASK = 0xFC
SIZE_MASK = ~REPORT_DESCRIPTOR_KEY_MASK
OUTPUT_ITEM = 0x90
INPUT_ITEM = 0x80
COLLECTION_ITEM = 0xA0
REPORT_COUNT = 0x94
REPORT_SIZE = 0x74
USAGE_PAGE = 0x04
USAGE = 0x08
def parse_report_descriptor(data: bytes) -> tuple[int, int]:
# Parse report descriptor data
usage, usage_page = None, None
max_input_size, max_output_size = None, None
report_count, report_size = None, None
remaining = 4
while data and remaining:
head, data = struct.unpack_from(">B", data)[0], data[1:]
key, size = REPORT_DESCRIPTOR_KEY_MASK & head, SIZE_MASK & head
value = struct.unpack_from("