From 9d2c3c9f800d5118867272c2e3c98086e2a9d7b1 Mon Sep 17 00:00:00 2001 From: Mike Mabey Date: Thu, 10 Sep 2026 15:46:24 -0600 Subject: [PATCH 1/3] Add hmac module: CPython-compatible HMAC backed by PSA Crypto New shared-bindings/shared-module module `hmac`, mirroring the CPython `hmac` API and the three-layer split already used by `hashlib` (bindings + shared-module on the PSA Crypto interface, no common-hal). - hmac.new(key, msg=b"", digestmod) -> HMAC - hmac.digest(key, msg, digest) -> bytes (one-shot) - hmac.compare_digest(a, b) -> bool (constant time) - HMAC: update, digest, hexdigest, copy, digest_size, block_size, name `digestmod` accepts "sha256" or "sha1". The key is a bytes-like object; it is kept as an owned copy and imported into PSA as a volatile PSA_KEY_TYPE_HMAC key for each digest() call, then destroyed -- so there is no finaliser and no PSA key-slot leak. The message fed via update() is buffered so digest() is a single psa_mac_compute(), which keeps copy(), repeated digest(), and update()-after-digest() all CPython-compatible (PSA has no psa_mac_clone()). Build flag CIRCUITPY_HMAC, enabled where a full PSA crypto build with HMAC is already present (espressif's ESP-IDF mbedtls, SSL builds). The CIRCUITPY_HASHLIB_MBEDTLS_ONLY subset does not yet ship the PSA MAC driver, so those ports (and zephyr-cp) are left for a follow-up. Verified on an ESP32-S3-DevKitC-1-N8R8 against RFC 4231 / RFC 2202 vectors and host openssl. --- .codespell/ignore-words.txt | 1 + locale/circuitpython.pot | 6 +- py/circuitpy_defns.mk | 9 +++ py/circuitpy_mpconfig.mk | 6 ++ shared-bindings/hmac/HMAC.c | 135 ++++++++++++++++++++++++++++++++ shared-bindings/hmac/HMAC.h | 26 ++++++ shared-bindings/hmac/__init__.c | 117 +++++++++++++++++++++++++++ shared-bindings/hmac/__init__.h | 9 +++ shared-module/hmac/HMAC.c | 91 +++++++++++++++++++++ shared-module/hmac/__init__.c | 41 ++++++++++ shared-module/hmac/__init__.h | 39 +++++++++ tests/circuitpython/hmac.py | 80 +++++++++++++++++++ tests/circuitpython/hmac.py.exp | 20 +++++ 13 files changed, 579 insertions(+), 1 deletion(-) create mode 100644 shared-bindings/hmac/HMAC.c create mode 100644 shared-bindings/hmac/HMAC.h create mode 100644 shared-bindings/hmac/__init__.c create mode 100644 shared-bindings/hmac/__init__.h create mode 100644 shared-module/hmac/HMAC.c create mode 100644 shared-module/hmac/__init__.c create mode 100644 shared-module/hmac/__init__.h create mode 100644 tests/circuitpython/hmac.py create mode 100644 tests/circuitpython/hmac.py.exp diff --git a/.codespell/ignore-words.txt b/.codespell/ignore-words.txt index 48bee0f30ba..d435ed01dac 100644 --- a/.codespell/ignore-words.txt +++ b/.codespell/ignore-words.txt @@ -27,3 +27,4 @@ straightaway ftbs ftb curren +mabey diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot index c55efbcf8c1..eb0fc413c0e 100644 --- a/locale/circuitpython.pot +++ b/locale/circuitpython.pot @@ -4023,7 +4023,7 @@ msgstr "" msgid "System entry must be gnss.SatelliteSystem" msgstr "" -#: shared-bindings/hashlib/__init__.c +#: shared-bindings/hashlib/__init__.c shared-bindings/hmac/__init__.c msgid "Unsupported hash algorithm" msgstr "" @@ -4366,6 +4366,10 @@ msgstr "" msgid "unsupported colorspace for GifWriter" msgstr "" +#: shared-module/hmac/HMAC.c +msgid "HMAC operation failed" +msgstr "" + #: shared-module/i2cdisplaybus/I2CDisplayBus.c #: shared-module/is31fl3741/IS31FL3741.c #, c-format diff --git a/py/circuitpy_defns.mk b/py/circuitpy_defns.mk index d91ac8ad23b..c3d83928ec4 100755 --- a/py/circuitpy_defns.mk +++ b/py/circuitpy_defns.mk @@ -264,6 +264,9 @@ endif ifeq ($(CIRCUITPY_HASHLIB),1) SRC_PATTERNS += hashlib/% endif +ifeq ($(CIRCUITPY_HMAC),1) +SRC_PATTERNS += hmac/% +endif ifeq ($(CIRCUITPY_I2CDISPLAYBUS),1) SRC_PATTERNS += i2cdisplaybus/% endif @@ -1038,6 +1041,12 @@ SRC_COMMON_HAL_ALL += \ hashlib/__init__.c endif +ifeq ($(CIRCUITPY_HMAC),1) +SRC_SHARED_MODULE_ALL += \ + hmac/HMAC.c \ + hmac/__init__.c +endif + ifeq ($(CIRCUITPY_RGBMATRIX),1) SRC_MOD += $(addprefix lib/protomatter/src/, \ core.c \ diff --git a/py/circuitpy_mpconfig.mk b/py/circuitpy_mpconfig.mk index e7303c1d3b1..7a186fcaa2b 100755 --- a/py/circuitpy_mpconfig.mk +++ b/py/circuitpy_mpconfig.mk @@ -372,6 +372,12 @@ CFLAGS += -DCIRCUITPY_HASHLIB_MBEDTLS=$(CIRCUITPY_HASHLIB_MBEDTLS) CIRCUITPY_HASHLIB_MBEDTLS_ONLY ?= $(call enable-if-all,$(CIRCUITPY_HASHLIB_MBEDTLS) $(call enable-if-not,$(CIRCUITPY_SSL))) CFLAGS += -DCIRCUITPY_HASHLIB_MBEDTLS_ONLY=$(CIRCUITPY_HASHLIB_MBEDTLS_ONLY) +# hmac: CPython-compatible HMAC, backed by PSA Crypto. Available wherever a full PSA +# crypto build with HMAC is already present (SSL builds, espressif's ESP-IDF mbedtls); +# the HASHLIB_MBEDTLS_ONLY subset does not include the PSA MAC driver yet. +CIRCUITPY_HMAC ?= $(call enable-if-all,$(CIRCUITPY_HASHLIB_MBEDTLS) $(call enable-if-not,$(CIRCUITPY_HASHLIB_MBEDTLS_ONLY))) +CFLAGS += -DCIRCUITPY_HMAC=$(CIRCUITPY_HMAC) + # Always zero because it is for Zephyr only CFLAGS += -DCIRCUITPY_HOSTNETWORK=0 diff --git a/shared-bindings/hmac/HMAC.c b/shared-bindings/hmac/HMAC.c new file mode 100644 index 00000000000..42b59ad5c4e --- /dev/null +++ b/shared-bindings/hmac/HMAC.c @@ -0,0 +1,135 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Mike Mabey +// +// SPDX-License-Identifier: MIT + +#include + +#include "shared-bindings/hmac/HMAC.h" + +#include "py/objproperty.h" +#include "py/objstr.h" +#include "py/runtime.h" + +//| class HMAC: +//| """An HMAC object, in progress. Created by `hmac.new()`; it has no user-visible +//| constructor.""" +//| + +//| def update(self, msg: ReadableBuffer) -> None: +//| """Feed more data into the HMAC.""" +//| ... +mp_obj_t hmac_hmac_update(mp_obj_t self_in, mp_obj_t buf_in) { + mp_check_self(mp_obj_is_type(self_in, &hmac_hmac_type)); + hmac_hmac_obj_t *self = MP_OBJ_TO_PTR(self_in); + + mp_buffer_info_t bufinfo; + mp_get_buffer_raise(buf_in, &bufinfo, MP_BUFFER_READ); + + common_hal_hmac_update(self, bufinfo.buf, bufinfo.len); + return mp_const_none; +} +static MP_DEFINE_CONST_FUN_OBJ_2(hmac_hmac_update_obj, hmac_hmac_update); + +//| def digest(self) -> bytes: +//| """Return the HMAC of the data fed so far, as ``digest_size`` bytes. +//| +//| The object can still be updated after this call.""" +//| ... +static mp_obj_t hmac_hmac_digest(mp_obj_t self_in) { + mp_check_self(mp_obj_is_type(self_in, &hmac_hmac_type)); + hmac_hmac_obj_t *self = MP_OBJ_TO_PTR(self_in); + + size_t size = common_hal_hmac_get_digest_size(self); + mp_obj_t obj = mp_obj_new_bytes_of_zeros(size); + mp_obj_str_t *o = MP_OBJ_TO_PTR(obj); + + common_hal_hmac_digest(self, (uint8_t *)o->data, size); + return obj; +} +static MP_DEFINE_CONST_FUN_OBJ_1(hmac_hmac_digest_obj, hmac_hmac_digest); + +//| def hexdigest(self) -> str: +//| """Like `digest()` but returns the MAC as a string of hexadecimal digits.""" +//| ... +static mp_obj_t hmac_hmac_hexdigest(mp_obj_t self_in) { + mp_check_self(mp_obj_is_type(self_in, &hmac_hmac_type)); + hmac_hmac_obj_t *self = MP_OBJ_TO_PTR(self_in); + + size_t size = common_hal_hmac_get_digest_size(self); + uint8_t digest[PSA_HASH_MAX_SIZE]; + common_hal_hmac_digest(self, digest, size); + + vstr_t vstr; + vstr_init_len(&vstr, size * 2); + for (size_t i = 0; i < size; i++) { + vstr.buf[i * 2] = nibble_to_hex_lower[digest[i] >> 4]; + vstr.buf[i * 2 + 1] = nibble_to_hex_lower[digest[i] & 0xf]; + } + return mp_obj_new_str_from_vstr(&vstr); +} +static MP_DEFINE_CONST_FUN_OBJ_1(hmac_hmac_hexdigest_obj, hmac_hmac_hexdigest); + +//| def copy(self) -> HMAC: +//| """Return a copy of this HMAC object, with the same key and data fed so far.""" +//| ... +static mp_obj_t hmac_hmac_copy(mp_obj_t self_in) { + mp_check_self(mp_obj_is_type(self_in, &hmac_hmac_type)); + hmac_hmac_obj_t *self = MP_OBJ_TO_PTR(self_in); + + hmac_hmac_obj_t *other = mp_obj_malloc(hmac_hmac_obj_t, &hmac_hmac_type); + common_hal_hmac_copy(self, other); + return MP_OBJ_FROM_PTR(other); +} +static MP_DEFINE_CONST_FUN_OBJ_1(hmac_hmac_copy_obj, hmac_hmac_copy); + +//| digest_size: int +//| """The size of the MAC in bytes (32 for sha256, 20 for sha1). (read-only)""" +static mp_obj_t hmac_hmac_get_digest_size(mp_obj_t self_in) { + mp_check_self(mp_obj_is_type(self_in, &hmac_hmac_type)); + hmac_hmac_obj_t *self = MP_OBJ_TO_PTR(self_in); + return MP_OBJ_NEW_SMALL_INT(common_hal_hmac_get_digest_size(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(hmac_hmac_get_digest_size_obj, hmac_hmac_get_digest_size); +MP_PROPERTY_GETTER(hmac_hmac_digest_size_obj, (mp_obj_t)&hmac_hmac_get_digest_size_obj); + +//| block_size: int +//| """The internal block size of the hash algorithm in bytes. (read-only)""" +static mp_obj_t hmac_hmac_get_block_size(mp_obj_t self_in) { + mp_check_self(mp_obj_is_type(self_in, &hmac_hmac_type)); + hmac_hmac_obj_t *self = MP_OBJ_TO_PTR(self_in); + return MP_OBJ_NEW_SMALL_INT(common_hal_hmac_get_block_size(self)); +} +MP_DEFINE_CONST_FUN_OBJ_1(hmac_hmac_get_block_size_obj, hmac_hmac_get_block_size); +MP_PROPERTY_GETTER(hmac_hmac_block_size_obj, (mp_obj_t)&hmac_hmac_get_block_size_obj); + +//| name: str +//| """The canonical name of this HMAC, e.g. ``"hmac-sha256"``. (read-only)""" +//| +static mp_obj_t hmac_hmac_get_name(mp_obj_t self_in) { + mp_check_self(mp_obj_is_type(self_in, &hmac_hmac_type)); + hmac_hmac_obj_t *self = MP_OBJ_TO_PTR(self_in); + const char *name = common_hal_hmac_get_name(self); + return mp_obj_new_str(name, strlen(name)); +} +MP_DEFINE_CONST_FUN_OBJ_1(hmac_hmac_get_name_obj, hmac_hmac_get_name); +MP_PROPERTY_GETTER(hmac_hmac_name_obj, (mp_obj_t)&hmac_hmac_get_name_obj); + +static const mp_rom_map_elem_t hmac_hmac_locals_dict_table[] = { + { MP_ROM_QSTR(MP_QSTR_update), MP_ROM_PTR(&hmac_hmac_update_obj) }, + { MP_ROM_QSTR(MP_QSTR_digest), MP_ROM_PTR(&hmac_hmac_digest_obj) }, + { MP_ROM_QSTR(MP_QSTR_hexdigest), MP_ROM_PTR(&hmac_hmac_hexdigest_obj) }, + { MP_ROM_QSTR(MP_QSTR_copy), MP_ROM_PTR(&hmac_hmac_copy_obj) }, + { MP_ROM_QSTR(MP_QSTR_digest_size), MP_ROM_PTR(&hmac_hmac_digest_size_obj) }, + { MP_ROM_QSTR(MP_QSTR_block_size), MP_ROM_PTR(&hmac_hmac_block_size_obj) }, + { MP_ROM_QSTR(MP_QSTR_name), MP_ROM_PTR(&hmac_hmac_name_obj) }, +}; +static MP_DEFINE_CONST_DICT(hmac_hmac_locals_dict, hmac_hmac_locals_dict_table); + +MP_DEFINE_CONST_OBJ_TYPE( + hmac_hmac_type, + MP_QSTR_HMAC, + MP_TYPE_FLAG_HAS_SPECIAL_ACCESSORS, + locals_dict, &hmac_hmac_locals_dict + ); diff --git a/shared-bindings/hmac/HMAC.h b/shared-bindings/hmac/HMAC.h new file mode 100644 index 00000000000..3672d718617 --- /dev/null +++ b/shared-bindings/hmac/HMAC.h @@ -0,0 +1,26 @@ +// 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/hmac/__init__.h" + +extern const mp_obj_type_t hmac_hmac_type; + +// Shared with __init__.c so hmac.new() can feed the initial msg argument. +mp_obj_t hmac_hmac_update(mp_obj_t self_in, mp_obj_t buf_in); + +void common_hal_hmac_new(hmac_hmac_obj_t *self, const uint8_t *key, size_t key_len, + psa_key_id_t borrowed_key_id, psa_algorithm_t hash_alg); +void common_hal_hmac_update(hmac_hmac_obj_t *self, const uint8_t *data, size_t data_len); +void common_hal_hmac_digest(hmac_hmac_obj_t *self, uint8_t *out, size_t out_len); +void common_hal_hmac_copy(hmac_hmac_obj_t *self, hmac_hmac_obj_t *dest); +size_t common_hal_hmac_get_digest_size(hmac_hmac_obj_t *self); +size_t common_hal_hmac_get_block_size(hmac_hmac_obj_t *self); +// Returns "hmac-sha256" / "hmac-sha1" for the .name property (CPython format). +const char *common_hal_hmac_get_name(hmac_hmac_obj_t *self); diff --git a/shared-bindings/hmac/__init__.c b/shared-bindings/hmac/__init__.c new file mode 100644 index 00000000000..79b64599c87 --- /dev/null +++ b/shared-bindings/hmac/__init__.c @@ -0,0 +1,117 @@ +// 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/objstr.h" +#include "py/runtime.h" + +#include "shared-bindings/hmac/__init__.h" +#include "shared-bindings/hmac/HMAC.h" +#include "shared-module/hmac/__init__.h" + +//| """Keyed hashing for message authentication +//| +//| |see_cpython_module| :mod:`cpython:hmac`. +//| +//| Only ``"sha256"`` and ``"sha1"`` are supported for ``digestmod``. +//| """ +//| + +static psa_algorithm_t hash_alg_from_digestmod(mp_obj_t digestmod) { + const char *name = mp_obj_str_get_str(digestmod); + psa_algorithm_t hash_alg; + if (!hmac_hash_alg_from_name(name, &hash_alg)) { + mp_raise_ValueError(MP_ERROR_TEXT("Unsupported hash algorithm")); + } + return hash_alg; +} + +static hmac_hmac_obj_t *hmac_new_internal(mp_obj_t key_in, psa_algorithm_t hash_alg) { + 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: +//| """Create a new HMAC object. +//| +//| :param ReadableBuffer key: the secret key +//| :param ReadableBuffer msg: initial data to authenticate; add more with `HMAC.update()` +//| :param str digestmod: the digest name, ``"sha256"`` or ``"sha1"``. Required. +//| """ +//| ... +//| +static mp_obj_t hmac_new(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + enum { ARG_key, ARG_msg, ARG_digestmod }; + static const mp_arg_t allowed_args[] = { + { MP_QSTR_key, MP_ARG_REQUIRED | MP_ARG_OBJ }, + { MP_QSTR_msg, MP_ARG_OBJ, {.u_obj = mp_const_none} }, + { MP_QSTR_digestmod, MP_ARG_REQUIRED | MP_ARG_OBJ }, + }; + mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)]; + mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args); + + psa_algorithm_t hash_alg = hash_alg_from_digestmod(args[ARG_digestmod].u_obj); + hmac_hmac_obj_t *self = hmac_new_internal(args[ARG_key].u_obj, hash_alg); + + if (args[ARG_msg].u_obj != mp_const_none) { + hmac_hmac_update(MP_OBJ_FROM_PTR(self), args[ARG_msg].u_obj); + } + return MP_OBJ_FROM_PTR(self); +} +static MP_DEFINE_CONST_FUN_OBJ_KW(hmac_new_obj, 1, hmac_new); + +//| def digest(key: ReadableBuffer, 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 +//| intermediate object.""" +//| ... +//| +static mp_obj_t hmac_digest(mp_obj_t key_in, mp_obj_t msg_in, mp_obj_t digest_in) { + psa_algorithm_t hash_alg = hash_alg_from_digestmod(digest_in); + hmac_hmac_obj_t *self = hmac_new_internal(key_in, hash_alg); + hmac_hmac_update(MP_OBJ_FROM_PTR(self), msg_in); + + size_t size = common_hal_hmac_get_digest_size(self); + mp_obj_t obj = mp_obj_new_bytes_of_zeros(size); + mp_obj_str_t *o = MP_OBJ_TO_PTR(obj); + common_hal_hmac_digest(self, (uint8_t *)o->data, size); + return obj; +} +static MP_DEFINE_CONST_FUN_OBJ_3(hmac_digest_obj, hmac_digest); + +//| def compare_digest(a: ReadableBuffer, b: ReadableBuffer) -> bool: +//| """Return ``a == b`` using a constant-time comparison, to avoid leaking timing +//| information about a MAC check.""" +//| ... +//| +static mp_obj_t hmac_compare_digest(mp_obj_t a_in, mp_obj_t b_in) { + mp_buffer_info_t a, b; + mp_get_buffer_raise(a_in, &a, MP_BUFFER_READ); + mp_get_buffer_raise(b_in, &b, MP_BUFFER_READ); + return mp_obj_new_bool(common_hal_hmac_compare_digest(a.buf, a.len, b.buf, b.len)); +} +static MP_DEFINE_CONST_FUN_OBJ_2(hmac_compare_digest_obj, hmac_compare_digest); + +static const mp_rom_map_elem_t hmac_module_globals_table[] = { + { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_hmac) }, + { MP_ROM_QSTR(MP_QSTR_new), MP_ROM_PTR(&hmac_new_obj) }, + { MP_ROM_QSTR(MP_QSTR_digest), MP_ROM_PTR(&hmac_digest_obj) }, + { MP_ROM_QSTR(MP_QSTR_compare_digest), MP_ROM_PTR(&hmac_compare_digest_obj) }, + { MP_ROM_QSTR(MP_QSTR_HMAC), MP_ROM_PTR(&hmac_hmac_type) }, +}; +static MP_DEFINE_CONST_DICT(hmac_module_globals, hmac_module_globals_table); + +const mp_obj_module_t hmac_module = { + .base = { &mp_type_module }, + .globals = (mp_obj_dict_t *)&hmac_module_globals, +}; + +MP_REGISTER_MODULE(MP_QSTR_hmac, hmac_module); diff --git a/shared-bindings/hmac/__init__.h b/shared-bindings/hmac/__init__.h new file mode 100644 index 00000000000..9ea3521ab1e --- /dev/null +++ b/shared-bindings/hmac/__init__.h @@ -0,0 +1,9 @@ +// 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 "shared-bindings/hmac/HMAC.h" diff --git a/shared-module/hmac/HMAC.c b/shared-module/hmac/HMAC.c new file mode 100644 index 00000000000..e208549eebc --- /dev/null +++ b/shared-module/hmac/HMAC.c @@ -0,0 +1,91 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Mike Mabey +// +// SPDX-License-Identifier: MIT + +#include + +#include "py/runtime.h" + +#include "shared-bindings/hmac/HMAC.h" +#include "shared-module/hmac/__init__.h" + +#include "psa/crypto.h" + +#define HMAC_ALG(self) (PSA_ALG_HMAC((self)->hash_alg)) + +void common_hal_hmac_new(hmac_hmac_obj_t *self, const uint8_t *key, size_t key_len, + psa_key_id_t borrowed_key_id, psa_algorithm_t hash_alg) { + self->hash_alg = hash_alg; + self->borrowed_key_id = borrowed_key_id; + self->key = NULL; + self->key_len = 0; + if (borrowed_key_id == 0) { + // Keep our own copy of the caller's key bytes; digest() imports it into + // PSA on demand and destroys the import immediately afterward. + uint8_t *copy = m_malloc(key_len == 0 ? 1 : key_len); + memcpy(copy, key, key_len); + self->key = copy; + self->key_len = key_len; + } + vstr_init(&self->msg, 0); +} + +void common_hal_hmac_update(hmac_hmac_obj_t *self, const uint8_t *data, size_t data_len) { + vstr_add_strn(&self->msg, (const char *)data, data_len); +} + +static void check_psa(psa_status_t status) { + if (status != PSA_SUCCESS) { + mp_raise_RuntimeError(MP_ERROR_TEXT("HMAC operation failed")); + } +} + +void common_hal_hmac_digest(hmac_hmac_obj_t *self, uint8_t *out, size_t out_len) { + check_psa(psa_crypto_init()); + + psa_key_id_t key_id = self->borrowed_key_id; + bool imported = false; + if (key_id == 0) { + psa_key_attributes_t attr = PSA_KEY_ATTRIBUTES_INIT; + psa_set_key_type(&attr, PSA_KEY_TYPE_HMAC); + psa_set_key_bits(&attr, self->key_len * 8); + psa_set_key_algorithm(&attr, HMAC_ALG(self)); + psa_set_key_usage_flags(&attr, PSA_KEY_USAGE_SIGN_MESSAGE | PSA_KEY_USAGE_VERIFY_MESSAGE); + psa_set_key_lifetime(&attr, PSA_KEY_LIFETIME_VOLATILE); + psa_status_t status = psa_import_key(&attr, self->key, self->key_len, &key_id); + check_psa(status); + imported = true; + } + + size_t mac_len = 0; + psa_status_t status = psa_mac_compute(key_id, HMAC_ALG(self), + (const uint8_t *)self->msg.buf, self->msg.len, + out, out_len, &mac_len); + + if (imported) { + psa_destroy_key(key_id); + } + check_psa(status); +} + +void common_hal_hmac_copy(hmac_hmac_obj_t *self, hmac_hmac_obj_t *dest) { + common_hal_hmac_new(dest, self->key, self->key_len, self->borrowed_key_id, self->hash_alg); + vstr_add_strn(&dest->msg, self->msg.buf, self->msg.len); +} + +size_t common_hal_hmac_get_digest_size(hmac_hmac_obj_t *self) { + return PSA_HASH_LENGTH(self->hash_alg); +} + +size_t common_hal_hmac_get_block_size(hmac_hmac_obj_t *self) { + return PSA_HASH_BLOCK_LENGTH(self->hash_alg); +} + +const char *common_hal_hmac_get_name(hmac_hmac_obj_t *self) { + if (self->hash_alg == PSA_ALG_SHA_1) { + return "hmac-sha1"; + } + return "hmac-sha256"; +} diff --git a/shared-module/hmac/__init__.c b/shared-module/hmac/__init__.c new file mode 100644 index 00000000000..756c23824fe --- /dev/null +++ b/shared-module/hmac/__init__.c @@ -0,0 +1,41 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Mike Mabey +// +// SPDX-License-Identifier: MIT + +#include + +#include "shared-module/hmac/__init__.h" + +bool hmac_hash_alg_from_name(const char *name, psa_algorithm_t *hash_alg) { + if (strcmp(name, "sha256") == 0) { + *hash_alg = PSA_ALG_SHA_256; + } else if (strcmp(name, "sha1") == 0) { + *hash_alg = PSA_ALG_SHA_1; + } else { + return false; + } + return true; +} + +bool common_hal_hmac_compare_digest(const uint8_t *a, size_t a_len, const uint8_t *b, size_t b_len) { + // Same shape as CPython's _tscmp: the running time depends only on len(a), + // never on where (or whether) the two inputs first differ. + const uint8_t *left = a; + const uint8_t *right = b; + uint8_t result = 0; + + if (a_len != b_len) { + // Compare a against itself so the loop still runs len(a) iterations, then + // force a mismatch. + right = a; + result = 1; + } + + for (size_t i = 0; i < a_len; i++) { + result |= left[i] ^ right[i]; + } + + return result == 0; +} diff --git a/shared-module/hmac/__init__.h b/shared-module/hmac/__init__.h new file mode 100644 index 00000000000..679b54463c3 --- /dev/null +++ b/shared-module/hmac/__init__.h @@ -0,0 +1,39 @@ +// 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 +#include + +#include "py/misc.h" +#include "py/obj.h" + +#include "psa/crypto.h" + +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; + // Key material for a bytes key: an owned copy on the GC heap. NULL when the + // key lives in hardware (see borrowed_key_id). + const uint8_t *key; + size_t key_len; + // A PSA key id borrowed from a hardwarekey.HardwareKey; the key is owned by + // that object, not this one. 0 (PSA_KEY_ID_NULL) for a bytes key. + psa_key_id_t borrowed_key_id; + // Every byte passed to update(), buffered so digest() can be a one-shot + // psa_mac_compute() and copy()/repeated digest() stay CPython-compatible. + vstr_t msg; +} hmac_hmac_obj_t; + +// Maps a CPython digest name ("sha1", "sha256") to a PSA hash algorithm. +// Returns false for an unsupported name. +bool hmac_hash_alg_from_name(const char *name, psa_algorithm_t *hash_alg); + +// Constant-time equality, matching hmac.compare_digest() / CPython's _tscmp. +bool common_hal_hmac_compare_digest(const uint8_t *a, size_t a_len, const uint8_t *b, size_t b_len); diff --git a/tests/circuitpython/hmac.py b/tests/circuitpython/hmac.py new file mode 100644 index 00000000000..5e6ef99eb96 --- /dev/null +++ b/tests/circuitpython/hmac.py @@ -0,0 +1,80 @@ +try: + import hmac +except ImportError: + print("SKIP") + raise SystemExit + + +def hx(b): + return "".join("%02x" % c for c in b) + + +# RFC 4231 Test Case 1 +print(hmac.new(b"\x0b" * 20, b"Hi There", digestmod="sha256").hexdigest()) +print(hmac.new(b"\x0b" * 20, b"Hi There", digestmod="sha1").hexdigest()) + +# RFC 4231 Test Case 2 ("Jefe") +print(hmac.new(b"Jefe", b"what do ya want for nothing?", digestmod="sha256").hexdigest()) + +# RFC 4231 Test Case 7 (key longer than the block size) +long_key = b"\xaa" * 131 +long_data = ( + b"This is a test using a larger than block-size key and a larger than block-size " + b"data. The key needs to be hashed before being used by the HMAC algorithm." +) +print(hmac.new(long_key, long_data, digestmod="sha256").hexdigest()) + +# key exactly one block, and an empty message +print(hmac.new(b"k" * 64, b"msg", digestmod="sha256").hexdigest()) +print(hmac.new(b"key", b"", digestmod="sha256").hexdigest()) + +# incremental update matches one-shot +m = hmac.new(b"key", digestmod="sha256") +m.update(b"ab") +m.update(b"cde") +print(m.hexdigest() == hmac.new(b"key", b"abcde", digestmod="sha256").hexdigest()) + +# digest() does not finalize: more data can still be added +x = hmac.new(b"key", b"12", digestmod="sha256") +d1 = x.hexdigest() +x.update(b"34") +d2 = x.hexdigest() +print(d1 == hmac.new(b"key", b"12", digestmod="sha256").hexdigest()) +print(d2 == hmac.new(b"key", b"1234", digestmod="sha256").hexdigest()) + +# copy() is independent of the original +a = hmac.new(b"key", b"foo", digestmod="sha256") +b = a.copy() +a.update(b"bar") +print(a.hexdigest() == hmac.new(b"key", b"foobar", digestmod="sha256").hexdigest()) +print(b.hexdigest() == hmac.new(b"key", b"foo", digestmod="sha256").hexdigest()) + +# digest() returns bytes; hexdigest() is its hex +h = hmac.new(b"key", b"abcde", digestmod="sha256") +print(hx(h.digest()) == h.hexdigest()) + +# one-shot module function +one_shot = hmac.digest(b"key", b"abcde", "sha256") +incremental = hmac.new(b"key", b"abcde", digestmod="sha256").digest() +print(one_shot == incremental) + +# metadata +s = hmac.new(b"k", digestmod="sha256") +print(s.digest_size, s.block_size, s.name) +s = hmac.new(b"k", digestmod="sha1") +print(s.digest_size, s.block_size, s.name) + +# unsupported algorithm +try: + hmac.new(b"k", digestmod="md5") +except ValueError: + print("ValueError") + +# compare_digest +good = hmac.new(b"key", b"msg", digestmod="sha256").digest() +bad = bytearray(good) +bad[0] ^= 1 +print(hmac.compare_digest(good, bytes(good))) +print(hmac.compare_digest(good, bytes(bad))) +print(hmac.compare_digest(good, good[:-1])) +print(hmac.compare_digest(memoryview(good), bytearray(good))) diff --git a/tests/circuitpython/hmac.py.exp b/tests/circuitpython/hmac.py.exp new file mode 100644 index 00000000000..edef0b927e1 --- /dev/null +++ b/tests/circuitpython/hmac.py.exp @@ -0,0 +1,20 @@ +b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7 +b617318655057264e28bc0b6fb378c8ef146be00 +5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843 +9b09ffa71b942fcb27635fbcd5b0e944bfdc63644f0713938a7f51535c3a35e2 +d62ad25bb128e96ab6ef43464aaf2bb91b5b85f013381e9ba6be747e8d0911b2 +5d5d139563c95b5967b9bd9a8c9b233a9dedb45072794cd232dc1b74832607d0 +True +True +True +True +True +True +True +32 64 hmac-sha256 +20 64 hmac-sha1 +ValueError +True +False +False +True From dcf977603d45006fff1b7ed423658ded7ca8ac05 Mon Sep 17 00:00:00 2001 From: Mike Mabey Date: Fri, 11 Sep 2026 16:14:32 -0600 Subject: [PATCH 2/3] hmac: use mbedtls_ct_memcmp for compare_digest Addresses tannewt's review comment on PR #11341: the hand-rolled constant-time loop was fine in source but not guaranteed constant-time after compiler optimization. Switch to mbedtls_ct_memcmp(), which uses volatile accesses (and assembly on some platforms) specifically to defeat that. It takes a single length, so the existing a_len/b_len mismatch handling (compare a against itself, force a mismatch) stays in place around it. Verified this compiles and links on espressif_esp32s3_devkitc_1_n8r8. --- shared-module/hmac/__init__.c | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/shared-module/hmac/__init__.c b/shared-module/hmac/__init__.c index 756c23824fe..7d3670dfcc7 100644 --- a/shared-module/hmac/__init__.c +++ b/shared-module/hmac/__init__.c @@ -6,6 +6,7 @@ #include +#include "mbedtls/constant_time.h" #include "shared-module/hmac/__init__.h" bool hmac_hash_alg_from_name(const char *name, psa_algorithm_t *hash_alg) { @@ -21,21 +22,21 @@ bool hmac_hash_alg_from_name(const char *name, psa_algorithm_t *hash_alg) { bool common_hal_hmac_compare_digest(const uint8_t *a, size_t a_len, const uint8_t *b, size_t b_len) { // Same shape as CPython's _tscmp: the running time depends only on len(a), - // never on where (or whether) the two inputs first differ. - const uint8_t *left = a; + // never on where (or whether) the two inputs first differ. The byte + // comparison itself is mbedtls_ct_memcmp(), which is hardened (volatile + // accesses, no early-exit branch) against being optimized into a + // variable-time comparison. const uint8_t *right = b; - uint8_t result = 0; + int mismatch = 0; if (a_len != b_len) { - // Compare a against itself so the loop still runs len(a) iterations, then - // force a mismatch. + // Compare a against itself so the call still does a_len bytes of work, + // then force a mismatch. right = a; - result = 1; + mismatch = 1; } - for (size_t i = 0; i < a_len; i++) { - result |= left[i] ^ right[i]; - } + mismatch |= mbedtls_ct_memcmp(a, right, a_len); - return result == 0; + return mismatch == 0; } From 616929f3b9639edb39edc745d19eb89eab40c5c1 Mon Sep 17 00:00:00 2001 From: Mike Mabey Date: Mon, 14 Sep 2026 17:10:44 -0600 Subject: [PATCH 3/3] hmac: switch update()/digest() to PSA's multipart MAC API Per tannewt's review on #11341: stream update() straight into psa_mac_sign_setup/update/sign_finish instead of buffering the whole message and computing a one-shot psa_mac_compute() at digest() time. digest()/hexdigest() finish the operation once and cache the MAC so repeated calls keep working, but update() after digest() now raises RuntimeError -- PSA has no way to resume a finished multipart operation. copy() now always raises NotImplementedError, since PSA also has no way to clone an in-progress MAC operation (unlike a plain hash, which supports psa_hash_clone()). The owned PSA key for a bytes key is imported once at construction (needed up front for psa_mac_sign_setup) and destroyed once the operation finishes, rather than imported/destroyed per digest() call. Verified on an ESP32-S3-DevKitC-1-N8R8: full RFC 4231/2202 test suite matches expected output, and 200 iterations of new()/update()/digest() show no PSA key-slot leak. --- locale/circuitpython.pot | 8 +++ shared-bindings/hmac/HMAC.c | 21 +++++--- shared-bindings/hmac/HMAC.h | 1 - shared-module/hmac/HMAC.c | 91 ++++++++++++++++++--------------- shared-module/hmac/__init__.h | 27 ++++++---- tests/circuitpython/hmac.py | 23 +++++---- tests/circuitpython/hmac.py.exp | 5 +- 7 files changed, 102 insertions(+), 74 deletions(-) diff --git a/locale/circuitpython.pot b/locale/circuitpython.pot index eb0fc413c0e..59161ef7cdc 100644 --- a/locale/circuitpython.pot +++ b/locale/circuitpython.pot @@ -4027,6 +4027,10 @@ msgstr "" msgid "Unsupported hash algorithm" msgstr "" +#: shared-bindings/hmac/HMAC.c +msgid "HMAC.copy() is not supported" +msgstr "" + #: shared-bindings/i2cioexpander/IOExpander.c msgid "num_pins must be 8 or 16" msgstr "" @@ -4370,6 +4374,10 @@ msgstr "" msgid "HMAC operation failed" msgstr "" +#: shared-module/hmac/HMAC.c +msgid "Cannot update() after digest()" +msgstr "" + #: shared-module/i2cdisplaybus/I2CDisplayBus.c #: shared-module/is31fl3741/IS31FL3741.c #, c-format diff --git a/shared-bindings/hmac/HMAC.c b/shared-bindings/hmac/HMAC.c index 42b59ad5c4e..b766dc34447 100644 --- a/shared-bindings/hmac/HMAC.c +++ b/shared-bindings/hmac/HMAC.c @@ -18,7 +18,10 @@ //| //| def update(self, msg: ReadableBuffer) -> None: -//| """Feed more data into the HMAC.""" +//| """Feed more data into the HMAC. +//| +//| Raises `RuntimeError` if called after `digest()` or `hexdigest()` -- unlike +//| CPython's ``hmac``, this cannot be resumed once a digest has been taken.""" //| ... mp_obj_t hmac_hmac_update(mp_obj_t self_in, mp_obj_t buf_in) { mp_check_self(mp_obj_is_type(self_in, &hmac_hmac_type)); @@ -35,7 +38,9 @@ static MP_DEFINE_CONST_FUN_OBJ_2(hmac_hmac_update_obj, hmac_hmac_update); //| def digest(self) -> bytes: //| """Return the HMAC of the data fed so far, as ``digest_size`` bytes. //| -//| The object can still be updated after this call.""" +//| The first call finishes the underlying MAC computation and caches the +//| result; later calls just return the cached bytes. `update()` can no +//| longer be called after this, unlike CPython's ``hmac``.""" //| ... static mp_obj_t hmac_hmac_digest(mp_obj_t self_in) { mp_check_self(mp_obj_is_type(self_in, &hmac_hmac_type)); @@ -72,15 +77,15 @@ static mp_obj_t hmac_hmac_hexdigest(mp_obj_t self_in) { static MP_DEFINE_CONST_FUN_OBJ_1(hmac_hmac_hexdigest_obj, hmac_hmac_hexdigest); //| def copy(self) -> HMAC: -//| """Return a copy of this HMAC object, with the same key and data fed so far.""" +//| """Not supported; always raises `NotImplementedError`. +//| +//| PSA Crypto's multipart MAC API has no way to duplicate an in-progress MAC +//| operation (unlike a plain hash, which can be cloned), so this HMAC object +//| cannot be copied.""" //| ... static mp_obj_t hmac_hmac_copy(mp_obj_t self_in) { mp_check_self(mp_obj_is_type(self_in, &hmac_hmac_type)); - hmac_hmac_obj_t *self = MP_OBJ_TO_PTR(self_in); - - hmac_hmac_obj_t *other = mp_obj_malloc(hmac_hmac_obj_t, &hmac_hmac_type); - common_hal_hmac_copy(self, other); - return MP_OBJ_FROM_PTR(other); + mp_raise_NotImplementedError(MP_ERROR_TEXT("HMAC.copy() is not supported")); } static MP_DEFINE_CONST_FUN_OBJ_1(hmac_hmac_copy_obj, hmac_hmac_copy); diff --git a/shared-bindings/hmac/HMAC.h b/shared-bindings/hmac/HMAC.h index 3672d718617..49006a131ec 100644 --- a/shared-bindings/hmac/HMAC.h +++ b/shared-bindings/hmac/HMAC.h @@ -19,7 +19,6 @@ void common_hal_hmac_new(hmac_hmac_obj_t *self, const uint8_t *key, size_t key_l psa_key_id_t borrowed_key_id, psa_algorithm_t hash_alg); void common_hal_hmac_update(hmac_hmac_obj_t *self, const uint8_t *data, size_t data_len); void common_hal_hmac_digest(hmac_hmac_obj_t *self, uint8_t *out, size_t out_len); -void common_hal_hmac_copy(hmac_hmac_obj_t *self, hmac_hmac_obj_t *dest); size_t common_hal_hmac_get_digest_size(hmac_hmac_obj_t *self); size_t common_hal_hmac_get_block_size(hmac_hmac_obj_t *self); // Returns "hmac-sha256" / "hmac-sha1" for the .name property (CPython format). diff --git a/shared-module/hmac/HMAC.c b/shared-module/hmac/HMAC.c index e208549eebc..7072c8bfa51 100644 --- a/shared-module/hmac/HMAC.c +++ b/shared-module/hmac/HMAC.c @@ -15,64 +15,73 @@ #define HMAC_ALG(self) (PSA_ALG_HMAC((self)->hash_alg)) -void common_hal_hmac_new(hmac_hmac_obj_t *self, const uint8_t *key, size_t key_len, - psa_key_id_t borrowed_key_id, psa_algorithm_t hash_alg) { - self->hash_alg = hash_alg; - self->borrowed_key_id = borrowed_key_id; - self->key = NULL; - self->key_len = 0; - if (borrowed_key_id == 0) { - // Keep our own copy of the caller's key bytes; digest() imports it into - // PSA on demand and destroys the import immediately afterward. - uint8_t *copy = m_malloc(key_len == 0 ? 1 : key_len); - memcpy(copy, key, key_len); - self->key = copy; - self->key_len = key_len; +// On failure, resets mac_op (per the PSA multipart contract: an operation +// that errors out must be aborted before it can be discarded) and raises. +static void check_psa(hmac_hmac_obj_t *self, psa_status_t status) { + if (status != PSA_SUCCESS) { + psa_mac_abort(&self->mac_op); + mp_raise_RuntimeError(MP_ERROR_TEXT("HMAC operation failed")); } - vstr_init(&self->msg, 0); } -void common_hal_hmac_update(hmac_hmac_obj_t *self, const uint8_t *data, size_t data_len) { - vstr_add_strn(&self->msg, (const char *)data, data_len); -} +void common_hal_hmac_new(hmac_hmac_obj_t *self, const uint8_t *key, size_t key_len, + psa_key_id_t borrowed_key_id, psa_algorithm_t hash_alg) { + self->hash_alg = hash_alg; + self->finished = false; + self->digest_len = 0; + self->mac_op = psa_mac_operation_init(); -static void check_psa(psa_status_t status) { - if (status != PSA_SUCCESS) { + if (psa_crypto_init() != PSA_SUCCESS) { mp_raise_RuntimeError(MP_ERROR_TEXT("HMAC operation failed")); } -} - -void common_hal_hmac_digest(hmac_hmac_obj_t *self, uint8_t *out, size_t out_len) { - check_psa(psa_crypto_init()); - psa_key_id_t key_id = self->borrowed_key_id; - bool imported = false; - if (key_id == 0) { + if (borrowed_key_id != 0) { + self->key_id = borrowed_key_id; + self->owns_key = false; + } else { psa_key_attributes_t attr = PSA_KEY_ATTRIBUTES_INIT; psa_set_key_type(&attr, PSA_KEY_TYPE_HMAC); - psa_set_key_bits(&attr, self->key_len * 8); + psa_set_key_bits(&attr, key_len * 8); psa_set_key_algorithm(&attr, HMAC_ALG(self)); - psa_set_key_usage_flags(&attr, PSA_KEY_USAGE_SIGN_MESSAGE | PSA_KEY_USAGE_VERIFY_MESSAGE); + psa_set_key_usage_flags(&attr, PSA_KEY_USAGE_SIGN_MESSAGE); psa_set_key_lifetime(&attr, PSA_KEY_LIFETIME_VOLATILE); - psa_status_t status = psa_import_key(&attr, self->key, self->key_len, &key_id); - check_psa(status); - imported = true; + if (psa_import_key(&attr, key, key_len, &self->key_id) != PSA_SUCCESS) { + mp_raise_RuntimeError(MP_ERROR_TEXT("HMAC operation failed")); + } + self->owns_key = true; } - size_t mac_len = 0; - psa_status_t status = psa_mac_compute(key_id, HMAC_ALG(self), - (const uint8_t *)self->msg.buf, self->msg.len, - out, out_len, &mac_len); + psa_status_t status = psa_mac_sign_setup(&self->mac_op, self->key_id, HMAC_ALG(self)); + if (status != PSA_SUCCESS) { + if (self->owns_key) { + psa_destroy_key(self->key_id); + self->owns_key = false; + } + mp_raise_RuntimeError(MP_ERROR_TEXT("HMAC operation failed")); + } +} - if (imported) { - psa_destroy_key(key_id); +void common_hal_hmac_update(hmac_hmac_obj_t *self, const uint8_t *data, size_t data_len) { + if (self->finished) { + mp_raise_RuntimeError(MP_ERROR_TEXT("Cannot update() after digest()")); } - check_psa(status); + check_psa(self, psa_mac_update(&self->mac_op, data, data_len)); } -void common_hal_hmac_copy(hmac_hmac_obj_t *self, hmac_hmac_obj_t *dest) { - common_hal_hmac_new(dest, self->key, self->key_len, self->borrowed_key_id, self->hash_alg); - vstr_add_strn(&dest->msg, self->msg.buf, self->msg.len); +void common_hal_hmac_digest(hmac_hmac_obj_t *self, uint8_t *out, size_t out_len) { + if (!self->finished) { + 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 + // can't be resumed, so the owned key's job is done too. + if (self->owns_key) { + psa_destroy_key(self->key_id); + self->owns_key = false; + } + check_psa(self, status); + self->finished = true; + } + memcpy(out, self->digest, out_len); } size_t common_hal_hmac_get_digest_size(hmac_hmac_obj_t *self) { diff --git a/shared-module/hmac/__init__.h b/shared-module/hmac/__init__.h index 679b54463c3..3d5ffa8556e 100644 --- a/shared-module/hmac/__init__.h +++ b/shared-module/hmac/__init__.h @@ -10,7 +10,6 @@ #include #include -#include "py/misc.h" #include "py/obj.h" #include "psa/crypto.h" @@ -19,16 +18,22 @@ 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; - // Key material for a bytes key: an owned copy on the GC heap. NULL when the - // key lives in hardware (see borrowed_key_id). - const uint8_t *key; - size_t key_len; - // A PSA key id borrowed from a hardwarekey.HardwareKey; the key is owned by - // that object, not this one. 0 (PSA_KEY_ID_NULL) for a bytes key. - psa_key_id_t borrowed_key_id; - // Every byte passed to update(), buffered so digest() can be a one-shot - // psa_mac_compute() and copy()/repeated digest() stay CPython-compatible. - vstr_t msg; + // 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. + 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 + // borrowed from a hardwarekey.HardwareKey, that object owns the key and + // owns_key is false. + psa_key_id_t key_id; + bool owns_key; + // Set once digest()/hexdigest() has finished mac_op. update() raises + // after this; further digest() calls just return the cached bytes. + bool finished; + uint8_t digest[PSA_HASH_MAX_SIZE]; + size_t digest_len; } hmac_hmac_obj_t; // Maps a CPython digest name ("sha1", "sha256") to a PSA hash algorithm. diff --git a/tests/circuitpython/hmac.py b/tests/circuitpython/hmac.py index 5e6ef99eb96..d909e7f7ee0 100644 --- a/tests/circuitpython/hmac.py +++ b/tests/circuitpython/hmac.py @@ -34,20 +34,23 @@ def hx(b): m.update(b"cde") print(m.hexdigest() == hmac.new(b"key", b"abcde", digestmod="sha256").hexdigest()) -# digest() does not finalize: more data can still be added -x = hmac.new(b"key", b"12", digestmod="sha256") +# digest() finalizes: a repeated call returns the same cached bytes, but +# update() afterward is no longer allowed +x = hmac.new(b"key", b"1234", digestmod="sha256") d1 = x.hexdigest() -x.update(b"34") d2 = x.hexdigest() -print(d1 == hmac.new(b"key", b"12", digestmod="sha256").hexdigest()) -print(d2 == hmac.new(b"key", b"1234", digestmod="sha256").hexdigest()) +print(d1 == d2 == hmac.new(b"key", b"1234", digestmod="sha256").hexdigest()) +try: + x.update(b"more") +except RuntimeError: + print("RuntimeError") -# copy() is independent of the original +# copy() is not supported: PSA's multipart MAC operation can't be cloned a = hmac.new(b"key", b"foo", digestmod="sha256") -b = a.copy() -a.update(b"bar") -print(a.hexdigest() == hmac.new(b"key", b"foobar", digestmod="sha256").hexdigest()) -print(b.hexdigest() == hmac.new(b"key", b"foo", digestmod="sha256").hexdigest()) +try: + a.copy() +except NotImplementedError: + print("NotImplementedError") # digest() returns bytes; hexdigest() is its hex h = hmac.new(b"key", b"abcde", digestmod="sha256") diff --git a/tests/circuitpython/hmac.py.exp b/tests/circuitpython/hmac.py.exp index edef0b927e1..4b646ba5d39 100644 --- a/tests/circuitpython/hmac.py.exp +++ b/tests/circuitpython/hmac.py.exp @@ -6,9 +6,8 @@ d62ad25bb128e96ab6ef43464aaf2bb91b5b85f013381e9ba6be747e8d0911b2 5d5d139563c95b5967b9bd9a8c9b233a9dedb45072794cd232dc1b74832607d0 True True -True -True -True +RuntimeError +NotImplementedError True True 32 64 hmac-sha256