diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot index cd4b57cba10..6d6e48defcd 100644 --- a/locale/circuitpython.pot +++ b/locale/circuitpython.pot @@ -1481,6 +1481,7 @@ msgstr "" #: ports/espressif/common-hal/espidf/__init__.c #: ports/raspberrypi/common-hal/picogame/Display.c +#: shared-bindings/hardwarekey/HardwareKey.c #: shared-bindings/picogame/__init__.c msgid "Operation or feature not supported" msgstr "" @@ -1534,6 +1535,28 @@ msgstr "" msgid "Only one %q can be set." msgstr "" +#: ports/espressif/common-hal/hardwarekey/HardwareKey.c +msgid "ds_params has the wrong length" +msgstr "" + +#: ports/espressif/common-hal/hardwarekey/HardwareKey.c +msgid "" +"This key already committed to a different algorithm; call load_ds_params() " +"again to use a different one" +msgstr "" + +#: ports/espressif/common-hal/hardwarekey/HardwareKey.c +msgid "load_ds_params() has not been called on this key" +msgstr "" + +#: ports/espressif/common-hal/hardwarekey/HardwareKey.c +msgid "ds_params is invalid for this key slot" +msgstr "" + +#: ports/espressif/common-hal/hardwarekey/HardwareKey.c +msgid "Digital Signature peripheral not available on this chip" +msgstr "" + #: ports/espressif/common-hal/i2ctarget/I2CTarget.c #: ports/raspberrypi/common-hal/i2ctarget/I2CTarget.c msgid "Only one address is allowed" @@ -3970,6 +3993,15 @@ msgstr "" msgid "RemoteTransmissionRequests limited to 8 bytes" msgstr "" +#: shared-bindings/crypto_primitives/__init__.c +#: shared-bindings/hardwarekey/HardwareKey.c +msgid "Only %q supported" +msgstr "" + +#: shared-bindings/crypto_primitives/__init__.c +msgid "label is not supported" +msgstr "" + #: shared-bindings/digitalio/DigitalInOut.c #: shared-bindings/i2cioexpander/IOPin.c msgid "Cannot set value when direction is input." @@ -4075,8 +4107,8 @@ msgstr "" msgid "Unsupported hash algorithm" msgstr "" -#: shared-bindings/hmac/__init__.c -msgid "hardware key slot is unused" +#: shared-bindings/hmac/__init__.c shared-bindings/ssl/SSLContext.c +msgid "key does not have the expected %q purpose" msgstr "" #: shared-bindings/i2cioexpander/IOExpander.c @@ -4418,6 +4450,10 @@ msgstr "" msgid "unsupported colorspace for GifWriter" msgstr "" +#: shared-module/hardwarekey/HardwareKey.c +msgid "OAEP requires this build to enable TLS 1.3 support" +msgstr "" + #: shared-module/hmac/HMAC.c msgid "key does not support this digest" msgstr "" diff --git a/ports/espressif/Makefile b/ports/espressif/Makefile index 881bfbf45e6..efd0aaf2b86 100644 --- a/ports/espressif/Makefile +++ b/ports/espressif/Makefile @@ -877,7 +877,11 @@ ifneq (,$(filter 1,$(CIRCUITPY_ENABLE_MPY_NATIVE) $(CIRCUITPY_LOAD_NATIVE))) NATIVE_SDKCONFIG := ;esp-idf-config/sdkconfig-native.defaults endif -SDKCONFIGS := esp-idf-config/sdkconfig.defaults;$(DEBUG_SDKCONFIG);$(FLASH_SIZE_SDKCONFIG);$(FLASH_MODE_SDKCONFIG);$(FLASH_SPEED_SDKCONFIG);$(PSRAM_SDKCONFIG);$(PSRAM_SIZE_SDKCONFIG);$(PSRAM_MODE_SDKCONFIG);$(PSRAM_SPEED_SDKCONFIG);$(BLE_SDKCONFIG)$(NATIVE_SDKCONFIG);$(TARGET_SDKCONFIG);boards/$(BOARD)/sdkconfig +ifeq ($(CIRCUITPY_HARDWAREKEY),1) + HARDWAREKEY_SDKCONFIG := ;esp-idf-config/sdkconfig-hardwarekey.defaults +endif + +SDKCONFIGS := esp-idf-config/sdkconfig.defaults;$(DEBUG_SDKCONFIG);$(FLASH_SIZE_SDKCONFIG);$(FLASH_MODE_SDKCONFIG);$(FLASH_SPEED_SDKCONFIG);$(PSRAM_SDKCONFIG);$(PSRAM_SIZE_SDKCONFIG);$(PSRAM_MODE_SDKCONFIG);$(PSRAM_SPEED_SDKCONFIG);$(BLE_SDKCONFIG)$(NATIVE_SDKCONFIG)$(HARDWAREKEY_SDKCONFIG);$(TARGET_SDKCONFIG);boards/$(BOARD)/sdkconfig # create the config headers .PHONY: do-sdkconfig diff --git a/ports/espressif/common-hal/hardwarekey/HardwareKey.c b/ports/espressif/common-hal/hardwarekey/HardwareKey.c index a93a4346d59..38958519e69 100644 --- a/ports/espressif/common-hal/hardwarekey/HardwareKey.c +++ b/ports/espressif/common-hal/hardwarekey/HardwareKey.c @@ -5,15 +5,20 @@ // 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 +// Everything after that -- hmac.new()'s use of the key id, sign() -- lives in // shared-module/hardwarekey/HardwareKey.c. +#include + +#include "py/runtime.h" + #include "common-hal/hardwarekey/__init__.h" #include "common-hal/hardwarekey/board.h" #include "shared-module/hardwarekey/HardwareKey.h" #include "esp_efuse.h" +#include "esp_heap_caps.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. @@ -21,9 +26,10 @@ _Static_assert(HARDWAREKEY_EFUSE_SLOT_COUNT == EFUSE_BLK_KEY_MAX - EFUSE_BLK_KEY "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. +// ESP_HMAC_OPAQUE_DRIVER_ENABLED / ESP_RSA_DS_DRIVER_ENABLED on chips that +// have those peripherals. Including only goes through the +// tf-psa-crypto config path and does NOT define either, so the opaque-driver +// headers 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 @@ -34,6 +40,17 @@ _Static_assert(HARDWAREKEY_EFUSE_SLOT_COUNT == EFUSE_BLK_KEY_MAX - EFUSE_BLK_KEY #error "hardwarekey requires the ESP-IDF PSA opaque HMAC driver (SOC_HMAC_SUPPORTED targets only)" #endif +// The Digital Signature peripheral driver, by contrast, is genuinely optional: +// CIRCUITPY_HARDWAREKEY is on for every HMAC-capable chip, but not every one +// of those also has SOC_DIG_SIGN_SUPPORTED. Where it's absent, +// ESP_RSA_DS_DRIVER_ENABLED is undefined and DS purpose is simply never +// reported by hardwarekey_efuse_slot_load() below -- no build-time #error. +#if defined(ESP_RSA_DS_DRIVER_ENABLED) +#include "esp_ds.h" +#include "psa_crypto_driver_esp_rsa_ds.h" +#include "psa_crypto_driver_esp_rsa_ds_contexts.h" +#endif + // The ESP HMAC peripheral consumes a 256-bit eFuse key. #define HMAC_KEY_BITS 256 @@ -64,9 +81,25 @@ bool hardwarekey_efuse_slot_load(mp_int_t slot, hardwarekey_hardwarekey_obj_t *k key->key_id = 0; key->purpose = HARDWAREKEY_PURPOSE_UNUSED; key->exportable = false; + key->rsa_key_bits = 0; 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) { + esp_efuse_purpose_t block_purpose = esp_efuse_get_key_purpose(block); + + if (block_purpose == ESP_EFUSE_KEY_PURPOSE_HMAC_DOWN_DIGITAL_SIGNATURE) { + // No PSA import yet: unlike HMAC_UP, this purpose alone doesn't name a + // full key -- the caller still has to supply ds_params via + // load_ds_params(). Just record that the slot is provisioned for it. + #if defined(ESP_RSA_DS_DRIVER_ENABLED) + key->purpose = HARDWAREKEY_PURPOSE_DS; + key->exportable = !esp_efuse_get_key_dis_read(block); + return true; + #else + return false; + #endif + } + + if (block_purpose != ESP_EFUSE_KEY_PURPOSE_HMAC_UP) { return false; } @@ -86,3 +119,106 @@ bool hardwarekey_efuse_slot_load(mp_int_t slot, hardwarekey_hardwarekey_obj_t *k key->exportable = !esp_efuse_get_key_dis_read(block); return true; } + +#if defined(ESP_RSA_DS_DRIVER_ENABLED) + +// Persistent (non-GC) storage for a DS slot's imported key. Allocated lazily +// on first load_ds_params() and reused (overwritten in place) on a later +// call for the same slot -- the PSA RSA-DS driver only supports volatile +// keys (IDF-15427), so there is no psa_destroy_key() to pair a replacement +// with; the old PSA key id is simply abandoned along with its one HMAC-key +// eFuse block's worth of state. +typedef struct { + esp_ds_data_t *data; + esp_ds_data_ctx_t *ctx; + esp_rsa_ds_opaque_key_t *opaque_key; +} ds_slot_cache_t; +static ds_slot_cache_t ds_slot_cache[HARDWAREKEY_EFUSE_SLOT_COUNT]; + +void common_hal_hardwarekey_hardwarekey_load_ds_params(hardwarekey_hardwarekey_obj_t *self, + const uint8_t *ds_params, size_t ds_params_len) { + if (ds_params_len != sizeof(esp_ds_data_t)) { + mp_raise_ValueError(MP_ERROR_TEXT("ds_params has the wrong length")); + } + + ds_slot_cache_t *cache = &ds_slot_cache[self->key_slot]; + if (cache->data == NULL) { + cache->data = heap_caps_malloc(sizeof(esp_ds_data_t), MALLOC_CAP_8BIT); + cache->ctx = heap_caps_malloc(sizeof(esp_ds_data_ctx_t), MALLOC_CAP_8BIT); + cache->opaque_key = heap_caps_malloc(sizeof(esp_rsa_ds_opaque_key_t), MALLOC_CAP_8BIT); + if (cache->data == NULL || cache->ctx == NULL || cache->opaque_key == NULL) { + m_malloc_fail(sizeof(esp_ds_data_t)); + } + } + memcpy(cache->data, ds_params, sizeof(esp_ds_data_t)); + + // rsa_length is stored as (bits / 32) - 1 (see esp_digital_signature_length_t). + mp_int_t rsa_bits = ((mp_int_t)cache->data->rsa_length + 1) * 32; + + *cache->ctx = (esp_ds_data_ctx_t) { + .esp_ds_data = cache->data, + .efuse_key_id = (uint8_t)self->key_slot, + .rsa_length_bits = (uint16_t)rsa_bits, + }; + *cache->opaque_key = (esp_rsa_ds_opaque_key_t) { + .ds_data_ctx = cache->ctx, + }; + + // The actual PSA import is deferred to ensure_algorithm(), on the first + // sign()/decrypt() call -- see its declaration in shared-module for why. + self->key_id = 0; + self->committed_alg = PSA_ALG_NONE; + self->rsa_key_bits = rsa_bits; +} + +void common_hal_hardwarekey_hardwarekey_ensure_algorithm(hardwarekey_hardwarekey_obj_t *self, + psa_algorithm_t alg, psa_key_usage_t usage) { + if (self->key_id != 0) { + if (self->committed_alg != alg) { + mp_raise_ValueError(MP_ERROR_TEXT( + "This key already committed to a different algorithm; call load_ds_params() again to use a different one")); + } + return; + } + + ds_slot_cache_t *cache = &ds_slot_cache[self->key_slot]; + if (cache->opaque_key == NULL) { + mp_raise_ValueError(MP_ERROR_TEXT("load_ds_params() has not been called on this key")); + } + + psa_key_attributes_t attr = PSA_KEY_ATTRIBUTES_INIT; + psa_set_key_type(&attr, PSA_KEY_TYPE_RSA_KEY_PAIR); + psa_set_key_bits(&attr, self->rsa_key_bits); + psa_set_key_usage_flags(&attr, usage); + psa_set_key_algorithm(&attr, alg); + psa_set_key_lifetime(&attr, PSA_KEY_LIFETIME_ESP_RSA_DS_VOLATILE); + + psa_key_id_t key_id = 0; + psa_status_t status = psa_import_key(&attr, + (const uint8_t *)cache->opaque_key, sizeof(*cache->opaque_key), &key_id); + if (status != PSA_SUCCESS) { + mp_raise_ValueError(MP_ERROR_TEXT("ds_params is invalid for this key slot")); + } + + self->key_id = key_id; + self->committed_alg = alg; +} + +#else + +void common_hal_hardwarekey_hardwarekey_load_ds_params(hardwarekey_hardwarekey_obj_t *self, + const uint8_t *ds_params, size_t ds_params_len) { + // Unreachable in practice: a Digital Signature purpose is never reported + // by hardwarekey_efuse_slot_load() on a chip without the driver, and + // shared-bindings checks `purpose` before calling this. Kept as a body + // (not a build error) so this file still compiles on those chips. + mp_raise_NotImplementedError(MP_ERROR_TEXT("Digital Signature peripheral not available on this chip")); +} + +void common_hal_hardwarekey_hardwarekey_ensure_algorithm(hardwarekey_hardwarekey_obj_t *self, + psa_algorithm_t alg, psa_key_usage_t usage) { + // Also unreachable: load_ds_params() above always raises first. + mp_raise_NotImplementedError(MP_ERROR_TEXT("Digital Signature peripheral not available on this chip")); +} + +#endif diff --git a/ports/espressif/esp-idf-config/sdkconfig-hardwarekey.defaults b/ports/espressif/esp-idf-config/sdkconfig-hardwarekey.defaults new file mode 100644 index 00000000000..64f9c3694fa --- /dev/null +++ b/ports/espressif/esp-idf-config/sdkconfig-hardwarekey.defaults @@ -0,0 +1,14 @@ +# Applied only when CIRCUITPY_HARDWAREKEY=1 (see ports/espressif/Makefile). +# +# A DS-purpose hardwarekey.HardwareKey signs through the ESP32-S2/S3 (and +# C3/C6/H2/P4) Digital Signature peripheral by way of ESP-IDF's PSA opaque +# RSA-DS driver. That driver is only compiled in when this option is set; it +# defaults to "n" upstream. On chips without SOC_DIG_SIGN_SUPPORTED the option +# has an unmet dependency and is ignored. +CONFIG_MBEDTLS_HARDWARE_RSA_DS_PERIPHERAL=y + +# The PSA RSA-DS driver needs RSA + PKCS#1 v1.5 signing wanted in the PSA build. +# Both are ESP-IDF defaults (TLS needs them), set here so a slimmed-down board +# config can't silently drop the DS peripheral. +CONFIG_MBEDTLS_RSA_C=y +CONFIG_MBEDTLS_PKCS1_V15=y diff --git a/ports/espressif/mpconfigport.mk b/ports/espressif/mpconfigport.mk index 5c3c5c0bc74..179d1c99247 100644 --- a/ports/espressif/mpconfigport.mk +++ b/ports/espressif/mpconfigport.mk @@ -84,6 +84,7 @@ CIRCUITPY_ESPULP ?= 1 CIRCUITPY_FRAMEBUFFERIO ?= 1 CIRCUITPY_FREQUENCYIO ?= 1 CIRCUITPY_HARDWAREKEY ?= 1 +CIRCUITPY_CRYPTO_PRIMITIVES ?= 1 CIRCUITPY_HASHLIB ?= 1 CIRCUITPY_I2CTARGET = 0 CIRCUITPY_MAX3421E ?= 1 @@ -111,6 +112,7 @@ CIRCUITPY_RGBMATRIX = 0 # No HMAC peripheral (introduced starting with ESP32-S2) CIRCUITPY_HARDWAREKEY = 0 +CIRCUITPY_CRYPTO_PRIMITIVES = 0 # Has no USB CIRCUITPY_USB_DEVICE = 0 @@ -127,6 +129,7 @@ CIRCUITPY_MEMORYMAP = 0 # No HMAC peripheral (SOC_HMAC_SUPPORTED is not defined for this target) CIRCUITPY_HARDWAREKEY = 0 +CIRCUITPY_CRYPTO_PRIMITIVES = 0 # No capacitive touch peripheral CIRCUITPY_ALARM_TOUCH = 0 @@ -271,6 +274,7 @@ CIRCUITPY_RGBMATRIX = 0 # No HMAC peripheral (SOC_HMAC_SUPPORTED is not defined for this target) CIRCUITPY_HARDWAREKEY = 0 +CIRCUITPY_CRYPTO_PRIMITIVES = 0 # No capacitive touch peripheral CIRCUITPY_ALARM_TOUCH = 0 diff --git a/py/circuitpy_defns.mk b/py/circuitpy_defns.mk index eec7e3ba1e4..a033d7e75a1 100755 --- a/py/circuitpy_defns.mk +++ b/py/circuitpy_defns.mk @@ -198,6 +198,9 @@ endif ifeq ($(CIRCUITPY_CODEOP),1) SRC_PATTERNS += codeop/% endif +ifeq ($(CIRCUITPY_CRYPTO_PRIMITIVES),1) +SRC_PATTERNS += crypto_primitives/% +endif ifeq ($(CIRCUITPY_COUNTIO),1) SRC_PATTERNS += countio/% endif @@ -669,6 +672,7 @@ $(filter $(SRC_PATTERNS), \ canio/Match.c \ codeop/__init__.c \ countio/Edge.c \ + crypto_primitives/__init__.c \ digitalio/DigitalInOutProtocol.c \ digitalio/Direction.c \ digitalio/DriveMode.c \ diff --git a/py/circuitpy_mpconfig.mk b/py/circuitpy_mpconfig.mk index b5b0921c4ac..dd40cee5ac0 100755 --- a/py/circuitpy_mpconfig.mk +++ b/py/circuitpy_mpconfig.mk @@ -249,6 +249,9 @@ CFLAGS += -DCIRCUITPY_COLLECTIONS=$(CIRCUITPY_COLLECTIONS) CIRCUITPY_COMPUTED_GOTO_SAVE_SPACE ?= 0 CFLAGS += -DCIRCUITPY_COMPUTED_GOTO_SAVE_SPACE=$(CIRCUITPY_COMPUTED_GOTO_SAVE_SPACE) +CIRCUITPY_CRYPTO_PRIMITIVES ?= 0 +CFLAGS += -DCIRCUITPY_CRYPTO_PRIMITIVES=$(CIRCUITPY_CRYPTO_PRIMITIVES) + CIRCUITPY_CYW43 ?= 0 CFLAGS += -DCIRCUITPY_CYW43=$(CIRCUITPY_CYW43) diff --git a/shared-bindings/crypto_primitives/__init__.c b/shared-bindings/crypto_primitives/__init__.c new file mode 100644 index 00000000000..e45c2ca88e8 --- /dev/null +++ b/shared-bindings/crypto_primitives/__init__.c @@ -0,0 +1,110 @@ +// 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/crypto_primitives/__init__.h" + +//| """Building blocks for asymmetric cryptographic operations +//| +//| ``crypto_primitives`` holds padding and hash-algorithm markers used to +//| parameterize an operation elsewhere, for example +//| `hardwarekey.HardwareKey.sign()` / `hardwarekey.HardwareKey.decrypt()`. It does +//| no cryptography itself +//| and holds no key material; it exists only so those operations can take explicit +//| ``padding``/``algorithm`` arguments instead of baking one fixed combination into +//| a method name. Named and organized after +//| :py:mod:`cryptography.hazmat.primitives`, the equivalent shared home for +//| :py:mod:`~cryptography.hazmat.primitives.asymmetric.padding` and +//| :py:mod:`~cryptography.hazmat.primitives.hashes` in the ``cryptography`` package. +//| +//| `PKCS1v15` and `SHA256` are fixed, stateless constants (compare with ``is``; +//| there is nothing to configure, so there is nothing to construct). `OAEP` is a +//| real class since it actually takes a parameter. +//| """ + +//| PKCS1v15: object +//| """PKCS#1 v1.5 padding, for either signing (RSASSA-PKCS1-v1_5) or +//| decryption (RSAES-PKCS1-v1_5, the older, legacy encryption padding; prefer +//| `OAEP` for new designs). Mirrors +//| :py:class:`cryptography.hazmat.primitives.asymmetric.padding.PKCS1v15`, which is +//| likewise used for both operations.""" +MP_DEFINE_CONST_OBJ_TYPE( + crypto_primitives_pkcs1v15_type, + MP_QSTR_PKCS1v15, + MP_TYPE_FLAG_NONE + ); +const mp_obj_base_t crypto_primitives_pkcs1v15_obj = { &crypto_primitives_pkcs1v15_type }; + +//| SHA256: object +//| """The SHA-256 hash algorithm. Mirrors +//| :py:class:`cryptography.hazmat.primitives.hashes.SHA256`.""" +MP_DEFINE_CONST_OBJ_TYPE( + crypto_primitives_sha256_type, + MP_QSTR_SHA256, + MP_TYPE_FLAG_NONE + ); +const mp_obj_base_t crypto_primitives_sha256_obj = { &crypto_primitives_sha256_type }; + +//| class OAEP: +//| """RSAES-OAEP decryption padding. Only ``algorithm=SHA256`` and +//| ``label=None`` are supported today -- an actual driver may use the same hash +//| for both the OAEP digest and its MGF1 mask, and may not support a label. +//| Mirrors the one supported combination of +//| :py:class:`cryptography.hazmat.primitives.asymmetric.padding.OAEP`. +//| +//| Whether `OAEP` is actually usable depends on the operation it's passed to -- +//| see e.g. `hardwarekey.HardwareKey.decrypt()`.""" +//| +//| def __init__(self, algorithm: object, *, label: Optional[ReadableBuffer] = None) -> None: +//| """ +//| :param object algorithm: must be `SHA256` -- the only hash algorithm supported +//| :param Optional[~circuitpython_typing.ReadableBuffer] label: must be omitted or +//| ``None`` -- a label is not supported +//| """ +//| ... +static mp_obj_t crypto_primitives_oaep_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) { + enum { ARG_algorithm, ARG_label }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_algorithm, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_label, MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = mp_const_none} }, + }; + 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); + + if (!mp_obj_is_type(args[ARG_algorithm].u_obj, &crypto_primitives_sha256_type)) { + mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("Only %q supported"), MP_QSTR_SHA256); + } + if (args[ARG_label].u_obj != mp_const_none) { + mp_raise_NotImplementedError(MP_ERROR_TEXT("label is not supported")); + } + + return MP_OBJ_FROM_PTR(&crypto_primitives_oaep_singleton); +} +MP_DEFINE_CONST_OBJ_TYPE( + crypto_primitives_oaep_type, + MP_QSTR_OAEP, + MP_TYPE_FLAG_NONE, + make_new, crypto_primitives_oaep_make_new + ); +const mp_obj_base_t crypto_primitives_oaep_singleton = { &crypto_primitives_oaep_type }; + +static const mp_rom_map_elem_t crypto_primitives_module_globals_table[] = { + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_crypto_primitives) }, + { MP_ROM_QSTR(MP_QSTR_PKCS1v15), MP_ROM_PTR(&crypto_primitives_pkcs1v15_obj) }, + { MP_ROM_QSTR(MP_QSTR_SHA256), MP_ROM_PTR(&crypto_primitives_sha256_obj) }, + { MP_ROM_QSTR(MP_QSTR_OAEP), MP_ROM_PTR(&crypto_primitives_oaep_type) }, +}; +static MP_DEFINE_CONST_DICT(crypto_primitives_module_globals, crypto_primitives_module_globals_table); + +const mp_obj_module_t crypto_primitives_module = { + .base = { &mp_type_module }, + .globals = (mp_obj_dict_t *)&crypto_primitives_module_globals, +}; + +MP_REGISTER_MODULE(MP_QSTR_crypto_primitives, crypto_primitives_module); diff --git a/shared-bindings/crypto_primitives/__init__.h b/shared-bindings/crypto_primitives/__init__.h new file mode 100644 index 00000000000..0f83da76bad --- /dev/null +++ b/shared-bindings/crypto_primitives/__init__.h @@ -0,0 +1,25 @@ +// 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" + +// Padding/hash-algorithm markers, mirroring +// cryptography.hazmat.primitives.asymmetric.padding.PKCS1v15/OAEP and +// cryptography.hazmat.primitives.hashes.SHA256. +// +// PKCS1v15 and SHA256 are plain constants: no parameters, nothing to +// construct, so *_obj is the only thing exposed under that name (the type +// itself isn't reachable, so it can't be called). OAEP takes a real +// parameter, so it stays a constructible type; *_singleton is what its +// make_new() returns after validating arguments. +extern const mp_obj_type_t crypto_primitives_pkcs1v15_type; +extern const mp_obj_base_t crypto_primitives_pkcs1v15_obj; +extern const mp_obj_type_t crypto_primitives_sha256_type; +extern const mp_obj_base_t crypto_primitives_sha256_obj; +extern const mp_obj_type_t crypto_primitives_oaep_type; +extern const mp_obj_base_t crypto_primitives_oaep_singleton; diff --git a/shared-bindings/hardwarekey/HardwareKey.c b/shared-bindings/hardwarekey/HardwareKey.c index 55789789aa2..e102b6c6490 100644 --- a/shared-bindings/hardwarekey/HardwareKey.c +++ b/shared-bindings/hardwarekey/HardwareKey.c @@ -5,11 +5,19 @@ // SPDX-License-Identifier: MIT #include "py/objproperty.h" +#include "py/objstr.h" #include "py/runtime.h" +#include "shared-bindings/crypto_primitives/__init__.h" #include "shared-bindings/hardwarekey/__init__.h" #include "shared-bindings/hardwarekey/HardwareKey.h" +static void check_purpose(hardwarekey_hardwarekey_obj_t *self, hardwarekey_purpose_t purpose) { + if (common_hal_hardwarekey_hardwarekey_get_purpose(self) != purpose) { + mp_raise_ValueError(MP_ERROR_TEXT("Operation or feature not supported")); + } +} + //| class HardwareKey: //| """A key held in a hardware key store, usable but not readable. //| @@ -19,11 +27,12 @@ //| 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. +//| ``bytes`` key, or sign/decrypt with a Digital Signature key by calling +//| `sign()` / `decrypt()`. //| //| 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``.""" +//| purpose ``HMAC_UP`` or ``HMAC_DOWN_DIGITAL_SIGNATURE``.""" //| static void hardwarekey_hardwarekey_print(const mp_print_t *print, mp_obj_t self_in, mp_print_kind_t kind) { @@ -60,8 +69,9 @@ 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.Purpose.HMAC_UP` or -//| `hardwarekey.Purpose.UNUSED`. (read-only)""" +//| """What this key slot is provisioned for -- `hardwarekey.Purpose.HMAC_UP`, +//| `hardwarekey.Purpose.HMAC_DOWN_DIGITAL_SIGNATURE`, 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)); @@ -85,10 +95,180 @@ static mp_obj_t hardwarekey_hardwarekey_get_exportable(mp_obj_t self_in) { 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); +//| rsa_key_bits: int +//| """The RSA modulus size in bits for a Digital Signature key (e.g. 2048, +//| 3072), or ``0`` for an HMAC key, or a Digital Signature key before +//| `load_ds_params()` has been called. Signatures are ``rsa_key_bits // 8`` +//| bytes long. (read-only)""" +static mp_obj_t hardwarekey_hardwarekey_get_rsa_key_bits(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_rsa_key_bits(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(hardwarekey_hardwarekey_get_rsa_key_bits_obj, hardwarekey_hardwarekey_get_rsa_key_bits); +MP_PROPERTY_GETTER(hardwarekey_hardwarekey_rsa_key_bits_obj, (mp_obj_t)&hardwarekey_hardwarekey_get_rsa_key_bits_obj); + +//| def load_ds_params(self, ds_params: ReadableBuffer) -> None: +//| """Make this Digital Signature key usable with `sign()` / `decrypt()`. +//| +//| :param ~circuitpython_typing.ReadableBuffer ds_params: the encrypted +//| Digital Signature parameter block for this RSA key, produced at +//| provisioning time by vendor tooling (never by this module). On +//| espressif this is the raw ``esp_ds_data_t`` structure. Not secret -- +//| it is only usable together with this slot's eFuse key -- so it is +//| fine to keep in a plain file, e.g. on the ``CIRCUITPY`` filesystem. +//| +//| Only for a key whose `purpose` is `hardwarekey.Purpose.HMAC_DOWN_DIGITAL_SIGNATURE`. +//| Safe to call more than once; a later call replaces the loaded key and clears +//| which algorithm it's committed to (see `sign()`). +//| +//| :raises ValueError: if this key's `purpose` is not +//| `hardwarekey.Purpose.HMAC_DOWN_DIGITAL_SIGNATURE`, or ``ds_params`` is malformed +//| """ +//| ... +static mp_obj_t hardwarekey_hardwarekey_load_ds_params(mp_obj_t self_in, mp_obj_t ds_params_in) { + hardwarekey_hardwarekey_obj_t *self = MP_OBJ_TO_PTR(self_in); + check_purpose(self, HARDWAREKEY_PURPOSE_DS); + + mp_buffer_info_t bufinfo; + mp_get_buffer_raise(ds_params_in, &bufinfo, MP_BUFFER_READ); + + common_hal_hardwarekey_hardwarekey_load_ds_params(self, bufinfo.buf, bufinfo.len); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_2(hardwarekey_hardwarekey_load_ds_params_obj, hardwarekey_hardwarekey_load_ds_params); + +//| def sign( +//| self, +//| data: ReadableBuffer, +//| padding: object, +//| algorithm: object, +//| ) -> bytes: +//| """Sign ``data`` with this Digital Signature key and return the +//| signature (``rsa_key_bits // 8`` bytes). The private key is never +//| returned or exposed; the exponentiation runs entirely inside the +//| Digital Signature peripheral. Mirrors +//| :py:meth:`cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey.sign`, +//| with the peripheral's one supported combination. +//| +//| Only for a key whose `purpose` is `hardwarekey.Purpose.HMAC_DOWN_DIGITAL_SIGNATURE`, +//| after `load_ds_params()` has been called. +//| +//| Reusing one RSA key under more than one algorithm is a real cryptographic +//| risk (arithmetic relations between operations under different algorithms +//| can be exploitable), not just an inconvenience -- so this key commits to +//| whichever of `sign()` or `decrypt()` (and, for `decrypt()`, which padding) +//| it's first called with after `load_ds_params()`, for as long as that +//| ``ds_params`` stays loaded. A later call requesting a different algorithm +//| raises `ValueError`; call `load_ds_params()` again to start over. +//| +//| :param ~circuitpython_typing.ReadableBuffer data: the message to sign +//| :param crypto_primitives.PKCS1v15 padding: must be +//| `crypto_primitives.PKCS1v15` -- the only padding the Digital +//| Signature peripheral supports +//| :param crypto_primitives.SHA256 algorithm: must be +//| `crypto_primitives.SHA256` -- the only hash algorithm the Digital +//| Signature peripheral supports +//| """ +//| ... +static mp_obj_t hardwarekey_hardwarekey_sign(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_data, ARG_padding, ARG_algorithm }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_data, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_padding, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_algorithm, MP_ARG_REQUIRED | MP_ARG_OBJ }, + }; + hardwarekey_hardwarekey_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]); + check_purpose(self, HARDWAREKEY_PURPOSE_DS); + + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + if (!mp_obj_is_type(args[ARG_padding].u_obj, &crypto_primitives_pkcs1v15_type) || + !mp_obj_is_type(args[ARG_algorithm].u_obj, &crypto_primitives_sha256_type)) { + mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("Only %q supported"), MP_QSTR_PKCS1v15_space_and_space_SHA256); + } + + mp_buffer_info_t bufinfo; + mp_get_buffer_raise(args[ARG_data].u_obj, &bufinfo, MP_BUFFER_READ); + + size_t sig_len = common_hal_hardwarekey_hardwarekey_get_rsa_key_bits(self) / 8; + mp_obj_t result = mp_obj_new_bytes_of_zeros(sig_len); + mp_obj_str_t *result_bytes = MP_OBJ_TO_PTR(result); + + common_hal_hardwarekey_hardwarekey_sign(self, bufinfo.buf, bufinfo.len, + (uint8_t *)result_bytes->data, sig_len); + return result; +} +static MP_DEFINE_CONST_FUN_OBJ_KW(hardwarekey_hardwarekey_sign_obj, 1, hardwarekey_hardwarekey_sign); + +//| def decrypt( +//| self, +//| ciphertext: ReadableBuffer, +//| padding: object, +//| ) -> bytes: +//| """Decrypt ``ciphertext`` (``rsa_key_bits // 8`` bytes) with this Digital +//| Signature key and return the recovered plaintext. The private key is +//| never returned or exposed; the exponentiation runs entirely inside the +//| Digital Signature peripheral. Mirrors +//| :py:meth:`cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey.decrypt`. +//| +//| Only for a key whose `purpose` is `hardwarekey.Purpose.HMAC_DOWN_DIGITAL_SIGNATURE`, +//| after `load_ds_params()` has been called. Subject to the same one-algorithm- +//| per-loaded-key rule as `sign()` -- using `crypto_primitives.PKCS1v15` here +//| after using `crypto_primitives.OAEP` (or vice versa), or after calling +//| `sign()`, raises `ValueError`. +//| +//| :param ~circuitpython_typing.ReadableBuffer ciphertext: the ciphertext to decrypt +//| :param crypto_primitives.PKCS1v15 padding: RSAES-PKCS1-v1_5 padding -- the +//| older, legacy scheme; prefer `crypto_primitives.OAEP` for new designs +//| :param crypto_primitives.OAEP padding: RSAES-OAEP padding. Only usable in a +//| build with TLS 1.3 support enabled -- raises `NotImplementedError` otherwise +//| """ +//| ... +static mp_obj_t hardwarekey_hardwarekey_decrypt(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_ciphertext, ARG_padding }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_ciphertext, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_padding, MP_ARG_REQUIRED | MP_ARG_OBJ }, + }; + hardwarekey_hardwarekey_obj_t *self = MP_OBJ_TO_PTR(pos_args[0]); + check_purpose(self, HARDWAREKEY_PURPOSE_DS); + + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + psa_algorithm_t alg; + if (mp_obj_is_type(args[ARG_padding].u_obj, &crypto_primitives_pkcs1v15_type)) { + alg = PSA_ALG_RSA_PKCS1V15_CRYPT; + } else if (mp_obj_is_type(args[ARG_padding].u_obj, &crypto_primitives_oaep_type)) { + alg = PSA_ALG_RSA_OAEP(PSA_ALG_SHA_256); + } else { + mp_raise_NotImplementedError_varg(MP_ERROR_TEXT("Only %q supported"), MP_QSTR_PKCS1v15_space_or_space_OAEP); + } + + mp_buffer_info_t bufinfo; + mp_get_buffer_raise(args[ARG_ciphertext].u_obj, &bufinfo, MP_BUFFER_READ); + + size_t max_len = common_hal_hardwarekey_hardwarekey_get_rsa_key_bits(self) / 8; + uint8_t *plaintext = m_new(uint8_t, max_len); + size_t output_len = 0; + common_hal_hardwarekey_hardwarekey_decrypt(self, alg, bufinfo.buf, bufinfo.len, + plaintext, max_len, &output_len); + + mp_obj_t result = mp_obj_new_bytes(plaintext, output_len); + m_del(uint8_t, plaintext, max_len); + return result; +} +static MP_DEFINE_CONST_FUN_OBJ_KW(hardwarekey_hardwarekey_decrypt_obj, 1, hardwarekey_hardwarekey_decrypt); + 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) }, + { MP_ROM_QSTR(MP_QSTR_rsa_key_bits), MP_ROM_PTR(&hardwarekey_hardwarekey_rsa_key_bits_obj) }, + { MP_ROM_QSTR(MP_QSTR_load_ds_params), MP_ROM_PTR(&hardwarekey_hardwarekey_load_ds_params_obj) }, + { MP_ROM_QSTR(MP_QSTR_sign), MP_ROM_PTR(&hardwarekey_hardwarekey_sign_obj) }, + { MP_ROM_QSTR(MP_QSTR_decrypt), MP_ROM_PTR(&hardwarekey_hardwarekey_decrypt_obj) }, }; static MP_DEFINE_CONST_DICT(hardwarekey_hardwarekey_locals_dict, hardwarekey_hardwarekey_locals_dict_table); diff --git a/shared-bindings/hardwarekey/__init__.c b/shared-bindings/hardwarekey/__init__.c index e8802722b58..e43a9734d9d 100644 --- a/shared-bindings/hardwarekey/__init__.c +++ b/shared-bindings/hardwarekey/__init__.c @@ -6,7 +6,6 @@ #include "py/enum.h" #include "py/obj.h" -#include "py/runtime.h" #include "shared-bindings/hardwarekey/__init__.h" #include "shared-bindings/hardwarekey/HardwareKey.h" @@ -15,18 +14,20 @@ //| //| 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). +//| back. Application code can compute a MAC, sign, or decrypt 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()`. +//| by passing it to `hmac.new()`; sign or decrypt with one by calling +//| `HardwareKey.sign()` / `HardwareKey.decrypt()`. //| """ MAKE_ENUM_VALUE(hardwarekey_purpose_type, hardwarekey_purpose, HMAC_UP, HARDWAREKEY_PURPOSE_HMAC); +MAKE_ENUM_VALUE(hardwarekey_purpose_type, hardwarekey_purpose, HMAC_DOWN_DIGITAL_SIGNATURE, HARDWAREKEY_PURPOSE_DS); MAKE_ENUM_VALUE(hardwarekey_purpose_type, hardwarekey_purpose, UNUSED, HARDWAREKEY_PURPOSE_UNUSED); //| class Purpose: @@ -36,12 +37,17 @@ MAKE_ENUM_VALUE(hardwarekey_purpose_type, hardwarekey_purpose, UNUSED, HARDWAREK //| HMAC_UP: object //| """The slot holds an HMAC key. It can be used with `hmac.new()`.""" //| +//| HMAC_DOWN_DIGITAL_SIGNATURE: object +//| """The slot holds the key for a Digital Signature (RSA) key. Call +//| `HardwareKey.load_ds_params()` once, then `HardwareKey.sign()`.""" +//| //| 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, HMAC_DOWN_DIGITAL_SIGNATURE), MAKE_ENUM_MAP_ENTRY(hardwarekey_purpose, UNUSED), }; static MP_DEFINE_CONST_DICT(hardwarekey_purpose_locals_dict, hardwarekey_purpose_locals_table); diff --git a/shared-bindings/hmac/__init__.c b/shared-bindings/hmac/__init__.c index df27b79f116..f6c5ca540e0 100644 --- a/shared-bindings/hmac/__init__.c +++ b/shared-bindings/hmac/__init__.c @@ -45,10 +45,11 @@ static hmac_hmac_obj_t *hmac_new_internal(mp_obj_t key_in, psa_algorithm_t hash_ #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")); + hardwarekey_hardwarekey_obj_t *key = MP_OBJ_TO_PTR(key_in); + if (common_hal_hardwarekey_hardwarekey_get_purpose(key) != HARDWAREKEY_PURPOSE_HMAC) { + mp_raise_ValueError_varg(MP_ERROR_TEXT("key does not have the expected %q purpose"), MP_QSTR_HMAC_UP); } + psa_key_id_t key_id = common_hal_hardwarekey_hardwarekey_get_key_id(key); common_hal_hmac_new(self, NULL, 0, key_id, hash_alg); return self; } diff --git a/shared-bindings/ssl/SSLContext.c b/shared-bindings/ssl/SSLContext.c index 9546c50ed7c..1697653520d 100644 --- a/shared-bindings/ssl/SSLContext.c +++ b/shared-bindings/ssl/SSLContext.c @@ -16,6 +16,10 @@ #include "shared-bindings/ssl/SSLContext.h" +#if CIRCUITPY_HARDWAREKEY +#include "shared-bindings/hardwarekey/HardwareKey.h" +#endif + //| class SSLContext: //| """Settings related to SSL that can be applied to a socket by wrapping it. //| This is useful to provide SSL certificates to specific connections @@ -31,13 +35,20 @@ static mp_obj_t ssl_sslcontext_make_new(const mp_obj_type_t *type, size_t n_args return MP_OBJ_FROM_PTR(s); } -//| def load_cert_chain(self, certfile: str, keyfile: str) -> None: +//| def load_cert_chain( +//| self, certfile: str, keyfile: str | hardwarekey.HardwareKey | None = None +//| ) -> None: //| """Load a private key and the corresponding certificate. //| //| The certfile string must be the path to a single file in PEM format //| containing the certificate as well as any number of CA certificates -//| needed to establish the certificate's authenticity. The keyfile string -//| must point to a file containing the private key. +//| needed to establish the certificate's authenticity. +//| +//| ``keyfile`` is either the path to a file containing the private key, or +//| a `hardwarekey.HardwareKey` whose key never leaves the hardware -- in +//| which case signing during the TLS handshake is done by the hardware and +//| the private key is never exposed. If ``keyfile`` is omitted, the private +//| key is read from ``certfile``. //| """ //| @@ -60,15 +71,34 @@ static mp_obj_t ssl_sslcontext_load_cert_chain(size_t n_args, const mp_obj_t *po mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; mp_arg_parse_all(n_args - 1, pos_args + 1, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); - mp_buffer_info_t cert_buf, key_buf; + mp_buffer_info_t cert_buf, key_buf = { 0 }; + psa_key_id_t hw_key_id = 0; get_file_contents(args[ARG_certfile].u_obj, &cert_buf); - if (args[ARG_keyfile].u_obj != mp_const_none) { - get_file_contents(args[ARG_keyfile].u_obj, &key_buf); + + mp_obj_t keyfile = args[ARG_keyfile].u_obj; + #if CIRCUITPY_HARDWAREKEY + if (mp_obj_is_type(keyfile, &hardwarekey_hardwarekey_type)) { + hardwarekey_hardwarekey_obj_t *key = MP_OBJ_TO_PTR(keyfile); + if (common_hal_hardwarekey_hardwarekey_get_purpose(key) != HARDWAREKEY_PURPOSE_DS) { + mp_raise_ValueError_varg(MP_ERROR_TEXT("key does not have the expected %q purpose"), MP_QSTR_HMAC_DOWN_DIGITAL_SIGNATURE); + } + // TLS client-cert auth signs the handshake, so this commits the key to + // signing (see HardwareKey.sign()'s docstring on the one-algorithm-per- + // loaded-key rule) -- raises if load_ds_params() hasn't been called, or + // if this key already committed to a different algorithm (e.g. decrypt()). + common_hal_hardwarekey_hardwarekey_ensure_algorithm(key, + PSA_ALG_RSA_PKCS1V15_SIGN(PSA_ALG_ANY_HASH), + PSA_KEY_USAGE_SIGN_MESSAGE | PSA_KEY_USAGE_SIGN_HASH); + hw_key_id = common_hal_hardwarekey_hardwarekey_get_key_id(key); + } else + #endif + if (keyfile != mp_const_none) { + get_file_contents(keyfile, &key_buf); } else { key_buf = cert_buf; } - common_hal_ssl_sslcontext_load_cert_chain(self, &cert_buf, &key_buf); + common_hal_ssl_sslcontext_load_cert_chain(self, &cert_buf, &key_buf, hw_key_id); return mp_const_none; } static MP_DEFINE_CONST_FUN_OBJ_KW(ssl_sslcontext_load_cert_chain_obj, 1, ssl_sslcontext_load_cert_chain); diff --git a/shared-bindings/ssl/SSLContext.h b/shared-bindings/ssl/SSLContext.h index e3654e707c2..9b16774326d 100644 --- a/shared-bindings/ssl/SSLContext.h +++ b/shared-bindings/ssl/SSLContext.h @@ -28,4 +28,5 @@ void common_hal_ssl_sslcontext_set_default_verify_paths(ssl_sslcontext_obj_t *se bool common_hal_ssl_sslcontext_get_check_hostname(ssl_sslcontext_obj_t *self); void common_hal_ssl_sslcontext_set_check_hostname(ssl_sslcontext_obj_t *self, bool value); -void common_hal_ssl_sslcontext_load_cert_chain(ssl_sslcontext_obj_t *self, mp_buffer_info_t *cert_buf, mp_buffer_info_t *key_buf); +// hw_key_id nonzero means the private key is an opaque PSA key and key_buf is ignored. +void common_hal_ssl_sslcontext_load_cert_chain(ssl_sslcontext_obj_t *self, mp_buffer_info_t *cert_buf, mp_buffer_info_t *key_buf, psa_key_id_t hw_key_id); diff --git a/shared-module/hardwarekey/HardwareKey.c b/shared-module/hardwarekey/HardwareKey.c index 8ce6a418d00..da399a283a6 100644 --- a/shared-module/hardwarekey/HardwareKey.c +++ b/shared-module/hardwarekey/HardwareKey.c @@ -4,6 +4,8 @@ // // SPDX-License-Identifier: MIT +#include "py/runtime.h" + #include "shared-module/hardwarekey/HardwareKey.h" // These accessors are port-independent. The one port-specific step -- turning a @@ -22,6 +24,49 @@ bool common_hal_hardwarekey_hardwarekey_get_exportable(hardwarekey_hardwarekey_o return self->exportable; } +mp_int_t common_hal_hardwarekey_hardwarekey_get_rsa_key_bits(hardwarekey_hardwarekey_obj_t *self) { + return self->rsa_key_bits; +} + psa_key_id_t common_hal_hardwarekey_hardwarekey_get_key_id(hardwarekey_hardwarekey_obj_t *self) { return self->key_id; } + +// Portable: once ensure_algorithm() (port-specific) has produced a PSA key id +// for the DS peripheral, signing is the same PSA call as any other opaque +// RSA key. The DS peripheral hashes+pads internally when driven this way, so +// callers never see a raw RSA exponentiation primitive. +void common_hal_hardwarekey_hardwarekey_sign(hardwarekey_hardwarekey_obj_t *self, + const uint8_t *data, size_t data_len, uint8_t *sig_out, size_t sig_out_len) { + common_hal_hardwarekey_hardwarekey_ensure_algorithm(self, + PSA_ALG_RSA_PKCS1V15_SIGN(PSA_ALG_ANY_HASH), + PSA_KEY_USAGE_SIGN_MESSAGE | PSA_KEY_USAGE_SIGN_HASH); + + size_t sig_len = 0; + psa_status_t status = psa_sign_message(self->key_id, + PSA_ALG_RSA_PKCS1V15_SIGN(PSA_ALG_SHA_256), + data, data_len, sig_out, sig_out_len, &sig_len); + if (status != PSA_SUCCESS) { + mp_raise_RuntimeError(NULL); + } +} + +// Portable: once ensure_algorithm() (port-specific) has produced a PSA key id +// committed to `alg`, decryption is the same PSA call as any other opaque RSA +// key. The DS peripheral removes the padding internally, so *output_len is the +// recovered plaintext length, not rsa_key_bits / 8. +void common_hal_hardwarekey_hardwarekey_decrypt(hardwarekey_hardwarekey_obj_t *self, + psa_algorithm_t alg, const uint8_t *ciphertext, size_t ciphertext_len, + uint8_t *plaintext_out, size_t plaintext_out_size, size_t *output_len) { + common_hal_hardwarekey_hardwarekey_ensure_algorithm(self, alg, PSA_KEY_USAGE_DECRYPT); + + psa_status_t status = psa_asymmetric_decrypt(self->key_id, alg, + ciphertext, ciphertext_len, NULL, 0, plaintext_out, plaintext_out_size, output_len); + if (status == PSA_ERROR_NOT_SUPPORTED && PSA_ALG_IS_RSA_OAEP(alg)) { + mp_raise_NotImplementedError( + MP_ERROR_TEXT("OAEP requires this build to enable TLS 1.3 support")); + } + if (status != PSA_SUCCESS) { + mp_raise_RuntimeError(NULL); + } +} diff --git a/shared-module/hardwarekey/HardwareKey.h b/shared-module/hardwarekey/HardwareKey.h index 375cb354513..e23b9b59049 100644 --- a/shared-module/hardwarekey/HardwareKey.h +++ b/shared-module/hardwarekey/HardwareKey.h @@ -6,6 +6,7 @@ #pragma once +#include #include #include "py/obj.h" @@ -18,6 +19,13 @@ typedef enum { HARDWAREKEY_PURPOSE_UNUSED = 0, HARDWAREKEY_PURPOSE_HMAC, + // A Digital Signature (RSA) key. Unlike HMAC, the key material isn't + // fully in the eFuse block itself -- the slot only holds the AES key + // that decrypts an externally-supplied, already-encrypted RSA private + // key (`ds_params`) inside the DS peripheral. So a DS-purpose slot is + // usable for `sign()` only after `load_ds_params()` has been called; + // key_id and rsa_key_bits are 0 until then. + HARDWAREKEY_PURPOSE_DS, } hardwarekey_purpose_t; // The handle is portable: it holds a PSA key id. How that id gets created -- @@ -32,15 +40,59 @@ typedef struct { // 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; + // DS purpose only: the RSA modulus size in bits, set by load_ds_params(). + // 0 for an HMAC key, or a DS key before load_ds_params() succeeds. + mp_int_t rsa_key_bits; + // DS purpose only: the PSA algorithm key_id was imported under, once + // ensure_algorithm() has run for the first time since the last + // load_ds_params(). PSA_ALG_NONE (0) until then. A single RSA key must + // not be used under more than one algorithm for its lifetime -- reusing + // it for both signing and decryption (or two different decrypt paddings) + // is a real cryptographic risk, not just a PSA API nicety; see the + // warning on psa_set_key_enrollment_algorithm() in the PSA Crypto API. + // So this key commits to the first algorithm it's actually used with, + // and later calls under a different algorithm are refused. + psa_algorithm_t committed_alg; } 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(). +// An HMAC key is used by passing the object to hmac.new(); a DS key is used by +// calling load_ds_params() once, then sign()/decrypt() repeatedly (with a single +// algorithm -- see committed_alg above). 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. +mp_int_t common_hal_hardwarekey_hardwarekey_get_rsa_key_bits(hardwarekey_hardwarekey_obj_t *self); +// The PSA key id, for hmac.new(). 0 if the slot is unused, or a DS slot whose +// ds_params has not been loaded yet. psa_key_id_t common_hal_hardwarekey_hardwarekey_get_key_id(hardwarekey_hardwarekey_obj_t *self); + +// DS purpose only. Port-specific: caches ds_params and computes rsa_key_bits, but +// does NOT import a PSA key yet -- see ensure_algorithm() below for why. Raises +// ValueError on a malformed blob or a non-DS-purpose key. Safe to call more than +// once: replaces the cached blob and clears any committed algorithm, so the key +// can be recommitted to a (possibly different) algorithm afterward. +void common_hal_hardwarekey_hardwarekey_load_ds_params(hardwarekey_hardwarekey_obj_t *self, const uint8_t *ds_params, size_t ds_params_len); + +// DS purpose only, after load_ds_params(). Port-specific: on the first call since +// load_ds_params(), imports self->key_id under exactly `alg` and `usage` (only the +// port knows how to turn the cached ds_params into a PSA key reference). On a later +// call, either confirms `alg` matches what was already committed (no-op) or raises +// ValueError -- this key already committed to a different algorithm. Callers +// (sign(), decrypt() below) call this before their PSA operation so a HardwareKey +// is never used under two different algorithms over its lifetime. +void common_hal_hardwarekey_hardwarekey_ensure_algorithm(hardwarekey_hardwarekey_obj_t *self, psa_algorithm_t alg, psa_key_usage_t usage); + +// DS purpose only, and only after load_ds_params(). Portable: psa_sign_message() +// against self->key_id. sig_out must be rsa_key_bits / 8 bytes. +void common_hal_hardwarekey_hardwarekey_sign(hardwarekey_hardwarekey_obj_t *self, const uint8_t *data, size_t data_len, uint8_t *sig_out, size_t sig_out_len); + +// DS purpose only, and only after load_ds_params(). Portable: psa_asymmetric_decrypt() +// against self->key_id. plaintext_out must be at least rsa_key_bits / 8 bytes; the +// actual plaintext length (after padding removal) is returned via *output_len. +void common_hal_hardwarekey_hardwarekey_decrypt(hardwarekey_hardwarekey_obj_t *self, + psa_algorithm_t alg, const uint8_t *ciphertext, size_t ciphertext_len, + uint8_t *plaintext_out, size_t plaintext_out_size, size_t *output_len); diff --git a/shared-module/ssl/SSLContext.c b/shared-module/ssl/SSLContext.c index 47e8b9f3149..649d2cd3020 100644 --- a/shared-module/ssl/SSLContext.c +++ b/shared-module/ssl/SSLContext.c @@ -47,7 +47,10 @@ void common_hal_ssl_sslcontext_set_check_hostname(ssl_sslcontext_obj_t *self, bo self->check_name = value; } -void common_hal_ssl_sslcontext_load_cert_chain(ssl_sslcontext_obj_t *self, mp_buffer_info_t *cert_buf, mp_buffer_info_t *key_buf) { +void common_hal_ssl_sslcontext_load_cert_chain(ssl_sslcontext_obj_t *self, mp_buffer_info_t *cert_buf, mp_buffer_info_t *key_buf, psa_key_id_t hw_key_id) { self->cert_buf = *cert_buf; - self->key_buf = *key_buf; + self->hw_key_id = hw_key_id; + if (hw_key_id == 0) { + self->key_buf = *key_buf; + } } diff --git a/shared-module/ssl/SSLContext.h b/shared-module/ssl/SSLContext.h index 293f3143ebc..c04706d066b 100644 --- a/shared-module/ssl/SSLContext.h +++ b/shared-module/ssl/SSLContext.h @@ -9,6 +9,7 @@ #include "py/obj.h" #include "mbedtls/ssl.h" +#include "psa/crypto.h" typedef struct { mp_obj_base_t base; @@ -17,4 +18,8 @@ typedef struct { size_t cacert_bytes; int (*crt_bundle_attach)(mbedtls_ssl_config *conf); mp_buffer_info_t cert_buf, key_buf; + // When nonzero, the client-certificate private key is an opaque PSA key + // (e.g. a hardwarekey.HardwareKey backed by the Digital Signature + // peripheral) and key_buf is unused. + psa_key_id_t hw_key_id; } ssl_sslcontext_obj_t; diff --git a/shared-module/ssl/SSLSocket.c b/shared-module/ssl/SSLSocket.c index a9969505509..30e77073248 100644 --- a/shared-module/ssl/SSLSocket.c +++ b/shared-module/ssl/SSLSocket.c @@ -313,7 +313,14 @@ ssl_sslsocket_obj_t *common_hal_ssl_sslcontext_wrap_socket(ssl_sslcontext_obj_t mbedtls_ssl_set_bio(&o->ssl, o, _mbedtls_ssl_send, _mbedtls_ssl_recv, NULL); if (self->cert_buf.buf != NULL) { - ret = mbedtls_pk_parse_key(&o->pkey, self->key_buf.buf, self->key_buf.len + 1, NULL, 0); + if (self->hw_key_id != 0) { + // The private key lives in hardware (e.g. hardwarekey.HardwareKey + // backed by the Digital Signature peripheral). Wrap the opaque PSA + // key; the handshake signature is computed by the hardware. + ret = mbedtls_pk_wrap_psa(&o->pkey, self->hw_key_id); + } else { + ret = mbedtls_pk_parse_key(&o->pkey, self->key_buf.buf, self->key_buf.len + 1, NULL, 0); + } if (ret != 0) { goto cleanup; }