diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot index 80062d2b7f7..cd4b57cba10 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 "" @@ -4080,6 +4075,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 "" @@ -4361,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 "" @@ -4419,6 +4418,14 @@ msgstr "" msgid "unsupported colorspace for GifWriter" msgstr "" +#: shared-module/hmac/HMAC.c +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/ports/espressif/common-hal/hardwarekey/HardwareKey.c b/ports/espressif/common-hal/hardwarekey/HardwareKey.c new file mode 100644 index 00000000000..a93a4346d59 --- /dev/null +++ b/ports/espressif/common-hal/hardwarekey/HardwareKey.c @@ -0,0 +1,88 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Mike Mabey +// +// SPDX-License-Identifier: MIT + +// 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 "common-hal/hardwarekey/__init__.h" +#include "common-hal/hardwarekey/board.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 +// 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 "hardwarekey requires the ESP-IDF PSA opaque HMAC driver (SOC_HMAC_SUPPORTED targets only)" +#endif + +// The ESP HMAC peripheral consumes a 256-bit eFuse key. +#define HMAC_KEY_BITS 256 + +// 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 + slot); + if (esp_efuse_get_key_purpose(block) != ESP_EFUSE_KEY_PURPOSE_HMAC_UP) { + 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; + } + + psa_key_id_t key_id = import_efuse_hmac_key(slot); + if (key_id == 0) { + return false; + } + + 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 new file mode 100644 index 00000000000..2d33ed9a4ab --- /dev/null +++ b/ports/espressif/common-hal/hardwarekey/__init__.c @@ -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 + +#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..0ac32fa140c --- /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_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. + +#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_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]) }, \ + { 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/mpconfigport.mk b/ports/espressif/mpconfigport.mk index cce516bcc7e..5c3c5c0bc74 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 @@ -108,6 +109,9 @@ ifeq ($(IDF_TARGET),esp32) # Modules CIRCUITPY_RGBMATRIX = 0 +# No HMAC peripheral (introduced starting with ESP32-S2) +CIRCUITPY_HARDWAREKEY = 0 + # Has no USB CIRCUITPY_USB_DEVICE = 0 @@ -121,6 +125,9 @@ CIRCUITPY_ESPCAMERA = 0 CIRCUITPY_ESPULP = 0 CIRCUITPY_MEMORYMAP = 0 +# No HMAC peripheral (SOC_HMAC_SUPPORTED is not defined for this target) +CIRCUITPY_HARDWAREKEY = 0 + # No capacitive touch peripheral CIRCUITPY_ALARM_TOUCH = 0 CIRCUITPY_TOUCHIO_USE_NATIVE = 0 @@ -254,7 +261,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 @@ -262,6 +269,9 @@ CIRCUITPY_ESPULP = 0 CIRCUITPY_MEMORYMAP = 0 CIRCUITPY_RGBMATRIX = 0 +# No HMAC peripheral (SOC_HMAC_SUPPORTED is not defined for this target) +CIRCUITPY_HARDWAREKEY = 0 + # No capacitive touch peripheral CIRCUITPY_ALARM_TOUCH = 0 CIRCUITPY_TOUCHIO_USE_NATIVE = 0 diff --git a/ports/espressif/supervisor/port.c b/ports/espressif/supervisor/port.c index f0308975a1e..68d97c20dcf 100644 --- a/ports/espressif/supervisor/port.c +++ b/ports/espressif/supervisor/port.c @@ -31,6 +31,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" @@ -294,6 +297,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/py/circuitpy_defns.mk b/py/circuitpy_defns.mk index c3d83928ec4..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 @@ -601,6 +604,8 @@ SRC_COMMON_HAL_ALL = \ rtc/__init__.c \ sdioio/SDCard.c \ sdioio/__init__.c \ + hardwarekey/HardwareKey.c \ + hardwarekey/__init__.c \ socketpool/__init__.c \ socketpool/SocketPool.c \ socketpool/Socket.c \ @@ -843,6 +848,7 @@ SRC_SHARED_MODULE_ALL = \ rotaryio/IncrementalEncoder.c \ sdcardio/SDCard.c \ sdcardio/__init__.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 2b42bf5f769..b5b0921c4ac 100755 --- a/py/circuitpy_mpconfig.mk +++ b/py/circuitpy_mpconfig.mk @@ -564,6 +564,11 @@ CFLAGS += -DCIRCUITPY_SDCARDIO=$(CIRCUITPY_SDCARDIO) CIRCUITPY_SDIOIO ?= 0 CFLAGS += -DCIRCUITPY_SDIOIO=$(CIRCUITPY_SDIOIO) +# 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/board/__init__.h b/shared-bindings/board/__init__.h index 43343099730..2df4af70593 100644 --- a/shared-bindings/board/__init__.h +++ b/shared-bindings/board/__init__.h @@ -11,6 +11,16 @@ #include "shared-bindings/microcontroller/Pin.h" // for the pin definitions +// 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_HARDWARE_KEYS +#define CIRCUITPY_BOARD_HARDWARE_KEYS +#endif + #if CIRCUITPY_MUTABLE_BOARD extern mp_obj_dict_t board_module_globals; #else @@ -41,8 +51,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_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) }, + { MP_ROM_QSTR(MP_QSTR_board_id), MP_OBJ_FROM_PTR(&board_module_id_obj) }, \ + CIRCUITPY_BOARD_HARDWARE_KEYS diff --git a/shared-bindings/hardwarekey/HardwareKey.c b/shared-bindings/hardwarekey/HardwareKey.c new file mode 100644 index 00000000000..55789789aa2 --- /dev/null +++ b/shared-bindings/hardwarekey/HardwareKey.c @@ -0,0 +1,102 @@ +// 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/runtime.h" + +#include "shared-bindings/hardwarekey/__init__.h" +#include "shared-bindings/hardwarekey/HardwareKey.h" + +//| class HardwareKey: +//| """A key held in a hardware key store, usable but not readable. +//| +//| 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.Purpose.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``.""" +//| + +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 __bool__(self) -> bool: +//| """``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) { + 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)""" +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(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.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 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); + +//| exportable: bool +//| """Whether the raw key bytes can ever leave the hardware. Always +//| 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)); +} +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_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); + +MP_DEFINE_CONST_OBJ_TYPE( + hardwarekey_hardwarekey_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 + ); diff --git a/shared-bindings/hardwarekey/HardwareKey.h b/shared-bindings/hardwarekey/HardwareKey.h new file mode 100644 index 00000000000..91bdfd3c633 --- /dev/null +++ b/shared-bindings/hardwarekey/HardwareKey.h @@ -0,0 +1,15 @@ +// 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_* accessors. +#include "shared-module/hardwarekey/HardwareKey.h" + +// Type object used in Python. Shared between ports. +extern const mp_obj_type_t hardwarekey_hardwarekey_type; diff --git a/shared-bindings/hardwarekey/__init__.c b/shared-bindings/hardwarekey/__init__.c new file mode 100644 index 00000000000..e8802722b58 --- /dev/null +++ b/shared-bindings/hardwarekey/__init__.c @@ -0,0 +1,65 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Mike Mabey +// +// SPDX-License-Identifier: MIT + +#include "py/enum.h" +#include "py/obj.h" +#include "py/runtime.h" + +#include "shared-bindings/hardwarekey/__init__.h" +#include "shared-bindings/hardwarekey/HardwareKey.h" + +//| """Cryptographic operations with keys held in hardware +//| +//| 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 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). +//| +//| `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()`. +//| """ + +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``.""" +//| +//| 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.""" +//| +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); + +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) }, +}; +static MP_DEFINE_CONST_DICT(hardwarekey_module_globals, hardwarekey_module_globals_table); + +const mp_obj_module_t hardwarekey_module = { + .base = { &mp_type_module }, + .globals = (mp_obj_dict_t *)&hardwarekey_module_globals, +}; + +MP_REGISTER_MODULE(MP_QSTR_hardwarekey, hardwarekey_module); diff --git a/shared-bindings/hardwarekey/__init__.h b/shared-bindings/hardwarekey/__init__.h new file mode 100644 index 00000000000..2f7a72b3e31 --- /dev/null +++ b/shared-bindings/hardwarekey/__init__.h @@ -0,0 +1,14 @@ +// 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/enum.h" +#include "py/obj.h" + +#include "shared-module/hardwarekey/HardwareKey.h" + +extern const mp_obj_type_t hardwarekey_purpose_type; diff --git a/shared-bindings/hmac/__init__.c b/shared-bindings/hmac/__init__.c index 79b64599c87..df27b79f116 100644 --- a/shared-bindings/hmac/__init__.c +++ b/shared-bindings/hmac/__init__.c @@ -12,10 +12,21 @@ #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. 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``. //| """ //| @@ -30,18 +41,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 +94,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 new file mode 100644 index 00000000000..8ce6a418d00 --- /dev/null +++ b/shared-module/hardwarekey/HardwareKey.c @@ -0,0 +1,27 @@ +// 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/hardwarekey/HardwareKey.h" + +// 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; +} + +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; +} + +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 new file mode 100644 index 00000000000..375cb354513 --- /dev/null +++ b/shared-module/hardwarekey/HardwareKey.h @@ -0,0 +1,46 @@ +// 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" + +// 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 +// 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; + +// 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(). + +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 36a5d73457d..20b89e0a16c 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(NULL); @@ -57,6 +61,11 @@ void common_hal_hmac_new(hmac_hmac_obj_t *self, const uint8_t *key, size_t key_l psa_destroy_key(self->key_id); self->owns_key = false; } + if (status == PSA_ERROR_NOT_PERMITTED) { + // 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(NULL); } } @@ -65,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;