From 420d052ac9c930d80e8e6266283416f5315ee64a Mon Sep 17 00:00:00 2001 From: Szczepan Zalega Date: Tue, 8 Jan 2019 20:07:50 +0100 Subject: [PATCH 1/6] Describe running Solo on the Nucleo32 board Signed-off-by: Szczepan Zalega --- docs/solo/nucleo32-board.md | 204 ++++++++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 205 insertions(+) create mode 100644 docs/solo/nucleo32-board.md diff --git a/docs/solo/nucleo32-board.md b/docs/solo/nucleo32-board.md new file mode 100644 index 0000000..ab7d91d --- /dev/null +++ b/docs/solo/nucleo32-board.md @@ -0,0 +1,204 @@ +# Nucleo32 board preparation +Additional steps are required to run the firmware on the Nucleo32 board. + +## USB-A cable +Board does not provide an USB cable / socket for the target MCU communication. +Own provided USB plug has to be connected in the following way: + + +| PIN / Arduino PIN | MCU leg | USB wire color | Signal | +|---|---|---|---| +| D10 / PA11 | 21 | white | D-| +| D2 / PA12 | 22 | green | D+ | +| GND (near D2) | ------- | black | GND | +| **not connected** | ------- | red | 5V | + +Each USB plug pin should be connected via the wire in a color defined by the standard. It might be confirmed with a +multimeter for additional safety. USB plug description: + +| PIN | USB wire color | Signal | +|---|---|---| +| 4 | black | GND | +| 3 | green | D+ | +| 2 | white | D-| +| 1 | red | 5V | + +See this [USB plug] image, and Wikipedia's [USB plug description]. + +Plug in [USB-A_schematic.pdf] has wrong wire order, registered as [solo-hw#1]. + +[solo-hw#1]: https://github.com/solokeys/solo-hw/issues/1 +[USB plug]: https://upload.wikimedia.org/wikipedia/commons/thumb/6/67/USB.svg/1200px-USB.svg.png +[USB plug description]: https://en.wikipedia.org/wiki/USB#Receptacle_(socket)_identification + +The power is taken from the debugger / board (unless the board is configured in another way). +Make sure 5V is not connected, and is covered from contacting with the board elements. + +Based on [USB-A_schematic.pdf]. + +## Firmware modification +Following patch has to be applied to skip the user presence confirmation, for tests. Might be applied at a later stage. +```text +diff --git a/targets/stm32l432/src/app.h b/targets/stm32l432/src/app.h +index c14a7ed..c89c3b5 100644 +--- a/targets/stm32l432/src/app.h ++++ b/targets/stm32l432/src/app.h +@@ -71,6 +71,6 @@ void hw_init(void); + #define SOLO_BUTTON_PIN LL_GPIO_PIN_0 + + #define SKIP_BUTTON_CHECK_WITH_DELAY 0 +-#define SKIP_BUTTON_CHECK_FAST 0 ++#define SKIP_BUTTON_CHECK_FAST 1 + + #endif +``` +It is possible to provide a button and connect it to the MCU pins, as instructed in [USB-A_schematic.pdf]: +```text +PA0 / pin 6 --> button --> GND +``` +In that case the mentioned patch would not be required. + +[USB-A_schematic.pdf]: https://github.com/solokeys/solo-hw/releases/download/1.2/USB-A_schematic.pdf + + +# Development environment setup +Environment: Fedora 29 x64, Linux 4.19.9 + +See https://docs.solokeys.io/solo/building/ for the original guide. Here details not included there will be covered. + +## Install ARM tools +1. Download current [ARM tools] package: [gcc-arm-none-eabi-8-2018-q4-major-linux.tar.bz2]. +2. Extract the archive. +3. Add full path to the `./bin` directory as first entry to the `$PATH` variable, +as in `~/gcc-arm/gcc-arm-none-eabi-8-2018-q4-major/bin/:$PATH`. + +[ARM tools]: https://developer.arm.com/open-source/gnu-toolchain/gnu-rm/downloads +[gcc-arm-none-eabi-8-2018-q4-major-linux.tar.bz2]: https://developer.arm.com/-/media/Files/downloads/gnu-rm/8-2018q4/gcc-arm-none-eabi-8-2018-q4-major-linux.tar.bz2?revision=d830f9dd-cd4f-406d-8672-cca9210dd220?product=GNU%20Arm%20Embedded%20Toolchain,64-bit,,Linux,8-2018-q4-major +## Install flashing software +ST provides a CLI flashing tool - `STM32_Programmer_CLI`. It can be downloaded directly from the vendor's site: +1. Go to [download site URL](https://www.st.com/content/st_com/en/products/development-tools/software-development-tools/stm32-software-development-tools/stm32-programmers/stm32cubeprog.html), +go to bottom page and from STM32CubeProg row select Download button. +2. Unzip contents of the archive. +3. Run *Linux setup +4. In installation directory go to ./bin - there the ./STM32_Programmer_CLI is located +5. Add symlink to the STM32 CLI binary to .local/bin. Make sure the latter it is in $PATH. + +# Building and flashing +## Building +Please follow https://docs.solokeys.io/solo/building/, as the build way changes rapidly. +Currently (8.1.19) to build the firmware, following lines should be executed +```bash +# while in the main project directory +cd targets/stm32l432 +make cbor +make build-hacker DEBUG=1 +``` + +Note: `DEBUG=2` stops the device initialization, until a serial client will be attached to its virtual port. +Do not use it, if you do not plan to do so. + +## Flashing via the Makefile command +```bash +# while in the main project directory +# create Python virtual environment with required packages, and activate +make env3 +. env3/bin/activate +# Run flashing +cd ./targets/stm32l442 +make flash + # which runs: + # flash: solo.hex bootloader.hex + # python merge_hex.py solo.hex bootloader.hex all.hex (intelhex library required) + # STM32_Programmer_CLI -c port=SWD -halt -e all --readunprotect + # STM32_Programmer_CLI -c port=SWD -halt -d all.hex -rst +``` + + +# Testing + +## Internal +Project-provided tests. +### Simulated device +A simulated device is provided to test the HID layer. +#### Build +```bash +make clean +cd tinycbor +make +cd .. +make env2 +``` + +#### Execution +```bash +# run simulated device (will create a network UDP server) +./main +# run test 1 +./env2/bin/python tools/ctap_test.py +# run test 2 (or other files in the examples directory) +./env2/bin/python python-fido2/examples/credential.py +``` + +### Real device +```bash +# while in the main project directory +# not passing as of 8.1.19, due to test solution issues +make fido2-test +``` + +## External + +### FIDO2 test sites +1. https://webauthn.bin.coffee/ +2. https://github.com/apowers313/fido2-server-demo/ +3. https://webauthn.org/ + +### U2F test sites +1. https://u2f.bin.coffee/ +2. https://demo.yubico.com/u2f + +### FIDO2 standalone clients +1. https://github.com/Nitrokey/u2f-ref-code +2. https://github.com/Yubico/libfido2 +3. https://github.com/Yubico/python-fido2 +4. https://github.com/google/pyu2f + + +# USB serial console reading +Device opens an USB-emulated serial port to output its messages. While Nucleo board offers such already, +the Solo device provides its own. +- Provided Python tool +```bash +python3 ../../tools/solotool.py monitor /dev/solokey-serial +``` +- External application +```bash +sudo picocom -b 115200 /dev/solokey-serial +``` + +where `/dev/solokey-serial` is an udev symlink to `/dev/ttyACM1`. + +# Other + +## Dumping firmware + +Size is calculated using bash arithmetic. +```bash +STM32_Programmer_CLI -c port=SWD -halt -u 0x0 $((256*1024)) current.hex +``` + +## Software reset + +```bash +STM32_Programmer_CLI -c port=SWD -rst +``` + +## Installing required Python packages +Client script requires some Python packages, which could be easily installed locally to the project +via the Makefile command. It is sufficient to run: +```bash +make env3 +``` + + + diff --git a/mkdocs.yml b/mkdocs.yml index 2d69cbc..3e289d2 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -10,6 +10,7 @@ nav: - Home: solo/index.md - FIDO2 Implementation: solo/fido2-impl.md - Build instructions: solo/building.md + - Running on Nucleo32 board: solo/nucleo32-board.md - Signed update process: solo/signed-updates.md - Code documentation: solo/code-overview.md - Contributing Code: solo/contributing.md From be37ed46f7bcf080744467e6a526cad031cd2fef Mon Sep 17 00:00:00 2001 From: Szczepan Zalega Date: Tue, 8 Jan 2019 20:33:35 +0100 Subject: [PATCH 2/6] Add instruction for manual flashing of the Nucleo board Signed-off-by: Szczepan Zalega --- docs/solo/nucleo32-board.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/solo/nucleo32-board.md b/docs/solo/nucleo32-board.md index ab7d91d..cdbafb3 100644 --- a/docs/solo/nucleo32-board.md +++ b/docs/solo/nucleo32-board.md @@ -113,6 +113,12 @@ make flash # STM32_Programmer_CLI -c port=SWD -halt -d all.hex -rst ``` +## Manual flashing +In case you already have a firmware to flash (named `all.hex`), please run the following: +```bash +STM32_Programmer_CLI -c port=SWD -halt -e all --readunprotect +STM32_Programmer_CLI -c port=SWD -halt -d all.hex -rst +``` # Testing From 09d450ed02f213fac86b08532aa3a7d6889eb55a Mon Sep 17 00:00:00 2001 From: Wessel dR Date: Sun, 4 Aug 2019 23:44:37 +0200 Subject: [PATCH 3/6] Little typo Fixes https://github.com/Nitrokey/nitrokey-fido2-firmware/pull/19 --- docs/solo/nucleo32-board.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/solo/nucleo32-board.md b/docs/solo/nucleo32-board.md index cdbafb3..1e26ba5 100644 --- a/docs/solo/nucleo32-board.md +++ b/docs/solo/nucleo32-board.md @@ -104,7 +104,7 @@ Do not use it, if you do not plan to do so. make env3 . env3/bin/activate # Run flashing -cd ./targets/stm32l442 +cd ./targets/stm32l432 make flash # which runs: # flash: solo.hex bootloader.hex @@ -207,4 +207,3 @@ make env3 ``` - From db479850a6da83af2b28d921a2731387c246aa53 Mon Sep 17 00:00:00 2001 From: Wessel dR Date: Mon, 5 Aug 2019 10:05:04 +0200 Subject: [PATCH 4/6] OsX path for STM32_Programmer_CLI Added how to add the STM32_Programmer_CLI to your OsX path Fixes https://github.com/Nitrokey/nitrokey-fido2-firmware/pull/20 --- docs/solo/nucleo32-board.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/solo/nucleo32-board.md b/docs/solo/nucleo32-board.md index 1e26ba5..25d103e 100644 --- a/docs/solo/nucleo32-board.md +++ b/docs/solo/nucleo32-board.md @@ -83,6 +83,12 @@ go to bottom page and from STM32CubeProg row select Download button. 4. In installation directory go to ./bin - there the ./STM32_Programmer_CLI is located 5. Add symlink to the STM32 CLI binary to .local/bin. Make sure the latter it is in $PATH. +If you're on OsX and installed the STM32CubeProg, you need to add the following to your path: +```bash +# ~/.bash_profile +export PATH="/Applications/STMicroelectronics/STM32Cube/STM32CubeProgrammer/STM32CubeProgrammer.app/Contents/MacOs/bin/":$PATH +``` + # Building and flashing ## Building Please follow https://docs.solokeys.io/solo/building/, as the build way changes rapidly. @@ -206,4 +212,3 @@ via the Makefile command. It is sufficient to run: make env3 ``` - From e2738d11d36161dd9f34688abe70b7c55d9d885e Mon Sep 17 00:00:00 2001 From: Conor Patrick Date: Tue, 6 Aug 2019 18:50:05 +0800 Subject: [PATCH 5/6] remove tests --- tools/testing/main.py | 64 -- tools/testing/tests/__init__.py | 11 - tools/testing/tests/fido2.py | 1334 ------------------------------- tools/testing/tests/hid.py | 252 ------ tools/testing/tests/solo.py | 83 -- tools/testing/tests/tester.py | 230 ------ tools/testing/tests/u2f.py | 133 --- tools/testing/tests/util.py | 12 - 8 files changed, 2119 deletions(-) delete mode 100644 tools/testing/main.py delete mode 100644 tools/testing/tests/__init__.py delete mode 100644 tools/testing/tests/fido2.py delete mode 100644 tools/testing/tests/hid.py delete mode 100644 tools/testing/tests/solo.py delete mode 100644 tools/testing/tests/tester.py delete mode 100644 tools/testing/tests/u2f.py delete mode 100644 tools/testing/tests/util.py diff --git a/tools/testing/main.py b/tools/testing/main.py deleted file mode 100644 index 437e938..0000000 --- a/tools/testing/main.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# -# Copyright 2019 SoloKeys Developers -# -# Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be -# copied, modified, or distributed except according to those terms. -# - -# Script for testing correctness of CTAP2/CTAP1 security token - -import sys - -from solo.fido2 import force_udp_backend -from tests import Tester, FIDO2Tests, U2FTests, HIDTests, SoloTests - - -if __name__ == "__main__": - if len(sys.argv) < 2: - print("Usage: %s [sim] [nfc] <[u2f]|[fido2]|[rk]|[hid]|[ping]>") - print(" sim - test via UDP simulation backend only") - print(" nfc - test via NFC interface only") - sys.exit(0) - - t = Tester() - t.set_user_count(3) - - if "sim" in sys.argv: - print("Using UDP backend.") - force_udp_backend() - t.set_sim(True) - t.set_user_count(10) - - nfcOnly = False - if "nfc" in sys.argv: - nfcOnly = True - - t.find_device(nfcOnly) - - if "solo" in sys.argv: - SoloTests(t).run() - - if "u2f" in sys.argv: - U2FTests(t).run() - - if "fido2" in sys.argv: - # t.test_fido2() - FIDO2Tests(t).run() - - # hid tests are a bit invasive and should be done last - if "hid" in sys.argv: - HIDTests(t).run() - - if "bootloader" in sys.argv: - if t.is_sim: - raise RuntimeError("Cannot test bootloader in simulation yet.") - # print("Put device in bootloader mode and then hit enter") - # input() - # t.test_bootloader() - - # t.test_responses() - # t.test_fido2_brute_force() diff --git a/tools/testing/tests/__init__.py b/tools/testing/tests/__init__.py deleted file mode 100644 index 23cf5a8..0000000 --- a/tools/testing/tests/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from . import fido2 -from . import hid -from . import solo -from . import u2f -from . import tester - -FIDO2Tests = fido2.FIDO2Tests -HIDTests = hid.HIDTests -U2FTests = u2f.U2FTests -SoloTests = solo.SoloTests -Tester = tester.Tester diff --git a/tools/testing/tests/fido2.py b/tools/testing/tests/fido2.py deleted file mode 100644 index dc76df3..0000000 --- a/tools/testing/tests/fido2.py +++ /dev/null @@ -1,1334 +0,0 @@ -from __future__ import print_function, absolute_import, unicode_literals -import time -from random import randint -import array -from functools import cmp_to_key - -from fido2 import cbor -from fido2.ctap import CtapError - -from fido2.ctap2 import ES256, PinProtocolV1, AttestedCredentialData -from fido2.utils import sha256, hmac_sha256 -from fido2.attestation import Attestation - -from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes - -from .u2f import U2FTests -from .tester import Tester, Test -from .util import shannon_entropy - -rp = {"id": "examplo.org", "name": "ExaRP"} -rp2 = {"id": "solokeys.com", "name": "ExaRP"} -user = {"id": b"usee_od", "name": "AB User"} -user1 = {"id": b"1234567890", "name": "Conor Patrick"} -user2 = {"id": b"oiewhfoi", "name": "Han Solo"} -user3 = {"id": b"23ohfpjwo@@", "name": "John Smith"} -challenge = "Y2hhbGxlbmdl" -pin_protocol = 1 -key_params = [{"type": "public-key", "alg": ES256.ALGORITHM}] -cdh = b"123456789abcdef0123456789abcdef0" - - -def VerifyAttestation(attest, data): - verifier = Attestation.for_type(attest.fmt) - verifier().verify(attest.att_statement, attest.auth_data, data.hash) - - -def cbor_key_to_representative(key): - if isinstance(key, int): - if key >= 0: - return (0, key) - return (1, -key) - elif isinstance(key, bytes): - return (2, key) - elif isinstance(key, str): - return (3, key) - else: - raise ValueError(key) - - -def cbor_str_cmp(a, b): - if isinstance(a, str) or isinstance(b, str): - a = a.encode("utf8") - b = b.encode("utf8") - - if len(a) == len(b): - for x, y in zip(a, b): - if x != y: - return x - y - return 0 - else: - return len(a) - len(b) - - -def cmp_cbor_keys(a, b): - a = cbor_key_to_representative(a) - b = cbor_key_to_representative(b) - if a[0] != b[0]: - return a[0] - b[0] - if a[0] in (2, 3): - return cbor_str_cmp(a[1], b[1]) - else: - return (a[1] > b[1]) - (a[1] < b[1]) - - -def TestCborKeysSorted(cbor_obj): - # Cbor canonical ordering of keys. - # https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#ctap2-canonical-cbor-encoding-form - - if isinstance(cbor_obj, bytes): - cbor_obj = cbor.decode_from(cbor_obj)[0] - - if isinstance(cbor_obj, dict): - l = [x for x in cbor_obj] - else: - l = cbor_obj - - l_sorted = sorted(l[:], key=cmp_to_key(cmp_cbor_keys)) - - for i in range(len(l)): - - if not isinstance(l[i], (str, int)): - raise ValueError(f"Cbor map key {l[i]} must be int or str for CTAP2") - - if l[i] != l_sorted[i]: - raise ValueError(f"Cbor map item {i}: {l[i]} is out of order") - - return l - - -# hot patch cbor map parsing to test the order of keys in map -_load_map_old = cbor.load_map - - -def _load_map_new(ai, data): - values, data = _load_map_old(ai, data) - TestCborKeysSorted(values) - return values, data - - -cbor.load_map = _load_map_new -cbor._DESERIALIZERS[5] = _load_map_new - - -class FIDO2Tests(Tester): - def __init__(self, tester=None): - super().__init__(tester) - self.self_test() - - def self_test(self,): - cbor_key_list_sorted = [ - 0, - 1, - 1, - 2, - 3, - -1, - -2, - "b", - "c", - "aa", - "aaa", - "aab", - "baa", - "bbb", - ] - with Test("Self test CBOR sorting"): - TestCborKeysSorted(cbor_key_list_sorted) - - with Test("Self test CBOR sorting integers", catch=ValueError): - TestCborKeysSorted([1, 0]) - - with Test("Self test CBOR sorting major type", catch=ValueError): - TestCborKeysSorted([-1, 0]) - - with Test("Self test CBOR sorting strings", catch=ValueError): - TestCborKeysSorted(["bb", "a"]) - - with Test("Self test CBOR sorting same length strings", catch=ValueError): - TestCborKeysSorted(["ab", "aa"]) - - def run(self,): - self.test_fido2() - - def test_fido2_simple(self, pin_token=None): - creds = [] - exclude_list = [] - PIN = pin_token - - fake_id1 = array.array("B", [randint(0, 255) for i in range(0, 150)]).tobytes() - fake_id2 = array.array("B", [randint(0, 255) for i in range(0, 73)]).tobytes() - - exclude_list.append({"id": fake_id1, "type": "public-key"}) - exclude_list.append({"id": fake_id2, "type": "public-key"}) - - t1 = time.time() * 1000 - attest, data = self.client.make_credential( - rp, user, challenge, pin=PIN, exclude_list=[] - ) - t2 = time.time() * 1000 - VerifyAttestation(attest, data) - print("Register time: %d ms" % (t2 - t1)) - - cred = attest.auth_data.credential_data - creds.append(cred) - - allow_list = [{"id": creds[0].credential_id, "type": "public-key"}] - t1 = time.time() * 1000 - assertions, client_data = self.client.get_assertion( - rp["id"], challenge, allow_list, pin=PIN - ) - t2 = time.time() * 1000 - assertions[0].verify(client_data.hash, creds[0].public_key) - - print("Assertion time: %d ms" % (t2 - t1)) - - def test_extensions(self,): - - salt1 = b"\x5a" * 32 - salt2 = b"\x96" * 32 - salt3 = b"\x03" * 32 - - # self.testReset() - - with Test("Get info has hmac-secret"): - info = self.ctap.get_info() - assert "hmac-secret" in info.extensions - - reg = self.testMC( - "Send MC with hmac-secret ext set to true, expect SUCCESS", - cdh, - rp, - user, - key_params, - expectedError=CtapError.ERR.SUCCESS, - other={"extensions": {"hmac-secret": True}, "options": {"rk": True}}, - ) - - with Test("Check 'hmac-secret' is set to true in auth_data extensions"): - assert reg.auth_data.extensions - assert "hmac-secret" in reg.auth_data.extensions - assert reg.auth_data.extensions["hmac-secret"] == True - - self.testMC( - "Send MC with fake extension set to true, expect SUCCESS", - cdh, - rp, - user, - key_params, - expectedError=CtapError.ERR.SUCCESS, - other={"extensions": {"tetris": True}}, - ) - - with Test("Get shared secret"): - key_agreement, shared_secret = self.client.pin_protocol.get_shared_secret() - cipher = Cipher( - algorithms.AES(shared_secret), - modes.CBC(b"\x00" * 16), - default_backend(), - ) - - def get_salt_params(salts): - enc = cipher.encryptor() - salt_enc = b"" - for salt in salts: - salt_enc += enc.update(salt) - salt_enc += enc.finalize() - - salt_auth = hmac_sha256(shared_secret, salt_enc)[:16] - return salt_enc, salt_auth - - for salt_list in ((salt1,), (salt1, salt2)): - salt_enc, salt_auth = get_salt_params(salt_list) - - auth = self.testGA( - "Send GA request with %d salts hmac-secret, expect success" - % len(salt_list), - rp["id"], - cdh, - other={ - "extensions": { - "hmac-secret": {1: key_agreement, 2: salt_enc, 3: salt_auth} - } - }, - expectedError=CtapError.ERR.SUCCESS, - ) - - with Test( - "Check that hmac-secret is in auth_data extensions and has %d bytes" - % (len(salt_list) * 32) - ): - ext = auth.auth_data.extensions - assert ext - assert "hmac-secret" in ext - assert isinstance(ext["hmac-secret"], bytes) - assert len(ext["hmac-secret"]) == len(salt_list) * 32 - - with Test("Check that shannon_entropy of hmac-secret is good"): - ext = auth.auth_data.extensions - dec = cipher.decryptor() - key = dec.update(ext["hmac-secret"]) + dec.finalize() - - print(shannon_entropy(ext["hmac-secret"])) - if len(salt_list) == 1: - assert shannon_entropy(ext["hmac-secret"]) > 4.6 - assert shannon_entropy(key) > 4.6 - if len(salt_list) == 2: - assert shannon_entropy(ext["hmac-secret"]) > 5.4 - assert shannon_entropy(key) > 5.4 - - with Test("Check that the assertion is valid"): - credential_data = AttestedCredentialData(reg.auth_data.credential_data) - auth.verify(cdh, credential_data.public_key) - - salt_enc, salt_auth = get_salt_params((salt3,)) - - auth = self.testGA( - "Send GA request with hmac-secret missing keyAgreement, expect error", - rp["id"], - cdh, - other={"extensions": {"hmac-secret": {2: salt_enc, 3: salt_auth}}}, - ) - auth = self.testGA( - "Send GA request with hmac-secret missing saltAuth, expect MISSING_PARAMETER", - rp["id"], - cdh, - other={"extensions": {"hmac-secret": {1: key_agreement, 2: salt_enc}}}, - expectedError=CtapError.ERR.MISSING_PARAMETER, - ) - auth = self.testGA( - "Send GA request with hmac-secret missing saltEnc, expect MISSING_PARAMETER", - rp["id"], - cdh, - other={"extensions": {"hmac-secret": {1: key_agreement, 3: salt_auth}}}, - expectedError=CtapError.ERR.MISSING_PARAMETER, - ) - - bad_auth = list(salt_auth[:]) - bad_auth[len(bad_auth) // 2] = bad_auth[len(bad_auth) // 2] ^ 1 - bad_auth = bytes(bad_auth) - - auth = self.testGA( - "Send GA request with hmac-secret containing bad saltAuth, expect EXTENSION_FIRST", - rp["id"], - cdh, - other={ - "extensions": { - "hmac-secret": {1: key_agreement, 2: salt_enc, 3: bad_auth} - } - }, - expectedError=CtapError.ERR.EXTENSION_FIRST, - ) - - salt4 = b"\x5a" * 16 - salt5 = b"\x96" * 64 - for salt_list in ((salt4,), (salt4, salt5)): - salt_enc, salt_auth = get_salt_params(salt_list) - - salt_auth = hmac_sha256(shared_secret, salt_enc)[:16] - auth = self.testGA( - "Send GA request with incorrect salt length %d, expect INVALID_LENGTH" - % len(salt_enc), - rp["id"], - cdh, - other={ - "extensions": { - "hmac-secret": {1: key_agreement, 2: salt_enc, 3: salt_auth} - } - }, - expectedError=CtapError.ERR.INVALID_LENGTH, - ) - - def test_get_info(self,): - with Test("Get info"): - info = self.ctap.get_info() - print("data:", bytes(info)) - print("decoded:", cbor.decode_from(bytes(info))) - - with Test("Check FIDO2 string is in VERSIONS field"): - assert "FIDO_2_0" in info.versions - - with Test("Check pin protocols field"): - if len(info.pin_protocols): - assert sum(info.pin_protocols) > 0 - - with Test("Check options field"): - for x in info.options: - assert info.options[x] in [True, False] - - if "uv" in info.options: - if info.options["uv"]: - self.testMC( - "Send MC request with uv set to true, expect SUCCESS", - cdh, - rp, - user, - key_params, - other={"options": {"uv": True}}, - expectedError=CtapError.ERR.SUCCESS, - ) - if "up" in info.options: - if info.options["up"]: - self.testMC( - "Send MC request with up set to true, expect INVALID_OPTION", - cdh, - rp, - user, - key_params, - other={"options": {"up": True}}, - expectedError=CtapError.ERR.INVALID_OPTION, - ) - - def test_make_credential(self,): - - prev_reg = self.testMC( - "Send MC request, expect success", - cdh, - rp, - user, - key_params, - expectedError=CtapError.ERR.SUCCESS, - ) - - allow_list = [ - { - "id": prev_reg.auth_data.credential_data.credential_id, - "type": "public-key", - } - ] - with Test("Check attestation format is correct"): - assert prev_reg.fmt in ["packed", "tpm", "android-key", "adroid-safetynet"] - - with Test("Check auth_data is at least 77 bytes"): - assert len(prev_reg.auth_data) >= 77 - - self.testMC( - "Send MC request with missing clientDataHash, expect error", - None, - rp, - user, - key_params, - expectedError=CtapError.ERR.MISSING_PARAMETER, - ) - - self.testMC( - "Send MC request with integer for clientDataHash, expect error", - 5, - rp, - user, - key_params, - ) - - self.testMC( - "Send MC request with missing user, expect error", - cdh, - rp, - None, - key_params, - expectedError=CtapError.ERR.MISSING_PARAMETER, - ) - - self.testMC( - "Send MC request with bytearray user, expect error", - cdh, - rp, - b"1234abcd", - key_params, - ) - - self.testMC( - "Send MC request with missing RP, expect error", - cdh, - None, - user, - key_params, - expectedError=CtapError.ERR.MISSING_PARAMETER, - ) - - self.testMC( - "Send MC request with bytearray RP, expect error", - cdh, - b"1234abcd", - user, - key_params, - ) - - self.testMC( - "Send MC request with missing pubKeyCredParams, expect error", - cdh, - rp, - user, - None, - ) - - self.testMC( - "Send MC request with incorrect pubKeyCredParams, expect error", - cdh, - rp, - user, - b"2356", - ) - - self.testMC( - "Send MC request with incorrect excludeList, expect error", - cdh, - rp, - user, - key_params, - other={"exclude_list": 8}, - ) - - self.testMC( - "Send MC request with incorrect extensions, expect error", - cdh, - rp, - user, - key_params, - other={"extensions": 8}, - ) - - self.testMC( - "Send MC request with incorrect options, expect error", - cdh, - rp, - user, - key_params, - other={"options": 8}, - ) - - self.testMC( - "Send MC request with bad RP.name", - cdh, - {"id": self.host, "name": 8, "icon": "icon"}, - user, - key_params, - ) - - self.testMC( - "Send MC request with bad RP.id", - cdh, - {"id": 8, "name": "name", "icon": "icon"}, - user, - key_params, - ) - - self.testMC( - "Send MC request with bad RP.icon", - cdh, - {"id": self.host, "name": "name", "icon": 8}, - user, - key_params, - ) - - self.testMC( - "Send MC request with bad user.name", - cdh, - rp, - {"id": b"usee_od", "name": 8}, - key_params, - ) - - self.testMC( - "Send MC request with bad user.id", - cdh, - rp, - {"id": "usee_od", "name": "name"}, - key_params, - ) - - self.testMC( - "Send MC request with bad user.displayName", - cdh, - rp, - {"id": "usee_od", "name": "name", "displayName": 8}, - key_params, - ) - - self.testMC( - "Send MC request with bad user.icon", - cdh, - rp, - {"id": "usee_od", "name": "name", "icon": 8}, - key_params, - ) - - self.testMC( - "Send MC request with non-map pubKeyCredParams item", - cdh, - rp, - user, - ["wrong"], - ) - - self.testMC( - "Send MC request with pubKeyCredParams item missing type field", - cdh, - rp, - user, - [{"alg": ES256.ALGORITHM}], - expectedError=CtapError.ERR.MISSING_PARAMETER, - ) - - self.testMC( - "Send MC request with pubKeyCredParams item with bad type field", - cdh, - rp, - user, - [{"alg": ES256.ALGORITHM, "type": b"public-key"}], - ) - - self.testMC( - "Send MC request with pubKeyCredParams item missing alg", - cdh, - rp, - user, - [{"type": "public-key"}], - expectedError=CtapError.ERR.MISSING_PARAMETER, - ) - - self.testMC( - "Send MC request with pubKeyCredParams item with bad alg", - cdh, - rp, - user, - [{"alg": "7", "type": "public-key"}], - ) - - self.testMC( - "Send MC request with pubKeyCredParams item with bogus alg, expect UNSUPPORTED_ALGORITHM", - cdh, - rp, - user, - [{"alg": 1234, "type": "public-key"}], - expectedError=CtapError.ERR.UNSUPPORTED_ALGORITHM, - ) - - self.testMC( - "Send MC request with pubKeyCredParams item with bogus type, expect UNSUPPORTED_ALGORITHM", - cdh, - rp, - user, - [{"alg": ES256.ALGORITHM, "type": "rot13"}], - expectedError=CtapError.ERR.UNSUPPORTED_ALGORITHM, - ) - - self.testMC( - "Send MC request with excludeList item with bogus type, expect SUCCESS", - cdh, - rp, - user, - key_params, - expectedError=CtapError.ERR.SUCCESS, - other={"exclude_list": [{"id": b"1234", "type": "rot13"}]}, - ) - - self.testMC( - "Send MC request with excludeList item with bogus type, expect SUCCESS", - cdh, - rp, - user, - key_params, - expectedError=CtapError.ERR.SUCCESS, - other={ - "exclude_list": [ - {"id": b"1234", "type": "mangoPapayaCoconutNotAPublicKey"} - ] - }, - ) - - self.testMC( - "Send MC request with excludeList with bad item, expect error", - cdh, - rp, - user, - key_params, - other={"exclude_list": ["1234"]}, - ) - - self.testMC( - "Send MC request with excludeList with item missing type field, expect error", - cdh, - rp, - user, - key_params, - other={"exclude_list": [{"id": b"1234"}]}, - ) - - self.testMC( - "Send MC request with excludeList with item missing id field, expect error", - cdh, - rp, - user, - key_params, - other={"exclude_list": [{"type": "public-key"}]}, - ) - - self.testMC( - "Send MC request with excludeList with item containing bad id field, expect error", - cdh, - rp, - user, - key_params, - other={"exclude_list": [{"type": "public-key", "id": "1234"}]}, - ) - - self.testMC( - "Send MC request with excludeList with item containing bad type field, expect error", - cdh, - rp, - user, - key_params, - other={"exclude_list": [{"type": b"public-key", "id": b"1234"}]}, - ) - - self.testMC( - "Send MC request with excludeList containing previous registration, expect CREDENTIAL_EXCLUDED", - cdh, - rp, - user, - key_params, - other={ - "exclude_list": [ - { - "type": "public-key", - "id": prev_reg.auth_data.credential_data.credential_id, - } - ] - }, - expectedError=CtapError.ERR.CREDENTIAL_EXCLUDED, - ) - - self.testMC( - "Send MC request with unknown option, expect SUCCESS", - cdh, - rp, - user, - key_params, - other={"options": {"unknown": False}}, - expectedError=CtapError.ERR.SUCCESS, - ) - - self.testReset() - - self.testGA( - "Send GA request with reset auth, expect NO_CREDENTIALS", - rp["id"], - cdh, - allow_list, - expectedError=CtapError.ERR.NO_CREDENTIALS, - ) - - def test_get_assertion(self,): - - self.testReset() - - prev_reg = self.testMC( - "Send MC request, expect success", - cdh, - rp, - user, - key_params, - expectedError=CtapError.ERR.SUCCESS, - ) - - allow_list = [ - { - "id": prev_reg.auth_data.credential_data.credential_id, - "type": "public-key", - } - ] - - prev_auth = self.testGA( - "Send GA request, expect success", - rp["id"], - cdh, - allow_list, - expectedError=CtapError.ERR.SUCCESS, - ) - - with Test("Check assertion is correct"): - credential_data = AttestedCredentialData(prev_reg.auth_data.credential_data) - prev_auth.verify(cdh, credential_data.public_key) - assert ( - prev_auth.credential["id"] - == prev_reg.auth_data.credential_data.credential_id - ) - - self.reboot() - - prev_auth = self.testGA( - "Send GA request after reboot, expect success", - rp["id"], - cdh, - allow_list, - expectedError=CtapError.ERR.SUCCESS, - ) - - with Test("Check assertion is correct"): - credential_data = AttestedCredentialData(prev_reg.auth_data.credential_data) - prev_auth.verify(cdh, credential_data.public_key) - assert ( - prev_auth.credential["id"] - == prev_reg.auth_data.credential_data.credential_id - ) - - prev_auth = self.testGA( - "Send GA request, expect success", - rp["id"], - cdh, - allow_list, - expectedError=CtapError.ERR.SUCCESS, - ) - - with Test("Test auth_data is 37 bytes"): - assert len(prev_auth.auth_data) == 37 - - with Test("Test that auth_data.rpIdHash is correct"): - assert sha256(rp["id"].encode()) == prev_auth.auth_data.rp_id_hash - - with Test("Check that AT flag is not set"): - assert (prev_auth.auth_data.flags & 0xF8) == 0 - - with Test("Test that user, credential and numberOfCredentials are not present"): - assert prev_auth.user == None - assert prev_auth.number_of_credentials == None - - self.testGA( - "Send GA request with empty allow_list, expect NO_CREDENTIALS", - rp["id"], - cdh, - [], - expectedError=CtapError.ERR.NO_CREDENTIALS, - ) - - # apply bit flip - badid = list(prev_reg.auth_data.credential_data.credential_id[:]) - badid[len(badid) // 2] = badid[len(badid) // 2] ^ 1 - badid = bytes(badid) - - self.testGA( - "Send GA request with corrupt credId in allow_list, expect NO_CREDENTIALS", - rp["id"], - cdh, - [{"id": badid, "type": "public-key"}], - expectedError=CtapError.ERR.NO_CREDENTIALS, - ) - - self.testGA( - "Send GA request with missing RPID, expect MISSING_PARAMETER", - None, - cdh, - allow_list, - expectedError=CtapError.ERR.MISSING_PARAMETER, - ) - - self.testGA( - "Send GA request with bad RPID, expect error", - {"type": "wrong"}, - cdh, - allow_list, - ) - - self.testGA( - "Send GA request with missing clientDataHash, expect MISSING_PARAMETER", - rp["id"], - None, - allow_list, - expectedError=CtapError.ERR.MISSING_PARAMETER, - ) - - self.testGA( - "Send GA request with bad clientDataHash, expect error", - rp["id"], - {"type": "wrong"}, - allow_list, - ) - - self.testGA( - "Send GA request with bad allow_list, expect error", - rp["id"], - cdh, - {"type": "wrong"}, - ) - - self.testGA( - "Send GA request with bad item in allow_list, expect error", - rp["id"], - cdh, - allow_list + ["wrong"], - ) - - self.testGA( - "Send GA request with unknown option, expect SUCCESS", - rp["id"], - cdh, - allow_list, - other={"options": {"unknown": True}}, - expectedError=CtapError.ERR.SUCCESS, - ) - with Test("Get info"): - info = self.ctap.get_info() - - if "uv" in info.options: - if info.options["uv"]: - res = self.testGA( - "Send GA request with uv set to true, expect SUCCESS", - rp["id"], - cdh, - allow_list, - other={"options": {"uv": True}}, - expectedError=CtapError.ERR.SUCCESS, - ) - with Test("Check that UV flag is set in response"): - assert res.auth_data.flags & (1 << 2) - if "up" in info.options: - if info.options["up"]: - res = self.testGA( - "Send GA request with up set to true, expect SUCCESS", - rp["id"], - cdh, - allow_list, - other={"options": {"up": True}}, - expectedError=CtapError.ERR.SUCCESS, - ) - with Test("Check that UP flag is set in response"): - assert res.auth_data.flags & 1 - - self.testGA( - "Send GA request with bogus type item in allow_list, expect SUCCESS", - rp["id"], - cdh, - allow_list + [{"type": "rot13", "id": b"1234"}], - expectedError=CtapError.ERR.SUCCESS, - ) - - self.testGA( - "Send GA request with item missing type field in allow_list, expect error", - rp["id"], - cdh, - allow_list + [{"id": b"1234"}], - ) - - self.testGA( - "Send GA request with item containing bad type field in allow_list, expect error", - rp["id"], - cdh, - allow_list + [{"type": b"public-key", "id": b"1234"}], - ) - - self.testGA( - "Send GA request with item containing bad id in allow_list, expect error", - rp["id"], - cdh, - allow_list + [{"type": b"public-key", "id": 42}], - ) - - self.testGA( - "Send GA request with item missing id in allow_list, expect error", - rp["id"], - cdh, - allow_list + [{"type": b"public-key"}], - ) - - self.testReset() - - appid = sha256(rp["id"].encode("utf8")) - chal = sha256(challenge.encode("utf8")) - with Test("Send CTAP1 register request"): - u2f = U2FTests(self) - reg = u2f.register(chal, appid) - reg.verify(appid, chal) - - with Test("Authenticate CTAP1"): - auth = u2f.authenticate(chal, appid, reg.key_handle) - auth.verify(appid, chal, reg.public_key) - - auth = self.testGA( - "Authenticate CTAP1 registration with CTAP2", - rp["id"], - cdh, - [{"id": reg.key_handle, "type": "public-key"}], - expectedError=CtapError.ERR.SUCCESS, - ) - - with Test("Check assertion is correct"): - credential_data = AttestedCredentialData.from_ctap1( - reg.key_handle, reg.public_key - ) - auth.verify(cdh, credential_data.public_key) - assert auth.credential["id"] == reg.key_handle - - def test_rk(self, pin_code=None): - - pin_auth = None - if pin_code: - pin_protocol = 1 - else: - pin_protocol = None - if pin_code: - with Test("Set pin code"): - self.client.pin_protocol.set_pin(pin_code) - pin_token = self.client.pin_protocol.get_pin_token(pin_code) - pin_auth = hmac_sha256(pin_token, cdh)[:16] - - self.testMC( - "Send MC request with rk option set to true, expect SUCCESS", - cdh, - rp, - user, - key_params, - other={ - "options": {"rk": True}, - "pin_auth": pin_auth, - "pin_protocol": pin_protocol, - }, - expectedError=CtapError.ERR.SUCCESS, - ) - - with Test("Get info"): - info = self.ctap.get_info() - - options = {"rk": True} - if "uv" in info.options and info.options["uv"]: - options["uv"] = False - - for i, x in enumerate([user1, user2, user3]): - self.testMC( - "Send MC request with rk option set to true, expect SUCCESS %d/3" - % (i + 1), - cdh, - rp2, - x, - key_params, - other={ - "options": options, - "pin_auth": pin_auth, - "pin_protocol": pin_protocol, - }, - expectedError=CtapError.ERR.SUCCESS, - ) - - auth1 = self.testGA( - "Send GA request with no allow_list, expect SUCCESS", - rp2["id"], - cdh, - other={"pin_auth": pin_auth, "pin_protocol": pin_protocol}, - expectedError=CtapError.ERR.SUCCESS, - ) - - with Test("Check that there are 3 credentials returned"): - assert auth1.number_of_credentials == 3 - - with Test("Get the next 2 assertions"): - auth2 = self.ctap.get_next_assertion() - auth3 = self.ctap.get_next_assertion() - - if not pin_code: - with Test("Check only the user ID was returned"): - assert "id" in auth1.user.keys() and len(auth1.user.keys()) == 1 - assert "id" in auth2.user.keys() and len(auth2.user.keys()) == 1 - assert "id" in auth3.user.keys() and len(auth3.user.keys()) == 1 - else: - with Test("Check that all user info was returned"): - for x in (auth1, auth2, auth3): - for y in ("name", "icon", "displayName", "id"): - if y not in x.user.keys(): - print("FAIL: %s was not in user: " % y, x.user) - - with Test("Send an extra getNextAssertion request, expect error"): - try: - self.ctap.get_next_assertion() - assert 0 - except CtapError as e: - print(e) - - def test_client_pin(self,): - pin1 = "1234567890" - self.test_rk(pin1) - - # PinProtocolV1 - res = self.testCP( - "Test getKeyAgreement, expect SUCCESS", - pin_protocol, - PinProtocolV1.CMD.GET_KEY_AGREEMENT, - expectedError=CtapError.ERR.SUCCESS, - ) - - with Test("Test getKeyAgreement has appropriate fields"): - key = res[1] - assert "Is public key" and key[1] == 2 - assert "Is P256" and key[-1] == 1 - assert "Is ALG_ECDH_ES_HKDF_256" and key[3] == -25 - - assert "Right key" and len(key[-3]) == 32 and isinstance(key[-3], bytes) - - with Test("Test setting a new pin"): - pin2 = "qwertyuiop\x11\x22\x33\x00123" - self.client.pin_protocol.change_pin(pin1, pin2) - - with Test("Test getting new pin_auth"): - pin_token = self.client.pin_protocol.get_pin_token(pin2) - pin_auth = hmac_sha256(pin_token, cdh)[:16] - - res_mc = self.testMC( - "Send MC request with new pin auth", - cdh, - rp, - user, - key_params, - other={"pin_auth": pin_auth, "pin_protocol": pin_protocol}, - expectedError=CtapError.ERR.SUCCESS, - ) - - with Test("Check UV flag is set"): - assert res_mc.auth_data.flags & (1 << 2) - - res_ga = self.testGA( - "Send GA request with pinAuth, expect SUCCESS", - rp["id"], - cdh, - [ - { - "type": "public-key", - "id": res_mc.auth_data.credential_data.credential_id, - } - ], - other={"pin_auth": pin_auth, "pin_protocol": pin_protocol}, - expectedError=CtapError.ERR.SUCCESS, - ) - - with Test("Check UV flag is set"): - assert res_ga.auth_data.flags & (1 << 2) - - res_ga = self.testGA( - "Send GA request with no pinAuth, expect SUCCESS", - rp["id"], - cdh, - [ - { - "type": "public-key", - "id": res_mc.auth_data.credential_data.credential_id, - } - ], - expectedError=CtapError.ERR.SUCCESS, - ) - - with Test("Check UV flag is NOT set"): - assert not (res_ga.auth_data.flags & (1 << 2)) - - self.testReset() - - with Test("Test sending zero-length pin_auth, expect PIN_NOT_SET"): - self.testMC( - "Send MC request with new pin auth", - cdh, - rp, - user, - key_params, - other={"pin_auth": b"", "pin_protocol": pin_protocol}, - expectedError=CtapError.ERR.PIN_NOT_SET, - ) - self.testGA( - "Send MC request with new pin auth", - rp["id"], - cdh, - other={"pin_auth": b"", "pin_protocol": pin_protocol}, - expectedError=[ - CtapError.ERR.PIN_AUTH_INVALID, - CtapError.ERR.NO_CREDENTIALS, - ], - ) - - with Test("Setting pin code, expect SUCCESS"): - self.client.pin_protocol.set_pin(pin1) - - with Test("Test sending zero-length pin_auth, expect PIN_INVALID"): - self.testMC( - "Send MC request with new pin auth", - cdh, - rp, - user, - key_params, - other={"pin_auth": b"", "pin_protocol": pin_protocol}, - expectedError=CtapError.ERR.PIN_AUTH_INVALID, - ) - self.testGA( - "Send MC request with new pin auth", - rp["id"], - cdh, - other={"pin_auth": b"", "pin_protocol": pin_protocol}, - expectedError=[ - CtapError.ERR.PIN_AUTH_INVALID, - CtapError.ERR.NO_CREDENTIALS, - ], - ) - - self.testReset() - with Test("Setting pin code >63 bytes, expect POLICY_VIOLATION "): - try: - self.client.pin_protocol.set_pin("A" * 64) - assert 0 - except CtapError as e: - assert e.code == CtapError.ERR.PIN_POLICY_VIOLATION - - with Test("Get pin token when no pin is set, expect PIN_NOT_SET"): - try: - self.client.pin_protocol.get_pin_token(pin1) - assert 0 - except CtapError as e: - assert e.code == CtapError.ERR.PIN_NOT_SET - - with Test("Get change pin when no pin is set, expect PIN_NOT_SET"): - try: - self.client.pin_protocol.change_pin(pin1, "1234") - assert 0 - except CtapError as e: - assert e.code == CtapError.ERR.PIN_NOT_SET - - with Test("Setting pin code and get pin_token, expect SUCCESS"): - self.client.pin_protocol.set_pin(pin1) - pin_token = self.client.pin_protocol.get_pin_token(pin1) - pin_auth = hmac_sha256(pin_token, cdh)[:16] - - with Test("Get info and assert that clientPin is set to true"): - info = self.ctap.get_info() - assert info.options["clientPin"] - - with Test("Test setting pin again fails"): - try: - self.client.pin_protocol.set_pin(pin1) - assert 0 - except CtapError as e: - print(e) - - res_mc = self.testMC( - "Send MC request with no pin_auth, expect PIN_REQUIRED", - cdh, - rp, - user, - key_params, - expectedError=CtapError.ERR.PIN_REQUIRED, - ) - - res_mc = self.testGA( - "Send GA request with no pin_auth, expect NO_CREDENTIALS", - rp["id"], - cdh, - expectedError=CtapError.ERR.NO_CREDENTIALS, - ) - - res = self.testCP( - "Test getRetries, expect SUCCESS", - pin_protocol, - PinProtocolV1.CMD.GET_RETRIES, - expectedError=CtapError.ERR.SUCCESS, - ) - - with Test("Check there is 8 pin attempts left"): - assert res[3] == 8 - - # Flip 1 bit - pin_wrong = list(pin1) - c = pin1[len(pin1) // 2] - - pin_wrong[len(pin1) // 2] = chr(ord(c) ^ 1) - pin_wrong = "".join(pin_wrong) - - for i in range(1, 3): - self.testPP( - "Get pin_token with wrong pin code, expect PIN_INVALID (%d/2)" % i, - pin_wrong, - expectedError=CtapError.ERR.PIN_INVALID, - ) - print("Check there is %d pin attempts left" % (8 - i)) - res = self.ctap.client_pin(pin_protocol, PinProtocolV1.CMD.GET_RETRIES) - assert res[3] == (8 - i) - print("Pass") - - for i in range(1, 3): - self.testPP( - "Get pin_token with wrong pin code, expect PIN_AUTH_BLOCKED %d/2" % i, - pin_wrong, - expectedError=CtapError.ERR.PIN_AUTH_BLOCKED, - ) - - self.reboot() - - with Test("Get pin_token, expect SUCCESS"): - pin_token = self.client.pin_protocol.get_pin_token(pin1) - pin_auth = hmac_sha256(pin_token, cdh)[:16] - - res_mc = self.testMC( - "Send MC request with correct pin_auth", - cdh, - rp, - user, - key_params, - other={"pin_auth": pin_auth, "pin_protocol": pin_protocol}, - expectedError=CtapError.ERR.SUCCESS, - ) - - with Test("Test getRetries resets to 8"): - res = self.ctap.client_pin(pin_protocol, PinProtocolV1.CMD.GET_RETRIES) - assert res[3] == (8) - - for i in range(1, 10): - err = CtapError.ERR.PIN_INVALID - if i in (3, 6): - err = CtapError.ERR.PIN_AUTH_BLOCKED - elif i >= 8: - err = [CtapError.ERR.PIN_BLOCKED, CtapError.ERR.PIN_INVALID] - self.testPP( - "Lock out authentictor and check correct error codes %d/9" % i, - pin_wrong, - expectedError=err, - ) - - attempts = 8 - i - if i > 8: - attempts = 0 - - with Test("Check there is %d pin attempts left" % attempts): - res = self.ctap.client_pin(pin_protocol, PinProtocolV1.CMD.GET_RETRIES) - assert res[3] == attempts - - if err == CtapError.ERR.PIN_AUTH_BLOCKED: - self.reboot() - - res_mc = self.testMC( - "Send MC request with correct pin_auth, expect error", - cdh, - rp, - user, - key_params, - other={"pin_auth": pin_auth, "pin_protocol": pin_protocol}, - ) - - self.reboot() - - self.testPP( - "Get pin_token with correct pin code, expect PIN_BLOCKED", - pin1, - expectedError=CtapError.ERR.PIN_BLOCKED, - ) - - def test_fido2(self,): - - self.testReset() - - # self.test_get_info() - # - # self.test_get_assertion() - # - # self.test_make_credential() - # - # self.test_rk(None) - - self.test_client_pin() - - self.testReset() - - self.test_extensions() - - print("Done") diff --git a/tools/testing/tests/hid.py b/tools/testing/tests/hid.py deleted file mode 100644 index 74d9d92..0000000 --- a/tools/testing/tests/hid.py +++ /dev/null @@ -1,252 +0,0 @@ -import sys, os, time -from binascii import hexlify - -from fido2.hid import CTAPHID -from fido2.ctap import CtapError - -from .tester import Tester, Test - - -class HIDTests(Tester): - def __init__(self, tester=None): - super().__init__(tester) - self.check_timeouts = False - - def set_check_timeouts(self, en): - self.check_timeouts = en - - def run(self,): - self.test_long_ping() - self.test_hid(self.check_timeouts) - - def test_long_ping(self): - amt = 1000 - pingdata = os.urandom(amt) - with Test("Send %d byte ping" % amt): - try: - t1 = time.time() * 1000 - r = self.send_data(CTAPHID.PING, pingdata) - t2 = time.time() * 1000 - delt = t2 - t1 - # if (delt < 140 ): - # raise RuntimeError('Fob is too fast (%d ms)' % delt) - if delt > 555 * (amt / 1000): - raise RuntimeError("Fob is too slow (%d ms)" % delt) - if r != pingdata: - raise ValueError("Ping data not echo'd") - except CtapError: - raise RuntimeError("ping failed") - - sys.stdout.flush() - - def test_hid(self, check_timeouts=False): - if check_timeouts: - with Test("idle"): - try: - cmd, resp = self.recv_raw() - except socket.timeout: - pass - - with Test("init"): - r = self.send_data(CTAPHID.INIT, "\x11\x11\x11\x11\x11\x11\x11\x11") - - with Test("100 byte ping"): - pingdata = os.urandom(100) - try: - r = self.send_data(CTAPHID.PING, pingdata) - if r != pingdata: - raise ValueError("Ping data not echo'd") - except CtapError as e: - print("100 byte Ping failed:", e) - raise RuntimeError("ping failed") - - self.test_long_ping() - - with Test("Wink"): - r = self.send_data(CTAPHID.WINK, "") - - with Test("CBOR msg with no data"): - try: - r = self.send_data(CTAPHID.CBOR, "") - if len(r) > 1 or r[0] == 0: - raise RuntimeError("Cbor is supposed to have payload") - except CtapError as e: - assert e.code == CtapError.ERR.INVALID_LENGTH - - with Test("No data in U2F msg"): - try: - r = self.send_data(CTAPHID.MSG, "") - print(hexlify(r)) - if len(r) > 2: - raise RuntimeError("MSG is supposed to have payload") - except CtapError as e: - assert e.code == CtapError.ERR.INVALID_LENGTH - - with Test("Use init command to resync"): - r = self.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") - - with Test("Invalid HID command"): - try: - r = self.send_data(0x66, "") - raise RuntimeError("Invalid command did not return error") - except CtapError as e: - assert e.code == CtapError.ERR.INVALID_COMMAND - - with Test("Sending packet with too large of a length."): - self.send_raw("\x81\x1d\xba\x00") - cmd, resp = self.recv_raw() - Tester.check_error(resp, CtapError.ERR.INVALID_LENGTH) - - r = self.send_data(CTAPHID.PING, "\x44" * 200) - with Test("Sending packets that skip a sequence number."): - self.send_raw("\x81\x04\x90") - self.send_raw("\x00") - self.send_raw("\x01") - # skip 2 - self.send_raw("\x03") - cmd, resp = self.recv_raw() - Tester.check_error(resp, CtapError.ERR.INVALID_SEQ) - - with Test("Resync and send ping"): - try: - r = self.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") - pingdata = os.urandom(100) - r = self.send_data(CTAPHID.PING, pingdata) - if r != pingdata: - raise ValueError("Ping data not echo'd") - except CtapError as e: - raise RuntimeError("resync fail: ", e) - - with Test("Send ping and abort it"): - self.send_raw("\x81\x04\x00") - self.send_raw("\x00") - self.send_raw("\x01") - try: - r = self.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") - except CtapError as e: - raise RuntimeError("resync fail: ", e) - - with Test("Send ping and abort it with different cid, expect timeout"): - oldcid = self.cid() - newcid = "\x11\x22\x33\x44" - self.send_raw("\x81\x10\x00") - self.send_raw("\x00") - self.send_raw("\x01") - self.set_cid(newcid) - self.send_raw( - "\x86\x00\x08\x11\x22\x33\x44\x55\x66\x77\x88" - ) # init from different cid - print("wait for init response") - cmd, r = self.recv_raw() # init response - assert cmd == 0x86 - self.set_cid(oldcid) - if check_timeouts: - # print('wait for timeout') - cmd, r = self.recv_raw() # timeout response - assert cmd == 0xBF - - with Test("Test timeout"): - self.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") - t1 = time.time() * 1000 - self.send_raw("\x81\x04\x00") - self.send_raw("\x00") - self.send_raw("\x01") - cmd, r = self.recv_raw() # timeout response - t2 = time.time() * 1000 - delt = t2 - t1 - assert cmd == 0xBF - assert r[0] == CtapError.ERR.TIMEOUT - assert delt < 1000 and delt > 400 - - with Test("Test not cont"): - self.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") - self.send_raw("\x81\x04\x00") - self.send_raw("\x00") - self.send_raw("\x01") - self.send_raw("\x81\x10\x00") # init packet - cmd, r = self.recv_raw() # timeout response - assert cmd == 0xBF - assert r[0] == CtapError.ERR.INVALID_SEQ - - if check_timeouts: - with Test("Check random cont ignored"): - self.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") - self.send_raw("\x01\x10\x00") - try: - cmd, r = self.recv_raw() # timeout response - except socket.timeout: - pass - - with Test("Check busy"): - t1 = time.time() * 1000 - self.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") - oldcid = self.cid() - newcid = "\x11\x22\x33\x44" - self.send_raw("\x81\x04\x00") - self.set_cid(newcid) - self.send_raw("\x81\x04\x00") - cmd, r = self.recv_raw() # busy response - t2 = time.time() * 1000 - assert t2 - t1 < 100 - assert cmd == 0xBF - assert r[0] == CtapError.ERR.CHANNEL_BUSY - - self.set_cid(oldcid) - cmd, r = self.recv_raw() # timeout response - assert cmd == 0xBF - assert r[0] == CtapError.ERR.TIMEOUT - - with Test("Check busy interleaved"): - cid1 = "\x11\x22\x33\x44" - cid2 = "\x01\x22\x33\x44" - self.set_cid(cid2) - self.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") - self.set_cid(cid1) - self.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") - self.send_raw("\x81\x00\x63") # echo 99 bytes first channel - - self.set_cid(cid2) # send ping on 2nd channel - self.send_raw("\x81\x00\x63") - Tester.delay(0.1) - self.send_raw("\x00") - - cmd, r = self.recv_raw() # busy response - - self.set_cid(cid1) # finish 1st channel ping - self.send_raw("\x00") - - self.set_cid(cid2) - - assert cmd == 0xBF - assert r[0] == CtapError.ERR.CHANNEL_BUSY - - self.set_cid(cid1) - cmd, r = self.recv_raw() # ping response - assert cmd == 0x81 - assert len(r) == 0x63 - - if check_timeouts: - with Test("Test idle, wait for timeout"): - sys.stdout.flush() - try: - cmd, resp = self.recv_raw() - except socket.timeout: - pass - - with Test("Test cid 0 is invalid"): - self.set_cid("\x00\x00\x00\x00") - self.send_raw( - "\x86\x00\x08\x11\x22\x33\x44\x55\x66\x77\x88", cid="\x00\x00\x00\x00" - ) - cmd, r = self.recv_raw() # timeout - assert cmd == 0xBF - assert r[0] == CtapError.ERR.INVALID_CHANNEL - - with Test("Test invalid broadcast cid use"): - self.set_cid("\xff\xff\xff\xff") - self.send_raw( - "\x81\x00\x08\x11\x22\x33\x44\x55\x66\x77\x88", cid="\xff\xff\xff\xff" - ) - cmd, r = self.recv_raw() # timeout - assert cmd == 0xBF - assert r[0] == CtapError.ERR.INVALID_CHANNEL diff --git a/tools/testing/tests/solo.py b/tools/testing/tests/solo.py deleted file mode 100644 index 8853f0d..0000000 --- a/tools/testing/tests/solo.py +++ /dev/null @@ -1,83 +0,0 @@ -from solo.client import SoloClient -from solo.commands import SoloExtension - -from fido2.ctap1 import ApduError -from fido2.utils import sha256 - -from .util import shannon_entropy -from .tester import Tester, Test - - -class SoloTests(Tester): - def __init__(self, tester=None): - super().__init__(tester) - - def run(self,): - self.test_solo() - - def test_solo(self,): - """ - Solo specific tests - """ - # RNG command - sc = SoloClient() - sc.find_device(self.dev) - sc.use_u2f() - memmap = (0x08005000, 0x08005000 + 198 * 1024 - 8) - - total = 1024 * 16 - with Test("Gathering %d random bytes..." % total): - entropy = b"" - while len(entropy) < total: - entropy += sc.get_rng() - - with Test("Test entropy is close to perfect"): - s = shannon_entropy(entropy) - assert s > 7.98 - print("Entropy is %.5f bits per byte." % s) - - with Test("Test Solo version command"): - assert len(sc.solo_version()) == 3 - - with Test("Test bootloader is not active"): - try: - sc.write_flash(memmap[0], b"1234") - except ApduError: - pass - - sc.exchange = sc.exchange_fido2 - - req = SoloClient.format_request(SoloExtension.version, 0, b"A" * 16) - a = sc.ctap2.get_assertion( - sc.host, b"B" * 32, [{"id": req, "type": "public-key"}] - ) - - with Test("Test custom command returned valid assertion"): - assert a.auth_data.rp_id_hash == sha256(sc.host.encode("utf8")) - assert a.credential["id"] == req - assert (a.auth_data.flags & 0x5) == 0x5 - - with Test("Test Solo version and random commands with fido2 layer"): - assert len(sc.solo_version()) == 3 - sc.get_rng() - - def test_bootloader(self,): - sc = SoloClient() - sc.find_device(self.dev) - sc.use_u2f() - - memmap = (0x08005000, 0x08005000 + 198 * 1024 - 8) - data = b"A" * 64 - - with Test("Test version command"): - assert len(sc.bootloader_version()) == 3 - - with Test("Test write command"): - sc.write_flash(memmap[0], data) - - for addr in (memmap[0] - 8, memmap[0] - 4, memmap[1], memmap[1] - 8): - with Test("Test out of bounds write command at 0x%04x" % addr): - try: - sc.write_flash(addr, data) - except CtapError as e: - assert e.code == CtapError.ERR.NOT_ALLOWED diff --git a/tools/testing/tests/tester.py b/tools/testing/tests/tester.py deleted file mode 100644 index 764bcca..0000000 --- a/tools/testing/tests/tester.py +++ /dev/null @@ -1,230 +0,0 @@ -import time, struct - -from fido2.hid import CtapHidDevice -from fido2.client import Fido2Client -from fido2.attestation import Attestation -from fido2.ctap1 import CTAP1 -from fido2.utils import Timeout - -from fido2.ctap import CtapError - - -def ForceU2F(client, device): - client.ctap = CTAP1(device) - client.pin_protocol = None - client._do_make_credential = client._ctap1_make_credential - client._do_get_assertion = client._ctap1_get_assertion - - -class Packet(object): - def __init__(self, data): - self.data = data - - def ToWireFormat(self,): - return self.data - - @staticmethod - def FromWireFormat(pkt_size, data): - return Packet(data) - - -class Test: - def __init__(self, msg, catch=None): - self.msg = msg - self.catch = catch - - def __enter__(self,): - print(self.msg) - - def __exit__(self, a, b, c): - if self.catch is None: - print("Pass") - elif isinstance(b, self.catch): - print("Pass") - return b - else: - raise RuntimeError(f"Expected exception {self.catch} did not occur.") - - -class Tester: - def __init__(self, tester=None): - self.origin = "https://examplo.org" - self.host = "examplo.org" - self.user_count = 10 - self.is_sim = False - self.nfc_interface_only = False - if tester: - self.initFromTester(tester) - - def initFromTester(self, tester): - self.user_count = tester.user_count - self.is_sim = tester.is_sim - self.dev = tester.dev - self.ctap = tester.ctap - self.ctap1 = tester.ctap1 - self.client = tester.client - self.nfc_interface_only = tester.nfc_interface_only - - def find_device(self, nfcInterfaceOnly=False): - dev = None - self.nfc_interface_only = nfcInterfaceOnly - if not nfcInterfaceOnly: - print("--- HID ---") - print(list(CtapHidDevice.list_devices())) - dev = next(CtapHidDevice.list_devices(), None) - - if not dev: - from fido2.pcsc import CtapPcscDevice - - print("--- NFC ---") - print(list(CtapPcscDevice.list_devices())) - dev = next(CtapPcscDevice.list_devices(), None) - - if not dev: - raise RuntimeError("No FIDO device found") - self.dev = dev - self.client = Fido2Client(dev, self.origin) - self.ctap = self.client.ctap2 - self.ctap1 = CTAP1(dev) - - # consume timeout error - # cmd,resp = self.recv_raw() - - def set_user_count(self, count): - self.user_count = count - - def set_sim(self, b): - self.is_sim = b - - def reboot(self,): - if self.is_sim: - print("Sending restart command...") - self.send_magic_reboot() - Tester.delay(0.25) - else: - print("Please reboot authentictor and hit enter") - input() - self.find_device(self.nfc_interface_only) - - def send_data(self, cmd, data): - if not isinstance(data, bytes): - data = struct.pack("%dB" % len(data), *[ord(x) for x in data]) - with Timeout(1.0) as event: - return self.dev.call(cmd, data, event) - - def send_raw(self, data, cid=None): - if cid is None: - cid = self.dev._dev.cid - elif not isinstance(cid, bytes): - cid = struct.pack("%dB" % len(cid), *[ord(x) for x in cid]) - if not isinstance(data, bytes): - data = struct.pack("%dB" % len(data), *[ord(x) for x in data]) - data = cid + data - l = len(data) - if l != 64: - pad = "\x00" * (64 - l) - pad = struct.pack("%dB" % len(pad), *[ord(x) for x in pad]) - data = data + pad - data = list(data) - assert len(data) == 64 - self.dev._dev.InternalSendPacket(Packet(data)) - - def send_magic_reboot(self,): - """ - For use in simulation and testing. Random bytes that authentictor should detect - and then restart itself. - """ - magic_cmd = ( - b"\xac\x10\x52\xca\x95\xe5\x69\xde\x69\xe0\x2e\xbf" - + b"\xf3\x33\x48\x5f\x13\xf9\xb2\xda\x34\xc5\xa8\xa3" - + b"\x40\x52\x66\x97\xa9\xab\x2e\x0b\x39\x4d\x8d\x04" - + b"\x97\x3c\x13\x40\x05\xbe\x1a\x01\x40\xbf\xf6\x04" - + b"\x5b\xb2\x6e\xb7\x7a\x73\xea\xa4\x78\x13\xf6\xb4" - + b"\x9a\x72\x50\xdc" - ) - self.dev._dev.InternalSendPacket(Packet(magic_cmd)) - - def cid(self,): - return self.dev._dev.cid - - def set_cid(self, cid): - if not isinstance(cid, (bytes, bytearray)): - cid = struct.pack("%dB" % len(cid), *[ord(x) for x in cid]) - self.dev._dev.cid = cid - - def recv_raw(self,): - with Timeout(1.0): - cmd, payload = self.dev._dev.InternalRecv() - return cmd, payload - - def check_error(data, err=None): - assert len(data) == 1 - if err is None: - if data[0] != 0: - raise CtapError(data[0]) - elif data[0] != err: - raise ValueError("Unexpected error: %02x" % data[0]) - - def testFunc(self, func, test, *args, **kwargs): - with Test(test): - res = None - expectedError = kwargs.get("expectedError", None) - otherArgs = kwargs.get("other", {}) - try: - res = func(*args, **otherArgs) - if expectedError != CtapError.ERR.SUCCESS: - raise RuntimeError("Expected error to occur for test: %s" % test) - except CtapError as e: - if expectedError is not None: - cond = e.code != expectedError - if isinstance(expectedError, list): - cond = e.code not in expectedError - else: - expectedError = [expectedError] - if cond: - raise RuntimeError( - f"Got error code {hex(e.code)}, expected {[hex(x) for x in expectedError]}" - ) - else: - print(e) - return res - - def testReset(self,): - print("Resetting Authenticator...") - try: - self.ctap.reset() - except CtapError: - # Some authenticators need a power cycle - print("You must power cycle authentictor. Hit enter when done.") - input() - time.sleep(0.2) - self.find_device(self.nfc_interface_only) - self.ctap.reset() - - def testMC(self, test, *args, **kwargs): - attestation_object = self.testFunc( - self.ctap.make_credential, test, *args, **kwargs - ) - if attestation_object: - verifier = Attestation.for_type(attestation_object.fmt) - client_data = args[0] - verifier().verify( - attestation_object.att_statement, - attestation_object.auth_data, - client_data, - ) - return attestation_object - - def testGA(self, test, *args, **kwargs): - return self.testFunc(self.ctap.get_assertion, test, *args, **kwargs) - - def testCP(self, test, *args, **kwargs): - return self.testFunc(self.ctap.client_pin, test, *args, **kwargs) - - def testPP(self, test, *args, **kwargs): - return self.testFunc( - self.client.pin_protocol.get_pin_token, test, *args, **kwargs - ) - - def delay(secs): - time.sleep(secs) diff --git a/tools/testing/tests/u2f.py b/tools/testing/tests/u2f.py deleted file mode 100644 index 96d7fd8..0000000 --- a/tools/testing/tests/u2f.py +++ /dev/null @@ -1,133 +0,0 @@ -from fido2.ctap1 import CTAP1, ApduError, APDU -from fido2.utils import sha256 -from fido2.client import _call_polling - -from .tester import Tester, Test - - -class U2FTests(Tester): - def __init__(self, tester=None): - super().__init__(tester) - - def run(self,): - self.test_u2f() - - def register(self, chal, appid): - reg_data = _call_polling(0.25, None, None, self.ctap1.register, chal, appid) - return reg_data - - def authenticate(self, chal, appid, key_handle, check_only=False): - auth_data = _call_polling( - 0.25, - None, - None, - self.ctap1.authenticate, - chal, - appid, - key_handle, - check_only=check_only, - ) - return auth_data - - def test_u2f(self,): - chal = sha256(b"AAA") - appid = sha256(b"BBB") - lastc = 0 - - regs = [] - - with Test("Check version"): - assert self.ctap1.get_version() == "U2F_V2" - - with Test("Check bad INS"): - try: - self.ctap1.send_apdu(0, 0, 0, 0, b"") - assert False - except ApduError as e: - assert e.code == 0x6D00 - - with Test("Check bad CLA"): - try: - self.ctap1.send_apdu(1, CTAP1.INS.VERSION, 0, 0, b"abc") - assert False - except ApduError as e: - assert e.code == 0x6E00 - - for i in range(0, self.user_count): - with Test( - "U2F reg + auth %d/%d (count: %02x)" % (i + 1, self.user_count, lastc) - ): - reg = self.register(chal, appid) - reg.verify(appid, chal) - auth = self.authenticate(chal, appid, reg.key_handle) - auth.verify(appid, chal, reg.public_key) - - regs.append(reg) - # check endianness - if lastc: - assert (auth.counter - lastc) < 10 - lastc = auth.counter - if lastc > 0x80000000: - print("WARNING: counter is unusually high: %04x" % lastc) - assert 0 - - for i in range(0, self.user_count): - with Test( - "Checking previous registration %d/%d" % (i + 1, self.user_count) - ): - auth = self.authenticate(chal, appid, regs[i].key_handle) - auth.verify(appid, chal, regs[i].public_key) - - self.reboot() - - for i in range(0, self.user_count): - with Test( - "Post reboot, Checking previous registration %d/%d" - % (i + 1, self.user_count) - ): - auth = self.authenticate(chal, appid, regs[i].key_handle) - auth.verify(appid, chal, regs[i].public_key) - - print("Check that all previous credentials are registered...") - for i in range(0, self.user_count): - with Test("Check that previous credential %d is registered" % i): - try: - auth = self.ctap1.authenticate( - chal, appid, regs[i].key_handle, check_only=True - ) - except ApduError as e: - # Indicates that key handle is registered - assert e.code == APDU.USE_NOT_SATISFIED - - with Test("Check an incorrect key handle is not registered"): - kh = bytearray(regs[0].key_handle) - kh[0] = kh[0] ^ (0x40) - try: - self.ctap1.authenticate(chal, appid, kh, check_only=True) - assert 0 - except ApduError as e: - assert e.code == APDU.WRONG_DATA - - with Test("Try to sign with incorrect key handle"): - try: - self.ctap1.authenticate(chal, appid, kh) - assert 0 - except ApduError as e: - assert e.code == APDU.WRONG_DATA - - with Test("Try to sign using an incorrect keyhandle length"): - try: - kh = regs[0].key_handle - self.ctap1.authenticate(chal, appid, kh[: len(kh) // 2]) - assert 0 - except ApduError as e: - assert e.code == APDU.WRONG_DATA - - with Test("Try to sign using an incorrect appid"): - badid = bytearray(appid) - badid[0] = badid[0] ^ (0x40) - try: - auth = self.ctap1.authenticate(chal, badid, regs[0].key_handle) - assert 0 - except ApduError as e: - assert e.code == APDU.WRONG_DATA diff --git a/tools/testing/tests/util.py b/tools/testing/tests/util.py deleted file mode 100644 index 94c3c45..0000000 --- a/tools/testing/tests/util.py +++ /dev/null @@ -1,12 +0,0 @@ -import math - - -def shannon_entropy(data): - s = 0.0 - total = len(data) - for x in range(0, 256): - freq = data.count(x) - p = freq / total - if p > 0: - s -= p * math.log2(p) - return s From f9f1e96c73f65f456467bcd8b5fa2abd69b75bc5 Mon Sep 17 00:00:00 2001 From: Conor Patrick Date: Tue, 6 Aug 2019 18:54:57 +0800 Subject: [PATCH 6/6] Update README.md --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index b243ea1..f3b61e3 100644 --- a/README.md +++ b/README.md @@ -94,10 +94,7 @@ Run the Solo application: ./main ``` -In another shell, you can run client software, for example our tests: -```bash -python tools/ctap_test.py sim fido2 -``` +In another shell, you can run our [test suite](https://github.com/solokeys/fido2-tests). You can find more details in our [documentation](https://docs.solokeys.io/solo/), including how to build on the the NUCLEO-L432KC development board.