Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .codespell/ignore-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ straightaway
ftbs
ftb
curren
mabey
12 changes: 12 additions & 0 deletions locale/circuitpython.pot
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
Expand Down
87 changes: 87 additions & 0 deletions ports/espressif/common-hal/securekey/HardwareKey.c
Original file line number Diff line number Diff line change
@@ -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
// <psa/crypto.h> 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);
}
8 changes: 8 additions & 0 deletions ports/espressif/common-hal/securekey/__init__.c
Original file line number Diff line number Diff line change
@@ -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.
12 changes: 11 additions & 1 deletion ports/espressif/mpconfigport.mk
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,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
Expand All @@ -108,6 +109,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

Expand All @@ -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_SECUREKEY = 0

# No capacitive touch peripheral
CIRCUITPY_ALARM_TOUCH = 0
CIRCUITPY_TOUCHIO_USE_NATIVE = 0
Expand Down Expand Up @@ -254,14 +261,17 @@ CIRCUITPY_SDIOIO = 0
CIRCUITPY_USB_DEVICE = 0
CIRCUITPY_ESP_USB_SERIAL_JTAG ?= 1

#### esp32c6 ##########################################################
#### esp32c61 #########################################################
else ifeq ($(IDF_TARGET),esp32c61)
# Modules
CIRCUITPY_ESPCAMERA = 0
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
Expand Down
6 changes: 6 additions & 0 deletions py/circuitpy_defns.mk
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 \
Expand Down Expand Up @@ -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 \
Expand Down
5 changes: 5 additions & 0 deletions py/circuitpy_mpconfig.mk
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
131 changes: 131 additions & 0 deletions shared-bindings/securekey/HardwareKey.c
Original file line number Diff line number Diff line change
@@ -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
);
16 changes: 16 additions & 0 deletions shared-bindings/securekey/HardwareKey.h
Original file line number Diff line number Diff line change
@@ -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;
43 changes: 43 additions & 0 deletions shared-bindings/securekey/__init__.c
Original file line number Diff line number Diff line change
@@ -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);
Loading
Loading