├── .envrc ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── LICENSE-APACHE ├── LICENSE-MIT ├── Makefile ├── README.md ├── data └── first-names.txt ├── dev-requirements.txt ├── requirements.txt └── tests ├── __init__.py ├── conftest.py ├── standard ├── __init__.py ├── connect_test.py ├── fido2 │ ├── __init__.py │ ├── extensions │ │ └── test_hmac_secret.py │ ├── pin │ │ ├── __init__.py │ │ ├── test_lockout.py │ │ ├── test_pin.py │ │ └── test_set_pin.py │ ├── test_ctap1_interop.py │ ├── test_get_assertion.py │ ├── test_getinfo.py │ ├── test_make_credential.py │ ├── test_reset.py │ ├── test_reset_credential.py │ ├── test_resident_key.py │ └── user_presence │ │ └── test_user_presence.py ├── fido2v1 │ ├── extensions │ │ └── test_cred_protect.py │ └── test_credmgmt.py ├── transport │ ├── __init__.py │ ├── test_hid.py │ └── test_nfc.py └── u2f │ ├── __init__.py │ └── test_u2f.py ├── utils.py └── vendor ├── solo ├── test_solo.py └── utils.py └── trezor ├── udp_backend.py └── utils.py /.envrc: -------------------------------------------------------------------------------- 1 | # to use this, install [direnv](https://direnv.net/) 2 | source venv/bin/activate 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | *.swp 107 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black 3 | rev: stable 4 | hooks: 5 | - id: black 6 | language_version: python3.7 7 | - repo: https://github.com/pre-commit/mirrors-isort 8 | rev: v4.3.21 9 | hooks: 10 | - id: isort 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache-2.0 OR MIT 2 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: standard-tests vendor-tests 2 | 3 | PY_VERSION=$(shell python -c "import sys; print('%d.%d'% sys.version_info[0:2])") 4 | VALID=$(shell python -c "print($(PY_VERSION) >= 3.6)") 5 | 6 | ifeq ($(OS),Windows_NT) 7 | BIN=venv/Scripts 8 | else 9 | BIN=venv/bin 10 | endif 11 | 12 | ifeq (True,$(VALID)) 13 | PYTHON=python 14 | else 15 | PYTHON=python3 16 | endif 17 | 18 | standard-tests: venv 19 | $(BIN)/pytest tests/standard 20 | 21 | vendor-tests: venv 22 | $(BIN)/pytest tests/vendor 23 | 24 | # setup development environment 25 | venv: 26 | $(PYTHON) -m venv venv 27 | $(BIN)/python -m pip install -U pip 28 | $(BIN)/pip install -U -r requirements.txt 29 | $(BIN)/pip install -U -r dev-requirements.txt 30 | $(BIN)/pre-commit install 31 | 32 | # re-run if dependencies change 33 | update: 34 | $(BIN)/python -m pip install -U pip 35 | $(BIN)/pip install -U -r requirements.txt 36 | $(BIN)/pip install -U -r dev-requirements.txt 37 | 38 | # ensure this passes before commiting 39 | check: 40 | $(BIN)/black --check tests/ 41 | $(BIN)/isort --check-only --recursive tests/ 42 | 43 | # automatic code fixes 44 | fix: black isort 45 | 46 | black: 47 | $(BIN)/black tests/ 48 | 49 | isort: 50 | $(BIN)/isort -y --recursive tests/ 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fido2-tests 2 | 3 | Test suite for FIDO2, U2F, and other security key functions 4 | 5 | # Setup 6 | 7 | Need python 3.6+. 8 | 9 | `make venv` and `source venv/bin/activate` 10 | 11 | Or simply `pip3 install --user -r requirements.txt` 12 | 13 | # Running the tests 14 | 15 | Run all FIDO2, U2F, and HID tests: 16 | 17 | ``` 18 | pytest tests/standard -s 19 | ``` 20 | 21 | Run vendor/model specific tests: 22 | 23 | ``` 24 | pytest tests/vendor -s 25 | ``` 26 | 27 | Run subset of tests with `-k` flag, example: 28 | ``` 29 | pytest -k "getinfo or hmac_secret" -s 30 | ``` 31 | 32 | To run tests via nfc, supply the `--nfc` option. 33 | Make sure that you have `pyscard` python module installed properly and have updated `python-fido2` (by Yubikey) library to lastest version 34 | 35 | ``` 36 | pytest --nfc tests/standard -s 37 | ``` 38 | 39 | Note that in most cases when testing a hardware authenticator, `-s` must be supplied to disable stdin/stdout capturing. This is so the prompts to power cycle the authenticator can be seen and continued. 40 | 41 | # Running against simulation 42 | 43 | To run tests against a "simulation" build of the Solo authenticator, supply the `--sim` option. 44 | 45 | ``` 46 | pytest --sim tests/standard 47 | ``` 48 | 49 | All tests should pass without having to use `-s` or provide any interaction. 50 | 51 | # Notes 52 | 53 | Initial SoloKeys models truncates the displayName, which causes a couple of the tests to fail. 54 | To succeed all tests, pass `--vendor solokeys` as an option. 55 | 56 | # Contributing 57 | 58 | We use `black` and `isort` to prevent code formatting discussions. 59 | 60 | The `make venv` setup method installs git pre-commit hooks that check conformance automatically. 61 | 62 | You can also `make check` and `make fix` manually, or use an editor plugins. 63 | 64 | # License 65 | 66 | Apache-2.0 OR MIT 67 | 68 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | black==18.9b0 # 19.3b0 has an issue on Windows 2 | isort 3 | pre-commit 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fido2 == 0.8.1 2 | solo-python == 0.0.26 3 | pyscard 4 | pytest 5 | pytest-ordering 6 | pytest-rerunfailures 7 | pytest-timeout 8 | seedweed >= 1.0rc7 -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trussed-dev/fido2-tests/591d3d2279949e08de0766897f24bcfd39af1339/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import struct 2 | import time 3 | import sys 4 | 5 | import pytest 6 | from fido2.attestation import Attestation 7 | from fido2.client import Fido2Client, _call_polling 8 | from fido2.ctap import CtapError 9 | from fido2.ctap1 import CTAP1 10 | from fido2.ctap2 import ES256, AttestedCredentialData, PinProtocolV1 11 | from fido2.hid import CtapHidDevice 12 | from fido2.utils import hmac_sha256, sha256 13 | 14 | from tests.utils import * 15 | 16 | if "trezor" in sys.argv: 17 | from .vendor.trezor.udp_backend import force_udp_backend 18 | else: 19 | from solo.fido2 import force_udp_backend 20 | 21 | 22 | def pytest_addoption(parser): 23 | parser.addoption("--sim", action="store_true") 24 | parser.addoption("--nfc", action="store_true") 25 | parser.addoption("--experimental", action="store_true") 26 | parser.addoption("--vendor", default="none") 27 | 28 | 29 | @pytest.fixture() 30 | def is_simulation(pytestconfig): 31 | return pytestconfig.getoption("sim") 32 | 33 | 34 | @pytest.fixture() 35 | def is_nfc(pytestconfig): 36 | return pytestconfig.getoption("nfc") 37 | 38 | 39 | @pytest.fixture(scope="module") 40 | def info(device): 41 | info = device.ctap2.get_info() 42 | # print("data:", bytes(info)) 43 | # print("decoded:", cbor.decode_from(bytes(info))) 44 | return info 45 | 46 | 47 | @pytest.fixture(scope="module") 48 | def MCRes( 49 | resetDevice, 50 | ): 51 | req = FidoRequest() 52 | res = resetDevice.sendMC(*req.toMC()) 53 | setattr(res, "request", req) 54 | return res 55 | 56 | 57 | @pytest.fixture(scope="class") 58 | def GARes(device, MCRes): 59 | req = FidoRequest( 60 | allow_list=[ 61 | {"id": MCRes.auth_data.credential_data.credential_id, "type": "public-key"} 62 | ] 63 | ) 64 | res = device.sendGA(*req.toGA()) 65 | setattr(res, "request", req) 66 | return res 67 | 68 | 69 | @pytest.fixture(scope="module") 70 | def RegRes( 71 | resetDevice, 72 | ): 73 | req = FidoRequest() 74 | res = resetDevice.register(req.challenge, req.appid) 75 | setattr(res, "request", req) 76 | return res 77 | 78 | 79 | @pytest.fixture(scope="class") 80 | def AuthRes(device, RegRes): 81 | req = FidoRequest() 82 | res = device.authenticate(req.challenge, req.appid, RegRes.key_handle) 83 | setattr(res, "request", req) 84 | return res 85 | 86 | 87 | @pytest.fixture(scope="module") 88 | def allowListItem(MCRes): 89 | return 90 | 91 | 92 | @pytest.fixture(scope="session") 93 | def device(pytestconfig): 94 | if pytestconfig.getoption("sim"): 95 | print("FORCE UDP") 96 | force_udp_backend() 97 | 98 | dev = TestDevice() 99 | dev.set_sim(pytestconfig.getoption("sim")) 100 | 101 | dev.find_device(pytestconfig.getoption("nfc")) 102 | 103 | return dev 104 | 105 | 106 | @pytest.fixture(scope="class") 107 | def rebootedDevice(device): 108 | device.reboot() 109 | return device 110 | 111 | 112 | @pytest.fixture(scope="module") 113 | def resetDevice(device): 114 | device.reset() 115 | return device 116 | 117 | 118 | class Packet(object): 119 | def __init__(self, data): 120 | self.data = data 121 | 122 | def ToWireFormat( 123 | self, 124 | ): 125 | return self.data 126 | 127 | @staticmethod 128 | def FromWireFormat(pkt_size, data): 129 | return Packet(data) 130 | 131 | from fido2.pcsc import CtapPcscDevice,_list_readers 132 | from fido2.hid import CAPABILITY, CTAPHID 133 | 134 | 135 | class MoreRobustPcscDevice(CtapPcscDevice): 136 | """ 137 | Some small tweaks to prevent failures in NFC when many 138 | tests are being run on the same connection. 139 | """ 140 | def __init__(self, connection, name): 141 | self._capabilities = 0 142 | self.use_ext_apdu = False 143 | self._conn = connection 144 | from smartcard.System import readers 145 | from smartcard.util import toHexString 146 | from smartcard.CardConnection import CardConnection 147 | from smartcard.pcsc.PCSCPart10 import (getFeatureRequest, hasFeature, 148 | getTlvProperties, FEATURE_CCID_ESC_COMMAND, SCARD_SHARE_DIRECT) 149 | from smartcard.scard import SCARD_LEAVE_CARD, SCARD_SHARE_EXCLUSIVE, SCARD_CTL_CODE, SCARD_UNPOWER_CARD, SCARD_RESET_CARD 150 | 151 | # res = self._conn.transmit([0xE0,0x00,0x00,0x24,0x02,0x00,0x00],CardConnection.T0_protocol) 152 | # res = self.control_exchange(SCARD_CTL_CODE(3500), b"\xE0\x00\x00\x24\x00") 153 | # print('read ctrl res:',res) 154 | # res = self.control_exchange(SCARD_CTL_CODE(3500), b"\xE0\x00\x00\x24\x02\x00\x00") 155 | 156 | self._conn.connect( 157 | # CardConnection.T0_protocol, 158 | # mode=SCARD_SHARE_DIRECT 159 | # disposition = SCARD_RESET_CARD, 160 | ) 161 | 162 | self._name = name 163 | self._select() 164 | 165 | # For ACR1252 readers, with drivers installed 166 | # https://www.acs.com.hk/en/products/342/acr1252u-usb-nfc-reader-iii-nfc-forum-certified-reader 167 | # disable auto pps, always use 106kbps 168 | # self.control_exchange(SCARD_CTL_CODE(3500), b"\xE0\x00\x00\x24\x02\x00\x00") 169 | # or always use 212kps 170 | # self.control_exchange(SCARD_CTL_CODE(3500), b"\xE0\x00\x00\x24\x02\x01\x01") 171 | 172 | try: # Probe for CTAP2 by calling GET_INFO 173 | self.call(CTAPHID.CBOR, b"\x04") 174 | self._capabilities |= CAPABILITY.CBOR 175 | except CtapError: 176 | if self._capabilities == 0: 177 | raise ValueError("Unsupported device") 178 | 179 | def apdu_exchange(self, apdu, protocol = None): 180 | try: 181 | return super().apdu_exchange(apdu,protocol) 182 | except: 183 | # Try reconnecting.. 184 | self._conn.disconnect() 185 | self._conn.connect() 186 | return super().apdu_exchange(apdu,protocol) 187 | 188 | def call(self, cmd, data=b"", event=None, on_keepalive=None): 189 | # Sometimes an NFC reader may suspend the field inbetween tests, 190 | # Which would require the app to be selected again. 191 | self._select() 192 | return super().call(cmd, data, event, on_keepalive) 193 | 194 | def _call_cbor(self, data=b"", event=None, on_keepalive=None): 195 | # Sometimes an NFC reader may suspend the field inbetween tests, 196 | # Which would require the app to be selected again. 197 | self._select() 198 | return super()._call_cbor(data, event, on_keepalive) 199 | 200 | @classmethod 201 | def list_devices(cls, name=""): 202 | for reader in _list_readers(): 203 | if name in reader.name: 204 | try: 205 | yield cls(reader.createConnection(), reader.name) 206 | except Exception as e: 207 | print(e) 208 | 209 | class TestDevice: 210 | def __init__(self, tester=None): 211 | self.origin = "https://examplo.org" 212 | self.host = "examplo.org" 213 | self.user_count = 10 214 | self.is_sim = False 215 | self.is_nfc = False 216 | self.nfc_interface_only = False 217 | if tester: 218 | self.initFrom(tester) 219 | 220 | def initFrom(self, tester): 221 | self.user_count = tester.user_count 222 | self.is_sim = tester.is_sim 223 | self.is_nfc = tester.is_nfc 224 | self.dev = tester.dev 225 | self.ctap2 = tester.ctap2 226 | self.ctap1 = tester.ctap1 227 | self.client = tester.client 228 | self.nfc_interface_only = tester.nfc_interface_only 229 | 230 | def find_device(self, nfcInterfaceOnly=False): 231 | dev = None 232 | self.nfc_interface_only = nfcInterfaceOnly 233 | if not nfcInterfaceOnly: 234 | print("--- HID ---") 235 | print(list(CtapHidDevice.list_devices())) 236 | dev = next(CtapHidDevice.list_devices(), None) 237 | 238 | else: 239 | from fido2.pcsc import CtapPcscDevice 240 | 241 | print("--- NFC ---") 242 | dev = next(MoreRobustPcscDevice.list_devices(), None) 243 | 244 | if dev: 245 | self.is_nfc = True 246 | # For ACR1252 readers, with drivers installed 247 | # https://www.acs.com.hk/en/products/342/acr1252u-usb-nfc-reader-iii-nfc-forum-certified-reader 248 | # disable auto pps, always use 106kbps 249 | # dev.control_exchange(SCARD_CTL_CODE(0x3500), b"\xE0\x00\x00\x24\x02\x00\x00") 250 | 251 | if not dev: 252 | raise RuntimeError("No FIDO device found") 253 | self.dev = dev 254 | self.client = Fido2Client(dev, self.origin) 255 | self.ctap2 = self.client.ctap2 256 | self.ctap1 = CTAP1(dev) 257 | 258 | def set_user_count(self, count): 259 | self.user_count = count 260 | 261 | def set_sim(self, b): 262 | self.is_sim = b 263 | 264 | def reboot( 265 | self, 266 | ): 267 | if self.is_sim: 268 | print("Sending restart command...") 269 | self.send_magic_reboot() 270 | TestDevice.delay(0.25) 271 | return 272 | 273 | if "solokeys" in sys.argv or "solobee" in sys.argv: 274 | if self.is_nfc: 275 | if self.send_nfc_reboot(): 276 | TestDevice.delay(5) 277 | self.find_device(self.nfc_interface_only) 278 | return 279 | try: 280 | self.dev.call(0x53 ^ 0x80, b"") 281 | except OSError: 282 | pass 283 | 284 | print("Rebooting..") 285 | for i in range(0, 10): 286 | time.sleep(0.1 * i) 287 | try: 288 | self.find_device(self.nfc_interface_only) 289 | return 290 | except (RuntimeError, FileNotFoundError): 291 | pass 292 | else: 293 | print("Please reboot authenticator and hit enter") 294 | input() 295 | self.find_device(self.nfc_interface_only) 296 | 297 | def send_data(self, cmd, data, timeout = 1.0, on_keepalive = None): 298 | if not isinstance(data, bytes): 299 | data = struct.pack("%dB" % len(data), *[ord(x) for x in data]) 300 | with Timeout(timeout) as event: 301 | event.is_set() 302 | return self.dev.call(cmd, data, event, on_keepalive = on_keepalive) 303 | 304 | def send_raw(self, data, cid=None): 305 | if cid is None: 306 | cid = self.dev._dev.cid 307 | elif not isinstance(cid, bytes): 308 | cid = struct.pack("%dB" % len(cid), *[ord(x) for x in cid]) 309 | if not isinstance(data, bytes): 310 | data = struct.pack("%dB" % len(data), *[ord(x) for x in data]) 311 | data = cid + data 312 | l = len(data) 313 | if l != 64: 314 | pad = "\x00" * (64 - l) 315 | pad = struct.pack("%dB" % len(pad), *[ord(x) for x in pad]) 316 | data = data + pad 317 | data = list(data) 318 | assert len(data) == 64 319 | self.dev._dev.InternalSendPacket(Packet(data)) 320 | 321 | def send_magic_reboot( 322 | self, 323 | ): 324 | """ 325 | For use in simulation and testing. Random bytes that authenticator should detect 326 | and then restart itself. 327 | """ 328 | magic_cmd = ( 329 | b"\xac\x10\x52\xca\x95\xe5\x69\xde\x69\xe0\x2e\xbf" 330 | + b"\xf3\x33\x48\x5f\x13\xf9\xb2\xda\x34\xc5\xa8\xa3" 331 | + b"\x40\x52\x66\x97\xa9\xab\x2e\x0b\x39\x4d\x8d\x04" 332 | + b"\x97\x3c\x13\x40\x05\xbe\x1a\x01\x40\xbf\xf6\x04" 333 | + b"\x5b\xb2\x6e\xb7\x7a\x73\xea\xa4\x78\x13\xf6\xb4" 334 | + b"\x9a\x72\x50\xdc" 335 | ) 336 | self.dev._dev.InternalSendPacket(Packet(magic_cmd)) 337 | 338 | def send_nfc_reboot( 339 | self, 340 | ): 341 | """ 342 | Send magic nfc reboot sequence for solokey, or reboot command for solov2. 343 | """ 344 | 345 | from smartcard.Exceptions import NoCardException, CardConnectionException 346 | 347 | if "solokeys" in sys.argv: 348 | data = b"\x12\x56\xab\xf0" 349 | resp, sw1, sw2 = self.dev.apdu_exchange(header + data) 350 | return sw1 == 0x90 and sw2 == 0x00 351 | else: 352 | # Select root app 353 | apdu = b"\x00\xA4\x04\x00\x09\xA0\x00\x00\x08\x47\x00\x00\x00\x01" 354 | resp, sw1, sw2 = self.dev._conn.transmit(list(apdu)) 355 | did_select = (sw1 == 0x90 and sw2 == 0x00) 356 | if not did_select: 357 | return False 358 | 359 | # Send reboot command 360 | apdu = b"\x00\x53\x00\x00" 361 | try: 362 | resp, sw1, sw2 = self.dev._conn.transmit(list(apdu)) 363 | return sw1 == 0x90 and sw2 == 0x00 364 | except (NoCardException, CardConnectionException): 365 | return True 366 | 367 | def cid( 368 | self, 369 | ): 370 | return self.dev._dev.cid 371 | 372 | def set_cid(self, cid): 373 | if not isinstance(cid, (bytes, bytearray)): 374 | cid = struct.pack("%dB" % len(cid), *[ord(x) for x in cid]) 375 | self.dev._dev.cid = cid 376 | 377 | def recv_raw( 378 | self, 379 | ): 380 | with Timeout(1.0): 381 | cmd, payload = self.dev._dev.InternalRecv() 382 | return cmd, payload 383 | 384 | def check_error(data, err=None): 385 | assert len(data) == 1 386 | if err is None: 387 | if data[0] != 0: 388 | raise CtapError(data[0]) 389 | elif data[0] != err: 390 | raise ValueError("Unexpected error: %02x" % data[0]) 391 | 392 | def register(self, chal, appid, on_keepalive=DeviceSelectCredential(1)): 393 | reg_data = _call_polling( 394 | 0.25, None, on_keepalive, self.ctap1.register, chal, appid 395 | ) 396 | return reg_data 397 | 398 | def authenticate( 399 | self, 400 | chal, 401 | appid, 402 | key_handle, 403 | check_only=False, 404 | on_keepalive=DeviceSelectCredential(1), 405 | ): 406 | auth_data = _call_polling( 407 | 0.25, 408 | None, 409 | on_keepalive, 410 | self.ctap1.authenticate, 411 | chal, 412 | appid, 413 | key_handle, 414 | check_only=check_only, 415 | ) 416 | return auth_data 417 | 418 | def reset( 419 | self, 420 | ): 421 | print("Resetting Authenticator...") 422 | try: 423 | self.ctap2.reset(on_keepalive=DeviceSelectCredential(1)) 424 | except CtapError: 425 | # Some authenticators need a power cycle 426 | print("Need to power cycle authentictor to reset..") 427 | self.reboot() 428 | self.ctap2.reset(on_keepalive=DeviceSelectCredential(1)) 429 | 430 | def sendMC(self, *args, **kwargs): 431 | 432 | if len(args) > 11: 433 | # Add additional arg to calculate pin auth on demand 434 | pin = args[-1] 435 | args = list(args[:-1]) 436 | if args[7] == None and args[8] == None: 437 | pin_token = self.client.pin_protocol.get_pin_token(pin) 438 | pin_auth = hmac_sha256(pin_token, args[0])[:16] 439 | args[7] = pin_auth 440 | args[8] = 1 441 | 442 | attestation_object = self.ctap2.make_credential(*args, **kwargs) 443 | if attestation_object: 444 | verifier = Attestation.for_type(attestation_object.fmt) 445 | client_data = args[0] 446 | verifier().verify( 447 | attestation_object.att_statement, 448 | attestation_object.auth_data, 449 | client_data, 450 | ) 451 | return attestation_object 452 | 453 | def sendGA(self, *args, **kwargs): 454 | if len(args) > 9: 455 | # Add additional arg to calculate pin auth on demand 456 | pin = args[-1] 457 | args = list(args[:-1]) 458 | if args[5] == None and args[6] == None: 459 | pin_token = self.client.pin_protocol.get_pin_token(pin) 460 | pin_auth = hmac_sha256(pin_token, args[1])[:16] 461 | args[5] = pin_auth 462 | args[6] = 1 463 | 464 | return self.ctap2.get_assertion(*args, **kwargs) 465 | 466 | def sendCP(self, *args, **kwargs): 467 | return self.ctap2.client_pin(*args, **kwargs) 468 | 469 | def sendPP(self, *args, **kwargs): 470 | return self.client.pin_protocol.get_pin_token(*args, **kwargs) 471 | 472 | def delay(secs): 473 | time.sleep(secs) 474 | -------------------------------------------------------------------------------- /tests/standard/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trussed-dev/fido2-tests/591d3d2279949e08de0766897f24bcfd39af1339/tests/standard/__init__.py -------------------------------------------------------------------------------- /tests/standard/connect_test.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import tests 4 | 5 | 6 | @pytest.mark.run(order=1) 7 | def test_answer(device): 8 | pass 9 | -------------------------------------------------------------------------------- /tests/standard/fido2/__init__.py: -------------------------------------------------------------------------------- 1 | import time 2 | from functools import cmp_to_key 3 | 4 | import tests 5 | from fido2 import cbor 6 | 7 | 8 | def cbor_key_to_representative(key): 9 | if isinstance(key, int): 10 | if key >= 0: 11 | return (0, key) 12 | return (1, -key) 13 | elif isinstance(key, bytes): 14 | return (2, key) 15 | elif isinstance(key, str): 16 | return (3, key) 17 | else: 18 | raise ValueError(key) 19 | 20 | 21 | def cbor_str_cmp(a, b): 22 | if isinstance(a, str) or isinstance(b, str): 23 | a = a.encode("utf8") 24 | b = b.encode("utf8") 25 | 26 | if len(a) == len(b): 27 | for x, y in zip(a, b): 28 | if x != y: 29 | return x - y 30 | return 0 31 | else: 32 | return len(a) - len(b) 33 | 34 | 35 | def cmp_cbor_keys(a, b): 36 | a = cbor_key_to_representative(a) 37 | b = cbor_key_to_representative(b) 38 | if a[0] != b[0]: 39 | return a[0] - b[0] 40 | if a[0] in (2, 3): 41 | return cbor_str_cmp(a[1], b[1]) 42 | else: 43 | return (a[1] > b[1]) - (a[1] < b[1]) 44 | 45 | 46 | def TestCborKeysSorted(cbor_obj): 47 | # Cbor canonical ordering of keys. 48 | # https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#ctap2-canonical-cbor-encoding-form 49 | 50 | if isinstance(cbor_obj, bytes): 51 | cbor_obj = cbor.loads(cbor_obj)[0] 52 | 53 | if isinstance(cbor_obj, dict): 54 | l = [x for x in cbor_obj] 55 | else: 56 | l = cbor_obj 57 | 58 | if len(l) > 0 and isinstance(l[0], dict): 59 | for obj in l: 60 | TestCborKeysSorted(obj) 61 | return 62 | 63 | l_sorted = sorted(l[:], key=cmp_to_key(cmp_cbor_keys)) 64 | 65 | for i in range(len(l)): 66 | 67 | if isinstance(cbor_obj, dict) and not isinstance(l[i], (str, int)): 68 | raise ValueError(f"Cbor map key {l[i]} must be int or str for CTAP2") 69 | 70 | if l[i] != l_sorted[i]: 71 | raise ValueError(f"Cbor map item {i}: {l[i]} is out of order") 72 | 73 | # value = None 74 | if isinstance(cbor_obj, dict): 75 | value = cbor_obj[l[i]] 76 | else: 77 | value = l[i] 78 | 79 | if isinstance(value, dict): 80 | TestCborKeysSorted(cbor_obj[l[i]]) 81 | 82 | elif isinstance(value, list): 83 | TestCborKeysSorted(cbor_obj[l[i]]) 84 | 85 | return l 86 | 87 | 88 | # hot patch cbor map parsing to test the order of keys in map 89 | _load_map_old = cbor.load_map 90 | 91 | 92 | def _load_map_new(ai, data): 93 | values, data = _load_map_old(ai, data) 94 | TestCborKeysSorted(values) 95 | return values, data 96 | 97 | cbor.load_map = _load_map_new 98 | cbor._DESERIALIZERS[5] = _load_map_new 99 | -------------------------------------------------------------------------------- /tests/standard/fido2/extensions/test_hmac_secret.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from cryptography.hazmat.backends import default_backend 3 | from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes 4 | from fido2.ctap import CtapError 5 | from fido2.utils import hmac_sha256, sha256 6 | 7 | from tests.utils import FidoRequest, shannon_entropy, verify, generate_user 8 | 9 | 10 | def get_salt_params(cipher, shared_secret, salts): 11 | enc = cipher.encryptor() 12 | salt_enc = b"" 13 | for salt in salts: 14 | salt_enc += enc.update(salt) 15 | salt_enc += enc.finalize() 16 | 17 | salt_auth = hmac_sha256(shared_secret, salt_enc)[:16] 18 | return salt_enc, salt_auth 19 | 20 | 21 | salt1 = b"\xa5" * 32 22 | salt2 = b"\x96" * 32 23 | salt3 = b"\x03" * 32 24 | salt4 = b"\x5a" * 16 25 | salt5 = b"\x96" * 64 26 | 27 | 28 | @pytest.fixture(scope="class") 29 | def MCHmacSecret( 30 | resetDevice, 31 | ): 32 | req = FidoRequest(extensions={"hmac-secret": True}, options={"rk": True}) 33 | res = resetDevice.sendMC(*req.toMC()) 34 | setattr(res, "request", req) 35 | return res 36 | 37 | 38 | @pytest.fixture(scope="class") 39 | def sharedSecret(device, MCHmacSecret): 40 | return device.client.pin_protocol.get_shared_secret() 41 | 42 | 43 | @pytest.fixture(scope="class") 44 | def cipher(device, sharedSecret): 45 | key_agreement, shared_secret = sharedSecret 46 | return Cipher( 47 | algorithms.AES(shared_secret), modes.CBC(b"\x00" * 16), default_backend() 48 | ) 49 | 50 | 51 | @pytest.fixture(scope="class") 52 | def fixed_users(): 53 | """ Fixed set of users to enable accounts to get overwritten """ 54 | return [generate_user() for i in range(0, 100)] 55 | 56 | 57 | class TestHmacSecret(object): 58 | def test_hmac_secret_make_credential(self, MCHmacSecret): 59 | assert MCHmacSecret.auth_data.extensions 60 | assert "hmac-secret" in MCHmacSecret.auth_data.extensions 61 | assert MCHmacSecret.auth_data.extensions["hmac-secret"] == True 62 | 63 | def test_hmac_secret_info(self, info): 64 | assert "hmac-secret" in info.extensions 65 | 66 | def test_fake_extension(self, device): 67 | req = FidoRequest(extensions={"tetris": True}) 68 | res = device.sendMC(*req.toMC()) 69 | 70 | def test_get_shared_secret(self, sharedSecret): 71 | pass 72 | 73 | @pytest.mark.parametrize("salts", [(salt1,), (salt1, salt2)]) 74 | def test_hmac_secret_entropy( 75 | self, device, MCHmacSecret, cipher, sharedSecret, salts 76 | ): 77 | key_agreement, shared_secret = sharedSecret 78 | salt_enc, salt_auth = get_salt_params(cipher, shared_secret, salts) 79 | req = FidoRequest( 80 | extensions={"hmac-secret": {1: key_agreement, 2: salt_enc, 3: salt_auth}} 81 | ) 82 | print("key-agreement", key_agreement) 83 | auth = device.sendGA(*req.toGA()) 84 | 85 | ext = auth.auth_data.extensions 86 | assert ext 87 | assert "hmac-secret" in ext 88 | assert isinstance(ext["hmac-secret"], bytes) 89 | assert len(ext["hmac-secret"]) == len(salts) * 32 90 | 91 | verify(MCHmacSecret, auth, req.cdh) 92 | 93 | dec = cipher.decryptor() 94 | key = dec.update(ext["hmac-secret"]) + dec.finalize() 95 | 96 | print(shannon_entropy(ext["hmac-secret"])) 97 | if len(salts) == 1: 98 | assert shannon_entropy(ext["hmac-secret"]) > 4.6 99 | assert shannon_entropy(key) > 4.6 100 | if len(salts) == 2: 101 | assert shannon_entropy(ext["hmac-secret"]) > 5.4 102 | assert shannon_entropy(key) > 5.4 103 | 104 | def get_output(self, device, MCHmacSecret, cipher, sharedSecret, salts): 105 | key_agreement, shared_secret = sharedSecret 106 | salt_enc, salt_auth = get_salt_params(cipher, shared_secret, salts) 107 | req = FidoRequest( 108 | extensions={"hmac-secret": {1: key_agreement, 2: salt_enc, 3: salt_auth}} 109 | ) 110 | auth = device.sendGA(*req.toGA()) 111 | 112 | ext = auth.auth_data.extensions 113 | assert ext 114 | assert "hmac-secret" in ext 115 | assert isinstance(ext["hmac-secret"], bytes) 116 | assert len(ext["hmac-secret"]) == len(salts) * 32 117 | 118 | verify(MCHmacSecret, auth, req.cdh) 119 | 120 | dec = cipher.decryptor() 121 | output = dec.update(ext["hmac-secret"]) + dec.finalize() 122 | 123 | if len(salts) == 2: 124 | return (output[0:32], output[32:64]) 125 | else: 126 | return output 127 | 128 | def test_hmac_secret_sanity(self, device, MCHmacSecret, cipher, sharedSecret): 129 | output1 = self.get_output(device, MCHmacSecret, cipher, sharedSecret, (salt1,)) 130 | output12 = self.get_output( 131 | device, MCHmacSecret, cipher, sharedSecret, (salt1, salt2) 132 | ) 133 | output21 = self.get_output( 134 | device, MCHmacSecret, cipher, sharedSecret, (salt2, salt1) 135 | ) 136 | 137 | assert output12[0] == output1 138 | assert output21[1] == output1 139 | assert output21[0] == output12[1] 140 | assert output12[0] != output12[1] 141 | 142 | def test_missing_keyAgreement(self, device, cipher, sharedSecret): 143 | key_agreement, shared_secret = sharedSecret 144 | 145 | salt_enc, salt_auth = get_salt_params(cipher, shared_secret, (salt3,)) 146 | 147 | req = FidoRequest(extensions={"hmac-secret": {2: salt_enc, 3: salt_auth}}) 148 | 149 | with pytest.raises(CtapError): 150 | device.sendGA(*req.toGA()) 151 | 152 | def test_missing_saltAuth(self, device, cipher, sharedSecret): 153 | key_agreement, shared_secret = sharedSecret 154 | 155 | salt_enc, salt_auth = get_salt_params(cipher, shared_secret, (salt3,)) 156 | 157 | req = FidoRequest(extensions={"hmac-secret": {1: key_agreement, 2: salt_enc}}) 158 | 159 | with pytest.raises(CtapError) as e: 160 | device.sendGA(*req.toGA()) 161 | assert e.value.code == CtapError.ERR.MISSING_PARAMETER 162 | 163 | def test_missing_saltEnc(self, device, cipher, sharedSecret): 164 | key_agreement, shared_secret = sharedSecret 165 | 166 | salt_enc, salt_auth = get_salt_params(cipher, shared_secret, (salt3,)) 167 | 168 | req = FidoRequest(extensions={"hmac-secret": {1: key_agreement, 3: salt_auth}}) 169 | 170 | with pytest.raises(CtapError) as e: 171 | device.sendGA(*req.toGA()) 172 | assert e.value.code == CtapError.ERR.MISSING_PARAMETER 173 | 174 | def test_bad_auth(self, device, cipher, sharedSecret): 175 | 176 | key_agreement, shared_secret = sharedSecret 177 | 178 | salt_enc, salt_auth = get_salt_params(cipher, shared_secret, (salt3,)) 179 | 180 | bad_auth = list(salt_auth[:]) 181 | bad_auth[len(bad_auth) // 2] = bad_auth[len(bad_auth) // 2] ^ 1 182 | bad_auth = bytes(bad_auth) 183 | 184 | req = FidoRequest( 185 | extensions={"hmac-secret": {1: key_agreement, 2: salt_enc, 3: bad_auth}} 186 | ) 187 | 188 | with pytest.raises(CtapError) as e: 189 | device.sendGA(*req.toGA()) 190 | assert e.value.code == CtapError.ERR.EXTENSION_FIRST 191 | 192 | @pytest.mark.parametrize("salts", [(salt4,), (salt4, salt5)]) 193 | def test_invalid_salt_length(self, device, cipher, sharedSecret, salts): 194 | key_agreement, shared_secret = sharedSecret 195 | salt_enc, salt_auth = get_salt_params(cipher, shared_secret, salts) 196 | 197 | req = FidoRequest( 198 | extensions={"hmac-secret": {1: key_agreement, 2: salt_enc, 3: salt_auth}} 199 | ) 200 | 201 | with pytest.raises(CtapError) as e: 202 | device.sendGA(*req.toGA()) 203 | assert e.value.code in [ CtapError.ERR.INVALID_LENGTH, CtapError.ERR.INVALID_CBOR ] 204 | 205 | @pytest.mark.parametrize("salts", [(salt1,), (salt1, salt2)]) 206 | def test_get_next_assertion_has_extension( 207 | self, device, MCHmacSecret, cipher, sharedSecret, salts, fixed_users 208 | ): 209 | """ Check that get_next_assertion properly returns extension information for multiple accounts. """ 210 | accounts = 3 211 | regs = [] 212 | auths = [] 213 | rp = {"id": f"example_salts_{len(salts)}.org", "name": "ExampleRP_2"} 214 | 215 | for i in range(0, accounts): 216 | req = FidoRequest( 217 | extensions={"hmac-secret": True}, 218 | options={"rk": True}, 219 | rp=rp, 220 | user=fixed_users[i], 221 | ) 222 | res = device.sendMC(*req.toMC()) 223 | regs.append(res) 224 | 225 | key_agreement, shared_secret = sharedSecret 226 | salt_enc, salt_auth = get_salt_params(cipher, shared_secret, salts) 227 | req = FidoRequest( 228 | extensions={"hmac-secret": {1: key_agreement, 2: salt_enc, 3: salt_auth}}, 229 | rp=rp, 230 | ) 231 | 232 | auth = device.sendGA(*req.toGA()) 233 | assert auth.number_of_credentials == accounts 234 | 235 | auths.append(auth) 236 | for i in range(0, accounts - 1): 237 | auths.append(device.ctap2.get_next_assertion()) 238 | 239 | for x in auths: 240 | assert x.auth_data.flags & (1 << 7) # has extension 241 | ext = auth.auth_data.extensions 242 | assert ext 243 | assert "hmac-secret" in ext 244 | assert isinstance(ext["hmac-secret"], bytes) 245 | assert len(ext["hmac-secret"]) == len(salts) * 32 246 | dec = cipher.decryptor() 247 | key = dec.update(ext["hmac-secret"]) + dec.finalize() 248 | 249 | auths.reverse() 250 | for x, y in zip(regs, auths): 251 | verify(x, y, req.cdh) 252 | 253 | 254 | class TestHmacSecretUV(object): 255 | def test_hmac_secret_different_with_uv( 256 | self, device, MCHmacSecret, cipher, sharedSecret 257 | ): 258 | salts = [salt1] 259 | key_agreement, shared_secret = sharedSecret 260 | salt_enc, salt_auth = get_salt_params(cipher, shared_secret, salts) 261 | req = FidoRequest( 262 | extensions={"hmac-secret": {1: key_agreement, 2: salt_enc, 3: salt_auth}} 263 | ) 264 | auth_no_uv = device.sendGA(*req.toGA()) 265 | assert (auth_no_uv.auth_data.flags & (1 << 2)) == 0 266 | 267 | ext_no_uv = auth_no_uv.auth_data.extensions 268 | assert ext_no_uv 269 | assert "hmac-secret" in ext_no_uv 270 | assert isinstance(ext_no_uv["hmac-secret"], bytes) 271 | assert len(ext_no_uv["hmac-secret"]) == len(salts) * 32 272 | 273 | verify(MCHmacSecret, auth_no_uv, req.cdh) 274 | 275 | # Now get same auth with UV 276 | pin = "1234" 277 | device.client.pin_protocol.set_pin(pin) 278 | pin_token = device.client.pin_protocol.get_pin_token(pin) 279 | pin_auth = hmac_sha256(pin_token, req.cdh)[:16] 280 | 281 | req = FidoRequest( 282 | req, 283 | pin_protocol=1, 284 | pin_auth=pin_auth, 285 | extensions={"hmac-secret": {1: key_agreement, 2: salt_enc, 3: salt_auth}}, 286 | ) 287 | 288 | auth_uv = device.sendGA(*req.toGA()) 289 | assert auth_uv.auth_data.flags & (1 << 2) 290 | ext_uv = auth_uv.auth_data.extensions 291 | assert ext_uv 292 | assert "hmac-secret" in ext_uv 293 | assert isinstance(ext_uv["hmac-secret"], bytes) 294 | assert len(ext_uv["hmac-secret"]) == len(salts) * 32 295 | 296 | verify(MCHmacSecret, auth_uv, req.cdh) 297 | 298 | # Now see if the hmac-secrets are different 299 | assert ext_no_uv["hmac-secret"] != ext_uv["hmac-secret"] 300 | -------------------------------------------------------------------------------- /tests/standard/fido2/pin/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trussed-dev/fido2-tests/591d3d2279949e08de0766897f24bcfd39af1339/tests/standard/fido2/pin/__init__.py -------------------------------------------------------------------------------- /tests/standard/fido2/pin/test_lockout.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import pytest 3 | from fido2.ctap import CtapError 4 | from fido2.ctap2 import ES256, AttestedCredentialData, PinProtocolV1 5 | 6 | from tests.utils import * 7 | 8 | 9 | @pytest.mark.skipif( 10 | "trezor" in sys.argv, reason="ClientPin is not supported on Trezor." 11 | ) 12 | def test_lockout(device, resetDevice): 13 | pin = "TestPin" 14 | device.client.pin_protocol.set_pin(pin) 15 | 16 | pin_token = device.client.pin_protocol.get_pin_token(pin) 17 | req = FidoRequest(pin_token=pin_token) 18 | 19 | req.pin_auth = hmac_sha256(pin_token, req.cdh)[:16] 20 | 21 | for i in range(1, 10): 22 | err = CtapError.ERR.PIN_INVALID 23 | if i in (3, 6): 24 | err = CtapError.ERR.PIN_AUTH_BLOCKED 25 | elif i >= 8: 26 | err = [CtapError.ERR.PIN_BLOCKED, CtapError.ERR.PIN_INVALID] 27 | 28 | with pytest.raises(CtapError) as e: 29 | device.sendPP("WrongPin") 30 | assert e.value.code == err or e.value.code in err 31 | 32 | attempts = 8 - i 33 | if i > 8: 34 | attempts = 0 35 | 36 | res = device.ctap2.client_pin(1, PinProtocolV1.CMD.GET_RETRIES) 37 | assert res[3] == attempts 38 | 39 | if err == CtapError.ERR.PIN_AUTH_BLOCKED: 40 | device.reboot() 41 | 42 | with pytest.raises(CtapError) as e: 43 | device.sendMC(*req.toMC()) 44 | 45 | device.reboot() 46 | 47 | with pytest.raises(CtapError) as e: 48 | device.sendPP(pin) 49 | assert e.value.code == CtapError.ERR.PIN_BLOCKED 50 | -------------------------------------------------------------------------------- /tests/standard/fido2/pin/test_pin.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import pytest 3 | from fido2.ctap import CtapError 4 | from fido2.ctap2 import ES256, AttestedCredentialData, PinProtocolV1 5 | 6 | from tests.utils import * 7 | 8 | PIN1 = "123456789A" 9 | PIN2 = "ABCDEF" 10 | 11 | 12 | @pytest.fixture(scope="class", params=[PIN1]) 13 | def SetPinRes(request, device): 14 | device.reset() 15 | 16 | pin = request.param 17 | req = FidoRequest() 18 | 19 | device.client.pin_protocol.set_pin(pin) 20 | 21 | req = FidoRequest(req, pin = pin) 22 | 23 | res = device.sendMC(*req.toMC()) 24 | setattr(res, "request", req) 25 | setattr(res, "PIN", pin) 26 | return res 27 | 28 | 29 | @pytest.fixture(scope="class") 30 | def CPRes(request, device, SetPinRes): 31 | res = device.sendCP(1, PinProtocolV1.CMD.GET_KEY_AGREEMENT) 32 | return res 33 | 34 | 35 | @pytest.fixture(scope="class") 36 | def MCPinRes(device, SetPinRes): 37 | req = FidoRequest(SetPinRes) 38 | res = device.sendMC(*req.toMC()) 39 | setattr(res, "request", req) 40 | return res 41 | 42 | 43 | @pytest.fixture(scope="class") 44 | def GAPinRes(device, MCPinRes): 45 | req = FidoRequest(MCPinRes) 46 | res = device.sendGA(*req.toGA()) 47 | setattr(res, "request", req) 48 | return res 49 | 50 | 51 | @pytest.mark.skipif( 52 | "trezor" in sys.argv, reason="ClientPin is not supported on Trezor." 53 | ) 54 | class TestPin(object): 55 | def test_pin(self, CPRes): 56 | pass 57 | 58 | def test_set_pin_twice(self, device, SetPinRes): 59 | """ Setting pin when a pin is already set should result in error NotAllowed. """ 60 | with pytest.raises(CtapError) as e: 61 | device.client.pin_protocol.set_pin('1234') 62 | 63 | assert e.value.code == CtapError.ERR.NOT_ALLOWED 64 | 65 | 66 | def test_get_key_agreement_fields(self, CPRes): 67 | key = CPRes[1] 68 | assert "Is public key" and key[1] == 2 69 | assert "Is P256" and key[-1] == 1 70 | assert "Is ALG_ECDH_ES_HKDF_256" and key[3] == -25 71 | 72 | assert "Right key" and len(key[-3]) == 32 and isinstance(key[-3], bytes) 73 | 74 | def test_verify_flag(self, device, SetPinRes): 75 | reg = device.sendMC(*FidoRequest(SetPinRes).toMC()) 76 | assert reg.auth_data.flags & (1 << 2) 77 | 78 | def test_get_no_pin_auth(self, device, SetPinRes): 79 | 80 | reg = device.sendMC(*FidoRequest(SetPinRes).toMC()) 81 | allow_list = [ 82 | {"type": "public-key", "id": reg.auth_data.credential_data.credential_id} 83 | ] 84 | auth = device.sendGA( 85 | *FidoRequest( 86 | SetPinRes, allow_list=allow_list, pin_auth=None, pin_protocol=None, pin = None, 87 | ).toGA() 88 | ) 89 | 90 | assert not (auth.auth_data.flags & (1 << 2)) 91 | 92 | with pytest.raises(CtapError) as e: 93 | reg = device.sendMC( 94 | *FidoRequest(SetPinRes, pin_auth=None, pin_protocol=None, pin = None).toMC() 95 | ) 96 | 97 | assert e.value.code == CtapError.ERR.PIN_REQUIRED 98 | 99 | def test_zero_length_pin_auth(self, device, SetPinRes): 100 | with pytest.raises(CtapError) as e: 101 | reg = device.sendMC(*FidoRequest(SetPinRes, pin_auth=b"").toMC()) 102 | assert e.value.code == CtapError.ERR.PIN_AUTH_INVALID 103 | 104 | with pytest.raises(CtapError) as e: 105 | reg = device.sendGA(*FidoRequest(SetPinRes, pin_auth=b"").toGA()) 106 | assert e.value.code == CtapError.ERR.PIN_AUTH_INVALID 107 | 108 | def test_make_credential_no_pin(self, device, SetPinRes): 109 | with pytest.raises(CtapError) as e: 110 | reg = device.sendMC(*FidoRequest().toMC()) 111 | assert e.value.code == CtapError.ERR.PIN_REQUIRED 112 | 113 | def test_get_assertion_no_pin(self, device, SetPinRes): 114 | with pytest.raises(CtapError) as e: 115 | reg = device.sendGA(*FidoRequest().toGA()) 116 | assert e.value.code == CtapError.ERR.NO_CREDENTIALS 117 | 118 | class TestChangePin: 119 | def test_change_pin(self, device, SetPinRes): 120 | device.client.pin_protocol.change_pin(PIN1, PIN2) 121 | 122 | req = FidoRequest(SetPinRes.request, pin = PIN2) 123 | 124 | reg = device.sendMC(*req.toMC()) 125 | auth = device.sendGA( 126 | *FidoRequest( 127 | req, 128 | allow_list=[ 129 | { 130 | "type": "public-key", 131 | "id": reg.auth_data.credential_data.credential_id, 132 | } 133 | ], 134 | ).toGA() 135 | ) 136 | 137 | assert reg.auth_data.flags & (1 << 2) 138 | assert auth.auth_data.flags & (1 << 2) 139 | 140 | verify(reg, auth, cdh=SetPinRes.request.cdh) 141 | 142 | class TestPinAttempts: 143 | @pytest.mark.skipif( 144 | "trezor" in sys.argv, reason="ClientPin is not supported on Trezor." 145 | ) 146 | def test_pin_attempts(self, device, SetPinRes): 147 | # Flip 1 bit 148 | pin = SetPinRes.PIN 149 | pin_wrong = list(pin) 150 | c = pin[len(pin) // 2] 151 | 152 | pin_wrong[len(pin) // 2] = chr(ord(c) ^ 1) 153 | pin_wrong = "".join(pin_wrong) 154 | 155 | for i in range(1, 3): 156 | with pytest.raises(CtapError) as e: 157 | device.sendPP(pin_wrong) 158 | assert e.value.code == CtapError.ERR.PIN_INVALID 159 | 160 | print("Check there is %d pin attempts left" % (8 - i)) 161 | res = device.ctap2.client_pin(1, PinProtocolV1.CMD.GET_RETRIES) 162 | assert res[3] == (8 - i) 163 | 164 | for i in range(1, 3): 165 | with pytest.raises(CtapError) as e: 166 | device.sendPP(pin_wrong) 167 | assert e.value.code == CtapError.ERR.PIN_AUTH_BLOCKED 168 | 169 | device.reboot() 170 | 171 | reg = device.sendMC(*FidoRequest(SetPinRes, pin = pin).toMC()) 172 | 173 | res = device.ctap2.client_pin(1, PinProtocolV1.CMD.GET_RETRIES) 174 | assert res[3] == (8) 175 | -------------------------------------------------------------------------------- /tests/standard/fido2/pin/test_set_pin.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import pytest 3 | from fido2.ctap import CtapError 4 | from fido2.ctap2 import ES256, AttestedCredentialData, PinProtocolV1 5 | 6 | from tests.utils import * 7 | 8 | 9 | @pytest.mark.skipif( 10 | "trezor" in sys.argv, reason="ClientPin is not supported on Trezor." 11 | ) 12 | class TestSetPin(object): 13 | def test_send_zero_length_pin_auth(self, resetDevice): 14 | with pytest.raises(CtapError) as e: 15 | reg = resetDevice.sendMC(*FidoRequest(pin_auth=b"").toMC()) 16 | assert e.value.code == CtapError.ERR.PIN_NOT_SET 17 | 18 | with pytest.raises(CtapError) as e: 19 | reg = resetDevice.sendGA(*FidoRequest(pin_auth=b"").toGA()) 20 | assert e.value.code in (CtapError.ERR.PIN_NOT_SET, CtapError.ERR.NO_CREDENTIALS) 21 | 22 | def test_set_pin(self, device): 23 | device.reset() 24 | device.client.pin_protocol.set_pin("TestPin") 25 | device.reset() 26 | 27 | def test_set_pin_too_big(self, device): 28 | with pytest.raises(CtapError) as e: 29 | device.client.pin_protocol.set_pin("A" * 64) 30 | assert e.value.code == CtapError.ERR.PIN_POLICY_VIOLATION 31 | 32 | def test_get_pin_token_but_no_pin_set(self, device): 33 | with pytest.raises(CtapError) as e: 34 | device.client.pin_protocol.get_pin_token("TestPin") 35 | assert e.value.code == CtapError.ERR.PIN_NOT_SET 36 | 37 | def test_change_pin_but_no_pin_set(self, device): 38 | with pytest.raises(CtapError) as e: 39 | device.client.pin_protocol.change_pin("TestPin", "1234") 40 | assert e.value.code == CtapError.ERR.PIN_NOT_SET 41 | 42 | def test_setting_pin_and_get_info(self, device): 43 | device.reset() 44 | device.client.pin_protocol.set_pin("TestPin") 45 | 46 | with pytest.raises(CtapError) as e: 47 | device.client.pin_protocol.set_pin("TestPin") 48 | 49 | info = device.ctap2.get_info() 50 | 51 | assert info.options["clientPin"] 52 | 53 | pin_token = device.client.pin_protocol.get_pin_token("TestPin") 54 | 55 | res = device.sendCP(1, PinProtocolV1.CMD.GET_RETRIES) 56 | assert res[3] == 8 57 | 58 | device.reset() 59 | -------------------------------------------------------------------------------- /tests/standard/fido2/test_ctap1_interop.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from fido2.ctap import CtapError 3 | from fido2.ctap2 import ES256, AttestedCredentialData, PinProtocolV1 4 | from fido2.utils import hmac_sha256, sha256 5 | 6 | from tests.utils import FidoRequest 7 | 8 | 9 | # Test U2F register works with FIDO2 auth 10 | class TestCtap1WithCtap2(object): 11 | def test_ctap1_register(self, RegRes): 12 | RegRes.verify(RegRes.request.appid, RegRes.request.challenge) 13 | 14 | def test_ctap1_authenticate(self, RegRes, AuthRes): 15 | AuthRes.verify( 16 | AuthRes.request.appid, AuthRes.request.challenge, RegRes.public_key 17 | ) 18 | 19 | def test_authenticate_ctap1_through_ctap2(self, device, RegRes): 20 | req = FidoRequest(allow_list=[{"id": RegRes.key_handle, "type": "public-key"}]) 21 | 22 | auth = device.sendGA(*req.toGA()) 23 | 24 | credential_data = AttestedCredentialData.from_ctap1( 25 | RegRes.key_handle, RegRes.public_key 26 | ) 27 | auth.verify(req.cdh, credential_data.public_key) 28 | assert auth.credential["id"] == RegRes.key_handle 29 | 30 | 31 | # Test FIDO2 register works with U2F auth 32 | class TestCtap2WithCtap1(object): 33 | def test_ctap1_authenticate(self, MCRes, device): 34 | req = FidoRequest() 35 | key_handle = MCRes.auth_data.credential_data.credential_id 36 | if len(key_handle) <= 255: 37 | res = device.authenticate(req.challenge, req.appid, key_handle) 38 | 39 | credential_data = AttestedCredentialData(MCRes.auth_data.credential_data) 40 | pubkey_string = ( 41 | b"\x04" 42 | + credential_data.public_key[-2] 43 | + credential_data.public_key[-3] 44 | ) 45 | 46 | res.verify(req.appid, req.challenge, pubkey_string) 47 | else: 48 | print("ctap2 credId is longer than 255 bytes, cannot use with U2F.") 49 | -------------------------------------------------------------------------------- /tests/standard/fido2/test_get_assertion.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import pytest 3 | from cryptography.exceptions import InvalidSignature 4 | from fido2.ctap import CtapError 5 | from fido2.utils import hmac_sha256, sha256 6 | 7 | from tests.utils import * 8 | 9 | 10 | class TestGetAssertion(object): 11 | def test_get_assertion(self, device, MCRes, GARes): 12 | verify(MCRes, GARes) 13 | 14 | def test_assertion_auth_data(self, GARes): 15 | assert len(GARes.auth_data) == 37 16 | assert sha256(GARes.request.rp["id"].encode()) == GARes.auth_data.rp_id_hash 17 | 18 | def test_Check_that_AT_flag_is_not_set(self, GARes): 19 | assert (GARes.auth_data.flags & 0xF8) == 0 20 | 21 | def test_that_user_credential_and_numberOfCredentials_are_not_present(self, GARes): 22 | assert GARes.user == None 23 | assert GARes.number_of_credentials == None 24 | 25 | def test_empty_allowList(self, device): 26 | with pytest.raises(CtapError) as e: 27 | device.sendGA(*FidoRequest(allow_list=[]).toGA()) 28 | assert e.value.code == CtapError.ERR.NO_CREDENTIALS 29 | 30 | def test_get_assertion_allow_list_filtering_and_buffering(self, device): 31 | """ Check that authenticator filters and stores items in allow list correctly """ 32 | allow_list = [] 33 | 34 | rp1 = {"id": "rp1.com", "name": "rp1.com"} 35 | rp2 = {"id": "rp2.com", "name": "rp2.com"} 36 | req1 = FidoRequest(rp=rp1) 37 | req2 = FidoRequest(rp=rp2) 38 | 39 | rp1_registrations = [] 40 | rp2_registrations = [] 41 | rp1_assertions = [] 42 | rp2_assertions = [] 43 | 44 | l1 = 4 45 | for i in range(0, l1): 46 | res = device.sendMC(*req1.toMC()) 47 | rp1_registrations.append(res) 48 | allow_list.append({ 49 | "id": res.auth_data.credential_data.credential_id[:], 50 | "type": "public-key", 51 | }) 52 | 53 | l2 = 6 54 | for i in range(0, l2): 55 | res = device.sendMC(*req2.toMC()) 56 | rp2_registrations.append(res) 57 | allow_list.append({ 58 | "id": res.auth_data.credential_data.credential_id[:], 59 | "type": "public-key", 60 | }) 61 | 62 | req1 = FidoRequest(req1, allow_list = allow_list) 63 | req2 = FidoRequest(req2, allow_list = allow_list) 64 | 65 | # CTAP 2.1: If allowlist is passed, only one (any) applicable 66 | # credential signs, and numberOfCredentials = None is returned. 67 | # 68 | # 69 | # CTAP 2.0: Expects the authenticator to return the total number 70 | # even when allowlist is passed (and hence keep the credential IDs 71 | # cached. 72 | 73 | # Should authenticate to all credentials matching rp1 74 | ga_res1 = device.sendGA(*req1.toGA()) 75 | rp1_assertions.append(ga_res1) 76 | if ga_res1.number_of_credentials != None: 77 | for _ in range(l1 - 1): 78 | rp1_assertions.append(device.ctap2.get_next_assertion()) 79 | 80 | # Should authenticate to all credentials matching rp2 81 | ga_res2 = device.sendGA(*req2.toGA()) 82 | rp2_assertions.append(ga_res2) 83 | if ga_res2.number_of_credentials != None: 84 | for _ in range(l2 - 1): 85 | rp2_assertions.append(device.ctap2.get_next_assertion()) 86 | 87 | counts = ( 88 | ga_res1.number_of_credentials, 89 | ga_res2.number_of_credentials) 90 | 91 | assert counts in [(None, None), (l1, l2)] 92 | 93 | if counts != (None, None): 94 | # Assertions return in order of most recently created credential. 95 | rp1_assertions.reverse() 96 | rp2_assertions.reverse() 97 | 98 | for (reg, auth) in zip(rp1_registrations, rp1_assertions): 99 | verify(reg, auth, req1.cdh) 100 | for (reg, auth) in zip(rp2_registrations, rp2_assertions): 101 | verify(reg, auth, req2.cdh) 102 | 103 | else: 104 | rp1_verifs = 0 105 | for reg in rp1_registrations: 106 | try: 107 | verify(reg, ga_res1, req1.cdh) 108 | rp1_verifs += 1 109 | except InvalidSignature: 110 | pass 111 | assert rp1_verifs == 1 112 | 113 | rp2_verifs = 0 114 | for reg in rp2_registrations: 115 | try: 116 | verify(reg, ga_res2, req2.cdh) 117 | rp2_verifs += 1 118 | except InvalidSignature: 119 | pass 120 | assert rp2_verifs == 1 121 | 122 | def test_corrupt_credId(self, device, MCRes): 123 | # apply bit flip 124 | badid = list(MCRes.auth_data.credential_data.credential_id[:]) 125 | badid[len(badid) // 2] = badid[len(badid) // 2] ^ 1 126 | badid = bytes(badid) 127 | 128 | allow_list = [{"id": badid, "type": "public-key"}] 129 | 130 | with pytest.raises(CtapError) as e: 131 | device.sendGA(*FidoRequest(allow_list=allow_list).toGA()) 132 | assert e.value.code == CtapError.ERR.NO_CREDENTIALS 133 | 134 | def test_mismatched_rp(self, device, GARes): 135 | rp_id = GARes.request.rp["id"][:] 136 | rp_name = GARes.request.rp["name"][:] 137 | rp_id += ".com" 138 | 139 | mismatch_rp = {"id": rp_id, "name": rp_name} 140 | 141 | with pytest.raises(CtapError) as e: 142 | device.sendGA(*FidoRequest(GARes, rp=mismatch_rp).toGA()) 143 | assert e.value.code == CtapError.ERR.NO_CREDENTIALS 144 | 145 | def test_missing_rp(self, device, GARes): 146 | with pytest.raises(CtapError) as e: 147 | device.sendGA(*FidoRequest(GARes, rp=None).toGA()) 148 | assert e.value.code == CtapError.ERR.MISSING_PARAMETER 149 | 150 | def test_bad_rp(self, device, GARes): 151 | 152 | with pytest.raises(CtapError) as e: 153 | device.sendGA(*FidoRequest(GARes, rp={"id": {"type": "wrong"}}).toGA()) 154 | 155 | def test_missing_cdh(self, device, GARes): 156 | with pytest.raises(CtapError) as e: 157 | device.sendGA(*FidoRequest(GARes, cdh=None).toGA()) 158 | assert e.value.code == CtapError.ERR.MISSING_PARAMETER 159 | 160 | def test_bad_cdh(self, device, GARes): 161 | with pytest.raises(CtapError) as e: 162 | device.sendGA(*FidoRequest(GARes, cdh={"type": "wrong"}).toGA()) 163 | 164 | def test_bad_allow_list(self, device, GARes): 165 | with pytest.raises(CtapError) as e: 166 | device.sendGA(*FidoRequest(GARes, allow_list={"type": "wrong"}).toGA()) 167 | 168 | def test_bad_allow_list_item(self, device, GARes): 169 | with pytest.raises(CtapError) as e: 170 | device.sendGA( 171 | *FidoRequest( 172 | GARes, allow_list=["wrong"] + GARes.request.allow_list 173 | ).toGA() 174 | ) 175 | 176 | def test_unknown_option(self, device, GARes): 177 | device.sendGA(*FidoRequest(GARes, options={"unknown": True}).toGA()) 178 | 179 | @pytest.mark.skipif( 180 | "trezor" in sys.argv, 181 | reason="User verification flag is intentionally set to true on Trezor even when user verification is not configured. (Otherwise some services refuse registration without giving a reason.)", 182 | ) 183 | def test_option_uv(self, device, info, GARes): 184 | if "uv" in info.options: 185 | if info.options["uv"]: 186 | res = device.sendGA(*FidoRequest(GARes, options={"uv": True}).toGA()) 187 | assert res.auth_data.flags & (1 << 2) 188 | 189 | def test_option_up(self, device, info, GARes): 190 | if "up" in info.options: 191 | if info.options["up"]: 192 | res = device.sendGA(*FidoRequest(GARes, options={"up": True}).toGA()) 193 | assert res.auth_data.flags & (1 << 0) 194 | 195 | def test_allow_list_fake_item(self, device, GARes): 196 | device.sendGA( 197 | *FidoRequest( 198 | GARes, 199 | allow_list=[{"type": "rot13", "id": b"1234"}] 200 | + GARes.request.allow_list, 201 | ).toGA() 202 | ) 203 | 204 | def test_allow_list_missing_field(self, device, GARes): 205 | with pytest.raises(CtapError) as e: 206 | device.sendGA( 207 | *FidoRequest( 208 | GARes, allow_list=[{"id": b"1234"}] + GARes.request.allow_list 209 | ).toGA() 210 | ) 211 | 212 | def test_allow_list_field_wrong_type(self, device, GARes): 213 | with pytest.raises(CtapError) as e: 214 | device.sendGA( 215 | *FidoRequest( 216 | GARes, 217 | allow_list=[{"type": b"public-key", "id": b"1234"}] 218 | + GARes.request.allow_list, 219 | ).toGA() 220 | ) 221 | 222 | def test_allow_list_id_wrong_type(self, device, GARes): 223 | with pytest.raises(CtapError) as e: 224 | device.sendGA( 225 | *FidoRequest( 226 | GARes, 227 | allow_list=[{"type": "public-key", "id": 42}] 228 | + GARes.request.allow_list, 229 | ).toGA() 230 | ) 231 | 232 | def test_allow_list_missing_id(self, device, GARes): 233 | with pytest.raises(CtapError) as e: 234 | device.sendGA( 235 | *FidoRequest( 236 | GARes, 237 | allow_list=[{"type": "public-key"}] + GARes.request.allow_list, 238 | ).toGA() 239 | ) 240 | 241 | def test_user_presence_option_false(self, device, MCRes, GARes): 242 | res = device.sendGA(*FidoRequest(GARes, options={"up": False}).toGA()) 243 | 244 | try: 245 | verify(MCRes, res, GARes.request.cdh) 246 | except InvalidSignature: 247 | if "trezor" not in sys.argv: 248 | raise 249 | 250 | if "--nfc" not in sys.argv: 251 | assert (res.auth_data.flags & 1) == 0 252 | 253 | 254 | @pytest.mark.skipif("trezor" in sys.argv, reason="Reboot is not supported on Trezor.") 255 | class TestGetAssertionAfterBoot(object): 256 | def test_assertion_after_reboot(self, rebootedDevice, MCRes, GARes): 257 | credential_data = AttestedCredentialData(MCRes.auth_data.credential_data) 258 | verify(MCRes, GARes) 259 | -------------------------------------------------------------------------------- /tests/standard/fido2/test_getinfo.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import pytest 3 | from fido2 import cbor 4 | from fido2.ctap import CtapError 5 | 6 | from tests.utils import * 7 | from tests.standard.fido2 import TestCborKeysSorted 8 | 9 | 10 | def test_get_info(info): 11 | pass 12 | 13 | 14 | def test_get_info_version(info): 15 | assert "FIDO_2_0" in info.versions 16 | 17 | 18 | def test_Check_pin_protocols_field(info): 19 | if len(info.pin_protocols): 20 | assert sum(info.pin_protocols) > 0 21 | 22 | 23 | def test_Check_options_field(info): 24 | for x in info.options: 25 | assert info.options[x] in [True, False] 26 | 27 | 28 | @pytest.mark.skipif( 29 | "trezor" in sys.argv, 30 | reason="User verification flag is intentionally set to true on Trezor even when user verification is not configured. (Otherwise some services refuse registration without giving a reason.)", 31 | ) 32 | def test_Check_uv_option(device, info): 33 | if "uv" in info.options: 34 | if info.options["uv"]: 35 | device.sendMC(*FidoRequest(options={"uv": True}).toMC()) 36 | 37 | 38 | def test_Check_up_option(device, info): 39 | if "up" in info.options: 40 | if info.options["up"]: 41 | with pytest.raises(CtapError) as e: 42 | device.sendMC(*FidoRequest(options={"up": True}).toMC()) 43 | assert e.value.code == CtapError.ERR.INVALID_OPTION 44 | 45 | 46 | def test_self_cbor_sorting(): 47 | cbor_key_list_sorted = [ 48 | 0, 49 | 1, 50 | 1, 51 | 2, 52 | 3, 53 | -1, 54 | -2, 55 | "b", 56 | "c", 57 | "aa", 58 | "aaa", 59 | "aab", 60 | "baa", 61 | "bbb", 62 | ] 63 | TestCborKeysSorted(cbor_key_list_sorted) 64 | 65 | 66 | def test_self_cbor_integers(): 67 | with pytest.raises(ValueError) as e: 68 | TestCborKeysSorted([1, 0]) 69 | 70 | 71 | def test_self_cbor_major_type(): 72 | with pytest.raises(ValueError) as e: 73 | TestCborKeysSorted([-1, 0]) 74 | 75 | 76 | def test_self_cbor_strings(): 77 | with pytest.raises(ValueError) as e: 78 | TestCborKeysSorted(["bb", "a"]) 79 | 80 | 81 | def test_self_cbor_same_length_strings(): 82 | with pytest.raises(ValueError) as e: 83 | TestCborKeysSorted(["ab", "aa"]) 84 | -------------------------------------------------------------------------------- /tests/standard/fido2/test_make_credential.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from fido2.ctap import CtapError 3 | from fido2.ctap2 import ES256, AttestedCredentialData, PinProtocolV1 4 | from fido2.cose import EdDSA 5 | from fido2.utils import hmac_sha256, sha256 6 | 7 | from tests.utils import FidoRequest, verify 8 | 9 | 10 | class TestMakeCredential(object): 11 | def test_make_credential(self, MCRes): 12 | pass 13 | 14 | def test_attestation_format(self, MCRes): 15 | assert MCRes.fmt in ["packed", "tpm", "android-key", "adroid-safetynet"] 16 | 17 | def test_authdata_length(self, MCRes): 18 | assert len(MCRes.auth_data) >= 77 19 | 20 | def test_missing_cdh(self, device, MCRes): 21 | req = FidoRequest(MCRes, cdh=None) 22 | 23 | with pytest.raises(CtapError) as e: 24 | device.sendMC(*req.toMC()) 25 | 26 | assert e.value.code == CtapError.ERR.MISSING_PARAMETER 27 | 28 | def test_bad_type_cdh(self, device, MCRes): 29 | req = FidoRequest(MCRes, cdh=5) 30 | 31 | with pytest.raises(CtapError) as e: 32 | device.sendMC(*req.toMC()) 33 | 34 | def test_missing_user(self, device, MCRes): 35 | req = FidoRequest(MCRes, user=None) 36 | 37 | with pytest.raises(CtapError) as e: 38 | device.sendMC(*req.toMC()) 39 | 40 | assert e.value.code == CtapError.ERR.MISSING_PARAMETER 41 | 42 | def test_bad_type_user(self, device, MCRes): 43 | req = FidoRequest(MCRes, user=b"1234abcdf") 44 | 45 | with pytest.raises(CtapError) as e: 46 | device.sendMC(*req.toMC()) 47 | 48 | def test_missing_rp(self, device, MCRes): 49 | req = FidoRequest(MCRes, rp=None) 50 | 51 | with pytest.raises(CtapError) as e: 52 | device.sendMC(*req.toMC()) 53 | 54 | assert e.value.code == CtapError.ERR.MISSING_PARAMETER 55 | 56 | def test_bad_type_rp(self, device, MCRes): 57 | req = FidoRequest(MCRes, rp=b"1234abcdef") 58 | 59 | with pytest.raises(CtapError) as e: 60 | device.sendMC(*req.toMC()) 61 | 62 | def test_missing_pubKeyCredParams(self, device, MCRes): 63 | req = FidoRequest(MCRes, key_params=None) 64 | 65 | with pytest.raises(CtapError) as e: 66 | device.sendMC(*req.toMC()) 67 | 68 | assert e.value.code == CtapError.ERR.MISSING_PARAMETER 69 | 70 | def test_bad_type_pubKeyCredParams(self, device, MCRes): 71 | req = FidoRequest(MCRes, key_params=b"1234a") 72 | 73 | with pytest.raises(CtapError) as e: 74 | device.sendMC(*req.toMC()) 75 | 76 | def test_bad_type_excludeList(self, device, MCRes): 77 | req = FidoRequest(MCRes, exclude_list=8) 78 | 79 | with pytest.raises(CtapError) as e: 80 | device.sendMC(*req.toMC()) 81 | 82 | def test_bad_type_extensions(self, device, MCRes): 83 | req = FidoRequest(MCRes, extensions=8) 84 | 85 | with pytest.raises(CtapError) as e: 86 | device.sendMC(*req.toMC()) 87 | 88 | def test_bad_type_options(self, device, MCRes): 89 | req = FidoRequest(MCRes, options=8) 90 | 91 | with pytest.raises(CtapError) as e: 92 | device.sendMC(*req.toMC()) 93 | 94 | def test_bad_type_rp_name(self, device, MCRes): 95 | req = FidoRequest(MCRes, rp={"id": "test.org", "name": 8, "icon": "icon"}) 96 | 97 | with pytest.raises(CtapError) as e: 98 | device.sendMC(*req.toMC()) 99 | 100 | def test_bad_type_rp_id(self, device, MCRes): 101 | req = FidoRequest(MCRes, rp={"id": 8, "name": "name", "icon": "icon"}) 102 | 103 | with pytest.raises(CtapError) as e: 104 | device.sendMC(*req.toMC()) 105 | 106 | def test_bad_type_rp_icon(self, device, MCRes): 107 | req = FidoRequest(MCRes, rp={"id": "test.org", "name": "name", "icon": 8}) 108 | 109 | with pytest.raises(CtapError) as e: 110 | device.sendMC(*req.toMC()) 111 | 112 | def test_bad_type_user_name(self, device, MCRes): 113 | req = FidoRequest(MCRes, user={"id": b"user_id", "name": 8}) 114 | 115 | with pytest.raises(CtapError) as e: 116 | device.sendMC(*req.toMC()) 117 | 118 | def test_bad_type_user_id(self, device, MCRes): 119 | req = FidoRequest(MCRes, user={"id": "user_id", "name": "name"}) 120 | 121 | with pytest.raises(CtapError) as e: 122 | device.sendMC(*req.toMC()) 123 | 124 | def test_bad_type_user_displayName(self, device, MCRes): 125 | req = FidoRequest( 126 | MCRes, user={"id": "user_id", "name": "name", "displayName": 8} 127 | ) 128 | 129 | with pytest.raises(CtapError) as e: 130 | device.sendMC(*req.toMC()) 131 | 132 | def test_bad_type_user_icon(self, device, MCRes): 133 | req = FidoRequest(MCRes, user={"id": "user_id", "name": "name", "icon": 8}) 134 | 135 | with pytest.raises(CtapError) as e: 136 | device.sendMC(*req.toMC()) 137 | 138 | def test_bad_type_pubKeyCredParams(self, device, MCRes): 139 | req = FidoRequest(MCRes, key_params=["wrong"]) 140 | 141 | with pytest.raises(CtapError) as e: 142 | device.sendMC(*req.toMC()) 143 | 144 | def test_missing_pubKeyCredParams_type(self, device, MCRes): 145 | req = FidoRequest(MCRes, key_params=[{"alg": ES256.ALGORITHM}]) 146 | 147 | with pytest.raises(CtapError) as e: 148 | device.sendMC(*req.toMC()) 149 | 150 | assert e.value.code == CtapError.ERR.MISSING_PARAMETER 151 | 152 | def test_missing_pubKeyCredParams_alg(self, device, MCRes): 153 | req = FidoRequest(MCRes, key_params=[{"type": "public-key"}]) 154 | 155 | with pytest.raises(CtapError) as e: 156 | device.sendMC(*req.toMC()) 157 | 158 | assert e.value.code in [ 159 | CtapError.ERR.MISSING_PARAMETER, 160 | CtapError.ERR.UNSUPPORTED_ALGORITHM, 161 | ] 162 | 163 | def test_bad_type_pubKeyCredParams_alg(self, device, MCRes): 164 | req = FidoRequest(MCRes, key_params=[{"alg": "7", "type": "public-key"}]) 165 | 166 | with pytest.raises(CtapError) as e: 167 | device.sendMC(*req.toMC()) 168 | 169 | def test_unsupported_algorithm(self, device, MCRes): 170 | req = FidoRequest(MCRes, key_params=[{"alg": 1337, "type": "public-key"}]) 171 | 172 | with pytest.raises(CtapError) as e: 173 | device.sendMC(*req.toMC()) 174 | 175 | assert e.value.code == CtapError.ERR.UNSUPPORTED_ALGORITHM 176 | 177 | def test_exclude_list(self, device, MCRes): 178 | req = FidoRequest(MCRes, exclude_list=[{"id": b"1234", "type": "rot13"}]) 179 | 180 | device.sendMC(*req.toMC()) 181 | 182 | def test_exclude_list2(self, device, MCRes): 183 | req = FidoRequest( 184 | MCRes, 185 | exclude_list=[{"id": b"1234", "type": "mangoPapayaCoconutNotAPublicKey"}], 186 | ) 187 | 188 | device.sendMC(*req.toMC()) 189 | 190 | def test_bad_type_exclude_list(self, device, MCRes): 191 | req = FidoRequest(MCRes, exclude_list=["1234"]) 192 | 193 | with pytest.raises(CtapError) as e: 194 | device.sendMC(*req.toMC()) 195 | 196 | def test_missing_exclude_list_type(self, device, MCRes): 197 | req = FidoRequest(MCRes, exclude_list=[{"id": b"1234"}]) 198 | 199 | with pytest.raises(CtapError) as e: 200 | device.sendMC(*req.toMC()) 201 | 202 | def test_missing_exclude_list_id(self, device, MCRes): 203 | req = FidoRequest(MCRes, exclude_list=[{"type": "public-key"}]) 204 | 205 | with pytest.raises(CtapError) as e: 206 | device.sendMC(*req.toMC()) 207 | 208 | def test_bad_type_exclude_list_id(self, device, MCRes): 209 | req = FidoRequest(MCRes, exclude_list=[{"type": "public-key", "id": "1234"}]) 210 | 211 | with pytest.raises(CtapError) as e: 212 | device.sendMC(*req.toMC()) 213 | 214 | def test_bad_type_exclude_list_type(self, device, MCRes): 215 | req = FidoRequest(MCRes, exclude_list=[{"type": b"public-key", "id": b"1234"}]) 216 | 217 | with pytest.raises(CtapError) as e: 218 | device.sendMC(*req.toMC()) 219 | 220 | def test_exclude_list_excluded(self, device, MCRes, GARes): 221 | req = FidoRequest(MCRes, exclude_list=GARes.request.allow_list) 222 | 223 | with pytest.raises(CtapError) as e: 224 | device.sendMC(*req.toMC()) 225 | 226 | assert e.value.code == CtapError.ERR.CREDENTIAL_EXCLUDED 227 | 228 | def test_unknown_option(self, device, MCRes): 229 | req = FidoRequest(MCRes, options={"unknown": False}) 230 | print("MC", req.toMC()) 231 | device.sendMC(*req.toMC()) 232 | 233 | def test_eddsa(self, device): 234 | mc_req = FidoRequest( 235 | key_params=[{"type": "public-key", "alg": EdDSA.ALGORITHM}] 236 | ) 237 | try: 238 | mc_res = device.sendMC(*mc_req.toMC()) 239 | except CtapError as e: 240 | if e.code == CtapError.ERR.UNSUPPORTED_ALGORITHM: 241 | print("ed25519 is not supported. Skip this test.") 242 | return 243 | 244 | setattr(mc_res, "request", mc_req) 245 | 246 | allow_list = [ 247 | { 248 | "id": mc_res.auth_data.credential_data.credential_id[:], 249 | "type": "public-key", 250 | } 251 | ] 252 | 253 | ga_req = FidoRequest(allow_list=allow_list) 254 | ga_res = device.sendGA(*ga_req.toGA()) 255 | setattr(ga_res, "request", ga_req) 256 | 257 | try: 258 | verify(mc_res, ga_res) 259 | except: 260 | # Print out extra details on failure 261 | from binascii import hexlify 262 | 263 | print("authdata", hexlify(ga_res.auth_data)) 264 | print("cdh", hexlify(ga_res.request.cdh)) 265 | print("sig", hexlify(ga_res.signature)) 266 | from fido2.ctap2 import AttestedCredentialData 267 | 268 | credential_data = AttestedCredentialData(mc_res.auth_data.credential_data) 269 | print("public key:", hexlify(credential_data.public_key[-2])) 270 | verify(mc_res, ga_res) 271 | -------------------------------------------------------------------------------- /tests/standard/fido2/test_reset.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import pytest 4 | from fido2.ctap import CtapError 5 | from tests.utils import DeviceSelectCredential 6 | import tests 7 | 8 | 9 | def test_reset(device): 10 | device.reset() 11 | 12 | def test_reset_window(device): 13 | print("Waiting 11s before sending reset...") 14 | time.sleep(11) 15 | with pytest.raises(CtapError) as e: 16 | device.ctap2.reset(on_keepalive=DeviceSelectCredential(1)) 17 | assert e.value.code == CtapError.ERR.NOT_ALLOWED 18 | -------------------------------------------------------------------------------- /tests/standard/fido2/test_reset_credential.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import pytest 3 | from fido2.ctap import CtapError 4 | 5 | from tests.utils import * 6 | 7 | 8 | @pytest.mark.skipif( 9 | "trezor" in sys.argv, 10 | reason="Trezor does not invalidate server-resident credentials.", 11 | ) 12 | def test_credential_resets(device, MCRes, GARes): 13 | verify(MCRes, GARes) 14 | device.reset() 15 | with pytest.raises(CtapError) as e: 16 | new_auth = device.sendGA(*FidoRequest(GARes).toGA()) 17 | assert e.value.code == CtapError.ERR.NO_CREDENTIALS 18 | -------------------------------------------------------------------------------- /tests/standard/fido2/test_resident_key.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import pytest 3 | import time 4 | import random 5 | from fido2.ctap import CtapError 6 | 7 | from tests.utils import * 8 | 9 | 10 | @pytest.fixture(scope="class", params=["", "123456"]) 11 | def SetPINRes(request, device, info): 12 | 13 | device.reset() 14 | 15 | pin = request.param 16 | req = FidoRequest() 17 | 18 | if pin: 19 | if "clientPin" in info.options: 20 | device.client.pin_protocol.set_pin(pin) 21 | req = FidoRequest(req, pin = pin) 22 | 23 | res = device.sendMC(*req.toMC()) 24 | setattr(res, "request", req) 25 | return res 26 | 27 | 28 | @pytest.fixture(scope="class") 29 | def MC_RK_Res(device, SetPINRes): 30 | req = FidoRequest(SetPINRes, options={"rk": True}) 31 | res = device.sendMC(*req.toMC()) 32 | setattr(res, "request", req) 33 | return res 34 | 35 | 36 | @pytest.fixture(scope="class") 37 | def GA_RK_Res(device, MC_RK_Res): 38 | req = FidoRequest(MC_RK_Res, options=None) 39 | res = device.sendGA(*req.toGA()) 40 | setattr(res, "request", req) 41 | return res 42 | 43 | 44 | class TestResidentKeyPersistance(object): 45 | @pytest.mark.parametrize("do_reboot", [False, True]) 46 | def test_user_info_returned_when_using_allowlist(self, device, MC_RK_Res, GA_RK_Res, do_reboot): 47 | assert "id" in GA_RK_Res.user.keys() 48 | 49 | allow_list = [ 50 | { 51 | "id": MC_RK_Res.auth_data.credential_data.credential_id[:], 52 | "type": "public-key", 53 | } 54 | ] 55 | 56 | if do_reboot: 57 | device.reboot() 58 | 59 | ga_req = FidoRequest(allow_list=allow_list) 60 | ga_res = device.sendGA(*ga_req.toGA()) 61 | setattr(ga_res, "request", ga_req) 62 | verify(MC_RK_Res, ga_res) 63 | 64 | assert MC_RK_Res.request.user["id"] == ga_res.user["id"] 65 | 66 | class TestResidentKeyAfterReset(object): 67 | def test_with_allow_list_after_reset(self, device, MC_RK_Res, GA_RK_Res): 68 | assert "id" in GA_RK_Res.user.keys() 69 | 70 | allow_list = [ 71 | { 72 | "id": MC_RK_Res.auth_data.credential_data.credential_id[:], 73 | "type": "public-key", 74 | } 75 | ] 76 | 77 | ga_req = FidoRequest(allow_list=allow_list) 78 | ga_res = device.sendGA(*ga_req.toGA()) 79 | setattr(ga_res, "request", ga_req) 80 | verify(MC_RK_Res, ga_res) 81 | 82 | assert MC_RK_Res.request.user["id"] == ga_res.user["id"] 83 | 84 | device.reset() 85 | 86 | ga_req = FidoRequest(allow_list=allow_list) 87 | with pytest.raises(CtapError) as e: 88 | ga_res = device.sendGA(*ga_req.toGA()) 89 | assert e.value.code == CtapError.ERR.NO_CREDENTIALS 90 | 91 | 92 | 93 | class TestResidentKey(object): 94 | def test_resident_key(self, MC_RK_Res, info): 95 | pass 96 | 97 | def test_resident_key_auth(self, MC_RK_Res, GA_RK_Res): 98 | verify(MC_RK_Res, GA_RK_Res) 99 | 100 | def test_user_info_returned(self, MC_RK_Res, GA_RK_Res): 101 | assert "id" in GA_RK_Res.user.keys() 102 | assert ( 103 | MC_RK_Res.auth_data.credential_data.credential_id 104 | == GA_RK_Res.credential["id"] 105 | ) 106 | assert MC_RK_Res.request.user["id"] == GA_RK_Res.user["id"] 107 | if not MC_RK_Res.request.pin_protocol or not GA_RK_Res.number_of_credentials: 108 | assert "id" in GA_RK_Res.user.keys() and len(GA_RK_Res.user.keys()) == 1 109 | else: 110 | assert MC_RK_Res.request.user == GA_RK_Res.user 111 | 112 | @pytest.mark.skipif( 113 | "trezor" in sys.argv, 114 | reason="Trezor does not support get_next_assertion() because it has a display.", 115 | ) 116 | def test_multiple_rk_nodisplay(self, device, MC_RK_Res): 117 | auths = [] 118 | regs = [] 119 | # Use unique RP to not collide with other credentials 120 | rp = {"id": f"unique-{random.random()}.com", "name": "Example"} 121 | for i in range(0, 3): 122 | req = FidoRequest(MC_RK_Res, user=generate_user(), rp = rp) 123 | print(f""" 124 | {req.user} 125 | {req.cdh} 126 | {req.rp} 127 | """) 128 | res = device.sendMC(*req.toMC()) 129 | regs.append(res) 130 | # time.sleep(2) 131 | 132 | req = FidoRequest(MC_RK_Res, options=None, user=generate_user(), rp = rp) 133 | res = device.sendGA(*req.toGA()) 134 | 135 | auths.append(res) 136 | auths.append(device.ctap2.get_next_assertion()) 137 | # time.sleep(2) 138 | auths.append(device.ctap2.get_next_assertion()) 139 | # time.sleep(2) 140 | 141 | with pytest.raises(CtapError) as e: 142 | device.ctap2.get_next_assertion() 143 | 144 | assert len(regs) == 3 145 | assert len(regs) == len(auths) 146 | 147 | if MC_RK_Res.request.pin_protocol: 148 | for x in auths: 149 | for y in ("name", "icon", "displayName", "id"): 150 | if y not in x.user.keys(): 151 | print("FAIL: %s was not in user: " % y, x.user) 152 | 153 | for x, y in zip(regs, auths[::-1]): 154 | verify(x, y, req.cdh) 155 | 156 | @pytest.mark.skipif("trezor" not in sys.argv, reason="Only Trezor has a display.") 157 | def test_multiple_rk_display(self, device, MC_RK_Res): 158 | regs = [MC_RK_Res] 159 | for i in range(0, 3): 160 | req = FidoRequest(MC_RK_Res, user=generate_user()) 161 | res = device.sendMC(*req.toMC()) 162 | setattr(res, "request", req) 163 | regs.append(res) 164 | 165 | for i, reg in enumerate(reversed(regs)): 166 | req = FidoRequest( 167 | MC_RK_Res, options=None, on_keepalive=DeviceSelectCredential(i + 1) 168 | ) 169 | res = device.sendGA(*req.toGA()) 170 | assert res.number_of_credentials is None 171 | 172 | with pytest.raises(CtapError) as e: 173 | device.ctap2.get_next_assertion() 174 | assert e.value.code == CtapError.ERR.NOT_ALLOWED 175 | 176 | assert res.user["id"] == reg.request.user["id"] 177 | verify(reg, res, req.cdh) 178 | 179 | @pytest.mark.skipif("trezor" not in sys.argv, reason="Only Trezor has a display.") 180 | def test_replace_rk_display(self, device): 181 | """ 182 | Test replacing resident keys. 183 | """ 184 | user1 = generate_user() 185 | user2 = generate_user() 186 | rp1 = {"id": "example.org", "name": "Example"} 187 | rp2 = {"id": "example.com", "name": "Example"} 188 | 189 | # Registration data is a list of (rp, user, number), where number is 190 | # the expected position of the credential after all registrations are 191 | # complete. 192 | reg_data = [ 193 | (rp1, user1, 2), 194 | (rp1, user2, None), 195 | (rp1, user2, 1), 196 | (rp2, user2, 1), 197 | ] 198 | regs = [] 199 | for rp, user, number in reg_data: 200 | req = FidoRequest(options={"rk": True}, rp=rp, user=user) 201 | res = device.sendMC(*req.toMC()) 202 | setattr(res, "request", req) 203 | setattr(res, "number", number) 204 | regs.append(res) 205 | 206 | # Check. 207 | for reg in regs: 208 | if reg.number is not None: 209 | req = FidoRequest( 210 | rp=reg.request.rp, 211 | options=None, 212 | on_keepalive=DeviceSelectCredential(reg.number), 213 | ) 214 | res = device.sendGA(*req.toGA()) 215 | assert res.user["id"] == reg.request.user["id"] 216 | verify(reg, res, req.cdh) 217 | 218 | @pytest.mark.skipif( 219 | "trezor" in sys.argv, 220 | reason="Trezor does not support get_next_assertion() because it has a display.", 221 | ) 222 | @pytest.mark.skipif( 223 | "solokeys" in sys.argv, reason="Initial SoloKeys model truncates displayName" 224 | ) 225 | def test_rk_maximum_size_nodisplay(self, device, MC_RK_Res): 226 | """ 227 | Check the lengths of the fields according to the FIDO2 spec 228 | https://github.com/solokeys/solo/issues/158#issue-426613303 229 | https://www.w3.org/TR/webauthn/#dom-publickeycredentialuserentity-displayname 230 | """ 231 | auths = [] 232 | user_max = generate_user_maximum() 233 | req = FidoRequest(MC_RK_Res, user=user_max) 234 | resMC = device.sendMC(*req.toMC()) 235 | req.options = {} 236 | resGA = device.sendGA(*req.toGA()) 237 | credentials = resGA.number_of_credentials 238 | 239 | auths.append(resGA) 240 | for i in range(credentials - 1): 241 | auths.append(device.ctap2.get_next_assertion()) 242 | 243 | user_max_GA = auths[0] 244 | verify(resMC, user_max_GA, req.cdh) 245 | 246 | if MC_RK_Res.request.pin_protocol: 247 | for y in ("name", "icon", "displayName", "id"): 248 | assert user_max_GA.user[y] == user_max[y] 249 | 250 | @pytest.mark.skipif("trezor" not in sys.argv, reason="Only Trezor has a display.") 251 | def test_rk_maximum_size_display(self, device, MC_RK_Res): 252 | """ 253 | Check the lengths of the fields according to the FIDO2 spec 254 | https://github.com/solokeys/solo/issues/158#issue-426613303 255 | https://www.w3.org/TR/webauthn/#dom-publickeycredentialuserentity-displayname 256 | """ 257 | user_max = generate_user_maximum() 258 | req = FidoRequest(MC_RK_Res, user=user_max) 259 | resMC = device.sendMC(*req.toMC()) 260 | req = FidoRequest(MC_RK_Res, options=None) 261 | resGA = device.sendGA(*req.toGA()) 262 | assert resGA.number_of_credentials is None 263 | verify(resMC, resGA, req.cdh) 264 | 265 | @pytest.mark.skipif( 266 | "trezor" in sys.argv, 267 | reason="Trezor does not support get_next_assertion() because it has a display.", 268 | ) 269 | @pytest.mark.skipif( 270 | "solokeys" in sys.argv, reason="Initial SoloKeys model truncates displayName" 271 | ) 272 | def test_rk_maximum_list_capacity_per_rp_nodisplay(self, info, device, MC_RK_Res): 273 | """ 274 | Test maximum returned capacity of the RK for the given RP 275 | """ 276 | 277 | # Try to determine from get_info, or default to 19. 278 | RK_CAPACITY_PER_RP = info.max_creds_in_list 279 | if not RK_CAPACITY_PER_RP: 280 | RK_CAPACITY_PER_RP = 19 281 | 282 | users = [] 283 | 284 | def get_user(): 285 | user = generate_user_maximum() 286 | users.append(user) 287 | return user 288 | 289 | # Use unique RP to not collide with other credentials from other tests. 290 | rp = {"id": f"unique-{random.random()}.com", "name": "Example"} 291 | 292 | # req = FidoRequest(MC_RK_Res, options=None, user=get_user(), rp = rp) 293 | # res = device.sendGA(*req.toGA()) 294 | current_credentials_count = 0 295 | 296 | auths = [] 297 | regs = [MC_RK_Res] 298 | RK_to_generate = RK_CAPACITY_PER_RP - current_credentials_count 299 | for i in range(RK_to_generate): 300 | req = FidoRequest(MC_RK_Res, user=get_user(), rp = rp) 301 | res = device.sendMC(*req.toMC()) 302 | regs.append(res) 303 | 304 | req = FidoRequest(MC_RK_Res, options=None, user=generate_user_maximum(), rp = rp) 305 | res = device.sendGA(*req.toGA()) 306 | assert res.number_of_credentials == RK_CAPACITY_PER_RP 307 | 308 | auths.append(res) 309 | for i in range(RK_CAPACITY_PER_RP - 1): 310 | auths.append(device.ctap2.get_next_assertion()) 311 | 312 | with pytest.raises(CtapError) as e: 313 | device.ctap2.get_next_assertion() 314 | 315 | auths = auths[::-1][-RK_to_generate:] 316 | regs = regs[-RK_to_generate:] 317 | users = users[-RK_to_generate:] 318 | 319 | assert len(auths) == len(users) 320 | 321 | if MC_RK_Res.request.pin_protocol: 322 | for x, u in zip(auths, users): 323 | for y in ("name", "icon", "displayName", "id"): 324 | assert y in x.user.keys() 325 | assert x.user[y] == u[y] 326 | 327 | assert len(auths) == len(regs) 328 | for x, y in zip(regs, auths): 329 | verify(x, y, req.cdh) 330 | 331 | @pytest.mark.skipif("trezor" not in sys.argv, reason="Only Trezor has a display.") 332 | def test_rk_maximum_list_capacity_per_rp_display(self, device): 333 | """ 334 | Test maximum capacity of resident keys. 335 | """ 336 | RK_CAPACITY = 16 337 | device.reset() 338 | req = FidoRequest(options={"rk": True}) 339 | 340 | regs = [] 341 | for i in range(RK_CAPACITY): 342 | req = FidoRequest(req, user=generate_user_maximum()) 343 | res = device.sendMC(*req.toMC()) 344 | setattr(res, "request", req) 345 | regs.append(res) 346 | 347 | req = FidoRequest(req, user=generate_user_maximum()) 348 | with pytest.raises(CtapError) as e: 349 | res = device.sendMC(*req.toMC()) 350 | assert e.value.code == CtapError.ERR.KEY_STORE_FULL 351 | 352 | for i, reg in enumerate(reversed(regs)): 353 | if i not in (0, 1, 7, 14, 15): 354 | continue 355 | req = FidoRequest( 356 | req, options=None, on_keepalive=DeviceSelectCredential(i + 1) 357 | ) 358 | res = device.sendGA(*req.toGA()) 359 | assert res.user["id"] == reg.request.user["id"] 360 | verify(reg, res, req.cdh) 361 | 362 | def test_rk_with_allowlist_of_different_rp(self, resetDevice): 363 | """ 364 | Test that a rk credential is not found when using an allowList item for a different RP 365 | """ 366 | 367 | rk_rp = {"id": "rk-cred.org", "name": "Example"} 368 | rk_req = FidoRequest(rp = rk_rp, options={"rk": True}) 369 | rk_res = resetDevice.sendMC(*rk_req.toMC()) 370 | 371 | server_rp = {"id": "server-cred.com", "name": "Example"} 372 | server_req = FidoRequest(rp = server_rp) 373 | server_res = resetDevice.sendMC(*server_req.toMC()) 374 | 375 | allow_list_with_different_rp_cred = [ 376 | { 377 | "id": server_res.auth_data.credential_data.credential_id[:], 378 | "type": "public-key", 379 | } 380 | ] 381 | 382 | test_req = FidoRequest(rp = rk_rp, allow_list = allow_list_with_different_rp_cred) 383 | 384 | with pytest.raises(CtapError) as e: 385 | res = resetDevice.sendGA(*test_req.toGA()) 386 | assert e.value.code == CtapError.ERR.NO_CREDENTIALS 387 | 388 | 389 | def test_same_userId_overwrites_rk(self, resetDevice): 390 | """ 391 | A make credential request with a UserId & Rp that is the same as an existing one should overwrite. 392 | """ 393 | rp = {"id": "overwrite.org", "name": "Example"} 394 | user = generate_user() 395 | 396 | req = FidoRequest(rp = rp, options={"rk": True}, user = user) 397 | mc_res1 = resetDevice.sendMC(*req.toMC()) 398 | 399 | # Should overwrite the first credential. 400 | mc_res2 = resetDevice.sendMC(*req.toMC()) 401 | 402 | ga_res = resetDevice.sendGA(*req.toGA()) 403 | 404 | # If there's only one credential, this is None 405 | assert ga_res.number_of_credentials == None 406 | 407 | verify(mc_res2, ga_res, req.cdh) 408 | 409 | def test_larger_icon_than_128(self, device): 410 | """ 411 | Test it works if we give an icon value larger than 128 bytes 412 | """ 413 | rp = {"id": "overwrite.org", "name": "Example"} 414 | user = generate_user() 415 | user['icon'] = 'https://www.w3.org/TR/webauthn/?icon=' + ("A" * 128) 416 | 417 | req = FidoRequest(rp = rp, options={"rk": True}, user = user) 418 | device.sendMC(*req.toMC()) 419 | 420 | 421 | def test_returned_credential(self, device): 422 | """ 423 | Test that when two rk credentials put in allow_list, 424 | only 1 will get returned. 425 | """ 426 | device.reset() 427 | pin = '12345' 428 | device.client.pin_protocol.set_pin(pin) 429 | req = FidoRequest(pin = pin, options={"rk": True}) 430 | 431 | regs = [] 432 | allow_list = [] 433 | for i in range(0, 2): 434 | req = FidoRequest(req, user = { 435 | "id": b'123456' + bytes([i]), "name": f'Test User {i}', "displayName": f'Test User display {i}' 436 | }) 437 | res = device.sendMC(*req.toMC()) 438 | setattr(res, "request", req) 439 | regs.append(res) 440 | allow_list.append({"id": res.auth_data.credential_data.credential_id[:], "type": "public-key"}) 441 | 442 | 443 | print('allow_list: ' , allow_list) 444 | ga_req = FidoRequest(pin = pin, allow_list=allow_list) 445 | ga_res = device.sendGA(*ga_req.toGA()) 446 | 447 | # No other credentials should be returned 448 | with pytest.raises(CtapError) as e: 449 | device.ctap2.get_next_assertion() 450 | 451 | # the returned credential should have user id in it 452 | print(ga_res) 453 | assert 'id' in ga_res.user and len(ga_res.user["id"]) > 0 454 | -------------------------------------------------------------------------------- /tests/standard/fido2/user_presence/test_user_presence.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import time 3 | 4 | import pytest 5 | from fido2.ctap import CtapError 6 | from fido2.utils import hmac_sha256, sha256 7 | from tests.utils import * 8 | 9 | 10 | @pytest.mark.skipif( 11 | ("--sim" in sys.argv or "--nfc" in sys.argv) and not "trezor" in sys.argv, 12 | reason="Simulation doesn't care about user presence", 13 | ) 14 | class TestUserPresence(object): 15 | def test_user_presence_instructions(self, MCRes, GARes): 16 | print() 17 | print() 18 | print("Starting User Presence (UP) tests.") 19 | time.sleep(1) 20 | print() 21 | print( 22 | "Follow instructions. You will have to give UP or not give UP to pass the tests." 23 | ) 24 | time.sleep(2) 25 | 26 | def test_user_presence(self, device, GARes): 27 | print("ACTIVATE UP ONCE") 28 | device.sendGA(*FidoRequest(GARes).toGA()) 29 | 30 | def test_no_user_presence(self, device, MCRes, GARes): 31 | print("DO NOT ACTIVATE UP") 32 | with pytest.raises(CtapError) as e: 33 | with Timeout(2.0) as event: 34 | device.sendGA( 35 | *FidoRequest(GARes, timeout=event, on_keepalive=None).toGA() 36 | ) 37 | assert e.value.code == CtapError.ERR.KEEPALIVE_CANCEL 38 | 39 | @pytest.mark.skipif( 40 | not "trezor" in sys.argv, reason="Only Trezor supports decline." 41 | ) 42 | def test_user_decline(self, device, MCRes, GARes): 43 | print("PRESS DECLINE") 44 | with pytest.raises(CtapError) as e: 45 | device.sendGA( 46 | *FidoRequest(GARes, on_keepalive=DeviceSelectCredential(0)).toGA() 47 | ) 48 | assert e.value.code == CtapError.ERR.OPERATION_DENIED 49 | 50 | def test_user_presence_option_false_on_get_assertion(self, device, MCRes, GARes): 51 | print("DO NOT ACTIVATE UP") 52 | time.sleep(1) 53 | with Timeout(2.0) as event: 54 | device.sendGA( 55 | *FidoRequest(GARes, options={"up": False}, timeout=event).toGA() 56 | ) 57 | 58 | def test_user_presence_option_false_on_make_credential(self, device, MCRes): 59 | print("DO NOT ACTIVATE UP") 60 | time.sleep(1) 61 | with pytest.raises(CtapError) as e: 62 | with Timeout(1.0) as event: 63 | device.sendMC( 64 | *FidoRequest(MCRes, options={"up": False}, timeout=event).toMC() 65 | ) 66 | assert e.value.code == CtapError.ERR.INVALID_OPTION 67 | with pytest.raises(CtapError) as e: 68 | with Timeout(1.0) as event: 69 | device.sendMC( 70 | *FidoRequest(MCRes, options={"up": True}, timeout=event).toMC() 71 | ) 72 | assert e.value.code == CtapError.ERR.INVALID_OPTION 73 | 74 | def test_user_presence_permits_only_one_request(self, device, MCRes, GARes): 75 | print("ACTIVATE UP ONCE") 76 | device.sendGA(*FidoRequest(GARes).toGA()) 77 | 78 | with pytest.raises(CtapError) as e: 79 | with Timeout(1.0) as event: 80 | device.sendGA( 81 | *FidoRequest(GARes, timeout=event, on_keepalive=None).toGA() 82 | ) 83 | assert e.value.code == CtapError.ERR.KEEPALIVE_CANCEL 84 | -------------------------------------------------------------------------------- /tests/standard/fido2v1/extensions/test_cred_protect.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from fido2.ctap import CtapError 4 | from tests.utils import * 5 | 6 | class CredProtect: 7 | UserVerificationOptional = 1 8 | UserVerificationOptionalWithCredentialId = 2 9 | UserVerificationRequired = 3 10 | 11 | @pytest.fixture(scope="class") 12 | def MCCredProtectOptional( 13 | resetDevice, 14 | ): 15 | req = FidoRequest(options = {'rk': True}, extensions={"credProtect": CredProtect.UserVerificationOptional}) 16 | res = resetDevice.sendMC(*req.toMC()) 17 | setattr(res, "request", req) 18 | return res 19 | 20 | @pytest.fixture(scope="class") 21 | def MCCredProtectOptionalList( 22 | resetDevice, 23 | ): 24 | req = FidoRequest(options = {'rk': True}, extensions={"credProtect": CredProtect.UserVerificationOptionalWithCredentialId}) 25 | res = resetDevice.sendMC(*req.toMC()) 26 | setattr(res, "request", req) 27 | return res 28 | 29 | @pytest.fixture(scope="class") 30 | def MCCredProtectRequired( 31 | resetDevice, 32 | ): 33 | req = FidoRequest(options = {'rk': True}, extensions={"credProtect": CredProtect.UserVerificationRequired}) 34 | res = resetDevice.sendMC(*req.toMC()) 35 | setattr(res, "request", req) 36 | return res 37 | 38 | 39 | 40 | class TestCredProtect(object): 41 | def test_credprotect_make_credential_1(self, MCCredProtectOptional): 42 | assert MCCredProtectOptional.auth_data.extensions 43 | assert "credProtect" in MCCredProtectOptional.auth_data.extensions 44 | assert MCCredProtectOptional.auth_data.extensions["credProtect"] == 1 45 | 46 | def test_credprotect_make_credential_2(self, MCCredProtectOptionalList): 47 | assert MCCredProtectOptionalList.auth_data.extensions 48 | assert "credProtect" in MCCredProtectOptionalList.auth_data.extensions 49 | assert MCCredProtectOptionalList.auth_data.extensions["credProtect"] == 2 50 | 51 | def test_credprotect_make_credential_3(self, MCCredProtectRequired): 52 | assert MCCredProtectRequired.auth_data.extensions 53 | assert "credProtect" in MCCredProtectRequired.auth_data.extensions 54 | assert MCCredProtectRequired.auth_data.extensions["credProtect"] == 3 55 | 56 | def test_credprotect_optional_excluded(self, device, MCCredProtectOptional): 57 | """ CredProtectOptional Cred should be visible to be excluded with no UV """ 58 | exclude_list = [ 59 | { 60 | "id": MCCredProtectOptional.auth_data.credential_data.credential_id[:], 61 | "type": "public-key", 62 | } 63 | ] 64 | 65 | req = FidoRequest(MCCredProtectOptional, exclude_list= exclude_list) 66 | 67 | with pytest.raises(CtapError) as e: 68 | device.sendMC(*req.toMC()) 69 | 70 | assert e.value.code == CtapError.ERR.CREDENTIAL_EXCLUDED 71 | 72 | def test_credprotect_optional_list_excluded(self, device, MCCredProtectOptionalList): 73 | """ CredProtectOptionalList Cred should be visible to be excluded with no UV """ 74 | exclude_list = [ 75 | { 76 | "id": MCCredProtectOptionalList.auth_data.credential_data.credential_id[:], 77 | "type": "public-key", 78 | } 79 | ] 80 | 81 | req = FidoRequest(MCCredProtectOptionalList, exclude_list= exclude_list) 82 | 83 | with pytest.raises(CtapError) as e: 84 | device.sendMC(*req.toMC()) 85 | 86 | assert e.value.code == CtapError.ERR.CREDENTIAL_EXCLUDED 87 | 88 | def test_credprotect_required_not_excluded_with_no_uv(self, device, MCCredProtectRequired): 89 | """ CredProtectRequired Cred should NOT be visible to be excluded with no UV """ 90 | exclude_list = [ 91 | { 92 | "id": MCCredProtectRequired.auth_data.credential_data.credential_id[:], 93 | "type": "public-key", 94 | } 95 | ] 96 | 97 | req = FidoRequest(MCCredProtectRequired, exclude_list= exclude_list) 98 | 99 | # works 100 | device.sendMC(*req.toMC()) 101 | 102 | def test_credprotect_optional_works_with_no_allowList_no_uv(self, device, MCCredProtectOptional): 103 | req = FidoRequest() 104 | 105 | # works 106 | res = device.sendGA(*req.toGA()) 107 | 108 | # If there's only one credential, this is None 109 | assert res.number_of_credentials == None 110 | 111 | def test_credprotect_optional_and_list_works_no_uv(self, device, MCCredProtectOptional, MCCredProtectOptionalList, MCCredProtectRequired): 112 | allow_list = [ 113 | { 114 | "id": MCCredProtectOptional.auth_data.credential_data.credential_id[:], 115 | "type": "public-key", 116 | }, 117 | { 118 | "id": MCCredProtectOptionalList.auth_data.credential_data.credential_id[:], 119 | "type": "public-key", 120 | }, 121 | { 122 | "id": MCCredProtectRequired.auth_data.credential_data.credential_id[:], 123 | "type": "public-key", 124 | }, 125 | ] 126 | 127 | req = FidoRequest(allow_list = allow_list) 128 | 129 | # works 130 | res1 = device.sendGA(*req.toGA()) 131 | assert res1.number_of_credentials in (None, 2) 132 | 133 | results = [res1] 134 | if res1.number_of_credentials == 2: 135 | res2 = device.ctap2.get_next_assertion() 136 | results.append(res2) 137 | 138 | # the required credProtect is not returned. 139 | for res in results: 140 | assert res.credential["id"] != MCCredProtectRequired.auth_data.credential_data.credential_id[:] 141 | 142 | def test_hmac_secret_and_credProtect_make_credential( 143 | self, resetDevice, MCCredProtectOptional 144 | ): 145 | 146 | req = FidoRequest(extensions={"credProtect": 1, "hmac-secret": True}) 147 | res = resetDevice.sendMC(*req.toMC()) 148 | setattr(res, "request", req) 149 | 150 | for ext in ["credProtect", "hmac-secret"]: 151 | assert res.auth_data.extensions 152 | assert ext in res.auth_data.extensions 153 | assert res.auth_data.extensions[ext] == True 154 | 155 | 156 | class TestCredProtectUv: 157 | def test_credprotect_all_with_uv(self, device, MCCredProtectOptional, MCCredProtectOptionalList, MCCredProtectRequired): 158 | allow_list = [ 159 | { 160 | "id": MCCredProtectOptional.auth_data.credential_data.credential_id[:], 161 | "type": "public-key", 162 | }, 163 | { 164 | "id": MCCredProtectOptionalList.auth_data.credential_data.credential_id[:], 165 | "type": "public-key", 166 | }, 167 | { 168 | "id": MCCredProtectRequired.auth_data.credential_data.credential_id[:], 169 | "type": "public-key", 170 | }, 171 | ] 172 | 173 | pin = "123456A" 174 | req = FidoRequest() 175 | 176 | device.client.pin_protocol.set_pin(pin) 177 | pin_token = device.client.pin_protocol.get_pin_token(pin) 178 | pin_auth = hmac_sha256(pin_token, req.cdh)[:16] 179 | 180 | req = FidoRequest(req, pin_protocol=1, pin_auth=pin_auth, allow_list = allow_list) 181 | 182 | res1 = device.sendGA(*req.toGA()) 183 | 184 | assert res1.number_of_credentials in (None, 3) 185 | 186 | if res1.number_of_credentials == 3: 187 | 188 | res2 = device.ctap2.get_next_assertion() 189 | res3 = device.ctap2.get_next_assertion() 190 | 191 | -------------------------------------------------------------------------------- /tests/standard/fido2v1/test_credmgmt.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import time 3 | import random 4 | from fido2.ctap import CtapError 5 | from fido2.ctap2 import CredentialManagement 6 | from tests.utils import * 7 | from binascii import hexlify 8 | 9 | PIN = "123456" 10 | 11 | 12 | @pytest.fixture(params=[PIN], scope = 'function') 13 | def PinToken(request, device): 14 | device.reboot() 15 | device.reset() 16 | pin = request.param 17 | device.client.pin_protocol.set_pin(pin) 18 | return device.client.pin_protocol.get_pin_token(pin) 19 | 20 | 21 | @pytest.fixture(scope = 'function') 22 | def MC_RK_Res(device, PinToken): 23 | req = FidoRequest() 24 | rp = {"id": "ssh:", "name": "Bate Goiko"} 25 | req = FidoRequest( 26 | request=None, 27 | pin=PIN, 28 | rp=rp, 29 | options={"rk": True}, 30 | ) 31 | device.sendMC(*req.toMC()) 32 | 33 | req = FidoRequest() 34 | rp = {"id": "xakcop.com", "name": "John Doe"} 35 | req = FidoRequest( 36 | request=None, 37 | pin=PIN, 38 | rp=rp, 39 | options={"rk": True}, 40 | ) 41 | device.sendMC(*req.toMC()) 42 | 43 | 44 | @pytest.fixture(scope = 'function') 45 | def CredMgmt(device, PinToken): 46 | pin_protocol = 1 47 | return CredentialManagement(device.ctap2, pin_protocol, PinToken) 48 | 49 | 50 | def _test_enumeration(CredMgmt, rp_map): 51 | "Enumerate credentials using BFS" 52 | res = CredMgmt.enumerate_rps() 53 | assert len(rp_map.keys()) == len(res) 54 | 55 | for rp in res: 56 | creds = CredMgmt.enumerate_creds(sha256(rp[3]["id"].encode("utf8"))) 57 | assert len(creds) == rp_map[rp[3]["id"]] 58 | 59 | 60 | def _test_enumeration_interleaved(CredMgmt, rp_map): 61 | "Enumerate credentials using DFS" 62 | first_rp = CredMgmt.enumerate_rps_begin() 63 | assert len(rp_map.keys()) == first_rp[CredentialManagement.RESULT.TOTAL_RPS] 64 | 65 | rk_count = 1 66 | first_rk = CredMgmt.enumerate_creds_begin(sha256(first_rp[3]["id"].encode("utf8"))) 67 | for i in range(1, first_rk[CredentialManagement.RESULT.TOTAL_CREDENTIALS]): 68 | c = CredMgmt.enumerate_creds_next() 69 | rk_count += 1 70 | 71 | assert rk_count == rp_map[first_rp[3]["id"]] 72 | 73 | for i in range(1, first_rp[CredentialManagement.RESULT.TOTAL_RPS]): 74 | next_rp = CredMgmt.enumerate_rps_next() 75 | 76 | rk_count = 1 77 | first_rk = CredMgmt.enumerate_creds_begin( 78 | sha256(next_rp[3]["id"].encode("utf8")) 79 | ) 80 | for i in range(1, first_rk[CredentialManagement.RESULT.TOTAL_CREDENTIALS]): 81 | c = CredMgmt.enumerate_creds_next() 82 | rk_count += 1 83 | 84 | assert rk_count == rp_map[next_rp[3]["id"]] 85 | 86 | 87 | def CredMgmtWrongPinAuth(device, pin_token): 88 | pin_protocol = 1 89 | wrong_pt = bytearray(pin_token) 90 | wrong_pt[0] = (wrong_pt[0] + 1) % 256 91 | return CredentialManagement(device.ctap2, pin_protocol, bytes(wrong_pt)) 92 | 93 | 94 | def assert_cred_response_has_all_fields(cred_res): 95 | for i in ( 96 | CredentialManagement.RESULT.USER, 97 | CredentialManagement.RESULT.CREDENTIAL_ID, 98 | CredentialManagement.RESULT.PUBLIC_KEY, 99 | CredentialManagement.RESULT.TOTAL_CREDENTIALS, 100 | CredentialManagement.RESULT.CRED_PROTECT, 101 | ): 102 | assert i in cred_res 103 | 104 | 105 | class TestCredentialManagement(object): 106 | def test_get_info(self, info): 107 | assert "credMgmt" in info.options 108 | assert info.options["credMgmt"] == True 109 | print(info) 110 | assert 0x7 in info 111 | assert info[0x7] > 1 112 | assert 0x8 in info 113 | assert info[0x8] > 1 114 | 115 | def test_get_metadata(self, CredMgmt, MC_RK_Res): 116 | metadata = CredMgmt.get_metadata() 117 | assert metadata[CredentialManagement.RESULT.EXISTING_CRED_COUNT] == 2 118 | assert metadata[CredentialManagement.RESULT.MAX_REMAINING_COUNT] >= 48 119 | 120 | def test_enumerate_rps(self, CredMgmt, MC_RK_Res): 121 | res = CredMgmt.enumerate_rps() 122 | print(res) 123 | assert len(res) == 2 124 | assert res[0][CredentialManagement.RESULT.RP]["id"] == "ssh:" 125 | assert res[0][CredentialManagement.RESULT.RP_ID_HASH] == sha256(b"ssh:") 126 | # Solo doesn't store rpId with the exception of "ssh:" 127 | assert res[1][CredentialManagement.RESULT.RP]["id"] == "xakcop.com" 128 | assert res[1][CredentialManagement.RESULT.RP_ID_HASH] == sha256(b"xakcop.com") 129 | 130 | def test_enumarate_creds(self, CredMgmt, MC_RK_Res): 131 | res = CredMgmt.enumerate_creds(sha256(b"ssh:")) 132 | assert len(res) == 1 133 | assert_cred_response_has_all_fields(res[0]) 134 | res = CredMgmt.enumerate_creds(sha256(b"xakcop.com")) 135 | assert len(res) == 1 136 | assert_cred_response_has_all_fields(res[0]) 137 | res = CredMgmt.enumerate_creds(sha256(b"missing.com")) 138 | assert not res 139 | 140 | def test_get_metadata_wrong_pinauth(self, device, MC_RK_Res, PinToken): 141 | cmd = lambda credMgmt: credMgmt.get_metadata() 142 | self._test_wrong_pinauth(device, cmd, PinToken) 143 | 144 | def test_rpbegin_wrong_pinauth(self, device, MC_RK_Res, PinToken): 145 | cmd = lambda credMgmt: credMgmt.enumerate_rps_begin() 146 | self._test_wrong_pinauth(device, cmd, PinToken) 147 | 148 | def test_rkbegin_wrong_pinauth(self, device, MC_RK_Res, PinToken): 149 | cmd = lambda credMgmt: credMgmt.enumerate_creds_begin(sha256(b"ssh:")) 150 | self._test_wrong_pinauth(device, cmd, PinToken) 151 | 152 | def test_rpnext_without_rpbegin(self, device, CredMgmt, MC_RK_Res): 153 | CredMgmt.enumerate_creds_begin(sha256(b"ssh:")) 154 | with pytest.raises(CtapError) as e: 155 | CredMgmt.enumerate_rps_next() 156 | assert e.value.code == CtapError.ERR.NOT_ALLOWED 157 | 158 | def test_rknext_without_rkbegin(self, device, CredMgmt, MC_RK_Res): 159 | CredMgmt.enumerate_rps_begin() 160 | with pytest.raises(CtapError) as e: 161 | CredMgmt.enumerate_creds_next() 162 | assert e.value.code == CtapError.ERR.NOT_ALLOWED 163 | 164 | def test_delete(self, device, PinToken, CredMgmt): 165 | 166 | # create a new RK 167 | req = FidoRequest() 168 | pin_auth = hmac_sha256(PinToken, req.cdh)[:16] 169 | rp = {"id": "example_3.com", "name": "John Doe 2"} 170 | req = FidoRequest( 171 | pin_protocol=1, 172 | pin_auth=pin_auth, 173 | options={"rk": True}, 174 | rp=rp, 175 | ) 176 | reg = device.sendMC(*req.toMC()) 177 | 178 | # make sure it works 179 | req = FidoRequest(rp=rp) 180 | auth = device.sendGA(*req.toGA()) 181 | 182 | verify(reg, auth, req.cdh) 183 | 184 | # get the ID from enumeration 185 | creds = CredMgmt.enumerate_creds(reg.auth_data.rp_id_hash) 186 | for cred in creds: 187 | if cred[7]["id"] == reg.auth_data.credential_data.credential_id: 188 | break 189 | 190 | # delete it 191 | cred = {"id": cred[7]["id"], "type": "public-key"} 192 | CredMgmt.delete_cred(cred) 193 | 194 | # make sure it doesn't work 195 | req = FidoRequest(rp=rp) 196 | with pytest.raises(CtapError) as e: 197 | auth = device.sendGA(*req.toGA()) 198 | assert e.value.code == CtapError.ERR.NO_CREDENTIALS 199 | 200 | def test_add_delete(self, device, PinToken, CredMgmt): 201 | """ Delete a credential in the 'middle' and ensure other credentials are not affected. """ 202 | 203 | rp = {"id": "example_4.com", "name": "John Doe 3"} 204 | regs = [] 205 | 206 | # create 3 new RK's 207 | for i in range(0, 3): 208 | req = FidoRequest() 209 | pin_auth = hmac_sha256(PinToken, req.cdh)[:16] 210 | req = FidoRequest( 211 | pin_protocol=1, 212 | pin_auth=pin_auth, 213 | options={"rk": True}, 214 | rp=rp, 215 | ) 216 | reg = device.sendMC(*req.toMC()) 217 | regs.append(reg) 218 | print("CREATE:", hexlify(reg.auth_data.credential_data.credential_id)) 219 | 220 | # Check they all enumerate 221 | res = CredMgmt.enumerate_creds(regs[1].auth_data.rp_id_hash) 222 | assert len(res) == 3 223 | 224 | # delete the middle one 225 | creds = CredMgmt.enumerate_creds(reg.auth_data.rp_id_hash) 226 | for cred in creds: 227 | print("CHECK: ", hexlify(cred[7]["id"])) 228 | if cred[7]["id"] == regs[1].auth_data.credential_data.credential_id: 229 | break 230 | 231 | assert cred[7]["id"] == regs[1].auth_data.credential_data.credential_id 232 | 233 | cred = {"id": cred[7]["id"], "type": "public-key"} 234 | CredMgmt.delete_cred(cred) 235 | 236 | # Check one less enumerates 237 | res = CredMgmt.enumerate_creds(regs[0].auth_data.rp_id_hash) 238 | assert len(res) == 2 239 | 240 | def test_multiple_creds_per_multiple_rps( 241 | self, device, PinToken, CredMgmt, MC_RK_Res 242 | ): 243 | res = CredMgmt.enumerate_rps() 244 | assert len(res) == 2 245 | 246 | new_rps = [ 247 | {"id": "new_example_1.com", "name": "Example-3-creds"}, 248 | {"id": "new_example_2.com", "name": "Example-3-creds"}, 249 | {"id": "new_example_3.com", "name": "Example-3-creds"}, 250 | ] 251 | 252 | # create 3 new credentials per RP 253 | for rp in new_rps: 254 | for i in range(0, 3): 255 | req = FidoRequest() 256 | pin_auth = hmac_sha256(PinToken, req.cdh)[:16] 257 | req = FidoRequest( 258 | pin_protocol=1, 259 | pin_auth=pin_auth, 260 | options={"rk": True}, 261 | rp=rp, 262 | ) 263 | reg = device.sendMC(*req.toMC()) 264 | 265 | res = CredMgmt.enumerate_rps() 266 | assert len(res) == 5 267 | 268 | for rp in res: 269 | if rp[3]["id"][:12] == "new_example_": 270 | creds = CredMgmt.enumerate_creds(sha256(rp[3]["id"].encode("utf8"))) 271 | assert len(creds) == 3 272 | 273 | @pytest.mark.parametrize( 274 | "enumeration_test", [_test_enumeration, _test_enumeration_interleaved] 275 | ) 276 | def test_multiple_enumeration( 277 | self, device, PinToken, MC_RK_Res, CredMgmt, enumeration_test 278 | ): 279 | """ Test enumerate still works after different commands """ 280 | 281 | res = CredMgmt.enumerate_rps() 282 | 283 | expected_enumeration = {"xakcop.com": 1, "ssh:": 1} 284 | 285 | enumeration_test(CredMgmt, expected_enumeration) 286 | 287 | new_rps = [ 288 | {"id": "example-2.com", "name": "Example-2-creds", "count": 2}, 289 | {"id": "example-1.com", "name": "Example-1-creds", "count": 1}, 290 | {"id": "example-5.com", "name": "Example-5-creds", "count": 5}, 291 | ] 292 | 293 | # create 3 new credentials per RP 294 | for rp in new_rps: 295 | for i in range(0, rp["count"]): 296 | req = FidoRequest() 297 | pin_auth = hmac_sha256(PinToken, req.cdh)[:16] 298 | req = FidoRequest( 299 | pin_protocol=1, 300 | pin_auth=pin_auth, 301 | options={"rk": True}, 302 | rp={"id": rp["id"], "name": rp["name"]}, 303 | ) 304 | reg = device.sendMC(*req.toMC()) 305 | 306 | # Now expect creds from this RP 307 | expected_enumeration[rp["id"]] = rp["count"] 308 | 309 | enumeration_test(CredMgmt, expected_enumeration) 310 | enumeration_test(CredMgmt, expected_enumeration) 311 | 312 | metadata = CredMgmt.get_metadata() 313 | 314 | enumeration_test(CredMgmt, expected_enumeration) 315 | enumeration_test(CredMgmt, expected_enumeration) 316 | 317 | @pytest.mark.parametrize( 318 | "enumeration_test", [_test_enumeration, _test_enumeration_interleaved] 319 | ) 320 | def test_multiple_enumeration_with_deletions( 321 | self, device, PinToken, MC_RK_Res, CredMgmt, enumeration_test 322 | ): 323 | """ Create each credential in random order. Test enumerate still works after randomly deleting each credential""" 324 | 325 | res = CredMgmt.enumerate_rps() 326 | 327 | expected_enumeration = {"xakcop.com": 1, "ssh:": 1} 328 | 329 | enumeration_test(CredMgmt, expected_enumeration) 330 | 331 | new_rps = [ 332 | {"id": "example-1.com", "name": "Example-1-creds", "count": 1}, 333 | {"id": "example-2.com", "name": "Example-2-creds", "count": 2}, 334 | {"id": "example-3.com", "name": "Example-3-creds", "count": 3}, 335 | ] 336 | 337 | reg_requests = [] 338 | 339 | # create new credentials per RP in random order 340 | for rp in new_rps: 341 | for i in range(0, rp["count"]): 342 | req = FidoRequest() 343 | pin_auth = hmac_sha256(PinToken, req.cdh)[:16] 344 | req = FidoRequest( 345 | pin_protocol=1, 346 | pin_auth=pin_auth, 347 | options={"rk": True}, 348 | rp={"id": rp["id"], "name": rp["name"]}, 349 | user=generate_user_maximum(), 350 | ) 351 | reg_requests.append(req) 352 | 353 | while len(reg_requests): 354 | req = random.choice(reg_requests) 355 | reg_requests.remove(req) 356 | device.sendMC(*req.toMC()) 357 | 358 | if req.rp["id"] not in expected_enumeration: 359 | expected_enumeration[req.rp["id"]] = 1 360 | else: 361 | expected_enumeration[req.rp["id"]] += 1 362 | 363 | enumeration_test(CredMgmt, expected_enumeration) 364 | 365 | total_creds = len(reg_requests) 366 | 367 | while total_creds != 0: 368 | rp = random.choice(list(expected_enumeration.keys())) 369 | 370 | num = expected_enumeration[rp] 371 | 372 | index = 0 if num == 1 else random.randint(0, num - 1) 373 | cred = CredMgmt.enumerate_creds(sha256(rp.encode("utf8")))[index] 374 | 375 | # print('Delete %d index (%d total) cred of %s' % (index, expected_enumeration[rp], rp)) 376 | CredMgmt.delete_cred({"id": cred[7]["id"], "type": "public-key"}) 377 | 378 | expected_enumeration[rp] -= 1 379 | if expected_enumeration[rp] == 0: 380 | del expected_enumeration[rp] 381 | 382 | if len(list(expected_enumeration.keys())) == 0: 383 | break 384 | 385 | enumeration_test(CredMgmt, expected_enumeration) 386 | 387 | def _test_wrong_pinauth(self, device, cmd, PinToken): 388 | 389 | credMgmt = CredMgmtWrongPinAuth(device, PinToken) 390 | 391 | for i in range(2): 392 | with pytest.raises(CtapError) as e: 393 | cmd(credMgmt) 394 | assert e.value.code == CtapError.ERR.PIN_AUTH_INVALID 395 | 396 | with pytest.raises(CtapError) as e: 397 | cmd(credMgmt) 398 | assert e.value.code == CtapError.ERR.PIN_AUTH_BLOCKED 399 | 400 | device.reboot() 401 | credMgmt = CredMgmtWrongPinAuth(device, PinToken) 402 | 403 | for i in range(2): 404 | time.sleep(0.2) 405 | with pytest.raises(CtapError) as e: 406 | cmd(credMgmt) 407 | assert e.value.code == CtapError.ERR.PIN_AUTH_INVALID 408 | 409 | with pytest.raises(CtapError) as e: 410 | cmd(credMgmt) 411 | assert e.value.code == CtapError.ERR.PIN_AUTH_BLOCKED 412 | 413 | device.reboot() 414 | credMgmt = CredMgmtWrongPinAuth(device, PinToken) 415 | 416 | for i in range(1): 417 | time.sleep(0.2) 418 | with pytest.raises(CtapError) as e: 419 | cmd(credMgmt) 420 | assert e.value.code == CtapError.ERR.PIN_AUTH_INVALID 421 | 422 | with pytest.raises(CtapError) as e: 423 | cmd(credMgmt) 424 | assert e.value.code == CtapError.ERR.PIN_BLOCKED 425 | 426 | 427 | -------------------------------------------------------------------------------- /tests/standard/transport/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trussed-dev/fido2-tests/591d3d2279949e08de0766897f24bcfd39af1339/tests/standard/transport/__init__.py -------------------------------------------------------------------------------- /tests/standard/transport/test_hid.py: -------------------------------------------------------------------------------- 1 | import os 2 | import socket 3 | import sys 4 | import time 5 | from binascii import hexlify, unhexlify 6 | 7 | import pytest 8 | from fido2.ctap import CtapError 9 | from fido2.hid import CTAPHID 10 | 11 | 12 | @pytest.mark.skipif("--nfc" in sys.argv, reason="Wrong transport") 13 | class TestHID(object): 14 | def test_long_ping(self, device): 15 | amt = 1000 16 | pingdata = os.urandom(amt) 17 | 18 | t1 = time.time() * 1000 19 | r = device.send_data(CTAPHID.PING, pingdata) 20 | t2 = time.time() * 1000 21 | delt = t2 - t1 22 | 23 | assert not (delt > 555 * (amt / 1000)) 24 | 25 | assert r == pingdata 26 | 27 | def test_init(self, device, check_timeouts=False): 28 | if check_timeouts: 29 | with pytest.raises(socket.timeout): 30 | cmd, resp = self.recv_raw() 31 | 32 | payload = b"\x11\x11\x11\x11\x11\x11\x11\x11" 33 | r = device.send_data(CTAPHID.INIT, payload) 34 | print(r) 35 | assert r[:8] == payload 36 | 37 | def test_ping(self, device): 38 | 39 | pingdata = os.urandom(100) 40 | r = device.send_data(CTAPHID.PING, pingdata) 41 | assert r == pingdata 42 | 43 | def test_wink(self, device): 44 | r = device.send_data(CTAPHID.WINK, "") 45 | 46 | def test_cbor_no_payload(self, device): 47 | payload = b"\x11\x11\x11\x11\x11\x11\x11\x11" 48 | r = device.send_data(CTAPHID.INIT, payload) 49 | capabilities = r[16] 50 | 51 | if (capabilities ^ 0x04) != 0: 52 | print("Implements CBOR.") 53 | with pytest.raises(CtapError) as e: 54 | r = device.send_data(CTAPHID.CBOR, "") 55 | assert e.value.code == CtapError.ERR.INVALID_LENGTH 56 | else: 57 | print("CBOR is not implemented.") 58 | 59 | def test_no_data_in_u2f_msg(self, device): 60 | payload = b"\x11\x11\x11\x11\x11\x11\x11\x11" 61 | r = device.send_data(CTAPHID.INIT, payload) 62 | capabilities = r[16] 63 | 64 | if (capabilities ^ 0x08) == 0: 65 | print("U2F implemented.") 66 | with pytest.raises(CtapError) as e: 67 | r = device.send_data(CTAPHID.MSG, "") 68 | print(hexlify(r)) 69 | assert e.value.code == CtapError.ERR.INVALID_LENGTH 70 | else: 71 | print("U2F not implemented.") 72 | 73 | def test_invalid_hid_cmd(self, device): 74 | r = device.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") 75 | 76 | with pytest.raises(CtapError) as e: 77 | r = device.send_data(0x66, "") 78 | assert e.value.code == CtapError.ERR.INVALID_COMMAND 79 | 80 | def test_oversize_packet(self, device): 81 | device.send_raw("\x81\x1d\xba\x00") 82 | cmd, resp = device.recv_raw() 83 | assert resp[0] == CtapError.ERR.INVALID_LENGTH 84 | 85 | def test_skip_sequence_number(self, device): 86 | r = device.send_data(CTAPHID.PING, "\x44" * 200) 87 | device.send_raw("\x81\x04\x90") 88 | device.send_raw("\x00") 89 | device.send_raw("\x01") 90 | # skip 2 91 | device.send_raw("\x03") 92 | cmd, resp = device.recv_raw() 93 | assert resp[0] == CtapError.ERR.INVALID_SEQ 94 | 95 | def test_resync_and_ping(self, device): 96 | r = device.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") 97 | pingdata = os.urandom(100) 98 | r = device.send_data(CTAPHID.PING, pingdata) 99 | if r != pingdata: 100 | raise ValueError("Ping data not echo'd") 101 | 102 | def test_ping_abort(self, device): 103 | device.send_raw("\x81\x04\x00") 104 | device.send_raw("\x00") 105 | device.send_raw("\x01") 106 | device.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") 107 | 108 | def test_ping_abort_from_different_cid(self, device, check_timeouts=False): 109 | oldcid = device.cid() 110 | newcid = "\x11\x22\x33\x44" 111 | device.send_raw("\x81\x10\x00") 112 | device.send_raw("\x00") 113 | device.send_raw("\x01") 114 | device.set_cid(newcid) 115 | device.send_raw( 116 | "\x86\x00\x08\x11\x22\x33\x44\x55\x66\x77\x88" 117 | ) # init from different cid 118 | print("wait for init response") 119 | cmd, r = device.recv_raw() # init response 120 | assert cmd == 0x86 121 | device.set_cid(oldcid) 122 | if check_timeouts: 123 | # print('wait for timeout') 124 | cmd, r = device.recv_raw() # timeout response 125 | assert cmd == 0xBF 126 | 127 | def test_timeout(self, device): 128 | device.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") 129 | t1 = time.time() * 1000 130 | device.send_raw("\x81\x04\x00") 131 | device.send_raw("\x00") 132 | device.send_raw("\x01") 133 | cmd, r = device.recv_raw() # timeout response 134 | t2 = time.time() * 1000 135 | delt = t2 - t1 136 | assert cmd == 0xBF 137 | assert r[0] == CtapError.ERR.TIMEOUT 138 | assert delt < 1000 and delt > 400 139 | 140 | def test_not_cont(self, device, check_timeouts=False): 141 | device.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") 142 | device.send_raw("\x81\x04\x00") 143 | device.send_raw("\x00") 144 | device.send_raw("\x01") 145 | device.send_raw("\x81\x10\x00") # init packet 146 | cmd, r = device.recv_raw() # timeout response 147 | assert cmd == 0xBF 148 | assert r[0] == CtapError.ERR.INVALID_SEQ 149 | 150 | if check_timeouts: 151 | device.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") 152 | device.send_raw("\x01\x10\x00") 153 | with pytest.raises(socket.timeout): 154 | cmd, r = device.recv_raw() # timeout response 155 | 156 | def test_check_busy(self, device): 157 | t1 = time.time() * 1000 158 | device.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") 159 | oldcid = device.cid() 160 | newcid = "\x11\x22\x33\x44" 161 | device.send_raw("\x81\x04\x00") 162 | device.set_cid(newcid) 163 | device.send_raw("\x81\x04\x00") 164 | cmd, r = device.recv_raw() # busy response 165 | t2 = time.time() * 1000 166 | assert t2 - t1 < 100 167 | assert cmd == 0xBF 168 | assert r[0] == CtapError.ERR.CHANNEL_BUSY 169 | 170 | device.set_cid(oldcid) 171 | cmd, r = device.recv_raw() # timeout response 172 | assert cmd == 0xBF 173 | assert r[0] == CtapError.ERR.TIMEOUT 174 | 175 | def test_check_busy_interleaved(self, device): 176 | cid1 = "\x11\x22\x33\x44" 177 | cid2 = "\x01\x22\x33\x44" 178 | device.set_cid(cid2) 179 | device.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") 180 | device.set_cid(cid1) 181 | device.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") 182 | device.send_raw("\x81\x00\x63") # echo 99 bytes first channel 183 | 184 | device.set_cid(cid2) # send ping on 2nd channel 185 | device.send_raw("\x81\x00\x63") 186 | time.sleep(0.1) 187 | device.send_raw("\x00") 188 | 189 | cmd, r = device.recv_raw() # busy response 190 | 191 | device.set_cid(cid1) # finish 1st channel ping 192 | device.send_raw("\x00") 193 | 194 | device.set_cid(cid2) 195 | 196 | assert cmd == 0xBF 197 | assert r[0] == CtapError.ERR.CHANNEL_BUSY 198 | 199 | device.set_cid(cid1) 200 | cmd, r = device.recv_raw() # ping response 201 | assert cmd == 0x81 202 | assert len(r) == 0x63 203 | 204 | def test_cid_0(self, device): 205 | device.set_cid("\x00\x00\x00\x00") 206 | device.send_raw( 207 | "\x86\x00\x08\x11\x22\x33\x44\x55\x66\x77\x88", cid="\x00\x00\x00\x00" 208 | ) 209 | cmd, r = device.recv_raw() # timeout 210 | assert cmd == 0xBF 211 | assert r[0] == CtapError.ERR.INVALID_CHANNEL 212 | device.set_cid("\x05\x04\x03\x02") 213 | 214 | def test_cid_ffffffff(self, device): 215 | 216 | device.set_cid("\xff\xff\xff\xff") 217 | device.send_raw( 218 | "\x81\x00\x08\x11\x22\x33\x44\x55\x66\x77\x88", cid="\xff\xff\xff\xff" 219 | ) 220 | cmd, r = device.recv_raw() # timeout 221 | assert cmd == 0xBF 222 | assert r[0] == CtapError.ERR.INVALID_CHANNEL 223 | device.set_cid("\x05\x04\x03\x02") 224 | 225 | def test_keep_alive(self, device, check_timeouts=False): 226 | 227 | precanned_make_credential = unhexlify( 228 | '01a401582031323334353637383961626364656630313233343536373'\ 229 | '8396162636465663002a26269646b6578616d706c652e6f7267646e61'\ 230 | '6d65694578616d706c65525003a462696446cc2abaf119f26469636f6'\ 231 | 'e781f68747470733a2f2f7777772e77332e6f72672f54522f77656261'\ 232 | '7574686e2f646e616d657256696e204f6c696d7069612047657272696'\ 233 | '56b646973706c61794e616d65781c446973706c617965642056696e20'\ 234 | '4f6c696d706961204765727269650481a263616c672664747970656a7'\ 235 | '075626c69632d6b6579') 236 | 237 | count = 0 238 | def count_keepalive(_x): 239 | nonlocal count 240 | count += 1 241 | 242 | # We should get a keepalive within .5s 243 | try: 244 | r = device.send_data(CTAPHID.CBOR, precanned_make_credential, timeout = .50, on_keepalive = count_keepalive) 245 | except CtapError as e: 246 | assert e.code == CtapError.ERR.KEEPALIVE_CANCEL 247 | assert count > 0 248 | 249 | # wait for authnr to get UP or timeout 250 | while True: 251 | try: 252 | r = device.send_data(CTAPHID.CBOR, '\x04') # getInfo 253 | break 254 | except CtapError as e: 255 | assert e.code == CtapError.ERR.CHANNEL_BUSY 256 | 257 | -------------------------------------------------------------------------------- /tests/standard/transport/test_nfc.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import pytest 3 | 4 | from tests.utils import FidoRequest 5 | 6 | 7 | @pytest.mark.skipif(not ("--nfc" in sys.argv), reason="NFC transport only") 8 | class TestMakeCredential(object): 9 | def test_big_request_response(self, device, MCRes): 10 | req = FidoRequest( 11 | MCRes, 12 | exclude_list=[ 13 | { 14 | "id": b"0123456789012345678901234567890123456789012345678901234567890123456789", 15 | "type": "public-key", 16 | }, 17 | { 18 | "id": b"1123456789012345678901234567890123456789012345678901234567890123456789", 19 | "type": "public-key", 20 | }, 21 | { 22 | "id": b"2123456789012345678901234567890123456789012345678901234567890123456789", 23 | "type": "public-key", 24 | }, 25 | ], 26 | ) 27 | device.sendMC(*req.toMC()) 28 | -------------------------------------------------------------------------------- /tests/standard/u2f/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trussed-dev/fido2-tests/591d3d2279949e08de0766897f24bcfd39af1339/tests/standard/u2f/__init__.py -------------------------------------------------------------------------------- /tests/standard/u2f/test_u2f.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from fido2.client import ClientError 3 | from fido2.ctap1 import APDU, CTAP1, ApduError 4 | from fido2.utils import sha256 5 | 6 | from tests.utils import FidoRequest, verify 7 | 8 | 9 | class TestU2F(object): 10 | def test_u2f_reg(self, device, RegRes): 11 | RegRes.verify(RegRes.request.appid, RegRes.request.challenge) 12 | 13 | def test_u2f_auth(self, device, RegRes, AuthRes): 14 | AuthRes.verify( 15 | AuthRes.request.appid, AuthRes.request.challenge, RegRes.public_key 16 | ) 17 | 18 | def test_u2f_auth_check_only(self, device, RegRes): 19 | with pytest.raises(ApduError) as e: 20 | device.ctap1.authenticate( 21 | RegRes.request.challenge, 22 | RegRes.request.appid, 23 | RegRes.key_handle, 24 | check_only=True, 25 | ) 26 | assert e.value.code == APDU.USE_NOT_SATISFIED 27 | 28 | def test_version(self, device): 29 | assert device.ctap1.get_version() == "U2F_V2" 30 | 31 | def test_bad_ins(self, device): 32 | with pytest.raises(ApduError) as e: 33 | device.ctap1.send_apdu(0, 0, 0, 0, b"") 34 | assert e.value.code == 0x6D00 35 | 36 | def test_bad_cla(self, device): 37 | with pytest.raises(ApduError) as e: 38 | device.ctap1.send_apdu(1, CTAP1.INS.VERSION, 0, 0, b"abc") 39 | assert e.value.code == 0x6E00 40 | 41 | @pytest.mark.parametrize("iterations", (5,)) 42 | def test_u2f(self, device, iterations): 43 | lastc = 0 44 | chal = FidoRequest().challenge 45 | appid = FidoRequest().appid 46 | 47 | regs = [] 48 | 49 | for i in range(0, iterations): 50 | print("U2F reg + auth %d/%d (count: %02x)" % (i + 1, iterations, lastc)) 51 | reg = device.register(chal, appid) 52 | reg.verify(appid, chal) 53 | auth = device.authenticate(chal, appid, reg.key_handle) 54 | auth.verify(appid, chal, reg.public_key) 55 | 56 | regs.append(reg) 57 | # check endianness 58 | if lastc: 59 | assert (auth.counter - lastc) < 256 60 | lastc = auth.counter 61 | if lastc > 0x80000000: 62 | print("WARNING: counter is unusually high: %04x" % lastc) 63 | assert 0 64 | 65 | for i in range(0, iterations): 66 | auth = device.authenticate(chal, appid, regs[i].key_handle) 67 | auth.verify(appid, chal, regs[i].public_key) 68 | 69 | device.reboot() 70 | 71 | for i in range(0, iterations): 72 | auth = device.authenticate(chal, appid, regs[i].key_handle) 73 | auth.verify(appid, chal, regs[i].public_key) 74 | 75 | print("Check that all previous credentials are registered...") 76 | for i in range(0, iterations): 77 | with pytest.raises(ApduError) as e: 78 | auth = device.ctap1.authenticate( 79 | chal, appid, regs[i].key_handle, check_only=True 80 | ) 81 | assert e.value.code == APDU.USE_NOT_SATISFIED 82 | 83 | def test_bad_key_handle(self, device, RegRes): 84 | kh = bytearray(RegRes.key_handle) 85 | kh[0] = kh[0] ^ (0x40) 86 | 87 | with pytest.raises(ApduError) as e: 88 | device.ctap1.authenticate( 89 | RegRes.request.challenge, RegRes.request.appid, kh, check_only=True 90 | ) 91 | assert e.value.code == APDU.WRONG_DATA 92 | 93 | with pytest.raises(ClientError) as e: 94 | device.authenticate( 95 | RegRes.request.challenge, RegRes.request.appid, kh 96 | ) 97 | assert e.value.cause.code == APDU.WRONG_DATA 98 | 99 | def test_bad_key_handle_length(self, device, RegRes): 100 | kh = bytearray(RegRes.key_handle) 101 | 102 | with pytest.raises(ClientError) as e: 103 | device.authenticate( 104 | RegRes.request.challenge, RegRes.request.appid, kh[: len(kh) // 2] 105 | ) 106 | assert e.value.cause.code == APDU.WRONG_DATA 107 | 108 | def test_incorrect_appid(self, device, RegRes): 109 | 110 | badid = bytearray(RegRes.request.appid) 111 | badid[0] = badid[0] ^ (0x40) 112 | with pytest.raises(ClientError) as e: 113 | device.authenticate( 114 | RegRes.request.challenge, badid, RegRes.key_handle 115 | ) 116 | assert e.value.cause.code == APDU.WRONG_DATA 117 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | import math 2 | import random 3 | import secrets 4 | import sys 5 | from threading import Event, Timer 6 | from numbers import Number 7 | 8 | from fido2.ctap2 import ES256, AttestedCredentialData, PinProtocolV1 9 | from fido2.utils import hmac_sha256, sha256 10 | 11 | if "trezor" in sys.argv: 12 | from .vendor.trezor.utils import DeviceSelectCredential 13 | else: 14 | from .vendor.solo.utils import DeviceSelectCredential 15 | 16 | name_list = open("data/first-names.txt").readlines() 17 | 18 | 19 | def shannon_entropy(data): 20 | s = 0.0 21 | total = len(data) 22 | for x in range(0, 256): 23 | freq = data.count(x) 24 | p = freq / total 25 | if p > 0: 26 | s -= p * math.log2(p) 27 | return s 28 | 29 | 30 | def verify(reg, auth, cdh=None): 31 | credential_data = AttestedCredentialData(reg.auth_data.credential_data) 32 | if cdh is None: 33 | cdh = auth.request.cdh 34 | auth.verify(cdh, credential_data.public_key) 35 | assert auth.auth_data.rp_id_hash == reg.auth_data.rp_id_hash 36 | if auth.credential is not None: 37 | assert auth.credential["id"] == reg.auth_data.credential_data.credential_id 38 | 39 | 40 | def generate_rp(): 41 | return {"id": "example.org", "name": "ExampleRP"} 42 | 43 | 44 | def generate_user(): 45 | # https://www.w3.org/TR/webauthn/#user-handle 46 | user_id_length = random.randint(1, 64) 47 | user_id = secrets.token_bytes(user_id_length) 48 | 49 | # https://www.w3.org/TR/webauthn/#dictionary-pkcredentialentity 50 | name = " ".join(random.choice(name_list).strip() for i in range(0, 3)) 51 | icon = "https://www.w3.org/TR/webauthn/" 52 | display_name = "Displayed " + name 53 | 54 | return {"id": user_id, "name": name, "icon": icon, "displayName": display_name} 55 | 56 | 57 | counter = 1 58 | 59 | 60 | def generate_user_maximum(): 61 | """ 62 | Generate RK with the maximum lengths of the fields, according to the minimal requirements of the FIDO2 spec 63 | """ 64 | global counter 65 | 66 | # https://www.w3.org/TR/webauthn/#user-handle 67 | user_id_length = 64 68 | user_id = secrets.token_bytes(user_id_length) 69 | 70 | # https://www.w3.org/TR/webauthn/#dictionary-pkcredentialentity 71 | name = " ".join(random.choice(name_list).strip() for i in range(0, 30)) 72 | name = f"{counter}: {name}" 73 | icon = "https://www.w3.org/TR/webauthn/" + "A" * 128 74 | display_name = "Displayed " + name 75 | 76 | name = name[:64] 77 | display_name = display_name[:64] 78 | icon = icon[:128] 79 | 80 | counter += 1 81 | 82 | return {"id": user_id, "name": name, "icon": icon, "displayName": display_name} 83 | 84 | 85 | def generate_challenge(): 86 | return secrets.token_bytes(32) 87 | 88 | 89 | def get_key_params(): 90 | return [{"type": "public-key", "alg": ES256.ALGORITHM}] 91 | 92 | 93 | def generate_cdh(): 94 | return b"123456789abcdef0123456789abcdef0" 95 | 96 | 97 | def generate(param): 98 | if param == "rp": 99 | return generate_rp() 100 | if param == "user": 101 | return generate_user() 102 | if param == "challenge": 103 | return generate_challenge() 104 | if param == "cdh": 105 | return generate_cdh() 106 | if param == "key_params": 107 | return get_key_params() 108 | if param == "allow_list": 109 | return [] 110 | if param == "on_keepalive": 111 | return DeviceSelectCredential(1) 112 | return None 113 | 114 | 115 | class Empty: 116 | pass 117 | 118 | 119 | class FidoRequest: 120 | def __init__(self, request=None, **kwargs): 121 | 122 | if not isinstance(request, FidoRequest) and request is not None: 123 | request = request.request 124 | 125 | self.request = request 126 | 127 | for i in ( 128 | "cdh", 129 | "key_params", 130 | "allow_list", 131 | "challenge", 132 | "rp", 133 | "user", 134 | "pin_protocol", 135 | "options", 136 | "appid", 137 | "exclude_list", 138 | "extensions", 139 | "pin_auth", 140 | "timeout", 141 | "on_keepalive", 142 | "pin", 143 | ): 144 | self.save_attr(i, kwargs.get(i, Empty), request) 145 | 146 | if isinstance(self.rp, dict) and "id" in self.rp: 147 | if hasattr(self.rp["id"], "encode"): 148 | self.appid = sha256(self.rp["id"].encode("utf8")) 149 | 150 | # self.chal = sha256(self.challenge.encode("utf8")) 151 | 152 | def save_attr(self, attr, value, request): 153 | """ 154 | Will assign attribute from source, in following priority: 155 | Argument, request object, generated 156 | """ 157 | if value != Empty: 158 | setattr(self, attr, value) 159 | elif request is not None: 160 | setattr(self, attr, getattr(request, attr)) 161 | else: 162 | setattr(self, attr, generate(attr)) 163 | 164 | def toGA( 165 | self, 166 | ): 167 | args = [ 168 | None if not self.rp else self.rp["id"], 169 | self.cdh, 170 | self.allow_list, 171 | self.extensions, 172 | self.options, 173 | self.pin_auth, 174 | self.pin_protocol, 175 | self.timeout, 176 | self.on_keepalive, 177 | ] 178 | if self.pin: 179 | args.append(self.pin) 180 | return args 181 | 182 | 183 | 184 | def toMC( 185 | self, 186 | ): 187 | args = [ 188 | self.cdh, 189 | self.rp, 190 | self.user, 191 | self.key_params, 192 | self.exclude_list, 193 | self.extensions, 194 | self.options, 195 | self.pin_auth, 196 | self.pin_protocol, 197 | self.timeout, 198 | self.on_keepalive, 199 | ] 200 | if self.pin: 201 | args.append(self.pin) 202 | return args 203 | 204 | 205 | # Timeout from: 206 | # https://github.com/Yubico/python-fido2/blob/f1dc028d6158e1d6d51558f72055c65717519b9b/fido2/utils.py 207 | # Copyright (c) 2013 Yubico AB 208 | # All rights reserved. 209 | # 210 | # Redistribution and use in source and binary forms, with or 211 | # without modification, are permitted provided that the following 212 | # conditions are met: 213 | # 214 | # 1. Redistributions of source code must retain the above copyright 215 | # notice, this list of conditions and the following disclaimer. 216 | # 2. Redistributions in binary form must reproduce the above 217 | # copyright notice, this list of conditions and the following 218 | # disclaimer in the documentation and/or other materials provided 219 | # with the distribution. 220 | # 221 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 222 | # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 223 | # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 224 | # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 225 | # COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 226 | # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 227 | # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 228 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 229 | # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 230 | # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 231 | # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 232 | # POSSIBILITY OF SUCH DAMAGE. 233 | 234 | 235 | class Timeout(object): 236 | """Utility class for adding a timeout to an event. 237 | :param time_or_event: A number, in seconds, or a threading.Event object. 238 | :ivar event: The Event associated with the Timeout. 239 | :ivar timer: The Timer associated with the Timeout, if any. 240 | """ 241 | 242 | def __init__(self, time_or_event): 243 | 244 | if isinstance(time_or_event, Number): 245 | self.event = Event() 246 | self.timer = Timer(time_or_event, self.event.set) 247 | else: 248 | self.event = time_or_event 249 | self.timer = None 250 | 251 | def __enter__(self): 252 | if self.timer: 253 | self.timer.start() 254 | return self.event 255 | 256 | def __exit__(self, exc_type, exc_val, exc_tb): 257 | if self.timer: 258 | self.timer.cancel() 259 | self.timer.join() 260 | -------------------------------------------------------------------------------- /tests/vendor/solo/test_solo.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import pytest 4 | import ecdsa 5 | import hashlib 6 | from fido2.ctap1 import ApduError 7 | from fido2.ctap2 import CtapError 8 | from fido2.utils import hmac_sha256, sha256, int2bytes 9 | 10 | try: 11 | from solo.client import SoloClient 12 | except: 13 | from solo.devices.solo_v1 import Client as SoloClient 14 | 15 | from solo.commands import SoloExtension 16 | 17 | from tests.utils import shannon_entropy, verify, FidoRequest 18 | 19 | 20 | def convert_der_sig_to_padded_binary(der): 21 | r, s = ecdsa.util.sigdecode_der(der, None) 22 | r = int2bytes(r) 23 | s = int2bytes(s) 24 | 25 | r = (b"\x00" * (32 - len(r))) + r 26 | s = (b"\x00" * (32 - len(s))) + s 27 | return r + s 28 | 29 | 30 | @pytest.fixture(scope="module", params=["u2f"]) 31 | def solo(request, device): 32 | sc = SoloClient() 33 | sc.find_device(device.dev) 34 | if request.param == "u2f": 35 | sc.use_u2f() 36 | else: 37 | sc.use_hid() 38 | return sc 39 | 40 | 41 | IS_EXPERIMENTAL = "--experimental" in sys.argv 42 | IS_NFC = "--nfc" in sys.argv 43 | 44 | 45 | @pytest.mark.skipif(IS_NFC, reason="Wrong transport") 46 | class TestSolo(object): 47 | def test_solo(self, solo): 48 | pass 49 | 50 | def test_rng(self, solo): 51 | 52 | total = 1024 * 16 53 | entropy = b"" 54 | while len(entropy) < total: 55 | entropy += solo.get_rng() 56 | 57 | s = shannon_entropy(entropy) 58 | assert s > 7.98 59 | print("Entropy is %.5f bits per byte." % s) 60 | 61 | def test_version(self, solo): 62 | assert len(solo.solo_version()) == 4 63 | 64 | def test_version_hid(self, solo): 65 | data = solo.send_data_hid(0x61, b"") 66 | assert len(data) == 4 67 | print(f"Version is {data[0]}.{data[1]}.{data[2]} locked?=={data[3]}") 68 | 69 | def test_bootloader_not(self, solo): 70 | with pytest.raises(ApduError) as e: 71 | solo.write_flash(0x0, b"1234") 72 | 73 | def test_fido2_bridge(self, solo): 74 | exchange = solo.exchange 75 | solo.exchange = solo.exchange_fido2 76 | 77 | req = SoloClient.format_request(SoloExtension.version, 0, b"A" * 16) 78 | a = solo.ctap2.get_assertion( 79 | solo.host, b"B" * 32, [{"id": req, "type": "public-key"}] 80 | ) 81 | 82 | assert a.auth_data.rp_id_hash == sha256(solo.host.encode("utf8")) 83 | assert a.credential["id"] == req 84 | assert (a.auth_data.flags & 0x5) == 0x5 85 | 86 | solo.get_rng() 87 | 88 | solo.exchange = exchange 89 | 90 | @pytest.mark.skipif(not IS_EXPERIMENTAL, reason="Experimental") 91 | def test_load_external_key_wrong_length( 92 | self, 93 | solo, 94 | ): 95 | ext_key_cmd = 0x62 96 | with pytest.raises(CtapError) as e: 97 | solo.send_data_hid(ext_key_cmd, b"\x01" + b"wrong length" * 2) 98 | assert e.value.code == CtapError.ERR.INVALID_LENGTH 99 | 100 | @pytest.mark.skipif(not IS_EXPERIMENTAL, reason="Experimental") 101 | def test_load_external_key_invalidate_old_cred(self, solo, device, MCRes, GARes): 102 | ext_key_cmd = 0x62 103 | verify(MCRes, GARes) 104 | print("Enter user presence THREE times.") 105 | solo.send_data_hid(ext_key_cmd, b"\x01" + b"Z" * 32 + b"dicekeys key") 106 | 107 | # Old credential should not exist now. 108 | with pytest.raises(CtapError) as e: 109 | ga_bad_req = FidoRequest(GARes) 110 | device.sendGA(*ga_bad_req.toGA()) 111 | assert e.value.code == CtapError.ERR.NO_CREDENTIALS 112 | 113 | @pytest.mark.skipif(not IS_EXPERIMENTAL, reason="Experimental") 114 | def test_load_external_key( 115 | self, 116 | solo, 117 | device, 118 | ): 119 | 120 | key_A = b"A" * 32 121 | key_B = b"B" * 32 122 | ext_state = b"I'm a dicekey key" 123 | version = b"\x01" 124 | 125 | ext_key_cmd = 0x62 126 | print("Enter user presence THREE times.") 127 | solo.send_data_hid(ext_key_cmd, version + key_A + ext_state) 128 | 129 | # New credential works. 130 | mc_A_req = FidoRequest() 131 | mc_A_res = device.sendMC(*mc_A_req.toMC()) 132 | 133 | allow_list = [ 134 | { 135 | "id": mc_A_res.auth_data.credential_data.credential_id, 136 | "type": "public-key", 137 | } 138 | ] 139 | ga_A_req = FidoRequest(mc_A_req, allow_list=allow_list) 140 | ga_A_res = device.sendGA(*FidoRequest(ga_A_req).toGA()) 141 | 142 | verify(mc_A_res, ga_A_res, ga_A_req.cdh) 143 | 144 | # Load up Key B and verify cred A doesn't exist. 145 | print("Enter user presence THREE times.") 146 | solo.send_data_hid(ext_key_cmd, version + key_B + ext_state) 147 | with pytest.raises(CtapError) as e: 148 | ga_A_res = device.sendGA(*FidoRequest(ga_A_req).toGA()) 149 | assert e.value.code == CtapError.ERR.NO_CREDENTIALS 150 | 151 | # Load up Key A and verify cred A is back. 152 | print("Enter user presence THREE times.") 153 | solo.send_data_hid(ext_key_cmd, version + key_A + ext_state) 154 | ga_A_res = device.sendGA(*FidoRequest(ga_A_req).toGA()) 155 | verify(mc_A_res, ga_A_res, ga_A_req.cdh) 156 | 157 | @pytest.mark.skipif(not IS_EXPERIMENTAL, reason="Experimental") 158 | def test_ext_state_in_credential_id( 159 | self, 160 | solo, 161 | device, 162 | ): 163 | 164 | key_A = b"A" * 32 165 | ext_state = b"I'm a dicekey key abc1234!!@@##" 166 | version = b"\x01" 167 | 168 | ext_key_cmd = 0x62 169 | print("Enter user presence THREE times.") 170 | solo.send_data_hid(ext_key_cmd, version + key_A + ext_state) 171 | 172 | # New credential works. 173 | mc_A_req = FidoRequest() 174 | mc_A_res = device.sendMC(*mc_A_req.toMC()) 175 | 176 | assert ext_state in mc_A_res.auth_data.credential_data.credential_id 177 | 178 | @pytest.mark.skipif(not IS_EXPERIMENTAL, reason="Experimental") 179 | def test_backup_credential_is_generated_correctly( 180 | self, 181 | solo, 182 | device, 183 | ): 184 | import seedweed 185 | from binascii import hexlify 186 | 187 | key_A = b"A" * 32 188 | ext_state = b"I'm a dicekey key!" 189 | version = b"\x01" 190 | 191 | ext_key_cmd = 0x62 192 | print("Enter user presence THREE times.") 193 | solo.send_data_hid(ext_key_cmd, version + key_A + ext_state) 194 | 195 | # New credential works. 196 | mc_A_req = FidoRequest() 197 | mc_A_res = device.sendMC(*mc_A_req.toMC()) 198 | 199 | rpIdHash = sha256(mc_A_req.rp["id"].encode("utf8")) 200 | 201 | credId = mc_A_res.auth_data.credential_data.credential_id 202 | 203 | ( 204 | uniqueId, 205 | extStateInCredId, 206 | credMacInCredId, 207 | ) = seedweed.nonce_extstate_mac_from_credential_id(credId) 208 | 209 | seedweed.validate_credential_id(key_A, credId, rpIdHash) 210 | credMac = hmac_sha256(key_A, rpIdHash + version + uniqueId + ext_state) 211 | 212 | allow_list = [ 213 | { 214 | "id": mc_A_res.auth_data.credential_data.credential_id, 215 | "type": "public-key", 216 | } 217 | ] 218 | ga_req = FidoRequest(allow_list=allow_list) 219 | 220 | ga_res = device.sendGA(*ga_req.toGA()) 221 | 222 | verify(mc_A_res, ga_res, ga_req.cdh) 223 | 224 | # Independently create the key and verify 225 | _, _, keypair, iterations = seedweed.keypair_from_seed_mac(key_A, credMac) 226 | assert iterations == 1 227 | keypair.verifying_key.verify( 228 | ga_res.signature, 229 | ga_res.auth_data + ga_req.cdh, 230 | sigdecode=ecdsa.util.sigdecode_der, 231 | hashfunc=hashlib.sha256, 232 | ) 233 | 234 | # @pytest.mark.skipif(False, reason="Experimental") 235 | @pytest.mark.skipif(not IS_EXPERIMENTAL, reason="Experimental") 236 | def test_seedweed_vectors_make_credential( 237 | self, 238 | solo, 239 | device, 240 | ): 241 | import seedweed 242 | from binascii import hexlify 243 | 244 | version = b"\x01" 245 | 246 | for i, v in enumerate(seedweed.load_test_vectors(shortlist=True)): 247 | print(f"{i}) Enter user presence THREE times.") 248 | ext_key_cmd = 0x62 249 | solo.send_data_hid(ext_key_cmd, version + v["seed"] + b"") 250 | 251 | mc_req = FidoRequest(rp={"id": v["rp_id"], "name": "seedweed"}) 252 | mc_res = device.sendMC(*mc_req.toMC()) 253 | seedweed.conformance.verify_make_credential( 254 | v, 255 | mc_res.auth_data.credential_data.credential_id, 256 | mc_res.auth_data.credential_data.public_key[-2] 257 | + mc_res.auth_data.credential_data.public_key[-3], 258 | ) 259 | 260 | @pytest.mark.skipif(not IS_EXPERIMENTAL, reason="Experimental") 261 | def test_seedweed_vectors_get_assertion( 262 | self, 263 | solo, 264 | device, 265 | ): 266 | import seedweed 267 | from binascii import hexlify 268 | 269 | version = b"\x01" 270 | 271 | for i, v in enumerate(seedweed.load_test_vectors(shortlist=True)): 272 | print(f"{i}) Enter user presence THREE times.") 273 | ext_key_cmd = 0x62 274 | solo.send_data_hid(ext_key_cmd, version + v["seed"] + b"") 275 | 276 | allow_list = [{"id": v["credential_id"], "type": "public-key"}] 277 | ga_req = FidoRequest( 278 | rp={"id": v["rp_id"], "name": "seedweed"}, allow_list=allow_list 279 | ) 280 | ga_res = device.sendGA(*ga_req.toGA()) 281 | # print(v) 282 | # print(ga_res.auth_data + ga_req.cdh) 283 | 284 | # assert ga_res.auth_data.rp_id_hash == reg.auth_data.rp_id_hash 285 | assert ga_res.credential["id"] == v["credential_id"] 286 | # reg.auth_data.credential_data.credential_id 287 | 288 | seedweed.conformance.verify_get_assertion( 289 | v, 290 | convert_der_sig_to_padded_binary(ga_res.signature), 291 | # ga_res.signature, 292 | ga_res.auth_data + ga_req.cdh, 293 | ) 294 | -------------------------------------------------------------------------------- /tests/vendor/solo/utils.py: -------------------------------------------------------------------------------- 1 | class DeviceSelectCredential: 2 | def __init__(self, number): 3 | pass 4 | 5 | def __call__(self, status): 6 | pass 7 | -------------------------------------------------------------------------------- /tests/vendor/trezor/udp_backend.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import sys 3 | 4 | import fido2._pyu2f 5 | import fido2._pyu2f.base 6 | 7 | 8 | def force_udp_backend(): 9 | fido2._pyu2f.InternalPlatformSwitch = _UDP_InternalPlatformSwitch 10 | 11 | 12 | def _UDP_InternalPlatformSwitch(funcname, *args, **kwargs): 13 | if funcname == "__init__": 14 | return HidOverUDP(*args, **kwargs) 15 | return getattr(HidOverUDP, funcname)(*args, **kwargs) 16 | 17 | 18 | def format_pkg(d, p): 19 | # print(d, " ".join(["%02x" % i for i in p])) 20 | pass 21 | 22 | 23 | class HidOverUDP(fido2._pyu2f.base.HidDevice): 24 | @staticmethod 25 | def Enumerate(): 26 | a = [ 27 | { 28 | "vendor_id": 0x1209, 29 | "product_id": 0x53C1, 30 | "product_string": "TREZOR", 31 | "serial_number": "12345678", 32 | "usage": 0x01, 33 | "usage_page": 0xF1D0, 34 | "path": "127.0.0.1:21328", 35 | } 36 | ] 37 | return a 38 | 39 | def __init__(self, path): 40 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 41 | self.sock.bind(("127.0.0.1", 7112)) 42 | addr, port = path.split(":") 43 | port = int(port) 44 | self.token = (addr, port) 45 | self.sock.settimeout(1.0) 46 | 47 | def GetInReportDataLength(self): 48 | return 64 49 | 50 | def GetOutReportDataLength(self): 51 | return 64 52 | 53 | def Write(self, packet): 54 | format_pkg(">>>", packet) 55 | self.sock.sendto(bytearray(packet), self.token) 56 | 57 | def Read(self): 58 | msg = [0] * 64 59 | pkt, _ = self.sock.recvfrom(64) 60 | for i, v in enumerate(pkt): 61 | try: 62 | msg[i] = ord(v) 63 | except TypeError: 64 | msg[i] = v 65 | format_pkg("<<<", msg) 66 | return msg 67 | -------------------------------------------------------------------------------- /tests/vendor/trezor/utils.py: -------------------------------------------------------------------------------- 1 | import socket 2 | from fido2.ctap import STATUS 3 | 4 | from trezorlib import debuglink 5 | from trezorlib.debuglink import TrezorClientDebugLink 6 | from trezorlib.device import wipe as wipe_device 7 | from trezorlib.transport import enumerate_devices 8 | 9 | 10 | def load_client(): 11 | devices = enumerate_devices() 12 | for device in devices: 13 | try: 14 | client = TrezorClientDebugLink(device) 15 | break 16 | except Exception: 17 | pass 18 | else: 19 | raise RuntimeError("No debuggable device found") 20 | 21 | wipe_device(client) 22 | debuglink.load_device_by_mnemonic( 23 | client, 24 | mnemonic=" ".join(["all"] * 12), 25 | pin=None, 26 | passphrase_protection=False, 27 | label="test", 28 | language="english", 29 | ) 30 | client.clear_session() 31 | 32 | client.open() 33 | return client 34 | 35 | 36 | TREZOR_CLIENT = load_client() 37 | 38 | 39 | class DeviceSelectCredential: 40 | def __init__(self, number=1): 41 | self.number = number 42 | 43 | def __call__(self, status): 44 | if status != STATUS.UPNEEDED: 45 | return 46 | 47 | if self.number == 0: 48 | TREZOR_CLIENT.debug.press_no() 49 | else: 50 | for _ in range(self.number - 1): 51 | TREZOR_CLIENT.debug.swipe_left() 52 | TREZOR_CLIENT.debug.press_yes() 53 | --------------------------------------------------------------------------------