Skip to content

STM32: add otp_crypto with mbedTLS#2299

Open
pguyot wants to merge 1 commit intoatomvm:release-0.7from
pguyot:w19/stm32-mbedtls
Open

STM32: add otp_crypto with mbedTLS#2299
pguyot wants to merge 1 commit intoatomvm:release-0.7from
pguyot:w19/stm32-mbedtls

Conversation

@pguyot
Copy link
Copy Markdown
Collaborator

@pguyot pguyot commented May 9, 2026

These changes are made under both the "Apache 2.0" and the "GNU Lesser General
Public License 2.1 or later" license terms (dual license).

SPDX-License-Identifier: Apache-2.0 OR LGPL-2.1-or-later

@petermm
Copy link
Copy Markdown
Contributor

petermm commented May 10, 2026

LGTM!

Amp found the following, pick and choose as always:

PR Review — STM32: add otp_crypto with mbedTLS

Commit: 2a4da4be18404d4a9708d753c95a08c42bc0e172
Author: Paul Guyot pguyot@kallisys.net
Date: Sat May 9 13:43:56 2026

TL;DR

Good direction, mostly coherent implementation. Almost there, but not merge-ready until two real correctness issues are fixed:

  1. platform_nifs.c reordering changes long-standing NIF lookup precedence on STM32.
  2. F4 RNG detection is incomplete — STM32F446 lacks RNG and is currently treated as having one.

A small robustness pass in sys.c (proper mbedTLS error code, state-tracked teardown, optional graceful degradation on RNG init failure) is also recommended.


Should fix before merge

1. NIF lookup precedence regression in platform_nifs.c

Severity: medium (no current functional break, but a long-standing platform invariant changes)

Old behavior:

const struct Nif *nif = nif_collection_resolve_nif(nifname);
if (nif) {
    return nif;
}
return NULL;

New behavior:

#ifdef ATOMVM_HAS_MBEDTLS
const struct Nif *nif = otp_crypto_nif_get_nif(nifname);
if (nif) {
    return nif;
}
#endif
return nif_collection_resolve_nif(nifname);

When ATOMVM_HAS_MBEDTLS is defined this gives otp_crypto precedence over registered collections (gpio, i2c, spi, …). No conflict today, but it diverges from the prior model and from how RP2/ESP32 do it.

Preferred fix: register otp_crypto as a normal NIF collection (like RP2/ESP32)

New file src/platforms/stm32/src/lib/otp_crypto_platform.c:

#include <otp_crypto.h>
#include <portnifloader.h>

REGISTER_NIF_COLLECTION(otp_crypto, NULL, NULL, otp_crypto_nif_get_nif)

CMake addition:

if (STM32_HAS_RNG)
    list(APPEND HEADER_FILES ../../../../libAtomVM/otp_crypto.h)
    list(APPEND SOURCE_FILES
        ../../../../libAtomVM/otp_crypto.c
        otp_crypto_platform.c
    )
endif()

Then revert platform_nifs.c to its previous shape:

const struct Nif *platform_nifs_get_nif(const char *nifname)
{
    if (strcmp("atomvm:platform/0", nifname) == 0) {
        TRACE("Resolved platform nif %s ...\n", nifname);
        return &atomvm_platform_nif;
    }
    return nif_collection_resolve_nif(nifname);
}

Smallest-diff alternative

Preserve previous precedence by keeping the collection lookup first:

const struct Nif *platform_nifs_get_nif(const char *nifname)
{
    if (strcmp("atomvm:platform/0", nifname) == 0) {
        TRACE("Resolved platform nif %s ...\n", nifname);
        return &atomvm_platform_nif;
    }

    const struct Nif *nif = nif_collection_resolve_nif(nifname);
    if (nif) {
        return nif;
    }

#ifdef ATOMVM_HAS_MBEDTLS
    return otp_crypto_nif_get_nif(nifname);
#else
    return NULL;
#endif
}

2. F4 RNG detection regex misses STM32F446

