From 6a2e0ab331a081aa11a90becc3f8644a68c2cfc9 Mon Sep 17 00:00:00 2001 From: Mike Mabey Date: Sat, 5 Sep 2026 16:50:37 -0600 Subject: [PATCH] Add securekey module for hardware-held cryptographic keys securekey exposes keys that live in a hardware key store -- eFuse, a key manager, a secure element -- and can be used but never read back. Python code can compute or verify an HMAC-SHA256 with the key; there is no API to read the raw key bytes and no API to write or burn keys. Provisioning is a manufacturing-time step done with vendor tools (e.g. espefuse.py). The module is portable, split across the usual three layers: - shared-bindings/securekey/ -- portable arg parsing + docstrings - shared-module/securekey/ -- HardwareKey object and the psa_mac_compute / psa_mac_verify operations, port-independent - ports/espressif/common-hal/securekey/ -- only construct(): validates the eFuse key block and imports a PSA opaque-key reference securekey.HardwareKey(key_slot) takes a port-defined identifier. On espressif it is the eFuse key block index 0-5; the block must be burned with purpose HMAC_UP or construction fails (fail-closed), so a HardwareKey can never be pointed at a block reserved for flash encryption, secure boot, or the Digital Signature peripheral. Methods: hmac_sha256(data), verify_hmac_sha256(data, mac) (constant-time). Properties: key_slot, exportable (informational; False once RD_DIS is burned). Build wiring: CIRCUITPY_SECUREKEY, default 0, enabled on espressif for all chips with the HMAC peripheral (off for esp32 / esp32c2 / esp32c61, which lack it). --- .codespell/ignore-words.txt | 1 + locale/circuitpython.pot | 12 ++ .../common-hal/securekey/HardwareKey.c | 87 ++++++++++++ .../espressif/common-hal/securekey/__init__.c | 8 ++ ports/espressif/mpconfigport.mk | 12 +- py/circuitpy_defns.mk | 6 + py/circuitpy_mpconfig.mk | 5 + shared-bindings/securekey/HardwareKey.c | 131 ++++++++++++++++++ shared-bindings/securekey/HardwareKey.h | 16 +++ shared-bindings/securekey/__init__.c | 43 ++++++ shared-bindings/securekey/__init__.h | 7 + shared-module/securekey/HardwareKey.c | 55 ++++++++ shared-module/securekey/HardwareKey.h | 38 +++++ 13 files changed, 420 insertions(+), 1 deletion(-) create mode 100644 ports/espressif/common-hal/securekey/HardwareKey.c create mode 100644 ports/espressif/common-hal/securekey/__init__.c create mode 100644 shared-bindings/securekey/HardwareKey.c create mode 100644 shared-bindings/securekey/HardwareKey.h create mode 100644 shared-bindings/securekey/__init__.c create mode 100644 shared-bindings/securekey/__init__.h create mode 100644 shared-module/securekey/HardwareKey.c create mode 100644 shared-module/securekey/HardwareKey.h diff --git a/.codespell/ignore-words.txt b/.codespell/ignore-words.txt index 48bee0f30ba..d435ed01dac 100644 --- a/.codespell/ignore-words.txt +++ b/.codespell/ignore-words.txt @@ -27,3 +27,4 @@ straightaway ftbs ftb curren +mabey diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot index c55efbcf8c1..3b5bf2a00b1 100644 --- a/locale/circuitpython.pot +++ b/locale/circuitpython.pot @@ -1459,6 +1459,18 @@ msgstr "" msgid "invalid setting" msgstr "" +#: ports/espressif/common-hal/securekey/HardwareKey.c +msgid "key_slot is not configured for HMAC use" +msgstr "" + +#: ports/espressif/common-hal/securekey/HardwareKey.c +msgid "crypto init failed" +msgstr "" + +#: shared-module/securekey/HardwareKey.c +msgid "HMAC calculation failed" +msgstr "" + #: ports/espressif/common-hal/espidf/__init__.c msgid "Generic Failure" msgstr "" diff --git a/ports/espressif/common-hal/securekey/HardwareKey.c b/ports/espressif/common-hal/securekey/HardwareKey.c new file mode 100644 index 00000000000..f4706e8ca7e --- /dev/null +++ b/ports/espressif/common-hal/securekey/HardwareKey.c @@ -0,0 +1,87 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Mike Mabey +// +// SPDX-License-Identifier: MIT + +// The only port-specific step: turn a hardware key slot (here, an eFuse key +// block index) into a PSA key id. Everything after that -- hmac_sha256(), +// verify_hmac_sha256() -- lives in shared-module/securekey/HardwareKey.c. + +#include "shared-module/securekey/HardwareKey.h" + +#include "py/runtime.h" + +#include "esp_efuse.h" + +// Pulls in MBEDTLS_CONFIG_FILE (esp_config.h), which is what defines +// ESP_HMAC_OPAQUE_DRIVER_ENABLED on HMAC-capable chips. Including only +// goes through the tf-psa-crypto config path and does NOT +// define it, so the opaque-driver header below would compile to nothing. +#include "mbedtls/build_info.h" +#include "psa/crypto.h" +// Public header of the ESP-IDF mbedtls component's PSA opaque-key driver for +// eFuse HMAC keys (components/mbedtls/port/psa_driver/include/). +#include "psa_crypto_driver_esp_hmac_opaque.h" + +#if !defined(ESP_HMAC_OPAQUE_DRIVER_ENABLED) +#error "securekey requires the ESP-IDF PSA opaque HMAC driver (SOC_HMAC_SUPPORTED targets only)" +#endif + +// ESP32-S3 has BLOCK_KEY0..BLOCK_KEY5; other HMAC-capable chips match. Python +// key_slot 0-5 maps to EFUSE_BLK_KEY0 + key_slot. +#define EFUSE_KEY_BLOCK_COUNT 6 + +// The ESP HMAC peripheral consumes a 256-bit eFuse key. +#define HMAC_KEY_BITS 256 + +// One PSA key is imported per eFuse block on first use and reused thereafter, so +// repeated HardwareKey() construction does not accumulate PSA key slots. The +// keys are volatile references (no key material); at most EFUSE_KEY_BLOCK_COUNT +// are ever imported. On espressif this cache is safe across a CircuitPython soft +// reset because ESP-IDF initializes PSA once at boot and never frees it (see the +// raspberrypi port's reset path for the contrasting case). +static psa_key_id_t imported_key[EFUSE_KEY_BLOCK_COUNT]; + +void common_hal_securekey_hardwarekey_construct(securekey_hardwarekey_obj_t *self, mp_int_t key_slot) { + if (key_slot < 0 || key_slot >= EFUSE_KEY_BLOCK_COUNT) { + mp_raise_ValueError_varg(MP_ERROR_TEXT("%q must be %d-%d"), + MP_QSTR_key_slot, 0, EFUSE_KEY_BLOCK_COUNT - 1); + } + + esp_efuse_block_t block = (esp_efuse_block_t)(EFUSE_BLK_KEY0 + key_slot); + if (esp_efuse_get_key_purpose(block) != ESP_EFUSE_KEY_PURPOSE_HMAC_UP) { + mp_raise_ValueError(MP_ERROR_TEXT("key_slot is not configured for HMAC use")); + } + + if (imported_key[key_slot] == 0) { + // PSA is already initialized by ssl / hashlib, but psa_crypto_init() is + // idempotent and this keeps securekey usable on its own. + if (psa_crypto_init() != PSA_SUCCESS) { + mp_raise_RuntimeError(MP_ERROR_TEXT("crypto init failed")); + } + + psa_key_attributes_t attr = PSA_KEY_ATTRIBUTES_INIT; + psa_set_key_type(&attr, PSA_KEY_TYPE_HMAC); + psa_set_key_bits(&attr, HMAC_KEY_BITS); + psa_set_key_algorithm(&attr, PSA_ALG_HMAC(PSA_ALG_SHA_256)); + psa_set_key_usage_flags(&attr, PSA_KEY_USAGE_SIGN_MESSAGE | PSA_KEY_USAGE_VERIFY_MESSAGE); + psa_set_key_lifetime(&attr, PSA_KEY_LIFETIME_ESP_HMAC_VOLATILE); + + // Import data is a *reference* to the eFuse block, not key material. The + // driver independently re-checks the HMAC_UP purpose and refuses + // anything else. + esp_hmac_opaque_key_t keyref = { .efuse_key_id = (uint8_t)key_slot }; + + psa_key_id_t key_id = 0; + psa_status_t status = psa_import_key(&attr, (const uint8_t *)&keyref, sizeof(keyref), &key_id); + if (status != PSA_SUCCESS) { + mp_raise_ValueError(MP_ERROR_TEXT("key_slot is not configured for HMAC use")); + } + imported_key[key_slot] = key_id; + } + + self->key_id = imported_key[key_slot]; + self->key_slot = key_slot; + self->exportable = !esp_efuse_get_key_dis_read(block); +} diff --git a/ports/espressif/common-hal/securekey/__init__.c b/ports/espressif/common-hal/securekey/__init__.c new file mode 100644 index 00000000000..80a51d35e67 --- /dev/null +++ b/ports/espressif/common-hal/securekey/__init__.c @@ -0,0 +1,8 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Mike Mabey +// +// SPDX-License-Identifier: MIT + +// No securekey module-level functions. The port-specific code is the +// HardwareKey constructor in HardwareKey.c. diff --git a/ports/espressif/mpconfigport.mk b/ports/espressif/mpconfigport.mk index 98fe3224a49..5877bb0adae 100644 --- a/ports/espressif/mpconfigport.mk +++ b/ports/espressif/mpconfigport.mk @@ -91,6 +91,7 @@ CIRCUITPY_PS2IO ?= 1 CIRCUITPY_RGBMATRIX ?= 1 CIRCUITPY_ROTARYIO ?= 1 CIRCUITPY_SDIOIO ?= 1 +CIRCUITPY_SECUREKEY ?= 1 CIRCUITPY_SETTABLE_PROCESSOR_FREQUENCY ?= 1 CIRCUITPY_SYNTHIO_MAX_CHANNELS ?= 12 CIRCUITPY_TOUCHIO ?= 1 @@ -105,6 +106,9 @@ ifeq ($(IDF_TARGET),esp32) # Modules CIRCUITPY_RGBMATRIX = 0 +# No HMAC peripheral (introduced starting with ESP32-S2) +CIRCUITPY_SECUREKEY = 0 + # Has no USB CIRCUITPY_USB_DEVICE = 0 @@ -118,6 +122,9 @@ CIRCUITPY_ESPCAMERA = 0 CIRCUITPY_ESPULP = 0 CIRCUITPY_MEMORYMAP = 0 +# No HMAC peripheral (SOC_HMAC_SUPPORTED is not defined for this target) +CIRCUITPY_SECUREKEY = 0 + # No capacitive touch peripheral CIRCUITPY_ALARM_TOUCH = 0 CIRCUITPY_TOUCHIO_USE_NATIVE = 0 @@ -220,7 +227,7 @@ CIRCUITPY_SDIOIO = 0 CIRCUITPY_USB_DEVICE = 0 CIRCUITPY_ESP_USB_SERIAL_JTAG ?= 1 -#### esp32c6 ########################################################## +#### esp32c61 ######################################################### else ifeq ($(IDF_TARGET),esp32c61) # Modules CIRCUITPY_ESPCAMERA = 0 @@ -228,6 +235,9 @@ CIRCUITPY_ESPULP = 0 CIRCUITPY_MEMORYMAP = 0 CIRCUITPY_RGBMATRIX = 0 +# No HMAC peripheral (SOC_HMAC_SUPPORTED is not defined for this target) +CIRCUITPY_SECUREKEY = 0 + # No capacitive touch peripheral CIRCUITPY_ALARM_TOUCH = 0 CIRCUITPY_TOUCHIO_USE_NATIVE = 0 diff --git a/py/circuitpy_defns.mk b/py/circuitpy_defns.mk index d91ac8ad23b..a3e87065732 100755 --- a/py/circuitpy_defns.mk +++ b/py/circuitpy_defns.mk @@ -393,6 +393,9 @@ endif ifeq ($(CIRCUITPY_SDIOIO),1) SRC_PATTERNS += sdioio/% endif +ifeq ($(CIRCUITPY_SECUREKEY),1) +SRC_PATTERNS += securekey/% +endif ifeq ($(CIRCUITPY_SHARPDISPLAY),1) SRC_PATTERNS += sharpdisplay/% endif @@ -598,6 +601,8 @@ SRC_COMMON_HAL_ALL = \ rtc/__init__.c \ sdioio/SDCard.c \ sdioio/__init__.c \ + securekey/HardwareKey.c \ + securekey/__init__.c \ socketpool/__init__.c \ socketpool/SocketPool.c \ socketpool/Socket.c \ @@ -840,6 +845,7 @@ SRC_SHARED_MODULE_ALL = \ rotaryio/IncrementalEncoder.c \ sdcardio/SDCard.c \ sdcardio/__init__.c \ + securekey/HardwareKey.c \ sharpdisplay/SharpMemoryFramebuffer.c \ sharpdisplay/__init__.c \ socket/__init__.c \ diff --git a/py/circuitpy_mpconfig.mk b/py/circuitpy_mpconfig.mk index e7303c1d3b1..487a887b5b7 100755 --- a/py/circuitpy_mpconfig.mk +++ b/py/circuitpy_mpconfig.mk @@ -555,6 +555,11 @@ CFLAGS += -DCIRCUITPY_SDCARDIO=$(CIRCUITPY_SDCARDIO) CIRCUITPY_SDIOIO ?= 0 CFLAGS += -DCIRCUITPY_SDIOIO=$(CIRCUITPY_SDIOIO) +# securekey: cryptographic operations with hardware-held, non-readable keys. +# Off unless a port provides a common-hal/securekey backend. +CIRCUITPY_SECUREKEY ?= 0 +CFLAGS += -DCIRCUITPY_SECUREKEY=$(CIRCUITPY_SECUREKEY) + CIRCUITPY_BLE_SERIAL_SERVICE ?= 0 CFLAGS += -DCIRCUITPY_BLE_SERIAL_SERVICE=$(CIRCUITPY_BLE_SERIAL_SERVICE) diff --git a/shared-bindings/securekey/HardwareKey.c b/shared-bindings/securekey/HardwareKey.c new file mode 100644 index 00000000000..3fe3bf89849 --- /dev/null +++ b/shared-bindings/securekey/HardwareKey.c @@ -0,0 +1,131 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Mike Mabey +// +// SPDX-License-Identifier: MIT + +#include "py/objproperty.h" +#include "py/objstr.h" +#include "py/runtime.h" + +#include "shared-bindings/securekey/HardwareKey.h" + +#define HMAC_SHA256_DIGEST_SIZE SECUREKEY_HMAC_SHA256_DIGEST_SIZE + +//| class HardwareKey: +//| """A key held in a hardware key store, usable but not readable. +//| +//| The constructor argument that selects the key is **port-defined**: +//| +//| * **espressif**: ``key_slot`` is the eFuse key block index (``0`` - +//| ``5``, i.e. ``BLOCK_KEY0`` - ``BLOCK_KEY5``). The block must already +//| be burned with purpose ``HMAC_UP``; construction fails otherwise, so +//| a `HardwareKey` can never be pointed at a block reserved for flash +//| encryption, secure boot, or the Digital Signature peripheral. +//| """ +//| +//| def __init__(self, key_slot: int) -> None: +//| """Bind to the hardware key identified by ``key_slot``. +//| +//| :param int key_slot: port-defined identifier for the hardware key +//| :raises ValueError: if ``key_slot`` does not name a usable key +//| """ +//| ... +static mp_obj_t securekey_hardwarekey_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { + enum { ARG_key_slot }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_key_slot, MP_ARG_REQUIRED | MP_ARG_INT }, + }; + mp_arg_check_num(n_args, n_kw, 1, 1, true); + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all_kw_array(n_args, n_kw, all_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + securekey_hardwarekey_obj_t *self = mp_obj_malloc(securekey_hardwarekey_obj_t, &securekey_hardwarekey_type); + common_hal_securekey_hardwarekey_construct(self, args[ARG_key_slot].u_int); + + return MP_OBJ_FROM_PTR(self); +} + +//| def hmac_sha256(self, data: ReadableBuffer) -> bytes: +//| """Compute the HMAC-SHA256 of ``data`` with this key and return the +//| 32-byte result. The key is never returned or exposed. +//| +//| :param ~circuitpython_typing.ReadableBuffer data: the message to authenticate +//| """ +//| ... +static mp_obj_t securekey_hardwarekey_hmac_sha256(mp_obj_t self_in, mp_obj_t data_in) { + securekey_hardwarekey_obj_t *self = MP_OBJ_TO_PTR(self_in); + + mp_buffer_info_t bufinfo; + mp_get_buffer_raise(data_in, &bufinfo, MP_BUFFER_READ); + + mp_obj_t result = mp_obj_new_bytes_of_zeros(HMAC_SHA256_DIGEST_SIZE); + mp_obj_str_t *result_bytes = MP_OBJ_TO_PTR(result); + + common_hal_securekey_hardwarekey_hmac_sha256(self, bufinfo.buf, bufinfo.len, + (uint8_t *)result_bytes->data, HMAC_SHA256_DIGEST_SIZE); + return result; +} +static MP_DEFINE_CONST_FUN_OBJ_2(securekey_hardwarekey_hmac_sha256_obj, securekey_hardwarekey_hmac_sha256); + +//| def verify_hmac_sha256(self, data: ReadableBuffer, mac: ReadableBuffer) -> bool: +//| """Return ``True`` if ``mac`` is the correct HMAC-SHA256 of ``data`` +//| for this key. The comparison is constant-time. +//| +//| :param ~circuitpython_typing.ReadableBuffer data: the message that was authenticated +//| :param ~circuitpython_typing.ReadableBuffer mac: the MAC to check +//| """ +//| ... +static mp_obj_t securekey_hardwarekey_verify_hmac_sha256(mp_obj_t self_in, mp_obj_t data_in, mp_obj_t mac_in) { + securekey_hardwarekey_obj_t *self = MP_OBJ_TO_PTR(self_in); + + mp_buffer_info_t data_info; + mp_get_buffer_raise(data_in, &data_info, MP_BUFFER_READ); + mp_buffer_info_t mac_info; + mp_get_buffer_raise(mac_in, &mac_info, MP_BUFFER_READ); + + bool ok = common_hal_securekey_hardwarekey_verify_hmac_sha256(self, + data_info.buf, data_info.len, mac_info.buf, mac_info.len); + return mp_obj_new_bool(ok); +} +static MP_DEFINE_CONST_FUN_OBJ_3(securekey_hardwarekey_verify_hmac_sha256_obj, securekey_hardwarekey_verify_hmac_sha256); + +//| key_slot: int +//| """The port-defined key identifier this handle is bound to. (read-only)""" +static mp_obj_t securekey_hardwarekey_get_key_slot(mp_obj_t self_in) { + securekey_hardwarekey_obj_t *self = MP_OBJ_TO_PTR(self_in); + return MP_OBJ_NEW_SMALL_INT(common_hal_securekey_hardwarekey_get_key_slot(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(securekey_hardwarekey_get_key_slot_obj, securekey_hardwarekey_get_key_slot); +MP_PROPERTY_GETTER(securekey_hardwarekey_key_slot_obj, (mp_obj_t)&securekey_hardwarekey_get_key_slot_obj); + +//| exportable: bool +//| """Whether the raw key bytes can ever leave the hardware. Always +//| informational -- it does not gate `hmac_sha256`. +//| +//| On espressif this is ``False`` once the key block's ``RD_DIS`` eFuse +//| bit is set (which ``espefuse.py`` does by default). It is meant for +//| manufacturing-time self-test code to confirm a key block was burned as +//| expected. (read-only)""" +static mp_obj_t securekey_hardwarekey_get_exportable(mp_obj_t self_in) { + securekey_hardwarekey_obj_t *self = MP_OBJ_TO_PTR(self_in); + return mp_obj_new_bool(common_hal_securekey_hardwarekey_get_exportable(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(securekey_hardwarekey_get_exportable_obj, securekey_hardwarekey_get_exportable); +MP_PROPERTY_GETTER(securekey_hardwarekey_exportable_obj, (mp_obj_t)&securekey_hardwarekey_get_exportable_obj); + +static const mp_rom_map_elem_t securekey_hardwarekey_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR_hmac_sha256), MP_ROM_PTR(&securekey_hardwarekey_hmac_sha256_obj) }, + { MP_ROM_QSTR(MP_QSTR_verify_hmac_sha256), MP_ROM_PTR(&securekey_hardwarekey_verify_hmac_sha256_obj) }, + { MP_ROM_QSTR(MP_QSTR_key_slot), MP_ROM_PTR(&securekey_hardwarekey_key_slot_obj) }, + { MP_ROM_QSTR(MP_QSTR_exportable), MP_ROM_PTR(&securekey_hardwarekey_exportable_obj) }, +}; +static MP_DEFINE_CONST_DICT(securekey_hardwarekey_locals_dict, securekey_hardwarekey_locals_dict_table); + +MP_DEFINE_CONST_OBJ_TYPE( + securekey_hardwarekey_type, + MP_QSTR_HardwareKey, + MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS, + make_new, securekey_hardwarekey_make_new, + locals_dict, &securekey_hardwarekey_locals_dict + ); diff --git a/shared-bindings/securekey/HardwareKey.h b/shared-bindings/securekey/HardwareKey.h new file mode 100644 index 00000000000..2caa7d18e93 --- /dev/null +++ b/shared-bindings/securekey/HardwareKey.h @@ -0,0 +1,16 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Mike Mabey +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include "py/obj.h" + +// Object struct and the common_hal_* contract (construct is per-port; the +// operations are implemented once in shared-module/securekey/HardwareKey.c). +#include "shared-module/securekey/HardwareKey.h" + +// Type object used in Python. Shared between ports. +extern const mp_obj_type_t securekey_hardwarekey_type; diff --git a/shared-bindings/securekey/__init__.c b/shared-bindings/securekey/__init__.c new file mode 100644 index 00000000000..3d1e651e151 --- /dev/null +++ b/shared-bindings/securekey/__init__.c @@ -0,0 +1,43 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Mike Mabey +// +// SPDX-License-Identifier: MIT + +#include "py/obj.h" +#include "py/runtime.h" + +#include "shared-bindings/securekey/__init__.h" +#include "shared-bindings/securekey/HardwareKey.h" + +//| """Cryptographic operations with keys held in hardware +//| +//| The ``securekey`` module exposes keys that live in a hardware key store -- +//| eFuse, a key manager, a secure element -- and can be *used* but never read +//| back. Application code can compute a MAC (and, in the future, a signature) +//| with the key; there is no API to read the raw key bytes, and no API to +//| write or burn keys. Provisioning a key is a manufacturing-time step done +//| with vendor tools (for example ``espefuse.py`` on Espressif chips). +//| +//| The operations are portable. Selecting *which* hardware key to use is not: +//| the `HardwareKey` constructor takes a port-defined identifier, in the same +//| way that :mod:`board` pin objects are port-defined. +//| +//| Availability by port: +//| +//| * **espressif** (ESP32-S2/S3/C3/C6/H2/P4): the on-chip HMAC peripheral +//| against an eFuse key block burned with purpose ``HMAC_UP``. +//| """ + +static const mp_rom_map_elem_t securekey_module_globals_table[] = { + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_securekey) }, + { MP_ROM_QSTR(MP_QSTR_HardwareKey), MP_ROM_PTR(&securekey_hardwarekey_type) }, +}; +static MP_DEFINE_CONST_DICT(securekey_module_globals, securekey_module_globals_table); + +const mp_obj_module_t securekey_module = { + .base = { &mp_type_module }, + .globals = (mp_obj_dict_t *)&securekey_module_globals, +}; + +MP_REGISTER_MODULE(MP_QSTR_securekey, securekey_module); diff --git a/shared-bindings/securekey/__init__.h b/shared-bindings/securekey/__init__.h new file mode 100644 index 00000000000..459b9fc64f4 --- /dev/null +++ b/shared-bindings/securekey/__init__.h @@ -0,0 +1,7 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Mike Mabey +// +// SPDX-License-Identifier: MIT + +#pragma once diff --git a/shared-module/securekey/HardwareKey.c b/shared-module/securekey/HardwareKey.c new file mode 100644 index 00000000000..523f80aa6c4 --- /dev/null +++ b/shared-module/securekey/HardwareKey.c @@ -0,0 +1,55 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Mike Mabey +// +// SPDX-License-Identifier: MIT + +#include "shared-module/securekey/HardwareKey.h" + +#include "py/runtime.h" + +#include "psa/crypto.h" + +#define HMAC_SHA256_DIGEST_SIZE SECUREKEY_HMAC_SHA256_DIGEST_SIZE +#define SECUREKEY_ALG (PSA_ALG_HMAC(PSA_ALG_SHA_256)) + +// The operations here are port-independent: they act on self->key_id, which the +// port's common-hal construct() resolved from the hardware key slot. Any port +// with a PSA Crypto backend (Espressif today, a future Zephyr port, ...) reuses +// this file unchanged. + +void common_hal_securekey_hardwarekey_hmac_sha256(securekey_hardwarekey_obj_t *self, + const uint8_t *data, size_t data_len, uint8_t *mac_out, size_t mac_out_len) { + size_t mac_len = 0; + psa_status_t status = psa_mac_compute(self->key_id, SECUREKEY_ALG, + data, data_len, mac_out, mac_out_len, &mac_len); + if (status != PSA_SUCCESS || mac_len != HMAC_SHA256_DIGEST_SIZE) { + mp_raise_RuntimeError(MP_ERROR_TEXT("HMAC calculation failed")); + } +} + +bool common_hal_securekey_hardwarekey_verify_hmac_sha256(securekey_hardwarekey_obj_t *self, + const uint8_t *data, size_t data_len, const uint8_t *mac, size_t mac_len) { + if (mac_len != HMAC_SHA256_DIGEST_SIZE) { + mp_raise_ValueError_varg(MP_ERROR_TEXT("%q length must be %d"), MP_QSTR_mac, HMAC_SHA256_DIGEST_SIZE); + } + psa_status_t status = psa_mac_verify(self->key_id, SECUREKEY_ALG, + data, data_len, mac, mac_len); + switch (status) { + case PSA_SUCCESS: + return true; + case PSA_ERROR_INVALID_SIGNATURE: + return false; + default: + mp_raise_RuntimeError(MP_ERROR_TEXT("HMAC calculation failed")); + return false; + } +} + +mp_int_t common_hal_securekey_hardwarekey_get_key_slot(securekey_hardwarekey_obj_t *self) { + return self->key_slot; +} + +bool common_hal_securekey_hardwarekey_get_exportable(securekey_hardwarekey_obj_t *self) { + return self->exportable; +} diff --git a/shared-module/securekey/HardwareKey.h b/shared-module/securekey/HardwareKey.h new file mode 100644 index 00000000000..10ce3ffde7b --- /dev/null +++ b/shared-module/securekey/HardwareKey.h @@ -0,0 +1,38 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Mike Mabey +// +// SPDX-License-Identifier: MIT + +#pragma once + +#include + +#include "py/obj.h" + +#include "psa/crypto.h" + +#define SECUREKEY_HMAC_SHA256_DIGEST_SIZE 32 + +// The handle is portable: it holds a PSA key id. How that id gets created -- +// which hardware key store, which slot -- is the one port-specific step, done +// by common_hal_securekey_hardwarekey_construct() in each port's common-hal. +typedef struct { + mp_obj_base_t base; + psa_key_id_t key_id; + mp_int_t key_slot; + bool exportable; +} securekey_hardwarekey_obj_t; + +// Implemented per-port in common-hal/securekey/HardwareKey.c. Must fail closed: +// a slot that does not name a key usable for HMAC-SHA256 is an error, never a +// silent success. Fills in key_id, key_slot and exportable on success. +void common_hal_securekey_hardwarekey_construct(securekey_hardwarekey_obj_t *self, mp_int_t key_slot); + +// Implemented once in shared-module/securekey/HardwareKey.c on top of PSA. +void common_hal_securekey_hardwarekey_hmac_sha256(securekey_hardwarekey_obj_t *self, + const uint8_t *data, size_t data_len, uint8_t *mac_out, size_t mac_out_len); +bool common_hal_securekey_hardwarekey_verify_hmac_sha256(securekey_hardwarekey_obj_t *self, + const uint8_t *data, size_t data_len, const uint8_t *mac, size_t mac_len); +mp_int_t common_hal_securekey_hardwarekey_get_key_slot(securekey_hardwarekey_obj_t *self); +bool common_hal_securekey_hardwarekey_get_exportable(securekey_hardwarekey_obj_t *self);