Skip to content

STM32: add otp_crypto with mbedTLS#2299

Merged
bettio merged 1 commit into
atomvm:release-0.7from
pguyot:w19/stm32-mbedtls
May 11, 2026
Merged

STM32: add otp_crypto with mbedTLS#2299
bettio merged 1 commit into
atomvm: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
Copy link
Copy Markdown
Contributor

@petermm petermm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The contributor addressed nearly all of our findings. New commit is 6e69109 on branch w19/stm32-mbedtls.

Addressed:

  • ✅ NIF precedence (fix #1) — New otp_crypto_platform.c uses REGISTER_NIF_COLLECTION; platform_nifs.c reverted to original shape.
  • ✅ F4 regex (fix #2) — Better than suggested: switched from blacklist to allowlist per AN4230. Universal-RNG families enumerated (f2/f7/g4/h5/h7/l4/u3/u5/wb), F4 RNG parts listed (05/15/07/17/10/12/13/23/27/37/29/39/69/79), G0 security line (41/61/81/c1). F446 now correctly excluded.
  • ✅ MBEDTLS_ERR_ENTROPY_SOURCE_FAILED (fix #3) — applied in both error paths.
  • ✅ Free platform_data + locked_pins (fix #4 partial) — added.
  • ✅ Test coverage — added deterministic crypto:hash tests for md5/sha/sha224/sha256/sha384/sha512 across binary/string/iolist inputs plus badarg checks.

Not addressed (intentional):

  • ❌ Graceful RNG degradation (fix #5) — kept fail-fast AVM_ABORT(). Comment now states "HAL_RNG_Init succeeded in stm32_rng_hw_init or we aborted", so HAL_RNG_DeInit remains unconditional and rng_is_initialized flag was not added. This is a deliberate policy choice — defensible given init runs once at boot.
  • ❌ CI build-matrix check for RNG detection per device — not added (low priority since the regex was rewritten more rigorously).

Overall: the PR is in much better shape and the remaining items are policy choices, not bugs.

@bettio bettio merged commit 4fcddfb into atomvm:release-0.7 May 11, 2026
171 of 182 checks passed
@pguyot pguyot deleted the w19/stm32-mbedtls branch May 11, 2026 18:35
bettio added a commit that referenced this pull request May 14, 2026
Merge fixes, features, and improvements from release-0.7, including:
- Add UART support to RP2 platform (#2125)
- Fix uart:open/1,2 and gpio:open/0, gpio:start/0 API (#2258)
- Fix localtime/1 memory leak, use-after-free, and TZ restore bugs (#2291)
- Fix failure of lldb test on macOS 26 runner (#2296)
- Fix two close bugs in atomvm:subprocess/4 (#2297)
- CI: fix esp32c61 flappiness (#2298)
- STM32: add otp_crypto with mbedTLS (#2299)

uart:open/1,2 and gpio:open/0, gpio:start/0 now raise on failure
instead of returning {error, Reason}. Update any code that
pattern-matched the error tuple.
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.

3 participants