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:

  1. Unpredictability: Even knowing all previous outputs, future outputs cannot be predicted

  2. Uniform distribution: All values are equally likely

  3. Independence: Each output is independent of all others

  4. 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:

  1. Collects entropy from multiple sources

  2. Mixes it using cryptographic hash functions (SHA-256)

  3. Extracts random bytes on demand

  4. 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()

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 backup

  • Platform independence: Different platforms implement std::random_device differently

  • Defense 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 bug

  • Used 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:

  1. Always use crypto_prng() for cryptographic operations

    auto& prng = crypto_prng();
    uint8_t key_material[32];
    prng(key_material, sizeof(key_material));
  2. Check return values

    if (RAND_bytes(buffer, size) != 1) {
        Throw<std::runtime_error>("RNG failure");
    }
  3. 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];
  4. Securely erase random material

    uint8_t temp[32];
    prng(temp, sizeof(temp));
    // ... use temp ...
    secure_erase(temp, sizeof(temp));

❌ DON'T:

  1. Don't use standard library RNGs for crypto

    std::mt19937 gen;  // ❌ NOT CRYPTOGRAPHICALLY SECURE
    std::rand();       // ❌ EXTREMELY WEAK
  2. Don't seed with predictable values

    srand(time(NULL));  // ❌ Predictable seed
    srand(getpid());    // ❌ Process ID is guessable
  3. 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;
    }
  4. 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:

  1. True randomness is essential: Predictable randomness = predictable keys = stolen funds

  2. Multiple entropy sources provide defense: Hardware RNG, OS entropy, timing, etc.

  3. OpenSSL's RAND_bytes is cryptographically secure: When properly seeded

  4. Rippled adds additional entropy: Via std::random_device for defense in depth

  5. Fail loudly on errors: Better to crash than generate weak keys

  6. Never use standard RNGs for crypto: std::rand(), std::mt19937 are not secure

  7. Thread 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