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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .codespell/ignore-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ straightaway
ftbs
ftb
curren
mabey
14 changes: 13 additions & 1 deletion locale/circuitpython.pot
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
Expand Down Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions py/circuitpy_defns.mk
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 \
Expand Down
6 changes: 6 additions & 0 deletions py/circuitpy_mpconfig.mk
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
140 changes: 140 additions & 0 deletions shared-bindings/hmac/HMAC.c
Original file line number Diff line number Diff line change
@@ -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 <string.h>

#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
);
25 changes: 25 additions & 0 deletions shared-bindings/hmac/HMAC.h
Original file line number Diff line number Diff line change
@@ -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);
117 changes: 117 additions & 0 deletions shared-bindings/hmac/__init__.c
Original file line number Diff line number Diff line change
@@ -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);
9 changes: 9 additions & 0 deletions shared-bindings/hmac/__init__.h
Original file line number Diff line number Diff line change
@@ -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"
Loading
Loading