diff --git a/makefiles/pseudomodules.inc.mk b/makefiles/pseudomodules.inc.mk index 681ceee0ced1..a6cc18d47612 100644 --- a/makefiles/pseudomodules.inc.mk +++ b/makefiles/pseudomodules.inc.mk @@ -37,6 +37,7 @@ PSEUDOMODULES += evtimer_mbox PSEUDOMODULES += evtimer_on_ztimer PSEUDOMODULES += fmt_% PSEUDOMODULES += gcoap_dtls +PSEUDOMODULES += fido2_tests PSEUDOMODULES += gnrc_dhcpv6_% PSEUDOMODULES += gnrc_ipv6_default PSEUDOMODULES += gnrc_ipv6_ext_frag_stats diff --git a/pkg/fido2_tests/Makefile b/pkg/fido2_tests/Makefile new file mode 100644 index 000000000000..4d8016393573 --- /dev/null +++ b/pkg/fido2_tests/Makefile @@ -0,0 +1,9 @@ +PKG_NAME=fido2_tests +PKG_URL=https://github.com/solokeys/fido2-tests +PKG_VERSION=3f7893d8d1a39b009cddad7913d3808ca664d3b7 +PKG_LICENSE=Apache-2.0 OR MIT + +include $(RIOTBASE)/pkg/pkg.mk + +all: + @ diff --git a/pkg/fido2_tests/patches/0001-Adaptions-for-RIOT-FIDO2-CTAP.patch b/pkg/fido2_tests/patches/0001-Adaptions-for-RIOT-FIDO2-CTAP.patch new file mode 100644 index 000000000000..7e3c933f76ae --- /dev/null +++ b/pkg/fido2_tests/patches/0001-Adaptions-for-RIOT-FIDO2-CTAP.patch @@ -0,0 +1,226 @@ +From 445c1fe93f6d0edbd1c59f318703b070c8ee445f Mon Sep 17 00:00:00 2001 +From: Ollrogge +Date: Tue, 7 Sep 2021 19:12:31 +0200 +Subject: [PATCH] Adaptions for RIOT FIDO2 CTAP + +--- + Makefile | 15 ++++++------ + tests/conftest.py | 2 +- + tests/standard/fido2/pin/test_pin.py | 24 ++++++++++++++++--- + tests/standard/fido2/test_reset.py | 5 ++++ + tests/standard/fido2/test_resident_key.py | 4 ++-- + .../fido2/user_presence/test_user_presence.py | 10 +++++++- + tests/standard/transport/test_hid.py | 11 +++++++++ + 7 files changed, 57 insertions(+), 14 deletions(-) + +diff --git a/Makefile b/Makefile +index 85aa451..c101826 100644 +--- a/Makefile ++++ b/Makefile +@@ -1,4 +1,4 @@ +-.PHONY: standard-tests vendor-tests ++.PHONY: standard-tests + + PY_VERSION=$(shell python -c "import sys; print('%d.%d'% sys.version_info[0:2])") + VALID=$(shell python -c "print($(PY_VERSION) >= 3.6)") +@@ -16,24 +16,21 @@ else + endif + + standard-tests: venv +- $(BIN)/pytest tests/standard ++ $(BIN)/pytest --ignore=tests/standard/fido2v1 --ignore=tests/standard/fido2/extensions --ignore=tests/standard/u2f --ignore tests/standard/fido2/user_presence -k "not test_ctap1_interop and not test_rk_maximum_list_capacity_per_rp_nodisplay and not test_keep_alive" tests/standard/ -s + +-vendor-tests: venv +- $(BIN)/pytest tests/vendor ++up-tests: venv ++ $(BIN)/pytest tests/standard/fido2/user_presence -s + + # setup development environment + venv: + $(PYTHON) -m venv venv + $(BIN)/python -m pip install -U pip + $(BIN)/pip install -U -r requirements.txt +- $(BIN)/pip install -U -r dev-requirements.txt +- $(BIN)/pre-commit install + + # re-run if dependencies change + update: + $(BIN)/python -m pip install -U pip + $(BIN)/pip install -U -r requirements.txt +- $(BIN)/pip install -U -r dev-requirements.txt + + # ensure this passes before commiting + check: +@@ -48,3 +45,7 @@ black: + + isort: + $(BIN)/isort -y --recursive tests/ ++ ++clean: ++ rm -r venv ++ +diff --git a/tests/conftest.py b/tests/conftest.py +index 761a684..d13d6dc 100644 +--- a/tests/conftest.py ++++ b/tests/conftest.py +@@ -175,7 +175,7 @@ class MoreRobustPcscDevice(CtapPcscDevice): + except CtapError: + if self._capabilities == 0: + raise ValueError("Unsupported device") +- ++ + def apdu_exchange(self, apdu, protocol = None): + try: + return super().apdu_exchange(apdu,protocol) +diff --git a/tests/standard/fido2/pin/test_pin.py b/tests/standard/fido2/pin/test_pin.py +index 78b09e3..f5ee4e4 100644 +--- a/tests/standard/fido2/pin/test_pin.py ++++ b/tests/standard/fido2/pin/test_pin.py +@@ -60,7 +60,11 @@ class TestPin(object): + with pytest.raises(CtapError) as e: + device.client.pin_protocol.set_pin('1234') + +- assert e.value.code == CtapError.ERR.NOT_ALLOWED ++ ''' ++ CTAP spec states: "If a PIN has already been set, authenticator ++ returns CTAP2_ERR_PIN_AUTH_INVALID error." ++ ''' ++ assert e.value.code == CtapError.ERR.PIN_AUTH_INVALID + + + def test_get_key_agreement_fields(self, CPRes): +@@ -99,11 +103,25 @@ class TestPin(object): + def test_zero_length_pin_auth(self, device, SetPinRes): + with pytest.raises(CtapError) as e: + reg = device.sendMC(*FidoRequest(SetPinRes, pin_auth=b"").toMC()) +- assert e.value.code == CtapError.ERR.PIN_AUTH_INVALID ++ ++ ''' ++ CTAP spec states: If platform sends zero length pinAuth, authenticator ++ needs to wait for user touch and then returns either ++ CTAP2_ERR_PIN_NOT_SET if pin is not set or CTAP2_ERR_PIN_INVALID ++ if pin has been set. [...]" ++ ''' ++ assert e.value.code == CtapError.ERR.PIN_INVALID + + with pytest.raises(CtapError) as e: + reg = device.sendGA(*FidoRequest(SetPinRes, pin_auth=b"").toGA()) +- assert e.value.code == CtapError.ERR.PIN_AUTH_INVALID ++ ++ ''' ++ CTAP spec states: If platform sends zero length pinAuth, authenticator ++ needs to wait for user touch and then returns either ++ CTAP2_ERR_PIN_NOT_SET if pin is not set or CTAP2_ERR_PIN_INVALID ++ if pin has been set. ++ ''' ++ assert e.value.code == CtapError.ERR.PIN_INVALID + + def test_make_credential_no_pin(self, device, SetPinRes): + with pytest.raises(CtapError) as e: +diff --git a/tests/standard/fido2/test_reset.py b/tests/standard/fido2/test_reset.py +index 508d755..adb2818 100644 +--- a/tests/standard/fido2/test_reset.py ++++ b/tests/standard/fido2/test_reset.py +@@ -9,9 +9,14 @@ import tests + def test_reset(device): + device.reset() + ++''' ++Not mentioned in any spec. ++''' ++''' + def test_reset_window(device): + print("Waiting 11s before sending reset...") + time.sleep(11) + with pytest.raises(CtapError) as e: + device.ctap2.reset(on_keepalive=DeviceSelectCredential(1)) + assert e.value.code == CtapError.ERR.NOT_ALLOWED ++''' +\ No newline at end of file +diff --git a/tests/standard/fido2/test_resident_key.py b/tests/standard/fido2/test_resident_key.py +index 2c5bece..32fe534 100644 +--- a/tests/standard/fido2/test_resident_key.py ++++ b/tests/standard/fido2/test_resident_key.py +@@ -45,7 +45,7 @@ class TestResidentKeyPersistance(object): + @pytest.mark.parametrize("do_reboot", [False, True]) + def test_user_info_returned_when_using_allowlist(self, device, MC_RK_Res, GA_RK_Res, do_reboot): + assert "id" in GA_RK_Res.user.keys() +- ++ + allow_list = [ + { + "id": MC_RK_Res.auth_data.credential_data.credential_id[:], +@@ -66,7 +66,7 @@ class TestResidentKeyPersistance(object): + class TestResidentKeyAfterReset(object): + def test_with_allow_list_after_reset(self, device, MC_RK_Res, GA_RK_Res): + assert "id" in GA_RK_Res.user.keys() +- ++ + allow_list = [ + { + "id": MC_RK_Res.auth_data.credential_data.credential_id[:], +diff --git a/tests/standard/fido2/user_presence/test_user_presence.py b/tests/standard/fido2/user_presence/test_user_presence.py +index c9904b2..0b74d24 100644 +--- a/tests/standard/fido2/user_presence/test_user_presence.py ++++ b/tests/standard/fido2/user_presence/test_user_presence.py +@@ -34,7 +34,10 @@ class TestUserPresence(object): + device.sendGA( + *FidoRequest(GARes, timeout=event, on_keepalive=None).toGA() + ) +- assert e.value.code == CtapError.ERR.KEEPALIVE_CANCEL ++ ''' ++ The CTAP states that if no UP has been activated, CTAP2_ERR_OPERATION_DENIED should be returned. ++ ''' ++ assert e.value.code == CtapError.ERR.OPERATION_DENIED + + @pytest.mark.skipif( + not "trezor" in sys.argv, reason="Only Trezor supports decline." +@@ -71,6 +74,10 @@ class TestUserPresence(object): + ) + assert e.value.code == CtapError.ERR.INVALID_OPTION + ++ ''' ++ This test makes no sense since device.sendGA is blocking ++ ''' ++ ''' + def test_user_presence_permits_only_one_request(self, device, MCRes, GARes): + print("ACTIVATE UP ONCE") + device.sendGA(*FidoRequest(GARes).toGA()) +@@ -81,3 +88,4 @@ class TestUserPresence(object): + *FidoRequest(GARes, timeout=event, on_keepalive=None).toGA() + ) + assert e.value.code == CtapError.ERR.KEEPALIVE_CANCEL ++ ''' +\ No newline at end of file +diff --git a/tests/standard/transport/test_hid.py b/tests/standard/transport/test_hid.py +index c79c933..6203a00 100644 +--- a/tests/standard/transport/test_hid.py ++++ b/tests/standard/transport/test_hid.py +@@ -105,6 +105,16 @@ class TestHID(object): + device.send_raw("\x01") + device.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") + ++ ''' ++ CTAP spec states: If an application tries to access the device from a ++ different channel while the device is busy with a transaction, that request ++ will immediately fail with a busy-error message sent to the requesting channel. ++ ++ This test tries to send an init from a different cid while the authenticator ++ is busy with the ping. In my understanding, based on the sentence above, ++ this should throw an error. Therefore the test does not make sense. ++ ''' ++ ''' + def test_ping_abort_from_different_cid(self, device, check_timeouts=False): + oldcid = device.cid() + newcid = "\x11\x22\x33\x44" +@@ -123,6 +133,7 @@ class TestHID(object): + # print('wait for timeout') + cmd, r = device.recv_raw() # timeout response + assert cmd == 0xBF ++ ''' + + def test_timeout(self, device): + device.send_data(CTAPHID.INIT, "\x11\x22\x33\x44\x55\x66\x77\x88") +-- +2.33.0 + diff --git a/sys/Kconfig b/sys/Kconfig index 94626af3b9a3..6087e99a0e9c 100644 --- a/sys/Kconfig +++ b/sys/Kconfig @@ -24,6 +24,7 @@ rsource "embunit/Kconfig" rsource "entropy_source/Kconfig" rsource "eepreg/Kconfig" rsource "event/Kconfig" +rsource "fido2/Kconfig" rsource "fmt/Kconfig" rsource "frac/Kconfig" rsource "hashes/Kconfig" diff --git a/sys/Makefile.dep b/sys/Makefile.dep index 82fbd6330649..55f18911d18f 100644 --- a/sys/Makefile.dep +++ b/sys/Makefile.dep @@ -779,4 +779,35 @@ ifneq (,$(filter dbgpin,$(USEMODULE))) FEATURES_REQUIRED += dbgpin endif +ifneq (,$(filter fido2_ctap_%,$(USEMODULE))) + USEMODULE += fido2_ctap_transport + USEMODULE += fido2_ctap + ifneq (,$(filter fido2_ctap_transport_hid,$(USEMODULE))) + USEMODULE += usbus_hid + DISABLE_MODULE += auto_init_usbus + endif +endif + +ifneq (,$(filter fido2_ctap,$(USEMODULE))) + FEATURES_REQUIRED += periph_flashpage + FEATURES_REQUIRED += periph_gpio_irq + + USEPKG += tiny-asn1 + USEPKG += tinycbor + USEPKG += micro-ecc + + INCLUDE += $(RIOTPKG)/tinycbor + + USEMODULE += mtd_flashpage + USEMODULE += mtd_write_page + USEMODULE += ztimer_msec + USEMODULE += event + USEMODULE += event_timeout + USEMODULE += prng_sha256prng + USEMODULE += cipher_modes + USEMODULE += crypto_aes_256 + USEMODULE += hashes + USEMODULE += fido2 +endif + include $(RIOTBASE)/sys/test_utils/Makefile.dep diff --git a/sys/fido2/Kconfig b/sys/fido2/Kconfig new file mode 100644 index 000000000000..1b94a5b22112 --- /dev/null +++ b/sys/fido2/Kconfig @@ -0,0 +1,7 @@ +# Copyright (C) 2021 Freie Universität Berlin +# +# This file is subject to the terms and conditions of the GNU Lesser +# General Public License v2.1. See the file LICENSE in the top level +# directory for more details. + +rsource "ctap/Kconfig" diff --git a/sys/fido2/Makefile b/sys/fido2/Makefile new file mode 100644 index 000000000000..83bbad13154f --- /dev/null +++ b/sys/fido2/Makefile @@ -0,0 +1,5 @@ +ifneq (,$(filter fido2_ctap%,$(USEMODULE))) + DIRS += ctap +endif + +include $(RIOTBASE)/Makefile.base diff --git a/sys/fido2/ctap/Kconfig b/sys/fido2/ctap/Kconfig new file mode 100644 index 000000000000..ce5c673bff6b --- /dev/null +++ b/sys/fido2/ctap/Kconfig @@ -0,0 +1,96 @@ +# Copyright (C) 2021 Freie Universität Berlin +# +# This file is subject to the terms and conditions of the GNU Lesser +# General Public License v2.1. See the file LICENSE in the top level +# directory for more details. + +menuconfig KCONFIG_USEMODULE_FIDO2_CTAP + bool "FIDO2 CTAP" + depends on USEMODULE_FIDO2_CTAP + help + Configure a FIDO2 CTAP authenticator via KConfig. + +if KCONFIG_USEMODULE_FIDO2_CTAP + +config FIDO2_CTAP_STACK_SIZE + int "CTAP thread stack size" + default 15000 + +config FIDO2_CTAP_DEVICE_AAGUID + string "AAGUID of the CTAP authenticator" + default "9c295865fa2c36b705a42320af9c8f16" + help + The AAGUID is identifying the type of the authenticator (e.g manufacturer + and model). The AAGUID needs to be 128 bits long. The default value here + is a fallback value that was randomly generated. + +config FIDO2_CTAP_DISABLE_UP + bool "Disable user presence tests" + help + When set, the authenticator will not ask for permission before creating + a new credential pair or authenticating. + +config FIDO2_CTAP_DISABLE_LED + bool "Disable LED animations" + help + When set, the authenticator will not use LED's. + +config FIDO2_CTAP_UP_TIMEOUT + int "Seconds until user presence test times out" + default 15 + +config FIDO2_CTAP_UP_BUTTON_PORT + int "Port of user presence button" + depends on !FIDO2_CTAP_DISABLE_UP + default -1 + +config FIDO2_CTAP_UP_BUTTON_PIN + int "Pin of user presence button" + depends on !FIDO2_CTAP_DISABLE_UP + default -1 + +choice + bool "User presence button mode" + depends on !FIDO2_CTAP_DISABLE_UP + default FIDO2_CTAP_UP_BUTTON_MODE_IN_PU + +config FIDO2_CTAP_UP_BUTTON_MODE_IN_PU + bool "GPIO_IN_PU" + help + Configure as input with pull-up resistor + +config FIDO2_CTAP_UP_BUTTON_MODE_IN_PD + bool "GPIO_IN_PD" + help + Configure as input with pull-down resistor + +config FIDO2_CTAP_UP_BUTTON_MODE_IN + bool "GPIO_IN" + help + Configure as input without pull resistor + +endchoice + +choice + bool "User presence button pin flank" + depends on !FIDO2_CTAP_DISABLE_UP + default FIDO2_CTAP_UP_BUTTON_FLANK_FALLING + +config FIDO2_CTAP_UP_BUTTON_FLANK_FALLING + bool "GPIO_FALLING" + +config FIDO2_CTAP_UP_BUTTON_FLANK_RISING + bool "GPIO_RISING" + +endchoice + +config FIDO2_CTAP_FLASH_START_PAGE + int "First flash page to store data in" + default -1 + help + Configuring this incorrectly can lead to firmware corruption so make sure + the flash page is located after the firmware. + +rsource "transport/Kconfig" + +endif # KCONFIG_USEMODULE_FIDO2_CTAP diff --git a/sys/fido2/ctap/Makefile b/sys/fido2/ctap/Makefile new file mode 100644 index 000000000000..4e008f7029e1 --- /dev/null +++ b/sys/fido2/ctap/Makefile @@ -0,0 +1,7 @@ +MODULE := fido2_ctap + +ifneq (,$(filter fido2_ctap_%,$(USEMODULE))) + DIRS += transport +endif + +include $(RIOTBASE)/Makefile.base diff --git a/sys/fido2/ctap/ctap.c b/sys/fido2/ctap/ctap.c new file mode 100644 index 000000000000..61473dc22a5a --- /dev/null +++ b/sys/fido2/ctap/ctap.c @@ -0,0 +1,1850 @@ +/* + * Copyright (C) 2021 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup fido2_ctap + * @{ + * @file + * + * @author Nils Ollrogge + * @} + */ + +#include +#include +#include + +#include "errno.h" +#include "fmt.h" +#include "ztimer.h" +#include "byteorder.h" + +#include "fido2/ctap/transport/ctap_transport.h" +#include "fido2/ctap.h" +#include "fido2/ctap/ctap_utils.h" +#include "fido2/ctap/ctap_cbor.h" +#include "fido2/ctap/ctap_mem.h" + +#if IS_USED(MODULE_FIDO2_CTAP_TRANSPORT_HID) +#include "fido2/ctap/transport/hid/ctap_hid.h" +#endif + +#define ENABLE_DEBUG (0) +#include "debug.h" + +/** + * @brief CTAP get_assertion state + */ +typedef struct { + ctap_resident_key_t rks[CTAP_MAX_EXCLUDE_LIST_SIZE]; /**< eligible resident keys found */ + uint8_t count; /**< number of rks found */ + uint8_t cred_counter; /**< amount of creds sent to host */ + uint32_t timer; /**< time gap between get_next_assertion calls in milliseconds */ + bool uv; /**< indicate if user verified */ + bool up; /**< indicate if user present */ + uint8_t client_data_hash[SHA256_DIGEST_LENGTH]; /**< SHA-256 hash of JSON serialized client data */ +} ctap_get_assertion_state_t; + +/*** CTAP methods ***/ + +/** + * @brief MakeCredential method + * + * CTAP specification (version 20190130) section 5.1 + */ +static int _make_credential(ctap_req_t *req_raw); + +/** + * @brief GetAssertion method + * + * CTAP specification (version 20190130) section 5.2 + */ +static int _get_assertion(ctap_req_t *req_raw); + +/** + * @brief GetNextAssertion method + * + * CTAP specification (version 20190130) section 5.3 + */ +static int _get_next_assertion(void); + +/** + * @brief GetInfo method + * + * CTAP specification (version 20190130) section 5.4 + */ +static int _get_info(void); + +/** + * @brief ClientPIN method + * + * CTAP specification (version 20190130) section 5.5 + */ +static int _client_pin(ctap_req_t *req_raw); + +/** + * @brief Reset method + * + * CTAP specification (version 20190130) section 5.6 + */ +static int _reset(void); + +/*** CTAP clientPIN functions ***/ + +/** + * @brief ClientPIN getRetries method + * + * CTAP specification (version 20190130) section 5.5.3 + */ +static int _get_retries(void); + +/** + * @brief ClientPIN getKeyAgreement method + * + * CTAP specification (version 20190130) section 5.5.4 + */ +static int _get_key_agreement(void); + +/** + * @brief ClientPIN setPIN method + * + * CTAP specification (version 20190130) section 5.5.5 + */ +static int _set_pin(ctap_client_pin_req_t *req); + +/** + * @brief ClientPIN changePIN method + * + * CTAP specification (version 20190130) section 5.5.6 + */ +static int _change_pin(ctap_client_pin_req_t *req); + +/** + * @brief ClientPIN getPINToken method + * + * CTAP specification (version 20190130) section 5.5.7 + */ +static int _get_pin_token(ctap_client_pin_req_t *req); + +/*** helper functions ***/ + +/** + * @brief Initialize authData of attestation object + * + * This function generates the public key pair used by the credential + */ +static int _make_auth_data_attest(ctap_make_credential_req_t *req, + ctap_auth_data_t *auth_data, + ctap_resident_key_t *k, bool uv, bool up, + bool rk); +/** + * @brief Initialize authData of assertion object + */ +static int _make_auth_data_assert(uint8_t *rp_id, size_t rp_id_len, + ctap_auth_data_header_t *auth_data, bool uv, + bool up, uint32_t sign_count); +/** + * @brief Initialize authData of next assertion object + */ +static int _make_auth_data_next_assert(uint8_t *rp_id_hash, + ctap_auth_data_header_t *auth_data, + bool uv, bool up, uint32_t sign_count); +/** + * @brief Find most recent rk matching rp_id_hash and present in allow_list + */ +static int _find_matching_rks(ctap_resident_key_t *rks, size_t rks_len, + ctap_cred_desc_alt_t *allow_list, + size_t allow_list_len, + uint8_t *rp_id, size_t rp_id_len); +/** + * @brief Check if any of the credentials in li belong to this authenticator + */ +static bool _rks_exist(ctap_cred_desc_alt_t *li, size_t len, uint8_t *rp_id, + size_t rp_id_len); + +/** + * @brief Decrypt credential that is stored by relying party + * + * This function is used when the credential is stored by the + * relying party in encrypted form. + * See Webauthn specification (version 20190304) section 4 (Credential ID) + * for more details. + */ +static int _ctap_decrypt_rk(ctap_resident_key_t *rk, ctap_cred_id_t *id); + +/** + * @brief Write credential to flash + */ +static int _save_rk(ctap_resident_key_t *rk); + +/** + * @brief Save PIN to authenticator state and write the updated state to flash + */ +static int _save_pin(uint8_t *pin, size_t len); + +/** + * @brief Write authenticator state to flash. + */ +static int _write_state_to_flash(const ctap_state_t *state); + +/** + * @brief Read authenticator state from flash + */ +static int _read_state_from_flash(ctap_state_t *state); + +/** + * @brief Write resident key to flash + */ +static int _write_rk_to_flash(const ctap_resident_key_t *rk, int page, int offset); + +/** + * @brief Check if PIN protocol version is supported + */ +static inline bool _pin_protocol_supported(uint8_t version); + +/** + * @brief Decrement PIN attempts and indicate if authenticator is locked + * + * Should the decrease of PIN attempts cross one of the two thresholds + * ( @ref CTAP_PIN_MAX_ATTS_BOOT, @ref CTAP_PIN_MAX_ATTS ) this methods returns + * an error code indicating the locking status. + */ +static int _decrement_pin_attempts(void); + +/** + * @brief Reset PIN attempts to its starting values + */ +static void _reset_pin_attempts(void); + +/** + * @brief Verify pinAuth sent by platform + * + * pinAuth is verified by comparing it to the first 16 bytes of + * HMAC-SHA-256(pinToken, clientDataHash) + */ +static int _verify_pin_auth(uint8_t *auth, uint8_t *hash, size_t len); + +/** + * @brief Check if authenticator is boot locked + * + * Authenticator needs to be rebooted in order to be used again. + */ +static inline bool _is_boot_locked(void); + +/** + * @brief Check if authenticator is completely locked + * + * Authenticator needs to be reset, deleting all stored credentials, in order + * to be used again. + */ +static inline bool _is_locked(void); + +/** + * @brief State of authenticator + */ +static ctap_state_t _state; + +/** + * @brief Assertion state + * + * The assertion state is needed to keep state between the GetAssertion and + * GetNextAssertion methods. + */ +static ctap_get_assertion_state_t _assert_state; + +/** + * @brief pinToken + * + * The pinToken is used to reduce the cost of the PIN mechanism and increase + * its security. + * + * See CTAP specification (version 20190130) section 5.5.2 for more details + */ +static uint8_t _pin_token[CTAP_PIN_TOKEN_SZ]; + +/** + * @brief remaining PIN attempts until authenticator is boot locked + */ +static int _rem_pin_att_boot = CTAP_PIN_MAX_ATTS_BOOT; + +int fido2_ctap_init(void) +{ + int ret; + + ret = fido2_ctap_mem_init(); + + if (ret != CTAP2_OK) { + return -EPROTO; + } + + ret = _read_state_from_flash(&_state); + + if (ret != CTAP2_OK) { + return -EPROTO; + } + + /* first startup of the device */ + if (_state.initialized_marker != CTAP_INITIALIZED_MARKER) { + ret = _reset(); + if (ret != CTAP2_OK) { + return -EPROTO; + } + } + +#if !IS_ACTIVE(CONFIG_FIDO2_CTAP_DISABLE_UP) +#ifdef CTAP_UP_BUTTON + ret = fido2_ctap_utils_init_gpio_pin(CTAP_UP_BUTTON, CTAP_UP_BUTTON_MODE, CTAP_UP_BUTTON_FLANK); + if (ret != CTAP2_OK) { + return -EPROTO; + } +#else + DEBUG("fido2_ctap: error - No button configured even though user presence is enabled \n"); + return -EIO; +#endif /* CTAP_UP_BUTTON */ +#endif /* !IS_ACTIVE(CONFIG_FIDO2_CTAP_DISABLE_UP) */ + + ret = fido2_ctap_crypto_init(); + + if (ret != CTAP2_OK) { + return -EPROTO; + } + + /* initialize pin_token */ + ret = fido2_ctap_crypto_prng(_pin_token, sizeof(_pin_token)); + + if (ret != CTAP2_OK) { + return -EPROTO; + } + + DEBUG("fido2_ctap: initialization successful \n"); + + return 0; +} + +size_t fido2_ctap_handle_request(ctap_req_t *req, ctap_resp_t *resp) +{ + assert(req); + assert(resp); + + switch (req->method) { + case CTAP_GET_INFO: + DEBUG("fido2_ctap: get_info req \n"); + return fido2_ctap_get_info(resp); + break; + case CTAP_MAKE_CREDENTIAL: + DEBUG("fido2_ctap: make_credential req \n"); + return fido2_ctap_make_credential(req, resp); + break; + case CTAP_GET_ASSERTION: + DEBUG("fido2_ctap: get_assertion req \n"); + return fido2_ctap_get_assertion(req, resp); + break; + case CTAP_GET_NEXT_ASSERTION: + DEBUG("fido2_ctap: get_next_assertion req \n"); + return fido2_ctap_get_next_assertion(resp); + break; + case CTAP_CLIENT_PIN: + DEBUG("fido2_ctap: client_pin req \n"); + return fido2_ctap_client_pin(req, resp); + break; + case CTAP_RESET: + DEBUG("fido2_ctap: reset req \n"); + return fido2_ctap_reset(resp); + break; + default: + DEBUG("fido2_ctap: unknown req: %u \n", req->method); + resp->status = CTAP1_ERR_INVALID_COMMAND; + break; + } + + return 0; +} + +size_t fido2_ctap_get_info(ctap_resp_t *resp) +{ + assert(resp); + + fido2_ctap_cbor_init_encoder(resp->data, sizeof(resp->data)); + + resp->status = _get_info(); + + return fido2_ctap_cbor_get_buffer_size(resp->data); +} + +size_t fido2_ctap_make_credential(ctap_req_t *req, ctap_resp_t *resp) +{ + assert(req); + assert(resp); + + fido2_ctap_cbor_init_encoder(resp->data, sizeof(resp->data)); + + resp->status = _make_credential(req); + + return fido2_ctap_cbor_get_buffer_size(resp->data); +} + +size_t fido2_ctap_get_assertion(ctap_req_t *req, ctap_resp_t *resp) +{ + assert(req); + assert(resp); + + fido2_ctap_cbor_init_encoder(resp->data, sizeof(resp->data)); + + resp->status = _get_assertion(req); + + return fido2_ctap_cbor_get_buffer_size(resp->data); +} + +size_t fido2_ctap_get_next_assertion(ctap_resp_t *resp) +{ + assert(resp); + + fido2_ctap_cbor_init_encoder(resp->data, sizeof(resp->data)); + + resp->status = _get_next_assertion(); + + return fido2_ctap_cbor_get_buffer_size(resp->data); +} + +size_t fido2_ctap_client_pin(ctap_req_t *req, ctap_resp_t *resp) +{ + assert(req); + assert(resp); + + fido2_ctap_cbor_init_encoder(resp->data, sizeof(resp->data)); + + resp->status = _client_pin(req); + + return fido2_ctap_cbor_get_buffer_size(resp->data); +} + +size_t fido2_ctap_reset(ctap_resp_t *resp) +{ + resp->status = _reset(); + + return 0; +} + +static int _reset(void) +{ + _state.initialized_marker = CTAP_INITIALIZED_MARKER; + _state.rem_pin_att = CTAP_PIN_MAX_ATTS; + _state.pin_is_set = false; + _state.rk_amount_stored = 0; + + _rem_pin_att_boot = CTAP_PIN_MAX_ATTS_BOOT; + + /* invalidate AES CCM key */ + memset(_state.cred_key, 0, sizeof(_state.cred_key)); + _state.cred_key_is_initialized = false; + + _state.config.options |= CTAP_INFO_OPTIONS_FLAG_PLAT; + _state.config.options |= CTAP_INFO_OPTIONS_FLAG_RK; + _state.config.options |= CTAP_INFO_OPTIONS_FLAG_CLIENT_PIN; + _state.config.options |= CTAP_INFO_OPTIONS_FLAG_UP; + + uint8_t aaguid[CTAP_AAGUID_SIZE]; + + fmt_hex_bytes(aaguid, CTAP_AAGUID); + + memcpy(_state.config.aaguid, aaguid, sizeof(_state.config.aaguid)); + + return _write_state_to_flash(&_state); +} + +static int _make_credential(ctap_req_t *req_raw) +{ + int ret; + bool uv = false; + bool up = false; + bool rk = false; + ctap_make_credential_req_t req = { 0 }; + ctap_auth_data_t auth_data = { 0 }; + ctap_resident_key_t k = { 0 }; + + if (_is_locked()) { + ret = CTAP2_ERR_PIN_BLOCKED; + goto done; + } + + if (_is_boot_locked()) { + ret = CTAP2_ERR_PIN_AUTH_BLOCKED; + goto done; + } + + /* set default values for options */ + req.options.rk = false; + req.options.uv = false; + req.options.up = -1; + + ret = fido2_ctap_cbor_parse_make_credential_req(&req, req_raw->buf, req_raw->len); + + if (ret != CTAP2_OK) { + goto done; + } + + /* true => authenticator is instructed to store credential on device */ + rk = req.options.rk; + + if (req.exclude_list_len > 0) { + if (_rks_exist(req.exclude_list, req.exclude_list_len, req.rp.id, + req.rp.id_len)) { +#if !IS_ACTIVE(CONFIG_FIDO2_CTAP_DISABLE_UP) + fido2_ctap_utils_user_presence_test(); +#endif + ret = CTAP2_ERR_CREDENTIAL_EXCLUDED; + goto done; + } + } + + /** + * The user presence (up) check is mandatory for the MakeCredential method. + * For the MakeCredential method the up key of the options dictionary, + * which is part of the MakeCredential request, is not defined. + * Therefore setting it is invalid for this method. + */ + if (req.options.up != -1) { + ret = CTAP2_ERR_INVALID_OPTION; + goto done; + } + + if (fido2_ctap_pin_is_set() && req.pin_auth_present) { + /* CTAP specification (version 20190130) section 5.5.8.1 */ + if (req.pin_auth_len == 0) { +#if !IS_ACTIVE(CONFIG_FIDO2_CTAP_DISABLE_UP) + fido2_ctap_utils_user_presence_test(); +#endif + ret = CTAP2_ERR_PIN_INVALID; + goto done; + } + + ret = _verify_pin_auth(req.pin_auth, req.client_data_hash, + sizeof(req.client_data_hash)); + + if (ret != CTAP2_OK) { + goto done; + } + + uv = true; + } + /* CTAP specification (version 20190130) section 5.5.8.1 */ + else if (!fido2_ctap_pin_is_set() && req.pin_auth_present + && req.pin_auth_len == 0) { +#if !IS_ACTIVE(CONFIG_FIDO2_CTAP_DISABLE_UP) + fido2_ctap_utils_user_presence_test(); +#endif + ret = CTAP2_ERR_PIN_NOT_SET; + goto done; + } + else if (fido2_ctap_pin_is_set() && !req.pin_auth_present) { + ret = CTAP2_ERR_PIN_REQUIRED; + goto done; + } + + if (req.pin_auth_present && !_pin_protocol_supported(req.pin_protocol)) { + ret = CTAP2_ERR_PIN_AUTH_INVALID; + goto done; + } + + /* last moment where transaction can be cancelled */ +#if IS_USED(MODULE_FIDO2_CTAP_TRANSPORT_HID) + if (fido2_ctap_transport_hid_should_cancel()) { + ret = CTAP2_ERR_KEEPALIVE_CANCEL; + goto done; + } +#endif + + /* user presence test to create a new credential */ +#if IS_ACTIVE(CONFIG_FIDO2_CTAP_DISABLE_UP) + up = true; +#else + if (fido2_ctap_utils_user_presence_test() == CTAP2_OK) { + up = true; + } +#endif + + ret = _make_auth_data_attest(&req, &auth_data, &k, uv, up, rk); + + if (ret != CTAP2_OK) { + goto done; + } + + ret = fido2_ctap_cbor_encode_attestation_object(&auth_data, req.client_data_hash, &k); + + if (ret != CTAP2_OK) { + goto done; + } + + /* if created credential is a resident credential, save it to flash */ + if (rk) { + ret = _save_rk(&k); + if (ret != CTAP2_OK) { + goto done; + } + } + + ret = CTAP2_OK; + +done: + /* clear rk to remove private key from memory */ + memset(&k, 0, sizeof(k)); + return ret; +} + +static int _get_assertion(ctap_req_t *req_raw) +{ + int ret; + bool uv = false; + bool up = false; + ctap_get_assertion_req_t req = { 0 }; + ctap_auth_data_header_t auth_data = { 0 }; + ctap_resident_key_t *rk = NULL; + + if (_is_locked()) { + ret = CTAP2_ERR_PIN_BLOCKED; + goto done; + } + + if (_is_boot_locked()) { + ret = CTAP2_ERR_PIN_AUTH_BLOCKED; + goto done; + } + + memset(&_assert_state, 0, sizeof(ctap_get_assertion_state_t)); + + /* set default values for options */ + req.options.up = true; + req.options.uv = false; + + ret = fido2_ctap_cbor_parse_get_assertion_req(&req, req_raw->buf, req_raw->len); + + if (ret != CTAP2_OK) { + goto done; + } + + /* find eligble credentials */ + _assert_state.count = _find_matching_rks(_assert_state.rks, + CTAP_MAX_EXCLUDE_LIST_SIZE, + req.allow_list, + req.allow_list_len, req.rp_id, + req.rp_id_len); + + if (fido2_ctap_pin_is_set() && req.pin_auth_present) { + /* CTAP specification (version 20190130) section 5.5.8.2 */ + if (req.pin_auth_len == 0) { +#if !IS_ACTIVE(CONFIG_FIDO2_CTAP_DISABLE_UP) + fido2_ctap_utils_user_presence_test(); +#endif + ret = CTAP2_ERR_PIN_INVALID; + goto done; + } + ret = _verify_pin_auth(req.pin_auth, req.client_data_hash, + sizeof(req.client_data_hash)); + + if (ret != CTAP2_OK) { + goto done; + } + + uv = true; + _assert_state.uv = true; + } + /* CTAP specification (version 20190130) section 5.5.8.2 */ + else if (!fido2_ctap_pin_is_set() && req.pin_auth_present + && req.pin_auth_len == 0) { +#if !IS_ACTIVE(CONFIG_FIDO2_CTAP_DISABLE_UP) + fido2_ctap_utils_user_presence_test(); +#endif + ret = CTAP2_ERR_PIN_NOT_SET; + goto done; + } + else if (fido2_ctap_pin_is_set() && !req.pin_auth_present) { + uv = false; + _assert_state.uv = false; + } + + if (req.pin_auth_present && !_pin_protocol_supported(req.pin_protocol)) { + ret = CTAP2_ERR_PIN_AUTH_INVALID; + goto done; + } + + if (req.options.up) { +#if IS_ACTIVE(CONFIG_FIDO2_CTAP_DISABLE_UP) + up = true; + _assert_state.up = true; +#else + if (fido2_ctap_utils_user_presence_test() == CTAP2_OK) { + up = true; + _assert_state.up = true; + } + else { + ret = CTAP2_ERR_OPERATION_DENIED; + goto done; + } +#endif + } + + if (req.options.uv) { + ret = CTAP2_ERR_UNSUPPORTED_OPTION; + goto done; + } + + if (_assert_state.count == 0) { + ret = CTAP2_ERR_NO_CREDENTIALS; + goto done; + } + + memcpy(_assert_state.client_data_hash, req.client_data_hash, + SHA256_DIGEST_LENGTH); + + /* most recently created eligble rk found */ + rk = &_assert_state.rks[_assert_state.cred_counter++]; + + /* last moment where transaction can be cancelled */ +#if IS_USED(MODULE_FIDO2_CTAP_TRANSPORT_HID) + if (fido2_ctap_transport_hid_should_cancel()) { + ret = CTAP2_ERR_KEEPALIVE_CANCEL; + goto done; + } +#endif + + ret = _make_auth_data_assert(req.rp_id, req.rp_id_len, &auth_data, uv, + up, + rk->sign_count); + + if (ret != CTAP2_OK) { + goto done; + } + + ret = fido2_ctap_cbor_encode_assertion_object(&auth_data, + req.client_data_hash, rk, + _assert_state.count); + + if (ret != CTAP2_OK) { + goto done; + } + + rk->sign_count++; + + /** + * if has_nonce is false, the credential is a resident credential and + * therefore needs to be saved on the device. + */ + if (!rk->cred_desc.has_nonce) { + ret = _save_rk(rk); + + if (ret != CTAP2_OK) { + goto done; + } + } + + /* save current time for get_next_assertion timeout */ + _assert_state.timer = ztimer_now(ZTIMER_MSEC); + + ret = CTAP2_OK; + +done: + /* clear rk to remove private key from memory */ + if (rk) { + memset(rk, 0, sizeof(*rk)); + } + return ret; +} + +static int _get_next_assertion(void) +{ + int ret; + uint32_t now; + ctap_resident_key_t *rk = NULL; + ctap_auth_data_header_t auth_data = { 0 }; + + if (_is_locked()) { + ret = CTAP2_ERR_PIN_BLOCKED; + goto done; + } + + if (_is_boot_locked()) { + ret = CTAP2_ERR_PIN_AUTH_BLOCKED; + goto done; + } + + /* no current valid assertion req pending */ + if (_assert_state.timer == 0) { + ret = CTAP2_ERR_NOT_ALLOWED; + goto done; + } + + if (_assert_state.cred_counter >= _assert_state.count) { + ret = CTAP2_ERR_NOT_ALLOWED; + goto done; + } + + now = ztimer_now(ZTIMER_MSEC); + if (now - _assert_state.timer > CTAP_GET_NEXT_ASSERTION_TIMEOUT) { + memset(&_assert_state, 0, sizeof(_assert_state)); + ret = CTAP2_ERR_NOT_ALLOWED; + goto done; + } + + /* next eligble rk */ + rk = &_assert_state.rks[_assert_state.cred_counter]; + _assert_state.cred_counter++; + + ret = _make_auth_data_next_assert(rk->rp_id_hash, &auth_data, + _assert_state.uv, _assert_state.up, + rk->sign_count); + + if (ret != CTAP2_OK) { + goto done; + } + + /* cred count set to 0 because omitted when get_next_assertion */ + ret = fido2_ctap_cbor_encode_assertion_object(&auth_data, + _assert_state.client_data_hash, rk, + 0); + + if (ret != CTAP2_OK) { + goto done; + } + + /* restart timer */ + _assert_state.timer = ztimer_now(ZTIMER_MSEC); + + rk->sign_count++; + + /** + * if has_nonce is false, the credential is a resident credential and + * therefore needs to be saved on the device. + */ + if (!rk->cred_desc.has_nonce) { + ret = _save_rk(rk); + + if (ret != CTAP2_OK) { + goto done; + } + } + + ret = CTAP2_OK; + +done: + /* clear rk to remove private key from memory */ + if (rk) { + memset(rk, 0, sizeof(*rk)); + } + return ret; +} + +static int _get_info(void) +{ + ctap_info_t info = { 0 }; + + info.versions |= CTAP_VERSION_FLAG_FIDO; + info.options = _state.config.options; + info.max_msg_size = CTAP_MAX_MSG_SIZE; + info.pin_protocol = CTAP_PIN_PROT_VER; + info.pin_is_set = fido2_ctap_pin_is_set(); + memcpy(info.aaguid, _state.config.aaguid, CTAP_AAGUID_SIZE); + + return fido2_ctap_cbor_encode_info(&info); +} + +static int _client_pin(ctap_req_t *req_raw) +{ + int ret; + ctap_client_pin_req_t req = { 0 }; + + ret = fido2_ctap_cbor_parse_client_pin_req(&req, req_raw->buf, req_raw->len); + + if (ret != CTAP2_OK) { + DEBUG("fido2_ctap: client_pin - error parsing request: %d \n", ret); + return ret; + } + + /* common error handling */ + if (req.sub_command != CTAP_CP_REQ_SUB_COMMAND_GET_RETRIES) { + if (_is_locked()) { + return CTAP2_ERR_PIN_BLOCKED; + } + + if (_is_boot_locked()) { + return CTAP2_ERR_PIN_AUTH_BLOCKED; + } + } + + if (req.pin_protocol != CTAP_PIN_PROT_VER) { + return CTAP1_ERR_OTHER; + } + + switch (req.sub_command) { + case CTAP_CP_REQ_SUB_COMMAND_GET_RETRIES: + ret = _get_retries(); + break; + case CTAP_CP_REQ_SUB_COMMAND_GET_KEY_AGREEMENT: + ret = _get_key_agreement(); + break; + case CTAP_CP_REQ_SUB_COMMAND_SET_PIN: + ret = _set_pin(&req); + break; + case CTAP_CP_REQ_SUB_COMMAND_CHANGE_PIN: + ret = _change_pin(&req); + break; + case CTAP_CP_REQ_SUB_COMMAND_GET_PIN_TOKEN: + ret = _get_pin_token(&req); + break; + default: + ret = CTAP1_ERR_OTHER; + DEBUG("fido2_crap: clientpin - subcommand unknown %u \n", + req.sub_command); + } + + return ret; +} + +static int _get_retries(void) +{ + return fido2_ctap_cbor_encode_retries(_state.rem_pin_att); +} + +static int _get_key_agreement(void) +{ + int ret; + ctap_public_key_cose_t key = { 0 }; + + + /* generate key agreement key */ + ret = + fido2_ctap_crypto_gen_keypair(&_state.ag_key.pub, _state.ag_key.priv, + sizeof(_state.ag_key.priv)); + + if (ret != CTAP2_OK) { + return ret; + } + + memcpy(key.pubkey.x, _state.ag_key.pub.x, CTAP_CRYPTO_KEY_SIZE); + memcpy(key.pubkey.y, _state.ag_key.pub.y, CTAP_CRYPTO_KEY_SIZE); + + key.alg_type = CTAP_COSE_ALG_ECDH_ES_HKDF_256; + key.cred_type = CTAP_PUB_KEY_CRED_PUB_KEY; + key.crv = CTAP_COSE_KEY_CRV_P256; + key.kty = CTAP_COSE_KEY_KTY_EC2; + + return fido2_ctap_cbor_encode_key_agreement(&key); +} + +static int _set_pin(ctap_client_pin_req_t *req) +{ + uint8_t shared_key[SHA256_DIGEST_LENGTH] = { 0 }; + uint8_t shared_secret[CTAP_CRYPTO_KEY_SIZE] = { 0 }; + uint8_t hmac[SHA256_DIGEST_LENGTH] = { 0 }; + uint8_t new_pin_dec[CTAP_PIN_MAX_SIZE] = { 0 }; + size_t sz; + int ret; + + if (!req->new_pin_enc_size || !req->pin_auth_present || + !req->key_agreement_present) { + ret = CTAP2_ERR_MISSING_PARAMETER; + goto done; + } + + if (fido2_ctap_pin_is_set()) { + ret = CTAP2_ERR_PIN_AUTH_INVALID; + goto done; + } + + if (req->new_pin_enc_size < CTAP_PIN_ENC_MIN_SIZE) { + ret = CTAP1_ERR_OTHER; + goto done; + } + + ret = fido2_ctap_crypto_ecdh(shared_secret, sizeof(shared_secret), + &req->key_agreement.pubkey, _state.ag_key.priv, + sizeof(_state.ag_key.priv)); + + if (ret != CTAP2_OK) { + goto done; + } + + /* sha256 of shared secret ((abG).x) to obtain shared key */ + ret = fido2_ctap_crypto_sha256(shared_secret, sizeof(shared_secret), shared_key); + + if (ret != CTAP2_OK) { + goto done; + } + + ret = fido2_ctap_crypto_hmac_sha256(shared_key, sizeof(shared_key), req->new_pin_enc, + req->new_pin_enc_size, hmac); + + if (ret != CTAP2_OK) { + goto done; + } + + if (memcmp(hmac, req->pin_auth, CTAP_PIN_AUTH_SZ) != 0) { + DEBUG("fido2_ctap: set pin - hmac and pin_auth differ \n"); + ret = CTAP2_ERR_PIN_AUTH_INVALID; + goto done; + } + + sz = sizeof(new_pin_dec); + ret = fido2_ctap_crypto_aes_dec(new_pin_dec, &sz, req->new_pin_enc, + req->new_pin_enc_size, shared_key, + sizeof(shared_key)); + + if (ret != CTAP2_OK) { + DEBUG("fido2_ctap: set pin - error while decrypting PIN \n"); + goto done; + } + + /* last moment where transaction can be cancelled */ +#if IS_USED(MODULE_FIDO2_CTAP_TRANSPORT_HID) + if (fido2_ctap_transport_hid_should_cancel()) { + ret = CTAP2_ERR_KEEPALIVE_CANCEL; + goto done; + } +#endif + + sz = fmt_strnlen((char *)new_pin_dec, CTAP_PIN_MAX_SIZE + 1); + if (sz < CTAP_PIN_MIN_SIZE || sz > CTAP_PIN_MAX_SIZE) { + ret = CTAP2_ERR_PIN_POLICY_VIOLATION; + goto done; + } + + _save_pin(new_pin_dec, sz); + + _reset_pin_attempts(); + + ret = CTAP2_OK; + +done: + /* clear key agreement key */ + memset(&_state.ag_key, 0, sizeof(_state.ag_key)); + return ret; +} + +static int _change_pin(ctap_client_pin_req_t *req) +{ + int ret; + size_t sz; + hmac_context_t ctx; + uint8_t shared_key[CTAP_CRYPTO_KEY_SIZE] = { 0 }; + uint8_t shared_secret[CTAP_CRYPTO_KEY_SIZE] = { 0 }; + uint8_t pin_hash_dec[CTAP_PIN_TOKEN_SZ] = { 0 }; + uint8_t hmac[SHA256_DIGEST_LENGTH] = { 0 }; + uint8_t new_pin_dec[CTAP_PIN_MAX_SIZE] = { 0 }; + + if (!req->pin_auth_present || !req->key_agreement_present || + !req->pin_hash_enc_present) { + ret = CTAP2_ERR_MISSING_PARAMETER; + goto done; + } + + if (!fido2_ctap_pin_is_set()) { + ret = CTAP2_ERR_PIN_NOT_SET; + goto done; + } + + if (req->new_pin_enc_size < CTAP_PIN_ENC_MIN_SIZE) { + ret = CTAP1_ERR_OTHER; + goto done; + } + + /* derive shared secret */ + ret = fido2_ctap_crypto_ecdh(shared_secret, sizeof(shared_secret), + &req->key_agreement.pubkey, _state.ag_key.priv, + sizeof(_state.ag_key.priv)); + if (ret != CTAP2_OK) { + goto done; + } + + /* sha256 of shared secret ((abG).x) to obtain shared key */ + ret = fido2_ctap_crypto_sha256(shared_secret, sizeof(shared_secret), shared_key); + if (ret != CTAP2_OK) { + goto done; + } + + ret = fido2_ctap_crypto_hmac_sha256_init(&ctx, shared_key, sizeof(shared_key)); + if (ret != CTAP2_OK) { + goto done; + } + + ret = fido2_ctap_crypto_hmac_sha256_update(&ctx, req->new_pin_enc, req->new_pin_enc_size); + if (ret != CTAP2_OK) { + goto done; + } + + ret = fido2_ctap_crypto_hmac_sha256_update(&ctx, req->pin_hash_enc, sizeof(req->pin_hash_enc)); + if (ret != CTAP2_OK) { + goto done; + } + + ret = fido2_ctap_crypto_hmac_sha256_final(&ctx, hmac); + if (ret != CTAP2_OK) { + goto done; + } + + /* verify pinAuth by comparing first 16 bytes of HMAC-SHA-256*/ + if (memcmp(hmac, req->pin_auth, CTAP_PIN_AUTH_SZ) != 0) { + DEBUG("fido2_ctap: pin hmac and pin_auth differ \n"); + ret = CTAP2_ERR_PIN_AUTH_INVALID; + goto done; + } + + sz = sizeof(pin_hash_dec); + /* decrypt pinHashEnc */ + ret = fido2_ctap_crypto_aes_dec(pin_hash_dec, &sz, req->pin_hash_enc, + sizeof(req->pin_hash_enc), shared_key, + sizeof(shared_key)); + + if (ret != CTAP2_OK) { + DEBUG("fido2_ctap: set pin - error while decrypting pin hash \n"); + goto done; + } + + /* last moment where transaction can be cancelled */ +#if IS_USED(MODULE_FIDO2_CTAP_TRANSPORT_HID) + if (fido2_ctap_transport_hid_should_cancel()) { + ret = CTAP2_ERR_KEEPALIVE_CANCEL; + goto done; + } +#endif + + /* verify decrypted pinHash against LEFT(SHA-256(curPin), 16) */ + if (memcmp(pin_hash_dec, _state.pin_hash, CTAP_PIN_TOKEN_SZ) != 0) { + DEBUG("fido2_ctap: _get_pin_token - invalid pin \n"); + + /* reset key agreement key */ + ret = + fido2_ctap_crypto_gen_keypair(&_state.ag_key.pub, _state.ag_key.priv, + sizeof(_state.ag_key.priv)); + + if (ret != CTAP2_OK) { + goto done; + } + + _write_state_to_flash(&_state); + + ret = _decrement_pin_attempts(); + + if (ret != CTAP2_OK) { + goto done; + } + + ret = CTAP2_ERR_PIN_INVALID; + goto done; + } + + _reset_pin_attempts(); + + sz = sizeof(new_pin_dec); + /* decrypt newPinEnc to obtain newPin */ + ret = fido2_ctap_crypto_aes_dec(new_pin_dec, &sz, req->new_pin_enc, + req->new_pin_enc_size, shared_key, + sizeof(shared_key)); + + if (ret != CTAP2_OK) { + DEBUG("fido2_ctap: set pin - error while decrypting new PIN \n"); + goto done; + } + + sz = fmt_strnlen((char *)new_pin_dec, CTAP_PIN_MAX_SIZE + 1); + if (sz < CTAP_PIN_MIN_SIZE || sz > CTAP_PIN_MAX_SIZE) { + ret = CTAP2_ERR_PIN_POLICY_VIOLATION; + goto done; + } + + _save_pin(new_pin_dec, sz); + + ret = CTAP2_OK; + +done: + /* clear key agreement key */ + memset(&_state.ag_key, 0, sizeof(_state.ag_key)); + return ret; +} + +static int _get_pin_token(ctap_client_pin_req_t *req) +{ + uint8_t shared_key[SHA256_DIGEST_LENGTH] = { 0 }; + uint8_t shared_secret[CTAP_CRYPTO_KEY_SIZE] = { 0 }; + uint8_t pin_hash_dec[CTAP_PIN_TOKEN_SZ] = { 0 }; + uint8_t pin_token_enc[CTAP_PIN_TOKEN_SZ] = { 0 }; + int ret; + size_t sz; + + if (!fido2_ctap_pin_is_set()) { + ret = CTAP2_ERR_PIN_NOT_SET; + goto done; + } + + if (!req->key_agreement_present || !req->pin_hash_enc_present) { + ret = CTAP2_ERR_MISSING_PARAMETER; + goto done; + } + + ret = fido2_ctap_crypto_ecdh(shared_secret, sizeof(shared_secret), + &req->key_agreement.pubkey, _state.ag_key.priv, + sizeof(_state.ag_key.priv)); + + if (ret != CTAP2_OK) { + goto done; + } + + /* last moment where transaction can be cancelled */ +#if IS_USED(MODULE_FIDO2_CTAP_TRANSPORT_HID) + if (fido2_ctap_transport_hid_should_cancel()) { + ret = CTAP2_ERR_KEEPALIVE_CANCEL; + goto done; + } +#endif + + /* sha256 of shared secret ((abG).x) to obtain shared key */ + ret = fido2_ctap_crypto_sha256(shared_secret, sizeof(shared_secret), shared_key); + + if (ret != CTAP2_OK) { + goto done; + } + + sz = sizeof(pin_hash_dec); + ret = fido2_ctap_crypto_aes_dec(pin_hash_dec, &sz, req->pin_hash_enc, + sizeof(req->pin_hash_enc), shared_key, + sizeof(shared_key)); + + if (ret != CTAP2_OK) { + DEBUG("set pin: error while decrypting pin hash \n"); + goto done; + } + + if (memcmp(pin_hash_dec, _state.pin_hash, sizeof(_state.pin_hash)) != 0) { + DEBUG("fido2_ctap: _get_pin_token - invalid pin \n"); + + /* reset key agreement key */ + ret = + fido2_ctap_crypto_gen_keypair(&_state.ag_key.pub, _state.ag_key.priv, + sizeof(_state.ag_key.priv)); + + if (ret != CTAP2_OK) { + goto done; + } + + ret = _decrement_pin_attempts(); + + if (ret != CTAP2_OK) { + goto done; + } + + ret = CTAP2_ERR_PIN_INVALID; + goto done; + } + + _reset_pin_attempts(); + + sz = sizeof(pin_token_enc); + ret = fido2_ctap_crypto_aes_enc(pin_token_enc, &sz, _pin_token, + sizeof(_pin_token), shared_key, + sizeof(shared_key)); + + if (ret != CTAP2_OK) { + DEBUG("get pin token: error encrypting pin token \n"); + goto done; + } + + ret = fido2_ctap_cbor_encode_pin_token(pin_token_enc, + sizeof(pin_token_enc)); + +done: + /* clear key agreement key */ + memset(&_state.ag_key, 0, sizeof(_state.ag_key)); + return ret; +} + +static int _save_pin(uint8_t *pin, size_t len) +{ + uint8_t buf[SHA256_DIGEST_LENGTH] = { 0 }; + int ret; + + /* store LEFT(SHA-256(newPin), 16) */ + ret = fido2_ctap_crypto_sha256(pin, len, buf); + + if (ret != CTAP2_OK) { + return ret; + } + + memcpy(_state.pin_hash, buf, sizeof(_state.pin_hash)); + _state.pin_is_set = true; + + return _write_state_to_flash(&_state); +} + +bool fido2_ctap_cred_params_supported(uint8_t cred_type, int32_t alg_type) +{ + if (cred_type == CTAP_PUB_KEY_CRED_PUB_KEY) { + if (alg_type == CTAP_COSE_ALG_ES256) { + return true; + } + } + + return false; +} + +static inline bool _pin_protocol_supported(uint8_t version) +{ + return version == CTAP_PIN_PROT_VER; +} + +bool fido2_ctap_pin_is_set(void) +{ + return _state.pin_is_set; +} + +static inline bool _is_locked(void) +{ + return _state.rem_pin_att <= 0; +} + +static inline bool _is_boot_locked(void) +{ + return _rem_pin_att_boot <= 0; +} + +static int _decrement_pin_attempts(void) +{ + if (_state.rem_pin_att > 0) { + _state.rem_pin_att--; + } + + if (_rem_pin_att_boot > 0) { + _rem_pin_att_boot--; + } + + _write_state_to_flash(&_state); + + if (_is_locked()) { + return CTAP2_ERR_PIN_BLOCKED; + } + + if (_is_boot_locked()) { + return CTAP2_ERR_PIN_AUTH_BLOCKED; + } + + return CTAP2_OK; +} + +static void _reset_pin_attempts(void) +{ + _state.rem_pin_att = CTAP_PIN_MAX_ATTS; + _rem_pin_att_boot = CTAP_PIN_MAX_ATTS_BOOT; + + _write_state_to_flash(&_state); +} + +static int _verify_pin_auth(uint8_t *auth, uint8_t *hash, size_t len) +{ + int ret; + uint8_t hmac[SHA256_DIGEST_LENGTH] = { 0 }; + + ret = fido2_ctap_crypto_hmac_sha256(_pin_token, sizeof(_pin_token), hash, len, hmac); + + if (ret != CTAP2_OK) { + return ret; + } + + if (memcmp(auth, hmac, CTAP_PIN_AUTH_SZ) != 0) { + return CTAP2_ERR_PIN_AUTH_INVALID; + } + + return CTAP2_OK; +} + +static bool _rks_exist(ctap_cred_desc_alt_t *li, size_t len, uint8_t *rp_id, + size_t rp_id_len) +{ + uint8_t rp_id_hash[SHA256_DIGEST_LENGTH] = { 0 }; + ctap_resident_key_t rk; + int ret; + + ret = fido2_ctap_crypto_sha256(rp_id, rp_id_len, rp_id_hash); + + if (ret != CTAP2_OK) { + return ret; + } + + /* no rks stored, try decrypt only */ + if (_state.rk_amount_stored == 0) { + for (uint16_t i = 0; i < len; i++) { + ret = _ctap_decrypt_rk(&rk, &li[i].cred_id); + if (ret == CTAP2_OK) { + if (memcmp(rk.rp_id_hash, rp_id_hash, SHA256_DIGEST_LENGTH) + == 0) { + return true; + } + } + } + } + + for (uint16_t i = 0; i < _state.rk_amount_stored; i++) { + int page_num = fido2_ctap_mem_get_flashpage_number_of_rk(i); + + if (page_num < 0) { + return false; + } + + int offset_into_page = fido2_ctap_mem_get_offset_of_rk_into_flashpage(i); + + if (offset_into_page < 0) { + return false; + } + + ret = fido2_ctap_mem_read(&rk, page_num, offset_into_page, sizeof(rk)); + + if (ret != CTAP2_OK) { + return false; + } + + if (memcmp(rk.rp_id_hash, rp_id_hash, SHA256_DIGEST_LENGTH) == 0) { + for (size_t j = 0; j < len; j++) { + if (memcmp(li[j].cred_id.id, rk.cred_desc.cred_id, + CTAP_CREDENTIAL_ID_SIZE) == 0) { + return true; + } + else { + /* no match with stored key, try to decrypt */ + ret = _ctap_decrypt_rk(&rk, &li[i].cred_id); + if (ret == CTAP2_OK) { + if (memcmp(rk.rp_id_hash, rp_id_hash, + SHA256_DIGEST_LENGTH) == 0) { + return true; + } + } + } + } + } + } + return false; +} + +static int _find_matching_rks(ctap_resident_key_t *rks, size_t rks_len, + ctap_cred_desc_alt_t *allow_list, + size_t allow_list_len, uint8_t *rp_id, + size_t rp_id_len) +{ + uint8_t index = 0; + uint8_t rp_id_hash[SHA256_DIGEST_LENGTH] = { 0 }; + ctap_resident_key_t rk; + int ret; + + ret = fido2_ctap_crypto_sha256(rp_id, rp_id_len, rp_id_hash); + + if (ret != CTAP2_OK) { + return ret; + } + + /* no rks stored, try decrypt only */ + if (_state.rk_amount_stored == 0) { + for (uint16_t i = 0; i < allow_list_len; i++) { + ret = _ctap_decrypt_rk(&rks[index], &allow_list[i].cred_id); + if (ret == CTAP2_OK) { + if (memcmp(rks[index].rp_id_hash, rp_id_hash, + SHA256_DIGEST_LENGTH) == 0) { + index++; + } + } + } + } + + for (int i = 0; i < _state.rk_amount_stored; i++) { + int page_num = fido2_ctap_mem_get_flashpage_number_of_rk(i); + + if (page_num < 0) { + return CTAP1_ERR_OTHER; + } + + int offset_into_page = fido2_ctap_mem_get_offset_of_rk_into_flashpage(i); + + if (offset_into_page < 0) { + return CTAP1_ERR_OTHER; + } + + ret = fido2_ctap_mem_read(&rk, page_num, offset_into_page, sizeof(rk)); + + if (ret != CTAP2_OK) { + return ret; + } + + /* search for rk's matching rp_id_hash */ + if (memcmp(rk.rp_id_hash, rp_id_hash, SHA256_DIGEST_LENGTH) == 0) { + if (allow_list_len == 0) { + memcpy(&rks[index], &rk, sizeof(rk)); + index++; + } + else { + /* if allow list is present, also check that cred_id is in list */ + for (size_t j = 0; j < allow_list_len; j++) { + if (memcmp(allow_list[j].cred_id.id, rk.cred_desc.cred_id, + sizeof(rk.cred_desc.cred_id)) == 0) { + memcpy(&rks[index], &rk, sizeof(rk)); + index++; + break; + } + else { + /* no match with stored key, try to decrypt */ + ret = _ctap_decrypt_rk(&rks[index], + &allow_list[j].cred_id); + if (ret == CTAP2_OK) { + if (memcmp(rks[index].rp_id_hash, rk.rp_id_hash, + SHA256_DIGEST_LENGTH) == 0) { + index++; + break; + } + } + } + } + } + } + + if (index >= rks_len) { + break; + } + } + + /** + * Sort in descending order based on creation time. Credential with the + * most recent (highest) creation time will be first in list. + */ + if (index > 0) { + qsort(rks, index, sizeof(ctap_resident_key_t), fido2_ctap_utils_cred_cmp); + } + + return index; +} + +/** + * overwrite existing key if equal, else find free space. + * + * The current official CTAP spec does not have credential management yet + * so rk's can't be deleted, only overwritten => we can be sure that there are + * no holes when reading keys from flash memory + */ +static int _save_rk(ctap_resident_key_t *rk) +{ + int ret; + int page_num = CTAP_FLASH_RK_START_PAGE, offset_into_page = 0; + bool equal = false; + ctap_resident_key_t rk_tmp = { 0 }; + + if (_state.rk_amount_stored >= fido2_ctap_mem_get_max_rk_amount()) { + return CTAP2_ERR_KEY_STORE_FULL; + } + + if (_state.rk_amount_stored > 0) { + for (uint16_t i = 0; i <= _state.rk_amount_stored; i++) { + page_num = fido2_ctap_mem_get_flashpage_number_of_rk(i); + + if (page_num < 0) { + return CTAP1_ERR_OTHER; + } + + offset_into_page = fido2_ctap_mem_get_offset_of_rk_into_flashpage(i); + + if (offset_into_page < 0) { + return CTAP1_ERR_OTHER; + } + + if (i == _state.rk_amount_stored) { + break; + } + + ret = fido2_ctap_mem_read(&rk_tmp, page_num, offset_into_page, sizeof(rk_tmp)); + + if (ret != CTAP2_OK) { + return CTAP1_ERR_OTHER; + } + + /* if equal overwrite */ + if (fido2_ctap_utils_ks_equal(&rk_tmp, rk)) { + equal = true; + break; + } + } + } + + if (!equal) { + _state.rk_amount_stored++; + ret = _write_state_to_flash(&_state); + + if (ret != CTAP2_OK) { + return ret; + } + } + + return _write_rk_to_flash(rk, page_num, offset_into_page); +} + +static int _make_auth_data_assert(uint8_t *rp_id, size_t rp_id_len, + ctap_auth_data_header_t *auth_data, bool uv, + bool up, uint32_t sign_count) +{ + int ret; + + ret = fido2_ctap_crypto_sha256(rp_id, rp_id_len, auth_data->rp_id_hash); + + if (ret != CTAP2_OK) { + return ret; + } + + auth_data->sign_count = htonl(sign_count); + + if (up) { + auth_data->flags |= CTAP_AUTH_DATA_FLAG_UP; + } + + if (uv) { + auth_data->flags |= CTAP_AUTH_DATA_FLAG_UV; + } + + return CTAP2_OK; +} + +static int _make_auth_data_next_assert(uint8_t *rp_id_hash, + ctap_auth_data_header_t *auth_data, + bool uv, bool up, uint32_t sign_count) +{ + memcpy(auth_data->rp_id_hash, rp_id_hash, sizeof(auth_data->rp_id_hash)); + + auth_data->sign_count = htonl(sign_count); + + if (up) { + auth_data->flags |= CTAP_AUTH_DATA_FLAG_UP; + } + + if (uv) { + auth_data->flags |= CTAP_AUTH_DATA_FLAG_UV; + } + + return CTAP2_OK; +} + +static int _make_auth_data_attest(ctap_make_credential_req_t *req, + ctap_auth_data_t *auth_data, + ctap_resident_key_t *k, + bool uv, bool up, bool rk) +{ + int ret; + /* device aaguid */ + uint8_t aaguid[] = { CTAP_AAGUID }; + ctap_auth_data_header_t *auth_header = &auth_data->header; + ctap_attested_cred_data_t *cred_data = &auth_data->attested_cred_data; + ctap_attested_cred_data_header_t *cred_header = &cred_data->header; + ctap_rp_ent_t *rp = &req->rp; + ctap_user_ent_t *user = &req->user; + + memset(k, 0, sizeof(*k)); + memset(auth_data, 0, sizeof(*auth_data)); + + ret = fido2_ctap_crypto_sha256(rp->id, rp->id_len, auth_header->rp_id_hash); + + if (ret != CTAP2_OK) { + return ret; + } + + /* set flag indicating that attested credential data included */ + auth_header->flags |= CTAP_AUTH_DATA_FLAG_AT; + + if (up) { + auth_header->flags |= CTAP_AUTH_DATA_FLAG_UP; + } + + if (uv) { + auth_header->flags |= CTAP_AUTH_DATA_FLAG_UV; + } + + auth_header->sign_count = 0; + + memcpy(cred_header->aaguid, aaguid, sizeof(cred_header->aaguid)); + + ret = + fido2_ctap_crypto_gen_keypair(&cred_data->key.pubkey, k->priv_key, + sizeof(_state.ag_key.priv)); + + if (ret != CTAP2_OK) { + return ret; + } + + cred_data->key.alg_type = req->alg_type; + cred_data->key.cred_type = req->cred_type; + cred_data->key.crv = CTAP_COSE_KEY_CRV_P256; + cred_data->key.kty = CTAP_COSE_KEY_KTY_EC2; + + /* init key */ + k->cred_desc.cred_type = req->cred_type; + k->user_id_len = user->id_len; + k->creation_time = ztimer_now(ZTIMER_MSEC); + + memcpy(k->user_id, user->id, user->id_len); + memcpy(k->rp_id_hash, auth_header->rp_id_hash, SHA256_DIGEST_LENGTH); + + if (rk) { + /* generate credential id as 16 random bytes */ + ret = fido2_ctap_crypto_prng(cred_header->cred_id.id, + CTAP_CREDENTIAL_ID_SIZE); + + if (ret != CTAP2_OK) { + return ret; + } + + memcpy(k->cred_desc.cred_id, cred_header->cred_id.id, + sizeof(k->cred_desc.cred_id)); + + cred_header->cred_len_h = (CTAP_CREDENTIAL_ID_SIZE & 0xff00) >> 8; + cred_header->cred_len_l = CTAP_CREDENTIAL_ID_SIZE & 0x00ff; + } + else { + /* generate credential id by encrypting resident key */ + uint8_t nonce[CTAP_AES_CCM_NONCE_SIZE]; + ret = fido2_ctap_crypto_prng(nonce, sizeof(nonce)); + + if (ret != CTAP2_OK) { + return ret; + } + + ret = fido2_ctap_encrypt_rk(k, nonce, sizeof(nonce), + &cred_header->cred_id); + + if (ret != CTAP2_OK) { + return ret; + } + + cred_header->cred_len_h = (sizeof(cred_header->cred_id) & 0xff00) >> 8; + cred_header->cred_len_l = sizeof(cred_header->cred_id) & 0x00ff; + } + + return CTAP2_OK; +} + +int fido2_ctap_encrypt_rk(ctap_resident_key_t *rk, uint8_t *nonce, + size_t nonce_len, ctap_cred_id_t *id) +{ + assert(rk); + assert(nonce); + assert(id); + + int ret; + + /** + * If not initialized, create a new AES_CCM key to be able to encrypt a + * credential when it is not a resident credential and therefore + * will be stored by the relying party. + */ + if (!_state.cred_key_is_initialized) { + ret = fido2_ctap_crypto_prng(_state.cred_key, sizeof(_state.cred_key)); + + if (ret != CTAP2_OK) { + return ret; + } + + _state.cred_key_is_initialized = true; + _write_state_to_flash(&_state); + } + + ret = fido2_ctap_crypto_aes_ccm_enc((uint8_t *)id, sizeof(id), + (uint8_t *)rk, + CTAP_CREDENTIAL_ID_ENC_SIZE, + NULL, 0, + CCM_MAC_MAX_LEN, CTAP_AES_CCM_L, + nonce, nonce_len, + _state.cred_key, + sizeof(_state.cred_key)); + + if (ret != CTAP2_OK) { + return ret; + } + + memcpy(id->nonce, nonce, CTAP_AES_CCM_NONCE_SIZE); + + return CTAP2_OK; +} + +static int _ctap_decrypt_rk(ctap_resident_key_t *rk, ctap_cred_id_t *id) +{ + int ret; + + ret = fido2_ctap_crypto_aes_ccm_dec((uint8_t *)rk, sizeof(*rk), + (uint8_t *)id, + sizeof(id->id) + sizeof(id->mac), + NULL, 0, + CCM_MAC_MAX_LEN, CTAP_AES_CCM_L, + id->nonce, sizeof(id->nonce), + _state.cred_key, + sizeof(_state.cred_key)); + + if (ret != CTAP2_OK) { + return ret; + } + + /* store nonce in key to be able to later encrypt again */ + memcpy(rk->cred_desc.nonce, id->nonce, CTAP_AES_CCM_NONCE_SIZE); + rk->cred_desc.has_nonce = true; + + return CTAP2_OK; +} + +static int _write_state_to_flash(const ctap_state_t *state) +{ + return fido2_ctap_mem_write(state, CTAP_FLASH_STATE_PAGE, 0, CTAP_FLASH_STATE_SZ); +} + +static int _read_state_from_flash(ctap_state_t *state) +{ + return fido2_ctap_mem_read(state, CTAP_FLASH_STATE_PAGE, 0, sizeof(*state)); +} + +static int _write_rk_to_flash(const ctap_resident_key_t *rk, int page, int offset) +{ + return fido2_ctap_mem_write(rk, page, offset, CTAP_FLASH_RK_SZ); +} + +int fido2_ctap_get_sig(const uint8_t *auth_data, size_t auth_data_len, + const uint8_t *client_data_hash, + const ctap_resident_key_t *rk, + uint8_t *sig, size_t *sig_len) +{ + assert(auth_data); + assert(client_data_hash); + assert(rk); + assert(sig); + assert(sig_len); + + int ret; + sha256_context_t ctx; + uint8_t hash[SHA256_DIGEST_LENGTH]; + + ret = fido2_ctap_crypto_sha256_init(&ctx); + + if (ret != CTAP2_OK) { + return ret; + } + + ret = fido2_ctap_crypto_sha256_update(&ctx, auth_data, auth_data_len); + + if (ret != CTAP2_OK) { + return ret; + } + + ret = fido2_ctap_crypto_sha256_update(&ctx, client_data_hash, SHA256_DIGEST_LENGTH); + + if (ret != CTAP2_OK) { + return ret; + } + + ret = fido2_ctap_crypto_sha256_final(&ctx, hash); + + if (ret != CTAP2_OK) { + return ret; + } + + return fido2_ctap_crypto_get_sig(hash, sizeof(hash), sig, sig_len, + rk->priv_key, + sizeof(rk->priv_key)); +} diff --git a/sys/fido2/ctap/ctap_cbor.c b/sys/fido2/ctap/ctap_cbor.c new file mode 100644 index 000000000000..25935d799156 --- /dev/null +++ b/sys/fido2/ctap/ctap_cbor.c @@ -0,0 +1,1740 @@ +/* + * Copyright (C) 2021 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup fido2_ctap_cbor + * @{ + * @file + * + * @author Nils Ollrogge + * @} + */ + +#include "fido2/ctap/ctap_cbor.h" + +#define ENABLE_DEBUG (0) +#include "debug.h" + +/** + * @brief CTAP CBOR entity types + */ +typedef enum { + USER, + RP +} entity_type_t; + +/** + * @brief Parse CBOR encoded PublicKeyCredentialRpEntity or PublicKeyCredentialUserEntity + * data structure into @ref ctap_rp_ent_t or @ref ctap_user_ent_t struct + * respectively. + */ +static int _parse_entity(CborValue *it, void *entity, entity_type_t type); + +/** + * @brief Parse CBOR encoded sequence of PublicKeyCredentialDescriptors into + * @ref ctap_cred_desc_alt_t struct + */ +static int _parse_exclude_list(CborValue *it, ctap_cred_desc_alt_t *exclude_list, + size_t *exclude_list_len); + +/** + * @brief Parse CBOR encoded sequence of PublicKeyCredentialDescriptors into + * @ref ctap_cred_desc_alt_t struct + */ +static int _parse_allow_list(CborValue *it, ctap_cred_desc_alt_t *allow_list, + size_t *allow_list_len); + +/** + * @brief Parse CBOR encoded sequence of PublicKeyCredentialType and cryptographic + * algorithm type pairs and check if the combination is supported + */ +static int _parse_pub_key_cred_params(CborValue *it, + ctap_make_credential_req_t *req); + +/** + * @brief Parse CBOR encoded PublicKeyCredentialType and cryptographic + * algorithm type + */ +static int _parse_pub_key_cred_param(CborValue *it, uint8_t *cred_type, + int32_t *alg_type); + +/** + * @brief Parse CBOR encoded map of authenticator options into @ref ctap_options_t + * struct + */ +static int _parse_options(CborValue *it, ctap_options_t *options); + +/** + * @brief Parse public key in COSE_KEY format into ctap_public_key_cose_t struct + */ +static int _parse_public_key_cose(CborValue *it, ctap_public_key_cose_t *cose_key); + +/** + * @brief Parse CBOR encoded fixed length array into dst + */ +static int _parse_fixed_len_byte_array(CborValue *map, uint8_t *dst, + size_t *len); + +/** + * @brief Parse CBOR encoded unknown length array into dst + */ +static int _parse_byte_array(CborValue *it, uint8_t *dst, size_t *len); + +/** + * @brief Parse CBOR encoded string into dst + */ +static int _parse_text_string(CborValue *it, char *dst, size_t *len); + +/** + * @brief Parse CBOR encoded int into num + */ +static int _parse_int(CborValue *it, int *num); + +/** + * @brief Parse credential description + */ +static int _fido2_ctap_cbor_parse_cred_desc(CborValue *arr, ctap_cred_desc_alt_t *cred); + +/** + * @brief Encode public key into COSE_KEY format + * + * See https://tools.ietf.org/html/rfc8152#page-34 Section 13.1.1 for details. + */ +static int _encode_public_key_cose(CborEncoder *cose_key, const ctap_public_key_cose_t *key); + +/** + * @brief Encode PublicKeyCredentialDescriptor into CBOR format + */ +static int _encode_credential(CborEncoder *encoder, const void *cred_ptr, + bool rk); + +/** + * @brief Encode PublicKeyCredentialUserEntity into CBOR format + */ +static int _encode_user_entity(CborEncoder *it, const ctap_resident_key_t *rk); + +/** + * @brief CBOR encoder + */ +CborEncoder _encoder; + +size_t fido2_ctap_cbor_get_buffer_size(const uint8_t *buf) +{ + return cbor_encoder_get_buffer_size(&_encoder, buf); +} + +void fido2_ctap_cbor_init_encoder(uint8_t *buf, size_t len) +{ + cbor_encoder_init(&_encoder, buf, len, 0); +} + +int fido2_ctap_cbor_encode_info(const ctap_info_t *info) +{ + int ret; + size_t sz = 0; + CborEncoder map; + CborEncoder map2; + CborEncoder array; + CborEncoder array2; + + /* CTAP_CBOR_INFO_MAP_SZ - 1 due to no extensions being supported atm */ + ret = cbor_encoder_create_map(&_encoder, &map, CTAP_CBOR_INFO_MAP_SZ - 1); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + if (info->versions & CTAP_VERSION_FLAG_FIDO) { + sz++; + } + if (info->versions & CTAP_VERSION_FLAG_FIDO_PRE) { + sz++; + } + + /* encode versions */ + ret = cbor_encode_uint(&map, CTAP_CBOR_GET_INFO_RESP_VERSIONS); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + ret = cbor_encoder_create_array(&map, &array, sz); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + if (info->versions & CTAP_VERSION_FLAG_FIDO) { + ret = cbor_encode_text_stringz(&array, CTAP_CBOR_VERSION_STRING_FIDO); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + } + if (info->versions & CTAP_VERSION_FLAG_FIDO_PRE) { + ret = cbor_encode_text_stringz(&array, CTAP_CBOR_VERSION_STRING_FIDO_PRE); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + } + + ret = cbor_encoder_close_container(&map, &array); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + /* todo: encode supported extensions once implemented */ + + /* encode aaguid */ + ret = cbor_encode_uint(&map, CTAP_CBOR_GET_INFO_RESP_AAGUID); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + ret = cbor_encode_byte_string(&map, info->aaguid, sizeof(info->aaguid)); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + sz = 0; + + if (info->options & CTAP_INFO_OPTIONS_FLAG_PLAT) { + sz++; + } + if (info->options & CTAP_INFO_OPTIONS_FLAG_RK) { + sz++; + } + if (info->options & CTAP_INFO_OPTIONS_FLAG_CLIENT_PIN) { + sz++; + } + if (info->options & CTAP_INFO_OPTIONS_FLAG_UP) { + sz++; + } + + /* encode options */ + /* order of the items is important. needs to be canonical CBOR */ + ret = cbor_encode_uint(&map, CTAP_CBOR_GET_INFO_RESP_OPTIONS); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + ret = cbor_encoder_create_map(&map, &map2, sz); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + if (info->options & CTAP_INFO_OPTIONS_FLAG_RK) { + ret = + cbor_encode_text_string(&map2, CTAP_GET_INFO_RESP_OPTIONS_ID_RK, + strlen(CTAP_GET_INFO_RESP_OPTIONS_ID_RK)); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + ret = cbor_encode_boolean(&map2, true); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + } + if (info->options & CTAP_INFO_OPTIONS_FLAG_UP) { + ret = + cbor_encode_text_string(&map2, CTAP_GET_INFO_RESP_OPTIONS_ID_UP, + strlen(CTAP_GET_INFO_RESP_OPTIONS_ID_UP)); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + ret = cbor_encode_boolean(&map2, true); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + } + /* default for up is true so need to set false explicitly if not supported */ + else { + ret = + cbor_encode_text_string(&map2, CTAP_GET_INFO_RESP_OPTIONS_ID_UP, + strlen(CTAP_GET_INFO_RESP_OPTIONS_ID_UP)); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + ret = cbor_encode_boolean(&map2, false); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + } + if (info->options & CTAP_INFO_OPTIONS_FLAG_PLAT) { + ret = cbor_encode_text_string(&map2, CTAP_GET_INFO_RESP_OPTIONS_ID_PLAT, + strlen(CTAP_GET_INFO_RESP_OPTIONS_ID_PLAT)); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + ret = cbor_encode_boolean(&map2, true); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + } + if (info->options & CTAP_INFO_OPTIONS_FLAG_CLIENT_PIN) { + ret = cbor_encode_text_string(&map2, + CTAP_GET_INFO_RESP_OPTIONS_ID_CLIENT_PIN, + strlen(CTAP_GET_INFO_RESP_OPTIONS_ID_CLIENT_PIN)); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + if (info->pin_is_set) { + ret = cbor_encode_boolean(&map2, true); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + } + else { + ret = cbor_encode_boolean(&map2, false); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + } + } + + ret = cbor_encoder_close_container(&map, &map2); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + /* encode maxMsgSize */ + ret = cbor_encode_uint(&map, CTAP_CBOR_GET_INFO_RESP_MAX_MSG_SIZE); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + ret = cbor_encode_uint(&map, info->max_msg_size); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + /* encode pinProtocols */ + ret = cbor_encode_uint(&map, CTAP_CBOR_GET_INFO_RESP_PIN_PROTOCOLS); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + ret = cbor_encoder_create_array(&map, &array2, CTAP_AMT_SUP_PIN_VER); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + ret = cbor_encode_int(&array2, info->pin_protocol); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + ret = cbor_encoder_close_container(&map, &array2); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + ret = cbor_encoder_close_container(&_encoder, &map); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + return CTAP2_OK; +} + +int fido2_ctap_cbor_encode_assertion_object(const ctap_auth_data_header_t *auth_data, + const uint8_t *client_data_hash, + ctap_resident_key_t *rk, + uint8_t valid_cred_count) +{ + int ret; + CborEncoder map; + uint8_t sig_buf[CTAP_CRYPTO_ES256_DER_MAX_SIZE]; + size_t sig_buf_len = sizeof(sig_buf); + ctap_cred_id_t id; + ctap_cred_desc_t *cred_desc = &rk->cred_desc; + bool is_resident = !cred_desc->has_nonce; + + /* map contains at least credential descriptor, authData and signature */ + uint8_t map_len = 3; + + if (valid_cred_count > 1) { + map_len++; + } + + if (is_resident) { + map_len++; + } + + ret = cbor_encoder_create_map(&_encoder, &map, map_len); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + ret = cbor_encode_int(&map, CTAP_CBOR_GA_RESP_CREDENTIAL); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + /** + * encode credential + * if not a resident key encrypt key and store it in credential id + */ + if (!is_resident) { + ret = fido2_ctap_encrypt_rk(rk, rk->cred_desc.nonce, + sizeof(rk->cred_desc.nonce), &id); + + if (ret != CTAP2_OK) { + return ret; + } + + ret = _encode_credential(&map, (void *)&id, false); + } + else { + ret = _encode_credential(&map, (void *)&rk->cred_desc, true); + } + + if (ret != CTAP2_OK) { + return ret; + } + + /* encode auth data */ + ret = cbor_encode_int(&map, CTAP_CBOR_GA_RESP_AUTH_DATA); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + ret = + cbor_encode_byte_string(&map, (uint8_t *)auth_data, sizeof(*auth_data)); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + /* get signature for assertion */ + ret = fido2_ctap_get_sig((uint8_t *)auth_data, sizeof(*auth_data), + client_data_hash, rk, sig_buf, + &sig_buf_len); + + if (ret != CTAP2_OK) { + return ret; + } + + /* encode signature */ + ret = cbor_encode_int(&map, CTAP_CBOR_GA_RESP_SIGNATURE); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + ret = cbor_encode_byte_string(&map, sig_buf, sig_buf_len); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + /* user_id mandatory if resident credential */ + if (is_resident) { + ret = cbor_encode_int(&map, CTAP_CBOR_GA_RESP_USER); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + ret = _encode_user_entity(&map, rk); + if (ret != CTAP2_OK) { + return ret; + } + } + + /* if more than 1 valid credential found, encode amount of eligible creds found */ + if (valid_cred_count > 1) { + ret = cbor_encode_int(&map, CTAP_CBOR_GA_RESP_NUMBER_OF_CREDENTIALS); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + ret = cbor_encode_int(&map, valid_cred_count); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + } + + ret = cbor_encoder_close_container(&_encoder, &map); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + return CTAP2_OK; +} + +int fido2_ctap_cbor_encode_attestation_object(const ctap_auth_data_t *auth_data, + const uint8_t *client_data_hash, + ctap_resident_key_t *rk) +{ + int ret; + uint16_t cred_id_sz; + uint16_t cred_header_sz; + size_t offset = 0; + uint8_t *cose_key_buf; + uint8_t sig_buf[CTAP_CRYPTO_ES256_DER_MAX_SIZE]; + size_t sig_buf_len = sizeof(sig_buf); + uint8_t auth_data_buf[CTAP_CBOR_ATT_STMT_AUTH_DATA_SZ] = { 0 }; + const ctap_attested_cred_data_header_t *cred_header; + CborEncoder map; + CborEncoder cose_key; + CborEncoder attest_stmt_map; + + cred_header = &auth_data->attested_cred_data.header; + cred_id_sz = (cred_header->cred_len_h << 8) | cred_header->cred_len_l; + + /* size varies depending on if cred_id is the encrypted rk or 16 rand bytes */ + cred_header_sz = cred_id_sz + sizeof(cred_header->aaguid) + \ + sizeof(cred_header->cred_len_h) + \ + sizeof(cred_header->cred_len_h); + + ret = cbor_encoder_create_map(&_encoder, &map, CTAP_CBOR_ATTESTATION_MAP_SZ); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + /* packed attestation format (webauthn specification (version 20190304) section 8.2) */ + ret = cbor_encode_int(&map, CTAP_CBOR_MC_RESP_FMT); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + ret = cbor_encode_text_string(&map, CTAP_CBOR_STR_PACKED, strlen(CTAP_CBOR_STR_PACKED)); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + /* move rp id hash, flash and counter into authenticator data buffer */ + memcpy(auth_data_buf, (void *)&auth_data->header, + sizeof(ctap_auth_data_header_t)); + offset += sizeof(ctap_auth_data_header_t); + + /* move attested credential data header into authenticator data buffer */ + memcpy(auth_data_buf + offset, (void *)cred_header, cred_header_sz); + offset += cred_header_sz; + + cose_key_buf = auth_data_buf + offset; + + cbor_encoder_init(&cose_key, cose_key_buf, sizeof(auth_data_buf) - offset, + 0); + + /* encode credential public key into COSE format */ + ret = _encode_public_key_cose(&cose_key, &auth_data->attested_cred_data.key); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + offset += cbor_encoder_get_buffer_size(&cose_key, cose_key_buf); + + /* encode the authenticator data */ + ret = cbor_encode_int(&map, CTAP_CBOR_MC_RESP_AUTH_DATA); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + ret = cbor_encode_byte_string(&map, auth_data_buf, offset); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + /* sign authenticator data */ + ret = fido2_ctap_get_sig(auth_data_buf, offset, client_data_hash, rk, + sig_buf, &sig_buf_len); + + if (ret != CTAP2_OK) { + return ret; + } + + /* encode attestation statement */ + ret = cbor_encode_int(&map, CTAP_CBOR_MC_RESP_ATT_STMT); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + ret = cbor_encoder_create_map(&map, &attest_stmt_map, CTAP_CBOR_ATTESTATION_STMT_MAP_SZ); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + ret = cbor_encode_text_string(&attest_stmt_map, CTAP_CBOR_STR_ALG, strlen(CTAP_CBOR_STR_ALG)); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + ret = cbor_encode_int(&attest_stmt_map, CTAP_COSE_ALG_ES256); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + ret = cbor_encode_text_string(&attest_stmt_map, CTAP_CBOR_STR_SIG, strlen(CTAP_CBOR_STR_SIG)); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + ret = cbor_encode_byte_string(&attest_stmt_map, sig_buf, sig_buf_len); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + ret = cbor_encoder_close_container(&map, &attest_stmt_map); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + ret = cbor_encoder_close_container(&_encoder, &map); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + /* todo: extensions once implemented */ + + return CTAP2_OK; +} + +static int _encode_credential(CborEncoder *encoder, const void *cred_ptr, + bool rk) +{ + CborEncoder desc; + int ret; + + ret = cbor_encoder_create_map(encoder, &desc, CTAP_CBOR_CRED_DESC_MAP_SZ); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + ret = cbor_encode_text_string(&desc, CTAP_CBOR_STR_ID, strlen(CTAP_CBOR_STR_ID)); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + if (rk) { + ctap_cred_desc_t *cred_desc = (ctap_cred_desc_t *)cred_ptr; + ret = cbor_encode_byte_string(&desc, cred_desc->cred_id, + sizeof(cred_desc->cred_id)); + } + else { + ctap_cred_id_t *id = (ctap_cred_id_t *)cred_ptr; + ret = cbor_encode_byte_string(&desc, (uint8_t *)id, + sizeof(*id)); + } + + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + ret = cbor_encode_text_string(&desc, CTAP_CBOR_STR_TYPE, strlen(CTAP_CBOR_STR_TYPE)); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + ret = + cbor_encode_text_string(&desc, CTAP_CBOR_STR_PUBLIC_KEY, strlen(CTAP_CBOR_STR_PUBLIC_KEY)); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + ret = cbor_encoder_close_container(encoder, &desc); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + return CTAP2_OK; +} + +int fido2_ctap_cbor_encode_key_agreement(const ctap_public_key_cose_t *key) +{ + int ret; + CborEncoder map; + + ret = cbor_encoder_create_map(&_encoder, &map, CTAP_CBOR_KEY_AGREEMENT_MAP_SZ); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + ret = cbor_encode_int(&map, CTAP_CBOR_CP_RESP_KEY_AGREEMENT); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + ret = _encode_public_key_cose(&map, key); + + if (ret != CTAP2_OK) { + return ret; + } + + ret = cbor_encoder_close_container(&_encoder, &map); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + return CTAP2_OK; +} + +int fido2_ctap_cbor_encode_retries(uint8_t tries_left) +{ + int ret; + CborEncoder map; + + ret = cbor_encoder_create_map(&_encoder, &map, CTAP_CBOR_RETRIES_MAP_SZ); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + ret = cbor_encode_int(&map, CTAP_CBOR_CP_RETRIES_RESP); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + ret = cbor_encode_int(&map, (int)tries_left); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + ret = cbor_encoder_close_container(&_encoder, &map); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + return CTAP2_OK; +} + +int fido2_ctap_cbor_encode_pin_token(uint8_t *token, size_t len) +{ + int ret; + CborEncoder map; + + ret = cbor_encoder_create_map(&_encoder, &map, CTAP_CBOR_PIN_TOKEN_MAP_SZ); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + ret = cbor_encode_int(&map, CTAP_CBOR_CP_PIN_TOKEN_RESP); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + ret = cbor_encode_byte_string(&map, token, len); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + ret = cbor_encoder_close_container(&_encoder, &map); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + return CTAP2_OK; +} + +static int _encode_user_entity(CborEncoder *encoder, + const ctap_resident_key_t *rk) +{ + int ret; + CborEncoder map; + + ret = cbor_encoder_create_map(encoder, &map, CTAP_CBOR_USER_ENTITY_MAP_SZ); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + ret = cbor_encode_text_string(&map, CTAP_CBOR_STR_ID, strlen(CTAP_CBOR_STR_ID)); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + ret = cbor_encode_byte_string(&map, rk->user_id, rk->user_id_len); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + ret = cbor_encoder_close_container(encoder, &map); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + return CTAP2_OK; +} + +static int _encode_public_key_cose(CborEncoder *cose_key, const ctap_public_key_cose_t *key) +{ + int ret; + CborEncoder map; + + ret = cbor_encoder_create_map(cose_key, &map, CTAP_CBOR_COSE_KEY_MAP_SZ); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + ret = cbor_encode_int(&map, CTAP_COSE_KEY_LABEL_KTY); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + ret = cbor_encode_int(&map, key->kty); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + ret = cbor_encode_int(&map, CTAP_COSE_KEY_LABEL_ALG); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + ret = cbor_encode_int(&map, key->alg_type); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + ret = cbor_encode_int(&map, CTAP_COSE_KEY_LABEL_CRV); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + ret = cbor_encode_int(&map, key->crv); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + ret = cbor_encode_int(&map, CTAP_COSE_KEY_LABEL_X); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + ret = cbor_encode_byte_string(&map, key->pubkey.x, sizeof(key->pubkey.x)); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + ret = cbor_encode_int(&map, CTAP_COSE_KEY_LABEL_Y); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + ret = cbor_encode_byte_string(&map, key->pubkey.y, sizeof(key->pubkey.y)); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + ret = cbor_encoder_close_container(cose_key, &map); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + return CTAP2_OK; +} + +int fido2_ctap_cbor_parse_get_assertion_req(ctap_get_assertion_req_t *req, + const uint8_t *req_raw, size_t len) +{ + uint8_t required_parsed = 0; + int ret; + int key; + int tmp; + size_t map_len; + CborParser parser; + CborValue it; + CborValue map; + CborType type; + + ret = cbor_parser_init(req_raw, len, CborValidateCanonicalFormat, &parser, + &it); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + type = cbor_value_get_type(&it); + if (type != CborMapType) { + return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + } + + ret = cbor_value_enter_container(&it, &map); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + ret = cbor_value_get_map_length(&it, &map_len); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + /* loop over CBOR GetAssertion map */ + for (size_t i = 0; i < map_len; i++) { + type = cbor_value_get_type(&map); + if (type != CborIntegerType) { + return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + } + + ret = cbor_value_get_int_checked(&map, &key); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + ret = cbor_value_advance(&map); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + switch (key) { + case CTAP_CBOR_GA_REQ_RP_ID: + DEBUG("ctap_cbor: parse rp_id \n"); + req->rp_id_len = CTAP_DOMAIN_NAME_MAX_SIZE; + ret = _parse_text_string(&map, (char *)req->rp_id, + (size_t *)&req->rp_id_len); + required_parsed++; + break; + case CTAP_CBOR_GA_REQ_CLIENT_DATA_HASH: + DEBUG("ctap_cbor: parse client_data_hash \n"); + len = SHA256_DIGEST_LENGTH; + ret = + _parse_fixed_len_byte_array(&map, req->client_data_hash, &len); + required_parsed++; + break; + case CTAP_CBOR_GA_REQ_ALLOW_LIST: + DEBUG("ctap_cbor: parse allow_list \n"); + ret = _parse_allow_list(&map, req->allow_list, + (size_t *)&req->allow_list_len); + break; + case CTAP_CBOR_GA_REQ_EXTENSIONS: + /* todo: implement once extensions are supported */ + DEBUG("ctap_cbor: parse extensions \n"); + break; + case CTAP_CBOR_GA_REQ_OPTIONS: + DEBUG("ctap_cbor: parse options \n"); + ret = _parse_options(&map, &req->options); + break; + case CTAP_CBOR_GA_REQ_PIN_AUTH: + DEBUG("ctap_cbor: parse pin_auth \n"); + len = 16; + ret = _parse_fixed_len_byte_array(&map, req->pin_auth, &len); + /* CTAP specification (version 20190130) section 5.5.8.1 */ + if (ret == CTAP1_ERR_INVALID_LENGTH && len == 0) { + ret = CTAP2_OK; + } + req->pin_auth_len = len; + req->pin_auth_present = true; + break; + case CTAP_CBOR_GA_REQ_PIN_PROTOCOL: + DEBUG("ctap_cbor: parse pin_protocol \n"); + ret = _parse_int(&map, &tmp); + req->pin_protocol = (uint8_t)tmp; + break; + default: + DEBUG("ctap_cbor: unknown GetAssertion key: %d \n", key); + break; + } + + if (ret != CTAP2_OK) { + return ret; + } + + ret = cbor_value_advance(&map); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + } + + /* rpId and clientDataHash are required */ + if (required_parsed != 2) { + return CTAP2_ERR_MISSING_PARAMETER; + } + + return CTAP2_OK; +} + +int fido2_ctap_cbor_parse_client_pin_req(ctap_client_pin_req_t *req, + const uint8_t *req_raw, size_t len) +{ + uint8_t required_parsed = 0; + int ret; + int key; + int cbor_type; + int tmp; + size_t map_len; + CborParser parser; + CborValue it; + CborValue map; + + ret = cbor_parser_init(req_raw, len, CborValidateCanonicalFormat, &parser, + &it); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + cbor_type = cbor_value_get_type(&it); + if (cbor_type != CborMapType) { + return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + } + + ret = cbor_value_enter_container(&it, &map); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + ret = cbor_value_get_map_length(&it, &map_len); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + /* loop over CBOR ClientPIN map */ + for (size_t i = 0; i < map_len; i++) { + cbor_type = cbor_value_get_type(&map); + if (cbor_type != CborIntegerType) { + return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + } + + ret = cbor_value_get_int_checked(&map, &key); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + ret = cbor_value_advance(&map); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + switch (key) { + case CTAP_CBOR_CP_REQ_PIN_PROTOCOL: + DEBUG("ctap_cbor: parse pinProtocol \n"); + ret = _parse_int(&map, &tmp); + req->pin_protocol = (uint8_t)tmp; + required_parsed++; + break; + case CTAP_CBOR_CP_REQ_SUB_COMMAND: + DEBUG("ctap_cbor: parse subCommand \n"); + ret = _parse_int(&map, &tmp); + req->sub_command = (uint8_t)tmp; + required_parsed++; + break; + case CTAP_CBOR_CP_REQ_KEY_AGREEMENT: + DEBUG("ctap_cbor: parse keyAgreement \n"); + ret = _parse_public_key_cose(&map, &req->key_agreement); + req->key_agreement_present = true; + break; + case CTAP_CBOR_CP_REQ_PIN_AUTH: + DEBUG("ctap_cbor: parse pinAuth \n"); + len = sizeof(req->pin_auth); + ret = _parse_fixed_len_byte_array(&map, req->pin_auth, &len); + req->pin_auth_present = true; + break; + case CTAP_CBOR_CP_REQ_NEW_PIN_ENC: + DEBUG("ctap_cbor: parse newPinEnc \n"); + len = sizeof(req->new_pin_enc); + ret = _parse_byte_array(&map, req->new_pin_enc, &len); + req->new_pin_enc_size = len; + break; + case CTAP_CBOR_CP_REQ_PIN_HASH_ENC: + DEBUG("ctap_cbor: parse pinHashEnc \n"); + len = sizeof(req->pin_hash_enc); + ret = _parse_fixed_len_byte_array(&map, req->pin_hash_enc, &len); + req->pin_hash_enc_present = true; + break; + default: + DEBUG("parse_client_pin unknown key: %d \n", key); + break; + } + if (ret != CTAP2_OK) { + return ret; + } + + ret = cbor_value_advance(&map); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + } + + /* pinProtocol and subCommand are required */ + if (required_parsed != 2) { + return CTAP2_ERR_MISSING_PARAMETER; + } + + return CTAP2_OK; +} + +int fido2_ctap_cbor_parse_make_credential_req(ctap_make_credential_req_t *req, + const uint8_t *buf, + size_t size) +{ + uint8_t required_parsed = 0; + int ret; + int key; + int tmp; + size_t len; + size_t map_len; + CborParser parser; + CborValue it; + CborValue map; + CborType type; + + ret = cbor_parser_init(buf, size, CborValidateCanonicalFormat, &parser, + &it); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + type = cbor_value_get_type(&it); + if (type != CborMapType) { + return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + } + + ret = cbor_value_enter_container(&it, &map); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + ret = cbor_value_get_map_length(&it, &map_len); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + for (size_t i = 0; i < map_len; i++) { + type = cbor_value_get_type(&map); + if (type != CborIntegerType) { + return CTAP2_ERR_CBOR_UNEXPECTED_TYPE; + } + + ret = cbor_value_get_int_checked(&map, &key); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + ret = cbor_value_advance(&map); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + switch (key) { + case CTAP_CBOR_MC_REQ_CLIENT_DATA_HASH: + DEBUG("ctap_cbor: parse clientDataHash \n"); + len = SHA256_DIGEST_LENGTH; + ret = + _parse_fixed_len_byte_array(&map, req->client_data_hash, &len); + required_parsed++; + break; + case CTAP_CBOR_MC_REQ_RP: + DEBUG("ctap_cbor: parse rp \n"); + ret = _parse_entity(&map, &req->rp, RP); + required_parsed++; + break; + case CTAP_CBOR_MC_REQ_USER: + DEBUG("ctap_cbor: parse user \n"); + ret = _parse_entity(&map, &req->user, USER); + required_parsed++; + break; + case CTAP_CBOR_MC_REQ_PUB_KEY_CRED_PARAMS: + DEBUG("ctap_cbor: parse key_cred params \n"); + ret = _parse_pub_key_cred_params(&map, req); + required_parsed++; + break; + case CTAP_CBOR_MC_REQ_EXCLUDE_LIST: + DEBUG("ctap_cbor: parse excludeList \n"); + ret = _parse_exclude_list(&map, req->exclude_list, + &req->exclude_list_len); + break; + case CTAP_CBOR_MC_REQ_EXTENSIONS: + DEBUG("ctap_cbor: parse exclude_list \n"); + ret = CTAP2_ERR_UNSUPPORTED_EXTENSION; + break; + case CTAP_CBOR_MC_REQ_OPTIONS: + DEBUG("ctap_cbor: parse options \n"); + ret = _parse_options(&map, &req->options); + break; + case CTAP_CBOR_MC_REQ_PIN_AUTH: + DEBUG("ctap_cbor: parse pin_auth \n"); + len = 16; + ret = _parse_fixed_len_byte_array(&map, req->pin_auth, &len); + /* CTAP specification (version 20190130) section 5.5.8.1 (pinAuth) */ + if (ret == CTAP1_ERR_INVALID_LENGTH && len == 0) { + ret = CTAP2_OK; + } + req->pin_auth_len = len; + req->pin_auth_present = true; + break; + case CTAP_CBOR_MC_REQ_PIN_PROTOCOL: + DEBUG("ctap_cbor: parse pin_protocol \n"); + ret = _parse_int(&map, &tmp); + req->pin_protocol = (uint8_t)tmp; + break; + default: + DEBUG("ctap_cbor: unknown MakeCredential key: %d \n", key); + break; + } + + if (ret != CTAP2_OK) { + return ret; + } + + ret = cbor_value_advance(&map); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + } + + /* clientDataHash, rp, user and pubKeyCredParams are required */ + if (required_parsed != 4) { + return CTAP2_ERR_MISSING_PARAMETER; + } + + return CTAP2_OK; +} + +static int _parse_public_key_cose(CborValue *it, ctap_public_key_cose_t *cose_key) +{ + int ret; + int type; + int key; + int tmp; + size_t map_len; + size_t len; + uint8_t required_parsed = 0; + CborValue map; + + type = cbor_value_get_type(it); + if (type != CborMapType) { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + + ret = cbor_value_enter_container(it, &map); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + ret = cbor_value_get_map_length(it, &map_len); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + for (size_t i = 0; i < map_len; i++) { + + ret = _parse_int(&map, &key); + + if (ret != CTAP2_OK) { + return ret; + } + + ret = cbor_value_advance(&map); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + switch (key) { + case CTAP_COSE_KEY_LABEL_KTY: + ret = _parse_int(&map, &cose_key->kty); + required_parsed++; + break; + case CTAP_COSE_KEY_LABEL_ALG: + ret = _parse_int(&map, &tmp); + cose_key->alg_type = (int32_t)tmp; + required_parsed++; + break; + case CTAP_COSE_KEY_LABEL_CRV: + ret = _parse_int(&map, &cose_key->crv); + required_parsed++; + break; + case CTAP_COSE_KEY_LABEL_X: + len = sizeof(cose_key->pubkey.x); + ret = _parse_fixed_len_byte_array(&map, cose_key->pubkey.x, &len); + required_parsed++; + break; + case CTAP_COSE_KEY_LABEL_Y: + len = sizeof(cose_key->pubkey.y); + ret = _parse_fixed_len_byte_array(&map, cose_key->pubkey.y, &len); + required_parsed++; + break; + default: + DEBUG("Parse cose key unknown key: %d \n", key); + } + + if (ret != CTAP2_OK) { + return ret; + } + + ret = cbor_value_advance(&map); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + } + + if (required_parsed != 5) { + return CTAP2_ERR_MISSING_PARAMETER; + } + + return CTAP2_OK; +} + +static int _parse_entity(CborValue *it, void *entity, entity_type_t type) +{ + int ret; + int cbor_type; + size_t map_len; + size_t key_len; + size_t len; + CborValue map; + char key[CTAP_CBOR_MAP_MAX_KEY_LEN]; + + uint8_t required_parsed = 0; + + cbor_type = cbor_value_get_type(it); + if (cbor_type != CborMapType) { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + + ret = cbor_value_enter_container(it, &map); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + ret = cbor_value_get_map_length(it, &map_len); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + for (size_t i = 0; i < map_len; i++) { + cbor_type = cbor_value_get_type(&map); + if (cbor_type != CborTextStringType) { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + + key_len = sizeof(key); + ret = cbor_value_copy_text_string(&map, key, &key_len, NULL); + if (ret == CborErrorOutOfMemory) { + return CTAP2_ERR_LIMIT_EXCEEDED; + } + + key[sizeof(key) - 1] = '\0'; + + ret = cbor_value_advance(&map); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + if (strncmp(key, CTAP_CBOR_STR_ID, strlen(CTAP_CBOR_STR_ID)) == 0) { + if (type == USER) { + ctap_user_ent_t *user = (ctap_user_ent_t *)entity; + + user->id_len = CTAP_USER_ID_MAX_SIZE; + ret = _parse_byte_array(&map, user->id, (size_t *)&user->id_len); + } + else { + ctap_rp_ent_t *rp = (ctap_rp_ent_t *)entity; + + rp->id_len = CTAP_DOMAIN_NAME_MAX_SIZE; + ret = _parse_text_string(&map, (char *)rp->id, (size_t *)&rp->id_len); + } + + if (ret != CTAP2_OK) { + return ret; + } + required_parsed++; + } + else if (strncmp(key, CTAP_CBOR_STR_NAME, strlen(CTAP_CBOR_STR_NAME)) == 0) { + if (type == USER) { + ctap_user_ent_t *user = (ctap_user_ent_t *)entity; + + len = CTAP_USER_MAX_NAME_SIZE; + ret = _parse_text_string(&map, (char *)user->name, &len); + } + else { + ctap_rp_ent_t *rp = (ctap_rp_ent_t *)entity; + + len = CTAP_RP_MAX_NAME_SIZE; + ret = _parse_text_string(&map, (char *)rp->name, &len); + } + + if (ret != CTAP2_OK) { + return ret; + } + } + else if (strncmp(key, CTAP_CBOR_STR_ICON, strlen(CTAP_CBOR_STR_ICON)) == 0) { + len = CTAP_DOMAIN_NAME_MAX_SIZE; + + if (type == USER) { + ctap_user_ent_t *user = (ctap_user_ent_t *)entity; + + ret = _parse_text_string(&map, (char *)user->icon, &len); + } + else { + ctap_rp_ent_t *rp = (ctap_rp_ent_t *)entity; + + ret = _parse_text_string(&map, (char *)rp->icon, &len); + } + + if (ret != CTAP2_OK) { + return ret; + } + } + else if (strncmp(key, CTAP_CBOR_DISPLAY_NAME, strlen(CTAP_CBOR_DISPLAY_NAME)) == 0) { + if (type == USER) { + ctap_user_ent_t *user = (ctap_user_ent_t *)entity; + + len = CTAP_USER_MAX_NAME_SIZE; + ret = _parse_text_string(&map, (char *)user->display_name, &len); + if (ret != CTAP2_OK) { + return ret; + } + } + } + else { + DEBUG("parse entity: ignoring unknown key: %s \n", key); + } + + ret = cbor_value_advance(&map); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + } + + /* userId / rpId is required */ + if (required_parsed != 1) { + return CTAP2_ERR_MISSING_PARAMETER; + } + + return CTAP2_OK; +} + +static int _parse_pub_key_cred_params(CborValue *it, + ctap_make_credential_req_t *req) +{ + int type; + int ret; + CborValue arr; + size_t arr_len; + uint8_t cred_type; + int32_t alg_type; + + type = cbor_value_get_type(it); + if (type != CborArrayType) { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + + ret = cbor_value_enter_container(it, &arr); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + ret = cbor_value_get_array_length(it, &arr_len); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + /* params ordered from most preferred (by the RP) to least */ + for (size_t i = 0; i < arr_len; i++) { + ret = _parse_pub_key_cred_param(&arr, &cred_type, &alg_type); + if (ret != CTAP2_OK) { + return ret; + } + + /* check if algorithm is supported */ + if (fido2_ctap_cred_params_supported(cred_type, alg_type)) { + req->cred_type = cred_type; + req->alg_type = alg_type; + return CTAP2_OK; + } + + ret = cbor_value_advance(&arr); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + } + + return CTAP2_ERR_UNSUPPORTED_ALGORITHM; +} + +static int _parse_pub_key_cred_param(CborValue *it, uint8_t *cred_type, + int32_t *alg_type) +{ + int ret; + int cbor_type; + char cred_type_str[CTAP_CBOR_MAX_CREDENTIAL_TYPE_LEN]; + size_t cred_type_str_len = sizeof(cred_type_str); + CborValue cred, alg; + + cbor_type = cbor_value_get_type(it); + if (cbor_type != CborMapType) { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + + ret = cbor_value_map_find_value(it, CTAP_CBOR_STR_TYPE, &cred); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + cbor_type = cbor_value_get_type(&cred); + if (cbor_type != CborTextStringType) { + return CTAP2_ERR_MISSING_PARAMETER; + } + + ret = cbor_value_map_find_value(it, CTAP_CBOR_STR_ALG, &alg); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + cbor_type = cbor_value_get_type(&alg); + if (cbor_type != CborIntegerType) { + return CTAP2_ERR_MISSING_PARAMETER; + } + + ret = cbor_value_copy_text_string(&cred, cred_type_str, &cred_type_str_len, NULL); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + cred_type_str[sizeof(cred_type_str) - 1] = '\0'; + + if (strncmp(cred_type_str, CTAP_CBOR_STR_PUBLIC_KEY, strlen(CTAP_CBOR_STR_PUBLIC_KEY)) == 0) { + *cred_type = CTAP_PUB_KEY_CRED_PUB_KEY; + } + else { + *cred_type = CTAP_PUB_KEY_CRED_UNKNOWN; + } + + ret = cbor_value_get_int_checked(&alg, (int *)alg_type); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + return CTAP2_OK; +} + +static int _parse_options(CborValue *it, ctap_options_t *options) +{ + int ret; + int cbor_type; + char key[CTAP_CBOR_MAP_MAX_KEY_LEN]; + size_t key_len = sizeof(key); + size_t map_len; + bool option_value; + CborValue map; + + cbor_type = cbor_value_get_type(it); + if (cbor_type != CborMapType) { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + + ret = cbor_value_enter_container(it, &map); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + ret = cbor_value_get_map_length(it, &map_len); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + for (size_t i = 0; i < map_len; i++) { + cbor_type = cbor_value_get_type(&map); + if (cbor_type != CborTextStringType) { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + + ret = cbor_value_copy_text_string(&map, key, &key_len, NULL); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + key[sizeof(key) - 1] = 0; + + ret = cbor_value_advance(&map); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + cbor_type = cbor_value_get_type(&map); + if (cbor_type != CborBooleanType) { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + + /* get boolean value of options parameter */ + ret = cbor_value_get_boolean(&map, &option_value); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + if (strncmp(key, CTAP_CBOR_STR_RESIDENT_KEY, strlen(CTAP_CBOR_STR_RESIDENT_KEY)) == 0) { + options->rk = option_value; + } + else if (strncmp(key, CTAP_CBOR_STR_USER_VERIFIED, + strlen(CTAP_CBOR_STR_USER_VERIFIED)) == 0) { + options->uv = option_value; + } + else if (strncmp(key, CTAP_CBOR_STR_USER_PRESENT, + strlen(CTAP_CBOR_STR_USER_PRESENT)) == 0) { + options->up = option_value; + } + else { + /* ignore unknown options */ + DEBUG("Ctap parse options, unknown uption: %s \n", key); + } + + cbor_value_advance(&map); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + } + + return CTAP2_OK; +} + +static int _parse_allow_list(CborValue *it, ctap_cred_desc_alt_t *allow_list, + size_t *allow_list_len) +{ + return _parse_exclude_list(it, allow_list, allow_list_len); +} + +static int _parse_exclude_list(CborValue *it, ctap_cred_desc_alt_t *exclude_list, + size_t *exclude_list_len) +{ + int ret; + int type; + CborValue arr; + + type = cbor_value_get_type(it); + if (type != CborArrayType) { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + + ret = cbor_value_get_array_length(it, exclude_list_len); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + if (*exclude_list_len > CTAP_MAX_EXCLUDE_LIST_SIZE) { + *exclude_list_len = CTAP_MAX_EXCLUDE_LIST_SIZE; + } + + ret = cbor_value_enter_container(it, &arr); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + for (uint8_t i = 0; i < *exclude_list_len; i++) { + /** + * parse the CBOR encoded PublicKeyCredentialDescriptors of the + * exclude list sent by the host. + */ + ret = _fido2_ctap_cbor_parse_cred_desc(&arr, &exclude_list[i]); + + if (ret != CTAP2_OK) { + return ret; + } + } + + return CTAP2_OK; +} + +static int _fido2_ctap_cbor_parse_cred_desc(CborValue *arr, ctap_cred_desc_alt_t *cred) +{ + int ret; + int type; + CborValue val; + char type_str[16]; + size_t buf_len; + + type = cbor_value_get_type(arr); + if (type != CborMapType) { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + + ret = cbor_value_map_find_value(arr, CTAP_CBOR_STR_TYPE, &val); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + type = cbor_value_get_type(&val); + if (type != CborTextStringType) { + return CTAP2_ERR_MISSING_PARAMETER; + } + + buf_len = sizeof(type_str); + + ret = cbor_value_copy_text_string(&val, type_str, &buf_len, NULL); + + /* CborErrorOutOfMemory == unknown type */ + if (ret != CborNoError && ret != CborErrorOutOfMemory) { + return CTAP2_ERR_CBOR_PARSING; + } + type_str[sizeof(type_str) - 1] = 0; + + if (strncmp(type_str, CTAP_CBOR_STR_PUBLIC_KEY, strlen(CTAP_CBOR_STR_PUBLIC_KEY)) == 0) { + cred->cred_type = CTAP_PUB_KEY_CRED_PUB_KEY; + } + else { + cred->cred_type = CTAP_PUB_KEY_CRED_UNKNOWN; + } + + ret = cbor_value_map_find_value(arr, CTAP_CBOR_STR_ID, &val); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + type = cbor_value_get_type(&val); + if (type != CborByteStringType) { + return CTAP2_ERR_MISSING_PARAMETER; + } + + buf_len = sizeof(cred->cred_id); + ret = cbor_value_copy_byte_string(&val, (uint8_t *)&cred->cred_id, &buf_len, + NULL); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + ret = cbor_value_advance(arr); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + return CTAP2_OK; +} + +static int _parse_fixed_len_byte_array(CborValue *it, uint8_t *dst, size_t *len) +{ + int ret; + int type; + size_t temp_len = *len; + + type = cbor_value_get_type(it); + if (type != CborByteStringType) { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + + ret = cbor_value_copy_byte_string(it, dst, len, NULL); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + if (temp_len != *len) { + return CTAP1_ERR_INVALID_LENGTH; + } + + return CTAP2_OK; +} + +static int _parse_byte_array(CborValue *it, uint8_t *dst, size_t *len) +{ + int type; + int ret; + + type = cbor_value_get_type(it); + if (type != CborByteStringType) { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + + ret = cbor_value_copy_byte_string(it, dst, len, NULL); + if (ret == CborErrorOutOfMemory) { + return CTAP2_ERR_LIMIT_EXCEEDED; + } + + return CTAP2_OK; +} + +static int _parse_text_string(CborValue *it, char *dst, size_t *len) +{ + int type; + int ret; + + type = cbor_value_get_type(it); + if (type != CborTextStringType) { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + + ret = cbor_value_copy_text_string(it, dst, len, NULL); + if (ret == CborErrorOutOfMemory) { + return CTAP2_ERR_LIMIT_EXCEEDED; + } + + dst[*len] = 0; + + return CTAP2_OK; +} + +static int _parse_int(CborValue *it, int *num) +{ + int type; + int ret; + + type = cbor_value_get_type(it); + if (type != CborIntegerType) { + return CTAP2_ERR_INVALID_CBOR_TYPE; + } + + ret = cbor_value_get_int_checked(it, num); + if (ret != CborNoError) { + return CTAP2_ERR_CBOR_PARSING; + } + + return CTAP2_OK; +} diff --git a/sys/fido2/ctap/ctap_crypto.c b/sys/fido2/ctap/ctap_crypto.c new file mode 100644 index 000000000000..86b54c562980 --- /dev/null +++ b/sys/fido2/ctap/ctap_crypto.c @@ -0,0 +1,343 @@ +/* + * Copyright (C) 2021 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup fido2_ctap_crypto + * @{ + * @file + * + * @author Nils Ollrogge + * @} + */ + +#include + +#include "assert.h" +#include "random.h" + +#include "crypto/ciphers.h" +#include "crypto/modes/ccm.h" +#include "crypto/modes/cbc.h" + +#include "uECC.h" +#include "tiny-asn1.h" + +#include "fido2/ctap/ctap_crypto.h" +#include "fido2/ctap.h" +#include "fido2/ctap/ctap_utils.h" + +#define ENABLE_DEBUG (0) +#include "debug.h" + +/** + * @brief Parse signature into ASN.1 DER format + */ +static int _sig_to_der_format(uint8_t *r, uint8_t *s, uint8_t *sig, + size_t *sig_len); + +/** + * @brief Random number generator + * + * wrapper for @ref fido2_ctap_crypto_prng + */ +static int _RNG(uint8_t *dest, unsigned size); + +int fido2_ctap_crypto_init(void) +{ + uECC_set_rng(&_RNG); + + return CTAP2_OK; +} + +static int _RNG(uint8_t *dest, unsigned size) +{ + fido2_ctap_crypto_prng(dest, (size_t)size); + return 1; +} + +int fido2_ctap_crypto_prng(uint8_t *buf, size_t len) +{ + random_bytes(buf, len); + return CTAP2_OK; +} + +int fido2_ctap_crypto_sha256_init(sha256_context_t *ctx) +{ + sha256_init(ctx); + return CTAP2_OK; +} + +int fido2_ctap_crypto_sha256_update(sha256_context_t *ctx, const void *data, size_t len) +{ + sha256_update(ctx, data, len); + return CTAP2_OK; +} + +int fido2_ctap_crypto_sha256_final(sha256_context_t *ctx, void *digest) +{ + sha256_final(ctx, digest); + return CTAP2_OK; +} + +int fido2_ctap_crypto_sha256(const void *data, size_t len, + void *digest) +{ + sha256(data, len, digest); + return CTAP2_OK; +} + +int fido2_ctap_crypto_hmac_sha256_init(hmac_context_t *ctx, const void *key, + size_t key_length) +{ + hmac_sha256_init(ctx, key, key_length); + return CTAP2_OK; +} + +int fido2_ctap_crypto_hmac_sha256_update(hmac_context_t *ctx, const void *data, size_t len) +{ + hmac_sha256_update(ctx, data, len); + return CTAP2_OK; +} + +int fido2_ctap_crypto_hmac_sha256_final(hmac_context_t *ctx, void *digest) +{ + hmac_sha256_final(ctx, digest); + return CTAP2_OK; +} + +int fido2_ctap_crypto_hmac_sha256(const void *key, + size_t key_length, const void *data, size_t len, + void *digest) +{ + hmac_sha256(key, key_length, data, len, digest); + return CTAP2_OK; +} + +int fido2_ctap_crypto_ecdh(uint8_t *out, size_t len, + ctap_crypto_pub_key_t *pub_key, uint8_t *priv_key, size_t key_len) +{ + assert(len == CTAP_CRYPTO_KEY_SIZE); + assert(key_len == CTAP_CRYPTO_KEY_SIZE); + + int ret; + const struct uECC_Curve_t *curve = uECC_secp256r1(); + + ret = uECC_shared_secret((uint8_t *)pub_key, priv_key, out, curve); + + if (ret == 0) { + return CTAP1_ERR_OTHER; + } + + return CTAP2_OK; +} + +int fido2_ctap_crypto_aes_enc(uint8_t *out, size_t *out_len, uint8_t *in, + size_t in_len, const uint8_t *key, + size_t key_len) +{ + assert(*out_len >= in_len); + int ret; + cipher_t cipher; + uint8_t iv[16] = { 0 }; + + ret = cipher_init(&cipher, CIPHER_AES, key, key_len); + + if (ret < 0) { + return CTAP1_ERR_OTHER; + } + + ret = cipher_encrypt_cbc(&cipher, iv, in, in_len, out); + + if (ret < 0) { + return CTAP1_ERR_OTHER; + } + + return CTAP2_OK; +} + +int fido2_ctap_crypto_aes_dec(uint8_t *out, size_t *out_len, uint8_t *in, + size_t in_len, const uint8_t *key, + size_t key_len) +{ + assert(*out_len >= in_len); + int ret; + cipher_t cipher; + uint8_t iv[16] = { 0 }; + + ret = cipher_init(&cipher, CIPHER_AES, key, key_len); + + if (ret < 0) { + return CTAP1_ERR_OTHER; + } + + ret = cipher_decrypt_cbc(&cipher, iv, in, in_len, out); + + if (ret < 0) { + return CTAP1_ERR_OTHER; + } + + return CTAP2_OK; +} + +int fido2_ctap_crypto_aes_ccm_enc(uint8_t *out, size_t out_len, + const uint8_t *in, size_t in_len, + uint8_t *auth_data, size_t auth_data_len, + uint8_t mac_len, uint8_t length_encoding, + const uint8_t *nonce, size_t nonce_len, + const uint8_t *key, size_t key_len) +{ + assert(key_len == CTAP_CRED_KEY_LEN); + + cipher_t cipher; + int ret; + + ret = cipher_init(&cipher, CIPHER_AES_128, key, key_len); + + if (ret != 1) { + return CTAP1_ERR_OTHER; + } + + ret = cipher_encrypt_ccm(&cipher, auth_data, auth_data_len, mac_len, + length_encoding, nonce, nonce_len, + in, in_len, out); + + if (ret < 0) { + return CTAP1_ERR_OTHER; + } + + return CTAP2_OK; +} + +int fido2_ctap_crypto_aes_ccm_dec(uint8_t *out, size_t out_len, + const uint8_t *in, size_t in_len, + uint8_t *auth_data, size_t auth_data_len, + uint8_t mac_len, uint8_t length_encoding, + const uint8_t *nonce, size_t nonce_len, + const uint8_t *key, size_t key_len) +{ + assert(key_len == CTAP_CRED_KEY_LEN); + + cipher_t cipher; + int ret, len; + + ret = cipher_init(&cipher, CIPHER_AES, key, key_len); + + if (ret != 1) { + return CTAP1_ERR_OTHER; + } + + len = cipher_decrypt_ccm(&cipher, auth_data, auth_data_len, + mac_len, length_encoding, nonce, nonce_len, + in, in_len, out); + + if (len < 0) { + return CTAP1_ERR_OTHER; + } + + return CTAP2_OK; +} + +int fido2_ctap_crypto_gen_keypair(ctap_crypto_pub_key_t *pub_key, + uint8_t *priv_key, size_t len) +{ + assert(len == CTAP_CRYPTO_KEY_SIZE); + + int ret; + const struct uECC_Curve_t *curve = uECC_secp256r1(); + + ret = uECC_make_key((uint8_t *)pub_key, priv_key, curve); + if (ret == 0) { + return CTAP1_ERR_OTHER; + } + + return CTAP2_OK; +} + +int fido2_ctap_crypto_get_sig(uint8_t *hash, size_t hash_len, uint8_t *sig, + size_t *sig_len, const uint8_t *key, + size_t key_len) +{ + assert(*sig_len >= CTAP_CRYPTO_ES256_DER_MAX_SIZE); + assert(key_len == CTAP_CRYPTO_KEY_SIZE); + + /** + * +1 to pad with leading zero to prevent integer from being interpreted as + * negative (e.g. MSB of r >= 0x80) + */ + uint8_t r[CTAP_CRYPTO_KEY_SIZE + 1] = { 0 }; + uint8_t s[CTAP_CRYPTO_KEY_SIZE + 1] = { 0 }; + int ret; + + const struct uECC_Curve_t *curve = uECC_secp256r1(); + + ret = uECC_sign(key, hash, hash_len, sig, curve); + + if (ret == 0) { + return CTAP1_ERR_OTHER; + } + + memcpy(r + 1, sig, CTAP_CRYPTO_KEY_SIZE); + memcpy(s + 1, sig + CTAP_CRYPTO_KEY_SIZE, CTAP_CRYPTO_KEY_SIZE); + + ret = _sig_to_der_format(r, s, sig, sig_len); + + if (ret != CTAP2_OK) { + return ret; + } + + return CTAP2_OK; +} + +static int _sig_to_der_format(uint8_t *r, uint8_t *s, uint8_t *sig, + size_t *sig_len) +{ + asn1_tree t; + asn1_tree c1; + asn1_tree c2; + uint8_t pad_s, pad_r; + int ret; + + /** + * if MSB >= 0x80, pad with leading zero byte in order to have number + * interpreted as positive. + */ + pad_r = ((r[1] & 0x80) == 0x80); + pad_s = ((s[1] & 0x80) == 0x80); + + memset(sig, 0, *sig_len); + + list_init(&t); + list_init(&c1); + list_init(&c2); + + t.type = ASN1_TYPE_SEQUENCE; + + c1.type = ASN1_TYPE_INTEGER; + c1.length = 0x20 + pad_r; + c1.data = pad_r ? r : r + 1; + + ret = add_child(&t, &c1); + + if (ret < 0) { + return CTAP1_ERR_OTHER; + } + + c2.type = ASN1_TYPE_INTEGER; + c2.length = 0x20 + pad_s; + c2.data = pad_s ? s : s + 1; + + ret = add_child(&t, &c2); + + if (ret < 0) { + return CTAP1_ERR_OTHER; + } + + *sig_len = der_encode(&t, sig, *sig_len); + + return CTAP2_OK; +} diff --git a/sys/fido2/ctap/ctap_mem.c b/sys/fido2/ctap/ctap_mem.c new file mode 100644 index 000000000000..1db6c94b24d2 --- /dev/null +++ b/sys/fido2/ctap/ctap_mem.c @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2021 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup fido2_ctap_mem + * @{ + * @file + * + * @author Nils Ollrogge + * @} + */ + +#include + +#include "mtd.h" +#include "mtd_flashpage.h" + +#include "fido2/ctap/ctap_mem.h" + +#define ENABLE_DEBUG (0) +#include "debug.h" + +/** + * @brief MTD device descriptor initialized with flash-page driver + */ +static mtd_dev_t _mtd_dev = MTD_FLASHPAGE_INIT_VAL(CTAP_FLASH_PAGES_PER_SECTOR); + +/** + * @brief Max amount of resident keys that can be stored + */ +static uint16_t _max_rk_amnt; + +/** + * @brief Check if flash region is erased + */ +static bool _flash_is_erased(int page, int offset, size_t len); + +/** + * @brief Get amount of flashpages + */ +static unsigned _amount_of_flashpages(void); + +int fido2_ctap_mem_init(void) +{ + int ret; + + ret = mtd_init(&_mtd_dev); + + if (ret < 0) { + return ret; + } + + for (unsigned i = CTAP_FLASH_RK_START_PAGE; i < _amount_of_flashpages(); i++) { + _max_rk_amnt += flashpage_size(i) / CTAP_FLASH_RK_SZ; + } + + return CTAP2_OK; +} + +static unsigned _amount_of_flashpages(void) +{ + return _mtd_dev.sector_count * _mtd_dev.pages_per_sector; +} + +int fido2_ctap_mem_read(void *buf, uint32_t page, uint32_t offset, uint32_t len) +{ + assert(buf); + + int ret; + + ret = mtd_read_page(&_mtd_dev, buf, page, offset, len); + + if (ret < 0) { + return CTAP1_ERR_OTHER; + } + + return CTAP2_OK; +} + +int fido2_ctap_mem_write(const void *buf, uint32_t page, uint32_t offset, uint32_t len) +{ + assert(buf); + + int ret; + + if (!_flash_is_erased(page, offset, len)) { + ret = mtd_write_page(&_mtd_dev, buf, page, offset, len); + + if (ret < 0) { + return CTAP1_ERR_OTHER; + } + } + else { + ret = mtd_write_page_raw(&_mtd_dev, buf, page, offset, len); + + if (ret < 0) { + return CTAP1_ERR_OTHER; + } + } + + return CTAP2_OK; +} + +static bool _flash_is_erased(int page, int offset, size_t len) +{ + uint8_t *addr = ((uint8_t *)flashpage_addr(page) + offset); + + for (size_t i = 0; i < len; i++) { + if (addr[i] != FLASHPAGE_ERASE_STATE) { + return false; + } + } + + return true; +} + +uint16_t fido2_ctap_mem_get_max_rk_amount(void) +{ + return _max_rk_amnt; +} + +int fido2_ctap_mem_get_flashpage_number_of_rk(uint16_t rk_idx) +{ + uint16_t idx = 0; + + for (unsigned i = CTAP_FLASH_RK_START_PAGE; i < _amount_of_flashpages(); i++) { + idx += flashpage_size(i) / CTAP_FLASH_RK_SZ; + + if (idx >= rk_idx) { + return i; + } + } + + return -1; +} + +int fido2_ctap_mem_get_offset_of_rk_into_flashpage(uint16_t rk_idx) +{ + uint16_t idx = 0; + + for (unsigned i = CTAP_FLASH_RK_START_PAGE; i < _amount_of_flashpages(); i++) { + uint16_t old_idx = idx; + idx += flashpage_size(i) / CTAP_FLASH_RK_SZ; + + if (idx >= rk_idx) { + return CTAP_FLASH_RK_SZ * (rk_idx - old_idx); + } + } + + return -1; +} diff --git a/sys/fido2/ctap/ctap_utils.c b/sys/fido2/ctap/ctap_utils.c new file mode 100644 index 000000000000..51207060346e --- /dev/null +++ b/sys/fido2/ctap/ctap_utils.c @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2021 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup fido2_ctap_utils + * @{ + * @file + * + * @author Nils Ollrogge + * @} + */ + +#include + +#include "ztimer.h" + +#include "fido2/ctap.h" +#include "fido2/ctap/ctap_utils.h" + +#if !IS_ACTIVE(CONFIG_FIDO2_CTAP_DISABLE_UP) +#include "periph/gpio.h" +#endif + +#define ENABLE_DEBUG (0) +#include "debug.h" + +#if !IS_ACTIVE(CONFIG_FIDO2_CTAP_DISABLE_UP) + +/** + * @brief Flag holding information if user is present or not + */ +static bool _user_present = false; + +/** + * @brief GPIO pin to use for user presence test + */ +static gpio_t _pin; + +/** + * @brief Button callback function + */ +static void _gpio_cb(void *arg); + +int fido2_ctap_utils_init_gpio_pin(gpio_t pin, gpio_mode_t mode, gpio_flank_t flank) +{ + if (gpio_init_int(pin, mode, flank, _gpio_cb, NULL) < 0) { + return CTAP1_ERR_OTHER; + } + + _pin = pin; + + return CTAP2_OK; +} + +int fido2_ctap_utils_user_presence_test(void) +{ + int ret; + + gpio_irq_enable(_pin); + +#if !IS_ACTIVE(CONFIG_FIDO2_CTAP_DISABLE_LED) + fido2_ctap_utils_led_animation(); +#endif + + ret = _user_present ? CTAP2_OK : CTAP2_ERR_ACTION_TIMEOUT; + + gpio_irq_disable(_pin); + + _user_present = false; + return ret; +} + +static void _gpio_cb(void *arg) +{ + (void)arg; + _user_present = true; +} + +#if !IS_ACTIVE(CONFIG_FIDO2_CTAP_DISABLE_LED) +void fido2_ctap_utils_led_animation(void) +{ + uint32_t start = ztimer_now(ZTIMER_MSEC); + uint32_t diff = 0; + uint32_t delay = 500; + + while (!_user_present && diff < CTAP_UP_TIMEOUT) { +#ifdef LED0_TOGGLE + LED0_TOGGLE; +#endif +#ifdef LED1_TOGGLE + LED1_TOGGLE; +#endif +#ifdef LED3_TOGGLE + LED3_TOGGLE; +#endif +#ifdef LED2_TOGGLE + LED2_TOGGLE; +#endif + ztimer_sleep(ZTIMER_MSEC, delay); + diff = ztimer_now(ZTIMER_MSEC) - start; + } + +#ifdef LED0_TOGGLE + LED0_OFF; +#endif +#ifdef LED1_TOGGLE + LED1_OFF; +#endif +#ifdef LED3_TOGGLE + LED3_OFF; +#endif +#ifdef LED2_TOGGLE + LED2_OFF; +#endif +} +#endif /* !IS_ACTIVE(CONFIG_FIDO2_CTAP_DISABLE_LED) */ +#endif /* CONFIG_FIDO2_CTAP_DISABLE_UP */ diff --git a/sys/fido2/ctap/transport/Kconfig b/sys/fido2/ctap/transport/Kconfig new file mode 100644 index 000000000000..5ec3a7e82490 --- /dev/null +++ b/sys/fido2/ctap/transport/Kconfig @@ -0,0 +1,7 @@ +# Copyright (C) 2021 Freie Universität Berlin +# +# This file is subject to the terms and conditions of the GNU Lesser +# General Public License v2.1. See the file LICENSE in the top level +# directory for more details. + +rsource "hid/Kconfig" diff --git a/sys/fido2/ctap/transport/Makefile b/sys/fido2/ctap/transport/Makefile new file mode 100644 index 000000000000..1018d4a90b29 --- /dev/null +++ b/sys/fido2/ctap/transport/Makefile @@ -0,0 +1,7 @@ +MODULE := fido2_ctap_transport + +ifneq (,$(filter fido2_ctap_transport_hid,$(USEMODULE))) + DIRS += hid +endif + +include $(RIOTBASE)/Makefile.base diff --git a/sys/fido2/ctap/transport/ctap_transport.c b/sys/fido2/ctap/transport/ctap_transport.c new file mode 100644 index 000000000000..224d4ccfa33e --- /dev/null +++ b/sys/fido2/ctap/transport/ctap_transport.c @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2021 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup fido2_ctap_transport + * @{ + * @file + * + * @author Nils Ollrogge + * @} + */ + +#include "event/timeout.h" + +#include "fido2/ctap/transport/ctap_transport.h" +#include "fido2/ctap/ctap.h" + +#if IS_USED(MODULE_FIDO2_CTAP_TRANSPORT_HID) +#include "usb/usbus.h" +#include "usb/usbus/hid_io.h" +#include "fido2/ctap/transport/hid/ctap_hid.h" +#endif + +#define ENABLE_DEBUG (0) +#include "debug.h" + +#if IS_USED(MODULE_FIDO2_CTAP_TRANSPORT_HID) +/** + * @brief CTAPHID timeout handler + */ +static void _ctap_hid_timeout_cb(event_t *arg); +/** + * @brief CTAPHID timeout event + */ +static event_t _ctap_hid_timeout_event = { .handler = _ctap_hid_timeout_cb }; + +/** + * @brief CTAPHID event_timeout object + */ +static event_timeout_t _ctap_hid_event_timeout; +#endif + +/** + * @brief CTAP stack + */ +static char _ctap_stack[CTAP_STACKSIZE]; + +/** + * @brief CTAP transport event queue + */ +static event_queue_t _queue; + +static void *_event_loop(void *arg) +{ + (void)arg; + int ret; + + ret = fido2_ctap_init(); + + if (ret < 0) { + return NULL; + } + + event_queue_init(&_queue); + +#if IS_USED(MODULE_FIDO2_CTAP_TRANSPORT_HID) + event_timeout_init(&_ctap_hid_event_timeout, &_queue, &_ctap_hid_timeout_event); + event_timeout_set(&_ctap_hid_event_timeout, CTAP_HID_TRANSACTION_TIMEOUT); +#endif + + event_loop(&_queue); + + return NULL; +} + +#if IS_USED(MODULE_FIDO2_CTAP_TRANSPORT_HID) +static void _ctap_hid_timeout_cb(event_t *arg) +{ + (void)arg; + fido2_ctap_transport_hid_check_timeouts(); + event_timeout_set(&_ctap_hid_event_timeout, CTAP_HID_TRANSACTION_TIMEOUT); +} +#endif + +void fido2_ctap_transport_init(void) +{ +#if IS_USED(MODULE_FIDO2_CTAP_TRANSPORT_HID) + fido2_ctap_transport_hid_init(&_queue); +#endif + + int ret = thread_create(_ctap_stack, sizeof(_ctap_stack), CTAP_TRANSPORT_PRIO, + THREAD_CREATE_STACKTEST, _event_loop, NULL, + "fido2_ctap_transport_loop"); + + (void)ret; + assert(ret > 0); +} diff --git a/sys/fido2/ctap/transport/hid/Kconfig b/sys/fido2/ctap/transport/hid/Kconfig new file mode 100644 index 000000000000..b989225a11d9 --- /dev/null +++ b/sys/fido2/ctap/transport/hid/Kconfig @@ -0,0 +1,23 @@ +# Copyright (C) 2021 Freie Universität Berlin +# +# This file is subject to the terms and conditions of the GNU Lesser +# General Public License v2.1. See the file LICENSE in the top level +# directory for more details. + +menuconfig KCONFIG_USEMODULE_FIDO2_CTAP_TRANSPORT_HID + bool "FIDO2 CTAP TRANSPORT HID" + depends on USEMODULE_FIDO2_CTAP_TRANSPORT_HID + help + Configure a FIDO2 CTAP authenticator via KConfig. + +if KCONFIG_USEMODULE_FIDO2_CTAP_TRANSPORT_HID + +config FIDO2_CTAP_TRANSPORT_HID_TRANSACTION_TIMEOUT + int "CTAPHID Transaction timeout in milliseconds" + default 500 + help + A CTAPHID transaction has to be completed within a specified period + of time to prevent the authenticator from being locked by a + stalling application. + +endif # KCONFIG_USEMODULE_FIDO2_CTAP_TRANSPORT_HID diff --git a/sys/fido2/ctap/transport/hid/Makefile b/sys/fido2/ctap/transport/hid/Makefile new file mode 100644 index 000000000000..64e896042678 --- /dev/null +++ b/sys/fido2/ctap/transport/hid/Makefile @@ -0,0 +1,3 @@ +MODULE := fido2_ctap_transport_hid + +include $(RIOTBASE)/Makefile.base diff --git a/sys/fido2/ctap/transport/hid/ctap_hid.c b/sys/fido2/ctap/transport/hid/ctap_hid.c new file mode 100644 index 000000000000..65179a6e60e4 --- /dev/null +++ b/sys/fido2/ctap/transport/hid/ctap_hid.c @@ -0,0 +1,723 @@ +/* + * Copyright (C) 2021 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup fido2_ctap_transport_hid + * @{ + * @file + * + * @author Nils Ollrogge + * @} + */ + +#include + +#include "xtimer.h" +#include "usb/usbus.h" +#include "usb/usbus/hid_io.h" + +#include "fido2/ctap.h" +#include "fido2/ctap/transport/hid/ctap_hid.h" +#include "fido2/ctap/ctap_utils.h" + +#define ENABLE_DEBUG (0) +#include "debug.h" + +/** + * @brief CTAP HID report descriptor + * + * CTAP specification (version 20190130) section 8.1.8.2 + */ +const uint8_t _hid_report_desc[] = { + 0x06, 0xD0, 0xF1, /**< HID_UsagePage ( FIDO_USAGE_PAGE ) */ + 0x09, 0x01, /**< HID_Usage ( FIDO_USAGE_CTAPHID ) */ + 0xA1, 0x01, /**< HID_Collection ( HID_Application ) */ + 0x09, 0x20, /**< HID_Usage ( FIDO_USAGE_DATA_IN ) */ + 0x15, 0x00, /**< HID_LogicalMin ( 0 ) */ + 0x26, 0xFF, 0x00, /**< HID_LogicalMaxS ( 0xff ) */ + 0x75, 0x08, /**< HID_ReportSize ( 8 ) */ + 0x95, 0x40, /**< HID_ReportCount ( HID_INPUT_REPORT_BYTES ) */ + 0x81, 0x02, /**< HID_Input ( HID_Data | HID_Absolute | HID_Variable ) */ + 0x09, 0x21, /**< HID_Usage ( FIDO_USAGE_DATA_OUT ) */ + 0x15, 0x00, /**< HID_LogicalMin ( 0 ) */ + 0x26, 0xFF, 0x00, /**< HID_LogicalMaxS ( 0xff ) */ + 0x75, 0x08, /**< HID_ReportSize ( 8 ) */ + 0x95, 0x40, /**< HID_ReportCount ( HID_OUTPUT_REPORT_BYTES ) */ + 0x91, 0x02, /**< HID_Output ( HID_Data | HID_Absolute | HID_Variable ) */ + 0xC0, /**< HID_EndCollection */ +}; + +/** + * @brief CTAP_HID buffer struct + * + */ +typedef struct { + uint32_t cid; /**< channel identifier */ + uint8_t cmd; /**< CTAP_HID command */ + uint8_t buffer[CTAP_HID_BUFFER_SIZE]; /**< data buffer */ + uint16_t offset; /**< current offset into data buffer */ + int16_t seq; /**< current sequence number */ + uint16_t bcnt; /**< expected amount of bytes to be received */ + uint8_t err; /**< error type if error */ + bool is_locked; /**< buffer is locked by transaction */ + bool should_cancel; /**< flag if current transaction should be cancelled */ +} ctap_hid_state_t; + +/** + * @brief Serialize data and transmit it via USB HID layer + */ +static void _ctap_hid_write(uint8_t cmd, uint32_t cid, const void *_data, size_t size); + +/** + * @brief CTAPHID_CBOR command + * + * CTAP specification (version 20190130) section 8.1.9.1.2 + */ +static void _handle_cbor_packet(uint8_t cmd, uint32_t cid, uint8_t *buf, uint16_t bcnt); + +/** + * @brief CTAPHID_INIT command + * + * CTAP specification (version 20190130) section 8.1.9.1.3 + */ +static uint32_t _handle_init_packet(uint32_t cid, uint16_t bcnt, + const uint8_t *nonce); + +/** + * @brief CTAPHID_WINK command + * + * CTAP specification (version 20190130) section 8.1.9.2.1 + */ +static void _wink(uint32_t cid, uint8_t cmd); + +/** + * @brief Encode response to CTAPHID_INIT command + */ +static void _send_init_response(uint32_t cid_old, uint32_t cid_new, + const uint8_t *nonce); + +/** + * @brief Clear the CTAP packet buffer + */ +static void _clear_ctap_buffer(void); + +/** + * @brief Buffer packet belonging to currently processed transaction + */ +static uint8_t _buffer_pkt(const ctap_hid_pkt_t *pkt); + +/** + * @brief Send error code to cid + */ +static void _send_error_response(uint32_t cid, uint8_t err); + +/** + * @brief Refresh the last_used timestamp for this cid + */ +static int8_t _refresh_cid(uint32_t cid); + +/** + * @brief Allocate a new logical channel + */ +static int8_t _add_cid(uint32_t cid); + +/** + * @brief Delete logical channel + */ +static int8_t _delete_cid(uint32_t cid); + +/** + * @brief Check if a logical channel with cid exists + */ +static bool _cid_exists(uint32_t cid); + +/** + * @brief Parse packet length from pkt + */ +static inline uint16_t _get_packet_len(const ctap_hid_pkt_t *pkt); + +/** + * @brief Process CTAPHID transaction + */ +static void _process_transaction(event_t *arg); + +/** + * @brief Check if packet is an initialization packet + */ +static inline bool _is_init_type_pkt(const ctap_hid_pkt_t *pkt); + +/* usbus functionality */ + +/** + * @brief USB stack + */ +static char _usb_stack[USBUS_STACKSIZE]; + +/** + * @brief USBUS context + */ +static usbus_t _usbus; + +/** + * @brief Indicate if authenticator is busy processing a transactions + * + * Transactions are atomic, therefore only 1 transaction can be processed at + * once + */ +static bool _is_busy = false; + +/** + * @brief State for handling transactions + */ +static ctap_hid_state_t _state; + +/** + * @brief Logical CTAPHID channels + */ +static ctap_hid_cid_t g_cids[CTAP_HID_CIDS_MAX]; + +/** + * @brief Incremental channel ids + * + * channel id 0 is reserved + */ +static uint32_t _cid = 1; + +/** + * @brief CTAP transport layer event queue + */ +static event_queue_t *_queue; + +/** + * @brief CTAPHID event + */ +static event_t _ctap_hid_event = { .handler = _process_transaction }; + +/** + * @brief USBUS context + */ +static usbus_t _usbus; + +static void _usb_cb(void *arg) +{ + (void)arg; + + uint8_t buffer[CONFIG_USBUS_HID_INTERRUPT_EP_SIZE]; + int read; + + read = usb_hid_io_read(buffer, CONFIG_USBUS_HID_INTERRUPT_EP_SIZE); + + if (read == CONFIG_USBUS_HID_INTERRUPT_EP_SIZE) { + fido2_ctap_transport_hid_handle_packet(buffer); + } +} + +void fido2_ctap_transport_hid_init(event_queue_t *queue) +{ + _queue = queue; + usbdev_t *usbdev = usbdev_get_ctx(0); + + assert(usbdev); + usbus_init(&_usbus, usbdev); + usb_hid_io_init(&_usbus, _hid_report_desc, sizeof(_hid_report_desc)); + usb_hid_io_set_rx_cb(_usb_cb, NULL); + usbus_create(_usb_stack, sizeof(_usb_stack), USBUS_PRIO, USBUS_TNAME, &_usbus); +} + +void fido2_ctap_transport_hid_handle_packet(void *pkt_raw) +{ + ctap_hid_pkt_t *pkt = (ctap_hid_pkt_t *)pkt_raw; + uint32_t cid = pkt->cid; + uint8_t status = CTAP_HID_BUFFER_STATUS_BUFFERING; + + if (cid == 0x00) { + /* cid = 0x00 always invalid */ + _send_error_response(cid, CTAP_HID_ERR_INVALID_CHANNEL); + return; + } + else if (_is_busy) { + if (_state.cid == cid) { + /* CTAP specification (version 20190130) section 8.1.5.3 */ + if (_is_init_type_pkt(pkt)) { + if (pkt->init.cmd == CTAP_HID_COMMAND_INIT) { + /* abort */ + _clear_ctap_buffer(); + status = _buffer_pkt(pkt); + } + else if (_state.is_locked && pkt->init.cmd == + CTAP_HID_COMMAND_CANCEL) { + _state.should_cancel = true; + } + /* random init type pkt. invalid sequence of pkts */ + else { + _send_error_response(cid, CTAP_HID_ERR_INVALID_SEQ); + return; + } + } + /* packet for this cid is currently being worked */ + else if (_state.is_locked) { + _send_error_response(cid, CTAP_HID_ERR_CHANNEL_BUSY); + return; + } + else { + /* buffer cont packets */ + status = _buffer_pkt(pkt); + } + } + /* transactions are atomic. Deny all other cids if busy with one cid */ + else { + _send_error_response(cid, CTAP_HID_ERR_CHANNEL_BUSY); + return; + } + } + else { + /* first init packet received starts a transaction */ + if (_is_init_type_pkt(pkt)) { + _is_busy = true; + status = _buffer_pkt(pkt); + } + /* ignore rest */ + } + + if (status == CTAP_HID_BUFFER_STATUS_ERROR) { + _send_error_response(cid, _state.err); + _delete_cid(cid); + _clear_ctap_buffer(); + _is_busy = false; + return; + } + + /* pkt->init.bcnt bytes have been received. Transaction can now be processed */ + if (status == CTAP_HID_BUFFER_STATUS_DONE) { + _state.is_locked = 1; + event_post(_queue, &_ctap_hid_event); + _is_busy = false; + } + else { + /* refresh timestamp of cid that is being buffered */ + _refresh_cid(_state.cid); + } +} + +static uint8_t _buffer_pkt(const ctap_hid_pkt_t *pkt) +{ + if (_is_init_type_pkt(pkt)) { + /** + * broadcast cid only allowed for CTAP_HID_COMMAND_INIT + */ + if (pkt->cid == CTAP_HID_BROADCAST_CID && + pkt->init.cmd != CTAP_HID_COMMAND_INIT) { + _send_error_response(pkt->cid, CTAP_HID_ERR_INVALID_CHANNEL); + } + + /** + * received CTAP_HID_COMMAND_CANCEL while buffering packet. + * Cancel request. + */ + if (pkt->init.cmd == CTAP_HID_COMMAND_CANCEL && !_state.is_locked && + pkt->cid == _state.cid) { + + _state.err = CTAP2_ERR_KEEPALIVE_CANCEL; + return CTAP_HID_BUFFER_STATUS_ERROR; + } + + _state.bcnt = _get_packet_len(pkt); + + /* check for init transaction size described in CTAP specification + (version 20190130) section 8.1.9.1.3 */ + if (pkt->init.cmd == CTAP_HID_COMMAND_INIT && _state.bcnt != 8) { + _state.err = CTAP_HID_ERR_INVALID_LEN; + return CTAP_HID_BUFFER_STATUS_ERROR; + } + + /* don't allow transactions bigger than max buffer size */ + if (_state.bcnt > CTAP_HID_BUFFER_SIZE) { + _state.err = CTAP_HID_ERR_INVALID_LEN; + return CTAP_HID_BUFFER_STATUS_ERROR; + } + + uint16_t size = (_state.bcnt < CTAP_HID_INIT_PAYLOAD_SIZE) ? + _state.bcnt : CTAP_HID_INIT_PAYLOAD_SIZE; + _state.cmd = pkt->init.cmd; + _state.cid = pkt->cid; + _state.seq = -1; + memcpy(_state.buffer, pkt->init.payload, size); + _state.offset = size; + } + else { + int left = _state.bcnt - _state.offset; + int diff = left - CTAP_HID_CONT_PAYLOAD_SIZE; + _state.seq++; + + /* seqs have to increase sequentially */ + if (pkt->cont.seq != _state.seq) { + _state.err = CTAP_HID_ERR_INVALID_SEQ; + return CTAP_HID_BUFFER_STATUS_ERROR; + } + + /* check for potential buffer overflow */ + if (_state.offset + CTAP_HID_CONT_PAYLOAD_SIZE > CTAP_HID_BUFFER_SIZE) { + _state.err = CTAP_HID_ERR_INVALID_LEN; + return CTAP_HID_BUFFER_STATUS_ERROR; + } + + if (diff <= 0) { + memcpy(_state.buffer + _state.offset, pkt->cont.payload, left); + _state.offset += left; + } + else { + memcpy(_state.buffer + _state.offset, pkt->cont.payload, + CTAP_HID_CONT_PAYLOAD_SIZE); + _state.offset += CTAP_HID_CONT_PAYLOAD_SIZE; + } + } + + return _state.offset == _state.bcnt ? + CTAP_HID_BUFFER_STATUS_DONE : CTAP_HID_BUFFER_STATUS_BUFFERING; +} + +static void _process_transaction(event_t *arg) +{ + (void)arg; + uint8_t *buf = (uint8_t *)&_state.buffer; + uint32_t cid = _state.cid; + uint16_t bcnt = _state.bcnt; + uint8_t cmd = _state.cmd; + + if (cmd == CTAP_HID_COMMAND_INIT) { + _handle_init_packet(cid, bcnt, buf); + } + else { + /* readding deleted cid */ + if (!_cid_exists(cid) && _add_cid(cid) == -1) { + _send_error_response(cid, CTAP_HID_ERR_CHANNEL_BUSY); + } + else { + switch (cmd) { + case CTAP_HID_COMMAND_MSG: + /* not implemented as of now */ + DEBUG("CTAP_HID: MSG COMMAND \n"); + _send_error_response(cid, CTAP_HID_ERR_INVALID_CMD); + break; + case CTAP_HID_COMMAND_CBOR: + DEBUG("CTAP_HID: CBOR COMMAND \n"); + _handle_cbor_packet(cmd, cid, buf, bcnt); + break; + case CTAP_HID_COMMAND_WINK: + DEBUG("CTAP_HID: wink \n"); + _wink(cid, cmd); + break; + case CTAP_HID_COMMAND_PING: + DEBUG("CTAP_HID: PING \n"); + _ctap_hid_write(cmd, cid, buf, bcnt); + break; + case CTAP_HID_COMMAND_CANCEL: + /* + * no transaction is currently being processed, + * no reason to send cancel + */ + break; + default: + _send_error_response(cid, CTAP_HID_ERR_INVALID_CMD); + DEBUG("Ctaphid: unknown command %u \n", cmd); + } + } + } + + /* transaction done, cleanup */ + _clear_ctap_buffer(); +} + +static uint32_t _handle_init_packet(uint32_t cid, uint16_t bcnt, + const uint8_t *nonce) +{ + uint32_t cid_new = 0; + + /* cid 0 is reserved */ + if (cid == 0) { + _send_error_response(cid, CTAP_HID_ERR_INVALID_CHANNEL); + return 0; + } + /* check for len described in standard */ + if (bcnt != 8) { + _send_error_response(cid, CTAP_HID_ERR_INVALID_LEN); + return 0; + } + /* create new channel */ + if (cid == CTAP_HID_BROADCAST_CID) { + cid_new = _cid++; + + if (_add_cid(cid_new) == -1) { + _send_error_response(cid, CTAP_HID_ERR_CHANNEL_BUSY); + return 0; + } + _send_init_response(cid, cid_new, nonce); + } + /* synchronize channel */ + else { + cid_new = cid; + if (!_cid_exists(cid)) { + if (_add_cid(cid) == -1) { + /* reached cid limit */ + _send_error_response(cid, CTAP_HID_ERR_CHANNEL_BUSY); + return 0; + } + } + _send_init_response(cid, cid, nonce); + } + + return cid_new; +} + +static void _handle_cbor_packet(uint8_t cmd, uint32_t cid, uint8_t *buf, uint16_t bcnt) +{ + ctap_resp_t resp; + uint8_t err; + size_t size; + + if (bcnt == 0) { + err = CTAP_HID_ERR_INVALID_LEN; + cmd = CTAP_HID_COMMAND_ERROR; + _ctap_hid_write(cmd, cid, &err, sizeof(err)); + return; + } + + memset(&resp, 0, sizeof(ctap_resp_t)); + + ctap_req_t req; + + req.method = *buf; + req.buf = buf + 1; + req.len = bcnt - 1; + + size = fido2_ctap_handle_request(&req, &resp); + + /* transaction done, clear should_cancel flag */ + _state.should_cancel = false; + + if (resp.status == CTAP2_OK && size > 0) { + /* status + data */ + _ctap_hid_write(cmd, cid, &resp, size + sizeof(resp.status)); + } + else { + /* status only */ + _ctap_hid_write(cmd, cid, &resp.status, sizeof(resp.status)); + } +} + +static inline bool _is_init_type_pkt(const ctap_hid_pkt_t *pkt) +{ + return ((pkt->init.cmd & CTAP_HID_INIT_PACKET) == CTAP_HID_INIT_PACKET); +} + +static void _clear_ctap_buffer(void) +{ + memset(&_state, 0, sizeof(_state)); +} + +bool fido2_ctap_transport_hid_should_cancel(void) +{ + return _state.should_cancel; +} + +void fido2_ctap_transport_hid_check_timeouts(void) +{ + uint64_t now = xtimer_now_usec64(); + + for (uint8_t i = 0; i < CTAP_HID_CIDS_MAX; i++) { + /* transaction timed out because cont packets didn't arrive in time */ + if (_is_busy && g_cids[i].taken && + (now - g_cids[i].last_used) >= CTAP_HID_TRANSACTION_TIMEOUT && + _state.cid == g_cids[i].cid && !_state.is_locked) { + + _send_error_response(g_cids[i].cid, CTAP_HID_ERR_MSG_TIMEOUT); + _delete_cid(g_cids[i].cid); + _clear_ctap_buffer(); + + _is_busy = false; + } + } +} + +static int8_t _add_cid(uint32_t cid) +{ + uint64_t oldest = xtimer_now_usec64(); + int8_t index_oldest = -1; + + for (int i = 0; i < CTAP_HID_CIDS_MAX; i++) { + if (!g_cids[i].taken) { + g_cids[i].taken = true; + g_cids[i].cid = cid; + g_cids[i].last_used = xtimer_now_usec64(); + + return CTAP_HID_OK; + } + + if (g_cids[i].last_used < oldest) { + oldest = g_cids[i].last_used; + index_oldest = i; + } + } + + /* remove oldest cid to make place for a new one */ + if (index_oldest > -1) { + g_cids[index_oldest].taken = true; + g_cids[index_oldest].cid = cid; + g_cids[index_oldest].last_used = xtimer_now_usec64(); + return CTAP_HID_OK; + } + + return CTAP_HID_ERR_OTHER; +} + +static int8_t _refresh_cid(uint32_t cid) +{ + for (int i = 0; i < CTAP_HID_CIDS_MAX; i++) { + if (g_cids[i].cid == cid) { + g_cids[i].last_used = xtimer_now_usec64(); + return CTAP_HID_OK; + } + } + return CTAP_HID_ERR_OTHER; +} + +static int8_t _delete_cid(uint32_t cid) +{ + for (int i = 0; i < CTAP_HID_CIDS_MAX; i++) { + if (g_cids[i].cid == cid) { + g_cids[i].taken = false; + g_cids[i].cid = 0; + + return CTAP_HID_OK; + } + } + return CTAP_HID_ERR_OTHER; +} + +static bool _cid_exists(uint32_t cid) +{ + for (int i = 0; i < CTAP_HID_CIDS_MAX; i++) { + if (g_cids[i].cid == cid) { + return true; + } + } + return false; +} + +static inline uint16_t _get_packet_len(const ctap_hid_pkt_t *pkt) +{ + return (uint16_t)((pkt->init.bcnth << 8) | pkt->init.bcntl); +} + +static void _wink(uint32_t cid, uint8_t cmd) +{ +#if !IS_ACTIVE(CONFIG_FIDO2_CTAP_DISABLE_LED) + uint32_t delay = CTAP_HID_WINK_DELAY; + for (int i = 1; i <= 8; i++) { +#ifdef LED0_TOGGLE + LED0_TOGGLE; + xtimer_msleep(delay); +#endif +#ifdef LED1_TOGGLE + LED1_TOGGLE; + xtimer_msleep(delay); +#endif +#ifdef LED2_TOGGLE + LED2_TOGGLE; + xtimer_msleep(delay); +#endif +#ifdef LED3_TOGGLE + LED3_TOGGLE; + xtimer_msleep(delay); +#endif + delay /= 2; + } +#endif /* CONFIG_FIDO2_CTAP_DISABLE_LED */ + + _ctap_hid_write(cmd, cid, NULL, 0); +} + +static void _send_error_response(uint32_t cid, uint8_t err) +{ + DEBUG("ctap_trans_hid err resp: %02x \n", err); + _ctap_hid_write(CTAP_HID_COMMAND_ERROR, cid, &err, sizeof(err)); +} + +static void _send_init_response(uint32_t cid_old, uint32_t cid_new, + const uint8_t *nonce) +{ + ctap_hid_init_resp_t resp; + + memset(&resp, 0, sizeof(ctap_hid_init_resp_t)); + + resp.cid = cid_new; + resp.protocol_version = CTAP_HID_PROTOCOL_VERSION; + resp.version_major = 0; + resp.version_minor = 0; + resp.build_version = 0; + memcpy(resp.nonce, nonce, sizeof(resp.nonce)); + + uint8_t cmd = (CTAP_HID_INIT_PACKET | CTAP_HID_COMMAND_INIT); + + resp.capabilities = CTAP_HID_CAPABILITY_CBOR | CTAP_HID_CAPABILITY_WINK + | CTAP_HID_CAPABILITY_NMSG; + + _ctap_hid_write(cmd, cid_old, &resp, sizeof(ctap_hid_init_resp_t)); +} + +void _ctap_hid_write(uint8_t cmd, uint32_t cid, const void *_data, size_t len) +{ + const uint8_t *data = (uint8_t *)_data; + uint8_t buf[CONFIG_USBUS_HID_INTERRUPT_EP_SIZE] = { 0 }; + uint16_t bytes_written = 0; + uint8_t seq = 0; + uint8_t offset = 0; + + memcpy(buf, &cid, sizeof(cid)); + offset += sizeof(cid); + buf[offset++] = cmd; + /* high part of payload length first */ + buf[offset++] = (len & 0xff00) >> 8; + buf[offset++] = (len & 0xff) >> 0; + + if (data == NULL) { + usb_hid_io_write(buf, CONFIG_USBUS_HID_INTERRUPT_EP_SIZE); + return; + } + + for (size_t i = 0; i < len; i++) { + if (offset == 0) { + memcpy(buf, &cid, sizeof(cid)); + offset += sizeof(cid); + + /* initialization packet */ + if (bytes_written == 0) { + buf[offset++] = cmd; + buf[offset++] = (len & 0xff00) >> 8; + buf[offset++] = (len & 0xff) >> 0; + } + /* continuation packet */ + else { + buf[offset++] = seq++; + } + } + + buf[offset++] = data[i]; + bytes_written++; + + if (offset == CONFIG_USBUS_HID_INTERRUPT_EP_SIZE) { + usb_hid_io_write(buf, CONFIG_USBUS_HID_INTERRUPT_EP_SIZE); + offset = 0; + } + } + + if (offset > 0) { + memset(buf + offset, 0, CONFIG_USBUS_HID_INTERRUPT_EP_SIZE - offset); + usb_hid_io_write(buf, CONFIG_USBUS_HID_INTERRUPT_EP_SIZE); + } +} diff --git a/sys/fido2/doc.txt b/sys/fido2/doc.txt new file mode 100644 index 000000000000..74e1bf0b8f2b --- /dev/null +++ b/sys/fido2/doc.txt @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2021 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @defgroup fido2 FIDO2 - Fast Identity Online 2 + * @ingroup sys + * @brief Description of the FIDO2 CTAP implementation in RIOT + * @author Nils Ollrogge + * + * @warning This feature is experimental! + * This API is experimental and in an early state - expect changes. + * + * @warning The FIDO2 implementation currently stores private keys in plain text inside flash memory. + * + * FIDO2 is an authentication standard that seeks to solve the password problem + * by enabling passwordless authentication. Instead of using passwords to + * authenticate to web services, FIDO2 enables users to use common devices + * (authenticators) to create cryptographic credentials which are then used + * for authentication. FIDO2 consists of the [W3C Web Authentication + * specification (WebAuthn)](https://www.w3.org/TR/2019/REC-webauthn-1-20190304/) + * and the [Client to Authenticator Protocol (CTAP)](https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html). + * + * **This code implements the FIDO2 CTAP protocol.** + * + * ### General + * + * Following is an overview of the entities of this implementation and their relationships: + * + * + * +-----------+ +-----------+ +-----------+ +-----------+ + * | | | | | | | | + * | ctap_cbor | |ctap_crypto| | ctap_mem | |ctap_utils | + * | | | | | | | | + * +-----------+ +-----------+ +-----------+ +-----|-----+ + * | | | | + * | | | | + * | +---------------------------+ | + * | | | | + * |---------| ctap |--------| + * | | + * +---------------------------+ + * | + * +-------------|-------------+ + * | | + * | ctap_transport | + * | | + * +---------------------------+ + * | + * | + * +-----------+ + * | | + * | ctap_hid | + * | | + * +-----------+ + * + * + * + * **ctap_hid** + * + * USB Human Interface Device (USB HID) transport binding for CTAP (CTAPHID). + * + * Initializes the USBUS HID interface. + * + * Communicates with the USBUS USB HID interface through the USBUS HID IO interface. + * + * **ctap_transport** + * + * Initializes CTAP layer. + * + * Initializes CTAP event queue. + * + * Manages CTAP transport bindings (currently only CTAPHID). + * + * **ctap** + * + * Contains the main CTAP logic. + * + * Makes use of helpers for flash access, cryptographic operations and CBOR operations. + * + * **ctap_cbor** + * + * Helper containing functionality to parse and encode CBOR messages. Uses the [tinyCBOR](https://doc.riot-os.org/group__pkg__tinycbor.html) pkg. + * + * **ctap_crypto** + * + * Helper containing functionality for cryptographic operations. + * + * Abstraction for cryptographic operations (SHA256, HMAC-SHA-256, AES CCM). + * + * @note This abstraction exposes error return values which are currently not implemented in all cases by the RIOT crypto API. + * + * Abstraction for Elliptic curve cryptography (ECC) operations. Uses the [micro-ecc](https://api.riot-os.org/group__pkg__micro__ecc.html) pkg. + * + * Parsing of cryptographic signatures into ASN.1 DER format. Uses the [tiny-asn1](http://doc.riot-os.org/group__pkg__tiny-asn1.html) pkg. + * + * **ctap_mem** + * + * Abstraction for flash operations. Uses the RIOT [Flashpage MTD driver](http://api.riot-os.org/group__drivers__mtd__flashpage.html). + * + * Adds additional functionality to speedup flash accesses (e.g. by checking if a flash page is erased to avoid unnecessary erasures of flash pages). + * + * **ctap_utils** + * + * Abstraction for GPIO functionality and LED animations. + * + * ### Implemented features + * **FIDO2 CTAP methods** + * + * All methods defined in the FIDO2 CTAP specification are implemented. Specifically + * these are: + * * MakeCredential + * * GetAssertion + * * GetNextAssertion + * * GetInfo + * * ClientPIN + * * Reset + * + * For information about the FIDO2 CTAP methods refer to the [CTAP specification](https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticator-api). + * + * **Transport bindings** + * + * The USB Human Interface Device (USB HID) transport binding is fully implemented. + * + * For more information about the available transport bindings refer to the [CTAP specification](https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#transport-specific-bindings). + * + * **Credentials** + * + * Both types of credentials are supported. Resident and non resident. + * + * * Resident Credentials + * * Resident credentials are credentials stored on the authenticator. + * * This implementation stores resident keys in flash memory. + * @warning As of now the credentials (containing a private key) are stored + * in plain text inside flash memory + * + * * Non-resident credentials + * * Non-resident credentials are credentials that are stored by the relying + * party in encrypted form. + * * To encrypt the credentials, this implementation uses the RIOT [AES-CCM 128 + * CCM implementation](https://api.riot-os.org/ccm_8h.html). + * + * For more information about the two types of credential refer to the [WebAuthn specification](https://www.w3.org/TR/2019/REC-webauthn-1-20190304/#sctn-credential-storage-modality) + * + * **Attestation types** + * + * Currently only self attestation is supported. + * + * For more information about available attestation types refer to the [WebAuthn specification](https://www.w3.org/TR/2019/REC-webauthn-1-20190304/#sctn-attestation-types). + * + * ### Unimplemented features + * + * **Backward compatibility with FIDO1** + * + * For more information about the backward compatibility of FIDO2 to FIDO1 + * refer to the [CTAP specification](https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#u2f-interoperability). + * + * **Support of further attestation types** + * + * Specifically these are: + * * Basic Attestation + * * Attestation Certificate Authority + * * Elliptic Curve based Direct Anonymous Attestation + * + * For more information about available attestation types refer to the [WebAuthn specification](https://www.w3.org/TR/2019/REC-webauthn-1-20190304/#sctn-attestation-types). + * + * **Support of further transport bindings** + * + * Specifically these are: + * * Near Field Communication (NFC) + * * Bluetooth Low Energy (BLE) + * + * For information about the available transport bindings refer to the + * [CTAP specification](https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#transport-specific-bindings). + * + * **Extensions** + * + * For information about CTAP extensions refer to the [CTAP specification](https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#sctn-defined-extensions) + * + * **CTAP 2.1 support** + * + * None of the additions from the [CTAP 2.1 specification](https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html) are implemented. + * + * ### Testing + * + * Testing is done with the help of the fido2-tests package based on the [solokeys fido2-tests](https://github.com/solokeys/fido2-tests). + * + * For for more information about testing the FIDO2 CTAP implementation refer to the README of the test application (`/tests/sys_fido2_ctap`). + * + * **Todo** + * + * * The expected return codes of some tests were changed due to different opinions of how to interpret the CTAP2 specification. Refer to the [issue](https://github.com/solokeys/fido2-tests/issues/55) for more information. As of writing this the issue is still open. + * + * ### Configuration + * + * There are two CFLAGS which can be used to change the behavior of the FIDO2 CTAP implementation: + * + * * **FIDO2_CTAP_DISABLE_UP**: Disables the user presence test. User presence will always be set to true. + * This is helpful when running the fido2-tests as one doesn't have to click the button many times, as well as other use cases + * where no user presence test is wanted. + * * **FIDO2_CTAP_DISABLE_LED**: Disables LED animations which are used to indicate that user action is needed. + * + * The CFLAGS can either be set in the Makefile or configured via KConfig. + * + * ### Future work + * + * Future improvements / extensions to the FIDO2 CTAP implementation that should be implemented are: + * + * * Usage of secure elements if available to safely store private keys of FIDO2 credentials. + * * Usage of an extra cryptographic processor (e.g. [ARM CryptoCell](https://infocenter.nordicsemi.com/index.jsp?topic=%2Fps_nrf9160%2Fcryptocell.html)) to improve efficiency and drop dependency for ECC cryptography. + * * Support of further attestation types. + * * Support of further CTAP transport bindings. + * * Support of CTAP 2.1. + */ \ No newline at end of file diff --git a/sys/include/fido2/ctap.h b/sys/include/fido2/ctap.h new file mode 100644 index 000000000000..07a3363a0ea0 --- /dev/null +++ b/sys/include/fido2/ctap.h @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2021 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @defgroup fido2_ctap CTAP + * @ingroup fido2 + * @brief FIDO2 CTAP + * + * The Client-to-Authenticator Protocol (CTAP) is an application layer protocol + * for the communication between an authenticator and a host. + * + * @{ + * + * @file + * @brief Public FIDO2 CTAP defines, structures and function declarations + * + * @author Nils Ollrogge + */ + +#ifndef FIDO2_CTAP_H +#define FIDO2_CTAP_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief CTAP max message size + * + * CTAP specification (version 20190130) section 6 + */ +#define CTAP_MAX_MSG_SIZE 0x400 + +/** + * @brief CTAP status codes + * + * CTAP specification (version 20190130) section 6.3 + * @{ + */ +typedef enum { + CTAP2_OK = 0x00, + CTAP1_ERR_INVALID_COMMAND = 0x01, + CTAP1_ERR_INVALID_PARAMETER = 0x02, + CTAP1_ERR_INVALID_LENGTH = 0x03, + CTAP1_ERR_INVALID_SEQ = 0x04, + CTAP1_ERR_TIMEOUT = 0x05, + CTAP1_ERR_CHANNEL_BUSY = 0x06, + CTAP1_ERR_LOCK_REQUIRED = 0x0A, + CTAP1_ERR_INVALID_CHANNEL = 0x0B, + CTAP2_ERR_CBOR_PARSING = 0x10, + CTAP2_ERR_CBOR_UNEXPECTED_TYPE = 0x11, + CTAP2_ERR_INVALID_CBOR = 0x12, + CTAP2_ERR_INVALID_CBOR_TYPE = 0x13, + CTAP2_ERR_MISSING_PARAMETER = 0x14, + CTAP2_ERR_LIMIT_EXCEEDED = 0x15, + CTAP2_ERR_UNSUPPORTED_EXTENSION = 0x16, + CTAP2_ERR_TOO_MANY_ELEMENTS = 0x17, + CTAP2_ERR_EXTENSION_NOT_SUPPORTED = 0x18, + CTAP2_ERR_CREDENTIAL_EXCLUDED = 0x19, + CTAP2_ERR_CREDENTIAL_NOT_VALID = 0x20, + CTAP2_ERR_PROCESSING = 0x21, + CTAP2_ERR_INVALID_CREDENTIAL = 0x22, + CTAP2_ERR_USER_ACTION_PENDING = 0x23, + CTAP2_ERR_OPERATION_PENDING = 0x24, + CTAP2_ERR_NO_OPERATIONS = 0x25, + CTAP2_ERR_UNSUPPORTED_ALGORITHM = 0x26, + CTAP2_ERR_OPERATION_DENIED = 0x27, + CTAP2_ERR_KEY_STORE_FULL = 0x28, + CTAP2_ERR_NOT_BUSY = 0x29, + CTAP2_ERR_NO_OPERATION_PENDING = 0x2A, + CTAP2_ERR_UNSUPPORTED_OPTION = 0x2B, + CTAP2_ERR_INVALID_OPTION = 0x2C, + CTAP2_ERR_KEEPALIVE_CANCEL = 0x2D, + CTAP2_ERR_NO_CREDENTIALS = 0x2E, + CTAP2_ERR_USER_ACTION_TIMEOUT = 0x2F, + CTAP2_ERR_NOT_ALLOWED = 0x30, + CTAP2_ERR_PIN_INVALID = 0x31, + CTAP2_ERR_PIN_BLOCKED = 0x32, + CTAP2_ERR_PIN_AUTH_INVALID = 0x33, + CTAP2_ERR_PIN_AUTH_BLOCKED = 0x34, + CTAP2_ERR_PIN_NOT_SET = 0x35, + CTAP2_ERR_PIN_REQUIRED = 0x36, + CTAP2_ERR_PIN_POLICY_VIOLATION = 0x37, + CTAP2_ERR_PIN_TOKEN_EXPIRED = 0x38, + CTAP2_ERR_REQUEST_TOO_LARGE = 0x39, + CTAP2_ERR_ACTION_TIMEOUT = 0x3A, + CTAP2_ERR_UP_REQUIRED = 0x3B, + CTAP1_ERR_OTHER = 0x7F, + CTAP2_ERR_SPEC_LAST = 0xDF, + CTAP2_ERR_EXTENSION_FIRST = 0xE0, + CTAP2_ERR_EXTENSION_LAST = 0xEF, + CTAP2_ERR_VENDOR_FIRST = 0xF0, + CTAP2_ERR_VENDOR_LAST = 0xFF +} ctap_status_codes_t; +/** @} */ + +/** + * @brief CTAP request struct + * + * CTAP specification (version 20190130) section 6.1 + */ +typedef struct { + uint8_t *buf; /**< Buffer holding CBOR encoded data */ + size_t len; /**< Length of buf */ + uint8_t method; /**< CTAP method identitifer */ +} ctap_req_t; + +/** + * @brief CTAP response struct + * + * CTAP specification (version 20190130) section 6.2 + */ +typedef struct { + uint8_t status; /**< response status */ + uint8_t data[CTAP_MAX_MSG_SIZE]; /**< response data */ +} ctap_resp_t; + +/** + * @brief Initialize ctap + * + * @return 0 for success + * @return negative error code otherwise + */ +int fido2_ctap_init(void); + +/** + * @brief Handle CBOR encoded ctap request. + * + * This is a convenience function that checks @p req->method and calls the + * appropriate CTAP method handler function + * + * @param[in] req request struct + * @param[in] resp response struct + * + * @return Length of @p resp->data + */ +size_t fido2_ctap_handle_request(ctap_req_t *req, ctap_resp_t *resp); + +/** + * @brief MakeCredential method + * + * CTAP specification (version 20190130) section 5.1 + * + * @param[in] req CTAP request + * @param[in, out] resp CTAP response + * + * @return Length of @p resp->data + */ +size_t fido2_ctap_make_credential(ctap_req_t *req, ctap_resp_t *resp); + +/** + * @brief GetAssertion method + * + * CTAP specification (version 20190130) section 5.2 + * + * @param[in] req CTAP request + * @param[in, out] resp CTAP response + * + * @return Length of @p resp->data + */ +size_t fido2_ctap_get_assertion(ctap_req_t *req, ctap_resp_t *resp); + +/** + * @brief GetNextAssertion method + * + * CTAP specification (version 20190130) section 5.3 + * + * @param[in, out] resp CTAP response + * + * @return Length of @p resp->data + */ +size_t fido2_ctap_get_next_assertion(ctap_resp_t *resp); + +/** + * @brief GetInfo method + * + * CTAP specification (version 20190130) section 5.4 + * + * @param[in, out] resp CTAP response + * + * @return Length of @p resp->data + */ +size_t fido2_ctap_get_info(ctap_resp_t *resp); + +/** + * @brief ClientPIN method + * + * CTAP specification (version 20190130) section 5.5 + * + * @param[in] req CTAP request + * @param[in, out] resp CTAP response + * + * @return Length of @p resp->data + */ +size_t fido2_ctap_client_pin(ctap_req_t *req, ctap_resp_t *resp); + +/** + * @brief Reset method + * + * CTAP specification (version 20190130) section 5.6 + * + * @param[in, out] resp CTAP response + * + * @return Length of @p resp->data + */ +size_t fido2_ctap_reset(ctap_resp_t *resp); + +#ifdef __cplusplus +} +#endif +#endif /* FIDO2_CTAP_H */ +/** @} */ diff --git a/sys/include/fido2/ctap/ctap.h b/sys/include/fido2/ctap/ctap.h new file mode 100644 index 000000000000..d596af062cf5 --- /dev/null +++ b/sys/include/fido2/ctap/ctap.h @@ -0,0 +1,674 @@ +/* + * Copyright (C) 2021 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @defgroup fido2_ctap_ctap FIDO2 CTAP + * @ingroup fido2_ctap + * @brief FIDO2 CTAP + * + * The Client-to-Authenticator Protocol (CTAP) is an application layer protocol + * for the communication between an authenticator and a host. + * + * @{ + * + * @file + * @brief Internal FIDO2 CTAP defines, structures and function declarations + * + * @author Nils Ollrogge + */ + +#ifndef FIDO2_CTAP_CTAP_H +#define FIDO2_CTAP_CTAP_H + +#include + +#include "mutex.h" +#include "cbor.h" +#include "assert.h" +#include "crypto/modes/ccm.h" +#include "timex.h" +#include "board.h" + +#include "fido2/ctap.h" +#include "fido2/ctap/ctap_crypto.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Size of pin auth + * + * First 16 bytes of a HMAC-256. + * + * CTAP specification (version 20190130) section 5.5.8.2. + */ +#define CTAP_PIN_AUTH_SZ 16 + +/** + * @name CTAP methods + * + * @{ + */ +#define CTAP_MAKE_CREDENTIAL 0x01 /**< authenticatorMakeCredential method */ +#define CTAP_GET_ASSERTION 0x02 /**< authenticatorGetAssertion method */ +#define CTAP_GET_INFO 0x04 /**< authenticatorGetInfo method */ +#define CTAP_CLIENT_PIN 0x06 /**< authenticatorClientPIN method */ +#define CTAP_RESET 0x07 /**< authenticatorReset method */ +#define CTAP_GET_NEXT_ASSERTION 0x08 /**< authenticatorGetNextAssertion method */ +/** @} */ + +/** + * @name CTAP authenticator data option flags + * + * @{ + */ +#define CTAP_AUTH_DATA_FLAG_UP (1 << 0) /**< user present */ +#define CTAP_AUTH_DATA_FLAG_UV (1 << 2) /**< user verified */ +#define CTAP_AUTH_DATA_FLAG_AT (1 << 6) /**< attested credential data included */ +#define CTAP_AUTH_DATA_FLAG_ED (1 << 7) /**< extension data included */ +/** @} */ + +/** + * @name CTAP version flags + * + * @{ + */ +#define CTAP_VERSION_FLAG_FIDO_PRE 0x01 /**< FIDO 2.1 flag */ +#define CTAP_VERSION_FLAG_FIDO 0x02 /**< FIDO 2 flag */ +#define CTAP_VERSION_FLAG_U2F_V2 0x04 /**< U2F V2 flag */ +/** @} */ + +/** + * @name CTAP get info response options map CBOR key values + * + * All options are in the form key-value pairs with string IDs and + * boolean values + * @{ + */ +#define CTAP_GET_INFO_RESP_OPTIONS_ID_PLAT "plat" /**< platform device string */ +#define CTAP_GET_INFO_RESP_OPTIONS_ID_RK "rk" /**< resident key string */ +#define CTAP_GET_INFO_RESP_OPTIONS_ID_CLIENT_PIN "clientPin" /**< client PIN string */ +#define CTAP_GET_INFO_RESP_OPTIONS_ID_UP "up" /**< user presence string */ +#define CTAP_GET_INFO_RESP_OPTIONS_ID_UV "uv" /**< user verification string */ +/** @} */ + +/** + * @name CTAP get info options flags + * + * @{ + */ +#define CTAP_INFO_OPTIONS_FLAG_PLAT (1 << 0) /**< platform device flag */ +#define CTAP_INFO_OPTIONS_FLAG_RK (1 << 1) /**< resident key flag */ +#define CTAP_INFO_OPTIONS_FLAG_CLIENT_PIN (1 << 2) /**< clientPIN flag */ +#define CTAP_INFO_OPTIONS_FLAG_UP (1 << 3) /**< user presence flag */ +#define CTAP_INFO_OPTIONS_FLAG_UV (1 << 4) /**< user verification flag */ +/** @} */ + +/** + * @name CTAP Client PIN request subCommand CBOR key values + * + * @{ + */ +#define CTAP_CP_REQ_SUB_COMMAND_GET_RETRIES 0x01 /**< getRetries subCommand */ +#define CTAP_CP_REQ_SUB_COMMAND_GET_KEY_AGREEMENT 0x02 /**< getKeyAgreement subCommand */ +#define CTAP_CP_REQ_SUB_COMMAND_SET_PIN 0x03 /**< setPIN subCommand */ +#define CTAP_CP_REQ_SUB_COMMAND_CHANGE_PIN 0x04 /**< changePIN subCommand */ +#define CTAP_CP_REQ_SUB_COMMAND_GET_PIN_TOKEN 0x05 /**< getPinToken subCommand */ +/** @} */ + +/** + * @brief CTAP thread stack size + */ +#ifdef CONFIG_FIDO2_CTAP_STACK_SIZE +#define CTAP_STACKSIZE CONFIG_FIDO2_CTAP_STACK_SIZE +#else +#define CTAP_STACKSIZE 15000 +#endif + +#if !IS_ACTIVE(CONFIG_FIDO2_CTAP_DISABLE_UP) +/** + * @brief CTAP user presence button + */ +#if defined(CONFIG_FIDO2_CTAP_UP_BUTTON_PORT) && defined(CONFIG_FIDO2_CTAP_UP_BUTTON_PIN) && \ + (CONFIG_FIDO2_CTAP_UP_BUTTON_PORT >= 0) && (CONFIG_FIDO2_CTAP_UP_BUTTON_PIN >= 0) +#define CTAP_UP_BUTTON GPIO_PIN(CONFIG_FIDO2_CTAP_UP_BUTTON_PORT, CONFIG_FIDO2_CTAP_UP_BUTTON_PIN) +#else +/* set default button if no button is configured */ +#ifdef BTN0_PIN +#define CTAP_UP_BUTTON BTN0_PIN +/* if no button available disable UP test */ +#else +#define CONFIG_FIDO2_CTAP_DISABLE_UP 1 +#endif +#endif + +/** + * @brief CTAP user presence button mode + */ +#if IS_ACTIVE(CONFIG_FIDO2_CTAP_UP_BUTTON_MODE_IN_PU) +#define CTAP_UP_BUTTON_MODE GPIO_IN_PU +#elif IS_ACTIVE(CONFIG_FIDO2_CTAP_UP_BUTTON_MODE_IN_PD) +#define CTAP_UP_BUTTON_MODE GPIO_IN_PD +#elif IS_ACTIVE(CONFIG_FIDO2_CTAP_UP_BUTTON_MODE_IN) +#define CTAP_UP_BUTTON_MODE GPIO_IN +#else +#define CTAP_UP_BUTTON_MODE GPIO_IN_PU +#endif + +/** + * @brief CTAP user presence button flank + */ +#if IS_ACTIVE(CONFIG_FIDO2_CTAP_UP_BUTTON_FLANK_FALLING) +#define CTAP_UP_BUTTON_FLANK GPIO_FALLING +#elif IS_ACTIVE(CONFIG_FIDO2_CTAP_UP_BUTTON_FLANK_RISING) +#define CTAP_UP_BUTTON_FLANK GPIO_RISING +#elif IS_ACTIVE(CONFIG_FIDO2_CTAP_UP_BUTTON_FLANK_BOTH) +#define CTAP_UP_BUTTON_FLANK GPIO_BOTH +#else +#define CTAP_UP_BUTTON_FLANK GPIO_FALLING +#endif + +#endif /* !IS_ACTIVE(CONFIG_FIDO2_CTAP_DISABLE_UP) */ + +/** + * @brief Max size of relying party name + */ +#define CTAP_RP_MAX_NAME_SIZE 32 + +/** + * @brief Max size of username including null character + */ +#define CTAP_USER_MAX_NAME_SIZE 64 + 1 + +/** + * @brief Max size of user id + */ +#define CTAP_USER_ID_MAX_SIZE 64 + +/** + * @brief Max size of a domain name including null character + */ +#define CTAP_DOMAIN_NAME_MAX_SIZE 253 + 1 + +/** + * @brief Max size of icon including null character + */ +#define CTAP_ICON_MAX_SIZE 128 + 1 + +/** + * @brief PIN min size + */ +#define CTAP_PIN_MIN_SIZE 4 + +/** + * @brief Encrypted newPin min size + * + * Encrypted PIN is padded with trailing 0x00 bytes to a minimum length + * of 64 in order to prevent leak of PIN length. + */ +#define CTAP_PIN_ENC_MIN_SIZE 64 + +/** + * @brief Encrypted newPin max size + * + */ +#define CTAP_PIN_ENC_MAX_SIZE 256 + +/** + * @brief PIN max size + */ +#define CTAP_PIN_MAX_SIZE 64 + +/** + * @brief Max total consecutive incorrect PIN attempts + */ +#define CTAP_PIN_MAX_ATTS 8 + +/** + * @brief Max consecutive incorrect PIN attempts for 1 boot cycle + */ +#define CTAP_PIN_MAX_ATTS_BOOT 3 + +/** + * @brief PIN protocol version + */ +#define CTAP_PIN_PROT_VER 1 + +/** + * @brief Total number of supported PIN protocol versions + */ +#define CTAP_AMT_SUP_PIN_VER 1 + +/** + * @brief Size of pin token + * + * Needs to be a multiple of 16 bytes (AES block length). + */ +#define CTAP_PIN_TOKEN_SZ 16 + +/** + * @brief Size of key used to encrypt credential + * + * Needed if authenticator is unable to store resident keys. + * See webauthn specification (version 20190304) section 4 (Credential ID) + * for details. + */ +#define CTAP_CRED_KEY_LEN 16 + +/** + * @brief AES_CCM_L parameter + * + * L has to be between 2 and 8. Value of 2 means that message has to be + * in the range 0 <= l(m) < 2^(16) = 65536. + * This should always be sufficient to send an encrypted resident key. + */ +#define CTAP_AES_CCM_L 2 + +/** + * @brief AES CCM nonce size + */ +#define CTAP_AES_CCM_NONCE_SIZE (15 - CTAP_AES_CCM_L) + +/** + * @brief Total size of AES CCM credential id + * + * Size of encrypted resident key = resident key - cred id - has_nonce + */ +#define CTAP_CREDENTIAL_ID_ENC_SIZE (sizeof(struct ctap_resident_key) - \ + sizeof(((struct ctap_resident_key *)0)-> \ + cred_desc.cred_id) - \ + sizeof(((struct ctap_resident_key *)0)-> \ + cred_desc.has_nonce)) + +/** + * @brief Timeout for user presence test + */ +#ifdef CONFIG_FIDO2_CTAP_UP_TIMEOUT +#define CTAP_UP_TIMEOUT (CONFIG_FIDO2_CTAP_UP_TIMEOUT * MS_PER_SEC) +#else +#define CTAP_UP_TIMEOUT (15 * MS_PER_SEC) +#endif + +/** + * @brief Max time between call to get_assertion or get_next_assertion until + * error is returned + */ +#define CTAP_GET_NEXT_ASSERTION_TIMEOUT (30 * MS_PER_SEC) + +/** + * 128 bit identifier of authenticator + */ +#ifdef CONFIG_FIDO2_CTAP_DEVICE_AAGUID +#define CTAP_AAGUID CONFIG_FIDO2_CTAP_DEVICE_AAGUID +#else +/* randomly generated fallback value */ +#define CTAP_AAGUID "9c295865fa2c36b705a42320af9c8f16" +#endif + +/** + * @name CTAP credential types + * + * @{ + */ +#define CTAP_PUB_KEY_CRED_PUB_KEY 0x01 /**< public key credential type */ +#define CTAP_PUB_KEY_CRED_UNKNOWN 0x02 /**< unknown credential type */ +/** @} */ + +/** + * @name CTAP COSE key CBOR map key values + * + * @{ + */ +#define CTAP_COSE_KEY_LABEL_KTY 1 /**< key type identifier */ +#define CTAP_COSE_KEY_LABEL_ALG 3 /**< algorithm identifier */ +#define CTAP_COSE_KEY_LABEL_CRV -1 /**< elliptic curve identifier */ +#define CTAP_COSE_KEY_LABEL_X -2 /**< x coordinate */ +#define CTAP_COSE_KEY_LABEL_Y -3 /**< y coordinate */ +#define CTAP_COSE_KEY_KTY_EC2 2 /**< 2 coordinate elliptic curve key identifier */ +#define CTAP_COSE_KEY_CRV_P256 1 /**< secp256r1 elliptic curve key identifier */ +/** @} */ + +/** + * @brief CTAP size of authenticator AAGUID in bytes + */ +#define CTAP_AAGUID_SIZE 16 + +/** + * @brief CTAP COSE Algorithms registry identifier for ES256 + */ +#define CTAP_COSE_ALG_ES256 -7 + +/** + * @brief CTAP COSE Algorithms registry identifier for ECDH ES HKDF 256 + */ +#define CTAP_COSE_ALG_ECDH_ES_HKDF_256 -25 + +/** + * @brief CTAP size of credential id + * + */ +#define CTAP_CREDENTIAL_ID_SIZE 16U + +/** + * @brief CTAP state initialized marker + * + * Used to check if authenticator state has already been initialized when + * reading data from flash. + */ +#define CTAP_INITIALIZED_MARKER 0x4e + +/** + * @brief Max size of allow list + */ +#define CTAP_MAX_EXCLUDE_LIST_SIZE 0x10 + +/** + * @brief CTAP cred struct forward declaration + */ +typedef struct ctap_cred_desc ctap_cred_desc_t; + +/** + * @brief Alternative CTAP cred struct forward declaration + */ +typedef struct ctap_cred_desc_alt ctap_cred_desc_alt_t; + +/** + * @brief CTAP resident key credential forward declaration + */ +typedef struct ctap_resident_key ctap_resident_key_t; + +/** + * @brief CTAP authenticator config struct + */ +typedef struct { + uint8_t aaguid[CTAP_AAGUID_SIZE]; /**< AAGUID of device */ + uint8_t options; /**< options */ +} ctap_config_t; + +/** + * @brief CTAP state struct + * + * state of authenticator. Stored in flash memory + */ +typedef struct { + ctap_config_t config; /**< configuration of authenticator */ + ctap_crypto_key_agreement_key_t ag_key; /**< Platform key agreement key */ + int rem_pin_att; /**< remaining PIN tries */ + uint16_t rk_amount_stored; /**< total number of resident keys stored on device */ + uint8_t initialized_marker; /**< CTAP initialized marker */ + uint8_t pin_hash[SHA256_DIGEST_LENGTH / 2]; /**< LEFT(SHA-256(pin), 16) */ + uint8_t cred_key[CTAP_CRED_KEY_LEN]; /**< AES CCM encryption key for cred */ + bool cred_key_is_initialized; /**< AES CCM key initialized flag */ + bool pin_is_set; /**< PIN is set or not */ +} ctap_state_t; + +/** + * @brief CTAP options struct + */ +typedef struct { + int rk; /**< resident key */ + int uv; /**< user verification */ + int up; /**< user presence */ +} ctap_options_t; + +/** + * @brief CTAP user entity struct + */ +typedef struct { + uint8_t id[CTAP_USER_ID_MAX_SIZE]; /**< RP-specific user account id */ + uint8_t id_len; /**< actual length of user id */ + uint8_t name[CTAP_USER_MAX_NAME_SIZE]; /**< user name */ + uint8_t display_name[CTAP_USER_MAX_NAME_SIZE]; /**< user display name */ + uint8_t icon[CTAP_DOMAIN_NAME_MAX_SIZE]; /**< URL referencing user icon image */ +} ctap_user_ent_t; + +/** + * @brief CTAP relying party entity struct + * + */ +typedef struct { + uint8_t id[CTAP_DOMAIN_NAME_MAX_SIZE + 1]; /**< relying party identifier */ + uint8_t id_len; /**< actual length of + relying party identifier */ + uint8_t name[CTAP_RP_MAX_NAME_SIZE + 1]; /**< human friendly relying + party name */ + uint8_t icon[CTAP_DOMAIN_NAME_MAX_SIZE + 1]; /**< URL referencing relying + party icon image */ +} ctap_rp_ent_t; + +/** + * @brief CTAP cose key struct + * + * https://www.iana.org/assignments/cose/cose.xhtml + */ +typedef struct { + ctap_crypto_pub_key_t pubkey; /**< public key */ + int kty; /**< identification of key type */ + int crv; /**< EC identifier */ + int32_t alg_type; /**< COSEAlgorithmIdentifier */ + uint8_t cred_type; /**< type of credential */ +} ctap_public_key_cose_t; + +/** + * @brief CTAP credential description struct + * + * Webauthn specification (version 20190304) section 5.8.3 + * + * @warning reordering this struct will break the AES CCM encryption of + * resident keys. + */ +struct ctap_cred_desc { + uint8_t cred_type; /**< type of credential */ + union { + uint8_t cred_id[CTAP_CREDENTIAL_ID_SIZE]; /**< credential identifier */ + uint8_t nonce[CTAP_AES_CCM_NONCE_SIZE]; /**< CTAP AES CCM nonce */ + }; + bool has_nonce; /**< Indicate if nonce or + cred_id */ +}; + +/** + * @brief CTAP resident key struct + * + * A resident key is a fido2 credential that is being stored on the + * authenticator. + */ +struct __attribute__((packed)) ctap_resident_key { + uint8_t rp_id_hash[SHA256_DIGEST_LENGTH]; /**< hash of rp domain string */ + uint8_t user_id[CTAP_USER_ID_MAX_SIZE]; /**< id of user */ + uint8_t user_id_len; /**< length of the user id */ + uint8_t priv_key[CTAP_CRYPTO_KEY_SIZE]; /**< private key */ + uint32_t sign_count; /**< signature counter. + See webauthn specification + (version 20190304) section 6.1.1 + for details. */ + uint32_t creation_time; /**< timestamp for when credential + was created */ + ctap_cred_desc_t cred_desc; /**< credential descriptor */ +}; + +/** + * @brief CTAP credential ID + * + * Credential ID can either be 16 random bytes or the encrypted resident + * key. (AES CCM cipher + mac + nonce used) + */ +typedef struct __attribute__((packed)) { + uint8_t id[CTAP_CREDENTIAL_ID_ENC_SIZE]; /**< id */ + uint8_t mac[CCM_MAC_MAX_LEN]; /**< AES CCM MAC */ + uint8_t nonce[CTAP_AES_CCM_NONCE_SIZE]; /**< AES CCM nonce */ +} ctap_cred_id_t; + +/** + * @brief CTAP credential description alternative struct + * + * This struct is used when parsing an allow or exclude list. + */ +struct ctap_cred_desc_alt { + uint8_t cred_type; /**< type of credential */ + ctap_cred_id_t cred_id; /**< credential id */ +}; + +/** + * @brief CTAP make credential request struct + */ +typedef struct { + ctap_cred_desc_alt_t exclude_list[CTAP_MAX_EXCLUDE_LIST_SIZE]; /**< exclude list */ + size_t exclude_list_len; /**< length of CBOR exclude list array */ + ctap_rp_ent_t rp; /**< relying party */ + ctap_user_ent_t user; /**< user */ + ctap_options_t options; /**< parameters to influence authenticator operation */ + uint8_t client_data_hash[SHA256_DIGEST_LENGTH]; /**< SHA-256 hash of JSON serialized client data */ + uint8_t pin_auth[CTAP_PIN_AUTH_SZ]; /**< pin_auth if PIN is set */ + size_t pin_auth_len; /**< pin_auth len */ + int32_t alg_type; /**< cryptographic algorithm identifier */ + bool pin_auth_present; /**< pin_auth present */ + uint8_t pin_protocol; /**< PIN protocol version */ + uint8_t cred_type; /**< type of credential */ +} ctap_make_credential_req_t; + +/** + * @brief CTAP get assertion request struct + */ +typedef struct { + ctap_options_t options; /**< parameters to influence authenticator operation */ + ctap_cred_desc_alt_t allow_list[CTAP_MAX_EXCLUDE_LIST_SIZE]; /**< allow list */ + uint8_t client_data_hash[SHA256_DIGEST_LENGTH]; /**< SHA-256 hash of JSON serialized client data */ + uint8_t rp_id[CTAP_DOMAIN_NAME_MAX_SIZE + 1]; /**< Relying Party Identifier */ + uint8_t rp_id_len; /**< Actual Length of Relying Party Identifier */ + uint8_t allow_list_len; /**< length of CBOR allow list array */ + uint8_t pin_auth[CTAP_PIN_AUTH_SZ]; /**< pin_auth if PIN is set */ + size_t pin_auth_len; /**< pin_auth length */ + uint8_t pin_protocol; /**< PIN protocol version */ + bool pin_auth_present; /**< indicate if pin_auth present */ +} ctap_get_assertion_req_t; + +/** + * @brief CTAP client pin request struct + */ +typedef struct { + ctap_public_key_cose_t key_agreement; /**< public key of platform_key_agreement_key*/ + uint16_t new_pin_enc_size; /**< size of encrypted new pin */ + uint8_t pin_auth[CTAP_PIN_AUTH_SZ]; /**< first 16 bytes of HMAC-SHA-256 of encrypted contents */ + uint8_t new_pin_enc[CTAP_PIN_ENC_MAX_SIZE]; /**< Encrypted new PIN using sharedSecret. */ + uint8_t pin_hash_enc[SHA256_DIGEST_LENGTH / 2]; /**< Encrypted first 16 bytes of SHA-256 of PIN using sharedSecret. */ + uint8_t sub_command; /**< authenticator Client PIN sub command */ + uint8_t pin_protocol; /**< PIN protocol version chosen by the client */ + bool pin_hash_enc_present; /**< indicate pin_hash_enc is present */ + bool pin_auth_present; /**< indicate if pin_auth present */ + bool key_agreement_present; /**< indicate if key_agreement present */ +} ctap_client_pin_req_t; + +/** + * @brief CTAP attested credential data header struct + * + * Defined for easier serialization + */ +typedef struct __attribute__((packed)){ + uint8_t aaguid[CTAP_AAGUID_SIZE]; /**< authenticator aaguid */ + uint8_t cred_len_h; /**< higher byte of credential length */ + uint8_t cred_len_l; /**< lower byte of credential length */ + ctap_cred_id_t cred_id; /**< credential id */ +} ctap_attested_cred_data_header_t; + +/** + * @brief CTAP attested credential data struct + */ +typedef struct { + ctap_attested_cred_data_header_t header; /**< attested credential data header */ + ctap_public_key_cose_t key; /**< cose key */ +} ctap_attested_cred_data_t; + +/** + * @brief CTAP authenticator data header struct + * + * Defined for easier serialization + */ +typedef struct __attribute__((packed)){ + uint8_t rp_id_hash[SHA256_DIGEST_LENGTH]; /**< hash of relying party id */ + uint8_t flags; /**< flags indicating result of user verification */ + uint32_t sign_count; /**< sign count of credential */ +} ctap_auth_data_header_t; + +/** + * @brief CTAP authenticator data struct + */ +typedef struct { + ctap_auth_data_header_t header; /**< auth data header */ + ctap_attested_cred_data_t attested_cred_data; /**< attested credential data */ +} ctap_auth_data_t; + +/** + * @brief CTAP info struct + */ +typedef struct { + uint16_t max_msg_size; /**< max message size */ + uint8_t aaguid[CTAP_AAGUID_SIZE]; /**< AAGUID */ + uint8_t versions; /**< supported versions of FIDO */ + uint8_t options; /**< supported options */ + uint8_t pin_protocol; /**< supported PIN protocol versions */ + bool pin_is_set; /**< PIN is set or not */ +} ctap_info_t; + +/** + * @brief Create signature from authenticator data + * + * Used for attestation and assertion statement. + * + * @param[in] auth_data authenticator data + * @param[in] auth_data_len length of @p auth_data + * @param[in] client_data_hash hash of client data sent by relying party in request + * @param[in] rk resident key used to sign the data + * @param[in] sig signature buffer + * @param[in] sig_len length of @p sig + * + * @return @ref ctap_status_codes_t + */ +int fido2_ctap_get_sig(const uint8_t *auth_data, size_t auth_data_len, + const uint8_t *client_data_hash, + const ctap_resident_key_t *rk, + uint8_t *sig, size_t *sig_len); + +/** + * @brief Check if requested algorithm is supported + * + * @param[in] cred_type type of credential + * @param[in] alg_type cryptographic algorithm identifier + * + * @return true if algorithm is supported + * @return false otherwise + */ +bool fido2_ctap_cred_params_supported(uint8_t cred_type, int32_t alg_type); + +/** + * @brief Encrypt resident key with AES CCM + * + * @param[in] rk type of credential + * @param[in] nonce CCM nonce + * @param[in] nonce_len length of @p nonce + * @param[in] id credential id struct storing encrypted resident key + * + * @return @ref ctap_status_codes_t + */ +int fido2_ctap_encrypt_rk(ctap_resident_key_t *rk, uint8_t *nonce, + size_t nonce_len, ctap_cred_id_t *id); + +/** + * @brief Check if PIN has been set on authenticator + * + * @return true if PIN has been set + * @return false otherwise + */ +bool fido2_ctap_pin_is_set(void); + +#ifdef __cplusplus +} +#endif +#endif /* FIDO2_CTAP_CTAP_H */ +/** @} */ diff --git a/sys/include/fido2/ctap/ctap_cbor.h b/sys/include/fido2/ctap/ctap_cbor.h new file mode 100644 index 000000000000..a5063241701e --- /dev/null +++ b/sys/include/fido2/ctap/ctap_cbor.h @@ -0,0 +1,338 @@ +/* + * Copyright (C) 2021 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @defgroup fido2_ctap_cbor FIDO2 CTAP CBOR + * @ingroup fido2_ctap + * @brief FIDO2 CTAP CBOR helper + * + * @{ + * + * @file + * @brief CTAP CBOR helper function declarations + * + * @author Nils Ollrogge + */ + +#ifndef FIDO2_CTAP_CTAP_CBOR_H +#define FIDO2_CTAP_CTAP_CBOR_H + +#include "fido2/ctap/ctap.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief CBOR map size of encoded getInfo response + */ +#define CTAP_CBOR_INFO_MAP_SZ 0x06 + +/** + * @brief CBOR map size of encoded attestation + */ +#define CTAP_CBOR_ATTESTATION_MAP_SZ 0x03 + +/** + * @brief CBOR map size of encoded attestation statement + */ +#define CTAP_CBOR_ATTESTATION_STMT_MAP_SZ 0x02 + +/** + * @brief CBOR map size of encoded credential description + */ +#define CTAP_CBOR_CRED_DESC_MAP_SZ 0x02 + +/** + * @brief CBOR map size of encoded pinToken + */ +#define CTAP_CBOR_PIN_TOKEN_MAP_SZ 0x01 + +/** + * @brief CBOR map size of encoded user entity + */ +#define CTAP_CBOR_USER_ENTITY_MAP_SZ 0x01 + +/** + * @brief CBOR map size of public key encoded in COSE format + */ +#define CTAP_CBOR_COSE_KEY_MAP_SZ 0x05 + +/** + * @brief CBOR map size of encoded clientPIN keyAgreement + */ +#define CTAP_CBOR_KEY_AGREEMENT_MAP_SZ 0x01 + +/** + * @brief CBOR map size of encoded clientPIN retries + */ +#define CTAP_CBOR_RETRIES_MAP_SZ 0x01 + +/** + * @brief Attestation statement data buffer size + */ +#define CTAP_CBOR_ATT_STMT_AUTH_DATA_SZ 0x134 + +/** + * @brief Max length of string key in CBOR map + */ +#define CTAP_CBOR_MAP_MAX_KEY_LEN 0x10 + +/** + * @brief Max length of PublicKeyCredentialType string + */ +#define CTAP_CBOR_MAX_CREDENTIAL_TYPE_LEN 0x10 + +/** + * @name CTAP CBOR map key string values + * @{ + */ +#define CTAP_CBOR_STR_PACKED "packed" /**< packed key string */ +#define CTAP_CBOR_STR_ALG "alg" /**< algorithm key string */ +#define CTAP_CBOR_STR_SIG "sig" /**< signature key string */ +#define CTAP_CBOR_STR_ID "id" /**< id key string */ +#define CTAP_CBOR_STR_TYPE "type" /**< type key string */ +#define CTAP_CBOR_STR_PUBLIC_KEY "public-key" /**< public-key key string */ +#define CTAP_CBOR_STR_USER_VERIFIED "uv" /**< user verification key string */ +#define CTAP_CBOR_STR_USER_PRESENT "up" /**< user presence key string */ +#define CTAP_CBOR_STR_RESIDENT_KEY "rk" /**< resident key key string */ +#define CTAP_CBOR_STR_NAME "name" /**< name key string */ +#define CTAP_CBOR_STR_ICON "icon" /**< icon key string */ +#define CTAP_CBOR_DISPLAY_NAME "displayName" /**< displayName key string */ +/** @} */ + +/** + * @name CTAP Client PIN response CBOR map key values + * + * @{ + */ +#define CTAP_CBOR_CP_RESP_KEY_AGREEMENT 0x01 /**< KeyAgreement key value */ +#define CTAP_CBOR_CP_PIN_TOKEN_RESP 0x02 /**< pinToken key value */ +#define CTAP_CBOR_CP_RETRIES_RESP 0x03 /**< retries key value */ +/** @} */ + +/** + * @name CTAP make credential request CBOR key values + * + * @{ + */ +#define CTAP_CBOR_MC_REQ_CLIENT_DATA_HASH 0x01 /**< clientDataHash key value */ +#define CTAP_CBOR_MC_REQ_RP 0x02 /**< relying party key value */ +#define CTAP_CBOR_MC_REQ_USER 0x03 /**< user key value */ +#define CTAP_CBOR_MC_REQ_PUB_KEY_CRED_PARAMS 0x04 /**< pubKeyCredParams key value */ +#define CTAP_CBOR_MC_REQ_EXCLUDE_LIST 0x05 /**< excludeList key value */ +#define CTAP_CBOR_MC_REQ_EXTENSIONS 0x06 /**< extensions key value */ +#define CTAP_CBOR_MC_REQ_OPTIONS 0x07 /**< options key value */ +#define CTAP_CBOR_MC_REQ_PIN_AUTH 0x08 /**< pinAuth key value */ +#define CTAP_CBOR_MC_REQ_PIN_PROTOCOL 0x09 /**< pinProtocol key value */ +/** @} */ + +/** + * @name CTAP get info response CBOR key values + * + * @{ + */ +#define CTAP_CBOR_GET_INFO_RESP_VERSIONS 0x01 /**< versions key value */ +#define CTAP_CBOR_GET_INFO_RESP_EXTENSIONS 0x02 /**< extensions key value */ +#define CTAP_CBOR_GET_INFO_RESP_AAGUID 0x03 /**< AAGUID key value */ +#define CTAP_CBOR_GET_INFO_RESP_OPTIONS 0x04 /**< options key value */ +#define CTAP_CBOR_GET_INFO_RESP_MAX_MSG_SIZE 0x05 /**< maxMsgSize key value */ +#define CTAP_CBOR_GET_INFO_RESP_PIN_PROTOCOLS 0x06 /**< pinProtocol key value */ +/** @} */ + +/** + * @name CTAP version strings + * @{ + */ +#define CTAP_CBOR_VERSION_STRING_FIDO_PRE "FIDO_2_1_PRE" /**< FIDO 2.1 flag */ +#define CTAP_CBOR_VERSION_STRING_FIDO "FIDO_2_0" /**< FIDO 2 flag */ +#define CTAP_CBOR_VERSION_STRING_U2F_V2 "U2F_V2" /**< U2F V2 flag */ +/** @} */ + +/** + * @name CTAP make credential response CBOR key values + * + * @{ + */ +#define CTAP_CBOR_MC_RESP_FMT 0x01 /**< attestation statement format identifier key value */ +#define CTAP_CBOR_MC_RESP_AUTH_DATA 0x02 /**< authData key value */ +#define CTAP_CBOR_MC_RESP_ATT_STMT 0x03 /**< attestation statement key value */ +/** @} */ + +/** + * @name CTAP get assertion request CBOR key values + * + * @{ + */ +#define CTAP_CBOR_GA_REQ_RP_ID 0x01 /**< relying party identifier key value */ +#define CTAP_CBOR_GA_REQ_CLIENT_DATA_HASH 0x02 /**< clientDataHash key value */ +#define CTAP_CBOR_GA_REQ_ALLOW_LIST 0x03 /**< allowList key value */ +#define CTAP_CBOR_GA_REQ_EXTENSIONS 0x04 /**< extensions key value */ +#define CTAP_CBOR_GA_REQ_OPTIONS 0x05 /**< options key value */ +#define CTAP_CBOR_GA_REQ_PIN_AUTH 0x06 /**< pinAuth key value */ +#define CTAP_CBOR_GA_REQ_PIN_PROTOCOL 0x07 /**< pinProtocol key value */ +/** @} */ + +/** + * @name CTAP get assertion response CBOR key values + * + * @{ + */ +#define CTAP_CBOR_GA_RESP_CREDENTIAL 0x01 /**< credential key value */ +#define CTAP_CBOR_GA_RESP_AUTH_DATA 0x02 /**< authData key value */ +#define CTAP_CBOR_GA_RESP_SIGNATURE 0x03 /**< signature key value */ +#define CTAP_CBOR_GA_RESP_USER 0x04 /**< user key value */ +#define CTAP_CBOR_GA_RESP_NUMBER_OF_CREDENTIALS 0x05 /**< numberOfCredentials key value */ +/** @} */ + +/** + * @name CTAP Client PIN request CBOR key values + * + * @{ + */ +#define CTAP_CBOR_CP_REQ_PIN_PROTOCOL 0x01 /**< pinProtocol key value */ +#define CTAP_CBOR_CP_REQ_SUB_COMMAND 0x02 /**< subCommand key value */ +#define CTAP_CBOR_CP_REQ_KEY_AGREEMENT 0x03 /**< keyAgreement key value */ +#define CTAP_CBOR_CP_REQ_PIN_AUTH 0x04 /**< pinAuth key value */ +#define CTAP_CBOR_CP_REQ_NEW_PIN_ENC 0x05 /**< newPinEnc key value */ +#define CTAP_CBOR_CP_REQ_PIN_HASH_ENC 0x06 /**< pinHashEnc key value */ +/** @} */ + +/** + * @brief Parse MakeCredential method + * + * CTAP specification (version 20190130) section 5.1 + * + * @param[in] req struct to parse into + * @param[in] req_raw raw request + * @param[in] len length of @p req_raw + * + * @return @ref ctap_status_codes_t + */ +int fido2_ctap_cbor_parse_make_credential_req(ctap_make_credential_req_t *req, + const uint8_t *req_raw, size_t len); + +/** + * @brief Parse GetAssertion method + * + * CTAP specification (version 20190130) section 5.2 + * + * @param[in] req struct to parse into + * @param[in] req_raw raw request + * @param[in] len length of @p req_raw + * + * @return @ref ctap_status_codes_t + */ +int fido2_ctap_cbor_parse_get_assertion_req(ctap_get_assertion_req_t *req, + const uint8_t *req_raw, size_t len); + +/** + * @brief Encode CBOR info map + * + * CTAP specification (version 20190130) section 5.4 + * + * @param[in] info information about capabilities + * + * @return @ref ctap_status_codes_t + */ + +int fido2_ctap_cbor_encode_info(const ctap_info_t *info); +/** + * @brief Parse ClientPIN method + * + * CTAP specification (version 20190130) section 5.5 + * + * @param[in] req struct to parse into + * @param[in] req_raw raw request + * @param[in] len length of @p req_raw + * + * @return @ref ctap_status_codes_t + */ +int fido2_ctap_cbor_parse_client_pin_req(ctap_client_pin_req_t *req, + const uint8_t *req_raw, size_t len); +/** + * @brief Encode attestation object + * + * Webauthn specification (version 20190304) section 6.5 + * + * @param[in] auth_data authenticator data + * @param[in] client_data_hash SHA-256 hash of JSON serialized client data + * @param[in] rk resident key + * + * @return @ref ctap_status_codes_t + */ +int fido2_ctap_cbor_encode_attestation_object(const ctap_auth_data_t *auth_data, + const uint8_t *client_data_hash, + ctap_resident_key_t *rk); + +/** + * @brief Encode assertion object + * + * CTAP specification (version 20190130) section 5.2 + * + * @param[in] auth_data authenticator data header + * @param[in] client_data_hash SHA-256 hash of JSON serialized client data + * @param[in] rk resident key + * @param[in] valid_cred_count amount of valid credentials found in allow list + * + * @return @ref ctap_status_codes_t + */ +int fido2_ctap_cbor_encode_assertion_object(const ctap_auth_data_header_t *auth_data, + const uint8_t *client_data_hash, + ctap_resident_key_t *rk, + uint8_t valid_cred_count); +/** + * @brief Encode key agreement + * + * @param[in] key Public key in COSE format + * + * @return @ref ctap_status_codes_t + */ +int fido2_ctap_cbor_encode_key_agreement(const ctap_public_key_cose_t *key); + +/** + * @brief Encode encrypted pin token + * + * @param[in] token encrypted pin token + * @param[in] len length of @p token + * + * @return @ref ctap_status_codes_t + */ +int fido2_ctap_cbor_encode_pin_token(uint8_t *token, size_t len); + +/** + * @brief Encode PIN tries left + * + * @param[in] tries_left amount of tries left + * + * @return @ref ctap_status_codes_t + */ +int fido2_ctap_cbor_encode_retries(uint8_t tries_left); + +/** + * @brief Get size of CBOR encoded data + * + * @param[in] buf Buffer holding the data + * + * @return size of CBOR encoded data + */ +size_t fido2_ctap_cbor_get_buffer_size(const uint8_t *buf); + +/** + * @brief Initialize CBOR encoder + * + * @param[in] buf Buffer to hold CBOR encoded data + * @param[in] len Length of @p buf + */ +void fido2_ctap_cbor_init_encoder(uint8_t *buf, size_t len); + +#ifdef __cplusplus +} +#endif +#endif /* FIDO2_CTAP_CTAP_CBOR_H */ +/** @} */ diff --git a/sys/include/fido2/ctap/ctap_crypto.h b/sys/include/fido2/ctap/ctap_crypto.h new file mode 100644 index 000000000000..ba548585bafe --- /dev/null +++ b/sys/include/fido2/ctap/ctap_crypto.h @@ -0,0 +1,301 @@ +/* + * Copyright (C) 2021 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @defgroup fido2_ctap_crypto FIDO2 CTAP crypto + * @ingroup fido2_ctap + * @brief FIDO2 CTAP crypto helper + * + * @{ + * + * @file + * @brief FIDO2 CTAP crypto helper defines, structures and function + * declarations. + * + * @author Nils Ollrogge + */ + +#ifndef FIDO2_CTAP_CTAP_CRYPTO_H +#define FIDO2_CTAP_CTAP_CRYPTO_H + +#include + +#include "hashes/sha256.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Size in bytes of cryptographic keys used + */ +#define CTAP_CRYPTO_KEY_SIZE 32 + +/** + * @brief Max size of ES256 signature in ASN.1 DER format + */ +#define CTAP_CRYPTO_ES256_DER_MAX_SIZE 72 + +/** + * @brief Elliptic curve public key + */ +typedef struct { + uint8_t x[CTAP_CRYPTO_KEY_SIZE]; /**< x coordinate of curve point */ + uint8_t y[CTAP_CRYPTO_KEY_SIZE]; /**< y coordinate of curve point */ +} ctap_crypto_pub_key_t; + +/** + * @brief Key agreement key + * + * CTAP specification (version 20190130) section 5.5.4 + */ +typedef struct { + ctap_crypto_pub_key_t pub; /**< public key */ + uint8_t priv[CTAP_CRYPTO_KEY_SIZE]; /**< private key */ +} ctap_crypto_key_agreement_key_t; + +/** + * @brief Initialize crypto helper + * + * Initializes crypto libs and creates key_agreement key pair + * + * @return @ref ctap_status_codes_t + */ +int fido2_ctap_crypto_init(void); + +/** + * @brief Wrapper function for @ref random_bytes + * + * @param[in] buf buffer to hold random bytes + * @param[in] len length of @p buf + * + * @return @ref ctap_status_codes_t + */ +int fido2_ctap_crypto_prng(uint8_t *buf, size_t len); + +/** + * @brief Wrapper function for @ref sha256_init + * + * @param ctx sha256_context_t handle to init + * + * @return @ref ctap_status_codes_t + */ +int fido2_ctap_crypto_sha256_init(sha256_context_t *ctx); + +/** + * @brief Wrapper function for @ref sha256_update + * + * @param ctx sha256_context_t handle to use + * @param[in] data Input data + * @param[in] len Length of @p data + * + * @return @ref ctap_status_codes_t + */ +int fido2_ctap_crypto_sha256_update(sha256_context_t *ctx, const void *data, size_t len); + +/** + * @brief Wrapper for @ref sha256_final + * + * @param ctx sha256_context_t handle to use + * @param digest resulting digest, this is the hash of all the bytes + * + * @return @ref ctap_status_codes_t + */ +int fido2_ctap_crypto_sha256_final(sha256_context_t *ctx, void *digest); + +/** + * @brief Wrapper function for @ref sha256 + * + * @param[in] data pointer to the buffer to generate hash from + * @param[in] len length of @p data + * @param[out] digest optional pointer to an array for the result, length must + * be SHA256_DIGEST_LENGTH + * + * @note discards the pointer returned by @ref sha256 + * + * @return @ref ctap_status_codes_t + */ +int fido2_ctap_crypto_sha256(const void *data, size_t len, + void *digest); + +/** + * @brief Wrapper function for @ref hmac_sha256_init + * + * @param[in] ctx hmac_context_t handle to use + * @param[in] key key used in the hmac-sha256 computation + * @param[in] key_length length of @p key + * + * @return @ref ctap_status_codes_t + */ +int fido2_ctap_crypto_hmac_sha256_init(hmac_context_t *ctx, const void *key, + size_t key_length); + +/** + * @brief Wrapper function for @ref hmac_sha256_update + * + * @param[in] ctx hmac_context_t handle to use + * @param[in] data pointer to the buffer to generate hash from + * @param[in] len length of @p data + * + * @return @ref ctap_status_codes_t + */ +int fido2_ctap_crypto_hmac_sha256_update(hmac_context_t *ctx, const void *data, size_t len); + +/** + * @brief Wrapper function for @ref hmac_sha256_final + * + * @param[in] ctx hmac_context_t handle to use + * @param[out] digest the computed hmac-sha256, + * length MUST be SHA256_DIGEST_LENGTH + * + * @return @ref ctap_status_codes_t + */ +int fido2_ctap_crypto_hmac_sha256_final(hmac_context_t *ctx, void *digest); + +/** + * @brief Wrapper function for @ref hmac_sha256 + * + * @param[in] key key used in the hmac-sha256 computation + * @param[in] key_length length of @p key + * @param[in] data pointer to the buffer to generate the hmac-sha256 + * @param[in] len length of @p data + * @param[out] digest the computed hmac-sha256, + * length MUST be SHA256_DIGEST_LENGTH + * + * @note discards the pointer returned by @ref hmac_sha256 + * + * @return @ref ctap_status_codes_t + */ +int fido2_ctap_crypto_hmac_sha256(const void *key, + size_t key_length, const void *data, size_t len, + void *digest); + +/** + * @brief Generate cryptographic key pair + * + * @param[in] pub_key public key buffer + * @param[in] priv_key private key buffer + * @param[in] len length of @p priv_key + * + * @return @ref ctap_status_codes_t + */ +int fido2_ctap_crypto_gen_keypair(ctap_crypto_pub_key_t *pub_key, uint8_t *priv_key, size_t len); + +/** + * @brief Elliptic-curve Diffie-Hellmann + * + * @param[in] out shared secret buffer + * @param[in] len length of @p out + * @param[in] pub_key public key of other party + * @param[in] priv_key private key + * @param[in] key_len length of @p priv_key + * + * @return @ref ctap_status_codes_t + */ +int fido2_ctap_crypto_ecdh(uint8_t *out, size_t len, + ctap_crypto_pub_key_t *pub_key, uint8_t *priv_key, size_t key_len); + +/** + * @brief Create cryptographic signature + * + * @param[in] hash Hash to be signed + * @param[in] hash_len length of @p hash + * @param[in] sig signature buffer + * @param[in] sig_len length of @p sig + * @param[in] key private key to use for signature + * @param[in] key_len length of @p key + * + * @return @ref ctap_status_codes_t + */ +int fido2_ctap_crypto_get_sig(uint8_t *hash, size_t hash_len, uint8_t *sig, + size_t *sig_len, const uint8_t *key, size_t key_len); + +/** + * @brief Encrypt data using AES-256-CBC + * + * @param[in] out encrypted data + * @param[in] out_len length of @p out + * @param[in] in data to be encrypted + * @param[in] in_len length of @p in + * @param[in] key symmetric key to use for encryption + * @param[in] key_len length of @p key + * + * @return @ref ctap_status_codes_t + */ +int fido2_ctap_crypto_aes_enc(uint8_t *out, size_t *out_len, uint8_t * in, + size_t in_len, const uint8_t * key, size_t key_len); + +/** + * @brief Decrypt data using AES-256-CBC + * + * @param[in] out decrypted data + * @param[in] out_len length of @p out + * @param[in] in encrypted data + * @param[in] in_len len of @p in + * @param[in] key symmetric key to use for decryption + * @param[in] key_len length of @p key + * + * @return @ref ctap_status_codes_t + */ +int fido2_ctap_crypto_aes_dec(uint8_t *out, size_t *out_len, uint8_t * in, + size_t in_len, const uint8_t * key, size_t key_len); + +/** + * @brief Encrypt data using AES-128-CCM + * + * @param[in] out encrypted data + * @param[in] out_len length of @p out + * @param[in] in data to be encrypted + * @param[in] in_len length of @p in + * @param[in] auth_data additional data to authenticate in MAC + * @param[in] auth_data_len length of @p auth_data + * @param[in] mac_len length of appended MAC + * @param[in] length_encoding max supported length of plaintext + * @param[in] nonce nonce for ctr mode encryption + * @param[in] nonce_len length of @p nonce + * @param[in] key symmetric key to use for encryption + * @param[in] key_len length of @p key + * + * @return @ref ctap_status_codes_t + */ +int fido2_ctap_crypto_aes_ccm_enc(uint8_t *out, size_t out_len, + const uint8_t *in, size_t in_len, + uint8_t *auth_data, size_t auth_data_len, + uint8_t mac_len, uint8_t length_encoding, + const uint8_t *nonce, size_t nonce_len, + const uint8_t *key, size_t key_len); + +/** + * @brief Encrypt data using AES-128-CCM + * + * @param[in] out encrypted data + * @param[in] out_len length of @p out + * @param[in] in data to be encrypted + * @param[in] in_len length of @p in + * @param[in] auth_data additional data to authenticate in MAC + * @param[in] auth_data_len length of @p auth_data + * @param[in] mac_len length of appended MAC + * @param[in] length_encoding max supported length of plaintext + * @param[in] nonce nonce for ctr mode encryption + * @param[in] nonce_len length of @p nonce + * @param[in] key symmetric key to use for encryption + * @param[in] key_len length of @p key + * + * @return @ref ctap_status_codes_t + */ +int fido2_ctap_crypto_aes_ccm_dec(uint8_t *out, size_t out_len, + const uint8_t *in, size_t in_len, + uint8_t *auth_data, size_t auth_data_len, + uint8_t mac_len, uint8_t length_encoding, + const uint8_t *nonce, size_t nonce_len, + const uint8_t *key, size_t key_len); +#ifdef __cplusplus +} +#endif +#endif /* FIDO2_CTAP_CTAP_CRYPTO_H */ +/** @} */ diff --git a/sys/include/fido2/ctap/ctap_mem.h b/sys/include/fido2/ctap/ctap_mem.h new file mode 100644 index 000000000000..f9b2bf4828ca --- /dev/null +++ b/sys/include/fido2/ctap/ctap_mem.h @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2021 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @defgroup fido2_ctap_mem FIDO2 CTAP flash + * @ingroup fido2_ctap + * @brief FIDO2 CTAP flash memory helper + * + * @{ + * + * @file + * @brief Definitions for CTAP flash memory helper functions + * + * @author Nils Ollrogge + */ + +#ifndef FIDO2_CTAP_CTAP_MEM_H +#define FIDO2_CTAP_CTAP_MEM_H + +#include + +#include "fido2/ctap/ctap.h" +#include "periph/flashpage.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief MAX function for internal use + * @{ + */ +#ifndef _MAX +#define _MAX(a, b) ((a) > (b) ? (a) : (b)) +#endif +/** @} */ + +/** + * @brief First flash page to store data in + * + * @note This can corrupt firmware if CTAP_FLASH_START_PAGE is set to a + * flash page containing firmware. Therefore make sure that CTAP_FLASH_START_PAGE + * is located after the firmware. + */ +#if defined(CONFIG_FIDO2_CTAP_FLASH_START_PAGE) && \ + (CONFIG_FIDO2_CTAP_FLASH_START_PAGE >= 0) +#define CTAP_FLASH_START_PAGE CONFIG_FIDO2_CTAP_FLASH_START_PAGE +#else +#define CTAP_FLASH_START_PAGE (FLASHPAGE_NUMOF - 4) +#endif + +/** + * @brief Start page for storing resident keys + */ +#define CTAP_FLASH_RK_START_PAGE CTAP_FLASH_START_PAGE + +/** + * @brief Page for storing authenticator state information + */ +#define CTAP_FLASH_STATE_PAGE CTAP_FLASH_RK_START_PAGE - 1 + +/** + * @brief Calculate padding needed to align struct size for saving to flash + */ +#define CTAP_FLASH_ALIGN_PAD(x) (sizeof(x) % FLASHPAGE_WRITE_BLOCK_SIZE == \ + 0 ? \ + 0 : FLASHPAGE_WRITE_BLOCK_SIZE - \ + sizeof(x) % FLASHPAGE_WRITE_BLOCK_SIZE) + +/** + * @brief Resident key size with alignment padding + */ +#define CTAP_FLASH_RK_SZ (sizeof(ctap_resident_key_t) + \ + CTAP_FLASH_ALIGN_PAD(ctap_resident_key_t)) + +/** + * @brief State struct size with alignment padding + */ +#define CTAP_FLASH_STATE_SZ (sizeof(ctap_state_t) + \ + CTAP_FLASH_ALIGN_PAD(ctap_state_t)) + +/** + * @brief Minimum flash sector size needed to hold CTAP related data + * + * This is needed to ensure that the MTD work_area buffer is big enough + */ +#define CTAP_FLASH_MIN_SECTOR_SZ _MAX(CTAP_FLASH_STATE_SZ, CTAP_FLASH_RK_SZ) + +/** + * @brief Pages per sector needed + */ +#define CTAP_FLASH_PAGES_PER_SECTOR ((CTAP_FLASH_MIN_SECTOR_SZ / FLASHPAGE_SIZE) + 1) + +/** + * @brief Initialize memory helper + * + * @return @ref ctap_status_codes_t + */ +int fido2_ctap_mem_init(void); + +/** + * @brief Write to flash memory + * + * @param[in] buf buffer to write + * @param[in] page page to write to + * @param[in] offset offset from the start of the page (in bytes) + * @param[in] len number of bytes to write + * + * @return @ref ctap_status_codes_t + */ +int fido2_ctap_mem_write(const void *buf, uint32_t page, uint32_t offset, uint32_t len); + +/** + * @brief Read from flash memory + * + * @param[out] buf buffer to fil in + * @param[in] page page to read from + * @param[in] offset offset from the start of the page (in bytes) + * @param[in] len number of bytes to write + * + * @return @ref ctap_status_codes_t + */ +int fido2_ctap_mem_read(void *buf, uint32_t page, uint32_t offset, uint32_t len); + +/** + * @brief Get maximum amount of resident credentials that can be stored + * + * @return maximum amount that can be stored + */ +uint16_t fido2_ctap_mem_get_max_rk_amount(void); + +/** + * @brief Get flashpage number resident key with index @p rk_idx. + * + * @param[in] rk_idx index of resident key + * + * @return page number if no error + * @return -1 if @p rk_idx is invalid + */ +int fido2_ctap_mem_get_flashpage_number_of_rk(uint16_t rk_idx); + +/** + * @brief Get offset of resident key into flashpage where flashpage = + * fido2_ctap_mem_get_flashpage_number_of_r(i) + * + * @param[in] rk_idx index of resident key + * + * @return page number if no error + * @return -1 if @p rk_idx is invalid + */ +int fido2_ctap_mem_get_offset_of_rk_into_flashpage(uint16_t rk_idx); + +#ifdef __cplusplus +} +#endif +#endif /* FIDO2_CTAP_CTAP_MEM_H */ +/** @} */ diff --git a/sys/include/fido2/ctap/ctap_utils.h b/sys/include/fido2/ctap/ctap_utils.h new file mode 100644 index 000000000000..b7dfc7298600 --- /dev/null +++ b/sys/include/fido2/ctap/ctap_utils.h @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2021 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @defgroup fido2_ctap_utils FIDO2 CTAP utils + * @ingroup fido2_ctap + * @brief FIDO2 CTAP utility helper + * + * @{ + * + * @file + * @brief Definition for CTAP utility functions + * + * @author Nils Ollrogge + */ + +#ifndef FIDO2_CTAP_CTAP_UTILS_H +#define FIDO2_CTAP_CTAP_UTILS_H + +#include +#include "fido2/ctap/ctap.h" +#include "periph/gpio.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief LED animation to indicate that user action is required + */ +void fido2_ctap_utils_led_animation(void); + +/** + * @brief Initialize button to be used for user presence test + * + * @return @ref ctap_status_codes_t + */ +int fido2_ctap_utils_init_gpio_pin(gpio_t pin, gpio_mode_t mode, gpio_flank_t flank); + +/** + * @brief Test user presence + * + * Successful if user clicks button in less than @ref CTAP_UP_TIMEOUT + * + * @return @ref ctap_status_codes_t + */ +int fido2_ctap_utils_user_presence_test(void); + +/** + * @brief Compare fido2 credentials based on creation time + * + * @param[in] k1 first resident key + * @param[in] k2 second resident key + * + * @return <0 if k2 has a bigger sign_count + * @return 0 if equal k1 and k2 have equal sign_count + * @return >0 if k1 has a bigger sign_count + */ +static inline int fido2_ctap_utils_cred_cmp(const void *k1, const void *k2) +{ + ctap_resident_key_t *_k1 = (ctap_resident_key_t *)k1; + ctap_resident_key_t *_k2 = (ctap_resident_key_t *)k2; + + return _k2->creation_time - _k1->creation_time; +} + +/** + * @brief Check equality of resident keys based on rp_id_hash and user_id + * + * @param[in] k1 first resident key + * @param[in] k2 second resident key + * + * @return true if equal false otherwise + */ +static inline bool fido2_ctap_utils_ks_equal(const ctap_resident_key_t *k1, + const ctap_resident_key_t *k2) +{ + return memcmp(k1->rp_id_hash, k2->rp_id_hash, sizeof(k1->rp_id_hash)) == 0 && + memcmp(k1->user_id, k2->user_id, sizeof(k1->user_id)) == 0; +} + +#ifdef __cplusplus +} +#endif +#endif /* FIDO2_CTAP_CTAP_UTILS_H */ +/** @} */ diff --git a/sys/include/fido2/ctap/transport/ctap_transport.h b/sys/include/fido2/ctap/transport/ctap_transport.h new file mode 100644 index 000000000000..41f0ff52f8df --- /dev/null +++ b/sys/include/fido2/ctap/transport/ctap_transport.h @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2021 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @defgroup fido2_ctap_transport FIDO2 CTAP transport + * @ingroup fido2_ctap + * @brief CTAP transport layer + * + * @{ + * + * @file + * @brief CTAP transport layer defines and function declarations + * + * @author Nils Ollrogge + */ + +#ifndef FIDO2_CTAP_TRANSPORT_CTAP_TRANSPORT_H +#define FIDO2_CTAP_TRANSPORT_CTAP_TRANSPORT_H + +#include +#include "mutex.h" +#include "timex.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief CTAP transport thread priority + */ +#ifndef CTAP_TRANSPORT_PRIO +#define CTAP_TRANSPORT_PRIO (THREAD_PRIORITY_MAIN - 5) +#endif +/** + * @brief Initialize ctap_transport layer and fido2_ctap + */ +void fido2_ctap_transport_init(void); + +#ifdef __cplusplus +} +#endif +#endif /* FIDO2_CTAP_TRANSPORT_CTAP_TRANSPORT_H */ +/** @} */ diff --git a/sys/include/fido2/ctap/transport/hid/ctap_hid.h b/sys/include/fido2/ctap/transport/hid/ctap_hid.h new file mode 100644 index 000000000000..c1ded965830e --- /dev/null +++ b/sys/include/fido2/ctap/transport/hid/ctap_hid.h @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2021 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @defgroup fido2_ctap_transport_hid FIDO2 CTAPHID + * @ingroup fido2_ctap_transport + * @brief FIDO2 CTAP USB_HID transport binding + * + * @{ + * + * @file + * @brief Definition for CTAPHID helper functions + * + * @author Nils Ollrogge + */ + +#ifndef FIDO2_CTAP_TRANSPORT_HID_CTAP_HID_H +#define FIDO2_CTAP_TRANSPORT_HID_CTAP_HID_H + +#include + +#include "usb/usbus/hid.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @name CTAP_HID packet type payload sizes + * + * @{ + */ +#define CTAP_HID_INIT_PAYLOAD_SIZE (CONFIG_USBUS_HID_INTERRUPT_EP_SIZE - 7) /**< endpoint size - init packet metadata */ +#define CTAP_HID_CONT_PAYLOAD_SIZE (CONFIG_USBUS_HID_INTERRUPT_EP_SIZE - 5) /**< endpoint size - cont packet metadata */ +/** @} */ + +/** + * @brief CTAP_HID protocol version + */ +#define CTAP_HID_PROTOCOL_VERSION 0x02 + +/** + * @name CTAP_HID packet type identifiers + * + * @{ + */ +#define CTAP_HID_INIT_PACKET 0x80 /**< initialization packet identifier */ +#define CTAP_HID_CONT_PACKET 0x00 /**< continuation packet identifier */ +/** @} */ + +/** + * @brief CTAP_HID size of nonce for init request + */ +#define CTAP_HID_INIT_NONCE_SIZE 8 + +/** + * @brief CTAP_HID transaction timeout in microseconds + */ +#ifdef CONFIG_FIDO2_CTAP_TRANSPORT_HID_TRANSACTION_TIMEOUT +#define CTAP_HID_TRANSACTION_TIMEOUT (CONFIG_FIDO2_CTAP_TRANSPORT_HID_TRANSACTION_TIMEOUT * \ + US_PER_MS) +#else +#define CTAP_HID_TRANSACTION_TIMEOUT (500 * US_PER_MS) +#endif + +/** + * @brief CTAP_HID max message payload size + * + * CTAP specification (version 20190130) section 8.2.4. + */ +#define CTAP_HID_BUFFER_SIZE 7609 + +/** + * @name CTAP_HID commands + * + * @{ + */ +#define CTAP_HID_COMMAND_PING (0x01 | CTAP_HID_INIT_PACKET) /**< CTAPHID_PING command */ +#define CTAP_HID_COMMAND_MSG (0x03 | CTAP_HID_INIT_PACKET) /**< CTAPHID_MSG command */ +#define CTAP_HID_COMMAND_LOCK (0x04 | CTAP_HID_INIT_PACKET) /**< CTAPHID_LOCK command */ +#define CTAP_HID_COMMAND_INIT (0x06 | CTAP_HID_INIT_PACKET) /**< CTAPHID_INIT command */ +#define CTAP_HID_COMMAND_WINK (0x08 | CTAP_HID_INIT_PACKET) /**< CTAPHID_WINK command */ +#define CTAP_HID_COMMAND_CBOR (0x10 | CTAP_HID_INIT_PACKET) /**< CTAPHID_CBOR command */ +#define CTAP_HID_COMMAND_CANCEL (0x11 | CTAP_HID_INIT_PACKET) /**< CTAPHID_CANCEL command */ +#define CTAP_HID_COMMAND_KEEPALIVE (0x3b | CTAP_HID_INIT_PACKET) /**< CTAPHID_KEEPALIVE command */ +#define CTAP_HID_COMMAND_ERROR (0x3f | CTAP_HID_INIT_PACKET) /**< CTAPHID_ERROR command */ +/** @} */ + +/** + * @name CTAP_HID capability flags + * + * @{ + */ +#define CTAP_HID_CAPABILITY_WINK 0x01 /**< If set, authenticator implements CTAPHID_WINK function */ +#define CTAP_HID_CAPABILITY_CBOR 0x04 /**< If set, authenticator implements CTAPHID_CBOR function */ +#define CTAP_HID_CAPABILITY_NMSG 0x08 /**< If set, authenticator DOES NOT implement CTAPHID_MSG function (CTAP1 / U2F) */ +/** @} */ + +/** + * @name CTAP_HID error codes + * + * @{ + */ +#define CTAP_HID_OK 0x00 /**< Success */ +#define CTAP_HID_ERR_INVALID_CMD 0x01 /**< The command in the request is invalid */ +#define CTAP_HID_ERR_INVALID_PAR 0x02 /**< The parameter(s) in the request is invalid */ +#define CTAP_HID_ERR_INVALID_LEN 0x03 /**< The length field (BCNT) is invalid for the request */ +#define CTAP_HID_ERR_INVALID_SEQ 0x04 /**< The sequence does not match expected value */ +#define CTAP_HID_ERR_MSG_TIMEOUT 0x05 /**< The message has timed out */ +#define CTAP_HID_ERR_CHANNEL_BUSY 0x06 /**< The device is busy for the requesting channel */ +#define CTAP_HID_ERR_LOCK_REQUIRED 0x0a /**< Command requires channel lock */ +#define CTAP_HID_ERR_INVALID_CHANNEL 0x0b /**< CID is not valid. */ +#define CTAP_HID_ERR_OTHER 0x7f /**< Unspecified error */ +/** @} */ + +/** + * @name CTAP_HID status codes + * + * @{ + */ +#define CTAP_HID_STATUS_PROCESSING 0x01 /**< processing status code */ +#define CTAP_HID_STATUS_UPNEEDED 0x02 /**< user presence needed status code */ +/** @} */ + +/** + * @brief CTAP_HID max number of channels + * + */ +#define CTAP_HID_CIDS_MAX 0x08 + +/** + * @brief CTAP_HID animation delay in milliseconds for wink command + */ +#define CTAP_HID_WINK_DELAY 400 + +/** + * @brief CTAP_HID broadcast channel identifier + * + */ +#define CTAP_HID_BROADCAST_CID 0xffffffff + +/** + * @name CTAP_HID buffer status + * + * @{ + */ +#define CTAP_HID_BUFFER_STATUS_BUFFERING 0x00 /**< packets are being buffered */ +#define CTAP_HID_BUFFER_STATUS_DONE 0x01 /**< packet processing done */ +#define CTAP_HID_BUFFER_STATUS_ERROR 0x02 /**< error occurred processing packets */ +/** @} */ + +/** + * @brief CTAP_HID initialization packet struct + * + */ +typedef struct { + uint8_t cmd; /**< CTAP_HID command */ + uint8_t bcnth; /**< higher byte */ + uint8_t bcntl; /**< lower byte */ + uint8_t payload[CTAP_HID_INIT_PAYLOAD_SIZE]; /**< packet payload */ +} ctap_hid_init_pkt_t; + +/** + * @brief CTAP_HID continuation packet struct + * + */ +typedef struct { + uint8_t seq; /**< packet sequence number */ + uint8_t payload[CTAP_HID_CONT_PAYLOAD_SIZE]; /**< packet payload */ +} ctap_hid_cont_pkt_t; + +/** + * @brief CTAP_HID packet struct + * + */ +typedef struct { + uint32_t cid; /**< channel identifier */ + union { + ctap_hid_init_pkt_t init; /**< initialization packet */ + ctap_hid_cont_pkt_t cont; /**< continuation packet */ + }; +} ctap_hid_pkt_t; + +/** + * @brief CTAP_HID initialization response struct + * + * CTAP specification (version 20190130) 8.1.9.1.3 + */ +typedef struct __attribute__((packed)){ + uint8_t nonce[CTAP_HID_INIT_NONCE_SIZE]; /**< nonce */ + uint32_t cid; /**< channel identifier */ + uint8_t protocol_version; /**< CTAP_HID protocol version */ + uint8_t version_major; /**< major device version */ + uint8_t version_minor; /**< minor device version */ + uint8_t build_version; /**< build device version */ + uint8_t capabilities; /**< capabilities flags */ +} ctap_hid_init_resp_t; + +/** + * @brief CTAP_HID channel identifier struct + * + * Used to keep state information about logical channels + */ +typedef struct { + bool taken; /**< is cid taken? */ + uint32_t cid; /**< channel identifier */ + uint64_t last_used; /**< timestamp of last usage */ +} ctap_hid_cid_t; + +/** + * @brief Initialize CTAPHID + * + * @param[in] queue CTAP transport layer event queue + */ +void fido2_ctap_transport_hid_init(event_queue_t *queue); + +/** + * @brief Handle CTAP_HID packet + * + * @param[in] pkt_raw raw CTAP_HID packet + */ +void fido2_ctap_transport_hid_handle_packet(void *pkt_raw); + +/** + * @brief Check logical channels for timeouts + * + * This function is used to prevent one channel from locking the authenticator. + * E.g. if a device starts a transaction that does not fit in one packet and + * sends a CTAPHID initialization packet but not continuation packet the + * authenticator will keep waiting. This function will prevent this by + * cancelling a transaction if it takes longer than + * + * CTAP specification (version 20190130) section 5.6 + * + * @ref CTAP_HID_TRANSACTION_TIMEOUT + */ +void fido2_ctap_transport_hid_check_timeouts(void); + +/** + * @brief Check if CTAPHID layer has received CANCEL command + * + * @return true if CANCEL command has been received + * @return false otherwise + */ +bool fido2_ctap_transport_hid_should_cancel(void); + +#ifdef __cplusplus +} +#endif +#endif /* FIDO2_CTAP_TRANSPORT_HID_CTAP_HID_H */ +/** @} */ diff --git a/tests/sys_fido2_ctap/Makefile b/tests/sys_fido2_ctap/Makefile new file mode 100644 index 000000000000..f36e2c2ee519 --- /dev/null +++ b/tests/sys_fido2_ctap/Makefile @@ -0,0 +1,37 @@ +BOARD ?= nrf52840dk +#BOARD ?= nrf52840dongle + +include ../Makefile.tests_common + +USEMODULE += fido2_ctap_transport_hid +USEPKG += fido2_tests + +USB_VID ?= $(USB_VID_TESTING) +USB_PID ?= $(USB_PID_TESTING) + +# Disable user presence tests +# Should be used when running fido2-test to make them run quicker +#CFLAGS += -DCONFIG_FIDO2_CTAP_DISABLE_UP=1 + +# Disable user LED animation +#CFLAGS += -DCONFIG_FIDO2_CTAP_DISABLE_LED=1 + +# FIDO2 tests except for the ones requiring user presence +# +# Use env -i because fido2-test has a depedency (pyscard) that needs to be +# compiled natively (x86-64). Therefore we need to clear the flags set by e.g. +# BOARD = nrf52840dk +fido2-test: + env -i PATH=$(PATH) $(MAKE) -C $(RIOTBASE)/build/pkg/fido2_tests + +# FIDO2 user presence tests. +# +# Make sure to enable user presence tests by uncommenting CFLAGS += -DCONFIG_FIDO2_CTAP_DISABLE_UP=1 +# +# Use env -i because fido2-test has a depedency (pyscard) that needs to be +# compiled natively (x86-64). Therefore we need to clear the flags set by e.g. +# BOARD = nrf52840dk +fido2-test-up: + env -i PATH=$(PATH) $(MAKE) -C $(RIOTBASE)/build/pkg/fido2_tests up-tests + +include $(RIOTBASE)/Makefile.include diff --git a/tests/sys_fido2_ctap/README.md b/tests/sys_fido2_ctap/README.md new file mode 100644 index 000000000000..acd46d4008f4 --- /dev/null +++ b/tests/sys_fido2_ctap/README.md @@ -0,0 +1,53 @@ +# Test Application for FIDO2 CTAP + +This test aims to test the FIDO2 CTAP implementation by creating a FIDO2 +authenticator which uses CTAPHID as communication protocol. + +Note: +* This test application has only been tested on an nrf52840 DK. + +The test application requires at least 16536 bytes of stack memory which are +divided as follows: +* 512 bytes isr_stack +* 1024 usbus +* 15000 bytes FIDO2 CTAP + +## Usage +The FIDO2 authenticator can be tested in two ways: + +### Functional testing +1. Flash the device with `make flash`. +2. Test the authenticator on a website like [Webauthn.io](https://webauthn.io/). + +Note: +* Due to limited support of FIDO2 CTAP in browsers as of now, make sure to use the + Chromium or Google Chrome browser when testing on [Webauthn.io](https://webauthn.io/). +* When registering and authenticating on [Webauthn.io](https://webauthn.io/) you +will need to push button 1 on your device in order to show user presence. + +### Unit testing +Unit testing is based on the `fido2_tests` package. + +There are two test targets (fido2-test, fido2-test-up). The former requires no user +interaction the latter does. + +Note: +* The tests require python 3.6+. +* The tests require [swig](http://www.swig.org/) to be installed on your host computer. +* Running the tests for the first time will setup a virtual python environment (venv) and install python dependencies of the tests. To check the dependencies please refer to the `requirements.txt` of the [fido2-tests repository](https://github.com/solokeys/fido2-tests). +* The unit tests will require you to reboot the authenticator multiple times. Be patient before continuing as it takes a few seconds for the connection between OS and authenticator to be re-established. +* If you keep getting errors while trying to run the tests try changing to another git branch and back e.g. `git checkout branch1 && git checkout -` in order to remove build artifacts. Then re-flash the device with `make flash term` and try to run the tests again with `make fido2-test` or `make fido2-test-up`. + +fido2-test + +1. To make benchmarking faster disable user presence tests by enabling the CFLAG + `CONFIG_FIDO2_CTAP_DISABLE_UP` in the Makefile or through KConfig. +2. Flash the device with `make flash`. +3. Run the unit tests by running `make fido2-test`. + +fido2-test-up + +1. Make sure that the CFLAG `CONFIG_FIDO2_CTAP_DISABLE_UP` is disabled as this test target + requires user interaction. +2. Flash the device with `make flash`. +3. Run the unit tests by running `make fido2-test-up` and follow the instructions. E.g. when `.ACTIVATE UP ONCE` is displayed, press the configured UP button (default button 1) once. diff --git a/tests/sys_fido2_ctap/main.c b/tests/sys_fido2_ctap/main.c new file mode 100644 index 000000000000..31ffbc44efd9 --- /dev/null +++ b/tests/sys_fido2_ctap/main.c @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2021 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup tests + * @{ + * @file + * @brief FIDO2 CTAP test application that creates an authenticator + * which uses CTAPHID as underlying communication protocol + * + * @author Nils Ollrogge + * @} + */ + +#include +#include + +#define ENABLE_DEBUG (0) +#include "debug.h" + +#include "xtimer.h" + +#include "fido2/ctap.h" +#include "fido2/ctap/transport/ctap_transport.h" + +int main(void) +{ + /* sleep in order to see early DEBUG outputs */ + xtimer_sleep(3); + fido2_ctap_transport_init(); +} diff --git a/tests/usbus_hid/main.c b/tests/usbus_hid/main.c index d8c39ffeef76..ac0700ebf52a 100644 --- a/tests/usbus_hid/main.c +++ b/tests/usbus_hid/main.c @@ -26,7 +26,7 @@ /* this descriptor is used, because the basic usb_hid interface was developed in - conjunction with FIDO2. Descriptor is taken from CTAP2 specification + conjunction with FIDO2. Descriptor is taken from CTAP specification (version 20190130) section 8.1.8.2 */ static const uint8_t report_desc_ctap[] = {