Severity: medium-high (will mis-build firmware that won't have a working RNG peripheral)

Per ST AN4230 Rev 13, Table 2 ("STM32 lines embedding the RNG hardware peripheral"):

  • F4 with RNG: F405/415, F407/417, F410, F412, F413/423, F427/437, F429/439, F469/479
  • F4 without RNG: F401, F411, F446

Current code:

if (DEVICE_LOWER MATCHES "^stm32f4(0[01]|11)")
    set(STM32_HAS_RNG FALSE)

This matches f400/f401/f411 and (correctly) leaves F410/F412 enabled, but it fails to disable F446.

Suggested fix

# Per ST AN4230 Rev 13, Table 2:
# F4 lines without RNG are F401, F411, and F446.
set(STM32_HAS_RNG TRUE)
if (DEVICE_LOWER MATCHES "^stm32f4(01|11|46)")
    set(STM32_HAS_RNG FALSE)
elseif (DEVICE_LOWER MATCHES "^stm32g0" AND NOT DEVICE_LOWER MATCHES "^stm32g0(41|61|81|c1)")
    set(STM32_HAS_RNG FALSE)
endif()

The G0 regex is correct against AN4230 (G041/G061/G081/G0C1 have RNG; G030/G031/G050/G051/G070/G071/G0B0/G0B1 do not). Worth tightening the comment to cite AN4230 as the source.


Small robustness fixes worth applying

3. Return MBEDTLS_ERR_ENTROPY_SOURCE_FAILED instead of -1

mbedTLS entropy callbacks are expected to return mbedTLS-style negative error codes. -1 happens to "work" but discards information and breaks convention.

int mbedtls_hardware_poll(void *data, unsigned char *output, size_t len, size_t *olen)
{
    *olen = 0;

    GlobalContext *global = data;
    if (IS_NULL_PTR(global) || IS_NULL_PTR(global->platform_data)) {
        return MBEDTLS_ERR_ENTROPY_SOURCE_FAILED;
    }

    struct STM32PlatformData *platform = global->platform_data;
    if (!platform->rng_is_initialized) {
        return MBEDTLS_ERR_ENTROPY_SOURCE_FAILED;
    }

    size_t written = 0;
    while (written < len) {
        uint32_t r;
        if (HAL_RNG_GenerateRandomNumber(&platform->rng, &r) != HAL_OK) {
            return MBEDTLS_ERR_ENTROPY_SOURCE_FAILED;
        }
        size_t chunk = len - written;
        if (chunk > sizeof(r)) {
            chunk = sizeof(r);
        }
        memcpy(output + written, &r, chunk);
        written += chunk;
    }
    *olen = written;
    return 0;
}

4. Track RNG init state, free platform_data, free locked_pins

Today sys_free_platform() calls HAL_RNG_DeInit(&platform->rng) unconditionally. It is "safe by accident" only because init failure aborts the process. It also never frees the platform allocation or the locked pins list.

stm_sys.h:

struct STM32PlatformData
{
    struct ListHead locked_pins;
#ifdef ATOMVM_HAS_MBEDTLS
    RNG_HandleTypeDef rng;
    mbedtls_entropy_context entropy_ctx;
    mbedtls_ctr_drbg_context random_ctx;
    bool rng_is_initialized;
    bool entropy_is_initialized;
    bool random_is_initialized;
#endif
};

sys.c:

void sys_free_platform(GlobalContext *glb)
{
    struct STM32PlatformData *platform = glb->platform_data;
    if (IS_NULL_PTR(platform)) {
        return;
    }

#ifdef ATOMVM_HAS_MBEDTLS
    if (platform->random_is_initialized) {
        mbedtls_ctr_drbg_free(&platform->random_ctx);
    }
    if (platform->entropy_is_initialized) {
        mbedtls_entropy_free(&platform->entropy_ctx);
    }
    if (platform->rng_is_initialized) {
        HAL_RNG_DeInit(&platform->rng);
    }
#endif

    struct ListHead *item;
    struct ListHead *tmp;
    MUTABLE_LIST_FOR_EACH (item, tmp, &platform->locked_pins) {
        struct LockedPin *gpio_pin =
            GET_LIST_ENTRY(item, struct LockedPin, locked_pins_list_head);
        list_remove(item);
        free(gpio_pin);
    }

    free(platform);
    glb->platform_data = NULL;
}

5. Don't AVM_ABORT() the whole VM on RNG init failure

Aborting the firmware boot because the RNG peripheral failed to come up is too aggressive — most of otp_crypto (hashes, ciphers, crypto:info_lib/0) does not need runtime randomness. Only strong_rand_bytes, generate_key, ECDSA signing, etc. need the DRBG path.

void sys_init_platform(GlobalContext *glb)
{
    struct STM32PlatformData *platform = calloc(1, sizeof(struct STM32PlatformData));
    if (IS_NULL_PTR(platform)) {
        AVM_LOGE(TAG, "Out of memory!");
        AVM_ABORT();
    }

    glb->platform_data = platform;
    list_init(&platform->locked_pins);

#ifdef ATOMVM_HAS_MBEDTLS
    if (stm32_rng_hw_init(platform) == 0) {
        platform->rng_is_initialized = true;
    } else {
        AVM_LOGW(TAG, "RNG init failed; crypto random APIs will be unavailable");
    }
#endif
}

And let sys_mbedtls_get_ctr_drbg_context_lock() return NULL instead of aborting on seed failure — nif_crypto_strong_rand_bytes/1 already handles a NULL DRBG context.


Validation of each concern raised in the review request

ID Concern Verdict
A AVM_ABORT() on RNG init failure too harsh Valid — see fix 5
B HAL_RNG_DeInit unconditional in free Valid — see fix 4
C mbedtls_hardware_poll returns -1 Valid — see fix 3
D Seed string duplicated from generic_unix Not borne out — STM32 uses "AtomVM STM32 Mbed-TLS initial seed.", generic_unix uses "AtomVM Mbed-TLS initial seed.". The string is a CTR_DRBG personalization, not a secret. Renaming the local from seed to personalization would be a minor clarity win.
E F4/G0 RNG regex correctness F410/F412 correctly enabled; F446 incorrectly enabled — see fix 2
F STM32_HAS_RNG propagates through add_subdirectory Yes, standard CMake directory-scope inheritance applies
G MBEDTLS_USER_CONFIG_FILE defined on both library and consumer Both are needed: target defs control how mbedTLS is built, library def controls how AtomVM sources see feature macros (e.g. MBEDTLS_PKCS5_C, MBEDTLS_VERSION_C). Keep both.
H platform_nifs.c precedence change Valid — see fix 1

Threading note

No current threading bug — STM32 disables SMP at the top level, so the lack of mutexes around mbedtls_entropy_context / mbedtls_ctr_drbg_context / RNG handle is acceptable today. If STM32 ever enables SMP, copy the locking pattern from generic_unix/rp2/esp32. Not a blocker.


Test coverage gaps

The current Renode stm32_crypto_test is a useful smoke test, but it only proves mbedTLS is linked and random bytes can be produced. It does not catch:

  • wrong no-RNG device classification (would have caught the F446 issue with a build-matrix check)
  • NIF precedence regressions
  • deterministic crypto functionality

Suggested additions

One deterministic crypto assertion in test_crypto.erl:

%% deterministic crypto path
<<16#BA,16#78,16#16,16#BF,16#8F,16#01,16#CF,16#EA,
  16#41,16#41,16#40,16#DE,16#5D,16#AE,16#22,16#23,
  16#B0,16#03,16#61,16#A3,16#96,16#17,16#7A,16#9C,
  16#B4,16#10,16#FF,16#61,16#F2,16#00,16#15,16#AD>> =
    crypto:hash(sha256, <<"abc">>),

Build-matrix check for representative parts (configure-time):

  • Should have RNG: stm32f410…, stm32f412…, stm32g041…, stm32g061…, stm32g081…, stm32g0c1…
  • Should not have RNG: stm32f401…, stm32f411…, stm32f446…, stm32g071…, stm32g0b1…

A CI job that asserts the printed Has RNG : TRUE/FALSE line per device would have caught the F446 omission.


Recommended patch set summary

  1. Preserve NIF lookup semantics — register otp_crypto as a normal NIF collection (preferred), or at minimum keep nif_collection_resolve_nif first in platform_nifs.c.
  2. Fix F4 regex — add 46 to the no-RNG alternation; cite AN4230 in the comment.
  3. Harden sys.ccalloc for platform, add rng_is_initialized, conditional HAL_RNG_DeInit, free platform and locked_pins, return MBEDTLS_ERR_ENTROPY_SOURCE_FAILED, prefer graceful degradation on RNG init failure.
  4. Expand tests — one deterministic crypto:hash/2 assertion + a build-matrix RNG-detection check.

Effort: roughly 1–3 hours.

@pguyot pguyot force-pushed the w19/stm32-mbedtls branch 4 times, most recently from a1f0c1a to b86212c Compare May 10, 2026 10:20
Signed-off-by: Paul Guyot <pguyot@kallisys.net>
@pguyot pguyot force-pushed the w19/stm32-mbedtls branch from b86212c to 6e69109 Compare May 10, 2026 10:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants