하드웨어 난수 생성기 (hwrng / QRNG)
hwrng 및 QRNG 경로를 커널 엔트로피 품질과 암호학적 안전성 관점에서 심층 분석합니다. hwrng 프레임워크와 드라이버 등록(Driver Registration) 모델, 하드웨어 난수 소스 신뢰도 평가, 엔트로피 풀 혼합과 credit 정책, 커널 CSPRNG(ChaCha20) 초기화·재시드 동작, 부팅 초기 entropy starvation 대응, 가상화(Virtualization)/임베디드 환경의 난수 공급 이슈, rngd와 userspace 연계, 품질 검증과 장애 진단 절차까지 안전한 난수 인프라 구축에 필요한 핵심을 다룹니다.
- 커널 모듈(Kernel Module) 개발 —
struct hwrng등록/해제 패턴 이해 필요 - Linux Crypto Framework — 암호학적 기초 (CSPRNG, ChaCha20)
- 메모리 관리(Memory Management) — 엔트로피 풀의 메모리 레이아웃 이해
컴퓨터는 본질적으로 결정론적 기계라 "진정한 우연"을 만들지 못합니다. 하드웨어 난수 생성기(hwrng)는 열잡음, 방사선 감쇠, 양자 효과 등 물리 세계의 예측 불가한 현상을 측정해 진짜 우연한 비트를 공급합니다. 커널은 이 원시 엔트로피를 ChaCha20 암호화(Encryption) 알고리즘으로 정제해 빠르고 안전한 난수 스트림(/dev/urandom)을 만듭니다.
핵심 요약
- TRNG vs CSPRNG: 하드웨어 TRNG는 물리적 엔트로피 원천, CSPRNG는 그것을 씨앗 삼아 대량 난수 생성
- /dev/random vs /dev/urandom: 현대 커널(5.18+)에서 두 인터페이스는 동일하게 동작 (CSPRNG 기반)
- getrandom() 시스템 콜(System Call): 커널 초기화 완료 전에는 블록, 이후 절대 블록하지 않음
- hwrng 드라이버:
struct hwrng하나만 구현하면 커널 엔트로피 풀에 자동 기여 - quality 파라미터: 하드웨어 품질을 0~1024로 표현 (1024 = 비트당 1비트 엔트로피)
- QRNG 물리 원리 3종: 빔 분리기(편광), 진공 요동(ΔEΔt≥ℏ/2), 광자 도착 시간(TOAD)은 모두 양자 역학적 비결정성 활용
- NIST SP 800-90B: min-entropy 기반 통계 검증으로 hwrng quality 파라미터를 인증, FIPS 140-3 요구사항과 연동
- PCIe vs USB QRNG: PCIe는 MSI 인터럽트(Interrupt)+MMIO 직접 읽기로 더 높은 처리율 달성, USB는 Bulk 전송으로 단순 구현
단계별 이해
- 엔트로피 풀 개념 파악 → 엔트로피 풀 메커니즘
- 커널 CSPRNG 내부 구조 이해 → 커널 CSPRNG 내부
- hwrng API 학습 → hwrng 드라이버 개발
- QRNG 물리 원리 → QRNG 물리적 원리
- 실제 QRNG 드라이버 분석 → QRNG 드라이버 구현
- NIST 800-90B 인증 이해 → NIST SP 800-90B 인증
- 암호 키 생성 플로우 → 암호 키 생성 완전 플로우
- 테스트/검증 방법 숙지 → 테스트 및 검증
hwrng 서브시스템 개요
Linux 커널의 hwrng(Hardware Random Number Generator) 서브시스템은
다양한 하드웨어 난수 생성기를 통일된 인터페이스로 추상화합니다.
Intel RDRAND, ARM TRNG, TPM 2.0, USB QRNG 등 모든 하드웨어 RNG는
동일한 struct hwrng API를 통해 커널 엔트로피 풀에 기여합니다.
난수 생성기 분류
| 유형 | 전체 명칭 | 특징 | 예시 |
|---|---|---|---|
| TRNG | True Random Number Generator | 물리적 현상 기반, 느리지만 진정한 무작위성 | 열잡음, 방사선, 광자 분리 |
| QRNG | Quantum Random Number Generator | 양자 역학 기반, 원리적으로 예측 불가 | ID Quantique, QuintessenceLabs |
| PRNG | Pseudo Random Number Generator | 결정론적, 빠름, 씨앗(seed)에 의존 | rand(), Mersenne Twister |
| CSPRNG | Cryptographically Secure PRNG | 역추적(Backtrace) 불가, 예측 저항성, 암호화 안전 | Linux ChaCha20-CRNG, /dev/urandom |
| DRBG | Deterministic Random Bit Generator | NIST SP 800-90A 표준, 재현 가능 | CTR-DRBG(AES), Hash-DRBG |
TRNG — 하드웨어 기반 진난수 생성기
TRNG(True Random Number Generator)는 열역학적·전기적 물리 현상의 본질적 불확실성을 이용하여 결정론적 알고리즘 없이 난수를 생성합니다. TRNG의 출력은 어떤 수학적 모델로도 완전히 예측할 수 없으며, 이는 소프트웨어 PRNG와 근본적으로 다른 점입니다. Linux 커널에서 hwrng 서브시스템으로 관리되는 대부분의 장치가 TRNG에 해당합니다.
열잡음(Thermal Noise, Johnson-Nyquist Noise):
도체 내 자유 전자의 열적 요동(Brownian Motion)에 의해 발생하는 전압 변동으로,
전력 스펙트럼 밀도가 S(f) = 4kTR(k: 볼츠만 상수, T: 절대온도, R: 저항)로 주어집니다.
이 잡음은 주파수에 무관한 백색 잡음(White Noise)이며,
고전적 열역학 법칙에 의해 예측 불가능합니다.
아날로그 TRNG 칩(예: Intel 82802 FWH, Broadcom BCM2835)은 이 열잡음을 증폭하고
비교기(Comparator)로 디지털 비트를 생성합니다.
/* drivers/char/hw_random/bcm2835-rng.c — Raspberry Pi 열잡음 기반 TRNG */
static int bcm2835_rng_read(struct hwrng *rng, void *buf,
size_t max, bool wait)
{
struct bcm2835_rng_priv *priv = to_rng_priv(rng);
u32 max_words = max / sizeof(u32);
u32 num_words, count;
/* RNG 상태 레지스터에서 사용 가능한 워드 수 확인 */
while ((rng_readl(priv, RNG_STATUS) >> 24) == 0) {
if (!wait)
return 0;
cpu_relax(); /* 열잡음 축적 대기 */
}
num_words = rng_readl(priv, RNG_STATUS) >> 24;
if (num_words > max_words)
num_words = max_words;
for (count = 0; count < num_words; count++)
((u32 *)buf)[count] = rng_readl(priv, RNG_DATA);
return num_words * sizeof(u32);
}
클록 지터(Clock Jitter): 오실레이터의 주기적 신호에서 발생하는 시간 변동(Phase Noise)을 이용합니다. 두 개의 독립 링 오실레이터(Ring Oscillator)를 동시에 구동하고, 한쪽의 상승 에지(Rising Edge)에서 다른 쪽의 상태를 샘플링하면 수 ps~ns 단위의 지터가 비결정적 비트로 변환됩니다. Intel의 RDRAND/RDSEED 명령어 내부에 이 방식의 TRNG가 탑재되어 있습니다.
/* arch/x86/include/asm/archrandom.h — Intel RDRAND 접근 */
static inline bool __must_check rdrand_long(unsigned long *v)
{
bool ok;
unsigned int retry = RDRAND_RETRY_LOOPS; /* 10회 재시도 */
do {
asm volatile(RDRAND_LONG
"\n\tsetc %1"
: "=a"(*v), "=qm"(ok));
if (ok)
return true;
} while (--retry);
return false;
}
/*
* RDRAND vs RDSEED 차이:
* - RDRAND: TRNG → AES-CBC-MAC 컨디셔닝 → DRBG → 출력 (후처리된 결정론적 출력)
* - RDSEED: TRNG → AES-CBC-MAC 컨디셔닝 → 직접 출력 (풀 엔트로피 비결정론적)
* Linux 커널은 RDSEED를 우선 사용하고, 실패 시 RDRAND로 폴백합니다.
*/
준안정 플립플롭(Metastable Flip-Flop): 래치(Latch) 회로의 셋업 타임(Setup Time) 위반 시 발생하는 준안정 상태(Metastable State)를 이용합니다. 비동기 클록으로 D 플립플롭의 입력을 토글(Toggle)하면 출력이 "0"과 "1" 사이에서 진동하다가 최종 안정 상태로 수렴하는데, 어느 쪽으로 수렴할지는 트랜지스터(Transistor) 레벨의 열잡음에 의해 결정됩니다. FPGA 기반 TRNG에서 흔히 사용되며, Xilinx/Intel FPGA의 LUT를 링 오실레이터로 구성하여 구현합니다.
jitterentropy — 소프트웨어 TRNG:
하드웨어 TRNG가 없는 환경(가상 머신, 임베디드)에서 CPU 타이밍 지터만으로 엔트로피를 생성하는
소프트웨어 TRNG입니다. Linux 커널의 crypto/jitterentropy.c에 구현되어 있으며,
메모리 접근 시간, CPU 명령어 실행 시간의 나노초 단위 변동을 엔트로피로 수집합니다.
/* crypto/jitterentropy.c — CPU 타이밍 지터 기반 엔트로피 수집 */
static u64 jent_loop_shuffle(struct rand_data *ec,
unsigned int bits, unsigned int min)
{
u64 time = 0;
u64 shuffle = 0;
/* 메모리 접근 루프 — 캐시 미스에 의한 지터 수집 */
jent_memaccess(ec, min);
/* 고해상도 타임스탬프 읽기 (TSC/CNTVCT) */
jent_get_nstime(&time);
/* 타이밍 차이(delta)에서 엔트로피 추출 */
if ((centered & 1) == 0)
shuffle = time; /* 홀수/짝수 비트 — 타이밍 비결정성 */
return shuffle;
}
/*
* jitterentropy가 커널에 제공하는 인터페이스:
* - crypto_alloc_rng("jitterentropy_rng") → crypto RNG API 통합
* - CRNG 초기 씨드로 자동 사용 (CONFIG_RANDOM_TRUST_CPU 비활성 시)
* - FIPS 140-3 환경에서 독립 엔트로피 소스로 인정
*/
input_pool에 혼합한 후
ChaCha20-CRNG를 통해 최종 난수를 생성합니다.
PRNG — 의사 난수 생성기
PRNG(Pseudo Random Number Generator)는 결정론적 알고리즘으로 난수처럼 보이는 수열을 생성합니다. 동일한 씨드(Seed)를 입력하면 항상 동일한 출력 수열이 나오므로 재현 가능합니다. 이 특성은 시뮬레이션, 게임, 테스트에서는 장점이지만 보안 용도에는 절대 사용해서는 안 됩니다 — 씨드를 알면 전체 출력을 예측할 수 있기 때문입니다.
선형 합동 생성기(LCG, Linear Congruential Generator):
가장 단순한 PRNG로, Xn+1 = (a·Xn + c) mod m 공식을 사용합니다.
C 표준 라이브러리의 rand(), glibc의 random()이 LCG 변형을 사용합니다.
주기(Period)가 최대 m으로 제한되며, 하위 비트의 품질이 특히 나쁩니다.
하위 k비트의 주기는 2k에 불과하여 rand() % 2는 0과 1이 교대로 나올 수 있습니다.
/* glibc — rand() 구현 (TYPE_0 모드, 단순 LCG) */
/* 파라미터: a = 1103515245, c = 12345, m = 2^31 */
static int32_t __random_r(struct random_data *buf, int32_t *result)
{
int32_t val = buf->state[0];
val = ((int32_t)(val * 1103515245) + 12345) & 0x7fffffff;
buf->state[0] = val;
*result = val;
return 0;
}
/* 문제점: 씨드를 알면 전체 수열 예측 가능
* 2^31 = ~21억 주기 — 현대 GPU로 수 초 만에 전수 탐색
* rand() 출력 연속 3개만 관찰하면 내부 상태 완전 복원 가능 (Knuth 알고리즘) */
메르센 트위스터(Mersenne Twister, MT19937):
623차원에서 균등분포를 보장하는 PRNG로, 주기가 219937-1(메르센 소수)입니다.
624개의 32비트 워드(2,496바이트)를 내부 상태로 사용하며,
SIMD 최적화(SFMT)를 통해 매우 빠른 난수 생성이 가능합니다.
Python의 random 모듈, Ruby, R, MATLAB 등 대부분의 언어 기본 PRNG가 MT19937입니다.
/* Mersenne Twister 핵심 알고리즘 (개념 코드) */
#define N 624 /* 상태 벡터 크기 */
#define M 397 /* 재귀 오프셋 */
static u32 mt[N]; /* 내부 상태 (2,496바이트) */
static int mti;
/* 상태 갱신 (twist) — 624워드 소진 시 일괄 갱신 */
static void generate_numbers(void)
{
u32 y;
for (int i = 0; i < N; i++) {
y = (mt[i] & 0x80000000) | (mt[(i+1) % N] & 0x7fffffff);
mt[i] = mt[(i + M) % N] ^ (y >> 1);
if (y & 1)
mt[i] ^= 0x9908b0df; /* 매직 상수 */
}
}
/* 출력 템퍼링 (tempering) — 균등분포 개선 */
static u32 extract_number(void)
{
u32 y = mt[mti++];
y ^= (y >> 11); /* 우시프트 11 */
y ^= (y << 7) & 0x9d2c5680; /* 좌시프트 7 + AND */
y ^= (y << 15) & 0xefc60000; /* 좌시프트 15 + AND */
y ^= (y >> 18); /* 우시프트 18 */
return y;
}
/*
* 보안 취약점: 연속 출력 624개를 관찰하면 템퍼링을 역산하여
* 내부 상태 624워드를 100% 복원 가능 → 이후 모든 출력 예측
* MT19937은 암호학적으로 완전히 안전하지 않습니다.
*/
xoshiro/xorshift 계열:
Blackman-Vigna가 설계한 최신 PRNG 계열로, 메르센 트위스터보다 빠르고 상태 크기가 작습니다.
xoshiro256**는 256비트(32바이트) 상태로 2256-1 주기를 제공하며,
BigCrush 통계 테스트를 모두 통과합니다.
V8(Chrome JavaScript 엔진)의 Math.random()이 xorshift128+를 사용합니다.
/* xoshiro256** — 현대적 고속 PRNG */
static u64 s[4]; /* 256비트 상태 */
static inline u64 rotl(u64 x, int k) { return (x << k) | (x >> (64 - k)); }
static u64 xoshiro256ss(void)
{
u64 result = rotl(s[1] * 5, 7) * 9; /* starstar 출력 함수 */
u64 t = s[1] << 17;
s[2] ^= s[0];
s[3] ^= s[1];
s[1] ^= s[2];
s[0] ^= s[3];
s[2] ^= t;
s[3] = rotl(s[3], 45);
return result;
}
/* 성능 비교 (cycles per byte, x86-64):
* LCG rand(): ~4 cpb
* MT19937: ~2.5 cpb
* xoshiro256**: ~0.8 cpb (SIMD 미사용)
* ChaCha20-CRNG: ~2.0 cpb (CSPRNG, 보안 보장)
*/
Linux 커널에서 PRNG의 변천:
과거 Linux 커널은 prandom_u32()라는 비보안 PRNG를 네트워킹 해시, 타이머 지터 등에 사용했습니다.
이 함수는 Tausworthe 생성기(LCG 변형)를 사용하여 빠르지만 예측 가능했습니다.
Linux 5.19에서 prandom_u32()는 get_random_u32()의 래퍼로 교체되었으며,
현재 커널에서는 비보안 PRNG를 사용할 수 있는 경로가 거의 없습니다.
이 변경은 커널 내부의 ASLR, 네트워크 시퀀스 번호 등이 PRNG를 통해 예측 공격에 노출되던 문제를 근본적으로 해결했습니다.
rand(), random(), drand48(), Math.random(),
Mersenne Twister, xoshiro 계열은 모두 보안 용도에 부적합합니다.
암호 키, 세션 토큰, 논스(Nonce), ASLR 오프셋 등에는 반드시
getrandom(), /dev/urandom, get_random_bytes()(CSPRNG)를 사용해야 합니다.
CSPRNG — 암호학적 안전 의사 난수 생성기
CSPRNG(Cryptographically Secure PRNG)는 PRNG에 두 가지 보안 속성을 추가한 것입니다. 첫째, 다음 비트 예측 불가(Next-Bit Unpredictability) — 출력의 처음 k비트를 알아도 (k+1)번째 비트를 다항식 시간(Polynomial Time) 내에 50% 이상의 확률로 예측할 수 없습니다. 둘째, 역추적 저항(Backtracking Resistance) — 내부 상태가 노출되더라도 과거에 생성한 출력을 복원할 수 없습니다. 이 두 속성은 IND-CPA(선택 평문 공격 하 구별 불가능성) 안전성을 보장하며, 현대 암호 시스템의 기반이 됩니다.
Linux ChaCha20-CRNG: Linux 커널의 핵심 CSPRNG입니다. Daniel Bernstein이 설계한 ChaCha20 스트림 암호를 per-CPU 키 상태와 결합하여 높은 성능과 보안을 동시에 달성합니다. 32바이트 키와 12바이트 논스로 초기화된 ChaCha20 블록 함수가 64바이트 출력 중 앞 32바이트를 즉시 새 키로 교체(Fast Key Erasure)하여 메모리 노출 시에도 이전 출력을 역추적할 수 없게 합니다.
/* drivers/char/random.c — ChaCha20 CRNG Fast Key Erasure (핵심 메커니즘) */
static void crng_fast_key_erasure(u8 key[CHACHA_KEY_SIZE],
u32 chacha_state[CHACHA_STATE_WORDS],
u8 *random_data, size_t random_data_len)
{
u8 first_block[CHACHA_BLOCK_SIZE]; /* 64바이트 */
/* ChaCha20 1블록 생성 */
chacha20_block(chacha_state, first_block);
/* 앞 32바이트 = 새 키로 교체 (Forward Secrecy 보장) */
memcpy(key, first_block, CHACHA_KEY_SIZE);
/* 나머지 32바이트 = 난수 출력으로 사용 */
memcpy(random_data, first_block + CHACHA_KEY_SIZE,
min_t(size_t, random_data_len, CHACHA_KEY_SIZE));
/* 중간 데이터 즉시 소거 */
memzero_explicit(first_block, sizeof(first_block));
}
/*
* per-CPU 구조로 동작:
* CPU 0: [key_0][nonce_0] → ChaCha20 → 출력 + 새 key_0
* CPU 1: [key_1][nonce_1] → ChaCha20 → 출력 + 새 key_1
* ...
* → 락(Lock) 경합 없이 병렬 난수 생성, NUMA 친화적
* → 5분마다 또는 256번 사용마다 input_pool에서 재씨딩(reseed)
*/
Fortuna (FreeBSD/macOS):
Niels Ferguson과 Bruce Schneier가 설계한 CSPRNG으로,
32개의 독립 엔트로피 풀을 사용하여 부분 상태 노출에 대한 복원력을 제공합니다.
풀 k는 2k번째 재씨드마다 참여하므로 공격자가 일부 풀을 제어하더라도
장기적으로 엔트로피가 축적됩니다. FreeBSD의 /dev/random, macOS의 SecRandomCopyBytes()가
Fortuna 기반입니다.
다른 운영체제의 CSPRNG:
| 운영체제 | CSPRNG | 암호 기반 | 재씨드 정책 | 인터페이스 |
|---|---|---|---|---|
| Linux 5.18+ | ChaCha20-CRNG | ChaCha20 (20라운드) | 5분 / 256회 사용 | getrandom(), /dev/urandom |
| FreeBSD | Fortuna | AES-256-CTR | 32풀 라운드 로빈 | arc4random() |
| macOS/iOS | Fortuna 변형 | AES-256-CTR | 이벤트 기반 | SecRandomCopyBytes() |
| Windows | BCryptGenRandom | AES-256-CTR-DRBG | 프로세스 단위 | BCryptGenRandom() |
| OpenBSD | arc4random | ChaCha20 | 1.6MB마다 | arc4random() |
DRBG — 결정론적 난수 비트 생성기 (NIST 표준)
DRBG(Deterministic Random Bit Generator)는 NIST SP 800-90A 표준에서 정의한 CSPRNG의 공식 명칭입니다. "결정론적"이라는 이름이 붙었지만 이는 내부 알고리즘이 결정론적이라는 의미이며, 외부에서 공급받는 엔트로피 씨드에 의해 출력의 예측 불가능성이 보장됩니다. DRBG는 FIPS 140-2/3 인증 환경에서 필수적으로 요구되며, 미국 정부 시스템, 금융 기관, 의료 기기 등에서 사용됩니다.
NIST SP 800-90A의 3가지 DRBG 메커니즘:
| 메커니즘 | 내부 암호 | 상태 크기 | 보안 강도 | 특징 |
|---|---|---|---|---|
| CTR-DRBG | AES-128/192/256 | AES 블록 + 키 (32~48B) | 128/192/256비트 | AES-NI 활용, FIPS 환경 기본 |
| Hash-DRBG | SHA-256/384/512 | 해시 출력 + V + C (110B) | 128~256비트 | AES 불필요, 임베디드 적합 |
| HMAC-DRBG | HMAC-SHA-256/512 | Key + V (64~128B) | 128~256비트 | 가장 단순한 구현, 결정론적 서명 |
CTR-DRBG 동작 원리:
AES-CTR 모드를 기반으로 동작합니다. 내부 상태는 AES 키(Key)와 카운터(V)로 구성되며,
씨딩(Seed) 시 Key ⊕ entropy, V ⊕ entropy로 상태를 갱신합니다.
난수 생성 시 V를 1씩 증가시키며 AES-ECB로 암호화한 결과를 출력합니다.
10만 번 요청(Request)마다 또는 248비트 생성마다 재씨딩이 필수입니다.
/* Linux 커널의 DRBG 사용 — crypto API를 통한 CTR-DRBG */
#include <crypto/drbg.h>
#include <crypto/rng.h>
static int fips_generate_key(u8 *key, size_t keylen)
{
struct crypto_rng *rng;
int ret;
/* FIPS 모드에서는 DRBG를 명시적으로 사용
* "drbg_nopr_ctr_aes256" = CTR-DRBG, AES-256, 예측 저항 없음(No PR)
* "drbg_pr_ctr_aes256" = CTR-DRBG, AES-256, 예측 저항 활성(PR)
*/
rng = crypto_alloc_rng("drbg_nopr_ctr_aes256", 0, 0);
if (IS_ERR(rng))
return PTR_ERR(rng);
/* DRBG 씨딩은 커널이 자동 수행 (input_pool에서 추출) */
/* 난수 생성 — 최대 요청 크기: 2^16 바이트 */
ret = crypto_rng_get_bytes(rng, key, keylen);
crypto_free_rng(rng);
return ret;
}
/*
* Linux 커널 DRBG 구현 위치: crypto/drbg.c
*
* 사용 가능한 DRBG 알고리즘 목록:
* drbg_nopr_ctr_aes128 — CTR-DRBG, AES-128, No Prediction Resistance
* drbg_nopr_ctr_aes256 — CTR-DRBG, AES-256, No Prediction Resistance
* drbg_pr_ctr_aes128 — CTR-DRBG, AES-128, Prediction Resistance
* drbg_pr_ctr_aes256 — CTR-DRBG, AES-256, Prediction Resistance
* drbg_nopr_hmac_sha256 — HMAC-DRBG, SHA-256
* drbg_nopr_hmac_sha512 — HMAC-DRBG, SHA-512
* drbg_nopr_sha256 — Hash-DRBG, SHA-256
* drbg_nopr_sha512 — Hash-DRBG, SHA-512
*
* 참고: FIPS 140-3 인증에서는 CTR-DRBG(AES-256)가 권장됩니다.
*/
HMAC-DRBG의 특수 용도 — 결정론적 서명:
RFC 6979는 HMAC-DRBG를 사용하여 ECDSA/DSA 서명의 k값(논스)을 결정론적으로 생성합니다.
이 방식은 서명할 때마다 동일한 메시지에 대해 동일한 k값이 나오므로
PRNG 실패로 인한 개인 키 노출 위험을 제거합니다(Sony PS3 ECDSA 사고가 이 취약점의 대표 사례입니다).
Linux 커널의 crypto/ecdsa.c에서 이 방식을 활용합니다.
예측 저항(Prediction Resistance) 모드:
DRBG의 PR 모드(drbg_pr_*)는 매 요청마다 엔트로피 소스에서 새 씨드를 받아 재씨딩합니다.
이는 내부 상태가 노출되더라도 다음 출력부터 즉시 안전성이 회복되므로
가장 높은 보안 수준을 제공하지만, 성능 비용이 큽니다.
FIPS 140-3 Level 4(물리적 보안) 환경에서 요구됩니다.
getrandom(), get_random_bytes())가
최적의 선택입니다. DRBG(crypto API)는 FIPS 140-2/3 인증이 필요한 환경이나
NIST 표준 준수가 요구되는 정부/금융 시스템에서 사용합니다.
성능 면에서 ChaCha20-CRNG는 per-CPU 병렬 처리로 DRBG보다 수배 빠르며,
Fast Key Erasure로 DRBG의 예측 저항과 동등한 전방 비밀성을 제공합니다.
QRNG 방식별 비교
| 방식 | 물리 현상 | 처리율 | 대표 제품 | 양자 원리 |
|---|---|---|---|---|
| 빔 분리기 | 광자 편광 불확정성 | 1~10 Mbps | ID Quantique Quantis | 하이젠베르크 불확정성 원리 |
| 진공 요동 | 진공 상태 전자기장 ΔE | 100+ Mbps | QuintessenceLabs qStream | ΔEΔt ≥ ℏ/2 (에너지-시간 불확정성) |
| 광자 도착 시간 (TOAD) | 광자 방출 시각 양자 요동 | 10~100 Mbps | Comscope, Toshiba QKD | 파동-입자 이중성 |
| 방사성 붕괴 | 핵 붕괴 타이밍 | 낮음 (kbps) | HotBits, Geiger counter | 양자 터널(Tunnel)링 |
| 위상 잡음 | 레이저 위상 자연 선 폭 | 1~10 Gbps | 연구 단계 (대규모) | 자연 선 폭 불확정성 |
커널 내 hwrng 위치 — 아키텍처 다이어그램
QRNG 물리적 원리
QRNG(Quantum Random Number Generator)는 양자 역학의 고유한 비결정성(inherent randomness)을 활용하여 원리적으로 예측 불가능한 진난수(True Random Number)를 생성합니다. 고전적 TRNG(열잡음, 지터)는 환경 조건에 따라 편향이 발생할 수 있지만, QRNG는 양자역학 법칙 자체가 비결정성을 보장하므로 어떤 물리적 모델로도 예측할 수 없습니다. 현재 상용화된 QRNG 방식은 크게 다섯 가지로 분류되며, 각각 서로 다른 양자 현상을 엔트로피 원천으로 활용합니다.
빔 분리기(Beam Splitter) 방식
빔 분리기 방식은 가장 직관적인 QRNG 원리입니다. 단일 광자를 50:50 빔 스플리터(Half Mirror)에 입사시키면, 양자역학의 중첩(Superposition) 원리에 의해 광자는 투과 경로와 반사 경로 중 하나를 완전히 비결정적으로 선택합니다. 이때 두 경로에 각각 배치된 단일 광자 검출기(SPD, Single Photon Detector)가 어느 쪽에서 검출되었는지에 따라 "0" 또는 "1" 비트를 생성합니다.
이 과정의 비결정성은 하이젠베르크 불확정성 원리(Heisenberg Uncertainty Principle)에서 직접 비롯됩니다.
광자의 편광 상태를 비직교 기저(Non-orthogonal Basis)로 측정하면 결과는 본질적으로 확률적이며,
어떤 숨은 변수(Hidden Variable)로도 예측할 수 없음이 벨 부등식(Bell's Inequality) 위반 실험으로 증명되었습니다.
대표 제품인 ID Quantique Quantis는 이 방식으로 4~16 Mbps의 진난수를 생성하며,
USB/PCIe 폼팩터로 Linux hwrng 서브시스템에 직접 등록됩니다.
진공 요동(Vacuum Fluctuation) 방식
양자전기역학(QED)에 따르면 완전한 진공 상태에서도 전자기장의 에너지는 영점 에너지(Zero-Point Energy)라는 최소값을 가지며, 이 영점 에너지 주위로 양자 요동(Quantum Fluctuation)이 끊임없이 발생합니다. 진공 요동 QRNG는 이 양자 요동을 호모다인 검출(Homodyne Detection) 기법으로 측정합니다.
로컬 오실레이터(Local Oscillator) 레이저와 진공 상태의 신호를 50:50 빔 스플리터에서 간섭시키면, 출력 포트에서 측정되는 광전류의 차이(차동 전류)가 바로 진공 요동을 반영합니다. 이 차동 신호를 고속 ADC(Analog-to-Digital Converter)로 디지털화하면 가우시안 분포를 따르는 원시 난수가 됩니다. 에너지-시간 불확정성 관계(ΔE·Δt ≥ ℏ/2)가 이 요동의 하한을 보장합니다.
진공 요동 방식의 핵심 장점은 높은 처리량입니다. 광대역 호모다인 검출기는 GHz 대역폭을 가질 수 있어 100 Mbps 이상의 난수 생성이 가능합니다. QuintessenceLabs의 qStream은 이 방식으로 1 Gbps 이상의 출력을 달성하며, 서버 랙 마운트 형태로 데이터센터 환경에 적합합니다.
광자 도착 시간(TOAD) 방식
약한 코히어런트 광원(Weak Coherent Source)에서 방출되는 광자의 수는 포아송 분포(Poisson Distribution)를 따릅니다. 평균 광자 수 μ가 1보다 훨씬 작은 감쇠 레이저를 사용하면, 대부분의 시간 슬롯에서 0개의 광자가 방출되고 드물게 1개의 광자가 방출됩니다. 이때 연속된 광자 검출 이벤트 사이의 시간 간격(Δt)은 양자역학적으로 비결정적이며, 이 시간 정보를 디지털화하여 난수 비트를 생성합니다.
TOAD(Time-Of-Arrival Detector) 방식은 파동-입자 이중성(Wave-Particle Duality)을 엔트로피 원천으로 활용합니다. 광자의 방출 시점은 자발 방출(Spontaneous Emission)에 의해 결정되므로 원리적으로 예측 불가능합니다. 한국의 EYL(이와이엘)은 이 방식의 QRNG를 CMOS 반도체 칩으로 집적하여 스마트폰 내장이 가능한 초소형 폼팩터(1mm² 이하)를 구현하였습니다. Samsung Galaxy Quantum 시리즈에 탑재된 QRNG 칩이 이 방식입니다.
레이저 위상 잡음(Laser Phase Noise) 방식
반도체 레이저의 발진 과정에서 자발 방출(Spontaneous Emission)은 유도 방출(Stimulated Emission)과 무관하게 레이저 위상에 무작위 섭동(Random Perturbation)을 발생시킵니다. 이 위상 확산(Phase Diffusion) 현상의 스펙트럼 폭은 Schawlow-Townes 선폭 공식으로 기술되며, 양자역학적 원천을 가집니다.
레이저 위상 잡음 QRNG는 지연 간섭계(Delay-Line Interferometer)를 통해 위상 잡음을 진폭 변동으로 변환합니다. 두 팔 사이의 위상 차이가 비결정적이므로 간섭계 출력 광강도의 변동도 비결정적이며, 이를 고속 포토디텍터와 ADC로 디지털화합니다. 이 방식의 최대 장점은 극한의 처리량입니다. 광통신용 변조기(Modulator)와 동일한 대역폭을 활용할 수 있어 이론적으로 10 Gbps 이상의 난수 생성이 가능합니다. Toshiba와 Quside가 이 방식의 상용화를 선도하고 있습니다.
방사성 붕괴(Radioactive Decay) 방식
방사성 붕괴는 가장 오래된 양자 난수 원천으로, 불안정한 원자핵이 알파 입자나 베타 입자를 방출하는 시점이 양자 터널링(Quantum Tunneling)에 의해 결정됩니다. 가이거-뮐러(Geiger-Müller) 계수관이 개별 붕괴 이벤트를 검출하며, 연속된 이벤트 사이의 시간 간격을 비트로 변환합니다.
이 방식은 양자역학의 비결정성이 가장 명확하게 드러나는 원리이지만, 붕괴율이 물리적으로 제한되어 처리량이 kbps 수준으로 매우 낮습니다. 또한 방사성 물질의 취급에 규제가 따르므로 상용 제품보다는 연구용으로 주로 활용됩니다. 대표적으로 Hotbits 프로젝트는 세슘-137 감마선 소스를 사용하여 인터넷을 통해 양자 난수를 제공합니다.
QRNG 방식별 종합 비교
| 방식 | 양자 현상 | 불확정성 관계 | 처리량 | min-entropy | 성숙도 | 대표 제품 |
|---|---|---|---|---|---|---|
| 빔 분리기 | 광자 경로 중첩 | 하이젠베르크(편광) | 1~16 Mbps | 0.99+ | 상용 (높음) | ID Quantique Quantis |
| 진공 요동 | 영점 에너지 요동 | 에너지-시간 | 100 Mbps~1 Gbps | 0.95+ | 상용 (높음) | QuintessenceLabs qStream |
| TOAD | 광자 도착 시점 | 파동-입자 이중성 | 10~100 Mbps | 0.98+ | 상용 (높음) | EYL QRNG 칩, SKT |
| 레이저 위상 잡음 | 자발 방출 위상 확산 | Schawlow-Townes | 1~10+ Gbps | 0.90+ | 상용화 초기 | Toshiba, Quside |
| 방사성 붕괴 | 양자 터널링 | 핵 불안정성 | ~kbps | 0.99+ | 연구용 | Hotbits |
struct hwrng 인터페이스를 통해 등록됩니다.
quality 파라미터에 장치의 min-entropy 수준을 반영하면
커널이 엔트로피 크레딧을 적절히 계산하여 input_pool에 기여합니다.
엔트로피 풀 메커니즘
커널 엔트로피 풀은 drivers/char/random.c에 구현된 핵심 난수 인프라입니다.
Linux 5.18에서 Jason Donenfeld(WireGuard 저자)의 대규모 재작성을 통해
기존의 LFSR(Linear Feedback Shift Register) 기반 SHA-1 믹싱 구조가
단일 input_pool + Blake2s 해시 기반 256비트 상태로 전면 교체되었습니다.
이 변경으로 코드량이 절반 이하로 줄었고, 암호학적 안전성이 크게 향상되었습니다.
현재 구조에서는 모든 엔트로피 소스(hwrng, 인터럽트 지터, 디스크 I/O 등)가
단일 풀에 혼합된 후 ChaCha20-CRNG로 추출되는 단순하고 검증 가능한 파이프라인을 형성합니다.
/dev/random vs /dev/urandom — 현대 커널의 통합
/dev/random과 /dev/urandom은 이제 동일한 CSPRNG 백엔드를 사용합니다.
/dev/random은 더 이상 엔트로피 고갈로 블록하지 않습니다.
블록킹 동작은 초기화 완료 전에만 발생합니다.
| 인터페이스 | 블록킹 조건 | 권장 용도 | 커널 구현 |
|---|---|---|---|
/dev/random |
CRNG 초기화 전 | 레거시 호환성 | random_read() |
/dev/urandom |
절대 블록 안 함 | 일반 용도 | urandom_read() |
getrandom() |
CRNG 초기화 전 (GRND_NONBLOCK 아닐 때) | 권장 — 파일 디스크립터(File Descriptor) 불필요 | sys_getrandom() |
get_random_bytes() |
없음 (커널 내부용) | 커널 드라이버 | lib/random.c |
엔트로피 소스와 수집 경로
/* drivers/char/random.c — 엔트로피 수집 함수들 */
/* 1. 인터럽트 지터 — 가장 풍부한 소프트웨어 엔트로피 소스 */
void add_interrupt_randomness(int irq)
{
struct fast_pool *fast_pool = this_cpu_ptr(&irq_randomness);
struct pt_regs *regs = get_irq_regs();
unsigned long now = jiffies;
cycles_t cycles = random_get_entropy(); /* TSC or 아키텍처별 타이머 */
fast_mix(fast_pool->pool, irq, now ^ cycles,
regs ? instruction_pointer(regs) : _RET_IP_);
/* 64회 인터럽트마다 input_pool에 기여 */
if (++fast_pool->count < 64 && !time_after(now, fast_pool->last + HZ))
return;
add_interrupt_bench(cycles);
mix_interrupt_randomness(fast_pool);
}
/* 2. hwrng 기여 — hw_random 코어가 자동으로 호출 */
void add_hwgenerator_randomness(const void *buf, size_t len, size_t entropy)
{
_mix_pool_bytes(buf, len); /* input_pool에 혼합 */
credit_init_bits(entropy); /* 엔트로피 추정값 증가 */
wake_up_interruptible(&random_wait); /* 대기 중인 getrandom() 깨움 */
}
/* 3. 시스템 이벤트 기여 */
void add_device_randomness(const void *buf, size_t len) /* MAC 주소, 시리얼 번호 등 */
void add_input_randomness(unsigned int type, unsigned int code, unsigned int val) /* 키보드/마우스 */
void add_disk_randomness(struct gendisk *disk) /* 디스크 I/O 완료 타이밍 */
코드 설명
-
cycles_t cycles
random_get_entropy()는 x86에서 TSC(Time Stamp Counter), ARM에서 CNTVCT를 읽습니다. 인터럽트 도착 시각의 나노초 단위 지터가 실제 엔트로피가 됩니다. - fast_pool per-CPU 고속 풀입니다. 모든 인터럽트마다 input_pool에 직접 쓰면 락 경합(Contention)이 심해지므로, 64회마다 한 번씩 배치 처리합니다.
-
credit_init_bits(entropy)
엔트로피 추정값을 비트 단위로 누적합니다. CRNG 초기화에는 256비트 이상이 필요합니다. hwrng의
quality값이 이 계산에 사용됩니다.
getrandom() 시스템 콜 흐름
/* 사용자 공간에서 사용 */
#include <sys/random.h>
unsigned char key[32];
ssize_t ret = getrandom(key, sizeof(key), 0); /* 0 = 블록킹 모드 */
if (ret < 0) {
perror("getrandom"); /* EINTR, EFAULT 가능 */
return -1;
}
/* GRND_NONBLOCK: CRNG 초기화 전에도 블록하지 않음 (EAGAIN 반환) */
ret = getrandom(key, sizeof(key), GRND_NONBLOCK);
/* GRND_RANDOM: /dev/random 동작 에뮬레이션 (레거시, 권장 안 함) */
ret = getrandom(key, sizeof(key), GRND_RANDOM);
/* 커널 내부: get_random_bytes() 계열 */
get_random_bytes(buf, nbytes); /* 범용 */
u32 r = get_random_u32(); /* 32비트 */
u64 r = get_random_u64(); /* 64비트 */
u32 r = get_random_u32_below(100); /* 0~99, 편향 없음 */
u32 r = get_random_u32_inclusive(1, 6); /* 주사위 1~6 */
엔트로피 예산 계산 — quality 파라미터의 역할
hwrng 코어는 hwrng_fillfn() 커널 스레드(Kernel Thread)를 통해 지속적으로 rng->read()를 호출하고,
읽은 바이트 수와 quality 값을 곱해 엔트로피 기여량을 계산합니다.
/* drivers/char/hw_random/core.c — hwrng_fillfn() 핵심 로직 */
static int hwrng_fillfn(void *unused)
{
long rc;
while (!kthread_should_stop()) {
struct hwrng *rng;
mutex_lock(&rng_mutex);
rng = get_current_rng_nolock();
mutex_unlock(&rng_mutex);
if (!rng) { msleep(1000); continue; }
/* read() 호출 — 최대 RNGD_BUFF_SIZE(4096)바이트 */
rc = rng->read(rng, rng_buffer, rng_buffer_size, true);
if (rc <= 0) { msleep(500); continue; }
/* 엔트로피 기여량 계산:
* entropy_bits = bytes_read × quality × 8 / 1024
* 예) 4096바이트 × quality=1024 × 8 / 1024 = 32768비트
* 예) 4096바이트 × quality=512 × 8 / 1024 = 16384비트
* 예) 4096바이트 × quality=0 × 8 / 1024 = 0비트 (기여 없음) */
add_hwgenerator_randomness(rng_buffer, rc,
(rc * rng->quality * 8) >> 10);
}
hwrng_put(rng);
return 0;
}
| quality 값 | 의미 | 4096바이트 읽기 시 기여 엔트로피 | CRNG 초기화(256비트) 위한 읽기 횟수 |
|---|---|---|---|
| 1024 | 이론적 최대 (검증 QRNG) | 32,768 비트 | 1회 미만 |
| 512 | 50% 효율 (Intel RDRAND) | 16,384 비트 | 1회 미만 |
| 256 | 25% 효율 (열잡음 TRNG) | 8,192 비트 | 1회 미만 |
| 128 | 12.5% 효율 (FPGA 지터) | 4,096 비트 | 1회 미만 |
| 0 | 엔트로피 기여 없음 | 0 비트 | ∞ (초기화 불가) |
커널 CSPRNG 내부
Linux 커널은 ChaCha20 기반 CRNG(Cryptographically secure Random Number Generator)를 사용합니다.
커밋 1e7f583903b(v5.17)부터 per-CPU ChaCha20 CRNG로 완전히 전환되었습니다.
ChaCha20가 AES-CTR 대신 선택된 이유는 세 가지입니다.
첫째, 순수 소프트웨어 구현으로도 상수 시간(constant-time) 동작이 보장되어
AES-NI 같은 하드웨어 가속 명령어가 없는 ARM 임베디드 환경에서도 타이밍 부채널(Side-Channel)에 안전합니다.
둘째, 20라운드라는 넉넉한 보안 마진(Security Margin)을 가지며,
현재까지 8라운드 ChaCha까지만 공격이 알려져 있습니다.
셋째, SIMD(SSE2, AVX2, NEON) 최적화가 용이하여 대량 난수 생성 시 AES와 동등하거나 더 빠른 성능을 보입니다.
ChaCha20-CRNG 상태 구조
/* include/linux/random.h, drivers/char/random.c */
/* per-CPU CRNG 상태 — 64바이트 (ChaCha20 키 크기) */
struct crng {
u8 key[CHACHA20_KEY_SIZE]; /* 32바이트 키 */
unsigned long generation; /* 리시드 세대 번호 */
local_lock_t lock; /* per-CPU 경량 락 */
};
/* 전역 초기화 완료 플래그 */
static bool crng_ready = false; /* 256비트 엔트로피 수집 후 true */
static u64 base_crng_generation; /* 전역 세대 카운터 */
/* CRNG 초기화 — 부팅 시 엔트로피 수집 */
static void crng_initialize_primary(void)
{
extract_entropy(base_crng.key, sizeof(base_crng.key));
crng_ready = true;
pr_notice("random: crng init done\n");
}
crng_reseed() — 주기적 갱신 메커니즘
/* 5분마다 또는 충분한 엔트로피 누적 시 호출 */
static void crng_reseed(struct crng *crng)
{
u8 key[CHACHA20_KEY_SIZE];
/* input_pool에서 32바이트 추출 (Blake2b 해시 압축) */
extract_entropy(key, sizeof(key));
/* 기존 키와 새 키를 XOR하여 forward secrecy 보장 */
memcpy(&crng->key, key, sizeof(key));
WRITE_ONCE(crng->generation, base_crng_generation);
memzero_explicit(key, sizeof(key)); /* 스택 잔류 키 소거 */
}
/* 난수 생성 — ChaCha20 스트림으로 64바이트씩 출력 */
static void crng_fast_key_erasure(u8 *key, struct chacha20_ctx *chacha_state,
u8 *random_data, size_t random_data_len)
{
/* ChaCha20 블록으로 난수 스트림 생성 */
chacha20_block(chacha_state, random_data);
/* Fast Key Erasure: 출력의 앞 32바이트를 새 키로 사용 */
/* → 이전 출력으로 키를 역추적 불가 (forward secrecy) */
memcpy(key, random_data, CHACHA20_KEY_SIZE);
memzero_explicit(random_data, CHACHA20_KEY_SIZE); /* 키 부분 소거 */
}
코드 설명
- extract_entropy() input_pool의 Blake2b 해시(Hash) 상태를 압축해 32바이트 키를 추출합니다. 이 과정은 풀 상태를 변경(absorb)하므로 같은 키가 두 번 출력되지 않습니다.
- Fast Key Erasure Daniel Bernstein이 설계한 기법입니다. ChaCha20 블록 출력의 첫 32바이트를 즉시 새 키로 교체합니다. 메모리가 탈취되어도 이전 출력을 재현할 수 없습니다.
-
memzero_explicit()
컴파일러 최적화(Compiler Optimization)로 소거 코드가 제거되지 않도록 강제합니다. 일반
memset()은 최적화 옵션에 따라 제거될 수 있습니다.
input_pool — Blake2b 압축 함수
/* input_pool: Blake2b 해시 상태 기반 엔트로피 수집 */
struct blake2s_state {
u32 h[8]; /* 256비트 해시 상태 */
u32 t[2]; /* 처리된 바이트 카운터 */
u32 f[2]; /* 최종화 플래그 */
u8 buf[BLAKE2S_BLOCK_SIZE]; /* 입력 버퍼 (64바이트) */
size_t buflen;
u8 outlen;
};
/* 엔트로피 혼합 — Blake2s 압축으로 되돌릴 수 없음 */
static void _mix_pool_bytes(const void *buf, size_t len)
{
blake2s_update(&input_pool, buf, len); /* 단방향 해시 업데이트 */
}
/* 엔트로피 추출 — 풀 상태를 변경하며 키 생성 */
static void extract_entropy(void *buf, size_t nbytes)
{
struct blake2s_state recovery = input_pool; /* 풀 스냅샷 */
blake2s_final(&recovery, buf); /* 최종 해시 출력 */
blake2s_update(&input_pool, buf, nbytes); /* 출력을 풀에 재혼합 */
}
hwrng 드라이버 개발
hwrng 서브시스템의 드라이버 API는 단순합니다.
struct hwrng 구조체(Struct)를 초기화하고 hwrng_register()를 호출하면 됩니다.
등록이 완료되면 프레임워크가 /dev/hwrng 캐릭터 디바이스 생성,
/sys/class/misc/hw_random/ sysfs 엔트리 관리,
그리고 hwrng_fillfn() 커널 스레드를 통한 주기적 엔트로피 풀 공급을
모두 자동으로 처리합니다.
드라이버 개발자는 하드웨어에서 난수 바이트를 읽어오는 read() 콜백만 구현하면 되며,
엔트로피 풀 주입, 크레딧 계산, 사용자 공간 인터페이스는 코어 프레임워크가 담당합니다.
struct hwrng — 핵심 API
/* include/linux/hw_random.h */
struct hwrng {
const char *name; /* 드라이버 이름 (/sys/class/misc/hw_random/rng_current) */
int (*init)( struct hwrng *rng); /* 선택사항: 초기화 */
void (*cleanup)(struct hwrng *rng); /* 선택사항: 정리 */
int (*data_present)(struct hwrng *rng, int wait); /* 데이터 준비 여부 */
int (*data_read)( struct hwrng *rng, u32 *data); /* 구형 API, 4바이트 */
int (*read)( struct hwrng *rng, void *data, /* 권장 API */
size_t max, bool wait);
unsigned long priv; /* 드라이버 사용 개인 데이터 (구형) */
unsigned short quality; /* 엔트로피 품질: 0(최저)~1024(최고) */
/* 1024 = 비트당 1.0비트 엔트로피 (물리적 최대) */
/* 내부 사용 — 드라이버가 초기화 금지 */
struct list_head list;
struct kref ref;
struct completion cleanup_done;
struct completion dying;
};
quality 파라미터 가이드
| quality 값 | 엔트로피 품질 | 해당 하드웨어 |
|---|---|---|
| 1024 | 1.0 비트/비트 (이론적 최대) | 검증된 QRNG, 방사선 기반 |
| 512~1023 | 0.5~1.0 비트/비트 | Intel RDRAND, ARM TRNG, TPM RNG |
| 256~511 | 0.25~0.5 비트/비트 | 열잡음 기반 아날로그 TRNG |
| 128~255 | 보통 품질 | FPGA 링 오실레이터 |
| 0~127 | 낮은 품질 | 지터 기반, 환경 의존적 |
| 0 | 엔트로피 기여 없음 | 테스트용 또는 PRNG |
최소 hwrng 드라이버 골격
#include <linux/module.h>
#include <linux/hw_random.h>
#include <linux/platform_device.h>
struct myrng_priv {
void __iomem *base; /* MMIO 기주소 */
struct clk *clk;
};
/* 필수: 난수 읽기 콜백 */
static int myrng_read(struct hwrng *rng, void *data, size_t max, bool wait)
{
struct myrng_priv *priv = (struct myrng_priv *)dev_get_drvdata(rng->dev);
size_t read = 0;
/* HW가 준비될 때까지 폴링 (실제는 인터럽트 권장) */
while (read + 4 <= max) {
/* 레지스터에서 FIFO 가득 찼는지 확인 */
if (!(readl(priv->base + MYRNG_STATUS_REG) & MYRNG_READY_BIT)) {
if (!wait)
break;
cpu_relax();
continue;
}
/* 32비트 난수 읽기 */
*(u32 *)((u8 *)data + read) = readl(priv->base + MYRNG_DATA_REG);
read += 4;
}
return read; /* 실제 읽은 바이트 수 반환 */
}
static int myrng_probe(struct platform_device *pdev)
{
struct myrng_priv *priv;
struct hwrng *rng;
struct resource *res;
int ret;
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
priv->base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(priv->base))
return PTR_ERR(priv->base);
rng = devm_kzalloc(&pdev->dev, sizeof(*rng), GFP_KERNEL);
if (!rng)
return -ENOMEM;
rng->name = "myrng";
rng->read = myrng_read;
rng->quality = 900; /* 하드웨어 스펙에 따라 조정 */
platform_set_drvdata(pdev, priv);
ret = devm_hwrng_register(&pdev->dev, rng); /* devm 버전: 해제 자동화 */
if (ret)
return dev_err_probe(&pdev->dev, ret, "hwrng_register 실패\n");
dev_info(&pdev->dev, "myrng 등록 완료 (quality=%u)\n", rng->quality);
return 0;
}
static const struct of_device_id myrng_of_match[] = {
{ .compatible = "myvendor,myrng" },
{}
};
MODULE_DEVICE_TABLE(of, myrng_of_match);
static struct platform_driver myrng_driver = {
.probe = myrng_probe,
.driver = {
.name = "myrng",
.of_match_table = myrng_of_match,
},
};
module_platform_driver(myrng_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Example hwrng driver");
코드 설명
- myrng_read 반환값 실제 읽은 바이트 수를 반환해야 합니다. 0 이상이면 성공, 음수면 오류입니다. hw_random 코어는 이 값으로 엔트로피 기여량을 계산합니다.
-
devm_hwrng_register()
디바이스 관리 버전으로, 드라이버 언로드 시 자동으로
hwrng_unregister()를 호출합니다. 수동hwrng_unregister()와 달리remove()콜백(Callback)에 별도 코드가 필요 없습니다. - quality = 900 88% 효율의 엔트로피 품질을 의미합니다. 하드웨어 데이터시트의 min-entropy 스펙(예: 0.875 비트/비트)을 ×1024로 환산합니다.
QRNG 드라이버 구현
QRNG(Quantum Random Number Generator)는 양자 역학적 현상(광자 방향, 진공 요동 등)을 이용해
원리적으로 예측 불가능한 비트를 생성합니다.
대부분의 상용 QRNG는 USB 또는 PCIe 인터페이스로 연결되어 hwrng 드라이버로 구현됩니다.
USB 방식은 벌크 전송(Bulk Transfer)으로 데이터를 수신하므로 구현이 단순하지만
지연 시간(Latency)이 ms 단위로 높고, 최대 처리량이 USB 대역폭(USB 2.0: ~40 MB/s)에 제한됩니다.
반면 PCIe 방식은 MMIO(Memory-Mapped I/O)를 통한 직접 레지스터 접근과
MSI(Message Signaled Interrupt)를 활용하여 μs 단위의 낮은 지연과
수 Gbps의 처리량을 달성할 수 있으나, 드라이버 복잡도가 상당히 높아집니다.
일반적으로 USB QRNG는 quality = 600~800, PCIe QRNG는 quality = 900~1024 범위를 설정합니다.
USB QRNG 드라이버 완전 예제
아래 코드는 USB HID 기반 QRNG 장치 (예: ID Quantique Quantis USB)의 드라이버 구조를 보여줍니다.
#include <linux/module.h>
#include <linux/usb.h>
#include <linux/hw_random.h>
#include <linux/slab.h>
#include <linux/completion.h>
#define QRNG_VENDOR_ID 0x1d50 /* 가상의 벤더 ID */
#define QRNG_PRODUCT_ID 0x6021
#define QRNG_BULK_IN_EP 0x81 /* Bulk IN 엔드포인트 */
#define QRNG_BUFFER_SIZE 4096 /* 한 번에 읽는 양자 난수 버퍼 크기 */
#define QRNG_QUALITY 1024 /* 양자 소스 — 이론적 최대 엔트로피 */
struct qrng_dev {
struct usb_device *udev;
struct usb_interface *interface;
struct hwrng hwrng;
/* 비동기 Bulk 전송용 */
struct urb *urb;
u8 *buf; /* DMA 가능 버퍼 */
size_t buf_len; /* 사용 가능한 바이트 수 */
size_t buf_off; /* 현재 읽기 오프셋 */
struct mutex lock;
struct completion urb_done;
bool disconnecting;
};
/* URB 완료 콜백 — 인터럽트 컨텍스트에서 호출됨 */
static void qrng_urb_complete(struct urb *urb)
{
struct qrng_dev *qdev = urb->context;
if (urb->status == 0) {
qdev->buf_len = urb->actual_length;
qdev->buf_off = 0;
} else if (urb->status == -ENOENT || urb->status == -ECONNRESET ||
urb->status == -ESHUTDOWN) {
qdev->buf_len = 0; /* 연결 해제 시 정상 종료 */
}
complete(&qdev->urb_done);
}
/* hwrng read 콜백 — 슬립 가능 컨텍스트 */
static int qrng_read(struct hwrng *rng, void *data, size_t max, bool wait)
{
struct qrng_dev *qdev = container_of(rng, struct qrng_dev, hwrng);
size_t available, to_copy;
int ret;
mutex_lock(&qdev->lock);
/* 버퍼에 데이터가 없으면 USB 전송 시작 */
if (qdev->buf_off >= qdev->buf_len) {
if (!wait) {
mutex_unlock(&qdev->lock);
return 0;
}
reinit_completion(&qdev->urb_done);
usb_fill_bulk_urb(qdev->urb, qdev->udev,
usb_rcvbulkpipe(qdev->udev, QRNG_BULK_IN_EP),
qdev->buf, QRNG_BUFFER_SIZE,
qrng_urb_complete, qdev);
qdev->urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
ret = usb_submit_urb(qdev->urb, GFP_KERNEL);
if (ret) {
mutex_unlock(&qdev->lock);
return ret;
}
mutex_unlock(&qdev->lock);
wait_for_completion(&qdev->urb_done); /* USB 전송 완료 대기 */
mutex_lock(&qdev->lock);
if (qdev->buf_len == 0) {
mutex_unlock(&qdev->lock);
return -EIO;
}
}
/* 버퍼에서 요청량만큼 복사 */
available = qdev->buf_len - qdev->buf_off;
to_copy = min(available, max);
memcpy(data, qdev->buf + qdev->buf_off, to_copy);
qdev->buf_off += to_copy;
mutex_unlock(&qdev->lock);
return to_copy;
}
static int qrng_probe(struct usb_interface *interface,
const struct usb_device_id *id)
{
struct qrng_dev *qdev;
int ret;
qdev = kzalloc(sizeof(*qdev), GFP_KERNEL);
if (!qdev)
return -ENOMEM;
qdev->udev = interface_to_usbdev(interface);
qdev->interface = interface;
mutex_init(&qdev->lock);
init_completion(&qdev->urb_done);
/* DMA 가능 버퍼 할당 */
qdev->buf = usb_alloc_coherent(qdev->udev, QRNG_BUFFER_SIZE,
GFP_KERNEL, &qdev->urb->transfer_dma);
if (!qdev->buf) {
ret = -ENOMEM;
goto err_free;
}
qdev->urb = usb_alloc_urb(0, GFP_KERNEL);
if (!qdev->urb) {
ret = -ENOMEM;
goto err_free_buf;
}
/* hwrng 등록 */
qdev->hwrng.name = "qrng-quantum";
qdev->hwrng.read = qrng_read;
qdev->hwrng.quality = QRNG_QUALITY; /* 1024 — 양자 소스 */
usb_set_intfdata(interface, qdev);
ret = hwrng_register(&qdev->hwrng);
if (ret) {
dev_err(&interface->dev, "hwrng 등록 실패: %d\n", ret);
goto err_free_urb;
}
dev_info(&interface->dev, "QRNG 연결됨 (quality=1024)\n");
return 0;
err_free_urb:
usb_free_urb(qdev->urb);
err_free_buf:
usb_free_coherent(qdev->udev, QRNG_BUFFER_SIZE, qdev->buf,
qdev->urb->transfer_dma);
err_free:
kfree(qdev);
return ret;
}
static void qrng_disconnect(struct usb_interface *interface)
{
struct qrng_dev *qdev = usb_get_intfdata(interface);
qdev->disconnecting = true;
hwrng_unregister(&qdev->hwrng); /* read() 완료 후 반환 보장 */
usb_kill_urb(qdev->urb); /* 진행 중인 URB 취소 */
usb_free_urb(qdev->urb);
usb_free_coherent(qdev->udev, QRNG_BUFFER_SIZE, qdev->buf,
qdev->urb->transfer_dma);
kfree(qdev);
dev_info(&interface->dev, "QRNG 분리됨\n");
}
static const struct usb_device_id qrng_id_table[] = {
{ USB_DEVICE(QRNG_VENDOR_ID, QRNG_PRODUCT_ID) },
{}
};
MODULE_DEVICE_TABLE(usb, qrng_id_table);
static struct usb_driver qrng_driver = {
.name = "qrng",
.probe = qrng_probe,
.disconnect = qrng_disconnect,
.id_table = qrng_id_table,
};
module_usb_driver(qrng_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("USB QRNG hwrng driver");
MODULE_AUTHOR("Example Author");
코드 설명
- QRNG_QUALITY = 1024 양자 소스는 원리적으로 예측 불가능하므로 최대 품질 1024를 사용합니다. 실제 하드웨어는 제조사 인증서(NIST SP 800-90B)를 확인한 후 설정하세요.
- usb_fill_bulk_urb() USB Bulk 전송 요청을 초기화합니다. QRNG는 대량 바이트를 전송하므로 Bulk 엔드포인트가 적합합니다. 인터럽트 엔드포인트는 소량(64B 이하)에만 적합합니다.
-
hwrng_unregister() 순서
hwrng_unregister()는 진행 중인read()콜백이 완료될 때까지 블록합니다. 그 후usb_kill_urb()를 호출해야 URB 사용 후 해제(Use-After-Free) 버그를 피할 수 있습니다. -
container_of()
struct hwrng가struct qrng_dev에 내장되어 있으므로,container_of()로 부모 구조체 포인터를 얻습니다. 별도 할당보다 메모리 효율적입니다.
알려진 상용 QRNG 드라이버 위치
| 벤더/제품 | 커널 소스 위치 | 인터페이스 | quality |
|---|---|---|---|
| BCM2835 (라즈베리파이) | drivers/char/hw_random/bcm2835-rng.c | MMIO | 200 |
| Intel RDRAND | arch/x86/kernel/cpu/rdrand.c | CPU 명령어 | 1024 |
| ARM TRNG (v8.5+) | drivers/char/hw_random/arm-smccc-trng.c | SMC 콜 | 1024 |
| TPM 2.0 RNG | drivers/char/tpm/tpm-chip.c | TPM 커맨드 | 질문에 따라 |
| VirtIO RNG (KVM 게스트) | drivers/char/hw_random/virtio-rng.c | virtqueue | 1024 |
| EXYNOS (삼성) | drivers/char/hw_random/exynos-trng.c | MMIO + 인터럽트 | 없음(기본) |
PCIe QRNG 드라이버
PCIe 기반 QRNG는 USB보다 높은 처리율과 낮은 지연(Latency)을 제공합니다. MMIO(Memory-Mapped I/O) BAR를 통해 직접 하드웨어 레지스터(Register)에 접근하고, MSI(Message Signaled Interrupt) 인터럽트로 데이터 준비를 통지받습니다.
#include <linux/module.h>
#include <linux/pci.h>
#include <linux/hw_random.h>
#include <linux/interrupt.h>
#include <linux/wait.h>
#define PCIE_QRNG_VENDOR 0x1234
#define PCIE_QRNG_DEVICE 0xABCD
#define QRNG_REG_STATUS 0x00 /* 상태 레지스터 */
#define QRNG_REG_DATA 0x04 /* 난수 데이터 레지스터 (32비트) */
#define QRNG_REG_FIFO_COUNT 0x08 /* FIFO 내 사용 가능 워드 수 */
#define QRNG_STATUS_READY BIT(0) /* FIFO에 데이터 있음 */
#define QRNG_QUALITY 1024
struct pcie_qrng_dev {
void __iomem *mmio_base; /* BAR0 MMIO 기주소 */
struct hwrng hwrng; /* hwrng 서브시스템 핸들 */
struct pci_dev *pdev;
wait_queue_head_t wait_q; /* 인터럽트 대기 큐 */
atomic_t data_ready; /* MSI 수신 플래그 */
};
/* MSI 인터럽트 핸들러 — 하드웨어가 FIFO에 데이터 채웠을 때 호출 */
static irqreturn_t pcie_qrng_irq(int irq, void *dev_id)
{
struct pcie_qrng_dev *qdev = dev_id;
u32 status = readl(qdev->mmio_base + QRNG_REG_STATUS);
if (!(status & QRNG_STATUS_READY))
return IRQ_NONE;
atomic_set(&qdev->data_ready, 1);
wake_up_interruptible(&qdev->wait_q); /* read()에서 대기 중인 스레드 깨움 */
return IRQ_HANDLED;
}
/* hwrng read 콜백 — MMIO BAR 직접 읽기 + MSI 인터럽트 대기 */
static int pcie_qrng_read(struct hwrng *rng, void *data, size_t max, bool wait)
{
struct pcie_qrng_dev *qdev = container_of(rng, struct pcie_qrng_dev, hwrng);
size_t bytes = 0;
u32 *out = data;
int ret;
while (bytes + sizeof(u32) <= max) {
/* FIFO에 데이터가 없으면 MSI 인터럽트 대기 */
if (!atomic_read(&qdev->data_ready)) {
if (!wait)
break;
ret = wait_event_interruptible(qdev->wait_q,
atomic_read(&qdev->data_ready));
if (ret)
return bytes ? (int)bytes : -EINTR;
}
/* FIFO에서 32비트 읽기 */
*out++ = readl(qdev->mmio_base + QRNG_REG_DATA);
bytes += sizeof(u32);
if (readl(qdev->mmio_base + QRNG_REG_FIFO_COUNT) == 0)
atomic_set(&qdev->data_ready, 0);
}
return (int)bytes;
}
static int pcie_qrng_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
struct pcie_qrng_dev *qdev;
int ret;
qdev = devm_kzalloc(&pdev->dev, sizeof(*qdev), GFP_KERNEL);
if (!qdev)
return -ENOMEM;
init_waitqueue_head(&qdev->wait_q);
atomic_set(&qdev->data_ready, 0);
qdev->pdev = pdev;
/* PCIe 활성화 + DMA 마스크 설정 */
ret = pcim_enable_device(pdev);
if (ret)
return dev_err_probe(&pdev->dev, ret, "PCIe 활성화 실패\n");
ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
if (ret)
return dev_err_probe(&pdev->dev, ret, "DMA 마스크 설정 실패\n");
/* BAR0 MMIO 매핑 (devm: 언로드 시 자동 해제) */
qdev->mmio_base = pcim_iomap(pdev, 0, 0);
if (IS_ERR(qdev->mmio_base))
return PTR_ERR(qdev->mmio_base);
/* MSI 인터럽트 1개 할당 */
ret = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI);
if (ret < 0)
return dev_err_probe(&pdev->dev, ret, "MSI 할당 실패\n");
ret = devm_request_irq(&pdev->dev, pci_irq_vector(pdev, 0),
pcie_qrng_irq, 0, "pcie-qrng", qdev);
if (ret)
return dev_err_probe(&pdev->dev, ret, "IRQ 등록 실패\n");
/* hwrng 등록 */
qdev->hwrng.name = "pcie-qrng";
qdev->hwrng.read = pcie_qrng_read;
qdev->hwrng.quality = QRNG_QUALITY;
pci_set_drvdata(pdev, qdev);
ret = devm_hwrng_register(&pdev->dev, &qdev->hwrng);
if (ret)
return dev_err_probe(&pdev->dev, ret, "hwrng 등록 실패\n");
dev_info(&pdev->dev, "PCIe QRNG 등록 완료 (quality=%u)\n", QRNG_QUALITY);
return 0;
}
static const struct pci_device_id pcie_qrng_ids[] = {
{ PCI_DEVICE(PCIE_QRNG_VENDOR, PCIE_QRNG_DEVICE) },
{}
};
MODULE_DEVICE_TABLE(pci, pcie_qrng_ids);
static struct pci_driver pcie_qrng_driver = {
.name = "pcie-qrng",
.id_table = pcie_qrng_ids,
.probe = pcie_qrng_probe,
};
module_pci_driver(pcie_qrng_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("PCIe QRNG hwrng driver with MSI interrupt");
코드 설명
-
pcim_enable_device()
devm 기반 PCIe 초기화 함수입니다. 드라이버 언로드 시 자동으로
pci_disable_device()를 호출합니다.pci_enable_device()+pci_set_master()를 자동 포함합니다. - pcim_iomap(pdev, 0, 0) BAR0을 전체 크기(0)로 MMIO 매핑(Mapping)합니다. devm 기반이라 언로드 시 자동 해제됩니다. 두 번째 인수 0은 BAR 인덱스, 세 번째 인수 0은 전체 크기를 의미합니다.
- pci_alloc_irq_vectors(MSI) 레거시 INTx 대신 MSI를 요청합니다. MSI는 공유 인터럽트 선 없이 메시지 방식으로 동작해 지연이 낮고 공유 IRQ 문제가 없습니다.
- wait_event_interruptible() 신호(SIGINT 등)에 의해 인터럽트될 수 있는 대기입니다. hwrng 코어 스레드(Thread)가 이 콜백을 호출하므로 -EINTR 처리가 필요합니다.
NIST SP 800-90B 인증
NIST SP 800-90B는 하드웨어 엔트로피 소스(TRNG/QRNG)의 통계적 품질을 검증하는 미국 표준입니다.
커널 hwrng의 quality 파라미터는 이 표준의 min-entropy 측정값에서 직접 유도됩니다.
FIPS 140-3 인증을 위한 필수 요건이기도 합니다.
min-entropy와 quality 파라미터 연관성
/* NIST SP 800-90B — min-entropy H_min 계산 및 quality 변환 */
/* 1단계: 가장 자주 나타나는 심볼의 확률 p_max 측정 */
/* 테스트 데이터 1,000,000비트에서 각 바이트값 출현 빈도 집계 */
double p_max = max_count / total_samples; /* 이상적 QRNG: p_max ≈ 1/256 */
/* 2단계: min-entropy 계산 (비트 단위) */
double H_min = -log2(p_max); /* 이상적: -log2(1/256) = 8.0 비트/바이트 */
/* 3단계: 비트당 min-entropy */
double H_min_per_bit = H_min / 8.0; /* 이상적: 1.0 비트/비트 */
/* 4단계: hwrng quality 값으로 변환 (0~1024 스케일) */
unsigned short quality = (unsigned short)(H_min_per_bit * 1024);
/* 예시:
* H_min_per_bit = 1.000 → quality = 1024 (검증된 QRNG, 빔 분리기)
* H_min_per_bit = 0.990 → quality = 1014 (실용적 QRNG, 진공 요동)
* H_min_per_bit = 0.875 → quality = 896 (Intel RDRAND 실측치)
* H_min_per_bit = 0.500 → quality = 512 (FPGA 링 오실레이터)
* H_min_per_bit = 0.000 → quality = 0 (PRNG, 엔트로피 없음) */
NIST SP 800-90B 검증 6단계
| 단계 | 검증 항목 | 도구/방법 | 판정 기준 |
|---|---|---|---|
| 1. 데이터 수집 | 1,000,000비트 원시 샘플 수집 | dd if=/dev/hwrng bs=1M count=1 |
데이터 손실 없음 |
| 2. IID 검증 | 독립-동일 분포(IID) 여부 확인 | ea_iid (NIST 공식 도구) |
IID 가설 기각 불가 시 IID 경로 |
| 3. min-entropy 추정 | 10가지 추정기로 최소값 결정 | ea_non_iid |
H_min > 목표값 (예: 0.9 비트/비트) |
| 4. 압축 검증 | Markov/LZ 압축 테스트 | 내장 압축기 테스트 | 예측 가능 패턴 없음 |
| 5. 적응형 비율 검증 | 하드웨어 고장 감지 임계값 설정 | Repetition Count, Adaptive Proportion | 오경보율 α < 2⁻²⁰ |
| 6. 보고서 제출 | 독립 실험실 검증 및 CAVP 등록 | NIST CAVP (Cryptographic Algorithm Validation Program) | 인증서 발급 |
FIPS 140-3 요구사항 연관
| FIPS 140-3 요구사항 | hwrng 구현 | 관련 커널 코드 |
|---|---|---|
| 연속 RNG 테스트 (CRNGT) | 연속 동일 출력 감지 | hwrng_fillfn() 내 반복 감지 |
| 시작 업 테스트 (SURT) | 초기화 시 통계 검증 | hwrng_register() 호출 시 |
| min-entropy ≥ 목표값 | quality 파라미터 설정 |
struct hwrng.quality |
| 조건부 예외 테스트 | 오류 시 드라이버 비활성화 | hwrng_unregister() |
암호 키 생성 완전 플로우
QRNG에서 생성된 엔트로피가 최종 암호 키로 변환되는 전체 흐름을 추적합니다. 커널 내부에서 여러 계층의 처리를 거쳐 안전한 키가 생성됩니다.
암호 키 생성 3가지 방식
/* 방식 1: get_random_bytes() — 권장 (CRNG 경유, 항상 안전) */
static int generate_aes_key(u8 *key, size_t keylen)
{
get_random_bytes(key, keylen); /* CRNG 초기화 후 항상 블록하지 않음 */
/* keylen = 16(AES-128), 24(AES-192), 32(AES-256) */
return 0;
}
/* 사용 후 반드시 소거 */
memzero_explicit(key, keylen); /* 스택 키: 컴파일러 최적화 무력화 */
kfree_sensitive(heap_key); /* 힙 키: 해제 전 자동 제로화 */
/* 방식 2: /dev/hwrng 직접 읽기 — 사용자 공간 (원시 엔트로피, CRNG 미경유) */
#include <fcntl.h>
#include <unistd.h>
static int read_hwrng_key(unsigned char *key, size_t len)
{
int fd = open("/dev/hwrng", O_RDONLY);
if (fd < 0) return -1;
ssize_t n = read(fd, key, len);
close(fd);
return (n == (ssize_t)len) ? 0 : -1;
/* 주의: 추출기(extractor) 미적용 시 편향 가능, NIST 권장은 getrandom() */
}
/* 방식 3: rng->read() — 커널 드라이버 내부 직접 접근 */
static int kernel_rng_keygen(struct hwrng *rng, u8 *key, size_t len)
{
int ret = rng->read(rng, key, len, true);
if (ret != (int)len) return -EIO;
/* 주의: 원시 엔트로피 — 반드시 KDF(HKDF/PBKDF2)로 처리 후 사용 */
return 0;
}
테스트 및 검증
hwrng 드라이버를 로드한 후 다양한 수준의 검증을 수행해야 합니다. 기본적인 sysfs 확인부터 NIST 통계 배터리까지, 검증 단계는 드라이버 로드 확인 → 바이트 스트림 읽기 → FIPS 빠른 검증 → 정밀 통계 검증 순서로 진행합니다. 다음 다이어그램은 전체 테스트 워크플로우를 보여줍니다.
sysfs 인터페이스
# 현재 활성 hwrng 확인
cat /sys/class/misc/hw_random/rng_current
# 출력: myrng (또는 qrng-quantum)
# 사용 가능한 hwrng 목록
cat /sys/class/misc/hw_random/rng_available
# 출력: myrng virtio_rng intel-rng
# 활성 hwrng 변경 (우선순위: quality 높은 것이 자동 선택)
echo "myrng" > /sys/class/misc/hw_random/rng_current
# hwrng에서 직접 읽기 (원시 바이트)
dd if=/dev/hwrng bs=1 count=32 | xxd
rng-tools를 사용한 엔트로피 공급
# rng-tools 설치 (Ubuntu/Debian)
apt-get install rng-tools5
# rngd 데몬 실행 — /dev/hwrng → /dev/random 엔트로피 공급
rngd -r /dev/hwrng -o /dev/random -f
# 엔트로피 풀 현재 크기 확인 (레거시, 5.18+에서는 의미 감소)
cat /proc/sys/kernel/random/entropy_avail
# CRNG 초기화 완료 여부 확인 (dmesg)
dmesg | grep -E "crng|random"
# 정상: "random: crng init done"
# 경고: "random: get_random_u32 called from ... with crng_init=0" → 초기화 전 사용
rngtest — NIST SP 800-22 통계 검증
# NIST 통계 테스트 실행 (20000비트 × 200 블록)
cat /dev/hwrng | rngtest -c 200
# 예상 출력 (정상 QRNG):
# rngtest: bits received from input: 4000032
# rngtest: FIPS 140-2 success: 196
# rngtest: FIPS 140-2 failures: 4
# rngtest: bits discarded due to failures: 80032
# → 200블록 중 ~4개 실패는 통계적으로 정상 (5% 이내)
# 실패율이 너무 높으면 (10% 초과) 드라이버 또는 하드웨어 문제
# ENT (entropy estimation 도구)
dd if=/dev/hwrng bs=1M count=1 | ent
# Entropy: 7.9999 bits per byte → 이상적 QRNG
# Chi-square: 초과 확률 > 1%이면 양호
커널 내부 엔트로피 디버그
# CRNG 초기화 타임스탬프 확인
dmesg --ctime | grep "crng init"
# hwrng 스레드 동작 확인 (hw_random 코어가 kthread로 지속 수집)
ps aux | grep hwrng
# 출력: ... [hwrng] (커널 스레드)
# /proc/sys/kernel/random/ 파라미터
ls /proc/sys/kernel/random/
# boot_id — 부팅 고유 UUID (변경 불가)
# uuid — 읽을 때마다 새 UUID 생성
# entropy_avail — 현재 엔트로피 추정값 (비트)
# poolsize — 입력 풀 크기 (항상 256)
# urandom_min_reseed_secs — 재시드 최소 간격
# 엔트로피 소모 시뮬레이션 (테스트 전용)
dd if=/dev/urandom of=/dev/null bs=1M count=100 &
watch -n 0.5 'cat /proc/sys/kernel/random/entropy_avail'
엔트로피 기여량 실시간(Real-time) 모니터링
# hwrng 스레드 동작 및 엔트로피 기여량 실시간 모니터링
# (1초 간격으로 entropy_avail 변화 추적)
watch -n 1 'echo "=== $(date +%T) ===" ;
echo "활성 hwrng: $(cat /sys/class/misc/hw_random/rng_current)";
echo "엔트로피 추정: $(cat /proc/sys/kernel/random/entropy_avail) bits";
echo "CRNG 재시드: $(cat /proc/sys/kernel/random/urandom_min_reseed_secs)s 간격"'
# hwrng 커널 스레드 CPU 사용률 확인
ps -eo pid,comm,pcpu | grep hwrng
# 1MB 샘플 수집 속도 측정 (처리율 벤치마크)
time dd if=/dev/hwrng of=/dev/null bs=4k count=256 2>&1
# 출력 예: 1048576 bytes copied, 0.08 s, 12.6 MB/s ← PCIe QRNG
# 1048576 bytes copied, 1.05 s, 998 kB/s ← USB QRNG
NIST SP 800-90B 오픈소스 검증 도구
# NIST SP 800-90B 공식 검증 도구 설치 (Python 3 기반)
git clone https://github.com/usnistgov/SP800-90B_EntropyAssessment.git
cd SP800-90B_EntropyAssessment
pip3 install -r requirements.txt
# 1,000,000바이트 원시 엔트로피 수집
dd if=/dev/hwrng bs=1M count=1 of=qrng_sample.bin
# IID 추정 (독립-동일 분포 가정)
python3 ./src/ea_iid.py qrng_sample.bin 8
# 출력: min-entropy estimate = 7.999 bits per symbol → quality=1023 적합
# Non-IID 추정 (보수적 추정, FIPS 140-3 요건)
python3 ./src/ea_non_iid.py qrng_sample.bin 8
# 10가지 추정기 중 최솟값이 실제 min-entropy
# 출력 예: min-entropy = 7.998 → H_min_per_bit = 0.9998 → quality=1023
# 재시작 테스트 (Restart Test) — 하드웨어 초기 조건 의존성 검증
python3 ./src/ea_restart.py qrng_restart.bin 8 1000
# 1000회 재시작 × 1000샘플 = 1,000,000바이트 필요
dieharder 통계 배터리 전체 실행
# dieharder 설치
apt-get install dieharder # Ubuntu/Debian
dnf install dieharder # Fedora/RHEL
# 전체 배터리 실행 (114개 통계 검정, 수 시간 소요)
cat /dev/hwrng | dieharder -a -g 200
# -a: 모든 테스트, -g 200: stdin 입력
# 결과: PASSED/WEAK/FAILED — WEAK는 5% 이내 정상
# 특정 테스트만 실행 (빠른 검증)
cat /dev/hwrng | dieharder -d 0 -g 200 # Birthday Spacings
cat /dev/hwrng | dieharder -d 15 -g 200 # RGB Bit Distribution
cat /dev/hwrng | dieharder -d 100 -g 200 # STS Monobit
# ENT 도구 — 빠른 엔트로피 추정
dd if=/dev/hwrng bs=1M count=1 | ent
# Entropy: 7.9999 bits per byte → 이상적 QRNG
# Chi-square: 적합 확률 50% → 정상 분포
# Arithmetic mean: 127.49 → 균등 분포 (이론: 127.5)
# Serial correlation: 0.000001 → 독립성 확인
드라이버 로드/언로드 검증 체크리스트
| 단계 | 확인 명령 | 정상 기대값 |
|---|---|---|
| 드라이버 로드 | insmod myrng.ko && dmesg | tail -5 |
"myrng 등록 완료" 메시지 |
| hwrng 인식 | cat /sys/class/misc/hw_random/rng_available |
목록에 "myrng" 포함 |
| 난수 읽기 | dd if=/dev/hwrng bs=16 count=1 | xxd |
16바이트 난수 출력 |
| 엔트로피 기여 | watch cat /proc/sys/kernel/random/entropy_avail |
값이 증가 |
| NIST 테스트 | cat /dev/hwrng | rngtest -c 100 |
실패율 5% 이하 |
| 드라이버 언로드 | rmmod myrng && dmesg | tail -3 |
정상 언로드 메시지, 크래시 없음 |
엔트로피 풀 구조 심층 분석
Linux 커널의 엔트로피 풀은 drivers/char/random.c에 구현된 핵심 난수 인프라의 심장부입니다.
커널 5.18 이전에는 input_pool과 blocking_pool 두 개의 풀이 존재했지만,
현대 커널에서는 Blake2s 기반의 단일 input_pool로 통합되었습니다.
이 섹션에서는 풀의 내부 구조, 믹싱 알고리즘, 엔트로피 추정 메커니즘을 심층적으로 분석합니다.
풀 구조의 진화: LFSR에서 Blake2s로
c1ea38a5e7c(v5.17)에서 Blake2s 해시 함수로 완전 교체되었으며,
이로써 풀 크기가 4096비트(512바이트)에서 256비트(32바이트)로 축소되었지만 암호학적 강도는 크게 향상되었습니다.
| 항목 | 레거시 (5.17 이전) | 현대 (5.18+) |
|---|---|---|
| 풀 구조 | input_pool + blocking_pool (2개) | input_pool 단일 |
| 믹싱 함수 | LFSR (비트 연산 기반) | Blake2s (암호학적 해시) |
| 풀 크기 | 4096비트 (512바이트) | 256비트 (32바이트 해시 상태) |
| 추출 함수 | SHA-1 기반 extract | Blake2s final + re-absorb |
| 엔트로피 카운터 | 정수 비트 카운터 (정밀) | init_bits 256비트 목표 |
| /dev/random 블록킹 | 엔트로피 고갈 시 블록 | CRNG 초기화 전에만 블록 |
| forward secrecy | 미보장 (SHA-1 출력 재사용) | Fast Key Erasure 보장 |
믹싱 알고리즘 상세
/* drivers/char/random.c — 현대 커널 엔트로피 풀 구조 */
/* 전역 엔트로피 풀 — Blake2s 해시 상태 */
static struct {
struct blake2s_state hash; /* 256비트 내부 상태 */
spinlock_t lock; /* 동시 접근 보호 */
unsigned int init_bits; /* 누적 엔트로피 비트 수 */
} input_pool = {
.hash.h = { BLAKE2S_IV0 ^ (0x01010000 | 32),
BLAKE2S_IV1, BLAKE2S_IV2, BLAKE2S_IV3,
BLAKE2S_IV4, BLAKE2S_IV5, BLAKE2S_IV6, BLAKE2S_IV7 },
.hash.outlen = 32,
.lock = __SPIN_LOCK_UNLOCKED(input_pool.lock),
};
/* 믹싱 함수 — 엔트로피를 풀에 혼합 (비가역적) */
static void _mix_pool_bytes(const void *buf, size_t len)
{
unsigned long flags;
spin_lock_irqsave(&input_pool.lock, flags);
blake2s_update(&input_pool.hash, buf, len);
spin_unlock_irqrestore(&input_pool.lock, flags);
}
/* 엔트로피 추출 — 풀 상태를 변경하며 32바이트 키 생성 */
static void extract_entropy(void *buf, size_t len)
{
unsigned long flags;
u8 hash[BLAKE2S_HASH_SIZE];
struct blake2s_state state;
spin_lock_irqsave(&input_pool.lock, flags);
/* 1. 현재 풀 상태의 스냅샷 생성 */
state = input_pool.hash;
/* 2. 스냅샷에서 최종 해시 계산 (256비트) */
blake2s_final(&state, hash);
/* 3. 출력 해시를 풀에 재혼합 → backtrack resistance */
blake2s_update(&input_pool.hash, hash, sizeof(hash));
spin_unlock_irqrestore(&input_pool.lock, flags);
memcpy(buf, hash, len);
memzero_explicit(hash, sizeof(hash));
memzero_explicit(&state, sizeof(state));
}
/* 엔트로피 크레딧 — init_bits 누적 (256비트 목표) */
static void credit_init_bits(size_t bits)
{
unsigned int new_bits, orig;
unsigned long flags;
if (!bits)
return;
spin_lock_irqsave(&input_pool.lock, flags);
orig = input_pool.init_bits;
new_bits = min_t(unsigned int, POOL_READY_BITS,
orig + bits); /* 최대 256비트까지만 */
WRITE_ONCE(input_pool.init_bits, new_bits);
spin_unlock_irqrestore(&input_pool.lock, flags);
/* 256비트 도달 시 CRNG 초기화 트리거 */
if (orig < POOL_READY_BITS && new_bits >= POOL_READY_BITS)
crng_reseed();
}
엔트로피 추정과 크레딧 정책
커널은 각 소스별로 다른 엔트로피 크레딧 정책을 적용합니다.
hwrng의 경우 quality 파라미터가 직접적으로 크레딧 계산에 사용됩니다.
| 엔트로피 소스 | 크레딧 정책 | 기여 조건 | 최대 기여량 |
|---|---|---|---|
| hwrng | bytes × quality × 8 / 1024 |
hwrng_fillfn() 스레드 주기적 호출 | quality 의존 (최대 무제한) |
| 인터럽트 지터 | fast_pool 64회마다 1비트 | TSC/사이클 카운터 차이 | 초당 수십 비트 |
| 디스크 I/O | 타이밍 델타 기반 | CONFIG_RANDOM_TRUST_BOOTLOADER |
I/O 빈도에 비례 |
| 입력 이벤트 | 이벤트 타이밍 + 코드 | 키보드/마우스 존재 시 | 서버에서는 거의 0 |
| CPU RDRAND | CONFIG_RANDOM_TRUST_CPU=y 시 256비트 |
부팅 초기 즉시 | 256비트 (일회성) |
RDSEED vs RDRAND 비교 분석
Intel x86 프로세서는 두 가지 하드웨어 난수 명령어를 제공합니다:
RDRAND(Ivy Bridge, 2012~)와 RDSEED(Broadwell, 2014~).
두 명령어는 같은 하드웨어 엔트로피 소스(열잡음 기반 디지털 난수 생성기)를 공유하지만,
출력 방식과 암호학적 보증이 근본적으로 다릅니다.
RDRAND 내부 동작
/* arch/x86/include/asm/archrandom.h — RDRAND 인라인 어셈블리 */
static inline bool rdrand_long(unsigned long *v)
{
bool ok;
unsigned int retry = RDRAND_RETRY_LOOPS; /* 기본 10회 */
do {
asm volatile(RDRAND_LONG
"\n\t"
CC_SET(c) /* CF=1이면 유효한 난수 */
: CC_OUT(c)(ok), "=a"(*v));
if (ok)
return true;
} while (--retry);
return false; /* DRNG 내부 고갈 — 매우 드뭄 */
}
/* RDSEED — 순수 하드웨어 엔트로피 (DRBG 미경유) */
static inline bool rdseed_long(unsigned long *v)
{
bool ok;
asm volatile(RDSEED_LONG
"\n\t"
CC_SET(c) /* CF=1이면 유효한 시드 */
: CC_OUT(c)(ok), "=a"(*v));
return ok; /* 실패 확률이 RDRAND보다 높음 */
}
/* 커널 엔트로피 수집에서의 사용 — random.c */
static void try_to_generate_entropy(void)
{
unsigned long v;
/* RDSEED 우선 시도 (순수 엔트로피) */
if (rdseed_long(&v)) {
_mix_pool_bytes(&v, sizeof(v));
credit_init_bits(sizeof(v) * 8); /* 64비트 크레딧 */
return;
}
/* RDSEED 실패 시 RDRAND 폴백 (DRBG 경유) */
if (rdrand_long(&v)) {
_mix_pool_bytes(&v, sizeof(v));
/* CONFIG_RANDOM_TRUST_CPU=y 설정 시에만 크레딧 부여 */
}
}
RDRAND vs RDSEED 상세 비교표
| 항목 | RDRAND | RDSEED |
|---|---|---|
| 도입 시기 | Ivy Bridge (2012, 3세대) | Broadwell (2014, 5세대) |
| CPUID 플래그 | CPUID.01H:ECX.RDRAND[30] | CPUID.07H:EBX.RDSEED[18] |
| 엔트로피 출처 | CTR-DRBG (AES-256-CTR) 출력 | ES 컨디셔너 직접 출력 |
| NIST 준수 | SP 800-90A (DRBG) | SP 800-90B (Entropy Source) |
| 실패 확률 | 매우 낮음 (내부 버퍼(Buffer)링) | 상대적으로 높음 (ES 직접) |
| 처리율 (Skylake) | ~500 MB/s | ~70 MB/s (변동 있음) |
| 예측 저항성 | 재시드 간격 내 예측 가능 | 매 출력마다 독립 |
| 커널 사용처 | 보조 엔트로피 믹싱 | input_pool 초기 시딩 |
| 지연(latency) | ~400 사이클 | ~800 사이클 (변동) |
| AMD 명칭 | 동일 (Zen+) | 동일 (Zen 2+) |
RDRAND/RDSEED는 CPU 마이크로코드 수준에서 구현되어
독립적 검증이 어렵습니다. 커널의 CONFIG_RANDOM_TRUST_CPU 옵션이 n이면
RDRAND 출력을 엔트로피로 신뢰하지 않고, 믹싱만 수행합니다.
nordrand 부트 파라미터로 RDRAND 사용을 완전히 비활성화할 수 있습니다.
FIPS 140-2/140-3 인증과 커널 RNG
FIPS 140(Federal Information Processing Standard 140)은 미국 연방 정부가 요구하는
암호 모듈 보안 표준입니다. Linux 커널은 CONFIG_CRYPTO_FIPS 옵션과 fips=1 부트 파라미터를 통해
FIPS 모드를 지원하며, 이 모드에서는 RNG 관련 추가 요구사항이 활성화됩니다.
FIPS 모드 커널 설정
/* crypto/fips.c — FIPS 모드 전역 플래그 */
int fips_enabled;
EXPORT_SYMBOL_GPL(fips_enabled);
/* 부팅 파라미터: fips=1 → FIPS 모드 활성화 */
static int __init fips_enable(char *str)
{
fips_enabled = simple_strtol(str, NULL, 0);
printk(KERN_INFO "fips mode is %s\n",
fips_enabled ? "enabled" : "disabled");
return 1;
}
__setup("fips=", fips_enable);
/* FIPS 모드에서의 RNG 셀프 테스트 */
/* crypto/testmgr.c — alg_test_drbg() */
static int alg_test_drbg(const struct alg_test_desc *desc,
const char *driver, u32 type, u32 mask)
{
int err = 0;
int i;
const struct drbg_testvec *tv;
/* 알려진 응답 테스트(KAT): 고정 시드 → 고정 출력 비교 */
for (i = 0; i < desc->suite.drbg.count; i++) {
tv = &desc->suite.drbg.vecs[i];
err = drbg_cavs_test(tv, desc->alg_common.cra_driver_name);
if (err) {
pr_err("DRBG KAT 실패 #%d: %s\n", i, driver);
return err;
}
}
return 0;
}
FIPS DRBG 요구사항과 커널 구현 매핑
| FIPS 요구사항 | SP 800-90A 조항 | 커널 구현 | 코드 위치 |
|---|---|---|---|
| 시드 소스 품질 | Section 8.6.1 | hwrng quality >= 256 |
drivers/char/hw_random/core.c |
| 재시드 주기 | Section 9.3.2 | 5분 또는 2^48 블록 | drivers/char/random.c crng_reseed() |
| 예측 저항 | Section 11.3 | Fast Key Erasure | crng_fast_key_erasure() |
| 상태 소거 | Section 11.4 | memzero_explicit() |
모든 키 사용 후 호출 |
| 건강 테스트 | Section 11.3.3 | 연속 출력 비교 | hwrng_fillfn() |
HW RNG 드라이버 구조
커널의 hwrng 서브시스템(drivers/char/hw_random/)은 다양한 하드웨어 RNG를 통일된
프레임워크로 관리합니다. 이 섹션에서는 코어 프레임워크의 내부 동작, 등록/해제 흐름,
주요 드라이버(virtio-rng, TPM RNG, Intel DRNG)의 구현 차이를 분석합니다.
hwrng 코어 프레임워크 내부
/* drivers/char/hw_random/core.c — hwrng_register() 흐름 */
int hwrng_register(struct hwrng *rng)
{
int err = -EINVAL;
struct hwrng *tmp;
bool is_new_current = false;
/* 필수 콜백 검증: read() 또는 data_read() 중 하나 필수 */
if (!rng->name || (!rng->data_read && !rng->read))
return -EINVAL;
mutex_lock(&rng_mutex);
/* 이름 중복 검사 */
list_for_each_entry(tmp, &rng_list, list) {
if (strcmp(tmp->name, rng->name) == 0) {
err = -EEXIST;
goto out_unlock;
}
}
/* 초기화 콜백 호출 (있으면) */
if (rng->init) {
err = rng->init(rng);
if (err)
goto out_unlock;
}
kref_init(&rng->ref);
init_completion(&rng->cleanup_done);
init_completion(&rng->dying);
/* 우선순위: quality가 높은 드라이버가 current_rng */
if (!current_rng || rng->quality > current_rng->quality) {
current_rng = rng;
is_new_current = true;
}
list_add_tail(&rng->list, &rng_list);
/* hwrng_fillfn 스레드 시작 (최초 등록 시) */
if (is_new_current || !hwrng_fill)
start_khwrngd();
mutex_unlock(&rng_mutex);
return 0;
out_unlock:
mutex_unlock(&rng_mutex);
return err;
}
virtio-rng 드라이버 상세
가상화 환경에서 게스트 OS의 엔트로피 부족은 심각한 보안 문제를 야기합니다.
virtio-rng 드라이버는 호스트의 /dev/urandom에서 생성된 난수를
virtqueue를 통해 게스트에 공급합니다.
/* drivers/char/hw_random/virtio-rng.c — 핵심 구조 */
struct virtrng_info {
struct hwrng hwrng;
struct virtqueue *vq; /* virtio 큐 */
struct completion have_data; /* 데이터 수신 완료 */
unsigned int data_avail; /* 사용 가능 바이트 */
unsigned int data_idx; /* 현재 읽기 인덱스 */
u8 *data; /* 수신 버퍼 */
bool busy;
bool hwrng_removed;
};
/* virtio 콜백 — 호스트에서 데이터 도착 */
static void random_recv_done(struct virtqueue *vq)
{
struct virtrng_info *vi = vq->vdev->priv;
vi->data_avail = virtqueue_get_buf(vq, &vi->data_avail);
complete(&vi->have_data);
}
/* hwrng read — virtqueue에서 난수 수신 */
static int virtio_read(struct hwrng *hwrng, void *buf,
size_t size, bool wait)
{
struct virtrng_info *vi = (struct virtrng_info *)hwrng->priv;
int ret;
if (!vi->busy) {
vi->busy = true;
reinit_completion(&vi->have_data);
register_buffer(vi); /* virtqueue에 수신 버퍼 등록 */
}
if (!wait)
return 0;
ret = wait_for_completion_killable(&vi->have_data);
if (ret < 0)
return ret;
vi->busy = false;
memcpy(buf, vi->data, vi->data_avail);
return vi->data_avail;
}
-device virtio-rng-pci,max-bytes=1024,period=1000으로
가상 RNG를 추가합니다. max-bytes와 period(ms)로 게스트에 공급하는 난수 대역폭(Bandwidth)을 제한할 수 있습니다.
-object rng-random,filename=/dev/urandom,id=rng0으로 호스트 소스를 지정합니다.
getrandom() 시스템 콜 심층 분석
getrandom()(syscall #318, x86-64)은 Linux 3.17에서 도입된 현대적 난수 획득 인터페이스입니다.
파일 디스크립터가 필요 없고, CRNG 초기화 상태를 정확히 반영하며,
/dev/urandom의 초기 부팅 시 안전하지 않은 난수 반환 문제를 해결합니다.
커널 내부 실행 경로
/* drivers/char/random.c — getrandom() 시스템 콜 구현 */
SYSCALL_DEFINE3(getrandom, char __user *, ubuf,
size_t, len, unsigned int, flags)
{
struct iov_iter iter;
int ret;
/* 플래그 유효성 검사 */
if (flags & ~(GRND_NONBLOCK | GRND_RANDOM | GRND_INSECURE))
return -EINVAL;
/* 크기 제한: INT_MAX (사실상 무제한) */
len = min_t(size_t, len, INT_MAX);
/* GRND_INSECURE: CRNG 초기화 전에도 즉시 반환 (안전하지 않음) */
if (flags & GRND_INSECURE)
goto insecure;
/* CRNG 초기화 대기 */
if (!crng_ready()) {
if (flags & GRND_NONBLOCK)
return -EAGAIN; /* 즉시 반환: 아직 준비 안 됨 */
/* 블록: CRNG 초기화(256비트 엔트로피 수집) 완료까지 대기 */
ret = wait_for_random_bytes();
if (ret)
return ret; /* -EINTR: 시그널에 의해 인터럽트 */
}
insecure:
/* ChaCha20-CRNG에서 난수 생성 → 사용자 공간으로 복사 */
import_ubuf(ITER_DEST, ubuf, len, &iter);
ret = get_random_bytes_user(&iter);
return ret;
}
/* wait_for_random_bytes() — CRNG 초기화 대기 */
int wait_for_random_bytes(void)
{
while (!crng_ready()) {
int ret;
/* jitterentropy 보조 시딩 시도 */
try_to_generate_entropy();
ret = wait_event_interruptible_timeout(
crng_init_wait, crng_ready(), HZ);
if (ret)
return ret > 0 ? 0 : ret;
}
return 0;
}
부팅 초기 블로킹 문제와 대응
getrandom()은 블록됩니다.
| 환경 | CRNG 초기화 시간 | 원인 | 해결책 |
|---|---|---|---|
| 물리 서버 (hwrng 있음) | < 1초 | hwrng가 즉시 엔트로피 공급 | 기본 설정으로 충분 |
| 물리 서버 (hwrng 없음) | 1~5초 | 인터럽트 지터 + 디스크 I/O | jitterentropy-rngd 설치 |
| KVM 가상머신 | 1~30초 | 가상화된 타이머(Timer) 정밀도 감소 | virtio-rng 장치 추가 |
| Docker 컨테이너 | 호스트 의존 | 호스트 CRNG 공유 | 호스트의 hwrng 보장 |
| 임베디드/IoT | 10초~수 분 | 최소 하드웨어, 입력 없음 | SoC 내장 TRNG 드라이버 활성화 |
# 부팅 초기 블로킹 진단
dmesg | grep -E "crng|random|getrandom"
# "random: crng init done" 메시지 확인 (타임스탬프 = 초기화 소요 시간)
# 커널 명령줄로 엔트로피 소스 신뢰 설정
# random.trust_cpu=1 — RDRAND 출력을 엔트로피로 신뢰
# random.trust_bootloader=1 — 부트로더 제공 시드 신뢰
# systemd-random-seed.service: 이전 부팅의 엔트로피 저장/복원
systemctl status systemd-random-seed.service
# 저장 위치: /var/lib/systemd/random-seed (32바이트)
ChaCha20 CRNG 내부 심층 분석
Linux 커널의 CRNG(Cryptographically secure Random Number Generator)는 Daniel Bernstein의 ChaCha20 스트림 암호를 핵심으로 사용합니다. 이 섹션에서는 per-CPU CRNG 구조, 리시드 메커니즘, NUMA 최적화, Fast Key Erasure 기법을 상세히 분석합니다.
ChaCha20 블록 함수 상세
/* lib/crypto/chacha20-generic.c — ChaCha20 핵심 */
/* ChaCha20 상태: 4×4 = 16개의 32비트 워드 (512비트 = 64바이트) */
/*
* 상태 레이아웃:
* cccccccc cccccccc cccccccc cccccccc ← 상수 "expand 32-byte k"
* kkkkkkkk kkkkkkkk kkkkkkkk kkkkkkkk ← 키 (256비트, 8워드)
* kkkkkkkk kkkkkkkk kkkkkkkk kkkkkkkk
* bbbbbbbb nnnnnnnn nnnnnnnn nnnnnnnn ← 블록 카운터 + 논스
*/
static void chacha20_block_generic(u32 *state, u8 *stream)
{
u32 x[16];
int i;
memcpy(x, state, 64);
/* 20라운드 (10 double-round) */
for (i = 0; i < 10; i++) {
/* 열(column) 라운드 */
QUARTERROUND(x[0], x[4], x[8], x[12]);
QUARTERROUND(x[1], x[5], x[9], x[13]);
QUARTERROUND(x[2], x[6], x[10], x[14]);
QUARTERROUND(x[3], x[7], x[11], x[15]);
/* 대각선(diagonal) 라운드 */
QUARTERROUND(x[0], x[5], x[10], x[15]);
QUARTERROUND(x[1], x[6], x[11], x[12]);
QUARTERROUND(x[2], x[7], x[8], x[13]);
QUARTERROUND(x[3], x[4], x[9], x[14]);
}
/* 최종 더하기: 원본 상태와 XOR → 역연산 방지 */
for (i = 0; i < 16; i++)
x[i] += state[i];
memcpy(stream, x, 64); /* 64바이트 키스트림 출력 */
state[12]++; /* 블록 카운터 증가 */
}
/* Quarter Round 매크로 — ChaCha20의 핵심 연산 */
#define QUARTERROUND(a, b, c, d) \
a += b; d ^= a; d = rol32(d, 16); \
c += d; b ^= c; b = rol32(b, 12); \
a += b; d ^= a; d = rol32(d, 8); \
c += d; b ^= c; b = rol32(b, 7)
NUMA per-node CRNG 최적화
/* drivers/char/random.c — per-CPU CRNG 키 갱신 로직 */
/* 난수 요청 시 per-CPU CRNG 세대 확인 */
static void crng_make_state(u32 chacha_state[CHACHA_STATE_WORDS],
u8 *random_data, size_t random_data_len)
{
unsigned long flags;
struct crng *crng;
/* preempt 비활성화 + per-CPU CRNG 접근 */
local_lock_irqsave(&crngs.lock, flags);
crng = raw_cpu_ptr(&crngs);
/* 세대(generation) 비교 → 불일치 시 base_crng에서 새 키 복사 */
if (unlikely(crng->generation != READ_ONCE(base_crng.generation))) {
spin_lock(&base_crng.lock);
memcpy(crng->key, base_crng.key, sizeof(crng->key));
crng->generation = base_crng.generation;
spin_unlock(&base_crng.lock);
}
/* per-CPU 키로 ChaCha20 상태 초기화 */
chacha_init_consts(chacha_state);
memcpy(&chacha_state[4], crng->key, CHACHA20_KEY_SIZE);
memset(&chacha_state[14], 0, 8); /* 논스 = 0 */
++chacha_state[12]; /* 블록 카운터 증가 */
/* Fast Key Erasure: 출력의 앞 32바이트 → 새 키 */
crng_fast_key_erasure(crng->key, chacha_state,
random_data, random_data_len);
local_unlock_irqrestore(&crngs.lock, flags);
}
get_random_bytes()는
다른 CPU와 락 경합 없이 동작합니다. NUMA 시스템에서 원격 노드 메모리 접근도 발생하지 않아
1000만+ 호출/초 처리율을 달성합니다.
엔트로피 소스 완전 가이드
Linux 커널은 다양한 엔트로피 소스를 수집하여 input_pool에 혼합합니다. 각 소스는 서로 다른 품질과 수집 속도를 가지며, 이들의 조합으로 전체 엔트로피 품질을 보장합니다. 이 섹션에서는 각 소스의 동작 원리, 수집 경로, 환경별 가용성을 분석합니다.
인터럽트 타이밍 지터
인터럽트 타이밍은 가장 풍부한 소프트웨어 엔트로피 소스입니다. 하드웨어 인터럽트(IRQ) 도착 시각의 나노초 단위 변동(지터)은 CPU 캐시(Cache) 상태, 메모리 접근 패턴, 버스(Bus) 경합 등에 의해 결정되며 외부에서 예측하기 매우 어렵습니다.
/* drivers/char/random.c — 인터럽트 엔트로피 수집 상세 */
/* per-CPU 고속 풀 — 인터럽트 컨텍스트에서 락 없이 동작 */
struct fast_pool {
union {
u32 pool32[4]; /* 128비트 풀 (SipHash 스타일) */
u64 pool64[2];
};
unsigned long last; /* 마지막 기여 시각 (jiffies) */
u16 count; /* 인터럽트 카운터 (64회마다 기여) */
u16 reg_idx; /* 레지스터 인덱스 */
};
/* fast_mix() — 128비트 풀에 빠르게 혼합 (SipHash 유사) */
static void fast_mix(u32 pool[4], u32 a, u32 b, u32 c)
{
pool[0] ^= a;
pool[1] ^= b;
pool[2] ^= c;
pool[3] ^= pool[0];
/* 4라운드 혼합 — 확산(diffusion) 보장 */
pool[0] = rol32(pool[0], 7) + pool[3];
pool[1] = rol32(pool[1], 13) + pool[2];
pool[2] = rol32(pool[2], 11) + pool[1];
pool[3] = rol32(pool[3], 17) + pool[0];
}
/* mix_interrupt_randomness() — fast_pool → input_pool 전이 */
static void mix_interrupt_randomness(struct work_struct *work)
{
struct fast_pool *fast_pool = container_of(work, ...);
u32 pool[4];
memcpy(pool, fast_pool->pool32, sizeof(pool));
memset(fast_pool->pool32, 0, sizeof(fast_pool->pool32));
_mix_pool_bytes(pool, sizeof(pool));
credit_init_bits(max(1u, ...) ); /* 최소 1비트 크레딧 */
}
jitterentropy — CPU 지터 엔트로피
/* crypto/jitterentropy.c — 소프트웨어 기반 엔트로피 생성 */
/* CPU 명령어 실행 시간의 미세 변동을 엔트로피로 활용 */
/* 하드웨어 RNG가 없는 환경에서 보조 엔트로피 소스 역할 */
static u64 jent_measure_jitter(struct rand_data *ec)
{
u64 time, delta;
/* 메모리 접근 루프: 캐시 라인 경합으로 지터 유발 */
jent_memaccess(ec, 0);
/* 고정밀 타임스탬프 측정 */
time = jent_get_nstime(); /* rdtsc / cntvct / clock_gettime */
delta = time - ec->prev_time;
ec->prev_time = time;
/* 1차 미분: 타임스탬프 간 차이 */
/* 2차 미분: 차이의 차이 → 진정한 지터 추출 */
return delta;
}
/* 커널 부팅 시 jitterentropy 자동 활성화 조건 */
/* CONFIG_CRYPTO_JITTERENTROPY=y (대부분 배포판 기본) */
/* CRNG 초기화 대기 중 try_to_generate_entropy()에서 호출 */
ftrace/bpftrace 엔트로피 모니터링
hwrng 서브시스템과 엔트로피 풀의 동작을 실시간으로 관찰하는 것은
드라이버 디버깅(Debugging)과 성능 분석에 필수적입니다. 이 섹션에서는 ftrace, bpftrace,
/proc/sys/kernel/random/ 인터페이스를 활용한 모니터링 기법을 다룹니다.
ftrace로 난수 생성 추적
# ftrace 설정: random 서브시스템 이벤트 활성화
cd /sys/kernel/tracing
# 사용 가능한 random 이벤트 확인
ls events/random/
# add_device_randomness credit_entropy_bits
# debit_entropy extract_entropy
# get_random_bytes mix_pool_bytes
# push_to_pool random_read
# urandom_read getrandom
# 엔트로피 크레딧 추적 활성화
echo 1 > events/random/credit_entropy_bits/enable
echo 1 > events/random/mix_pool_bytes/enable
echo 1 > events/random/extract_entropy/enable
# 트레이스 시작
echo 1 > tracing_on
# 트레이스 확인 (엔트로피 수집/소비 패턴)
cat trace_pipe | head -50
# 출력 예:
# [hwrng]-123 credit_entropy_bits: bits 256
# sshd-456 extract_entropy: nbytes 32
# nginx-789 get_random_bytes: nbytes 16
# 트레이스 종료
echo 0 > tracing_on
bpftrace 엔트로피 모니터링 스크립트
/* bpftrace: 엔트로피 수집 함수 호출 빈도 히스토그램 */
/* 파일명: entropy_monitor.bt */
/* hwrng에서 엔트로피 수집 추적 */
kprobe:add_hwgenerator_randomness
{
@hw_count = count();
@hw_bytes = hist(arg1); /* 수집 바이트 수 분포 */
}
/* 인터럽트 엔트로피 수집 추적 */
kprobe:add_interrupt_randomness
{
@irq_count = count();
@irq_num[arg0] = count(); /* IRQ 번호별 빈도 */
}
/* 엔트로피 추출(소비) 추적 */
kprobe:extract_entropy
{
@extract_count = count();
@extract_bytes = hist(arg1); /* 추출 바이트 수 분포 */
}
/* get_random_bytes 호출자 추적 */
kprobe:get_random_bytes
{
@callers[kstack(3)] = count(); /* 상위 3프레임 콜스택 */
}
/* getrandom 시스콜 추적 */
tracepoint:syscalls:sys_enter_getrandom
{
@getrandom_size = hist(args->count);
@getrandom_flags[args->flags] = count();
}
interval:s:10
{
printf("\n--- %s 엔트로피 통계 ---\n", strftime("%H:%M:%S", nsecs));
print(@hw_count);
print(@irq_count);
print(@extract_count);
}
# bpftrace 스크립트 실행
bpftrace entropy_monitor.bt
# /proc/sys/kernel/random/ 파라미터 종합 분석
echo "=== 커널 난수 시스템 상태 ==="
echo "엔트로피 추정: $(cat /proc/sys/kernel/random/entropy_avail) bits"
echo "풀 크기: $(cat /proc/sys/kernel/random/poolsize) bits"
echo "부팅 UUID: $(cat /proc/sys/kernel/random/boot_id)"
echo "재시드 간격: $(cat /proc/sys/kernel/random/urandom_min_reseed_secs)초"
echo "읽기 깨움: $(cat /proc/sys/kernel/random/read_wakeup_threshold) bits"
echo "쓰기 깨움: $(cat /proc/sys/kernel/random/write_wakeup_threshold) bits"
# hwrng 드라이버 상태 종합
echo ""
echo "=== hwrng 드라이버 상태 ==="
echo "활성 RNG: $(cat /sys/class/misc/hw_random/rng_current)"
echo "가용 RNG: $(cat /sys/class/misc/hw_random/rng_available)"
# hwrng 처리율 벤치마크
echo ""
echo "=== hwrng 처리율 벤치마크 ==="
dd if=/dev/hwrng of=/dev/null bs=4096 count=1024 2>&1 | tail -1
# 4194304 bytes (4.2 MB, 4.0 MiB) copied, 0.05 s, 83.9 MB/s
# perf stat으로 RDRAND 명령어 실행 횟수 측정
perf stat -e instructions,cycles,cache-misses \
dd if=/dev/urandom of=/dev/null bs=4096 count=256 2>/dev/null
/proc/sys/kernel/random/ 파라미터 레퍼런스
| 파라미터 | 읽기/쓰기 | 기본값 | 설명 |
|---|---|---|---|
entropy_avail |
읽기 전용(Read-Only) | 0~256 | 현재 엔트로피 추정값 (비트). 5.18+에서는 0 또는 256 |
poolsize |
읽기 전용 | 256 | input_pool 크기 (비트). Blake2s 해시 상태 크기 |
boot_id |
읽기 전용 | UUID | 부팅마다 새로 생성되는 128비트 UUID |
uuid |
읽기 전용 | UUID | 읽을 때마다 새 UUID 생성 (UUID v4) |
urandom_min_reseed_secs |
읽기/쓰기 | 60 | CRNG 재시드 최소 간격 (초). 0이면 항상 재시드 |
read_wakeup_threshold |
읽기/쓰기 | 64 | /dev/random 블로킹 해제 엔트로피 임계값 |
write_wakeup_threshold |
읽기/쓰기 | 896 | 엔트로피 부족 시 poll() 이벤트 발생 임계값 |
RNG 구현 비교: Linux vs BSD vs Windows
운영체제마다 난수 생성 서브시스템의 설계 철학과 구현이 크게 다릅니다. 이 섹션에서는 Linux, FreeBSD, OpenBSD, Windows의 RNG 구현을 비교하고, 각각의 장단점과 성능 특성을 분석합니다.
운영체제별 RNG 구현 비교표
| 항목 | Linux (5.18+) | FreeBSD (14) | OpenBSD (7.x) | Windows (11) |
|---|---|---|---|---|
| 핵심 알고리즘 | ChaCha20 (CRNG) | Fortuna (AES-256-CTR) | ChaCha20 (arc4random) | AES-256-CTR (BCryptGenRandom) |
| 엔트로피 풀 | Blake2s 256비트 | 32개 풀 (Fortuna) | 256비트 (단일) | 다중 풀 (비공개) |
| 리시드 주기 | 5분 또는 수요 기반 | 10초 ~ 풀 로테이션 | 매 1.6MB 출력마다 | 자동 (비공개) |
| per-CPU 구조 | per-CPU ChaCha20 키 | 단일 전역 Fortuna | per-CPU arc4random | per-프로세서 (비공개) |
| 사용자 API | getrandom() |
getentropy() |
getentropy() |
BCryptGenRandom() |
| hwrng 프레임워크 | struct hwrng (다중 드라이버) | random_harvest (단일) | 없음 (RDRAND 직접) | CNG Provider |
| forward secrecy | Fast Key Erasure | Fortuna 풀 교체 | 매 블록 키 교체 | 미공개 |
| FIPS 인증 | CONFIG_CRYPTO_FIPS | 미인증 | 미인증 | FIPS 140-2 Level 1 |
| 부팅 시 블로킹 | getrandom() 블록 | getentropy() 블록 | 항상 비블록 | 항상 비블록 |
| 엔트로피 고갈 시 | CRNG 계속 출력 (안전) | Fortuna 계속 출력 | ChaCha20 계속 출력 | 계속 출력 |
성능 벤치마크 비교
# Linux /dev/urandom 처리율 측정
dd if=/dev/urandom of=/dev/null bs=4096 count=262144 2>&1 | tail -1
# 1073741824 bytes (1.1 GB) copied, 0.95 s, 1.1 GB/s ← ChaCha20 per-CPU
# getrandom() 레이턴시 측정 (C 프로그램)
# 32바이트 × 100만 회 → 평균 ~150ns/호출 (x86-64, ChaCha20 SIMD)
# /dev/hwrng 처리율 (하드웨어 의존)
dd if=/dev/hwrng of=/dev/null bs=4096 count=256 2>&1 | tail -1
# Intel RDRAND: ~500 MB/s
# virtio-rng: 호스트 의존 (~100 MB/s)
# USB QRNG: ~1 MB/s
# PCIe QRNG: ~100 MB/s
# TPM RNG: ~1 MB/s
| 인터페이스 | Linux (ChaCha20) | FreeBSD (Fortuna) | OpenBSD (arc4random) | 비고 |
|---|---|---|---|---|
| 커널 내부 (32B) | ~50ns | ~120ns | ~60ns | per-CPU 최적화 효과 |
| 시스콜 (32B) | ~150ns | ~200ns | ~100ns | 시스콜 오버헤드(Overhead) 포함 |
| 벌크 처리율 | ~1.1 GB/s | ~400 MB/s | ~800 MB/s | AVX2/NEON SIMD 활용 |
| 다중 스레드 확장성 | 선형 (per-CPU) | 제한적 (전역 락) | 선형 (per-CPU) | Linux, OpenBSD 우수 |
설계 철학 비교
arc4random()은 항상 비블록킹이며 libc에서 직접 제공합니다.
getentropy()의 최대 크기가 256바이트로 제한됩니다.
관련 문서
- Linux Crypto Framework (Crypto API) — ChaCha20, AES-NI, 커널 암호화 프레임워크 전반
- 커널 보안 — 커널 하드닝, 랜덤화(ASLR, stack canary), 보안 모델
- LSM / Seccomp — getrandom() 시스템 콜 필터링, seccomp 정책
- 커널 모듈 개발 —
struct hwrng기반 드라이버 기초 - 커널 디버깅 — 드라이버 크래시 분석, dmesg 해석
- TPM 2.0 — TPM 난수 생성(tpm_hwrng), TPM 아키텍처, hwrng 프레임워크 연동
외부 참고 자료
- Crypto Engine — Kernel Documentation — hwrng가 연동하는 커널 암호화 엔진 문서입니다
- Linux 커널 hw_random/ 소스 코드 — hwrng 드라이버 커널 소스 디렉토리입니다
- random(4) — Linux man page — /dev/random, /dev/urandom 장치 매뉴얼입니다
- getrandom(2) — Linux man page — getrandom() 시스템 콜 매뉴얼입니다
- NIST SP 800-90A — DRBG — CTR_DRBG, HMAC_DRBG, Hash_DRBG 결정론적 난수 생성기 표준입니다
- NIST SP 800-90B — Entropy Sources — 엔트로피 소스 요구사항 및 검증 표준입니다
- BSI — Random Number Generators — 독일 BSI의 난수 생성기 보안 요구사항입니다
- Intel DRNG Implementation Guide — Intel RDRAND/RDSEED 구현 가이드입니다
- Kernel Keys Documentation — 난수로 생성한 키의 관리에 사용되는 키링 문서입니다
- LWN — The rest of the 5.17 merge window (random) — 커널 /dev/random 서브시스템 대규모 리팩토링 기사입니다
- Jitter Entropy — CPU Jitter RNG — 소프트웨어 기반 엔트로피 수집기 Jitterentropy 프로젝트입니다