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..59161ef7cdc 100644 --- a/locale/circuitpython.pot +++ b/locale/circuitpython.pot @@ -4023,10 +4023,14 @@ 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 "" +#: 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 "" @@ -4366,6 +4370,14 @@ msgstr "" msgid "unsupported colorspace for GifWriter" msgstr "" +#: shared-module/hmac/HMAC.c +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/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..b766dc34447 --- /dev/null +++ b/shared-bindings/hmac/HMAC.c @@ -0,0 +1,140 @@ +// 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. +//| +//| 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)); + 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 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)); + 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: +//| """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)); + 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); + +//| 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..49006a131ec --- /dev/null +++ b/shared-bindings/hmac/HMAC.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" + +#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); +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..7072c8bfa51 --- /dev/null +++ b/shared-module/hmac/HMAC.c @@ -0,0 +1,100 @@ +// 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)) + +// 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")); + } +} + +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(); + + if (psa_crypto_init() != PSA_SUCCESS) { + mp_raise_RuntimeError(MP_ERROR_TEXT("HMAC operation failed")); + } + + 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, key_len * 8); + psa_set_key_algorithm(&attr, HMAC_ALG(self)); + psa_set_key_usage_flags(&attr, PSA_KEY_USAGE_SIGN_MESSAGE); + psa_set_key_lifetime(&attr, PSA_KEY_LIFETIME_VOLATILE); + 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; + } + + 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")); + } +} + +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(self, psa_mac_update(&self->mac_op, data, data_len)); +} + +void common_hal_hmac_digest(hmac_hmac_obj_t *self, uint8_t *out, size_t out_len) { + if (!self->finished) { + 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) { + 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..7d3670dfcc7 --- /dev/null +++ b/shared-module/hmac/__init__.c @@ -0,0 +1,42 @@ +// This file is part of the CircuitPython project: https://circuitpython.org +// +// SPDX-FileCopyrightText: Copyright (c) 2026 Mike Mabey +// +// SPDX-License-Identifier: MIT + +#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) { + 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. 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; + int mismatch = 0; + + if (a_len != b_len) { + // Compare a against itself so the call still does a_len bytes of work, + // then force a mismatch. + right = a; + mismatch = 1; + } + + mismatch |= mbedtls_ct_memcmp(a, right, a_len); + + return mismatch == 0; +} diff --git a/shared-module/hmac/__init__.h b/shared-module/hmac/__init__.h new file mode 100644 index 00000000000..3d5ffa8556e --- /dev/null +++ b/shared-module/hmac/__init__.h @@ -0,0 +1,44 @@ +// 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/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; + // 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. +// 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..d909e7f7ee0 --- /dev/null +++ b/tests/circuitpython/hmac.py @@ -0,0 +1,83 @@ +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() 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() +d2 = x.hexdigest() +print(d1 == d2 == hmac.new(b"key", b"1234", digestmod="sha256").hexdigest()) +try: + x.update(b"more") +except RuntimeError: + print("RuntimeError") + +# copy() is not supported: PSA's multipart MAC operation can't be cloned +a = hmac.new(b"key", b"foo", digestmod="sha256") +try: + a.copy() +except NotImplementedError: + print("NotImplementedError") + +# 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..4b646ba5d39 --- /dev/null +++ b/tests/circuitpython/hmac.py.exp @@ -0,0 +1,19 @@ +b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7 +b617318655057264e28bc0b6fb378c8ef146be00 +5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843 +9b09ffa71b942fcb27635fbcd5b0e944bfdc63644f0713938a7f51535c3a35e2 +d62ad25bb128e96ab6ef43464aaf2bb91b5b85f013381e9ba6be747e8d0911b2 +5d5d139563c95b5967b9bd9a8c9b233a9dedb45072794cd232dc1b74832607d0 +True +True +RuntimeError +NotImplementedError +True +True +32 64 hmac-sha256 +20 64 hmac-sha1 +ValueError +True +False +False +True