From 6a2e0ab331a081aa11a90becc3f8644a68c2cfc9 Mon Sep 17 00:00:00 2001 From: Mike Mabey Date: Sat, 5 Sep 2026 16:50:37 -0600 Subject: [PATCH 1/8] 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); From 91ed703904965f48f0a914c14f7f3a9a8f75d964 Mon Sep 17 00:00:00 2001 From: Mike Mabey Date: Thu, 10 Sep 2026 17:16:50 -0600 Subject: [PATCH 2/8] hardwarekey: rename the securekey module to hardwarekey Per review feedback on #11319: "securekey" over-claims. The module can't guarantee the key is safe -- that depends on the eFuse burn, RD_DIS, the chip, and the threat model. "hardwarekey" just describes where the key lives (a hardware key store), which is the honest claim. Pure rename, no behavior change: - shared-bindings/securekey/ -> shared-bindings/hardwarekey/ - shared-module/securekey/ -> shared-module/hardwarekey/ - ports/espressif/common-hal/securekey/ -> .../hardwarekey/ - securekey_hardwarekey_* -> hardwarekey_hardwarekey_* (C symbols) - SECUREKEY_* macros -> HARDWAREKEY_* - CIRCUITPY_SECUREKEY -> CIRCUITPY_HARDWAREKEY The reshape tannewt asked for (HardwareKey as a board singleton, crypto methods moving to the hmac module) is in following commits. --- locale/circuitpython.pot | 6 +- .../{securekey => hardwarekey}/HardwareKey.c | 10 +-- .../{securekey => hardwarekey}/__init__.c | 2 +- ports/espressif/mpconfigport.mk | 8 +-- py/circuitpy_defns.mk | 12 ++-- py/circuitpy_mpconfig.mk | 8 +-- .../{securekey => hardwarekey}/HardwareKey.c | 66 +++++++++---------- .../{securekey => hardwarekey}/HardwareKey.h | 6 +- .../{securekey => hardwarekey}/__init__.c | 20 +++--- .../{securekey => hardwarekey}/__init__.h | 0 .../{securekey => hardwarekey}/HardwareKey.c | 18 ++--- .../{securekey => hardwarekey}/HardwareKey.h | 20 +++--- 12 files changed, 88 insertions(+), 88 deletions(-) rename ports/espressif/common-hal/{securekey => hardwarekey}/HardwareKey.c (89%) rename ports/espressif/common-hal/{securekey => hardwarekey}/__init__.c (75%) rename shared-bindings/{securekey => hardwarekey}/HardwareKey.c (55%) rename shared-bindings/{securekey => hardwarekey}/HardwareKey.h (64%) rename shared-bindings/{securekey => hardwarekey}/__init__.c (62%) rename shared-bindings/{securekey => hardwarekey}/__init__.h (100%) rename shared-module/{securekey => hardwarekey}/HardwareKey.c (67%) rename shared-module/{securekey => hardwarekey}/HardwareKey.h (51%) diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot index c1ac45c6cbb..1ce7a52c238 100644 --- a/locale/circuitpython.pot +++ b/locale/circuitpython.pot @@ -1459,15 +1459,15 @@ msgstr "" msgid "invalid setting" msgstr "" -#: ports/espressif/common-hal/securekey/HardwareKey.c +#: ports/espressif/common-hal/hardwarekey/HardwareKey.c msgid "key_slot is not configured for HMAC use" msgstr "" -#: ports/espressif/common-hal/securekey/HardwareKey.c +#: ports/espressif/common-hal/hardwarekey/HardwareKey.c msgid "crypto init failed" msgstr "" -#: shared-module/securekey/HardwareKey.c +#: shared-module/hardwarekey/HardwareKey.c msgid "HMAC calculation failed" msgstr "" diff --git a/ports/espressif/common-hal/securekey/HardwareKey.c b/ports/espressif/common-hal/hardwarekey/HardwareKey.c similarity index 89% rename from ports/espressif/common-hal/securekey/HardwareKey.c rename to ports/espressif/common-hal/hardwarekey/HardwareKey.c index f4706e8ca7e..b9bfdb7b638 100644 --- a/ports/espressif/common-hal/securekey/HardwareKey.c +++ b/ports/espressif/common-hal/hardwarekey/HardwareKey.c @@ -6,9 +6,9 @@ // 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. +// verify_hmac_sha256() -- lives in shared-module/hardwarekey/HardwareKey.c. -#include "shared-module/securekey/HardwareKey.h" +#include "shared-module/hardwarekey/HardwareKey.h" #include "py/runtime.h" @@ -25,7 +25,7 @@ #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)" +#error "hardwarekey 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 @@ -43,7 +43,7 @@ // 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) { +void common_hal_hardwarekey_hardwarekey_construct(hardwarekey_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); @@ -56,7 +56,7 @@ void common_hal_securekey_hardwarekey_construct(securekey_hardwarekey_obj_t *sel 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. + // idempotent and this keeps hardwarekey usable on its own. if (psa_crypto_init() != PSA_SUCCESS) { mp_raise_RuntimeError(MP_ERROR_TEXT("crypto init failed")); } diff --git a/ports/espressif/common-hal/securekey/__init__.c b/ports/espressif/common-hal/hardwarekey/__init__.c similarity index 75% rename from ports/espressif/common-hal/securekey/__init__.c rename to ports/espressif/common-hal/hardwarekey/__init__.c index 80a51d35e67..f834bb130ee 100644 --- a/ports/espressif/common-hal/securekey/__init__.c +++ b/ports/espressif/common-hal/hardwarekey/__init__.c @@ -4,5 +4,5 @@ // // SPDX-License-Identifier: MIT -// No securekey module-level functions. The port-specific code is the +// No hardwarekey 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 b10d2bf5a74..e1f3a13e4e1 100644 --- a/ports/espressif/mpconfigport.mk +++ b/ports/espressif/mpconfigport.mk @@ -83,6 +83,7 @@ CIRCUITPY_ESPIDF ?= 1 CIRCUITPY_ESPULP ?= 1 CIRCUITPY_FRAMEBUFFERIO ?= 1 CIRCUITPY_FREQUENCYIO ?= 1 +CIRCUITPY_HARDWAREKEY ?= 1 CIRCUITPY_HASHLIB ?= 1 CIRCUITPY_I2CTARGET = 0 CIRCUITPY_MAX3421E ?= 1 @@ -94,7 +95,6 @@ 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 @@ -110,7 +110,7 @@ ifeq ($(IDF_TARGET),esp32) CIRCUITPY_RGBMATRIX = 0 # No HMAC peripheral (introduced starting with ESP32-S2) -CIRCUITPY_SECUREKEY = 0 +CIRCUITPY_HARDWAREKEY = 0 # Has no USB CIRCUITPY_USB_DEVICE = 0 @@ -126,7 +126,7 @@ CIRCUITPY_ESPULP = 0 CIRCUITPY_MEMORYMAP = 0 # No HMAC peripheral (SOC_HMAC_SUPPORTED is not defined for this target) -CIRCUITPY_SECUREKEY = 0 +CIRCUITPY_HARDWAREKEY = 0 # No capacitive touch peripheral CIRCUITPY_ALARM_TOUCH = 0 @@ -270,7 +270,7 @@ CIRCUITPY_MEMORYMAP = 0 CIRCUITPY_RGBMATRIX = 0 # No HMAC peripheral (SOC_HMAC_SUPPORTED is not defined for this target) -CIRCUITPY_SECUREKEY = 0 +CIRCUITPY_HARDWAREKEY = 0 # No capacitive touch peripheral CIRCUITPY_ALARM_TOUCH = 0 diff --git a/py/circuitpy_defns.mk b/py/circuitpy_defns.mk index 90af68d547b..eec7e3ba1e4 100755 --- a/py/circuitpy_defns.mk +++ b/py/circuitpy_defns.mk @@ -261,6 +261,9 @@ endif ifeq ($(CIRCUITPY_GNSS),1) SRC_PATTERNS += gnss/% endif +ifeq ($(CIRCUITPY_HARDWAREKEY),1) +SRC_PATTERNS += hardwarekey/% +endif ifeq ($(CIRCUITPY_HASHLIB),1) SRC_PATTERNS += hashlib/% endif @@ -396,9 +399,6 @@ 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 @@ -604,8 +604,8 @@ SRC_COMMON_HAL_ALL = \ rtc/__init__.c \ sdioio/SDCard.c \ sdioio/__init__.c \ - securekey/HardwareKey.c \ - securekey/__init__.c \ + hardwarekey/HardwareKey.c \ + hardwarekey/__init__.c \ socketpool/__init__.c \ socketpool/SocketPool.c \ socketpool/Socket.c \ @@ -848,7 +848,7 @@ SRC_SHARED_MODULE_ALL = \ rotaryio/IncrementalEncoder.c \ sdcardio/SDCard.c \ sdcardio/__init__.c \ - securekey/HardwareKey.c \ + hardwarekey/HardwareKey.c \ sharpdisplay/SharpMemoryFramebuffer.c \ sharpdisplay/__init__.c \ socket/__init__.c \ diff --git a/py/circuitpy_mpconfig.mk b/py/circuitpy_mpconfig.mk index 82a083c6cc5..61eb763f0ba 100755 --- a/py/circuitpy_mpconfig.mk +++ b/py/circuitpy_mpconfig.mk @@ -561,10 +561,10 @@ 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) +# hardwarekey: cryptographic operations with hardware-held, non-readable keys. +# Off unless a port provides a common-hal/hardwarekey backend. +CIRCUITPY_HARDWAREKEY ?= 0 +CFLAGS += -DCIRCUITPY_HARDWAREKEY=$(CIRCUITPY_HARDWAREKEY) 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/hardwarekey/HardwareKey.c similarity index 55% rename from shared-bindings/securekey/HardwareKey.c rename to shared-bindings/hardwarekey/HardwareKey.c index 3fe3bf89849..f1122488101 100644 --- a/shared-bindings/securekey/HardwareKey.c +++ b/shared-bindings/hardwarekey/HardwareKey.c @@ -8,9 +8,9 @@ #include "py/objstr.h" #include "py/runtime.h" -#include "shared-bindings/securekey/HardwareKey.h" +#include "shared-bindings/hardwarekey/HardwareKey.h" -#define HMAC_SHA256_DIGEST_SIZE SECUREKEY_HMAC_SHA256_DIGEST_SIZE +#define HMAC_SHA256_DIGEST_SIZE HARDWAREKEY_HMAC_SHA256_DIGEST_SIZE //| class HardwareKey: //| """A key held in a hardware key store, usable but not readable. @@ -31,7 +31,7 @@ //| :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) { +static mp_obj_t hardwarekey_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 }, @@ -40,8 +40,8 @@ static mp_obj_t securekey_hardwarekey_make_new(const mp_obj_type_t *type, size_t 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); + hardwarekey_hardwarekey_obj_t *self = mp_obj_malloc(hardwarekey_hardwarekey_obj_t, &hardwarekey_hardwarekey_type); + common_hal_hardwarekey_hardwarekey_construct(self, args[ARG_key_slot].u_int); return MP_OBJ_FROM_PTR(self); } @@ -53,8 +53,8 @@ static mp_obj_t securekey_hardwarekey_make_new(const mp_obj_type_t *type, size_t //| :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); +static mp_obj_t hardwarekey_hardwarekey_hmac_sha256(mp_obj_t self_in, mp_obj_t data_in) { + hardwarekey_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); @@ -62,11 +62,11 @@ static mp_obj_t securekey_hardwarekey_hmac_sha256(mp_obj_t self_in, mp_obj_t dat 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, + common_hal_hardwarekey_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); +static MP_DEFINE_CONST_FUN_OBJ_2(hardwarekey_hardwarekey_hmac_sha256_obj, hardwarekey_hardwarekey_hmac_sha256); //| def verify_hmac_sha256(self, data: ReadableBuffer, mac: ReadableBuffer) -> bool: //| """Return ``True`` if ``mac`` is the correct HMAC-SHA256 of ``data`` @@ -76,28 +76,28 @@ static MP_DEFINE_CONST_FUN_OBJ_2(securekey_hardwarekey_hmac_sha256_obj, secureke //| :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); +static mp_obj_t hardwarekey_hardwarekey_verify_hmac_sha256(mp_obj_t self_in, mp_obj_t data_in, mp_obj_t mac_in) { + hardwarekey_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, + bool ok = common_hal_hardwarekey_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); +static MP_DEFINE_CONST_FUN_OBJ_3(hardwarekey_hardwarekey_verify_hmac_sha256_obj, hardwarekey_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)); +static mp_obj_t hardwarekey_hardwarekey_get_key_slot(mp_obj_t self_in) { + hardwarekey_hardwarekey_obj_t *self = MP_OBJ_TO_PTR(self_in); + return MP_OBJ_NEW_SMALL_INT(common_hal_hardwarekey_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); +MP_DEFINE_CONST_FUN_OBJ_1(hardwarekey_hardwarekey_get_key_slot_obj, hardwarekey_hardwarekey_get_key_slot); +MP_PROPERTY_GETTER(hardwarekey_hardwarekey_key_slot_obj, (mp_obj_t)&hardwarekey_hardwarekey_get_key_slot_obj); //| exportable: bool //| """Whether the raw key bytes can ever leave the hardware. Always @@ -107,25 +107,25 @@ MP_PROPERTY_GETTER(securekey_hardwarekey_key_slot_obj, (mp_obj_t)&securekey_hard //| 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)); +static mp_obj_t hardwarekey_hardwarekey_get_exportable(mp_obj_t self_in) { + hardwarekey_hardwarekey_obj_t *self = MP_OBJ_TO_PTR(self_in); + return mp_obj_new_bool(common_hal_hardwarekey_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) }, +MP_DEFINE_CONST_FUN_OBJ_1(hardwarekey_hardwarekey_get_exportable_obj, hardwarekey_hardwarekey_get_exportable); +MP_PROPERTY_GETTER(hardwarekey_hardwarekey_exportable_obj, (mp_obj_t)&hardwarekey_hardwarekey_get_exportable_obj); + +static const mp_rom_map_elem_t hardwarekey_hardwarekey_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR_hmac_sha256), MP_ROM_PTR(&hardwarekey_hardwarekey_hmac_sha256_obj) }, + { MP_ROM_QSTR(MP_QSTR_verify_hmac_sha256), MP_ROM_PTR(&hardwarekey_hardwarekey_verify_hmac_sha256_obj) }, + { MP_ROM_QSTR(MP_QSTR_key_slot), MP_ROM_PTR(&hardwarekey_hardwarekey_key_slot_obj) }, + { MP_ROM_QSTR(MP_QSTR_exportable), MP_ROM_PTR(&hardwarekey_hardwarekey_exportable_obj) }, }; -static MP_DEFINE_CONST_DICT(securekey_hardwarekey_locals_dict, securekey_hardwarekey_locals_dict_table); +static MP_DEFINE_CONST_DICT(hardwarekey_hardwarekey_locals_dict, hardwarekey_hardwarekey_locals_dict_table); MP_DEFINE_CONST_OBJ_TYPE( - securekey_hardwarekey_type, + hardwarekey_hardwarekey_type, MP_QSTR_HardwareKey, MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS, - make_new, securekey_hardwarekey_make_new, - locals_dict, &securekey_hardwarekey_locals_dict + make_new, hardwarekey_hardwarekey_make_new, + locals_dict, &hardwarekey_hardwarekey_locals_dict ); diff --git a/shared-bindings/securekey/HardwareKey.h b/shared-bindings/hardwarekey/HardwareKey.h similarity index 64% rename from shared-bindings/securekey/HardwareKey.h rename to shared-bindings/hardwarekey/HardwareKey.h index 2caa7d18e93..967a66b3bc4 100644 --- a/shared-bindings/securekey/HardwareKey.h +++ b/shared-bindings/hardwarekey/HardwareKey.h @@ -9,8 +9,8 @@ #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" +// operations are implemented once in shared-module/hardwarekey/HardwareKey.c). +#include "shared-module/hardwarekey/HardwareKey.h" // Type object used in Python. Shared between ports. -extern const mp_obj_type_t securekey_hardwarekey_type; +extern const mp_obj_type_t hardwarekey_hardwarekey_type; diff --git a/shared-bindings/securekey/__init__.c b/shared-bindings/hardwarekey/__init__.c similarity index 62% rename from shared-bindings/securekey/__init__.c rename to shared-bindings/hardwarekey/__init__.c index 3d1e651e151..9a89a4ed71c 100644 --- a/shared-bindings/securekey/__init__.c +++ b/shared-bindings/hardwarekey/__init__.c @@ -7,12 +7,12 @@ #include "py/obj.h" #include "py/runtime.h" -#include "shared-bindings/securekey/__init__.h" -#include "shared-bindings/securekey/HardwareKey.h" +#include "shared-bindings/hardwarekey/__init__.h" +#include "shared-bindings/hardwarekey/HardwareKey.h" //| """Cryptographic operations with keys held in hardware //| -//| The ``securekey`` module exposes keys that live in a hardware key store -- +//| The ``hardwarekey`` 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 @@ -29,15 +29,15 @@ //| 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 const mp_rom_map_elem_t hardwarekey_module_globals_table[] = { + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_hardwarekey) }, + { MP_ROM_QSTR(MP_QSTR_HardwareKey), MP_ROM_PTR(&hardwarekey_hardwarekey_type) }, }; -static MP_DEFINE_CONST_DICT(securekey_module_globals, securekey_module_globals_table); +static MP_DEFINE_CONST_DICT(hardwarekey_module_globals, hardwarekey_module_globals_table); -const mp_obj_module_t securekey_module = { +const mp_obj_module_t hardwarekey_module = { .base = { &mp_type_module }, - .globals = (mp_obj_dict_t *)&securekey_module_globals, + .globals = (mp_obj_dict_t *)&hardwarekey_module_globals, }; -MP_REGISTER_MODULE(MP_QSTR_securekey, securekey_module); +MP_REGISTER_MODULE(MP_QSTR_hardwarekey, hardwarekey_module); diff --git a/shared-bindings/securekey/__init__.h b/shared-bindings/hardwarekey/__init__.h similarity index 100% rename from shared-bindings/securekey/__init__.h rename to shared-bindings/hardwarekey/__init__.h diff --git a/shared-module/securekey/HardwareKey.c b/shared-module/hardwarekey/HardwareKey.c similarity index 67% rename from shared-module/securekey/HardwareKey.c rename to shared-module/hardwarekey/HardwareKey.c index 523f80aa6c4..50ab6313284 100644 --- a/shared-module/securekey/HardwareKey.c +++ b/shared-module/hardwarekey/HardwareKey.c @@ -4,36 +4,36 @@ // // SPDX-License-Identifier: MIT -#include "shared-module/securekey/HardwareKey.h" +#include "shared-module/hardwarekey/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)) +#define HMAC_SHA256_DIGEST_SIZE HARDWAREKEY_HMAC_SHA256_DIGEST_SIZE +#define HARDWAREKEY_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, +void common_hal_hardwarekey_hardwarekey_hmac_sha256(hardwarekey_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, + psa_status_t status = psa_mac_compute(self->key_id, HARDWAREKEY_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, +bool common_hal_hardwarekey_hardwarekey_verify_hmac_sha256(hardwarekey_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, + psa_status_t status = psa_mac_verify(self->key_id, HARDWAREKEY_ALG, data, data_len, mac, mac_len); switch (status) { case PSA_SUCCESS: @@ -46,10 +46,10 @@ bool common_hal_securekey_hardwarekey_verify_hmac_sha256(securekey_hardwarekey_o } } -mp_int_t common_hal_securekey_hardwarekey_get_key_slot(securekey_hardwarekey_obj_t *self) { +mp_int_t common_hal_hardwarekey_hardwarekey_get_key_slot(hardwarekey_hardwarekey_obj_t *self) { return self->key_slot; } -bool common_hal_securekey_hardwarekey_get_exportable(securekey_hardwarekey_obj_t *self) { +bool common_hal_hardwarekey_hardwarekey_get_exportable(hardwarekey_hardwarekey_obj_t *self) { return self->exportable; } diff --git a/shared-module/securekey/HardwareKey.h b/shared-module/hardwarekey/HardwareKey.h similarity index 51% rename from shared-module/securekey/HardwareKey.h rename to shared-module/hardwarekey/HardwareKey.h index 10ce3ffde7b..ac2c47816c9 100644 --- a/shared-module/securekey/HardwareKey.h +++ b/shared-module/hardwarekey/HardwareKey.h @@ -12,27 +12,27 @@ #include "psa/crypto.h" -#define SECUREKEY_HMAC_SHA256_DIGEST_SIZE 32 +#define HARDWAREKEY_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. +// by common_hal_hardwarekey_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; +} hardwarekey_hardwarekey_obj_t; -// Implemented per-port in common-hal/securekey/HardwareKey.c. Must fail closed: +// Implemented per-port in common-hal/hardwarekey/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); +void common_hal_hardwarekey_hardwarekey_construct(hardwarekey_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, +// Implemented once in shared-module/hardwarekey/HardwareKey.c on top of PSA. +void common_hal_hardwarekey_hardwarekey_hmac_sha256(hardwarekey_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, +bool common_hal_hardwarekey_hardwarekey_verify_hmac_sha256(hardwarekey_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); +mp_int_t common_hal_hardwarekey_hardwarekey_get_key_slot(hardwarekey_hardwarekey_obj_t *self); +bool common_hal_hardwarekey_hardwarekey_get_exportable(hardwarekey_hardwarekey_obj_t *self); From 6d40a1c45f9c652acd13a634d25080748ca9c967 Mon Sep 17 00:00:00 2001 From: Mike Mabey Date: Thu, 10 Sep 2026 17:41:03 -0600 Subject: [PATCH 3/8] hardwarekey: make HardwareKey a board object; add Purpose Per tannewt's review of #11319: HardwareKey is no longer constructed by application code. Every eFuse key block is a fixed object in `board` (board.EFUSE_KEY0 .. board.EFUSE_KEY5), like a pin. A startup probe in port_init() reads each block's eFuse purpose and, for blocks burned HMAC_UP, imports the PSA key. - shared-bindings/board/__init__.h: new CIRCUITPY_BOARD_EXTRA_DICT_ITEMS hook, appended to the standard board-globals macros, so a port can inject fixed entries into every board with no per-board pins.c change. espressif fills it in from common-hal/hardwarekey/board.h. - The board.EFUSE_KEY* objects are static (they outlive the GC heap across a soft reset), pointed at by the const board dict -- the same pattern board.DISPLAY uses. No mutable-board needed. - New `Purpose` enum with singletons hardwarekey.HMAC_UP and hardwarekey.UNUSED. A block with no HMAC_UP key still has a HardwareKey; its .purpose is UNUSED. New .purpose property and a repr. - HardwareKey has no make_new; hmac_sha256() / verify_hmac_sha256() stay for now (they move to the hmac module next). Verified on an ESP32-S3-DevKitC-1-N8R8: board.EFUSE_KEY5 (burned HMAC_UP) computes the expected HMAC; board.EFUSE_KEY0 (unburned) and EFUSE_KEY4 (burned for the DS peripheral, not HMAC_UP) both report purpose UNUSED. --- locale/circuitpython.pot | 17 +-- .../common-hal/hardwarekey/HardwareKey.c | 103 +++++++++--------- .../common-hal/hardwarekey/__init__.c | 26 ++++- .../common-hal/hardwarekey/__init__.h | 20 ++++ .../espressif/common-hal/hardwarekey/board.h | 30 +++++ ports/espressif/supervisor/port.c | 9 ++ shared-bindings/board/__init__.h | 17 ++- shared-bindings/hardwarekey/HardwareKey.c | 60 +++++----- shared-bindings/hardwarekey/__init__.c | 61 +++++++++-- shared-bindings/hardwarekey/__init__.h | 12 ++ shared-module/hardwarekey/HardwareKey.c | 4 + shared-module/hardwarekey/HardwareKey.h | 22 +++- 12 files changed, 269 insertions(+), 112 deletions(-) create mode 100644 ports/espressif/common-hal/hardwarekey/__init__.h create mode 100644 ports/espressif/common-hal/hardwarekey/board.h diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot index 1ce7a52c238..0464f9d7bd4 100644 --- a/locale/circuitpython.pot +++ b/locale/circuitpython.pot @@ -1459,18 +1459,6 @@ msgstr "" msgid "invalid setting" msgstr "" -#: ports/espressif/common-hal/hardwarekey/HardwareKey.c -msgid "key_slot is not configured for HMAC use" -msgstr "" - -#: ports/espressif/common-hal/hardwarekey/HardwareKey.c -msgid "crypto init failed" -msgstr "" - -#: shared-module/hardwarekey/HardwareKey.c -msgid "HMAC calculation failed" -msgstr "" - #: ports/espressif/common-hal/espidf/__init__.c msgid "Generic Failure" msgstr "" @@ -2371,6 +2359,7 @@ msgid "%q length must be <= %d" msgstr "" #: py/argcheck.c shared-bindings/usb_hid/Device.c +#: shared-module/hardwarekey/HardwareKey.c msgid "%q length must be %d" msgstr "" @@ -4378,6 +4367,10 @@ msgstr "" msgid "unsupported colorspace for GifWriter" msgstr "" +#: shared-module/hardwarekey/HardwareKey.c +msgid "HMAC calculation failed" +msgstr "" + #: shared-module/hmac/HMAC.c msgid "HMAC operation failed" msgstr "" diff --git a/ports/espressif/common-hal/hardwarekey/HardwareKey.c b/ports/espressif/common-hal/hardwarekey/HardwareKey.c index b9bfdb7b638..a93a4346d59 100644 --- a/ports/espressif/common-hal/hardwarekey/HardwareKey.c +++ b/ports/espressif/common-hal/hardwarekey/HardwareKey.c @@ -4,16 +4,22 @@ // // 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/hardwarekey/HardwareKey.c. +// The one port-specific step: turn an eFuse key block into a PSA key id. +// Everything after that -- hmac_sha256(), verify_hmac_sha256() -- lives in +// shared-module/hardwarekey/HardwareKey.c. -#include "shared-module/hardwarekey/HardwareKey.h" +#include "common-hal/hardwarekey/__init__.h" +#include "common-hal/hardwarekey/board.h" -#include "py/runtime.h" +#include "shared-module/hardwarekey/HardwareKey.h" #include "esp_efuse.h" +// board.h hardcodes the slot count (enum values can't be used in #if); make sure +// it still matches this chip's eFuse layout. +_Static_assert(HARDWAREKEY_EFUSE_SLOT_COUNT == EFUSE_BLK_KEY_MAX - EFUSE_BLK_KEY0, + "eFuse key block count changed; update common-hal/hardwarekey/board.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 @@ -28,60 +34,55 @@ #error "hardwarekey 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_hardwarekey_hardwarekey_construct(hardwarekey_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); +// One PSA key is imported per eFuse block. The imports are volatile references +// (no key material) and survive a CircuitPython soft reset -- ESP-IDF initializes +// PSA once at boot and never frees it -- so this only runs once per block. +static psa_key_id_t import_efuse_hmac_key(mp_int_t slot) { + 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)slot }; + + psa_key_id_t key_id = 0; + if (psa_import_key(&attr, (const uint8_t *)&keyref, sizeof(keyref), &key_id) != PSA_SUCCESS) { + return 0; } + return key_id; +} + +bool hardwarekey_efuse_slot_load(mp_int_t slot, hardwarekey_hardwarekey_obj_t *key) { + key->key_slot = slot; + key->key_id = 0; + key->purpose = HARDWAREKEY_PURPOSE_UNUSED; + key->exportable = false; - esp_efuse_block_t block = (esp_efuse_block_t)(EFUSE_BLK_KEY0 + key_slot); + esp_efuse_block_t block = (esp_efuse_block_t)(EFUSE_BLK_KEY0 + 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")); + return false; + } + + // PSA is already initialized by ssl / hashlib, but psa_crypto_init() is + // idempotent and keeps hardwarekey working on a build with neither. + if (psa_crypto_init() != PSA_SUCCESS) { + return false; } - if (imported_key[key_slot] == 0) { - // PSA is already initialized by ssl / hashlib, but psa_crypto_init() is - // idempotent and this keeps hardwarekey 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; + psa_key_id_t key_id = import_efuse_hmac_key(slot); + if (key_id == 0) { + return false; } - self->key_id = imported_key[key_slot]; - self->key_slot = key_slot; - self->exportable = !esp_efuse_get_key_dis_read(block); + key->key_id = key_id; + key->purpose = HARDWAREKEY_PURPOSE_HMAC; + key->exportable = !esp_efuse_get_key_dis_read(block); + return true; } diff --git a/ports/espressif/common-hal/hardwarekey/__init__.c b/ports/espressif/common-hal/hardwarekey/__init__.c index f834bb130ee..2d33ed9a4ab 100644 --- a/ports/espressif/common-hal/hardwarekey/__init__.c +++ b/ports/espressif/common-hal/hardwarekey/__init__.c @@ -4,5 +4,27 @@ // // SPDX-License-Identifier: MIT -// No hardwarekey module-level functions. The port-specific code is the -// HardwareKey constructor in HardwareKey.c. +#include "common-hal/hardwarekey/__init__.h" +#include "common-hal/hardwarekey/board.h" + +#include "shared-bindings/hardwarekey/HardwareKey.h" + +// The objects board.EFUSE_KEY0 .. board.EFUSE_KEY point at (see board.h). +// Static, not heap: the board globals dict is const and outlives the GC heap +// across a soft reset, so these must too. No GC-traced pointers inside. +hardwarekey_hardwarekey_obj_t hardwarekey_efuse_keys[HARDWAREKEY_EFUSE_SLOT_COUNT]; + +// board.EFUSE_KEYn, for repr(). Can't assume MP_QSTR_EFUSE_KEY0 + n are contiguous. +static const qstr slot_names[HARDWAREKEY_EFUSE_SLOT_COUNT] = { + MP_QSTR_EFUSE_KEY0, MP_QSTR_EFUSE_KEY1, MP_QSTR_EFUSE_KEY2, + MP_QSTR_EFUSE_KEY3, MP_QSTR_EFUSE_KEY4, MP_QSTR_EFUSE_KEY5, +}; + +void espressif_hardwarekey_init(void) { + for (mp_int_t slot = 0; slot < HARDWAREKEY_EFUSE_SLOT_COUNT; slot++) { + hardwarekey_hardwarekey_obj_t *key = &hardwarekey_efuse_keys[slot]; + key->base.type = &hardwarekey_hardwarekey_type; + key->name = slot_names[slot]; + hardwarekey_efuse_slot_load(slot, key); + } +} diff --git a/ports/espressif/common-hal/hardwarekey/__init__.h b/ports/espressif/common-hal/hardwarekey/__init__.h new file mode 100644 index 00000000000..e6c55b51bd2 --- /dev/null +++ b/ports/espressif/common-hal/hardwarekey/__init__.h @@ -0,0 +1,20 @@ +// 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" + +#include "shared-module/hardwarekey/HardwareKey.h" + +// Probe every eFuse key block and fill in the board.EFUSE_KEY* HardwareKey +// objects. Call once at startup, before user code. Never raises. +void espressif_hardwarekey_init(void); + +// Fill in `key` for eFuse key block `slot`. Sets key->purpose to HMAC when the +// block is burned HMAC_UP (importing its PSA key), else HARDWAREKEY_PURPOSE_UNUSED. +// Never raises. Returns true when the slot ended up usable. +bool hardwarekey_efuse_slot_load(mp_int_t slot, hardwarekey_hardwarekey_obj_t *key); diff --git a/ports/espressif/common-hal/hardwarekey/board.h b/ports/espressif/common-hal/hardwarekey/board.h new file mode 100644 index 00000000000..9a2e86e2198 --- /dev/null +++ b/ports/espressif/common-hal/hardwarekey/board.h @@ -0,0 +1,30 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Mike Mabey +// +// SPDX-License-Identifier: MIT + +#pragma once + +// board.EFUSE_KEY0 .. board.EFUSE_KEY5: one fixed entry per eFuse key block, +// injected into every board's globals table through CIRCUITPY_BOARD_EXTRA_DICT_ITEMS +// (see shared-bindings/board/__init__.h). Each points at a static HardwareKey the +// startup probe (espressif_hardwarekey_init) fills in -- so, like board pins, the +// names exist at compile time and the objects are ready before user code runs. + +#include "shared-module/hardwarekey/HardwareKey.h" + +// Every HMAC-capable Espressif chip (S2/S3/C3/C5/C6/H2/P4) has 6 eFuse key blocks. +// HardwareKey.c static-asserts this against EFUSE_BLK_KEY_MAX - EFUSE_BLK_KEY0; +// if a future chip differs, update this list (and the assert) to match. +#define HARDWAREKEY_EFUSE_SLOT_COUNT 6 + +extern hardwarekey_hardwarekey_obj_t hardwarekey_efuse_keys[HARDWAREKEY_EFUSE_SLOT_COUNT]; + +#define CIRCUITPY_BOARD_EXTRA_DICT_ITEMS \ + { MP_ROM_QSTR(MP_QSTR_EFUSE_KEY0), MP_ROM_PTR(&hardwarekey_efuse_keys[0]) }, \ + { MP_ROM_QSTR(MP_QSTR_EFUSE_KEY1), MP_ROM_PTR(&hardwarekey_efuse_keys[1]) }, \ + { MP_ROM_QSTR(MP_QSTR_EFUSE_KEY2), MP_ROM_PTR(&hardwarekey_efuse_keys[2]) }, \ + { MP_ROM_QSTR(MP_QSTR_EFUSE_KEY3), MP_ROM_PTR(&hardwarekey_efuse_keys[3]) }, \ + { MP_ROM_QSTR(MP_QSTR_EFUSE_KEY4), MP_ROM_PTR(&hardwarekey_efuse_keys[4]) }, \ + { MP_ROM_QSTR(MP_QSTR_EFUSE_KEY5), MP_ROM_PTR(&hardwarekey_efuse_keys[5]) }, diff --git a/ports/espressif/supervisor/port.c b/ports/espressif/supervisor/port.c index 34cfba32b7e..e6bc45754b4 100644 --- a/ports/espressif/supervisor/port.c +++ b/ports/espressif/supervisor/port.c @@ -28,6 +28,9 @@ #include "common-hal/busio/SPI.h" #include "common-hal/busio/UART.h" #include "common-hal/dualbank/__init__.h" +#if CIRCUITPY_HARDWAREKEY +#include "common-hal/hardwarekey/__init__.h" +#endif #include "common-hal/ps2io/Ps2.h" #include "common-hal/watchdog/WatchDogTimer.h" #include "common-hal/socketpool/Socket.h" @@ -287,6 +290,12 @@ safe_mode_t port_init(void) { _never_reset_spi_ram_flash(); + #if CIRCUITPY_HARDWAREKEY + // Populate board.EFUSE_KEY* from the eFuse key blocks. eFuse reads and the + // PSA key import need no filesystem or VM, so this is safe here. + espressif_hardwarekey_init(); + #endif + esp_reset_reason_t reason = esp_reset_reason(); switch (reason) { case ESP_RST_BROWNOUT: diff --git a/shared-bindings/board/__init__.h b/shared-bindings/board/__init__.h index 43343099730..764ec7a274e 100644 --- a/shared-bindings/board/__init__.h +++ b/shared-bindings/board/__init__.h @@ -11,6 +11,17 @@ #include "shared-bindings/microcontroller/Pin.h" // for the pin definitions +// A port can inject extra fixed entries into every board's globals table by +// defining CIRCUITPY_BOARD_EXTRA_DICT_ITEMS (a comma-terminated list of +// { MP_ROM_QSTR(...), MP_ROM_PTR(...) } pairs). hardwarekey uses this for +// board.EFUSE_KEY* on espressif. +#if CIRCUITPY_HARDWAREKEY +#include "common-hal/hardwarekey/board.h" +#endif +#ifndef CIRCUITPY_BOARD_EXTRA_DICT_ITEMS +#define CIRCUITPY_BOARD_EXTRA_DICT_ITEMS +#endif + #if CIRCUITPY_MUTABLE_BOARD extern mp_obj_dict_t board_module_globals; #else @@ -41,8 +52,10 @@ MP_DECLARE_CONST_FUN_OBJ_0(board_uart_obj); #define CIRCUITPYTHON_BOARD_DICT_STANDARD_ITEMS \ { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_board) }, \ - { MP_ROM_QSTR(MP_QSTR_board_id), MP_ROM_PTR(&board_module_id_obj) }, + { MP_ROM_QSTR(MP_QSTR_board_id), MP_ROM_PTR(&board_module_id_obj) }, \ + CIRCUITPY_BOARD_EXTRA_DICT_ITEMS #define CIRCUITPYTHON_MUTABLE_BOARD_DICT_STANDARD_ITEMS \ { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_board) }, \ - { MP_ROM_QSTR(MP_QSTR_board_id), MP_OBJ_FROM_PTR(&board_module_id_obj) }, + { MP_ROM_QSTR(MP_QSTR_board_id), MP_OBJ_FROM_PTR(&board_module_id_obj) }, \ + CIRCUITPY_BOARD_EXTRA_DICT_ITEMS diff --git a/shared-bindings/hardwarekey/HardwareKey.c b/shared-bindings/hardwarekey/HardwareKey.c index f1122488101..694ae351b54 100644 --- a/shared-bindings/hardwarekey/HardwareKey.c +++ b/shared-bindings/hardwarekey/HardwareKey.c @@ -8,6 +8,7 @@ #include "py/objstr.h" #include "py/runtime.h" +#include "shared-bindings/hardwarekey/__init__.h" #include "shared-bindings/hardwarekey/HardwareKey.h" #define HMAC_SHA256_DIGEST_SIZE HARDWAREKEY_HMAC_SHA256_DIGEST_SIZE @@ -15,35 +16,23 @@ //| class HardwareKey: //| """A key held in a hardware key store, usable but not readable. //| -//| The constructor argument that selects the key is **port-defined**: +//| This class cannot be instantiated. Every hardware key slot the board has +//| is exposed as a fixed `HardwareKey` in :mod:`board` -- for example +//| ``board.EFUSE_KEY0`` -- just like pins. A slot with no key burned into it +//| still has a `HardwareKey` object; its `purpose` is `hardwarekey.UNUSED`. //| -//| * **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. -//| """ +//| On espressif the slots are the eFuse key blocks (``BLOCK_KEY0`` - +//| ``BLOCK_KEY5``); a slot is usable only if its block was burned with +//| purpose ``HMAC_UP``.""" //| -//| 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 hardwarekey_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); - - hardwarekey_hardwarekey_obj_t *self = mp_obj_malloc(hardwarekey_hardwarekey_obj_t, &hardwarekey_hardwarekey_type); - common_hal_hardwarekey_hardwarekey_construct(self, args[ARG_key_slot].u_int); - - return MP_OBJ_FROM_PTR(self); + +static void hardwarekey_hardwarekey_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) { + hardwarekey_hardwarekey_obj_t *self = MP_OBJ_TO_PTR(self_in); + if (self->name != MP_QSTRnull) { + mp_printf(print, "", self->name); + } else { + mp_printf(print, "", (int)self->key_slot); + } } //| def hmac_sha256(self, data: ReadableBuffer) -> bytes: @@ -91,7 +80,8 @@ static mp_obj_t hardwarekey_hardwarekey_verify_hmac_sha256(mp_obj_t self_in, mp_ static MP_DEFINE_CONST_FUN_OBJ_3(hardwarekey_hardwarekey_verify_hmac_sha256_obj, hardwarekey_hardwarekey_verify_hmac_sha256); //| key_slot: int -//| """The port-defined key identifier this handle is bound to. (read-only)""" +//| """The port-defined key identifier this handle is bound to. On espressif, +//| the eFuse key block index. (read-only)""" static mp_obj_t hardwarekey_hardwarekey_get_key_slot(mp_obj_t self_in) { hardwarekey_hardwarekey_obj_t *self = MP_OBJ_TO_PTR(self_in); return MP_OBJ_NEW_SMALL_INT(common_hal_hardwarekey_hardwarekey_get_key_slot(self)); @@ -99,6 +89,17 @@ static mp_obj_t hardwarekey_hardwarekey_get_key_slot(mp_obj_t self_in) { MP_DEFINE_CONST_FUN_OBJ_1(hardwarekey_hardwarekey_get_key_slot_obj, hardwarekey_hardwarekey_get_key_slot); MP_PROPERTY_GETTER(hardwarekey_hardwarekey_key_slot_obj, (mp_obj_t)&hardwarekey_hardwarekey_get_key_slot_obj); +//| purpose: Purpose +//| """What this key slot is provisioned for -- `hardwarekey.HMAC_UP` or +//| `hardwarekey.UNUSED`. (read-only)""" +//| +static mp_obj_t hardwarekey_hardwarekey_get_purpose(mp_obj_t self_in) { + hardwarekey_hardwarekey_obj_t *self = MP_OBJ_TO_PTR(self_in); + return hardwarekey_purpose_to_obj(common_hal_hardwarekey_hardwarekey_get_purpose(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(hardwarekey_hardwarekey_get_purpose_obj, hardwarekey_hardwarekey_get_purpose); +MP_PROPERTY_GETTER(hardwarekey_hardwarekey_purpose_obj, (mp_obj_t)&hardwarekey_hardwarekey_get_purpose_obj); + //| exportable: bool //| """Whether the raw key bytes can ever leave the hardware. Always //| informational -- it does not gate `hmac_sha256`. @@ -118,6 +119,7 @@ static const mp_rom_map_elem_t hardwarekey_hardwarekey_locals_dict_table[] = { { MP_ROM_QSTR(MP_QSTR_hmac_sha256), MP_ROM_PTR(&hardwarekey_hardwarekey_hmac_sha256_obj) }, { MP_ROM_QSTR(MP_QSTR_verify_hmac_sha256), MP_ROM_PTR(&hardwarekey_hardwarekey_verify_hmac_sha256_obj) }, { MP_ROM_QSTR(MP_QSTR_key_slot), MP_ROM_PTR(&hardwarekey_hardwarekey_key_slot_obj) }, + { MP_ROM_QSTR(MP_QSTR_purpose), MP_ROM_PTR(&hardwarekey_hardwarekey_purpose_obj) }, { MP_ROM_QSTR(MP_QSTR_exportable), MP_ROM_PTR(&hardwarekey_hardwarekey_exportable_obj) }, }; static MP_DEFINE_CONST_DICT(hardwarekey_hardwarekey_locals_dict, hardwarekey_hardwarekey_locals_dict_table); @@ -126,6 +128,6 @@ MP_DEFINE_CONST_OBJ_TYPE( hardwarekey_hardwarekey_type, MP_QSTR_HardwareKey, MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS, - make_new, hardwarekey_hardwarekey_make_new, + print, hardwarekey_hardwarekey_print, locals_dict, &hardwarekey_hardwarekey_locals_dict ); diff --git a/shared-bindings/hardwarekey/__init__.c b/shared-bindings/hardwarekey/__init__.c index 9a89a4ed71c..b22ea380c93 100644 --- a/shared-bindings/hardwarekey/__init__.c +++ b/shared-bindings/hardwarekey/__init__.c @@ -4,6 +4,7 @@ // // SPDX-License-Identifier: MIT +#include "py/enum.h" #include "py/obj.h" #include "py/runtime.h" @@ -14,24 +15,62 @@ //| //| The ``hardwarekey`` 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). +//| back. Application code can compute a MAC 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. +//| `HardwareKey` objects are not created by application code. Every hardware key +//| slot the board has is exposed as a fixed object in :mod:`board` (for example +//| ``board.EFUSE_KEY0``), in the same way that pins are. Compute a MAC with one +//| by passing it to `hmac.new()`. +//| """ + +//| class Purpose: +//| """What a hardware key slot is provisioned for. Instances are singletons; +//| compare with ``is``.""" //| -//| Availability by port: +static void hardwarekey_purpose_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) { + cp_enum_obj_t *self = MP_OBJ_TO_PTR(self_in); + mp_printf(print, "%q.%q", MP_QSTR_hardwarekey, self->name); +} + +MP_DEFINE_CONST_OBJ_TYPE( + hardwarekey_purpose_type, + MP_QSTR_Purpose, + MP_TYPE_FLAG_NONE, + print, hardwarekey_purpose_print + ); + +//| HMAC_UP: Purpose +//| """The slot holds an HMAC key. It can be used with `hmac.new()`.""" +const cp_enum_obj_t hardwarekey_purpose_hmac_obj = { + { &hardwarekey_purpose_type }, HARDWAREKEY_PURPOSE_HMAC, MP_QSTR_HMAC_UP +}; + +//| UNUSED: Purpose +//| """No key is burned into the slot (or it is burned for something this module +//| does not expose). The slot's `HardwareKey` still exists but cannot be used.""" //| -//| * **espressif** (ESP32-S2/S3/C3/C6/H2/P4): the on-chip HMAC peripheral -//| against an eFuse key block burned with purpose ``HMAC_UP``. -//| """ +const cp_enum_obj_t hardwarekey_purpose_unused_obj = { + { &hardwarekey_purpose_type }, HARDWAREKEY_PURPOSE_UNUSED, MP_QSTR_UNUSED +}; + +mp_obj_t hardwarekey_purpose_to_obj(hardwarekey_purpose_t purpose) { + switch (purpose) { + case HARDWAREKEY_PURPOSE_HMAC: + return MP_OBJ_FROM_PTR(&hardwarekey_purpose_hmac_obj); + default: + return MP_OBJ_FROM_PTR(&hardwarekey_purpose_unused_obj); + } +} static const mp_rom_map_elem_t hardwarekey_module_globals_table[] = { { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_hardwarekey) }, { MP_ROM_QSTR(MP_QSTR_HardwareKey), MP_ROM_PTR(&hardwarekey_hardwarekey_type) }, + { MP_ROM_QSTR(MP_QSTR_Purpose), MP_ROM_PTR(&hardwarekey_purpose_type) }, + { MP_ROM_QSTR(MP_QSTR_HMAC_UP), MP_ROM_PTR(&hardwarekey_purpose_hmac_obj) }, + { MP_ROM_QSTR(MP_QSTR_UNUSED), MP_ROM_PTR(&hardwarekey_purpose_unused_obj) }, }; static MP_DEFINE_CONST_DICT(hardwarekey_module_globals, hardwarekey_module_globals_table); diff --git a/shared-bindings/hardwarekey/__init__.h b/shared-bindings/hardwarekey/__init__.h index 459b9fc64f4..064ed0ad485 100644 --- a/shared-bindings/hardwarekey/__init__.h +++ b/shared-bindings/hardwarekey/__init__.h @@ -5,3 +5,15 @@ // SPDX-License-Identifier: MIT #pragma once + +#include "py/enum.h" +#include "py/obj.h" + +#include "shared-module/hardwarekey/HardwareKey.h" + +extern const mp_obj_type_t hardwarekey_purpose_type; +extern const cp_enum_obj_t hardwarekey_purpose_hmac_obj; +extern const cp_enum_obj_t hardwarekey_purpose_unused_obj; + +// The Purpose singleton for a hardwarekey_purpose_t code. +mp_obj_t hardwarekey_purpose_to_obj(hardwarekey_purpose_t purpose); diff --git a/shared-module/hardwarekey/HardwareKey.c b/shared-module/hardwarekey/HardwareKey.c index 50ab6313284..d575a62aa10 100644 --- a/shared-module/hardwarekey/HardwareKey.c +++ b/shared-module/hardwarekey/HardwareKey.c @@ -50,6 +50,10 @@ mp_int_t common_hal_hardwarekey_hardwarekey_get_key_slot(hardwarekey_hardwarekey return self->key_slot; } +hardwarekey_purpose_t common_hal_hardwarekey_hardwarekey_get_purpose(hardwarekey_hardwarekey_obj_t *self) { + return self->purpose; +} + bool common_hal_hardwarekey_hardwarekey_get_exportable(hardwarekey_hardwarekey_obj_t *self) { return self->exportable; } diff --git a/shared-module/hardwarekey/HardwareKey.h b/shared-module/hardwarekey/HardwareKey.h index ac2c47816c9..4c7d316fa61 100644 --- a/shared-module/hardwarekey/HardwareKey.h +++ b/shared-module/hardwarekey/HardwareKey.h @@ -14,20 +14,31 @@ #define HARDWAREKEY_HMAC_SHA256_DIGEST_SIZE 32 +// What a hardware key slot is provisioned for. UNUSED means no key has been +// burned into the slot (or it is burned for something this module does not +// expose); the slot is present in `board` but not usable. +typedef enum { + HARDWAREKEY_PURPOSE_UNUSED = 0, + HARDWAREKEY_PURPOSE_HMAC, +} hardwarekey_purpose_t; + // 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_hardwarekey_hardwarekey_construct() in each port's common-hal. +// when the port populates its per-slot HardwareKey objects at startup. typedef struct { mp_obj_base_t base; psa_key_id_t key_id; mp_int_t key_slot; + hardwarekey_purpose_t purpose; bool exportable; + // Name this key is exposed under in `board` (e.g. MP_QSTR_EFUSE_KEY0), for + // repr(). MP_QSTRnull if the object was not placed in `board`. + qstr name; } hardwarekey_hardwarekey_obj_t; -// Implemented per-port in common-hal/hardwarekey/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_hardwarekey_hardwarekey_construct(hardwarekey_hardwarekey_obj_t *self, mp_int_t key_slot); +// HardwareKey objects are created by the port at startup, one per hardware key +// slot, and placed in `board`; application code never constructs them. The +// per-port startup code fills in key_id, key_slot, purpose, exportable and name. // Implemented once in shared-module/hardwarekey/HardwareKey.c on top of PSA. void common_hal_hardwarekey_hardwarekey_hmac_sha256(hardwarekey_hardwarekey_obj_t *self, @@ -35,4 +46,5 @@ void common_hal_hardwarekey_hardwarekey_hmac_sha256(hardwarekey_hardwarekey_obj_ bool common_hal_hardwarekey_hardwarekey_verify_hmac_sha256(hardwarekey_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_hardwarekey_hardwarekey_get_key_slot(hardwarekey_hardwarekey_obj_t *self); +hardwarekey_purpose_t common_hal_hardwarekey_hardwarekey_get_purpose(hardwarekey_hardwarekey_obj_t *self); bool common_hal_hardwarekey_hardwarekey_get_exportable(hardwarekey_hardwarekey_obj_t *self); From 965a59e19634da7efd9e25600a1d31524dddf798 Mon Sep 17 00:00:00 2001 From: Mike Mabey Date: Thu, 10 Sep 2026 18:03:11 -0600 Subject: [PATCH 4/8] hardwarekey: use HardwareKey through the hmac module Completes tannewt's redesign: a HardwareKey is used by passing it to hmac.new() (or hmac.digest()) in place of a bytes key, so the same code works with a software key during development and a hardware key in production. - hmac.new() / hmac.digest() accept a hardwarekey.HardwareKey. An unused slot raises ValueError. A key locked to one digest (the ESP32 HMAC peripheral only does SHA-256) raises ValueError if asked for another -- PSA_ERROR_NOT_PERMITTED is translated to a clear message. - HardwareKey.hmac_sha256() / verify_hmac_sha256() are removed; the object is now just key_slot / purpose / exportable + a repr. - shared-module/hardwarekey/HardwareKey.c drops the PSA MAC ops and keeps only the accessors, including a new get_key_id() for hmac. Verified on an ESP32-S3-DevKitC-1-N8R8: hmac.new(board.EFUSE_KEY5, msg, "sha256") matches the host reference and the same key passed as bytes; sha1 against that key, and an unused slot, both raise ValueError. --- locale/circuitpython.pot | 11 +++-- shared-bindings/hardwarekey/HardwareKey.c | 56 ++--------------------- shared-bindings/hardwarekey/HardwareKey.h | 3 +- shared-bindings/hmac/__init__.c | 36 +++++++++++++-- shared-module/hardwarekey/HardwareKey.c | 46 +++---------------- shared-module/hardwarekey/HardwareKey.h | 10 ++-- shared-module/hmac/HMAC.c | 6 +++ 7 files changed, 60 insertions(+), 108 deletions(-) diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot index 0464f9d7bd4..370dad50c01 100644 --- a/locale/circuitpython.pot +++ b/locale/circuitpython.pot @@ -2359,7 +2359,6 @@ msgid "%q length must be <= %d" msgstr "" #: py/argcheck.c shared-bindings/usb_hid/Device.c -#: shared-module/hardwarekey/HardwareKey.c msgid "%q length must be %d" msgstr "" @@ -4028,6 +4027,10 @@ msgstr "" msgid "Unsupported hash algorithm" msgstr "" +#: shared-bindings/hmac/__init__.c +msgid "hardware key slot is unused" +msgstr "" + #: shared-bindings/i2cioexpander/IOExpander.c msgid "num_pins must be 8 or 16" msgstr "" @@ -4367,12 +4370,12 @@ msgstr "" msgid "unsupported colorspace for GifWriter" msgstr "" -#: shared-module/hardwarekey/HardwareKey.c -msgid "HMAC calculation failed" +#: shared-module/hmac/HMAC.c +msgid "HMAC operation failed" msgstr "" #: shared-module/hmac/HMAC.c -msgid "HMAC operation failed" +msgid "key does not support this digest" msgstr "" #: shared-module/i2cdisplaybus/I2CDisplayBus.c diff --git a/shared-bindings/hardwarekey/HardwareKey.c b/shared-bindings/hardwarekey/HardwareKey.c index 694ae351b54..f44a3328c87 100644 --- a/shared-bindings/hardwarekey/HardwareKey.c +++ b/shared-bindings/hardwarekey/HardwareKey.c @@ -5,14 +5,11 @@ // SPDX-License-Identifier: MIT #include "py/objproperty.h" -#include "py/objstr.h" #include "py/runtime.h" #include "shared-bindings/hardwarekey/__init__.h" #include "shared-bindings/hardwarekey/HardwareKey.h" -#define HMAC_SHA256_DIGEST_SIZE HARDWAREKEY_HMAC_SHA256_DIGEST_SIZE - //| class HardwareKey: //| """A key held in a hardware key store, usable but not readable. //| @@ -21,6 +18,9 @@ //| ``board.EFUSE_KEY0`` -- just like pins. A slot with no key burned into it //| still has a `HardwareKey` object; its `purpose` is `hardwarekey.UNUSED`. //| +//| Compute a MAC with a key by passing it to `hmac.new()` in place of a +//| ``bytes`` key. +//| //| On espressif the slots are the eFuse key blocks (``BLOCK_KEY0`` - //| ``BLOCK_KEY5``); a slot is usable only if its block was burned with //| purpose ``HMAC_UP``.""" @@ -35,50 +35,6 @@ static void hardwarekey_hardwarekey_print(const mp_print_t *print, mp_obj_t 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 hardwarekey_hardwarekey_hmac_sha256(mp_obj_t self_in, mp_obj_t data_in) { - hardwarekey_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_hardwarekey_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(hardwarekey_hardwarekey_hmac_sha256_obj, hardwarekey_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 hardwarekey_hardwarekey_verify_hmac_sha256(mp_obj_t self_in, mp_obj_t data_in, mp_obj_t mac_in) { - hardwarekey_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_hardwarekey_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(hardwarekey_hardwarekey_verify_hmac_sha256_obj, hardwarekey_hardwarekey_verify_hmac_sha256); - //| key_slot: int //| """The port-defined key identifier this handle is bound to. On espressif, //| the eFuse key block index. (read-only)""" @@ -92,7 +48,6 @@ MP_PROPERTY_GETTER(hardwarekey_hardwarekey_key_slot_obj, (mp_obj_t)&hardwarekey_ //| purpose: Purpose //| """What this key slot is provisioned for -- `hardwarekey.HMAC_UP` or //| `hardwarekey.UNUSED`. (read-only)""" -//| static mp_obj_t hardwarekey_hardwarekey_get_purpose(mp_obj_t self_in) { hardwarekey_hardwarekey_obj_t *self = MP_OBJ_TO_PTR(self_in); return hardwarekey_purpose_to_obj(common_hal_hardwarekey_hardwarekey_get_purpose(self)); @@ -102,12 +57,13 @@ MP_PROPERTY_GETTER(hardwarekey_hardwarekey_purpose_obj, (mp_obj_t)&hardwarekey_h //| exportable: bool //| """Whether the raw key bytes can ever leave the hardware. Always -//| informational -- it does not gate `hmac_sha256`. +//| informational. //| //| 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 hardwarekey_hardwarekey_get_exportable(mp_obj_t self_in) { hardwarekey_hardwarekey_obj_t *self = MP_OBJ_TO_PTR(self_in); return mp_obj_new_bool(common_hal_hardwarekey_hardwarekey_get_exportable(self)); @@ -116,8 +72,6 @@ MP_DEFINE_CONST_FUN_OBJ_1(hardwarekey_hardwarekey_get_exportable_obj, hardwareke MP_PROPERTY_GETTER(hardwarekey_hardwarekey_exportable_obj, (mp_obj_t)&hardwarekey_hardwarekey_get_exportable_obj); static const mp_rom_map_elem_t hardwarekey_hardwarekey_locals_dict_table[] = { - { MP_ROM_QSTR(MP_QSTR_hmac_sha256), MP_ROM_PTR(&hardwarekey_hardwarekey_hmac_sha256_obj) }, - { MP_ROM_QSTR(MP_QSTR_verify_hmac_sha256), MP_ROM_PTR(&hardwarekey_hardwarekey_verify_hmac_sha256_obj) }, { MP_ROM_QSTR(MP_QSTR_key_slot), MP_ROM_PTR(&hardwarekey_hardwarekey_key_slot_obj) }, { MP_ROM_QSTR(MP_QSTR_purpose), MP_ROM_PTR(&hardwarekey_hardwarekey_purpose_obj) }, { MP_ROM_QSTR(MP_QSTR_exportable), MP_ROM_PTR(&hardwarekey_hardwarekey_exportable_obj) }, diff --git a/shared-bindings/hardwarekey/HardwareKey.h b/shared-bindings/hardwarekey/HardwareKey.h index 967a66b3bc4..91bdfd3c633 100644 --- a/shared-bindings/hardwarekey/HardwareKey.h +++ b/shared-bindings/hardwarekey/HardwareKey.h @@ -8,8 +8,7 @@ #include "py/obj.h" -// Object struct and the common_hal_* contract (construct is per-port; the -// operations are implemented once in shared-module/hardwarekey/HardwareKey.c). +// Object struct and the common_hal_* accessors. #include "shared-module/hardwarekey/HardwareKey.h" // Type object used in Python. Shared between ports. diff --git a/shared-bindings/hmac/__init__.c b/shared-bindings/hmac/__init__.c index 79b64599c87..2ea31a6899c 100644 --- a/shared-bindings/hmac/__init__.c +++ b/shared-bindings/hmac/__init__.c @@ -12,10 +12,18 @@ #include "shared-bindings/hmac/HMAC.h" #include "shared-module/hmac/__init__.h" +#if CIRCUITPY_HARDWAREKEY +#include "shared-bindings/hardwarekey/HardwareKey.h" +#endif + //| """Keyed hashing for message authentication //| //| |see_cpython_module| :mod:`cpython:hmac`. //| +//| The key may be a ``bytes``-like object or, where the port provides them, a +//| ``hardwarekey.HardwareKey`` -- so the same code works with a key in flash +//| during development and a key held in hardware in production. +//| //| Only ``"sha256"`` and ``"sha1"`` are supported for ``digestmod``. //| """ //| @@ -30,18 +38,34 @@ static psa_algorithm_t hash_alg_from_digestmod(mp_obj_t digestmod) { } static hmac_hmac_obj_t *hmac_new_internal(mp_obj_t key_in, psa_algorithm_t hash_alg) { + hmac_hmac_obj_t *self = mp_obj_malloc(hmac_hmac_obj_t, &hmac_hmac_type); + + #if CIRCUITPY_HARDWAREKEY + if (mp_obj_is_type(key_in, &hardwarekey_hardwarekey_type)) { + psa_key_id_t key_id = common_hal_hardwarekey_hardwarekey_get_key_id(MP_OBJ_TO_PTR(key_in)); + if (key_id == 0) { + mp_raise_ValueError(MP_ERROR_TEXT("hardware key slot is unused")); + } + common_hal_hmac_new(self, NULL, 0, key_id, hash_alg); + return self; + } + #endif + mp_buffer_info_t keyinfo; mp_get_buffer_raise(key_in, &keyinfo, MP_BUFFER_READ); - - hmac_hmac_obj_t *self = mp_obj_malloc(hmac_hmac_obj_t, &hmac_hmac_type); common_hal_hmac_new(self, keyinfo.buf, keyinfo.len, 0, hash_alg); return self; } -//| def new(key: ReadableBuffer, msg: ReadableBuffer = b"", digestmod: str = ...) -> HMAC: +//| def new( +//| key: ReadableBuffer | hardwarekey.HardwareKey, +//| msg: ReadableBuffer = b"", +//| digestmod: str = ..., +//| ) -> HMAC: //| """Create a new HMAC object. //| -//| :param ReadableBuffer key: the secret key +//| :param key: the secret key -- a ``bytes``-like object, or a +//| ``hardwarekey.HardwareKey`` on ports that provide it //| :param ReadableBuffer msg: initial data to authenticate; add more with `HMAC.update()` //| :param str digestmod: the digest name, ``"sha256"`` or ``"sha1"``. Required. //| """ @@ -67,7 +91,9 @@ static mp_obj_t hmac_new(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_a } static MP_DEFINE_CONST_FUN_OBJ_KW(hmac_new_obj, 1, hmac_new); -//| def digest(key: ReadableBuffer, msg: ReadableBuffer, digest: str) -> bytes: +//| def digest( +//| key: ReadableBuffer | hardwarekey.HardwareKey, msg: ReadableBuffer, digest: str +//| ) -> bytes: //| """Return the HMAC of ``msg`` under ``key`` for the named ``digest``, in one call. //| //| Equivalent to ``new(key, msg, digestmod=digest).digest()`` but does not build an diff --git a/shared-module/hardwarekey/HardwareKey.c b/shared-module/hardwarekey/HardwareKey.c index d575a62aa10..8ce6a418d00 100644 --- a/shared-module/hardwarekey/HardwareKey.c +++ b/shared-module/hardwarekey/HardwareKey.c @@ -6,45 +6,9 @@ #include "shared-module/hardwarekey/HardwareKey.h" -#include "py/runtime.h" - -#include "psa/crypto.h" - -#define HMAC_SHA256_DIGEST_SIZE HARDWAREKEY_HMAC_SHA256_DIGEST_SIZE -#define HARDWAREKEY_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_hardwarekey_hardwarekey_hmac_sha256(hardwarekey_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, HARDWAREKEY_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_hardwarekey_hardwarekey_verify_hmac_sha256(hardwarekey_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, HARDWAREKEY_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; - } -} +// These accessors are port-independent. The one port-specific step -- turning a +// hardware key slot into self->key_id -- happens when the port fills in its +// per-slot HardwareKey objects at startup. mp_int_t common_hal_hardwarekey_hardwarekey_get_key_slot(hardwarekey_hardwarekey_obj_t *self) { return self->key_slot; @@ -57,3 +21,7 @@ hardwarekey_purpose_t common_hal_hardwarekey_hardwarekey_get_purpose(hardwarekey bool common_hal_hardwarekey_hardwarekey_get_exportable(hardwarekey_hardwarekey_obj_t *self) { return self->exportable; } + +psa_key_id_t common_hal_hardwarekey_hardwarekey_get_key_id(hardwarekey_hardwarekey_obj_t *self) { + return self->key_id; +} diff --git a/shared-module/hardwarekey/HardwareKey.h b/shared-module/hardwarekey/HardwareKey.h index 4c7d316fa61..375cb354513 100644 --- a/shared-module/hardwarekey/HardwareKey.h +++ b/shared-module/hardwarekey/HardwareKey.h @@ -12,8 +12,6 @@ #include "psa/crypto.h" -#define HARDWAREKEY_HMAC_SHA256_DIGEST_SIZE 32 - // What a hardware key slot is provisioned for. UNUSED means no key has been // burned into the slot (or it is burned for something this module does not // expose); the slot is present in `board` but not usable. @@ -39,12 +37,10 @@ typedef struct { // HardwareKey objects are created by the port at startup, one per hardware key // slot, and placed in `board`; application code never constructs them. The // per-port startup code fills in key_id, key_slot, purpose, exportable and name. +// The key is used by passing the object to hmac.new(). -// Implemented once in shared-module/hardwarekey/HardwareKey.c on top of PSA. -void common_hal_hardwarekey_hardwarekey_hmac_sha256(hardwarekey_hardwarekey_obj_t *self, - const uint8_t *data, size_t data_len, uint8_t *mac_out, size_t mac_out_len); -bool common_hal_hardwarekey_hardwarekey_verify_hmac_sha256(hardwarekey_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_hardwarekey_hardwarekey_get_key_slot(hardwarekey_hardwarekey_obj_t *self); hardwarekey_purpose_t common_hal_hardwarekey_hardwarekey_get_purpose(hardwarekey_hardwarekey_obj_t *self); bool common_hal_hardwarekey_hardwarekey_get_exportable(hardwarekey_hardwarekey_obj_t *self); +// The PSA key id, for hmac.new(). 0 if the slot is unused. +psa_key_id_t common_hal_hardwarekey_hardwarekey_get_key_id(hardwarekey_hardwarekey_obj_t *self); diff --git a/shared-module/hmac/HMAC.c b/shared-module/hmac/HMAC.c index e208549eebc..461b96fa0b2 100644 --- a/shared-module/hmac/HMAC.c +++ b/shared-module/hmac/HMAC.c @@ -67,6 +67,12 @@ void common_hal_hmac_digest(hmac_hmac_obj_t *self, uint8_t *out, size_t out_len) if (imported) { psa_destroy_key(key_id); } + + if (status == PSA_ERROR_NOT_PERMITTED) { + // A hardware key can be locked to one digest (the ESP32 HMAC peripheral + // only does SHA-256). + mp_raise_ValueError(MP_ERROR_TEXT("key does not support this digest")); + } check_psa(status); } From 88e780a26c60e8450d2775330f172e48fe5e5e39 Mon Sep 17 00:00:00 2001 From: Mike Mabey Date: Fri, 11 Sep 2026 16:28:37 -0600 Subject: [PATCH 5/8] hardwarekey: make HardwareKey falsy when unused Addresses tannewt's review comment on PR #11319: let application code write `if key:` instead of checking `key.purpose is hardwarekey.UNUSED` directly. Truthiness is defined as "purpose is not UNUSED" rather than "purpose is HMAC_UP" specifically, so a future non-HMAC purpose (e.g. DS/RSA signing) is also correctly truthy without revisiting this. Verified this compiles and links on espressif_esp32s3_devkitc_1_n8r8. --- shared-bindings/hardwarekey/HardwareKey.c | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/shared-bindings/hardwarekey/HardwareKey.c b/shared-bindings/hardwarekey/HardwareKey.c index f44a3328c87..041e5c51b4d 100644 --- a/shared-bindings/hardwarekey/HardwareKey.c +++ b/shared-bindings/hardwarekey/HardwareKey.c @@ -35,6 +35,20 @@ static void hardwarekey_hardwarekey_print(const mp_print_t *print, mp_obj_t self } } +//| def __bool__(self) -> bool: +//| """``False`` if `purpose` is `hardwarekey.UNUSED`, ``True`` otherwise. +//| This lets you write ``if key:`` to check whether a slot is usable.""" +//| ... +static mp_obj_t hardwarekey_hardwarekey_unary_op(mp_unary_op_t op, mp_obj_t self_in) { + hardwarekey_hardwarekey_obj_t *self = MP_OBJ_TO_PTR(self_in); + switch (op) { + case MP_UNARY_OP_BOOL: + return mp_obj_new_bool(common_hal_hardwarekey_hardwarekey_get_purpose(self) != HARDWAREKEY_PURPOSE_UNUSED); + default: + return MP_OBJ_NULL; // op not supported + } +} + //| key_slot: int //| """The port-defined key identifier this handle is bound to. On espressif, //| the eFuse key block index. (read-only)""" @@ -83,5 +97,6 @@ MP_DEFINE_CONST_OBJ_TYPE( MP_QSTR_HardwareKey, MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS, print, hardwarekey_hardwarekey_print, + unary_op, hardwarekey_hardwarekey_unary_op, locals_dict, &hardwarekey_hardwarekey_locals_dict ); From 658c7408c5e22489aa8ac9d9f2314073aad4ad05 Mon Sep 17 00:00:00 2001 From: Mike Mabey Date: Tue, 15 Sep 2026 18:01:50 -0600 Subject: [PATCH 6/8] hmac: buffer update() for a hardware key, whose PSA driver only supports one-shot MAC compute Some hardware-backed HMAC peripherals only expose a one-shot compute primitive: their PSA driver's psa_mac_update() independently computes the HMAC of just that call's bytes and discards prior state, so streaming multiple update() calls into it (as bytes keys correctly do) silently produces the wrong MAC. hmac.HMAC now detects a borrowed hardware key and buffers update() calls instead, feeding the complete message to the PSA operation in one call at digest() time. Bytes keys are unaffected and keep true multipart streaming. Also found and fixed a related edge case: the same one-shot driver rejects a zero-length update() outright, and skipping it entirely would leave the result at all-zero bytes from setup -- a wrong answer, not an error. Computing the HMAC of an empty message with a hardware key now raises ValueError instead of silently returning 32 zero bytes. Verified on an ESP32-S3-DevKitC-1-N8R8 with a HardwareKey backed by a burned eFuse HMAC_UP key block: incremental update() now matches the one-shot and bytes-key results, and the empty-message case raises cleanly instead of returning the wrong digest. --- locale/circuitpython.pot | 11 +++++------ shared-bindings/hmac/__init__.c | 5 ++++- shared-module/hmac/HMAC.c | 24 ++++++++++++++++++++++-- shared-module/hmac/__init__.h | 19 +++++++++++++++---- 4 files changed, 46 insertions(+), 13 deletions(-) diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot index 2be6695c939..a1e97024720 100644 --- a/locale/circuitpython.pot +++ b/locale/circuitpython.pot @@ -1728,11 +1728,6 @@ msgstr "" msgid "Failed to connect: timeout" msgstr "" -#: ports/nordic/common-hal/_bleio/Connection.c -#: ports/zephyr-cp/common-hal/_bleio/Connection.c -msgid "Numeric comparison pairing" -msgstr "" - #: ports/nordic/common-hal/_bleio/UUID.c msgid "Unexpected nrfx uuid type" msgstr "" @@ -4365,7 +4360,7 @@ msgstr "" msgid "Failed to parse MP3 file" msgstr "" -#: shared-module/bitbangio/I2C.c +#: shared-module/bitbangio/I2C.c shared-module/msgpack/__init__.c msgid "%q too long" msgstr "" @@ -4431,6 +4426,10 @@ msgstr "" msgid "key does not support this digest" msgstr "" +#: shared-module/hmac/HMAC.c +msgid "HMAC of an empty message is not supported with a hardware key" +msgstr "" + #: shared-module/i2cdisplaybus/I2CDisplayBus.c #: shared-module/is31fl3741/IS31FL3741.c #, c-format diff --git a/shared-bindings/hmac/__init__.c b/shared-bindings/hmac/__init__.c index 2ea31a6899c..df27b79f116 100644 --- a/shared-bindings/hmac/__init__.c +++ b/shared-bindings/hmac/__init__.c @@ -22,7 +22,10 @@ //| //| The key may be a ``bytes``-like object or, where the port provides them, a //| ``hardwarekey.HardwareKey`` -- so the same code works with a key in flash -//| during development and a key held in hardware in production. +//| during development and a key held in hardware in production. The one +//| exception: an empty message raises `ValueError` with a +//| ``hardwarekey.HardwareKey`` (a hardware driver limitation), where it is +//| valid with a ``bytes`` key. //| //| Only ``"sha256"`` and ``"sha1"`` are supported for ``digestmod``. //| """ diff --git a/shared-module/hmac/HMAC.c b/shared-module/hmac/HMAC.c index a6975105b65..2a21dabf499 100644 --- a/shared-module/hmac/HMAC.c +++ b/shared-module/hmac/HMAC.c @@ -30,6 +30,10 @@ void common_hal_hmac_new(hmac_hmac_obj_t *self, const uint8_t *key, size_t key_l self->finished = false; self->digest_len = 0; self->mac_op = psa_mac_operation_init(); + self->buffered = (borrowed_key_id != 0); + if (self->buffered) { + vstr_init(&self->buf, 0); + } if (psa_crypto_init() != PSA_SUCCESS) { mp_raise_RuntimeError(MP_ERROR_TEXT("HMAC operation failed")); @@ -58,8 +62,8 @@ void common_hal_hmac_new(hmac_hmac_obj_t *self, const uint8_t *key, size_t key_l self->owns_key = false; } if (status == PSA_ERROR_NOT_PERMITTED) { - // A hardware key can be locked to one digest (the ESP32 HMAC peripheral - // only does SHA-256). + // A hardware-backed key can be locked to one digest algorithm -- + // a common restriction for this kind of peripheral. mp_raise_ValueError(MP_ERROR_TEXT("key does not support this digest")); } mp_raise_RuntimeError(MP_ERROR_TEXT("HMAC operation failed")); @@ -70,11 +74,27 @@ void common_hal_hmac_update(hmac_hmac_obj_t *self, const uint8_t *data, size_t d if (self->finished) { mp_raise_RuntimeError(NULL); } + if (self->buffered) { + vstr_add_strn(&self->buf, (const char *)data, data_len); + return; + } check_psa(self, psa_mac_update(&self->mac_op, data, data_len)); } void common_hal_hmac_digest(hmac_hmac_obj_t *self, uint8_t *out, size_t out_len) { if (!self->finished) { + if (self->buffered) { + if (self->buf.len == 0) { + // Some hardware-backed keys' PSA drivers reject a + // zero-length psa_mac_update() outright (and skipping the + // call entirely would leave the result at all-zero bytes + // from setup -- a wrong answer, not an error), so an empty + // message can't be computed through this path. + psa_mac_abort(&self->mac_op); + mp_raise_ValueError(MP_ERROR_TEXT("HMAC of an empty message is not supported with a hardware key")); + } + check_psa(self, psa_mac_update(&self->mac_op, (const uint8_t *)self->buf.buf, self->buf.len)); + } psa_status_t status = psa_mac_sign_finish(&self->mac_op, self->digest, sizeof(self->digest), &self->digest_len); // The operation is spent either way -- successful finish or not, it diff --git a/shared-module/hmac/__init__.h b/shared-module/hmac/__init__.h index 3d5ffa8556e..677ed0aab81 100644 --- a/shared-module/hmac/__init__.h +++ b/shared-module/hmac/__init__.h @@ -10,6 +10,7 @@ #include #include +#include "py/misc.h" #include "py/obj.h" #include "psa/crypto.h" @@ -18,10 +19,10 @@ typedef struct { mp_obj_base_t base; // The digest algorithm the HMAC is built on, e.g. PSA_ALG_SHA_256. psa_algorithm_t hash_alg; - // The PSA multipart MAC operation. update() streams straight into this; - // PSA has no psa_mac_clone(), so unlike shared-module/hashlib's Hash this - // can't be rewound -- digest() finishes it exactly once and caches the - // result below. + // The PSA multipart MAC operation. update() normally streams straight + // into this (see `buffered` below for the exception); PSA has no + // psa_mac_clone(), so unlike shared-module/hashlib's Hash this can't be + // rewound -- digest() finishes it exactly once and caches the result. psa_mac_operation_t mac_op; // The PSA key used by mac_op. For a bytes key, imported at construction // time and destroyed once mac_op is finished (owns_key true). For a key @@ -29,6 +30,16 @@ typedef struct { // owns_key is false. psa_key_id_t key_id; bool owns_key; + // True for a borrowed hardware key whose PSA driver doesn't implement + // real multipart MAC support: some hardware-backed HMAC peripherals only + // expose a one-shot compute primitive, so a PSA driver built on one may + // independently compute the HMAC of just each update() call's bytes and + // discard prior state -- silently giving the wrong answer if update() is + // called more than once. When true, update() accumulates into `buf` + // instead and digest() feeds it to mac_op in one call. Bytes keys stream + // normally -- this only affects the (typically short) hardware-key path. + bool buffered; + vstr_t buf; // Set once digest()/hexdigest() has finished mac_op. update() raises // after this; further digest() calls just return the cached bytes. bool finished; From 33b8d1a967f95151613469ba5524aedcfe9df96a Mon Sep 17 00:00:00 2001 From: Mike Mabey Date: Fri, 18 Sep 2026 14:56:59 -0600 Subject: [PATCH 7/8] board: Rename CIRCUITPY_BOARD_EXTRA_DICT_ITEMS to CIRCUITPY_BOARD_HARDWARE_KEYS Per tannewt's review on PR #11319: this extension point exists solely for hardwarekey's board.EFUSE_KEY* entries, so name it for that specific purpose instead of presenting it as a generic board-globals extension mechanism. Co-Authored-By: Claude Sonnet 5 --- ports/espressif/common-hal/hardwarekey/board.h | 4 ++-- shared-bindings/board/__init__.h | 15 +++++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/ports/espressif/common-hal/hardwarekey/board.h b/ports/espressif/common-hal/hardwarekey/board.h index 9a2e86e2198..0ac32fa140c 100644 --- a/ports/espressif/common-hal/hardwarekey/board.h +++ b/ports/espressif/common-hal/hardwarekey/board.h @@ -7,7 +7,7 @@ #pragma once // board.EFUSE_KEY0 .. board.EFUSE_KEY5: one fixed entry per eFuse key block, -// injected into every board's globals table through CIRCUITPY_BOARD_EXTRA_DICT_ITEMS +// injected into every board's globals table through CIRCUITPY_BOARD_HARDWARE_KEYS // (see shared-bindings/board/__init__.h). Each points at a static HardwareKey the // startup probe (espressif_hardwarekey_init) fills in -- so, like board pins, the // names exist at compile time and the objects are ready before user code runs. @@ -21,7 +21,7 @@ extern hardwarekey_hardwarekey_obj_t hardwarekey_efuse_keys[HARDWAREKEY_EFUSE_SLOT_COUNT]; -#define CIRCUITPY_BOARD_EXTRA_DICT_ITEMS \ +#define CIRCUITPY_BOARD_HARDWARE_KEYS \ { MP_ROM_QSTR(MP_QSTR_EFUSE_KEY0), MP_ROM_PTR(&hardwarekey_efuse_keys[0]) }, \ { MP_ROM_QSTR(MP_QSTR_EFUSE_KEY1), MP_ROM_PTR(&hardwarekey_efuse_keys[1]) }, \ { MP_ROM_QSTR(MP_QSTR_EFUSE_KEY2), MP_ROM_PTR(&hardwarekey_efuse_keys[2]) }, \ diff --git a/shared-bindings/board/__init__.h b/shared-bindings/board/__init__.h index 764ec7a274e..2df4af70593 100644 --- a/shared-bindings/board/__init__.h +++ b/shared-bindings/board/__init__.h @@ -11,15 +11,14 @@ #include "shared-bindings/microcontroller/Pin.h" // for the pin definitions -// A port can inject extra fixed entries into every board's globals table by -// defining CIRCUITPY_BOARD_EXTRA_DICT_ITEMS (a comma-terminated list of -// { MP_ROM_QSTR(...), MP_ROM_PTR(...) } pairs). hardwarekey uses this for -// board.EFUSE_KEY* on espressif. +// A port can inject board.EFUSE_KEY* (or other hardware key) entries into +// every board's globals table by defining CIRCUITPY_BOARD_HARDWARE_KEYS (a +// comma-terminated list of { MP_ROM_QSTR(...), MP_ROM_PTR(...) } pairs). #if CIRCUITPY_HARDWAREKEY #include "common-hal/hardwarekey/board.h" #endif -#ifndef CIRCUITPY_BOARD_EXTRA_DICT_ITEMS -#define CIRCUITPY_BOARD_EXTRA_DICT_ITEMS +#ifndef CIRCUITPY_BOARD_HARDWARE_KEYS +#define CIRCUITPY_BOARD_HARDWARE_KEYS #endif #if CIRCUITPY_MUTABLE_BOARD @@ -53,9 +52,9 @@ MP_DECLARE_CONST_FUN_OBJ_0(board_uart_obj); #define CIRCUITPYTHON_BOARD_DICT_STANDARD_ITEMS \ { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_board) }, \ { MP_ROM_QSTR(MP_QSTR_board_id), MP_ROM_PTR(&board_module_id_obj) }, \ - CIRCUITPY_BOARD_EXTRA_DICT_ITEMS + CIRCUITPY_BOARD_HARDWARE_KEYS #define CIRCUITPYTHON_MUTABLE_BOARD_DICT_STANDARD_ITEMS \ { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_board) }, \ { MP_ROM_QSTR(MP_QSTR_board_id), MP_OBJ_FROM_PTR(&board_module_id_obj) }, \ - CIRCUITPY_BOARD_EXTRA_DICT_ITEMS + CIRCUITPY_BOARD_HARDWARE_KEYS From 99f7f2130f120801d6547b57341fca80170b9c7c Mon Sep 17 00:00:00 2001 From: Mike Mabey Date: Fri, 18 Sep 2026 14:57:06 -0600 Subject: [PATCH 8/8] hardwarekey: Use the MAKE_ENUM macros for Purpose Per tannewt's review on PR #11319: replace the hand-rolled cp_enum_obj_t type/print function/switch statement with the standard MAKE_ENUM_VALUE / MAKE_ENUM_MAP / MAKE_PRINTER / MAKE_ENUM_TYPE macros (see shared-bindings/wifi/Packet.c), and cp_enum_find() in place of the hand-rolled hardwarekey_purpose_to_obj() lookup. This moves Purpose's members under the class, matching every other enum in shared-bindings: hardwarekey.HMAC_UP / hardwarekey.UNUSED become hardwarekey.Purpose.HMAC_UP / hardwarekey.Purpose.UNUSED. Docstrings referencing them are updated to match. Co-Authored-By: Claude Sonnet 5 --- shared-bindings/hardwarekey/HardwareKey.c | 10 ++--- shared-bindings/hardwarekey/__init__.c | 49 ++++++++--------------- shared-bindings/hardwarekey/__init__.h | 5 --- 3 files changed, 21 insertions(+), 43 deletions(-) diff --git a/shared-bindings/hardwarekey/HardwareKey.c b/shared-bindings/hardwarekey/HardwareKey.c index 041e5c51b4d..55789789aa2 100644 --- a/shared-bindings/hardwarekey/HardwareKey.c +++ b/shared-bindings/hardwarekey/HardwareKey.c @@ -16,7 +16,7 @@ //| This class cannot be instantiated. Every hardware key slot the board has //| is exposed as a fixed `HardwareKey` in :mod:`board` -- for example //| ``board.EFUSE_KEY0`` -- just like pins. A slot with no key burned into it -//| still has a `HardwareKey` object; its `purpose` is `hardwarekey.UNUSED`. +//| still has a `HardwareKey` object; its `purpose` is `hardwarekey.Purpose.UNUSED`. //| //| Compute a MAC with a key by passing it to `hmac.new()` in place of a //| ``bytes`` key. @@ -36,7 +36,7 @@ static void hardwarekey_hardwarekey_print(const mp_print_t *print, mp_obj_t self } //| def __bool__(self) -> bool: -//| """``False`` if `purpose` is `hardwarekey.UNUSED`, ``True`` otherwise. +//| """``False`` if `purpose` is `hardwarekey.Purpose.UNUSED`, ``True`` otherwise. //| This lets you write ``if key:`` to check whether a slot is usable.""" //| ... static mp_obj_t hardwarekey_hardwarekey_unary_op(mp_unary_op_t op, mp_obj_t self_in) { @@ -60,11 +60,11 @@ MP_DEFINE_CONST_FUN_OBJ_1(hardwarekey_hardwarekey_get_key_slot_obj, hardwarekey_ MP_PROPERTY_GETTER(hardwarekey_hardwarekey_key_slot_obj, (mp_obj_t)&hardwarekey_hardwarekey_get_key_slot_obj); //| purpose: Purpose -//| """What this key slot is provisioned for -- `hardwarekey.HMAC_UP` or -//| `hardwarekey.UNUSED`. (read-only)""" +//| """What this key slot is provisioned for -- `hardwarekey.Purpose.HMAC_UP` or +//| `hardwarekey.Purpose.UNUSED`. (read-only)""" static mp_obj_t hardwarekey_hardwarekey_get_purpose(mp_obj_t self_in) { hardwarekey_hardwarekey_obj_t *self = MP_OBJ_TO_PTR(self_in); - return hardwarekey_purpose_to_obj(common_hal_hardwarekey_hardwarekey_get_purpose(self)); + return cp_enum_find(&hardwarekey_purpose_type, common_hal_hardwarekey_hardwarekey_get_purpose(self)); } MP_DEFINE_CONST_FUN_OBJ_1(hardwarekey_hardwarekey_get_purpose_obj, hardwarekey_hardwarekey_get_purpose); MP_PROPERTY_GETTER(hardwarekey_hardwarekey_purpose_obj, (mp_obj_t)&hardwarekey_hardwarekey_get_purpose_obj); diff --git a/shared-bindings/hardwarekey/__init__.c b/shared-bindings/hardwarekey/__init__.c index b22ea380c93..e8802722b58 100644 --- a/shared-bindings/hardwarekey/__init__.c +++ b/shared-bindings/hardwarekey/__init__.c @@ -26,51 +26,34 @@ //| by passing it to `hmac.new()`. //| """ +MAKE_ENUM_VALUE(hardwarekey_purpose_type, hardwarekey_purpose, HMAC_UP, HARDWAREKEY_PURPOSE_HMAC); +MAKE_ENUM_VALUE(hardwarekey_purpose_type, hardwarekey_purpose, UNUSED, HARDWAREKEY_PURPOSE_UNUSED); + //| class Purpose: //| """What a hardware key slot is provisioned for. Instances are singletons; //| compare with ``is``.""" //| -static void hardwarekey_purpose_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) { - cp_enum_obj_t *self = MP_OBJ_TO_PTR(self_in); - mp_printf(print, "%q.%q", MP_QSTR_hardwarekey, self->name); -} - -MP_DEFINE_CONST_OBJ_TYPE( - hardwarekey_purpose_type, - MP_QSTR_Purpose, - MP_TYPE_FLAG_NONE, - print, hardwarekey_purpose_print - ); - -//| HMAC_UP: Purpose -//| """The slot holds an HMAC key. It can be used with `hmac.new()`.""" -const cp_enum_obj_t hardwarekey_purpose_hmac_obj = { - { &hardwarekey_purpose_type }, HARDWAREKEY_PURPOSE_HMAC, MP_QSTR_HMAC_UP -}; - -//| UNUSED: Purpose -//| """No key is burned into the slot (or it is burned for something this module -//| does not expose). The slot's `HardwareKey` still exists but cannot be used.""" +//| HMAC_UP: object +//| """The slot holds an HMAC key. It can be used with `hmac.new()`.""" +//| +//| UNUSED: object +//| """No key is burned into the slot (or it is burned for something this module +//| does not expose). The slot's `HardwareKey` still exists but cannot be used.""" //| -const cp_enum_obj_t hardwarekey_purpose_unused_obj = { - { &hardwarekey_purpose_type }, HARDWAREKEY_PURPOSE_UNUSED, MP_QSTR_UNUSED +MAKE_ENUM_MAP(hardwarekey_purpose) { + MAKE_ENUM_MAP_ENTRY(hardwarekey_purpose, HMAC_UP), + MAKE_ENUM_MAP_ENTRY(hardwarekey_purpose, UNUSED), }; +static MP_DEFINE_CONST_DICT(hardwarekey_purpose_locals_dict, hardwarekey_purpose_locals_table); + +MAKE_PRINTER(hardwarekey, hardwarekey_purpose); -mp_obj_t hardwarekey_purpose_to_obj(hardwarekey_purpose_t purpose) { - switch (purpose) { - case HARDWAREKEY_PURPOSE_HMAC: - return MP_OBJ_FROM_PTR(&hardwarekey_purpose_hmac_obj); - default: - return MP_OBJ_FROM_PTR(&hardwarekey_purpose_unused_obj); - } -} +MAKE_ENUM_TYPE(hardwarekey, Purpose, hardwarekey_purpose); static const mp_rom_map_elem_t hardwarekey_module_globals_table[] = { { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_hardwarekey) }, { MP_ROM_QSTR(MP_QSTR_HardwareKey), MP_ROM_PTR(&hardwarekey_hardwarekey_type) }, { MP_ROM_QSTR(MP_QSTR_Purpose), MP_ROM_PTR(&hardwarekey_purpose_type) }, - { MP_ROM_QSTR(MP_QSTR_HMAC_UP), MP_ROM_PTR(&hardwarekey_purpose_hmac_obj) }, - { MP_ROM_QSTR(MP_QSTR_UNUSED), MP_ROM_PTR(&hardwarekey_purpose_unused_obj) }, }; static MP_DEFINE_CONST_DICT(hardwarekey_module_globals, hardwarekey_module_globals_table); diff --git a/shared-bindings/hardwarekey/__init__.h b/shared-bindings/hardwarekey/__init__.h index 064ed0ad485..2f7a72b3e31 100644 --- a/shared-bindings/hardwarekey/__init__.h +++ b/shared-bindings/hardwarekey/__init__.h @@ -12,8 +12,3 @@ #include "shared-module/hardwarekey/HardwareKey.h" extern const mp_obj_type_t hardwarekey_purpose_type; -extern const cp_enum_obj_t hardwarekey_purpose_hmac_obj; -extern const cp_enum_obj_t hardwarekey_purpose_unused_obj; - -// The Purpose singleton for a hardwarekey_purpose_t code. -mp_obj_t hardwarekey_purpose_to_obj(hardwarekey_purpose_t purpose);