From 72414f70ca18e9c0edff982f0d7693e06ff666f2 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 24 Aug 2026 10:33:44 +0200 Subject: [PATCH 1/3] IoT-Safe: serialize APDU transactions with a port mutex All IoT-Safe operations share the file-scope static command/response buffers on a single CSIM channel. Concurrent operations (e.g. a random generation and an ECDSA sign callback from different TLS sessions) could overwrite each other's buffers and consume the wrong modem reply, corrupting cryptographic output or leaving the applet in a persistent error state. Add a port-level mutex (active in multi-threaded builds, no-op when SINGLE_THREADED) held across each APDU transaction. The seven single-transaction operations get thin serialized wrappers. GetRandom locks after its lazy-init check, and the ECDH callback locks its APDU branch, calling the locked impls directly since it already holds the mutex (the software fallback never takes the lock). The mutex is created in iotsafe_init(); document the single-threaded init contract in the doxygen of wolfSSL_CTX_iotsafe_enable() and the CSIM callback setters. Fixes F-10045. --- doc/dox_comments/header_files/iotsafe.h | 10 ++ wolfcrypt/src/port/iotsafe/iotsafe.c | 153 ++++++++++++++++++++++-- 2 files changed, 152 insertions(+), 11 deletions(-) diff --git a/doc/dox_comments/header_files/iotsafe.h b/doc/dox_comments/header_files/iotsafe.h index 31d97838efa..cac143e44d0 100644 --- a/doc/dox_comments/header_files/iotsafe.h +++ b/doc/dox_comments/header_files/iotsafe.h @@ -6,6 +6,12 @@ \return 0 on success \return WC_HW_E on hardware error + This function loads the IoT-Safe applet and must be called from a single + thread, before the context is used concurrently from multiple threads. + Once initialized, individual APDU transactions are serialized internally + in multi-threaded builds, so concurrent TLS sessions can safely share the + same IoT-Safe context. + _Example_ \code WOLFSSL_CTX *ctx; @@ -124,6 +130,8 @@ int wolfSSL_iotsafe_on_ex(WOLFSSL *ssl, byte *privkey_id, usually associated to a read event of a UART channel communicating with the modem. The read callback associated is global and changes for all the contexts that use IoT-safe support at the same time. + This function must be called from a single thread, during initialization, + before any IoT-Safe operation is performed. \param rf Read callback associated to a UART read event. The callback function takes two arguments (buf, len) and return the number of characters read, up to len. When a newline is encountered, the callback should return the number of characters received @@ -149,6 +157,8 @@ void wolfIoTSafe_SetCSIM_read_cb(wolfSSL_IOTSafe_CSIM_read_cb rf); usually associated to a write event on a UART channel communicating with the modem. The write callback associated is global and changes for all the contexts that use IoT-safe support at the same time. + This function must be called from a single thread, during initialization, + before any IoT-Safe operation is performed. \param rf Write callback associated to a UART write event. The callback function takes two arguments (buf, len) and return the number of characters written, up to len. diff --git a/wolfcrypt/src/port/iotsafe/iotsafe.c b/wolfcrypt/src/port/iotsafe/iotsafe.c index 16ebc4ee683..13650dbb86b 100644 --- a/wolfcrypt/src/port/iotsafe/iotsafe.c +++ b/wolfcrypt/src/port/iotsafe/iotsafe.c @@ -76,6 +76,31 @@ static wolfSSL_IOTSafe_CSIM_write_cb csim_write_cb = NULL; static char csim_read_buf[MAXBUF]; static char csim_cmd[IOTSAFE_CMDSIZE_MAX]; +/* All APDU transactions run against a single shared CSIM channel + * (one modem/SIM), so they must be serialized. In multi-threaded + * builds the port keeps a mutex for that purpose; in single-threaded + * builds the lock helpers are no-ops. The mutex is created by + * iotsafe_init(), which must be called from a single thread (see + * wolfSSL_CTX_iotsafe_enable()) before concurrent use of the port. + */ +#if !defined(SINGLE_THREADED) +static wolfSSL_Mutex iotsafe_mutex; +#endif + +static void iotsafe_lock(void) +{ +#if !defined(SINGLE_THREADED) + wc_LockMutex(&iotsafe_mutex); +#endif +} + +static void iotsafe_unlock(void) +{ +#if !defined(SINGLE_THREADED) + wc_UnLockMutex(&iotsafe_mutex); +#endif +} + /* APDU layer: I/O */ static int csim_read(char *buf, int len) { @@ -414,6 +439,12 @@ static int iotsafe_init(void) "AT+CSIM=24,\"01A4040007A0000005590010\"\r\n"; int ret; +#if !defined(SINGLE_THREADED) + ret = wc_InitMutex(&iotsafe_mutex); + if (ret != 0) + return ret; +#endif + do { ret = expect_ok("ATE0\r\n", 6); if (ret == 0) @@ -438,7 +469,7 @@ static int iotsafe_init(void) /* internal: Read File content into a buffer */ -static int iotsafe_readfile(uint8_t *file_id, uint16_t file_id_sz, +static int iotsafe_readfile_locked(uint8_t *file_id, uint16_t file_id_sz, unsigned char *content, int max_size) { char *resp; @@ -522,6 +553,18 @@ static int iotsafe_readfile(uint8_t *file_id, uint16_t file_id_sz, return off; } +/* Serialized entry point: holds iotsafe_mutex across the APDU + * transaction. */ +static int iotsafe_readfile(uint8_t *file_id, uint16_t file_id_sz, + unsigned char *content, int max_size) +{ + int ret; + iotsafe_lock(); + ret = iotsafe_readfile_locked(file_id, file_id_sz, content, max_size); + iotsafe_unlock(); + return ret; +} + static int iotsafe_getrandom(unsigned char* output, unsigned long sz) { char *resp = NULL; @@ -538,6 +581,9 @@ static int iotsafe_getrandom(unsigned char* output, unsigned long sz) } } + /* iotsafe_init() has created iotsafe_mutex, lock from here on */ + iotsafe_lock(); + iotsafe_cmd_start(csim_cmd, IOTSAFE_CLASS, IOTSAFE_INS_GETRANDOM,0,0); bytes_to_hex(&len, csim_cmd + AT_CSIM_CMD_SIZE + AT_CMD_LC_POS, 1); csim_cmd[AT_CSIM_CMD_SIZE + AT_CMD_HDR_SIZE] = 0; @@ -559,6 +605,7 @@ static int iotsafe_getrandom(unsigned char* output, unsigned long sz) for (i = 0; i < IOTSAFE_MAX_RETRIES; i++) { (void)expect_tok(NULL, 0, NULL, NULL); } + iotsafe_unlock(); return ret; } @@ -623,8 +670,8 @@ static int iotsafe_parse_public_key(char* resp, int len, ecc_key *key) * the operation is successful and the public key was found in the * command response and copied to the `key` structure, if not NULL. */ -static int iotsafe_gen_keypair(byte *wr_slot, unsigned long id_size, - ecc_key *key) +static int iotsafe_gen_keypair_locked(byte *wr_slot, unsigned long id_size, + ecc_key *key) { char *resp; int ret = WC_NO_ERR_TRACE(WC_HW_E); @@ -653,7 +700,19 @@ static int iotsafe_gen_keypair(byte *wr_slot, unsigned long id_size, return ret; } -static int iotsafe_get_public_key(byte *pubkey_id, unsigned long id_size, +/* Serialized entry point: holds iotsafe_mutex across the APDU + * transaction. */ +static int iotsafe_gen_keypair(byte *wr_slot, unsigned long id_size, + ecc_key *key) +{ + int ret; + iotsafe_lock(); + ret = iotsafe_gen_keypair_locked(wr_slot, id_size, key); + iotsafe_unlock(); + return ret; +} + +static int iotsafe_get_public_key_locked(byte *pubkey_id, unsigned long id_size, ecc_key *key) { int ret; @@ -672,8 +731,20 @@ static int iotsafe_get_public_key(byte *pubkey_id, unsigned long id_size, return iotsafe_parse_public_key(resp, ret, key); } +/* Serialized entry point: holds iotsafe_mutex across the APDU + * transaction. */ +static int iotsafe_get_public_key(byte *pubkey_id, unsigned long id_size, + ecc_key *key) +{ + int ret; + iotsafe_lock(); + ret = iotsafe_get_public_key_locked(pubkey_id, id_size, key); + iotsafe_unlock(); + return ret; +} + #define PUT_PK_SID 0x02 -static int iotsafe_put_public_key(byte *pubkey_id, unsigned long id_size, +static int iotsafe_put_public_key_locked(byte *pubkey_id, unsigned long id_size, ecc_key *key) { char *resp; @@ -749,8 +820,20 @@ static int iotsafe_put_public_key(byte *pubkey_id, unsigned long id_size, } return ret; } + +/* Serialized entry point: holds iotsafe_mutex across the APDU + * transaction. */ +static int iotsafe_put_public_key(byte *pubkey_id, unsigned long id_size, + ecc_key *key) +{ + int ret; + iotsafe_lock(); + ret = iotsafe_put_public_key_locked(pubkey_id, id_size, key); + iotsafe_unlock(); + return ret; +} #ifdef HAVE_HKDF -static int iotsafe_hkdf_extract(byte* prk, const byte* salt, word32 saltLen, +static int iotsafe_hkdf_extract_locked(byte* prk, const byte* salt, word32 saltLen, byte* ikm, word32 ikmLen, int digest) { int ret; @@ -821,9 +904,21 @@ static int iotsafe_hkdf_extract(byte* prk, const byte* salt, word32 saltLen, } return ret; } + +/* Serialized entry point: holds iotsafe_mutex across the APDU + * transaction. */ +static int iotsafe_hkdf_extract(byte* prk, const byte* salt, word32 saltLen, + byte* ikm, word32 ikmLen, int digest) +{ + int ret; + iotsafe_lock(); + ret = iotsafe_hkdf_extract_locked(prk, salt, saltLen, ikm, ikmLen, digest); + iotsafe_unlock(); + return ret; +} #endif -static int iotsafe_sign_hash(byte *privkey_idx, uint16_t id_size, +static int iotsafe_sign_hash_locked(byte *privkey_idx, uint16_t id_size, uint16_t hash_algo, uint8_t sign_algo, const byte *hash, word32 hashLen, byte *signature, word32 *sigLen) { @@ -938,7 +1033,21 @@ static int iotsafe_sign_hash(byte *privkey_idx, uint16_t id_size, return ret; } -static int iotsafe_verify_hash(byte *pubkey_idx, uint16_t id_size, +/* Serialized entry point: holds iotsafe_mutex across the APDU + * transaction. */ +static int iotsafe_sign_hash(byte *privkey_idx, uint16_t id_size, + uint16_t hash_algo, uint8_t sign_algo, const byte *hash, word32 hashLen, + byte *signature, word32 *sigLen) +{ + int ret; + iotsafe_lock(); + ret = iotsafe_sign_hash_locked(privkey_idx, id_size, hash_algo, sign_algo, + hash, hashLen, signature, sigLen); + iotsafe_unlock(); + return ret; +} + +static int iotsafe_verify_hash_locked(byte *pubkey_idx, uint16_t id_size, uint16_t hash_algo, uint8_t sign_algo, const byte *hash, word32 hashLen, const byte *sig, word32 sigLen, @@ -1032,6 +1141,22 @@ static int iotsafe_verify_hash(byte *pubkey_idx, uint16_t id_size, return ret; } +/* Serialized entry point: holds iotsafe_mutex across the APDU + * transaction. */ +static int iotsafe_verify_hash(byte *pubkey_idx, uint16_t id_size, + uint16_t hash_algo, uint8_t sign_algo, + const byte *hash, word32 hashLen, + const byte *sig, word32 sigLen, + int *result) +{ + int ret; + iotsafe_lock(); + ret = iotsafe_verify_hash_locked(pubkey_idx, id_size, hash_algo, sign_algo, + hash, hashLen, sig, sigLen, result); + iotsafe_unlock(); + return ret; +} + /* * Callbacks for IoT-Safe ECC/ECDH functions @@ -1305,17 +1430,22 @@ static int wolfIoT_ecc_shared_secret(WOLFSSL* ssl, struct ecc_key* otherKey, keypair_slot = (byte *)(&iotsafe->ecdh_keypair_slot); pubkey_idx = (byte *)(&iotsafe->peer_pubkey_slot); + /* Serialize the APDU transactions of this ECDH operation */ + iotsafe_lock(); + /* TLS v1.3 calls key gen already, so don't do it here */ if (wolfSSL_GetVersion(ssl) < WOLFSSL_TLSV1_3) { WOLFSSL_MSG("Generating ECDH key pair"); - ret = iotsafe_gen_keypair(keypair_slot, id_size, tmpKey); + /* iotsafe_mutex is already held: call the locked impls */ + ret = iotsafe_gen_keypair_locked(keypair_slot, id_size, tmpKey); if (ret < 0) { WOLFSSL_MSG("Error generating IoT-safe key pair"); } if (ret == 0) { WOLFSSL_MSG("Public key not yet retrieved, using GetPublic"); /* Importing generated public key */ - ret = iotsafe_get_public_key(keypair_slot, id_size, tmpKey); + ret = iotsafe_get_public_key_locked(keypair_slot, id_size, + tmpKey); if (ret < 0) { WOLFSSL_MSG("Error retrieving public key via GetPublic"); ret = WC_HW_E; @@ -1337,7 +1467,7 @@ static int wolfIoT_ecc_shared_secret(WOLFSSL* ssl, struct ecc_key* otherKey, if (ret == 0) { /* Store received public key from other endpoint in applet */ - ret = iotsafe_put_public_key(pubkey_idx, id_size, otherKey); + ret = iotsafe_put_public_key_locked(pubkey_idx, id_size, otherKey); if (ret < 0) { WOLFSSL_MSG("IoT-SAFE: Error in PutPublic"); } @@ -1378,6 +1508,7 @@ static int wolfIoT_ecc_shared_secret(WOLFSSL* ssl, struct ecc_key* otherKey, ret = 0; } } + iotsafe_unlock(); } else { ecc_key* privKey = NULL; ecc_key* pubKey = NULL; From 9db79413c3ab7a5f572eb68480a1cd34165f8578 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 24 Aug 2026 12:21:31 +0200 Subject: [PATCH 2/3] addressed copilot review - iotsafe_init(): free the mutex on the ATE0 and drain failure paths so a retried init never double-initializes it - guard every exported API entry (wc_iotsafe_*, wolfIoTSafe_GetCert_ex, wolfIoTSafe_GetRandom) against locking an uninitialized mutex via iotsafe_ensure_init(), which lazy-initializes the port before locking - doxygen: wolfIoTSafe_SetCSIM_write_cb param rf -> wf --- doc/dox_comments/header_files/iotsafe.h | 2 +- wolfcrypt/src/port/iotsafe/iotsafe.c | 44 ++++++++++++++++++++----- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/doc/dox_comments/header_files/iotsafe.h b/doc/dox_comments/header_files/iotsafe.h index cac143e44d0..b16ebeb0eb5 100644 --- a/doc/dox_comments/header_files/iotsafe.h +++ b/doc/dox_comments/header_files/iotsafe.h @@ -159,7 +159,7 @@ void wolfIoTSafe_SetCSIM_read_cb(wolfSSL_IOTSafe_CSIM_read_cb rf); IoT-safe support at the same time. This function must be called from a single thread, during initialization, before any IoT-Safe operation is performed. - \param rf Write callback associated to a UART write event. The callback function takes + \param wf Write callback associated to a UART write event. The callback function takes two arguments (buf, len) and return the number of characters written, up to len. _Example_ diff --git a/wolfcrypt/src/port/iotsafe/iotsafe.c b/wolfcrypt/src/port/iotsafe/iotsafe.c index 13650dbb86b..e92c156f7fb 100644 --- a/wolfcrypt/src/port/iotsafe/iotsafe.c +++ b/wolfcrypt/src/port/iotsafe/iotsafe.c @@ -450,8 +450,12 @@ static int iotsafe_init(void) if (ret == 0) ret = expect_tok(NULL, 0, NULL, NULL); } while (ret == 0); - if (ret < 0) + if (ret < 0) { +#if !defined(SINGLE_THREADED) + wc_FreeMutex(&iotsafe_mutex); +#endif return ret; + } WOLFSSL_MSG("ATE0 OK!"); if (expect_csim_response(atcmd_load_applet_str, @@ -461,12 +465,26 @@ static int iotsafe_init(void) } else { WOLFSSL_MSG("IoT Safe Applet INIT OK"); } - if (expect_tok(NULL, 0, NULL, NULL) < 0) + if (expect_tok(NULL, 0, NULL, NULL) < 0) { +#if !defined(SINGLE_THREADED) + wc_FreeMutex(&iotsafe_mutex); +#endif return -1; + } wolfIoT_initialized++; return 0; } +/* Ensure the port is initialized (mutex + applet). First use must + * happen from a single thread; see wolfSSL_CTX_iotsafe_enable(). + * Returns 0 on success, < 0 if initialization failed. */ +static int iotsafe_ensure_init(void) +{ + if (!wolfIoT_initialized) + return iotsafe_init(); + return 0; +} + /* internal: Read File content into a buffer */ static int iotsafe_readfile_locked(uint8_t *file_id, uint16_t file_id_sz, @@ -559,6 +577,8 @@ static int iotsafe_readfile(uint8_t *file_id, uint16_t file_id_sz, unsigned char *content, int max_size) { int ret; + if (iotsafe_ensure_init() != 0) + return WC_HW_E; iotsafe_lock(); ret = iotsafe_readfile_locked(file_id, file_id_sz, content, max_size); iotsafe_unlock(); @@ -575,13 +595,9 @@ static int iotsafe_getrandom(unsigned char* output, unsigned long sz) if (sz == 0 || sz > 255) { return BAD_FUNC_ARG; } - if (!wolfIoT_initialized) { - if (iotsafe_init() < 0) { - return WC_HW_E; - } - } + if (iotsafe_ensure_init() != 0) + return WC_HW_E; - /* iotsafe_init() has created iotsafe_mutex, lock from here on */ iotsafe_lock(); iotsafe_cmd_start(csim_cmd, IOTSAFE_CLASS, IOTSAFE_INS_GETRANDOM,0,0); @@ -706,6 +722,8 @@ static int iotsafe_gen_keypair(byte *wr_slot, unsigned long id_size, ecc_key *key) { int ret; + if (iotsafe_ensure_init() != 0) + return WC_HW_E; iotsafe_lock(); ret = iotsafe_gen_keypair_locked(wr_slot, id_size, key); iotsafe_unlock(); @@ -737,6 +755,8 @@ static int iotsafe_get_public_key(byte *pubkey_id, unsigned long id_size, ecc_key *key) { int ret; + if (iotsafe_ensure_init() != 0) + return WC_HW_E; iotsafe_lock(); ret = iotsafe_get_public_key_locked(pubkey_id, id_size, key); iotsafe_unlock(); @@ -827,6 +847,8 @@ static int iotsafe_put_public_key(byte *pubkey_id, unsigned long id_size, ecc_key *key) { int ret; + if (iotsafe_ensure_init() != 0) + return WC_HW_E; iotsafe_lock(); ret = iotsafe_put_public_key_locked(pubkey_id, id_size, key); iotsafe_unlock(); @@ -911,6 +933,8 @@ static int iotsafe_hkdf_extract(byte* prk, const byte* salt, word32 saltLen, byte* ikm, word32 ikmLen, int digest) { int ret; + if (iotsafe_ensure_init() != 0) + return WC_HW_E; iotsafe_lock(); ret = iotsafe_hkdf_extract_locked(prk, salt, saltLen, ikm, ikmLen, digest); iotsafe_unlock(); @@ -1040,6 +1064,8 @@ static int iotsafe_sign_hash(byte *privkey_idx, uint16_t id_size, byte *signature, word32 *sigLen) { int ret; + if (iotsafe_ensure_init() != 0) + return WC_HW_E; iotsafe_lock(); ret = iotsafe_sign_hash_locked(privkey_idx, id_size, hash_algo, sign_algo, hash, hashLen, signature, sigLen); @@ -1150,6 +1176,8 @@ static int iotsafe_verify_hash(byte *pubkey_idx, uint16_t id_size, int *result) { int ret; + if (iotsafe_ensure_init() != 0) + return WC_HW_E; iotsafe_lock(); ret = iotsafe_verify_hash_locked(pubkey_idx, id_size, hash_algo, sign_algo, hash, hashLen, sig, sigLen, result); From 0b58788f9db445e07fc168c19ef6ba6692cd8ab6 Mon Sep 17 00:00:00 2001 From: Daniele Lacamera Date: Mon, 24 Aug 2026 13:42:45 +0200 Subject: [PATCH 3/3] addressed copilot review wolfIoT_ecc_shared_secret(): ensure the port is initialized before the APDU branch can lock iotsafe_mutex. The check sits before tmpKey allocation (no leak on the early WC_HW_E return) and is gated on iotsafe->enabled so the software fallback path never forces init. --- wolfcrypt/src/port/iotsafe/iotsafe.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/wolfcrypt/src/port/iotsafe/iotsafe.c b/wolfcrypt/src/port/iotsafe/iotsafe.c index e92c156f7fb..36484c3a786 100644 --- a/wolfcrypt/src/port/iotsafe/iotsafe.c +++ b/wolfcrypt/src/port/iotsafe/iotsafe.c @@ -1438,6 +1438,13 @@ static int wolfIoT_ecc_shared_secret(WOLFSSL* ssl, struct ecc_key* otherKey, return BAD_FUNC_ARG; } + /* The APDU path below requires the port to be initialized (mutex + + * applet). Fail early, before allocating, if it is not. */ + if (iotsafe->enabled && iotsafe_ensure_init() != 0) { + WOLFSSL_MSG("IOTSAFE: not initialized"); + return WC_HW_E; + } + WOLFSSL_MSG("IOTSAFE: Called wolfIoT_ecc_shared_secret"); #ifdef DEBUG_IOTSAFE