Chapter 175: Random Number Generator (RNG)
Chapter Objectives
By the end of this chapter, you will be able to:
- Understand the importance of true random numbers in embedded systems, especially for security.
- Differentiate between True Random Number Generators (TRNGs) and Pseudo-Random Number Generators (PRNGs).
- Describe the basic principles behind the ESP32’s hardware Random Number Generator.
- Utilize ESP-IDF APIs to obtain random numbers from the hardware RNG.
- Understand the role of the bootloader in initializing the RNG.
- Recognize best practices for using random numbers in applications.
- Identify potential issues and considerations when working with hardware RNGs.
- Be aware of any significant differences in RNG implementation across ESP32 variants.
Introduction
Random numbers are a fundamental building block in modern computing, particularly in areas like cryptography, secure communications, simulation, and gaming. The quality of these random numbers is paramount; predictable or biased “random” sequences can lead to severe security vulnerabilities or inaccurate results.
Many systems rely on Pseudo-Random Number Generators (PRNGs), which are algorithms that produce sequences of numbers that appear random but are actually deterministic, starting from an initial “seed” value. Given the same seed, a PRNG will always produce the same sequence. While useful for many applications, PRNGs are not suitable for security-sensitive tasks where true unpredictability is required.
ESP32 microcontrollers are equipped with a hardware-based True Random Number Generator (TRNG). This peripheral is designed to harvest entropy (randomness) from physical processes within the chip, such as thermal noise or clock jitter, to produce genuinely unpredictable random numbers.
This chapter explores the ESP32’s hardware RNG, explaining its significance, how to access it using ESP-IDF, and considerations for its effective use. Employing the hardware RNG is crucial for implementing robust security features, generating unique identifiers, and any task demanding high-quality, unpredictable random data.
Theory
True Random Number Generators (TRNGs) vs. Pseudo-Random Number Generators (PRNGs)
It’s essential to distinguish between these two types of random number generators:
- Pseudo-Random Number Generator (PRNG):
- Algorithm-based: Generates numbers using a deterministic mathematical formula.
- Seed-dependent: The sequence is determined by an initial seed value. The same seed always yields the same sequence.
- Periodic: If run long enough, the sequence will eventually repeat.
- Fast: Generally very fast to produce numbers.
- Not cryptographically secure (typically): Standard library PRNGs like
rand()
fromstdlib.h
are usually not suitable for security applications due to their predictability if the seed or algorithm is known. Cryptographically Secure PRNGs (CSPRNGs) exist, but they also require a high-entropy seed. - Example: The
rand()
function in C, Linear Congruential Generators (LCGs), Mersenne Twister.
- True Random Number Generator (TRNG):
- Hardware-based: Derives randomness from unpredictable physical phenomena.
- Non-deterministic: The output cannot be predicted, even with knowledge of previous outputs.
- Aperiodic: The sequence does not repeat.
- Slower (potentially): Harvesting physical entropy can take more time than executing an algorithm.
- Cryptographically secure source: Provides high-quality entropy suitable for cryptographic keys, nonces, initialization vectors, etc.
- Example: ESP32’s hardware RNG.
%%{init: {'theme':'base', 'themeVariables': {'primaryColor': '#DBEAFE', 'primaryTextColor': '#1E40AF', 'primaryBorderColor': '#2563EB'}}}%% graph LR subgraph "True Random Number Generator (TRNG)" direction LR A[Physical Noise Source<br><i>e.g., Thermal Noise, Clock Jitter</i>] --> B(Digitizer); B --> C{Conditioning &<br>Post-Processing}; C --> D[True Random Output]; note["<b>Non-Deterministic</b><br>Unpredictable"] end subgraph "Pseudo-Random Number Generator (PRNG)" direction LR E[Seed Value] --> F(Algorithm<br><i>e.g., LCG, Mersenne Twister</i>); F --> G[Pseudo-Random Output]; G --> H(New State); H --> F; note["<b>Deterministic</b><br>Same seed = Same sequence"] end %% Styling classDef trngNode fill:#D1FAE5,stroke:#059669,stroke-width:1.5px,color:#065F46; classDef prngNode fill:#FEF3C7,stroke:#D97706,stroke-width:1.5px,color:#92400E; classDef note fill:#F3F4F6,stroke:#6B7280; class A,B,C,D trngNode; class E,F,G,H prngNode;
ESP32 Hardware Random Number Generator Architecture
The ESP32’s hardware RNG is designed to provide a continuous stream of true random bits. The exact internal workings are proprietary to Espressif, but common principles for such hardware TRNGs involve:
- Entropy Source: This is the core of the TRNG. It leverages inherently noisy physical processes within the silicon. Common sources include:
- Thermal Noise (Johnson-Nyquist noise): Random motion of charge carriers in resistors.
- Shot Noise: Random fluctuations in electrical current due to the discrete nature of electrons.
- Clock Jitter: Tiny, unpredictable variations in the timing of high-frequency clock signals.
- Metastability in Flip-Flops: Exploiting the unpredictable state resolution when a flip-flop is clocked close to its setup/hold time violations.
- On ESP32 chips, the Wi-Fi/Bluetooth RF subsystem is often a significant source of entropy when active, due to the complex analog noise and environmental radio signals it picks up. The ADC can also be used to sample random noise.
- Digitizer/Sampler: The analog noise from the entropy source is sampled and converted into a digital bitstream.
- Conditioning/Post-Processing (Optional but common): The raw bitstream from the digitizer might have some bias or correlation. TRNGs often include post-processing stages to improve the statistical quality of the random numbers. This can involve:
- Health Tests: Continuous monitoring of the entropy source to detect failures or degradation.
- Decorrelation: Using techniques like XORing with previous bits or passing through cryptographic hash functions or ciphers to remove statistical biases.
- Buffering: Storing generated random bits in a buffer for quick retrieval.
The ESP32’s RNG is designed to meet the requirements for cryptographic applications, providing a reliable source of unpredictability.
ESP-IDF RNG API (esp_random.h
, esp_system.h
)
ESP-IDF provides simple APIs to access the hardware RNG:
void esp_fill_random(void *buf, size_t len)
(Recommended):- This function, declared in
esp_system.h
, fills a bufferbuf
oflen
bytes with random numbers obtained from the hardware RNG. - It is the preferred function for getting random data.
- It ensures that the RNG is properly initialized and functional.
- This function, declared in
uint32_t esp_random(void)
(Legacy, but still functional):- This function, also in
esp_system.h
, returns a 32-bit unsigned random integer from the hardware RNG. - While it works,
esp_fill_random()
is generally more versatile for obtaining multiple bytes.
- This function, also in
Function | Description | Recommendation |
---|---|---|
void esp_fill_random(void *buf, size_t len) |
Fills a buffer of a specified length with random bytes from the hardware RNG. | Preferred Method |
uint32_t esp_random(void) |
Returns a single 32-bit random number from the hardware RNG. | Legacy, but suitable for getting a single random integer. |
bootloader_enable_random() |
Called automatically by the bootloader to initialize the RNG. | No application action needed. |
Bootloader’s Role in RNG Initialization:
The ESP-IDF bootloader plays a crucial role in the early initialization of the hardware RNG. During boot, it collects some initial entropy (e.g., from Wi-Fi/Bluetooth RF noise if the radio is briefly powered, or other sources) to ensure the RNG is seeded and ready for use by the time the application starts.
bootloader_enable_random()
: This function is called by the bootloader to enable and seed the RNG.bootloader_random_disable()
: This function can be called by the application (though rarely needed) to disable the RNG clock, potentially saving a small amount of power if the RNG is not used for extended periods. However, this is generally not recommended unless power is extremely critical and RNG is not needed, as re-enabling and ensuring sufficient entropy might take time.
Quality and Standards:
The random numbers generated by ESP32’s hardware RNG are intended to be suitable for cryptographic purposes and aim to comply with standards like NIST SP 800-90B (Recommendation for the Entropy Sources Used for Random Bit Generation).
Why stdlib.h
rand()
is Not Enough for Security
The standard C library function rand()
(and its companion srand()
for seeding) is a PRNG.
DO: Use ESP-IDF Hardware RNG | DON’T: Use Standard Library PRNG |
---|---|
Use esp_fill_random() to generate cryptographic keys, nonces, IVs, and salts. |
Do not use rand() or srand() for anything security-related. |
Relies on the hardware TRNG for true, unpredictable randomness. | Is a predictable, deterministic algorithm. |
The sequence of numbers is non-repeating and non-deterministic. | The sequence is determined entirely by the initial seed value. |
Suitable for all cryptographic and security applications. | Can lead to severe security vulnerabilities if the seed is known or guessed. |
- Predictable: If an attacker can determine or guess the seed used with
srand()
, they can reproduce the entire sequence of “random” numbers. - Poor Statistical Properties: Often,
rand()
implementations have poor statistical properties that make them unsuitable for cryptography. - Global State:
rand()
often uses a global internal state, which can be problematic in multi-threaded applications if not handled carefully.
For any security-sensitive operation on ESP32 (generating keys, nonces, salts, initialization vectors, challenge values, etc.), always use esp_fill_random()
or esp_random()
to leverage the hardware TRNG.
Practical Examples
The following examples demonstrate how to use the ESP-IDF API to get random numbers.
Example 1: Generating a Random 32-bit Number
This example shows how to get a single 32-bit random number and print it.
1. Project Setup:
Create a new ESP-IDF project or use an existing one.
2. Code (main/main.c
):
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h" // For esp_random() and esp_fill_random()
#include "esp_log.h"
static const char *TAG = "rng_example_1";
void app_main(void)
{
ESP_LOGI(TAG, "Generating a random 32-bit number...");
// The bootloader already enables the RNG.
// No explicit initialization is typically needed in the app.
uint32_t random_number = esp_random();
ESP_LOGI(TAG, "Random number: %u (0x%08X)", random_number, random_number);
// Generate a few more to see different values
for (int i = 0; i < 5; i++) {
ESP_LOGI(TAG, "Another random number: %u", esp_random());
vTaskDelay(pdMS_TO_TICKS(200)); // Small delay
}
}
3. CMakeLists.txt (main/CMakeLists.txt):
Ensure your CMakeLists.txt includes the necessary components (usually pulled in by default esp_system or freertos).
idf_component_register(SRCS "main.c"
INCLUDE_DIRS "."
REQUIRES esp_system esp_log freertos)
4. Build Instructions:
- Open VS Code.
- Ensure your ESP-IDF project is selected and configured for your target ESP32 variant.
- Build the project (Espressif IDF: Build your project).
5. Run/Flash/Observe Steps:
- Flash the built firmware to your ESP32 (Espressif IDF: Flash your project (UART)).
- Open the ESP-IDF Monitor (Espressif IDF: Monitor your device).
- You should see different random numbers printed each time the program runs and within the loop.
Example 2: Filling a Buffer with Random Bytes
This example demonstrates using esp_fill_random()
to populate a buffer with random data, suitable for generating keys or nonces.
1. Code (main/main.c
):
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_log.h"
#include "esp_heap_caps.h" // For heap_caps_print_heap_info
static const char *TAG = "rng_example_2";
#define RANDOM_BUFFER_SIZE 16 // Example: For a 128-bit AES key or IV
void app_main(void)
{
ESP_LOGI(TAG, "Generating %d random bytes...", RANDOM_BUFFER_SIZE);
uint8_t random_buffer[RANDOM_BUFFER_SIZE];
// Fill the buffer with random data
esp_fill_random(random_buffer, RANDOM_BUFFER_SIZE);
ESP_LOGI(TAG, "Generated random bytes:");
// Print the buffer contents in hex
for (int i = 0; i < RANDOM_BUFFER_SIZE; i++) {
printf("%02X ", random_buffer[i]);
}
printf("\n");
// Example: Generate another set of random bytes
ESP_LOGI(TAG, "Generating another %d random bytes...", RANDOM_BUFFER_SIZE);
esp_fill_random(random_buffer, RANDOM_BUFFER_SIZE);
ESP_LOGI(TAG, "New generated random bytes:");
for (int i = 0; i < RANDOM_BUFFER_SIZE; i++) {
printf("%02X ", random_buffer[i]);
}
printf("\n");
// You can use these random bytes for cryptographic purposes,
// for example, as an Initialization Vector (IV) for AES encryption.
}
2. Build, Flash, and Observe:
Follow the same build/flash steps. You will see two different sets of 16 random bytes printed in hexadecimal format. Each run of the program will produce different byte sequences.
Example 3: Generating a Random Number within a Range
While esp_random()
gives a uint32_t
, often you need a random number within a specific range (e.g., for array indexing or dice rolls). A common, though slightly biased for large ranges, method is using the modulo operator. For cryptographic nonces or where uniform distribution is critical over a specific range, more advanced techniques might be needed to avoid modulo bias, but for general purposes:
1. Code (main/main.c
):
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_log.h"
static const char *TAG = "rng_example_3";
// Function to get a random number within a specific range [min, max]
uint32_t get_random_in_range(uint32_t min_val, uint32_t max_val) {
if (min_val > max_val) {
// Swap if min > max
uint32_t temp = min_val;
min_val = max_val;
max_val = temp;
}
uint32_t range = max_val - min_val + 1;
if (range == 0) { // Avoid division by zero if min_val and max_val are at extremes of uint32_t
return esp_random(); // Or handle error appropriately
}
return min_val + (esp_random() % range);
}
void app_main(void)
{
ESP_LOGI(TAG, "Generating random numbers in specific ranges...");
uint32_t r_0_99 = get_random_in_range(0, 99);
ESP_LOGI(TAG, "Random number between 0 and 99: %u", r_0_99);
uint32_t r_1_6 = get_random_in_range(1, 6); // Simulate a dice roll
ESP_LOGI(TAG, "Random number between 1 and 6 (dice roll): %u", r_1_6);
uint32_t r_1000_1005 = get_random_in_range(1000, 1005);
ESP_LOGI(TAG, "Random number between 1000 and 1005: %u", r_1000_1005);
ESP_LOGI(TAG, "Generating 10 dice rolls (1-6):");
for (int i = 0; i < 10; i++) {
printf("%u ", get_random_in_range(1, 6));
}
printf("\n");
}
2. Build, Flash, and Observe:
Build and flash. Observe the random numbers generated within the specified ranges.
Note on Modulo Bias: Using
esp_random() % range
can introduce a slight bias ifrange
does not evenly divide the maximum value ofesp_random()
(2^32). For most non-cryptographic applications where the range is much smaller than 2^32, this bias is often negligible. For cryptographically secure selection within a range, more sophisticated debiasing techniques are required (e.g., repeatedly drawing numbers until one falls into a multiple of the desired range). ESP-IDF’s mbedTLS component, for instance, has functions for generating cryptographically secure random numbers within ranges that handle this.
Variant Notes
The hardware Random Number Generator is a standard peripheral across the ESP32 family, but there can be subtle differences or considerations:
- ESP32 (Original): Contains a hardware RNG that gathers entropy from various sources, including Wi-Fi/Bluetooth RF noise and an independent noise source.
- ESP32-S2: Also includes a hardware RNG. Its entropy sources are similar, designed to provide true randomness.
- ESP32-S3: Features a hardware RNG. Given its dual-core nature and advanced peripherals, entropy sources are robust.
- ESP32-C3 (RISC-V): Equipped with a hardware RNG. As a Wi-Fi and Bluetooth LE chip, RF noise contributes to entropy.
- ESP32-C6 (RISC-V): Includes a hardware RNG, leveraging its Wi-Fi 6 and 802.15.4 radio systems as potent entropy sources.
- ESP32-H2 (RISC-V): Contains a hardware RNG. Its 802.15.4 radio system is a primary entropy source.
General Consistency:
The esp_random() and esp_fill_random() APIs are designed to provide a consistent interface to the hardware RNG across all supported ESP32 variants. The underlying hardware implementation details (specific noise sources, conditioning logic) might differ slightly between chip generations, but all aim to provide true random numbers suitable for cryptographic use.
Bootloader Behavior: The bootloader’s RNG initialization process (bootloader_random_enable()
) is standard across variants, ensuring the RNG is operational before the application starts.
Power Consumption: The RNG hardware consumes a small amount of power when active. If an application enters a very deep sleep state where the RNG is not needed for an extended period, and if bootloader_random_disable()
were used (which is rare), the RNG would need to be re-enabled upon wakeup. However, for typical light sleep or active modes, the RNG remains available. The esp_timer
system, which might also rely on some level of randomness or precise timing, is usually kept active.
Common Mistakes & Troubleshooting Tips
Mistake / Issue | Symptom(s) | Troubleshooting / Solution |
---|---|---|
Using rand() for keys |
Your device security is compromised. An attacker can predict your “random” keys. | Solution: NEVER use rand() for security. Always use esp_fill_random() to leverage the hardware TRNG. |
Expecting repeatable sequences | Tests that rely on a specific random sequence fail because the numbers are different each time. | Solution: The hardware RNG is *supposed* to be unpredictable. For testing, create a software PRNG and seed it once with a true random number from esp_random() . |
Noticeable modulo bias | In a sensitive statistical application, some numbers in a generated range appear slightly more often than others. | Solution: For non-crypto tasks, this is often fine. For crypto, use a library function that performs debiasing or implement a rejection sampling method. |
Getting the same number repeatedly | Calling esp_random() multiple times in a row gives the same value. (This is extremely rare). |
Solution: This indicates a potential hardware fault or an issue in the bootloader’s entropy collection. Check ESP-IDF errata or report the issue. The hardware includes health tests to prevent this. |
Exercises
- Unique Session ID Generator:
- Write a function
void generate_session_id(char *id_buffer, size_t buffer_len)
that generates a cryptographically random session ID. - The ID should be a string of hexadecimal characters. For example, if
buffer_len
is 33 (for 16 random bytes + null terminator), the output might look like “A1B2C3D4E5F6071892A3B4C5D6E7F809”. - Use
esp_fill_random()
to get 16 random bytes. - Convert these bytes into a null-terminated hexadecimal string and store it in
id_buffer
. - In
app_main
, call this function and print the generated session ID.
- Write a function
- Random Delay Task:
- Create a FreeRTOS task that, in a loop, generates a random delay between 500ms and 2500ms using
esp_random()
andget_random_in_range()
from Example 3. - After the random delay, the task should print “Task woke up after a random delay of X ms!”, where X is the actual delay generated.
- Run this for a few iterations to observe the varying delay periods.
- Create a FreeRTOS task that, in a loop, generates a random delay between 500ms and 2500ms using
- Comparing TRNG with
rand()
:- Objective: Visually (not statistically rigorously) observe the difference in output sequences.
- Task:
- In
app_main
, seed the standardrand()
PRNG usingsrand(12345);
(a fixed seed). - Print the first 10 numbers generated by
rand() % 100
. - Reset the device and run the program again. Observe that the sequence from
rand()
is identical. - Now, print the first 10 numbers generated by
esp_random() % 100
. - Reset the device and run the program again. Observe that the sequence from
esp_random()
is different each time. - Discuss why this difference is critical for security.
- In
Summary
- ESP32 microcontrollers feature a hardware True Random Number Generator (TRNG) that sources entropy from physical noise.
- TRNGs produce non-deterministic, unpredictable random numbers, essential for security, unlike deterministic Pseudo-Random Number Generators (PRNGs) like
stdlib.h
‘srand()
. - ESP-IDF provides
esp_fill_random(void *buf, size_t len)
(recommended) andesp_random(void)
to access the hardware TRNG. - The bootloader initializes the RNG, making it ready for application use.
- For any security-sensitive data (keys, nonces, IVs), always use the hardware RNG via the
esp_
APIs. - Generating random numbers within a specific range using the modulo operator (
%
) can introduce slight bias; for cryptographic use, ensure unbiased methods are employed if necessary. - The hardware RNG is consistently available across ESP32 variants, abstracted by the ESP-IDF API.
Further Reading
- ESP-IDF API Reference – System API (includes
esp_random
,esp_fill_random
):- https://docs.espressif.com/projects/esp-idf/en/v5.4/esp32/api-reference/system/system.html#random-number-generation (Select your target chip for specific details, though the API is general).
- ESP-IDF Bootloader Random Number Generation:
- NIST Special Publication 800-90B: Recommendation for the Entropy Sources Used for Random Bit Generation:
- While highly technical, this provides insight into the standards and testing for TRNGs.
- https://csrc.nist.gov/publications/detail/sp/800-90b/final