Random Number Generation & Entropy
Introduction
Cryptographic security begins with randomness. Every secret key, every nonce, every session identifier starts as random bytes. If an attacker can predict your random numbers, they can predict your keys. If they can predict your keys, they own your accounts. The stakes could not be higher.
This chapter explores how rippled generates cryptographically secure random numbers, where entropy comes from, and why weak randomness has led to some of the most catastrophic security failures in blockchain history.
The CSPRNG: Cryptographically Secure Pseudo-Random Number Generator
What Makes Randomness "Cryptographically Secure"?
Not all random number generators are created equal:
// ❌ NOT CRYPTOGRAPHICALLY SECURE
std::srand(time(NULL));
int random = std::rand(); // Predictable, sequential, weak
// ❌ BETTER BUT STILL NOT SECURE
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> dis(0, 255);
int random = dis(gen); // Better distribution, still not secure for crypto
// ✅ CRYPTOGRAPHICALLY SECURE
auto& prng = crypto_prng();
uint8_t random_bytes[32];
prng(random_bytes, sizeof(random_bytes)); // Unpredictable, secure
Requirements for cryptographic randomness:
Unpredictability: Even knowing all previous outputs, future outputs cannot be predicted
Uniform distribution: All values are equally likely
Independence: Each output is independent of all others
Entropy: Derived from sources an attacker cannot observe or control
Rippled's CSPRNG Implementation
// From src/libxrpl/crypto/csprng.cpp
class csprng_engine
{
private:
std::mutex mutex_; // Thread safety for older OpenSSL
public:
using result_type = std::uint64_t;
csprng_engine()
{
// Initialize entropy pool from system sources
if (RAND_poll() != 1)
Throw<std::runtime_error>("CSPRNG: Initial polling failed");
}
void operator()(void* ptr, std::size_t count)
{
// Thread-safety for older OpenSSL versions
#if (OPENSSL_VERSION_NUMBER < 0x10100000L) || !defined(OPENSSL_THREADS)
std::lock_guard lock(mutex_);
#endif
// Get cryptographically secure random bytes
auto const result = RAND_bytes(
reinterpret_cast<unsigned char*>(ptr),
count);
if (result != 1)
Throw<std::runtime_error>("CSPRNG: Insufficient entropy");
}
result_type operator()()
{
result_type ret;
(*this)(&ret, sizeof(result_type));
return ret;
}
void mix_entropy(void* buffer, std::size_t count)
{
// Add entropy from std::random_device
std::array<std::random_device::result_type, 128> entropy;
std::random_device rd;
for (auto& e : entropy)
e = rd();
std::lock_guard lock(mutex_);
// Add to OpenSSL's entropy pool
RAND_add(
entropy.data(),
entropy.size() * sizeof(std::random_device::result_type),
0); // Conservatively assume no actual entropy contribution
if (buffer != nullptr && count != 0)
RAND_add(buffer, count, 0);
}
};
// Global singleton - initialized once per process
csprng_engine& crypto_prng()
{
static csprng_engine engine;
return engine;
}
Where Does Entropy Come From?
Entropy is the measure of unpredictability. For cryptography, we need entropy from sources that an attacker cannot observe or control.
System Entropy Sources
When RAND_poll()
is called during csprng_engine
initialization, OpenSSL collects entropy from multiple system sources:
┌──────────────────────────────────────────────┐
│ OpenSSL Entropy Collection │
└──────────────────────────────────────────────┘
│
┌────────────┼────────────┐
│ │ │
┌────▼───┐ ┌────▼────┐ ┌────▼────┐
│Hardware│ │Operating│ │ Timing │
│ RNG │ │ System │ │ Sources │
└────┬───┘ └────┬────┘ └────┬────┘
│ │ │
└───────────┴────────────┘
│
┌─────▼──────┐
│ Entropy │
│ Pool │
└────────────┘
1. Hardware Random Number Generators
Modern CPUs include dedicated hardware for generating random numbers:
Intel/AMD (x86_64):
; RDRAND instruction - hardware RNG
rdrand rax ; Random 64-bit value in RAX register
; RDSEED instruction - direct entropy source
rdseed rax ; Random seed value
These instructions access physical processes:
Thermal noise: Random fluctuations in transistor behavior due to heat
Quantum effects: Subatomic randomness at the hardware level
Avalanche noise: Random current fluctuations in reverse-biased PN junctions
ARM processors:
; ARM TrustZone random number generation
; Hardware entropy source
2. Operating System Entropy
Different operating systems maintain entropy pools:
Linux/Unix:
// /dev/urandom - Non-blocking, cryptographically secure
int fd = open("/dev/urandom", O_RDONLY);
read(fd, buffer, size);
// /dev/random - Blocks if entropy is low (usually not needed)
The kernel's entropy pool is fed by:
Disk I/O timings (unpredictable due to physical mechanics and caching)
Network packet arrival times (dependent on network conditions)
Keyboard and mouse input timings (human unpredictability)
Interrupt timing (hardware event randomness)
Windows:
// CryptGenRandom - Windows CSPRNG
CryptGenRandom(hProvider, dwLen, pbBuffer);
Windows gathers entropy from:
System performance counters
Process and thread IDs
High-resolution timestamps
Hardware random number generators
3. Timing-Based Entropy
Even without dedicated hardware, timing provides entropy:
// High-resolution clock measurements
auto t1 = std::chrono::high_resolution_clock::now();
// ... do some work ...
auto t2 = std::chrono::high_resolution_clock::now();
auto duration = t2 - t1; // Unpredictable due to system load, caching, etc.
Sources of timing unpredictability:
CPU frequency scaling
Cache behavior (hits vs. misses)
Branch prediction success/failure
Memory access patterns
Context switches
Interrupt handling
The Entropy Pool
OpenSSL maintains an internal entropy pool that:
Collects entropy from multiple sources
Mixes it using cryptographic hash functions (SHA-256)
Extracts random bytes on demand
Reseeds automatically when needed
[Entropy Sources]
│
├─> Hardware RNG ────┐
├─> OS Entropy ────┤
├─> Timing Data ────┤
└─> Process State────┤
│
┌────▼─────┐
│ Mixing │
│(SHA-256) │
└────┬─────┘
│
┌───────▼────────┐
│ Entropy Pool │
│ (256+ bits) │
└───────┬────────┘
│
┌─────▼──────┐
│ RAND_bytes │
└────────────┘
Defense in Depth: mix_entropy()
mix_entropy()
Rippled doesn't just rely on OpenSSL's entropy—it adds additional entropy from std::random_device
:
void mix_entropy(void* buffer, std::size_t count)
{
// Create array to hold additional entropy
std::array<std::random_device::result_type, 128> entropy;
std::random_device rd;
// Fill array with random_device output
for (auto& e : entropy)
e = rd();
std::lock_guard lock(mutex_);
// Add to OpenSSL's pool
// Last parameter = 0 means "don't increase entropy estimate"
// (conservative: we don't assume how much entropy random_device provides)
RAND_add(
entropy.data(),
entropy.size() * sizeof(std::random_device::result_type),
0);
// Also mix in provided buffer if any
if (buffer != nullptr && count != 0)
RAND_add(buffer, count, 0);
}
Why this matters:
Belt and suspenders: Even if OpenSSL's entropy is weak,
std::random_device
provides backupPlatform independence: Different platforms implement
std::random_device
differentlyDefense against implementation bugs: Bugs in one source don't compromise the entire system
Thread Safety
#if (OPENSSL_VERSION_NUMBER < 0x10100000L) || !defined(OPENSSL_THREADS)
std::lock_guard lock(mutex_);
#endif
Why thread safety matters:
Rippled is multi-threaded
Multiple threads may request random bytes simultaneously
Older OpenSSL versions aren't thread-safe for
RAND_bytes
Modern OpenSSL (1.1.0+) handles thread safety internally
The code defensively uses a mutex for older versions while allowing modern OpenSSL to use its own (more efficient) thread safety mechanisms.
Error Handling: Fail Loudly
if (result != 1)
Throw<std::runtime_error>("CSPRNG: Insufficient entropy");
Why throw on error:
If random number generation fails, the only safe response is to stop immediately. Continuing with predictable or weak random numbers would be catastrophic:
// ❌ WRONG - Dangerous fallback
uint8_t get_random_byte() {
uint8_t result;
if (RAND_bytes(&result, 1) != 1) {
// DON'T DO THIS!
return std::rand() % 256; // Weak fallback
}
return result;
}
// ✅ CORRECT - Fail loudly
uint8_t get_random_byte() {
uint8_t result;
if (RAND_bytes(&result, 1) != 1) {
Throw<std::runtime_error>("RNG failure");
}
return result;
}
Better to crash than to generate weak keys.
Using the CSPRNG
Generating a Random Secret Key
SecretKey randomSecretKey()
{
std::uint8_t buf[32];
beast::rngfill(buf, sizeof(buf), crypto_prng());
SecretKey sk(Slice{buf, sizeof(buf)});
secure_erase(buf, sizeof(buf));
return sk;
}
Generating Random Transaction IDs (for testing)
uint256 randomUInt256()
{
uint256 result;
crypto_prng()(result.data(), result.size());
return result;
}
Generating Session Tokens
uint64_t generateSessionToken()
{
return crypto_prng()(); // Returns random uint64_t
}
Historical Failures: Lessons from Weak Randomness
Case Study 1: Weak RNG in Android (2013)
Problem:
Android's Java
SecureRandom
had a bugUsed insufficient entropy
Generated predictable private keys
Consequence:
Multiple wallets generated identical or predictable keys
Attackers stole funds by guessing private keys
Lesson: Never trust that "SecureRandom" is actually secure without verification.
Case Study 2: Blockchain.info RNG Bug (2014)
Problem:
Browser-based RNG used weak entropy
Some generated keys were predictable
Consequence:
Attackers could brute-force private keys
Multiple wallets compromised
Lesson: Browser RNGs are insufficient for key generation. Always use system CSPRNG.
Case Study 3: Reused Nonces (PlayStation 3)
Problem:
PlayStation 3 signing algorithm reused the same nonce value
ECDSA signatures with known nonce expose the private key
Consequence:
PS3 signing key was extracted
Anyone could sign "official" PS3 software
Lesson: Nonces must be random and never reused. (XRPL uses deterministic nonces via RFC 6979 to avoid this entirely.)
Testing Randomness Quality
How can you verify randomness is actually random?
Statistical Tests
// Test: Check distribution over many samples
void test_distribution() {
std::map<uint8_t, int> frequency;
// Generate 256,000 bytes
for (int i = 0; i < 256000; ++i) {
uint8_t byte;
crypto_prng()(&byte, 1);
frequency[byte]++;
}
// Each byte value should appear ~1000 times
// Statistical deviation should be small
for (int i = 0; i < 256; ++i) {
int count = frequency[i];
// Allow ±10% deviation (±100 occurrences)
assert(count > 900 && count < 1100);
}
}
Independence Test
// Test: Sequential bytes should be independent
void test_independence() {
std::map<std::pair<uint8_t, uint8_t>, int> pairs;
uint8_t prev = 0;
crypto_prng()(&prev, 1);
for (int i = 0; i < 65536; ++i) {
uint8_t curr;
crypto_prng()(&curr, 1);
pairs[{prev, curr}]++;
prev = curr;
}
// Each pair should appear ~1 time (65536 pairs, 65536 iterations)
// No pattern should emerge
}
Professional Test Suites
NIST Statistical Test Suite: Industry standard for RNG testing
Diehard tests: Comprehensive statistical analysis
TestU01: Modern, extensive RNG test suite
Best Practices for Random Number Generation
✅ DO:
Always use
crypto_prng()
for cryptographic operationsauto& prng = crypto_prng(); uint8_t key_material[32]; prng(key_material, sizeof(key_material));
Check return values
if (RAND_bytes(buffer, size) != 1) { Throw<std::runtime_error>("RNG failure"); }
Use enough random bytes
// ✅ 32 bytes = 256 bits of security uint8_t key[32]; // ❌ 8 bytes = only 64 bits (too weak!) uint8_t weak_key[8];
Securely erase random material
uint8_t temp[32]; prng(temp, sizeof(temp)); // ... use temp ... secure_erase(temp, sizeof(temp));
❌ DON'T:
Don't use standard library RNGs for crypto
std::mt19937 gen; // ❌ NOT CRYPTOGRAPHICALLY SECURE std::rand(); // ❌ EXTREMELY WEAK
Don't seed with predictable values
srand(time(NULL)); // ❌ Predictable seed srand(getpid()); // ❌ Process ID is guessable
Don't implement your own CSPRNG
// ❌ DON'T DO THIS uint8_t my_rand() { static uint64_t state = time(NULL); state = state * 6364136223846793005ULL + 1; return state >> 56; }
Don't ignore entropy exhaustion
// ❌ Don't loop indefinitely while (RAND_bytes(buf, size) != 1) { // If this fails, STOP, don't retry forever }
Summary
Random number generation is the foundation of cryptographic security:
True randomness is essential: Predictable randomness = predictable keys = stolen funds
Multiple entropy sources provide defense: Hardware RNG, OS entropy, timing, etc.
OpenSSL's RAND_bytes is cryptographically secure: When properly seeded
Rippled adds additional entropy: Via
std::random_device
for defense in depthFail loudly on errors: Better to crash than generate weak keys
Never use standard RNGs for crypto:
std::rand()
,std::mt19937
are not secureThread safety matters: Older OpenSSL versions need mutex protection
In the next chapter, we'll see how this cryptographically secure randomness is used to generate the secret keys that secure accounts on the XRP Ledger.
Last updated