From f85ed03adcd1ec1f2a3d2d1a5e4154a0d44103fd Mon Sep 17 00:00:00 2001 From: CValdesS Date: Tue, 13 Oct 2026 00:34:18 +0200 Subject: [PATCH 1/5] nRF54Crypto: run the TRNG only while a draw takes from it begin() started the TRNG and end() stopped it, whoever was still drawing. Readers run on different tasks: Bluefruit seeds the SoftDevice with begin() and never ends, uECC starts it on demand for LESC key generation, and an application that pairs begin()/random()/end() (Meshtastic's HardwareRNG does) could stop it under another task's draw, which then timed out. Meanwhile, after BLE came up nothing stopped it: about 0.5 mA on an nRF54LM20, whose CRACEN has no rings-off standby, measured. random() and uECC's default_CSPRNG now start it, fill and stop it, one draw at a time under a mutex. begin() only registers the generator with uECC and end() has nothing left to do. --- libraries/Bluefruit54Lib/src/bluefruit.cpp | 2 +- libraries/nRF54Crypto/src/nRF54Crypto.cpp | 50 ++++++++++++++++++---- 2 files changed, 42 insertions(+), 10 deletions(-) diff --git a/libraries/Bluefruit54Lib/src/bluefruit.cpp b/libraries/Bluefruit54Lib/src/bluefruit.cpp index 6408faa..0d3c369 100644 --- a/libraries/Bluefruit54Lib/src/bluefruit.cpp +++ b/libraries/Bluefruit54Lib/src/bluefruit.cpp @@ -85,7 +85,7 @@ static void bluefruit_blinky_cb( TimerHandle_t xTimer ) } // s145 has no entropy source of its own and asks the application for a seed (NRF_EVT_RAND_SEED_REQUEST). -// The RNG is left running: LESC key generation draws from it on another task. +// nRF54Crypto runs the TRNG only for each draw, LESC key generation's included. static bool seed_softdevice_rng(void) { uint8_t seed[SD_RAND_SEED_SIZE]; diff --git a/libraries/nRF54Crypto/src/nRF54Crypto.cpp b/libraries/nRF54Crypto/src/nRF54Crypto.cpp index 8218f83..27adc7b 100644 --- a/libraries/nRF54Crypto/src/nRF54Crypto.cpp +++ b/libraries/nRF54Crypto/src/nRF54Crypto.cpp @@ -29,6 +29,10 @@ #include "nrf_cracen.h" #include "nrf_cracen_rng.h" +#include +#include +#include + #include #include #include @@ -100,6 +104,30 @@ static void cracen_rng_stop(void) _rng_started = false; } +// One draw at a time, and the TRNG runs only for the draw: no caller can leave it on and no task can +// stop it under another one's draw. Readers are tasks (SoftDevice seed requests, LESC key generation, +// the application), never interrupts. +static SemaphoreHandle_t _rng_lock; +static StaticSemaphore_t _rng_lock_buf; + +static void rng_lock(void) +{ + if (xTaskGetSchedulerState() == taskSCHEDULER_NOT_STARTED) return; + if (_rng_lock == NULL) { + vTaskSuspendAll(); + if (_rng_lock == NULL) _rng_lock = xSemaphoreCreateMutexStatic(&_rng_lock_buf); + (void) xTaskResumeAll(); + } + (void) xSemaphoreTake(_rng_lock, portMAX_DELAY); +} + +static void rng_unlock(void) +{ + if (_rng_lock != NULL && xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED) { + (void) xSemaphoreGive(_rng_lock); + } +} + // Readers run on different tasks (SoftDevice seed requests, LESC key generation). Another reader, or // a reset clearing the FIFO, between the level check and the read would hand out a word that is not // random, so the two happen in one critical section. Waiting for data stays outside it. @@ -141,30 +169,34 @@ static bool cracen_rng_fill(uint8_t *dest, size_t len) // default_CSPRNG - called by TinyCrypt uECC //--------------------------------------------------------------------+ +static bool rng_draw(uint8_t *dest, size_t len) +{ + rng_lock(); + bool ok = cracen_rng_start() && cracen_rng_fill(dest, len); + cracen_rng_stop(); + rng_unlock(); + return ok; +} + extern "C" int default_CSPRNG(uint8_t *dest, unsigned int size) { - // uECC also draws randomness during ECDH, which may run long after end() stopped the RNG - if (!_rng_started && !cracen_rng_start()) return 0; - return cracen_rng_fill(dest, size) ? 1 : 0; + return rng_draw(dest, size) ? 1 : 0; } //--------------------------------------------------------------------+ // nRF54CryptoClass //--------------------------------------------------------------------+ +// The TRNG starts and stops around each draw (rng_draw), so begin() only registers it with uECC and +// end() has nothing left to stop. Both stay for the callers that pair them. bool nRF54CryptoClass::begin(void) { - if (!cracen_rng_start()) return false; - - // Register our RNG with uECC uECC_set_rng(default_CSPRNG); - return true; } void nRF54CryptoClass::end(void) { - cracen_rng_stop(); } bool nRF54CryptoClass::genKeyPair(uint8_t privkey[32], uint8_t pubkey[64]) @@ -181,7 +213,7 @@ bool nRF54CryptoClass::sharedSecret(const uint8_t peer_pubkey[64], bool nRF54CryptoClass::random(uint8_t* dest, size_t len) { - return cracen_rng_fill(dest, len); + return rng_draw(dest, len); } #endif // NRF_CRACEN From 1f63646adfbbc32875022070a34e9bbc0c73b138 Mon Sep 17 00:00:00 2001 From: CValdesS Date: Tue, 13 Oct 2026 12:01:38 +0200 Subject: [PATCH 2/5] nRF54Crypto: turn the TRNG off when it fails to start cracen_rng_start() enables the RNG module before it waits for the generator. When that wait timed out it returned with _rng_started false, so the cracen_rng_stop() after the failed draw returned early and left the module on until the next successful draw. Both paths now go through cracen_rng_disable(). --- libraries/nRF54Crypto/src/nRF54Crypto.cpp | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/libraries/nRF54Crypto/src/nRF54Crypto.cpp b/libraries/nRF54Crypto/src/nRF54Crypto.cpp index 27adc7b..80cac96 100644 --- a/libraries/nRF54Crypto/src/nRF54Crypto.cpp +++ b/libraries/nRF54Crypto/src/nRF54Crypto.cpp @@ -68,6 +68,16 @@ static void cracen_rng_reset_if_halted(void) cracen_rng_control(false); } +// Turns the generator and its module off, whether it got as far as starting or not +static void cracen_rng_disable(void) +{ + nrf_cracen_rng_control_t cfg = { 0 }; + cfg.enable = false; + nrf_cracen_rng_control_set(NRF_CRACENCORE, &cfg); + + nrf_cracen_module_disable(NRF_CRACEN, NRF_CRACEN_MODULE_RNG_MASK); +} + static bool cracen_rng_start(void) { if (_rng_started) return true; @@ -88,6 +98,9 @@ static bool cracen_rng_start(void) } cracen_rng_reset_if_halted(); } + + // The module is already on: a start that times out must not leave it running + cracen_rng_disable(); return false; } @@ -95,12 +108,7 @@ static void cracen_rng_stop(void) { if (!_rng_started) return; - // Disable RNG - nrf_cracen_rng_control_t cfg = { 0 }; - cfg.enable = false; - nrf_cracen_rng_control_set(NRF_CRACENCORE, &cfg); - - nrf_cracen_module_disable(NRF_CRACEN, NRF_CRACEN_MODULE_RNG_MASK); + cracen_rng_disable(); _rng_started = false; } From 0bf3a702a4df8e15198a4499f3829d312a61a32d Mon Sep 17 00:00:00 2001 From: CValdesS Date: Thu, 15 Oct 2026 17:10:39 +0200 Subject: [PATCH 3/5] nRF54Crypto: guard the CRACEN ENABLE update, don't block on a suspended scheduler The module enable and disable on every draw are read-modify-writes of CRACEN->ENABLE, which also holds the CRYPTOMASTER and PKE/IKG bits. Do them inside a critical section so an application using CRACEN through nrfx cannot lose its bit to a preempted draw. rng_lock() waited on the mutex whenever the scheduler had started, also while it was suspended, where the take asserts. Take it only while the scheduler runs; with it stopped or suspended no other task can get in. rng_lock() returns whether it took the mutex and rng_unlock() takes that. With draws serialized, the rest simplifies: the mutex is created statically up front, rng_draw() starts, fills and disables directly (no _rng_started guards), and the critical section around each FIFO poll goes. begin() no longer calls uECC_set_rng(), which only re-installed the default default_CSPRNG and could undo an RNG installed by the application. The header says random() is for tasks, not interrupts or critical sections. --- libraries/nRF54Crypto/src/nRF54Crypto.cpp | 92 ++++++++++------------- libraries/nRF54Crypto/src/nRF54Crypto.h | 4 +- 2 files changed, 43 insertions(+), 53 deletions(-) diff --git a/libraries/nRF54Crypto/src/nRF54Crypto.cpp b/libraries/nRF54Crypto/src/nRF54Crypto.cpp index 80cac96..90d0806 100644 --- a/libraries/nRF54Crypto/src/nRF54Crypto.cpp +++ b/libraries/nRF54Crypto/src/nRF54Crypto.cpp @@ -47,8 +47,6 @@ nRF54CryptoClass nRF54Crypto; // CRACEN RNG //--------------------------------------------------------------------+ -static bool _rng_started = false; - // Enable with 1x 128-bit block for AES conditioning static void cracen_rng_control(bool soft_reset) { @@ -59,8 +57,7 @@ static void cracen_rng_control(bool soft_reset) nrf_cracen_rng_control_set(NRF_CRACENCORE, &cfg); } -// A halted generator needs a soft reset before it produces again. Done in place rather than through -// stop/start, so a reader on another task never finds the RNG disabled under it. +// A halted generator needs a soft reset before it produces again static void cracen_rng_reset_if_halted(void) { if (nrf_cracen_rng_fsm_state_get(NRF_CRACENCORE) != NRF_CRACEN_RNG_FSM_STATE_ERROR) return; @@ -68,6 +65,19 @@ static void cracen_rng_reset_if_halted(void) cracen_rng_control(false); } +// CRACEN->ENABLE also holds the CRYPTOMASTER and PKE/IKG bits, which an application may be using +// through nrfx; the read-modify-write must not interleave with one of its own. +static void cracen_rng_module_set(bool on) +{ + taskENTER_CRITICAL(); + if (on) { + nrf_cracen_module_enable(NRF_CRACEN, NRF_CRACEN_MODULE_RNG_MASK); + } else { + nrf_cracen_module_disable(NRF_CRACEN, NRF_CRACEN_MODULE_RNG_MASK); + } + taskEXIT_CRITICAL(); +} + // Turns the generator and its module off, whether it got as far as starting or not static void cracen_rng_disable(void) { @@ -75,15 +85,12 @@ static void cracen_rng_disable(void) cfg.enable = false; nrf_cracen_rng_control_set(NRF_CRACENCORE, &cfg); - nrf_cracen_module_disable(NRF_CRACEN, NRF_CRACEN_MODULE_RNG_MASK); + cracen_rng_module_set(false); } static bool cracen_rng_start(void) { - if (_rng_started) return true; - - // Enable CRACEN RNG module - nrf_cracen_module_enable(NRF_CRACEN, NRF_CRACEN_MODULE_RNG_MASK); + cracen_rng_module_set(true); cracen_rng_control(false); // Wait for the FSM to leave RESET/STARTUP; a full FIFO parks it in IDLE_STANDBY (rings off) @@ -93,7 +100,6 @@ static bool cracen_rng_start(void) if (state == NRF_CRACEN_RNG_FSM_STATE_IDLE_READY || state == NRF_CRACEN_RNG_FSM_STATE_IDLE_STANDBY || state == NRF_CRACEN_RNG_FSM_STATE_FILL_FIFO) { - _rng_started = true; return true; } cracen_rng_reset_if_halted(); @@ -104,58 +110,36 @@ static bool cracen_rng_start(void) return false; } -static void cracen_rng_stop(void) -{ - if (!_rng_started) return; - - cracen_rng_disable(); - _rng_started = false; -} - -// One draw at a time, and the TRNG runs only for the draw: no caller can leave it on and no task can -// stop it under another one's draw. Readers are tasks (SoftDevice seed requests, LESC key generation, -// the application), never interrupts. -static SemaphoreHandle_t _rng_lock; +// One draw at a time, and the TRNG runs only for the draw. The mutex is taken only while the +// scheduler runs: before it starts, or while it is suspended, no other task can get in, and blocking +// there would trip FreeRTOS's assert. static StaticSemaphore_t _rng_lock_buf; +static SemaphoreHandle_t _rng_lock = xSemaphoreCreateMutexStatic(&_rng_lock_buf); -static void rng_lock(void) +static bool rng_lock(void) { - if (xTaskGetSchedulerState() == taskSCHEDULER_NOT_STARTED) return; - if (_rng_lock == NULL) { - vTaskSuspendAll(); - if (_rng_lock == NULL) _rng_lock = xSemaphoreCreateMutexStatic(&_rng_lock_buf); - (void) xTaskResumeAll(); - } + if (xTaskGetSchedulerState() != taskSCHEDULER_RUNNING) return false; (void) xSemaphoreTake(_rng_lock, portMAX_DELAY); + return true; } -static void rng_unlock(void) +static void rng_unlock(bool locked) { - if (_rng_lock != NULL && xTaskGetSchedulerState() != taskSCHEDULER_NOT_STARTED) { - (void) xSemaphoreGive(_rng_lock); - } + if (locked) (void) xSemaphoreGive(_rng_lock); } -// Readers run on different tasks (SoftDevice seed requests, LESC key generation). Another reader, or -// a reset clearing the FIFO, between the level check and the read would hand out a word that is not -// random, so the two happen in one critical section. Waiting for data stays outside it. static bool cracen_rng_word(uint32_t *word) { - taskENTER_CRITICAL(); - bool got = nrf_cracen_rng_fifo_level_get(NRF_CRACENCORE) > 0; - if (got) { - *word = nrf_cracen_rng_fifo_get(NRF_CRACENCORE); - } else { + if (nrf_cracen_rng_fifo_level_get(NRF_CRACENCORE) == 0) { cracen_rng_reset_if_halted(); + return false; } - taskEXIT_CRITICAL(); - return got; + *word = nrf_cracen_rng_fifo_get(NRF_CRACENCORE); + return true; } static bool cracen_rng_fill(uint8_t *dest, size_t len) { - if (!_rng_started) return false; - size_t offset = 0; while (offset < len) { uint32_t word; @@ -179,10 +163,14 @@ static bool cracen_rng_fill(uint8_t *dest, size_t len) static bool rng_draw(uint8_t *dest, size_t len) { - rng_lock(); - bool ok = cracen_rng_start() && cracen_rng_fill(dest, len); - cracen_rng_stop(); - rng_unlock(); + bool locked = rng_lock(); + // A failed start has already turned the TRNG off + bool ok = cracen_rng_start(); + if (ok) { + ok = cracen_rng_fill(dest, len); + cracen_rng_disable(); + } + rng_unlock(locked); return ok; } @@ -195,11 +183,11 @@ extern "C" int default_CSPRNG(uint8_t *dest, unsigned int size) // nRF54CryptoClass //--------------------------------------------------------------------+ -// The TRNG starts and stops around each draw (rng_draw), so begin() only registers it with uECC and -// end() has nothing left to stop. Both stay for the callers that pair them. +// The TRNG starts and stops around each draw, and uECC already defaults to default_CSPRNG +// (ecc_platform_specific.h), so begin() and end() have nothing to do. Both stay for the callers that +// pair them. bool nRF54CryptoClass::begin(void) { - uECC_set_rng(default_CSPRNG); return true; } diff --git a/libraries/nRF54Crypto/src/nRF54Crypto.h b/libraries/nRF54Crypto/src/nRF54Crypto.h index 8cd4597..3736a30 100644 --- a/libraries/nRF54Crypto/src/nRF54Crypto.h +++ b/libraries/nRF54Crypto/src/nRF54Crypto.h @@ -40,7 +40,9 @@ class nRF54CryptoClass { const uint8_t privkey[32], uint8_t dhkey[32]); - // Fill buffer with random bytes from CRACEN TRNG + // Fill buffer with random bytes from CRACEN TRNG. Call from a task, or before the scheduler + // starts; not from an interrupt or inside a critical section, where waiting for another + // task's draw cannot work. bool random(uint8_t* dest, size_t len); }; From ff5a19c371f72da437cc224bf373ed60ff5f312a Mon Sep 17 00:00:00 2001 From: CValdesS Date: Thu, 15 Oct 2026 19:36:37 +0200 Subject: [PATCH 4/5] nRF54Crypto: load a new conditioning key on each start, refuse a draw that would cut into another Each draw now starts the TRNG from a soft reset and loads the conditioning key from the first four words out of the FIFO, as nrfx_cracen_entropy_get() does. A generator halted on an error is reset the same way and gets a new key. The wait for those words counts against the per-word timeout of the fill, so cracen_rng_start() no longer has a failure path of its own. A draw with the scheduler suspended skips the mutex, so it could turn the TRNG off under a task in the middle of a draw. That task's next FIFO level read then takes a bus fault (a HardFault at 0x51801004 on the L15-DK). Such a draw now fails while the mutex is held. --- libraries/nRF54Crypto/src/nRF54Crypto.cpp | 104 ++++++++++++---------- libraries/nRF54Crypto/src/nRF54Crypto.h | 3 +- 2 files changed, 61 insertions(+), 46 deletions(-) diff --git a/libraries/nRF54Crypto/src/nRF54Crypto.cpp b/libraries/nRF54Crypto/src/nRF54Crypto.cpp index 90d0806..a0f4ee0 100644 --- a/libraries/nRF54Crypto/src/nRF54Crypto.cpp +++ b/libraries/nRF54Crypto/src/nRF54Crypto.cpp @@ -41,28 +41,32 @@ // Timeout for RNG FIFO fill (in loop iterations) #define RNG_TIMEOUT 100000 +// Words taken from the FIFO for the AES conditioning key (4 words, 128 bits) +#define RNG_KEY_WORDS 4 + nRF54CryptoClass nRF54Crypto; //--------------------------------------------------------------------+ // CRACEN RNG //--------------------------------------------------------------------+ -// Enable with 1x 128-bit block for AES conditioning -static void cracen_rng_control(bool soft_reset) +static bool _rng_key_loaded; + +// Soft reset, then enable with 1x 128-bit block for AES conditioning. As in +// nrfx_cracen_entropy_get(), the first words out of the FIFO after each reset become the new +// conditioning key (cracen_rng_word()). +static void cracen_rng_reset(void) { nrf_cracen_rng_control_t cfg = { 0 }; + cfg.soft_reset = true; + nrf_cracen_rng_control_set(NRF_CRACENCORE, &cfg); + + cfg.soft_reset = false; cfg.enable = true; cfg.number_128_blocks = 1; - cfg.soft_reset = soft_reset; nrf_cracen_rng_control_set(NRF_CRACENCORE, &cfg); -} -// A halted generator needs a soft reset before it produces again -static void cracen_rng_reset_if_halted(void) -{ - if (nrf_cracen_rng_fsm_state_get(NRF_CRACENCORE) != NRF_CRACEN_RNG_FSM_STATE_ERROR) return; - cracen_rng_control(true); - cracen_rng_control(false); + _rng_key_loaded = false; } // CRACEN->ENABLE also holds the CRYPTOMASTER and PKE/IKG bits, which an application may be using @@ -78,7 +82,7 @@ static void cracen_rng_module_set(bool on) taskEXIT_CRITICAL(); } -// Turns the generator and its module off, whether it got as far as starting or not +// Turns the generator and its module off static void cracen_rng_disable(void) { nrf_cracen_rng_control_t cfg = { 0 }; @@ -88,39 +92,34 @@ static void cracen_rng_disable(void) cracen_rng_module_set(false); } -static bool cracen_rng_start(void) +// Every draw starts from a reset, so it gets its own conditioning key +static void cracen_rng_start(void) { cracen_rng_module_set(true); - cracen_rng_control(false); - - // Wait for the FSM to leave RESET/STARTUP; a full FIFO parks it in IDLE_STANDBY (rings off) - uint32_t timeout = RNG_TIMEOUT; - while (timeout--) { - nrf_cracen_rng_fsm_state_t state = nrf_cracen_rng_fsm_state_get(NRF_CRACENCORE); - if (state == NRF_CRACEN_RNG_FSM_STATE_IDLE_READY || - state == NRF_CRACEN_RNG_FSM_STATE_IDLE_STANDBY || - state == NRF_CRACEN_RNG_FSM_STATE_FILL_FIFO) { - return true; - } - cracen_rng_reset_if_halted(); - } - - // The module is already on: a start that times out must not leave it running - cracen_rng_disable(); - return false; + cracen_rng_reset(); } // One draw at a time, and the TRNG runs only for the draw. The mutex is taken only while the -// scheduler runs: before it starts, or while it is suspended, no other task can get in, and blocking -// there would trip FreeRTOS's assert. +// scheduler runs: before it starts no other task can get in, and blocking there, or while the +// scheduler is suspended, would trip FreeRTOS's assert. With the scheduler suspended the draw fails +// if another task holds the mutex: that task may be between the FIFO level check and the read, and +// turning the TRNG off under it would hand it a word that isn't random. static StaticSemaphore_t _rng_lock_buf; static SemaphoreHandle_t _rng_lock = xSemaphoreCreateMutexStatic(&_rng_lock_buf); -static bool rng_lock(void) -{ - if (xTaskGetSchedulerState() != taskSCHEDULER_RUNNING) return false; - (void) xSemaphoreTake(_rng_lock, portMAX_DELAY); - return true; +static bool rng_lock(bool *locked) +{ + *locked = false; + switch (xTaskGetSchedulerState()) { + case taskSCHEDULER_RUNNING: + (void) xSemaphoreTake(_rng_lock, portMAX_DELAY); + *locked = true; + return true; + case taskSCHEDULER_SUSPENDED: + return uxSemaphoreGetCount(_rng_lock) != 0; + default: // not started yet + return true; + } } static void rng_unlock(bool locked) @@ -128,12 +127,27 @@ static void rng_unlock(bool locked) if (locked) (void) xSemaphoreGive(_rng_lock); } +// Takes one word from the FIFO, or returns false while there is none yet. After a reset the first +// RNG_KEY_WORDS words load the conditioning key instead. A generator halted on an error is reset and +// starts again with a new key. static bool cracen_rng_word(uint32_t *word) { - if (nrf_cracen_rng_fifo_level_get(NRF_CRACENCORE) == 0) { - cracen_rng_reset_if_halted(); + if (nrf_cracen_rng_fsm_state_get(NRF_CRACENCORE) == NRF_CRACEN_RNG_FSM_STATE_ERROR) { + cracen_rng_reset(); return false; } + + uint32_t level = nrf_cracen_rng_fifo_level_get(NRF_CRACENCORE); + if (!_rng_key_loaded) { + if (level < RNG_KEY_WORDS) return false; + for (uint8_t i = 0; i < RNG_KEY_WORDS; i++) { + nrf_cracen_rng_key_set(NRF_CRACENCORE, i, nrf_cracen_rng_fifo_get(NRF_CRACENCORE)); + } + _rng_key_loaded = true; + level -= RNG_KEY_WORDS; + } + + if (level == 0) return false; *word = nrf_cracen_rng_fifo_get(NRF_CRACENCORE); return true; } @@ -163,13 +177,13 @@ static bool cracen_rng_fill(uint8_t *dest, size_t len) static bool rng_draw(uint8_t *dest, size_t len) { - bool locked = rng_lock(); - // A failed start has already turned the TRNG off - bool ok = cracen_rng_start(); - if (ok) { - ok = cracen_rng_fill(dest, len); - cracen_rng_disable(); - } + bool locked; + if (!rng_lock(&locked)) return false; + + cracen_rng_start(); + bool ok = cracen_rng_fill(dest, len); + cracen_rng_disable(); + rng_unlock(locked); return ok; } diff --git a/libraries/nRF54Crypto/src/nRF54Crypto.h b/libraries/nRF54Crypto/src/nRF54Crypto.h index 3736a30..c3dfd6d 100644 --- a/libraries/nRF54Crypto/src/nRF54Crypto.h +++ b/libraries/nRF54Crypto/src/nRF54Crypto.h @@ -42,7 +42,8 @@ class nRF54CryptoClass { // Fill buffer with random bytes from CRACEN TRNG. Call from a task, or before the scheduler // starts; not from an interrupt or inside a critical section, where waiting for another - // task's draw cannot work. + // task's draw cannot work. With the scheduler suspended it returns false while another task + // is drawing. bool random(uint8_t* dest, size_t len); }; From 0635d51fd58156e9ebbb621d1d3e493a9618778a Mon Sep 17 00:00:00 2001 From: CValdesS Date: Thu, 15 Oct 2026 19:36:38 +0200 Subject: [PATCH 5/5] BLESecurity: refuse LESC pairing without a key pair or a shared secret begin() ignored the result of genKeyPair(), and the DHKEY request was answered with success even when sharedSecret() failed. A key pair that failed is now generated again when a pairing asks for it, and the pairing is refused if that fails too. A failed shared secret is answered with BLE_GAP_SEC_STATUS_DHKEY_FAILURE. --- libraries/Bluefruit54Lib/src/BLESecurity.cpp | 35 +++++++++++++++++--- libraries/Bluefruit54Lib/src/BLESecurity.h | 3 ++ 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/libraries/Bluefruit54Lib/src/BLESecurity.cpp b/libraries/Bluefruit54Lib/src/BLESecurity.cpp index 757a9ac..a65fdb7 100644 --- a/libraries/Bluefruit54Lib/src/BLESecurity.cpp +++ b/libraries/Bluefruit54Lib/src/BLESecurity.cpp @@ -90,11 +90,24 @@ BLESecurity::BLESecurity(void) _passkey_req_cb = NULL; _complete_cb = NULL; _secured_cb = NULL; +#if LESC_CRYPTO_AVAILABLE + _lesc_keys = false; +#endif } bool BLESecurity::begin(void) { #if LESC_CRYPTO_AVAILABLE + // If this fails, the next pairing tries again before it needs the key pair + _lesc_keys = _genLescKeys(); +#endif + + return true; +} + +#if LESC_CRYPTO_AVAILABLE +bool BLESecurity::_genLescKeys(void) +{ #ifdef NRF_CRYPTOCELL // Initalize Crypto lib for LESC (safe to call multiple times) nRFCrypto.begin(); @@ -118,7 +131,7 @@ bool BLESecurity::begin(void) // Generate P-256 key pair (raw big-endian bytes) uint8_t pubkey_raw[64]; - nRF54Crypto.genKeyPair(_private_key, pubkey_raw); + if ( !nRF54Crypto.genKeyPair(_private_key, pubkey_raw) ) return false; // Copy raw pubkey into _pubkey (skip header byte at [0]) _pubkey[0] = 0x04; // uncompressed point header @@ -129,10 +142,10 @@ bool BLESecurity::begin(void) // Public Key = 32-byte N1 + 32-byte N2 swap_endian(_pubkey+1, 32); swap_endian(_pubkey+1+32, 32); -#endif return true; } +#endif void BLESecurity::setIOCaps(bool display, bool yes_no, bool keyboard) { @@ -310,6 +323,15 @@ void BLESecurity::_eventHandler(ble_evt_t* evt) }; #if LESC_CRYPTO_AVAILABLE + // No key pair from begin(): try again, and refuse the pairing if there is still none + if ( !_lesc_keys ) _lesc_keys = _genLescKeys(); + if ( !_lesc_keys ) + { + LOG_LV1("PAIR", "No LESC key pair, refusing to pair"); + sd_ble_gap_sec_params_reply(conn_hdl, BLE_GAP_SEC_STATUS_UNSPECIFIED, NULL, NULL); + break; + } + keyset.keys_own.p_pk = (ble_gap_lesc_p256_pk_t*) (_pubkey+1); keyset.keys_peer.p_pk = (ble_gap_lesc_p256_pk_t*) (_peer_pubkey+1); #endif @@ -357,6 +379,7 @@ void BLESecurity::_eventHandler(ble_evt_t* evt) swap_endian(_peer_pubkey+1+32, 32); ble_gap_lesc_dhkey_t dhkey; + uint8_t dh_status = BLE_GAP_SEC_STATUS_SUCCESS; #ifdef NRF_CRYPTOCELL // Raw public key from peer device is uncompressed @@ -372,9 +395,11 @@ void BLESecurity::_eventHandler(ble_evt_t* evt) peerPublickKey.end(); #else - // CRACEN path: use TinyCrypt uECC for ECDH + // CRACEN path: use TinyCrypt uECC for ECDH. It fails when the TRNG can't supply its random + // initial Z, or when the result is the point at infinity (a bad peer key). bool ecdh_ok = nRF54Crypto.sharedSecret(_peer_pubkey+1, _private_key, dhkey.key); LOG_LV2("PAIR", "ECDH shared secret %s", ecdh_ok ? "ok" : "FAILED"); + if ( !ecdh_ok ) dh_status = BLE_GAP_SEC_STATUS_DHKEY_FAILURE; #endif // Swap Endian before sending to air @@ -386,8 +411,8 @@ void BLESecurity::_eventHandler(ble_evt_t* evt) // S145 v9 added a sec_status parameter before the dhkey pointer. // BLE_GAP_SEC_STATUS_SUCCESS accepts the derivation; non-success - // codes (BLE_GAP_SEC_STATUS_INVALID_PARAMS etc.) reject pairing. - uint32_t dh_err = sd_ble_gap_lesc_dhkey_reply(conn_hdl, BLE_GAP_SEC_STATUS_SUCCESS, &dhkey); + // codes (BLE_GAP_SEC_STATUS_INVALID_PARAMS etc.) reject pairing, and the dhkey is ignored. + uint32_t dh_err = sd_ble_gap_lesc_dhkey_reply(conn_hdl, dh_status, &dhkey); LOG_LV2("PAIR", "DHKey reply err %lu", dh_err); (void) dh_err; } diff --git a/libraries/Bluefruit54Lib/src/BLESecurity.h b/libraries/Bluefruit54Lib/src/BLESecurity.h index b7c6e6d..fa36e63 100644 --- a/libraries/Bluefruit54Lib/src/BLESecurity.h +++ b/libraries/Bluefruit54Lib/src/BLESecurity.h @@ -89,6 +89,9 @@ class BLESecurity #endif uint8_t _pubkey[1+BLE_GAP_LESC_P256_PK_LEN]; // our public key: 1 header + 64 data uint8_t _peer_pubkey[1+BLE_GAP_LESC_P256_PK_LEN]; // peer public key when using LESC + bool _lesc_keys; // _private_key and _pubkey hold a generated key pair + + bool _genLescKeys(void); #endif bond_keys_t _bond_keys; // Shared keys with bonded device during securing connection, size ~ 80 bytes