하드웨어 난수 생성기 (hwrng / TRNG)
hwrng 경로를 커널 엔트로피 품질과 암호학적 안전성 관점에서 심층 분석합니다. 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)을 만듭니다.
더 쉽게 비유하자면, hwrng 시스템은 제빵소와 같습니다: 하드웨어 난수 생성기(TRNG)는 신선한 재료를 공급하는 농장이고, 엔트로피 풀(input_pool)은 모든 재료를 섞어 보관하는 큰 그릇이며, ChaCha20 CSPRNG는 반죽을 일정한 품질로 빵으로 만드는 제빵기이고, getrandom()는 고객에게 빵을 배달하는 창구입니다.
우리가 매일 사용하는 기술에 난수가 숨어 있습니다:
- 비밀번호와 인증 키 — 예측 가능한 키는 해커가 쉽게 뚫을 수 있습니다. 강력한 난수가 있어야 안전한 키를 만들 수 있습니다.
- HTTPS/TLS 암호화 — 웹사이트 접속 시 매번 새로운 암호화 키를 생성하는데, 이 키가 예측 가능하면 통신 내용이 노출됩니다.
- SSH 접속 — 서버에 원격 접속할 때 생성되는 세션 키도 난수에서 비롯됩니다.
- 게임 — 주사위, 카드 섞기, 아이템 드롭 확률 등 게임의 공정성은 난수 품질에 달려 있습니다.
- 가상화/컨테이너 — VM과 컨테이너의 ASLR(주소 공간 랜덤화)도 커널 난수를 사용합니다.
난수가 약하면 2012년 Debian OpenSSL 버그(CVE-2008-0166)처럼 전체 시스템 보안이 무너질 수 있습니다.
핵심 요약
- TRNG vs CSPRNG: 하드웨어 TRNG는 물리적 엔트로피 원천, CSPRNG는 그것을 씨앗 삼아 대량 난수 생성
- /dev/random vs /dev/urandom: 현대 커널(5.18+)에서 두 인터페이스는 동일하게 동작 (CSPRNG 기반, blocking_pool 제거)
- getrandom() 시스템 콜(System Call): 커널 초기화 완료 전에는 블록, 이후 절대 블록하지 않음
- hwrng 드라이버:
struct hwrng하나만 구현하면 커널 엔트로피 풀에 자동 기여 - quality 파라미터: 하드웨어 품질을 0~1024로 표현 (1024 = 비트당 1비트 엔트로피)
- NIST SP 800-90B: min-entropy 기반 통계 검증으로 hwrng quality 파라미터를 인증, FIPS 140-3 요구사항과 연동
- 외장 hwrng 장치: USB는 Bulk 전송으로 단순 구현하고, PCIe는 MSI 인터럽트(Interrupt)+MMIO 직접 읽기로 더 높은 처리율을 달성
단계별 이해
- 엔트로피 풀과 CSPRNG
엔트로피 풀 메커니즘과 커널 CSPRNG 내부(add_hwgenerator_randomness())를 함께 이해합니다. - hwrng API
struct hwrng인터페이스를 익히고 드라이버 등록 경로를 분석합니다. - hwrng 드라이버 구현
SoC 내장 TRNG, USB/PCIe 외장 RNG, 보안 모듈 RNG 같은 장치가 같은struct hwrng인터페이스로 통합되는 방식을 살펴봅니다. - NIST 인증과 키 생성
NIST SP 800-90B min-entropy 평가와 암호 키 생성 전체 플로우를 연결합니다. - 테스트·검증
dieharder/TestU01 등 통계 테스트와 커널 내부 self-test로 품질을 지속 검증합니다.
초보자 용어집
| 용어 | 쉬운 설명 | 비유 |
|---|---|---|
| 엔트로피 (Entropy) | 무작위성의 양을 나타내는 척도. 높을수록 예측하기 어려움 | 주사위를 굴릴 때 나올 수 있는 경우의 수가 많을수록 엔트로피가 높음 |
| TRNG | 물리적 현상(열잡음, 양자효과)으로 진짜 난수를 만드는 하드웨어 | 진짜 주사위를 굴리는 것 |
| PRNG | 수학 공식으로 난수처럼 보이는 수를 만드는 소프트웨어. 씨앗(seed)이 필요 | 계산기로 주사위 결과를 흉내 내는 것 |
| CSPRNG | 암호학적으로 안전한 PRNG. 과거 출력으로 미래 출력을 예측할 수 없음 | 아무리 빵을 먹어봐도 반죽 비법을 알 수 없는 제빵소 |
| 시드 (Seed) | PRNG/CSPRNG의 시작 값. 엔트로피 풀에서 추출 | 제빵사가 반죽을 시작할 때 넣는 첫 재료 |
| input_pool | 커널의 엔트로피 저장소. 모든 난수 소스가 여기에 섞임 | 모든 재료를 한데 섞어 보관하는 큰 그릇 |
| ChaCha20 | Linux 커널이 사용하는 암호화 알고리즘. CSPRNG의 핵심 엔진 | 일정한 품질로 빵을 찍어내는 제빵기 |
| quality | hwrng 드라이버의 엔트로피 품질 지수 (0~1024). 1024가 최고 품질 | 식재료의 신선도 점수. 1024점이 가장 신선함 |
| forward secrecy | 이전 키가 유출되어도 이전 출력을 재현할 수 없는 보안 속성 | 한 번 구운 빵은 다시 반죽으로 되돌릴 수 없음 |
| getrandom() | 사용자 공간에서 난수를 얻는 권장 시스템 콜. 파일 디스크립터 불필요 | 빵을 주문하는 가장 편리한 방법 |
| hwrng_fillfn() | 커널 스레드가 주기적으로 하드웨어에서 난수를 읽어 엔트로피 풀에 주입 | 정기적으로 농장에서 재료를 수거하는 배달 트럭 |
hwrng 서브시스템 개요
Linux 커널의 hwrng(Hardware Random Number Generator) 서브시스템은
다양한 하드웨어 난수 생성기를 통일된 인터페이스로 추상화합니다.
SoC 내장 TRNG, CPU RNG 명령, TPM 2.0, USB/PCIe 외장 RNG 등 모든 하드웨어 RNG는
동일한 struct hwrng API를 통해 커널 엔트로피 풀에 기여합니다.
아래 다이어그램은 엔트로피 수집부터 난수 생성·사용까지 커널 RNG 전체 파이프라인과
핵심 알고리즘 구현 함수를 한눈에 보여줍니다. hwrng·input_pool(Blake2s)·
ChaCha20 CRNG(출력 풀)·get_random_bytes()·getrandom() 시스템 콜과 vDSO 빠른 경로,
/dev/random·/dev/urandom·/dev/hwrng 장치까지 모든 경로를 총망라합니다.
난수 생성기 분류
| 유형 | 전체 명칭 | 특징 | 예시 |
|---|---|---|---|
| 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를 네트워킹 해시(Hash), 타이머(Timer) 지터 등에 사용했습니다.
이 함수는 Tausworthe 생성기(LCG 변형)를 사용하여 빠르지만 예측 가능했습니다.
Linux 5.19에서 일부 prandom_u32() 호출처가 get_random_u32()로 변경되었으며,
현재 커널에서는 비보안 PRNG를 사용할 수 있는 경로가 거의 없습니다.
이 변경은 커널 내부의 ASLR, 네트워크 시퀀스 번호 등이 PRNG를 통해 예측 공격에 노출되던 문제를 근본적으로 해결했습니다.
rand(), random(), drand48(), Math.random(),
Mersenne Twister, xoshiro 계열은 모두 보안 용도에 부적합합니다.
암호 키, 세션 토큰, 논스(Nonce), ASLR 오프셋(Offset) 등에는 반드시
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],
struct chacha_state *chacha_state,
u8 *random_data, size_t random_data_len)
{
u8 first_block[CHACHA_BLOCK_SIZE]; /* 64바이트 */
/* ChaCha20 state 조립: 상수 + 키 + 카운터(0) + 논스(0) */
chacha_init_consts(chacha_state);
memcpy(&chacha_state->x[4], key, CHACHA_KEY_SIZE);
memset(&chacha_state->x[12], 0, sizeof(u32) * 4);
/* ChaCha20 1블록 생성 (카운터 0에서 시작) */
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, random_data_len);
/* 중간 데이터 즉시 소거 */
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 친화적
* → 약 60초마다 또는 세대 갱신 필요 시 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라운드) | 60초 / 세대 갱신 기반 | getrandom(), /dev/urandom |
| FreeBSD | Fortuna | AES-256-CTR | 32풀 라운드 로빈(Round Robin) | arc4random() |
| macOS/iOS | Fortuna 변형 | AES-256-CTR | 이벤트 기반 | SecRandomCopyBytes() |
| Windows | BCryptGenRandom | AES-256-CTR-DRBG | 프로세스(Process) 단위 | 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)가 권장됩니다.
*/
인증 시스템에 미친 영향: Dual_EC_DRBG를 사용하는 시스템에서 TLS 세션 키, SSH 호스트 키, IPSec SA 키, 인증서 서명 논스 등 모든 암호 자료가 예측 가능해졌습니다. RSA Security가 BSAFE 라이브러리의 기본 DRBG로 Dual_EC_DRBG를 채택한 사례는 이 백도어가 상용 제품에까지 확산되었음을 보여줍니다.
Linux 커널의 대응: Linux 커널은
crypto/drbg.c에
Dual_EC_DRBG를 구현한 적이 없으며, SP 800-90A에서 Dual_EC_DRBG가 제거된 후(2014년)
공식적으로 사용 중단되었습니다. 이 사례는 RNG 설계의 투명성과
독립적 검증 가능성이 인증 보안의 전제 조건임을 보여주는 교훈입니다.
현대 Linux 커널은 CTR-DRBG, Hash-DRBG, HMAC-DRBG 3종만 지원하며,
모든 DRBG의 씨드는 커널 input_pool에서 자동 공급됩니다.
HMAC-DRBG의 특수 용도 — 결정론적 서명:
RFC 6979는 HMAC-DRBG를 사용하여 ECDSA/DSA 서명의 k값(논스)을 결정론적으로 생성합니다.
이 방식은 서명할 때마다 동일한 메시지에 대해 동일한 k값이 나오므로
PRNG 실패로 인한 개인 키 노출 위험을 제거합니다(Sony PS3 ECDSA 사고가 이 취약점(Vulnerability)의 대표 사례입니다).
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의 예측 저항과 동등한 전방 비밀성을 제공합니다.
양자 기반 난수 방식별 비교
| 방식 | 물리 현상 | 처리율 | 대표 제품 | 양자 원리 |
|---|---|---|---|---|
| 빔 분리기 | 광자 편광 불확정성 | 1~10 Mbps | ID Quantique Quantis | 하이젠베르크 불확정성 원리 |
| 진공 요동 | 진공 상태 전자기장 ΔE | 100+ Mbps | QuintessenceLabs qStream | ΔEΔt ≥ ℏ/2 (에너지-시간 불확정성) |
| 광자 도착 시간 (TOAD) | 광자 방출 시각 양자 요동 | 10~100 Mbps | Comscope, Toshiba QKD | 파동-입자 이중성 |
| 방사성 붕괴 TRNG | 핵 붕괴 타이밍 | 낮음 (kbps) | HotBits, Geiger counter | 양자 터널(Tunnel)링 |
| 위상 잡음 | 레이저 위상 자연 선 폭 | 1~10 Gbps | 연구 단계 (대규모) | 자연 선 폭 불확정성 |
커널 내 hwrng 위치 — 아키텍처 다이어그램
QRNG와 양자 기반 TRNG 물리 원리
QRNG(Quantum Random Number Generator)는 양자 역학의 고유한 비결정성(inherent randomness)을 활용하여 원리적으로 예측 불가능한 진난수(True Random Number)를 생성합니다. 고전적 TRNG(열잡음, 지터)는 환경 조건에 따라 편향이 발생할 수 있지만, QRNG는 양자역학 법칙 자체가 비결정성을 보장하므로 어떤 물리적 모델로도 예측할 수 없습니다. 상용 QRNG는 주로 광자·진공 요동·레이저 위상 잡음 같은 광학적 양자 현상을 엔트로피 원천으로 사용합니다. 방사성 붕괴도 양자역학적 비결정성을 이용하는 진난수 원천이지만, 일반적으로 광학 기반 QRNG 제품군보다는 별도의 양자 기반 TRNG 또는 연구용 난수 소스로 구분하는 편이 정확합니다.
빔 분리기(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)가 이 요동의 하한을 보장합니다.
진공 요동 방식의 핵심 장점은 높은 처리량(Throughput)입니다. 광대역 호모다인 검출기는 GHz 대역폭(Bandwidth)을 가질 수 있어 100 Mbps 이상의 난수 생성이 가능합니다. QuintessenceLabs의 qStream은 이 방식으로 1 Gbps 이상의 출력을 달성하며, 서버 랙 마운트(Mount) 형태로 데이터센터 환경에 적합합니다.
광자 도착 시간(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는 지연(Latency) 간섭계(Delay-Line Interferometer)를 통해 위상 잡음을 진폭 변동으로 변환합니다. 두 팔 사이의 위상 차이가 비결정적이므로 간섭계 출력 광강도의 변동도 비결정적이며, 이를 고속 포토디텍터와 ADC로 디지털화합니다. 이 방식의 최대 장점은 극한의 처리량입니다. 광통신용 변조기(Modulator)와 동일한 대역폭을 활용할 수 있어 이론적으로 10 Gbps 이상의 난수 생성이 가능합니다. Toshiba와 Quside가 이 방식의 상용화를 선도하고 있습니다.
방사성 붕괴(Radioactive Decay) 기반 TRNG
방사성 붕괴는 가장 오래된 양자 난수 원천으로, 불안정한 원자핵이 알파 입자나 베타 입자를 방출하는 시점이 양자 터널링(Quantum Tunneling)에 의해 결정됩니다. 가이거-뮐러(Geiger-Müller) 계수관이 개별 붕괴 이벤트를 검출하며, 연속된 이벤트 사이의 시간 간격을 비트로 변환합니다.
이 방식은 양자역학의 비결정성이 명확하게 드러나는 원리이지만, 광학 QRNG 제품군과 달리 방사성 동위원소와 계수관을 쓰는 양자 기반 TRNG로 보는 것이 더 정확합니다. 붕괴율이 물리적으로 제한되어 처리량이 kbps 수준으로 매우 낮습니다. 또한 방사성 물질의 취급에 규제가 따르므로 상용 제품보다는 연구용으로 주로 활용됩니다. 대표적으로 Hotbits 프로젝트는 세슘-137 감마선 소스를 사용하여 인터넷을 통해 양자 난수를 제공합니다.
QRNG 및 양자 기반 TRNG 방식별 종합 비교
| 방식 | 양자 현상 | 불확정성 관계 | 처리량 | 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 |
| 방사성 붕괴 TRNG | 양자 터널링 | 핵 불안정성 | ~kbps | 0.99+ | 연구용 (QRNG와 구분) | Hotbits |
struct hwrng 인터페이스를 통해 등록됩니다.
quality 파라미터에 장치의 min-entropy 수준을 반영하면
커널이 엔트로피 크레딧을 적절히 계산하여 input_pool에 기여합니다.
엔트로피 풀 메커니즘
커널 엔트로피 풀은 drivers/char/random.c에 구현된 핵심 난수 인프라입니다.
Linux 5.17~5.18에서 Jason Donenfeld(WireGuard 저자)의 대규모 재작성을 통해
기존의 LFSR(Linear Feedback Shift Register) 기반 SHA-1 믹싱 구조가(SHA-1→Blake2s 전환: 5.17)
단일 input_pool + Blake2s 해시 기반 256비트 상태로 전면 교체되었습니다(구조 재작성: 5.18).
이 변경으로 코드량이 절반 이하로 줄었고, 암호학적 안전성이 크게 향상되었습니다.
현재 구조에서는 모든 엔트로피 소스(hwrng, 인터럽트 지터, 디스크 I/O 등)가
단일 풀에 혼합된 후 ChaCha20-CRNG로 추출되는 단순하고 검증 가능한 파이프라인(Pipeline)을 형성합니다.
/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, bool sleep_after)
{
_mix_pool_bytes(buf, len); /* input_pool에 혼합 */
credit_init_bits(entropy); /* 엔트로피 추정값 증가 */
wake_up_interruptible(&random_wait); /* 대기 중인 getrandom() 깨움 */
if (sleep_after && !kthread_should_stop() &&
(crng_ready() || !entropy))
schedule_timeout_interruptible(crng_reseed_interval());
}
/* 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 완료 타이밍 */
getrandom() 시스템 콜에 직접 데이터를 제공하지 않습니다.
hwrng 은 오직 엔트로피 풀에 시드 (Seed) 를 주입하는 역할만 하며,
실제 난수 생성은 ChaCha20-CRNG 가 담당합니다.
CRNG 는 약 60초마다 재시드되며 (CRNG_RESEED_INTERVAL = 60 * HZ),
hwrng_fillfn 스레드는 데이터 읽기 후 add_hwgenerator_randomness(..., true) 를 호출하며,
CRNG 초기화 이후 또는 엔트로피를 credit 하지 않는 경우에는 재시드 간격만큼 throttle sleep 을 수행합니다.
msleep(500) 은 읽기 오류 경로에서 사용합니다.
커널 내부에서 hwrng 의 read() 콜백을 직접 호출하는 함수는 없으며,
모든 커널 코드는 get_random_bytes(), getrandom(), /dev/urandom 을 사용해야 합니다.
코드 설명
-
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: 현대 커널(5.18+)에서는 no-op (레거시 호환용) */
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,
true);
}
hwrng_put(rng);
return 0;
}
| quality 값 | 의미 | 4096바이트 읽기 시 기여 엔트로피 | CRNG 초기화(256비트) 위한 읽기 횟수 |
|---|---|---|---|
| 1024 | 이론적 최대 (검증된 고품질 엔트로피 소스) | 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 비트 | ∞ (초기화 불가) |
hwrng 엔트로피 공급 주기
hwrng_fillfn() 커널 스레드는 읽기 성공 시 add_hwgenerator_randomness(..., true) 를 호출합니다.
현대 커널에서는 CRNG 초기화 이후 또는 엔트로피 credit 이 없는 경우 crng_reseed_interval() 만큼 throttle 되므로,
실제 공급 주기는 하드웨어 처리율과 커널 재시드 정책의 영향을 함께 받습니다:
- Intel RDRAND: 마이크로초 (μs) 단위 (~1 GB/s)
- PCIe hwrng 장치: 마이크로초 (μs) 단위 (~100 MB/s)
- USB hwrng 장치: 밀리초 (ms) 단위 (~1 MB/s)
- BCM2835 (라즈베리파이): 밀리초 (ms) 단위 (~200 KB/s)
이와 별개로 CRNG 는 약 60초마다 input_pool 에서 재시드됩니다 (CRNG_RESEED_INTERVAL = 60 * HZ).
credit 메커니즘 심층 분석 — mix와 credit의 분리
현대 리눅스 커널 RNG 설계에서 가장 중요한 개념 중 하나는 데이터 혼합(mix)과 엔트로피 크레딧(credit)이 서로 분리되어 있다는 점입니다. 모든 엔트로피 소스는 input_pool에 바이트를 섞을 수 있지만, 그 바이트를 "얼마만큼의 엔트로피로 신뢰할 것인가"를 선언하는 credit 동작은 별도로 수행됩니다. 이 분리가 없다면 신뢰할 수 없는 소스(예: 조작된 가상 RNG)가 input_pool을 가득 채워 CRNG를 거짓으로 "준비됨(ready)" 상태로 만들 수 있습니다.
mix와 credit의 역할 분담
| 구분 | mix (혼합) | credit (크레딧) |
|---|---|---|
| 담당 함수 | mix_pool_bytes() / _mix_pool_bytes() |
credit_init_bits() (매크로) / _credit_init_bits() (구현) |
| 수행 작업 | 원시 바이트를 Blake2s 해시 상태에 갱신 | input_pool.init_bits 카운터에 엔트로피 비트 누적 |
| 신뢰 여부 | 무조건 수행 — 신뢰도와 무관 | 선택적 — 소스의 quality에 따라 부여 비트 결정 |
| 보안 효과 | 풀 상태 변경 (taint 주입) | CRNG 초기화/재시드 트리거 조건 갱신 |
| credit 없이 mix만 수행하는 경우 | quality=0인 hwrng, add_device_randomness() (MAC 주소 등 비-엔트로피 데이터), RDRAND의 제한적 credit 경로 — 데이터는 풀에 영향을 주지만 init_bits는 증가하지 않음 |
|
quality를 1024로
위조하더라도, 해당 소스가 실제로 저품질이라면 다른 소스(인터럽트 지터, jitterentropy)의
credit이 동시에 누적되어 있으므로 전체 init_bits는 과대 평가되지 않습니다.
반대로 quality=0으로 설정된 소스는 데이터를 무한히 섞을 수 있지만
init_bits에 전혀 기여하지 않으므로 CRNG 초기화를 가속하지 못합니다.
이것이 "데이터는 무조건 섞되, 신뢰는 보수적으로"라는 커널 RNG의 설계 원칙입니다.
credit_init_bits() — 초기화 단계 크레딧
credit_init_bits()는 커널 RNG의 유일한 엔트로피 크레딧 함수입니다.
이름은 함수처럼 보이지만 실제로는 매크로이며, crng_ready()가 false일 때만
_credit_init_bits() 구현 함수를 호출합니다 — CRNG 초기화 완료 후에는 크레딧이 no-op가 됩니다.
_credit_init_bits()는 input_pool.init_bits 카운터를 lock-free
try_cmpxchg로 원자적 갱신하며, 두 단계 임계치를 거쳐 CRNG를 초기화합니다:
128비트 도달 시 CRNG_EARLY (조기 단계), 256비트 도달 시 CRNG_READY (완전 준비) 전환.
/* drivers/char/random.c — 엔트로피 크레딧 (유일한 크레딧 경로) */
#define POOL_BITS BLAKE2S_HASH_SIZE * 8 /* 256 */
#define POOL_READY_BITS POOL_BITS /* 256 — CRNG_READY 전환 */
#define POOL_EARLY_BITS POOL_READY_BITS / 2 /* 128 — CRNG_EARLY 전환 */
/* 매크로: CRNG가 이미 준비된 상태면 크레딧 자체를 수행하지 않음
* 런타임 재시드는 60초 주기 delayed_work (crng_reseed)가 독립 처리 */
#define credit_init_bits(bits) if (!crng_ready()) _credit_init_bits(bits)
static void _credit_init_bits(size_t bits)
{
unsigned int new, orig, add;
if (!bits)
return; /* 0비트 credit — 의미 없음 */
add = min_t(size_t, bits, POOL_BITS);
/* lock-free 원자적 갱신: try_cmpxchg로 경쟁 없이 init_bits 클램프 누적
* spin_lock이 아닌 CAS(Compare-And-Swap) 사용 — 경합 최소화 */
orig = READ_ONCE(input_pool.init_bits);
do {
new = min_t(unsigned int, POOL_BITS, orig + add);
} while (!try_cmpxchg(&input_pool.init_bits, &orig, new));
/* 임계치 1: 128비트 도달 — CRNG_EARLY (조기 단계)
* input_pool에서 즉시 키를 추출하여 기본 CRNG에 주입
* 아직 getrandom()은 블록하지만 커널 내부 get_random_bytes() 사용 가능 */
if (orig < POOL_EARLY_BITS && new >= POOL_EARLY_BITS) {
if (crng_init == CRNG_EMPTY) {
extract_entropy(base_crng.key, sizeof(base_crng.key));
crng_init = CRNG_EARLY;
}
}
/* 임계치 2: 256비트 도달 — CRNG_READY (완전 준비)
* crng_reseed()가 input_pool에서 최종 시드 추출 → base_crng.key 갱신
* 이후 getrandom() 블로킹 해제, 대기 프로세스 웨이크업, vDSO 갱신 */
if (orig < POOL_READY_BITS && new >= POOL_READY_BITS) {
crng_reseed(NULL); /* CRNG 키를 input_pool에서 추출하여 갱신 */
queue_work(system_dfl_wq, &set_ready);
wake_up_interruptible(&crng_init_wait);
pr_notice("crng init done\n");
}
}
코드 설명
- POOL_READY_BITS = 256 CRNG의 ChaCha20 키가 256비트이므로, 초기화에 필요한 엔트로피도 256비트입니다. 이 값은 NIST SP 800-90A 권장 시드 길이와 일치합니다.
- try_cmpxchg(&input_pool.init_bits, &orig, new) lock-free CAS(Compare-And-Swap)로 원자적 갱신합니다. spin_lock 대신 try_cmpxchg를 사용하여 경합을 최소화합니다. CAS가 실패하면 orig가 새 값으로 갱신되어 루프가 재시도하므로, 여러 소스가 동시에 credit을 부여해도 안전하게 누적됩니다.
- POOL_EARLY_BITS = 128 (조기 단계) 엔트로피가 128비트(256의 절반)에 도달하면 CRNG_EARLY 상태로 전환합니다. input_pool에서 즉시 키를 추출하여 base_crng.key에 주입하며, 이후 커널 내부 get_random_bytes()를 사용할 수 있습니다. 단 getrandom() 시스템 콜은 여전히 블록합니다.
- orig < POOL_READY_BITS && new >= POOL_READY_BITS "이전에는 미달, 이제는 달성"인 경계 통과 순간을 정확히 1회 감지합니다. try_cmpxchg의 원자성으로 인해 경쟁 조건(Race Condition)에서도 중복 트리거가 발생하지 않습니다.
- crng_reseed() input_pool에서 extract_entropy()로 32바이트(256비트) 시드를 추출하여 base_crng.key를 갱신합니다. 이 호출 이후 crng_ready()가 true를 반환하고 getrandom() 블로킹이 해제됩니다.
단일 크레딧 함수 — credit_init_bits()만 존재
현대 리눅스 커널(5.18+)에는 credit_entropy_bits() 같은 별도의 런타임 크레딧 함수가
존재하지 않습니다. 모든 엔트로피 소스 — hwrng kthread, 인터럽트 경로, 디바이스/디스크 이벤트,
부트로더 시드 — 가 동일한 credit_init_bits() 매크로 하나를 공통으로 호출합니다.
이 매크로의 핵심 설계는 if (!crng_ready()) 가드입니다. CRNG가 한 번 CRNG_READY 상태에
도달하면, 이후의 모든 credit_init_bits() 호출은 no-op가 됩니다 — 런타임 재시드는
credit_init_bits()가 아니라 60초 주기의 delayed_work 타이머가 독립적으로
crng_reseed()를 호출하여 처리합니다. 이 설계로 인해 크레딧 경로와 재시드 경로가
명확히 분리됩니다.
credit_init_bits()— hwrng, 인터럽트, 디바이스, 디스크, 부트로더 등 모든 소스가 공통 호출. 별도의 런타임 전용 함수 없음.- 매크로 가드
if (!crng_ready())— CRNG 초기화 완료 후에는 크레딧이 no-op. 런타임 재시드는 60초 주기delayed_work가 독립 처리. - 두 단계 임계치 — 128비트 도달 시
CRNG_EARLY(조기 단계, 커널 내부 사용 가능), 256비트 도달 시CRNG_READY(완전 준비,getrandom()블로킹 해제).
quality → credit 변환 공식 상세
hwrng 코어는 드라이버의 quality 파라미터(0~1024)와 읽은 바이트 수를 곱하여
credit할 엔트로피 비트 수를 계산합니다. 핵심 변환 공식은 다음과 같습니다.
/* drivers/char/hw_random/core.c — quality → credit 변환 */
/*
* entropy_bits = bytes_read × quality × 8 / 1024
*
* 풀어서 설명:
* bytes_read — hwrng read() 콜백이 반환한 바이트 수
* × 8 — 바이트를 비트로 변환 (1바이트 = 8비트)
* × quality — 비트당 엔트로피 밀도 (0~1024 스케일)
* / 1024 — quality 스케일 정규화 (1024 = 1.0 비트/비트)
*
* 비트당 엔트로피 밀도 = quality / 1024
* quality=1024 → 1.0 비트/비트 (이론적 최대, 완벽한 TRNG)
* quality=512 → 0.5 비트/비트 (50% 효율)
* quality=0 → 0.0 비트/비트 (엔트로피 없음, PRNG 출력)
*/
size_t hwrng_entropy_bits(const struct hwrng *rng, size_t bytes_read)
{
/* (rc * rng->quality * 8) >> 10
* >> 10 은 / 1024 와 동일 (비트 시프트로 정수 연산) */
return (bytes_read * rng->quality * 8) >> 10;
}
/* hwrng_fillfn()에서의 실제 호출 */
size_t entropy = hwrng_entropy_bits(rng, rc);
add_hwgenerator_randomness(rng_buffer, rc, entropy, true);
/* 내부적으로:
* mix_pool_bytes(rng_buffer, rc) ← 데이터 섞기
* credit_init_bits(entropy) ← 엔트로피 크레딧
* wake_up_interruptible(&random_wait) ← getrandom() 대기자 깨우기 */
| quality | 비트당 엔트로피 | 읽기 32바이트(256비트) | 읽기 512바이트 | 읽기 4096바이트 | CRNG 초기화(256비트) 필요 바이트 |
|---|---|---|---|---|---|
| 1024 | 1.0 비트/비트 | 256 비트 (1회 충분) | 4,096 비트 | 32,768 비트 | 32바이트 |
| 1000 | 0.976 비트/비트 | 250 비트 | 3,906 비트 | 31,250 비트 | 33바이트 |
| 512 | 0.5 비트/비트 | 128 비트 | 2,048 비트 | 16,384 비트 | 64바이트 |
| 256 | 0.25 비트/비트 | 64 비트 | 1,024 비트 | 8,192 비트 | 128바이트 |
| 128 | 0.125 비트/비트 | 32 비트 | 512 비트 | 4,096 비트 | 256바이트 |
| 32 | 0.031 비트/비트 | 8 비트 | 128 비트 | 1,024 비트 | 1,024바이트 |
| 0 | 0 비트/비트 | 0 비트 | 0 비트 | 0 비트 | ∞ (초기화 불가) |
(bytes_read * quality * 8) >> 10에서
>> 10은 / 1024와 동일한 정수 나눗셈입니다. 예를 들어
bytes_read=32, quality=512이면 (32 × 512 × 8) >> 10 = 131072 >> 10 = 128비트가
credit됩니다. 정수 연산이므로 소수점 이하 버림이 발생하지만, 누적 credit이 256에 도달하면
CRNG가 초기화되므로 실무적 영향은 없습니다.
소스별 credit 정책 비교
커널은 각 엔트로피 소스에 대해 서로 다른 credit 정책을 적용합니다. 이 정책 차이가 "보수적 크레딧(conservative credit)" 철학의 핵심입니다.
| 소스 | credit 함수 | credit 크기 기준 | 보수적 정책 | 이유 |
|---|---|---|---|---|
| hwrng (TPM/PCIe/virtio-rng) | credit_init_bits() |
bytes × quality × 8 / 1024 |
quality 기반 전량 credit | 드라이버 개발자가 quality를 선언하면 커널은 이를 신뢰 — 단, 다중 소스 혼합으로 단일 소스 결함 방어 |
| RDSEED (x86 온다이 TRNG) | credit_init_bits() |
전체 비트 (원시 엔트로피) | 전량 credit — 물리적 TRNG이므로 신뢰 | RDSEED는 하드웨어 TRNG 출력이므로 DRBG 재시드가 아닌 원시 엔트로피로 취급 |
| RDRAND (x86 온다이 DRBG) | credit_init_bits() |
제한적 credit | 보수적 — 단독으로 init_bits 256 달성 불가 | RDRAND는 내부 DRBG 출력이므로 원시 엔트로피가 아님. random.trust_cpu=1 부트 파라미터로 전량 credit 정책 전환 가능 |
| ARM RNDR/RNDRRS | credit_init_bits() |
전체 비트 (원시 엔트로피) | 전량 credit — RDSEED와 동급 | ARM 아키텍처 RNG는 원시 엔트로피 소스로 분류 |
| 인터럽트 타이밍 지터 | credit_init_bits() |
max(1u, ...) — 최소 1비트 |
매우 보수적 — 64회 인터럽트마다 최소 1비트 | 인터럽트 타이밍의 실제 엔트로피는 환경에 따라 변동이 크므로 최소값만 보장 |
| jitterentropy | credit_init_bits() |
측정 기반 추정치 | 보수적 — 측정값의 일부만 credit | CPU 지터의 엔트로피 밀도는 하드웨어에 따라 불확실하므로 추정치를 할인 |
| add_device_randomness() | credit_init_bits() |
0 (credit 없음) | credit 없음 — mix만 수행 | MAC 주소, 시리얼 번호 등 고정값은 엔트로피가 아닌 고유값(uniqueness)만 제공 |
quality파라미터는 드라이버 개발자가 수동으로 설정하는 선언값(declaration)이지, NIST SP 800-90B 검증을 거친 실제 엔트로피 품질이 아닙니다.quality=1024로 설정하면 커널은 읽은 바이트 수만큼의 비트를 전량 credit하지만, 이것이 "비트당 1.0비트 엔트로피"를 보장하는 것이 아닙니다.- 커널의 다중 소스 혼합(Blake2s)은 단일 소스의 과장된 credit이 전체 시스템 안전성을 훼손하지 않도록 방어합니다. RDRAND가 Intel에 의해 조작되더라도 인터럽트 지터와 혼합되면 출력은 여전히 예측 불가능합니다.
- 올바른 접근: NIST SP 800-90B min-entropy 측정 후
quality = H∞ × 1024로 설정하고, 검증 없이 1024를 단정하지 않기.
quality → credit 변환 파이프라인
다음 다이어그램은 hwrng 드라이버의 read() 콜백부터 init_bits 누적까지의
전체 파이프라인을 보여줍니다. 각 단계에서 수행되는 연산과 데이터 변환을 추적할 수 있습니다.
보수적 credit 철학과 random.trust_cpu
커널은 RDRAND와 같은 CPU 내장 DRBG 출력에 대해 기본적으로 보수적 credit 정책을
적용합니다. RDRAND는 하드웨어 내부의 DRBG(결정론적 PRNG) 출력이므로, 원시 물리 엔트로피가
아닙니다. 따라서 커널은 RDRAND 출력을 input_pool에 섞되(mix), 단독으로 init_bits 256에
도달할 수 있는 전량 credit는 부여하지 않습니다.
이 정책은 random.trust_cpu 부트 파라미터로 변경할 수 있습니다:
| 부트 파라미터 | credit 정책 | CRNG 초기화 시간 | 보안 가정 |
|---|---|---|---|
random.trust_cpu=0 (기본값) |
RDRAND 보수적 credit — 단독 256 달성 불가 | 수초~수십초 (인터럽트/hwrng 대기) | CPU RNG가 신뢰할 수 없을 수 있음 (공급망 공격, CPU 버그) |
random.trust_cpu=1 |
RDRAND 전량 credit — 즉시 256 달성 가능 | < 100ms (즉시 CRNG ready) | CPU RNG를 신뢰 — 부팅 속도 우선 (클라우드/가상화 환경) |
random.trust_bootloader=1 |
부트로더(Bootloader)가 전달한 RNG 시드 전량 credit | 즉시 (부트로더 시드가 충분하면) | UEFI/GRUB가 제공한 시드를 신뢰 — 컨테이너/IoT 부팅 가속 |
random.trust_cpu=1 없이도
빠른 CRNG 초기화가 가능합니다. 반면 베어메탈 서버에서 CPU 신뢰성이 의심되는 환경
(공급망 보안(Supply Chain Security) 우려)에서는 기본값을 유지하는 것이 안전합니다.
random.trust_cpu 설정 여부는 다음 명령으로 확인할 수 있습니다:
cat /proc/cmdline | grep -o 'random.trust_cpu=[0-9]'
# 출력이 없으면 기본값(0) 적용 중
credit 관측 — ftrace와 /proc
credit 동작은 런타임에 ftrace tracepoint와 /proc/sys/kernel/random/ 인터페이스로
관측할 수 있습니다. 운영 중 credit이 어떤 소스에서 얼만큼 부여되고 있는지 추적하면
엔트로피 기반 보안 진단에 직접 활용할 수 있습니다.
# 1. kprobe: _credit_init_bits 추적 (5.18+에서 tracepoint 제거됨)
# 구버전(5.17 이전)에서는 events/random/credit_entropy_bits tracepoint 사용
cd /sys/kernel/tracing
echo 'p:cred _credit_init_bits bits=%x0:u64' > kprobe_events
echo 1 > events/kprobes/cred/enable
echo 1 > tracing_on
# credit 추적 결과 — 각 소스가 부여한 엔트로피 비트 수 확인
cat trace_pipe | head -30
# 출력 예:
# hwrng-123 cred: (_credit_init_bits+0x0/0x120) bits=256
# irq/24-eth0 cred: (_credit_init_bits+0x0/0x120) bits=1
# kworker-45 cred: (_credit_init_bits+0x0/0x120) bits=12
# 2. /proc: 현재 엔트로피 가용량 확인 (init_bits 기반)
cat /proc/sys/kernel/random/entropy_avail
# 출력 예: 256 (CRNG 초기화 완료, 충분한 엔트로피)
# 0~256 범위 — 256이면 CRNG ready
# 3. bpftrace: 소스별 credit 비트 수 집계
cat > /tmp/credit_trace.bt << 'EOF'
kprobe:_credit_init_bits
{
@credit_bits = hist(arg0);
@credit_count = count();
}
EOF
bpftrace /tmp/credit_trace.bt
# Ctrl+C 후 출력:
# @credit_bits:
# [0, 1] 8472 | 인터럽트 (max(1u,...))
# [16, 32) 132 | jitterentropy
# [128, 256) 15 | RDSEED
# [256, 512) 3 | hwrng (quality=1024, 32바이트 읽기)
- 어떤 소스가 CRNG 초기화에 기여했는가 —
_credit_init_bitskprobe의 프로세스 이름으로 판별 (hwrng-*,irq/*,kworker등). 5.18+에서는 random 서브시스템 tracepoint가 제거되었으므로 kprobe를 사용해야 합니다. - quality가 과대 설정되었는가 —
quality=1024인데_credit_init_bits: bits=256이 한 번에 나오면 32바이트만 읽어도 즉시 CRNG가 초기화됨. 이 시간이 비정상적으로 빠르면 quality 값을 재검토. - 엔트로피 기아(entropy starvation) 여부 —
entropy_avail이 장시간 0에 머물면 credit 소스가 부족하거나 hwrng 드라이버가 로드되지 않았을 수 있음. - 부팅 시간과 CRNG ready 시점의 상관관계 —
dmesg | grep "random: crng init done"으로 CRNG 초기화 완료 시각을 확인하고, 그 시각까지의 credit tracepoint 패턴을 분석.
DRBG vs CRNG vs CSRNG vs CSPRNG 용어 정리
암호학적 난수 생성기 분야에서는 DRBG, CRNG, CSRNG, CSPRNG 네 용어가 혼용되어 사용됩니다. 이 용어들은 대부분 같은 대상을 가리키지만, 정의한 기관, 결정론적(Deterministic) vs 비결정론적(Non-deterministic) 구분, 그리고 "의사(Pseudo)"라는 수식어 포함 여부에 따라 미묘한 차이가 있습니다. 정확한 용어 이해는 표준 문서(NIST SP 800-90, RFC 4086, FIPS 140)를 읽거나 커널 소스에서 RNG 관련 코드를 추적할 때 필수적입니다.
각 용어의 정의
DRBG (Deterministic Random Bit Generator)
DRBG(결정론적 난수 비트 생성기)는 NIST SP 800-90A에서 정의한 표준 용어입니다. "결정론적(Deterministic)"이라는 수식어가 붙은 이유는, 동일한 시드(Seed)를 입력하면 항상 동일한 출력 시퀀스를 생성하기 때문입니다. 즉, DRBG 자체는 내부에 물리적 엔트로피 소스를 가지지 않으며, 외부에서 주입된 엔트로피(시드)를 기반으로 결정론적 알고리즘을 통해 난수 비트 스트림을 확장(extend)합니다. NIST SP 800-90A는 세 가지 승인된 DRBG 메커니즘을 정의합니다:
- Hash-DRBG — SHA-2/SHA-3 해시 함수 기반 (SP 800-90A §10.1.1)
- HMAC-DRBG — HMAC-SHA-2 기반 (SP 800-90A §10.1.2)
- CTR-DRBG — AES-CTR 모드 기반 (SP 800-90A §10.2.1)
Linux 커널의 ChaCha20 CRNG는 NIST 승인 DRBG 중 하나는 아니지만, 구조적으로 DRBG와 동일한 패턴(엔트로피 시드 → 결정론적 확장 → 주기적 재시드)을 따릅니다. 정확히는 NIST SP 800-90A/C/D 전체 프레임워크를 따르지 않고 독자 설계를 유지하지만, 보안 속성(예측 불가능성, 역추적 방지)은 DRBG 요구사항을 만족합니다.
CSPRNG (Cryptographically Secure Pseudo Random Number Generator)
CSPRNG(암호학적으로 안전한 의사난수 생성기)는 학술적으로 가장 널리 사용되는 용어로, "Pseudo(의사)"라는 수식어가 핵심입니다. "의사"라는 말은 출력이 결정론적 알고리즘의 산물임을 명시하며, 진정한 물리적 무작위성이 아니라 알고리즘에 의해 확장된 난수라는 뜻입니다. CSPRNG가 "암호학적으로 안전(Cryptographically Secure)"하려면 다음 두 가지 속성을 만족해야 합니다:
- 다음 비트 예측 불가능성(Next-bit unpredictability) — 지금까지 출력된 비트 시퀀스를 알아도 다음 비트를 1/2 확률 이상으로 예측할 수 없어야 합니다.
- 역추적 불가능성(Backtracking resistance / Forward secrecy) — 현재 내부 상태가 노출되더라도 과거에 생성된 난수를 역산할 수 없어야 합니다.
DRBG는 NIST 표준 관점에서 CSPRNG의 구체적 구현 명세로 볼 수 있습니다. 즉, 모든 DRBG는 CSPRNG의 요구사항을 만족하지만, 모든 CSPRNG가 NIST 승인 DRBG인 것은 아닙니다 (Linux ChaCha20 CRNG가 그 예입니다).
CRNG (Cryptographic Random Number Generator)
CRNG(암호학적 난수 생성기)는 "Pseudo" 수식어가 생략된 용어입니다. 이 생략은 의도적일 수도 있고 단순한 관행일 수도 있습니다:
- 좁은 의미: CSPRNG와 동의어로 사용. "Cryptographic"이 이미 "의사난수 중 암호학적으로 안전한 것"을 함의한다고 보는 관행.
- 넓은 의미: 결정론적 의사난수 생성기뿐 아니라 물리적 TRNG(진난수 생성기)를 포함한 모든 암호학적 난수 생성 장치를 통칭. 이 경우 CSPRNG보다 상위 개념이 됩니다.
Linux 커널 소스에서는 struct crng, crng_reseed(), crng_fast_key_extraction() 등
CRNG라는 명칭을 일관되게 사용합니다. 이는 위 "좁은 의미"에 해당하며, ChaCha20 기반 CSPRNG 구현체를 지칭합니다.
커널 문서(Documentation/crypto/drbg.rst)에서는 NIST DRBG를 별도로 다루고, drivers/char/random.c의 CRNG는 독자 설계로 분류합니다.
CSRNG (Cryptographically Secure Random Number Generator)
CSRNG(암호학적으로 안전한 난수 생성기)는 CSPRNG에서 "Pseudo"를 제거한 형태입니다. "Pseudo"가 없다는 것은 결정론적 의사난수에 국한하지 않고, 비결정론적 TRNG도 포함한다는 의미입니다. 즉, CSRNG는 가장 넓은 의미의 용어로, 다음을 모두 포함합니다:
- 물리적 엔트로피 소스 기반 TRNG (비결정론적)
- TRNG 시드를 받아 결정론적 확장을 수행하는 DRBG/CSPRNG (결정론적)
- TRNG + DRBG 하이브리드 구조 (NIST SP 800-90C에서 정의하는 "Root DRBG + Prediction-Resistant DRBG" 결합 형태)
CSRNG라는 명칭은 FIPS 140-3과 Common Criteria(CC) 인증 문서에서 전체 난수 생성 시스템을 지칭할 때 자주 사용됩니다. 반면 학술 논문이나 일반 프로그래밍 문서에서는 CSPRNG가 더 흔하게 쓰이며, CSRNG는 상대적으로 덜 보편적입니다.
용어 비교 표
| 구분 기준 | DRBG | CSPRNG | CRNG | CSRNG |
|---|---|---|---|---|
| 정의 기관 | NIST SP 800-90A | 학계 / 산업계 일반 | 구현체 / 커널 명명 | 인증 표준 (FIPS, CC) |
| "Pseudo" 포함 | 포함 (결정론적) | 포함 (결정론적) | 생략 (문맥 의존) | 생략 (TRNG 포함) |
| 비결정론적 TRNG 포함 | 제외 (시드만 받음) | 제외 (의사난수만) | 문맥에 따라 포함 | 포함 |
| 결정론적 PRNG 포함 | 포함 (핵심 대상) | 포함 (핵심 대상) | 포함 | 포함 |
| 표준 명세 존재 | 있음 (SP 800-90A) | 느슨한 정의만 | 없음 (구현체 명칭) | 있음 (FIPS 140-3) |
| Linux 커널 사용 | crypto/drbg.c (별도 모듈) |
문서/주석에서 사용 | struct crng (주 명칭) |
거의 미사용 |
| 관계 | CSPRNG의 NIST 표준화 버전 | DRBG의 상위(일반) 개념 | CSPRNG의 구현체 명칭 | TRNG + CSPRNG 모두 포함 |
개념 계층 구조
네 용어의 포함 관계를 벤다이어그램으로 나타내면 다음과 같습니다. 가장 바깥쪽이 CSRNG(가장 넓은 의미), 가장 안쪽이 DRBG(가장 구체적인 표준 명세)입니다:
Linux 커널에서의 매핑
Linux 커널에서 이 네 용어가 실제 코드에 어떻게 대응되는지 정리합니다:
| 용어 | 커널 코드 위치 | 역할 |
|---|---|---|
| CSRNG (전체 시스템) | drivers/char/random.c 전체 |
엔트로피 수집(input_pool) + CRNG + hwrng 통합 시스템 |
| TRNG (엔트로피 소스) | drivers/char/hw_random/, arch/x86/kernel/cpu/ (RDRAND/RDSEED) |
물리적 난수 → input_pool 주입. CSPRNG 자체는 아님 |
| CSPRNG / CRNG | drivers/char/random.c 내 struct crng, crng_reseed() |
ChaCha20 기반 per-CPU CRNG. 커널의 주 난수 생성기 |
| DRBG (NIST 표준) | crypto/drbg.c |
crypto API용 NIST SP 800-90A DRBG. /dev/random 경로와는 별개 |
crypto/drbg.c의 NIST DRBG와 drivers/char/random.c의 CRNG는 서로 다른 코드 경로입니다.
일반적인 getrandom(2) 시스템 콜이나 /dev/urandom 읽기는 CRNG(ChaCha20)를 사용하며,
crypto/drbg.c는 crypto/ 프레임워크를 통해 DRBG 알고리즘을 명시적으로 요청하는 경우(tls, ipsec 등)에만 사용됩니다.
두 경로는 모두 CSPRNG의 요구사항을 만족하지만, 구현체와 인증 상태가 다릅니다.
핵심 요약
- DRBG — NIST SP 800-90A 표준 명세. "결정론적"이라는 수식어가 핵심. 세 가지 승인 메커니즘(Hash, HMAC, CTR)이 있음.
- CSPRNG — 학술적 일반 용어. "Pseudo"가 포함되어 결정론적 의사난수만 지칭. DRBG는 CSPRNG의 NIST 표준화 버전.
- CRNG — "Pseudo"가 생략된 명칭. Linux 커널에서는 ChaCha20 CSPRNG 구현체의 공식 명칭으로 사용. 문맥에 따라 TRNG까지 포함하는 넓은 의미로도 쓰임.
- CSRNG — "Pseudo"가 제거된 가장 넓은 의미. 결정론적 CSPRNG와 비결정론적 TRNG를 모두 포함. FIPS 140-3, Common Criteria 인증 문서에서 전체 시스템을 지칭.
- 포함 관계:
DRBG ⊂ CSPRNG ⊂ CSRNG,CRNG ≈ CSPRNG(구현체 명칭) 또는CRNG ⊂ CSRNG(넓은 의미). - 실무 팁: 표준 문서를 읽을 때는 DRBG, 일반 기술 문서에서는 CSPRNG, 커널 소스에서는 CRNG라는 명칭을 접하게 됩니다. 세 용어가 동일한 보안 속성을 논의하고 있을 가능성이 높으니, 문맥에서 "어떤 구현체를 가리키는가"를 확인하는 것이 중요합니다.
커널 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[CHACHA_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 초기화 — 256비트 엔트로피 누적 시 _credit_init_bits()가 crng_reseed() 호출 */
/* crng_reseed()가 extract_entropy()로 input_pool에서 시드 추출 → base_crng.key 갱신 */
crng_reseed() — 주기적 갱신 메커니즘
/* 약 60초마다 또는 충분한 엔트로피 누적 시 호출 */
static void crng_reseed(struct crng *crng)
{
u8 key[CHACHA_KEY_SIZE];
/* input_pool에서 32바이트 추출 (Blake2s 해시 압축) */
extract_entropy(key, sizeof(key));
/* 새 키로 교체 (Fast Key Erasure) — 이전 키는 더 이상 사용되지 않아 forward secrecy 보장 */
memcpy(&crng->key, key, sizeof(key));
WRITE_ONCE(crng->generation, base_crng_generation);
memzero_explicit(key, sizeof(key)); /* 스택 잔류 키 소거 */
}
/* 난수 생성 — ChaCha20 state 조립 + 1블록 생성 + Fast Key Erasure */
static void crng_fast_key_erasure(u8 key[CHACHA_KEY_SIZE],
struct chacha_state *chacha_state,
u8 *random_data, size_t random_data_len)
{
u8 first_block[CHACHA_BLOCK_SIZE];
/* ChaCha20 state 조립: 상수 + 키 + 카운터(0) + 논스(0) */
chacha_init_consts(chacha_state);
memcpy(&chacha_state->x[4], key, CHACHA_KEY_SIZE);
memset(&chacha_state->x[12], 0, sizeof(u32) * 4);
/* 1블록 생성 (카운터 0에서 시작) */
chacha20_block(chacha_state, first_block);
/* Fast Key Erasure: 출력의 앞 32바이트를 새 키로 사용 */
/* → 이전 출력으로 키를 역추적 불가 (forward secrecy) */
memcpy(key, first_block, CHACHA_KEY_SIZE);
memcpy(random_data, first_block + CHACHA_KEY_SIZE, random_data_len);
memzero_explicit(first_block, sizeof(first_block));
}
코드 설명
- extract_entropy() input_pool의 Blake2s 해시 상태를 압축해 32바이트 키를 추출합니다. 이 과정은 풀 상태를 변경(absorb)하므로 같은 키가 두 번 출력되지 않습니다.
- Fast Key Erasure Daniel Bernstein이 설계한 기법입니다. ChaCha20 블록 출력의 첫 32바이트를 즉시 새 키로 교체합니다. 메모리가 탈취되어도 이전 출력을 재현할 수 없습니다.
-
memzero_explicit()
컴파일러 최적화(Compiler Optimization)로 소거 코드가 제거되지 않도록 강제합니다. 일반
memset()은 최적화 옵션에 따라 제거될 수 있습니다.
input_pool — Blake2s 압축 함수
/* input_pool: Blake2s 해시 상태 기반 엔트로피 수집 */
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 드라이버 개발
read() 콜백뿐입니다.
hwrng 서브시스템의 드라이버 API는 단순합니다.
struct hwrng 구조체(Struct)를 초기화하고 hwrng_register()를 호출하면 됩니다.
등록이 완료되면 프레임워크가 /dev/hwrng 캐릭터 디바이스 생성,
/sys/class/misc/hw_random/ sysfs 엔트리 관리,
그리고 hwrng_fillfn() 커널 스레드를 통한 주기적 엔트로피 풀 공급을
모두 자동으로 처리합니다.
드라이버 개발자는 하드웨어에서 난수 바이트를 읽어오는 read() 콜백(Callback)만 구현하면 되며,
엔트로피 풀 주입, 크레딧 계산, 사용자 공간(User Space) 인터페이스는 코어 프레임워크가 담당합니다.
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 비트/비트 (이론적 최대) | 검증된 고품질 TRNG |
| 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 *)rng->priv;
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; /* 하드웨어 스펙에 따라 조정 */
rng->priv = (unsigned long)priv;
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로 환산합니다.
외장 hwrng 드라이버 구현
외장 하드웨어 RNG 장치는 USB 또는 PCIe 인터페이스로 연결되어 hwrng 드라이버로 구현되는 경우가 많습니다.
장치 내부의 엔트로피 원천은 열잡음 TRNG, 애벌랜치 노이즈, 링 오실레이터 지터, 양자 광학 소스 등으로 다양하며,
커널 드라이버 관점에서는 모두 struct hwrng의 read() 콜백으로 난수 바이트를 제공하는 장치입니다.
USB 방식은 벌크 전송(Bulk Transfer)으로 데이터를 수신하므로 구현이 단순하지만
지연 시간(Latency)이 ms 단위로 높고, 최대 처리량이 USB 대역폭(USB 2.0: ~40 MB/s)에 제한됩니다.
반면 PCIe 방식은 MMIO(Memory-Mapped I/O)를 통한 직접 레지스터(Register) 접근과
MSI(Message Signaled Interrupt)를 활용하여 μs 단위의 낮은 지연과
수 Gbps의 처리량을 달성할 수 있으나, 드라이버 복잡도가 상당히 높아집니다.
일반적으로 USB 외장 RNG는 quality = 600~800, PCIe 전용 RNG는 quality = 900~1024 범위를 설정할 수 있지만,
최종 값은 반드시 데이터시트와 NIST SP 800-90B 같은 엔트로피 평가 결과를 기준으로 정해야 합니다.
USB hwrng 드라이버 완전 예제
아래 코드는 USB 벌크 엔드포인트로 난수 블록을 제공하는 외장 하드웨어 RNG 장치의 드라이버 구조를 보여주는 예시입니다. 실제 상용 장치는 벤더별 프로토콜과 인증 절차가 다르므로 데이터시트에 맞게 엔드포인트, 명령, 헬스 테스트를 구현해야 합니다.
#include <linux/module.h>
#include <linux/usb.h>
#include <linux/hw_random.h>
#include <linux/slab.h>
#include <linux/completion.h>
#define USB_RNG_VENDOR_ID 0x1d50 /* 가상의 벤더 ID */
#define USB_RNG_PRODUCT_ID 0x6021
#define USB_RNG_BULK_IN_EP 0x81 /* Bulk IN 엔드포인트 */
#define USB_RNG_BUFFER_SIZE 4096 /* 한 번에 읽는 원시 난수 버퍼 크기 */
#define USB_RNG_QUALITY 900 /* 엔트로피 평가 결과에 따라 조정 */
struct usb_rng_dev {
struct usb_device *udev;
struct usb_interface *interface;
struct hwrng hwrng;
/* 비동기 Bulk 전송용 */
struct urb *urb;
u8 *buf; /* DMA 가능 버퍼 */
dma_addr_t buf_dma;
size_t buf_len; /* 사용 가능한 바이트 수 */
size_t buf_off; /* 현재 읽기 오프셋 */
struct mutex lock;
struct completion urb_done;
bool disconnecting;
};
/* URB 완료 콜백 — 인터럽트 컨텍스트에서 호출됨 */
static void usb_rng_urb_complete(struct urb *urb)
{
struct usb_rng_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 usb_rng_read(struct hwrng *rng, void *data, size_t max, bool wait)
{
struct usb_rng_dev *qdev = container_of(rng, struct usb_rng_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, USB_RNG_BULK_IN_EP),
qdev->buf, USB_RNG_BUFFER_SIZE,
usb_rng_urb_complete, qdev);
qdev->urb->transfer_dma = qdev->buf_dma;
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 usb_rng_probe(struct usb_interface *interface,
const struct usb_device_id *id)
{
struct usb_rng_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);
qdev->urb = usb_alloc_urb(0, GFP_KERNEL);
if (!qdev->urb) {
ret = -ENOMEM;
goto err_free;
}
/* DMA 가능 버퍼 할당 */
qdev->buf = usb_alloc_coherent(qdev->udev, USB_RNG_BUFFER_SIZE,
GFP_KERNEL, &qdev->buf_dma);
if (!qdev->buf) {
ret = -ENOMEM;
goto err_free_urb;
}
/* hwrng 등록 */
qdev->hwrng.name = "usb-hwrng";
qdev->hwrng.read = usb_rng_read;
qdev->hwrng.quality = USB_RNG_QUALITY;
usb_set_intfdata(interface, qdev);
ret = hwrng_register(&qdev->hwrng);
if (ret) {
dev_err(&interface->dev, "hwrng 등록 실패: %d\n", ret);
goto err_free_buf;
}
dev_info(&interface->dev, "USB hwrng 연결됨 (quality=%u)\n", USB_RNG_QUALITY);
return 0;
err_free_buf:
usb_free_coherent(qdev->udev, USB_RNG_BUFFER_SIZE, qdev->buf,
qdev->buf_dma);
err_free_urb:
usb_free_urb(qdev->urb);
err_free:
kfree(qdev);
return ret;
}
static void usb_rng_disconnect(struct usb_interface *interface)
{
struct usb_rng_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, USB_RNG_BUFFER_SIZE, qdev->buf,
qdev->buf_dma);
kfree(qdev);
dev_info(&interface->dev, "USB hwrng 분리됨\n");
}
static const struct usb_device_id usb_rng_id_table[] = {
{ USB_DEVICE(USB_RNG_VENDOR_ID, USB_RNG_PRODUCT_ID) },
{}
};
MODULE_DEVICE_TABLE(usb, usb_rng_id_table);
static struct usb_driver usb_rng_driver = {
.name = "usb-hwrng",
.probe = usb_rng_probe,
.disconnect = usb_rng_disconnect,
.id_table = usb_rng_id_table,
};
module_usb_driver(usb_rng_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("USB hwrng driver");
MODULE_AUTHOR("Example Author");
코드 설명
- USB_RNG_QUALITY 하드웨어 데이터시트와 NIST SP 800-90B 평가 결과를 기준으로 quality 값을 설정합니다. QRNG라도 인증 없이 1024를 단정하지 않습니다.
- usb_fill_bulk_urb() USB Bulk 전송 요청을 초기화합니다. 외장 RNG는 대량 바이트를 전송하므로 Bulk 엔드포인트가 적합합니다. 인터럽트 엔드포인트는 소량(64B 이하)에만 적합합니다.
-
hwrng_unregister() 순서
hwrng_unregister()는 진행 중인read()콜백이 완료될 때까지 블록합니다. 그 후usb_kill_urb()를 호출해야 URB 사용 후 해제(Use-After-Free) 버그를 피할 수 있습니다. -
container_of()
struct hwrng가struct usb_rng_dev에 내장되어 있으므로,container_of()로 부모 구조체 포인터를 얻습니다. 별도 할당보다 메모리 효율적입니다.
메인라인 hwrng/TRNG 드라이버 예시
아래 항목은 Linux 메인라인 커널에서 확인할 수 있는 hwrng/TRNG 계열 드라이버 예시입니다. 상용 QRNG 전용 드라이버 목록이 아니며, 대부분 SoC 내장 TRNG, CPU RNG 명령, TPM, 가상 RNG 인터페이스입니다.
| 대상 | 커널 소스 위치 | 분류 | 인터페이스 |
|---|---|---|---|
| BCM2835 / BCM63xx | drivers/char/hw_random/bcm2835-rng.c | SoC 내장 RNG/TRNG | MMIO |
| Intel 하드웨어 RNG | drivers/char/hw_random/intel-rng.c | 칩셋/플랫폼 RNG | MMIO/I/O 포트 |
| ARM SMCCC TRNG | drivers/char/hw_random/arm_smccc_trng.c | 펌웨어 TRNG 인터페이스 | SMC/HVC 콜 |
| TPM 2.0 RNG | drivers/char/tpm/ | TPM 내부 RNG | TPM 커맨드 |
| VirtIO RNG (KVM 게스트) | drivers/char/hw_random/virtio-rng.c | 가상 RNG | virtqueue |
| EXYNOS (삼성) | drivers/char/hw_random/exynos-trng.c | SoC 내장 TRNG | MMIO |
PCIe hwrng 드라이버
PCIe 기반 전용 RNG는 USB보다 높은 처리율과 낮은 지연을 제공합니다. MMIO(Memory-Mapped I/O) BAR를 통해 직접 하드웨어 레지스터에 접근하고, 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_RNG_VENDOR 0x1234
#define PCIE_RNG_DEVICE 0xABCD
#define RNG_REG_STATUS 0x00 /* 상태 레지스터 */
#define RNG_REG_DATA 0x04 /* 난수 데이터 레지스터 (32비트) */
#define RNG_REG_FIFO_COUNT 0x08 /* FIFO 내 사용 가능 워드 수 */
#define RNG_STATUS_READY BIT(0) /* FIFO에 데이터 있음 */
#define PCIE_RNG_QUALITY 900 /* 엔트로피 평가 결과에 따라 조정 */
struct pcie_rng_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_rng_irq(int irq, void *dev_id)
{
struct pcie_rng_dev *qdev = dev_id;
u32 status = readl(qdev->mmio_base + RNG_REG_STATUS);
if (!(status & RNG_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_rng_read(struct hwrng *rng, void *data, size_t max, bool wait)
{
struct pcie_rng_dev *qdev = container_of(rng, struct pcie_rng_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 + RNG_REG_DATA);
bytes += sizeof(u32);
if (readl(qdev->mmio_base + RNG_REG_FIFO_COUNT) == 0)
atomic_set(&qdev->data_ready, 0);
}
return (int)bytes;
}
static int pcie_rng_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
struct pcie_rng_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_rng_irq, 0, "pcie-hwrng", qdev);
if (ret)
return dev_err_probe(&pdev->dev, ret, "IRQ 등록 실패\n");
/* hwrng 등록 */
qdev->hwrng.name = "pcie-hwrng";
qdev->hwrng.read = pcie_rng_read;
qdev->hwrng.quality = PCIE_RNG_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 hwrng 등록 완료 (quality=%u)\n", PCIE_RNG_QUALITY);
return 0;
}
static const struct pci_device_id pcie_rng_ids[] = {
{ PCI_DEVICE(PCIE_RNG_VENDOR, PCIE_RNG_DEVICE) },
{}
};
MODULE_DEVICE_TABLE(pci, pcie_rng_ids);
static struct pci_driver pcie_rng_driver = {
.name = "pcie-hwrng",
.id_table = pcie_rng_ids,
.probe = pcie_rng_probe,
};
module_pci_driver(pcie_rng_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("PCIe 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 인증
quality 파라미터는 바로 이 검사에서 얻은 점수를 반영합니다.
NIST SP 800-90B(Recommendation for the Entropy Sources Used for Random Bit Generation)는
2018년 1월 발행된 미국 표준으로, 난수 생성기(RBG)가 사용하는 엔트로피 소스(Entropy Source)의
설계 원칙과 검증 요구사항을 정의합니다. 핵심 지표는 min-entropy(H∞)이며,
이는 가장 발생 확률이 높은 출력값의 확률 pmax로부터 계산되는 가장 보수적인 엔트로피 측정값입니다.
커널 hwrng의 quality 파라미터는 이 표준의 min-entropy 측정값에서 직접 유도되며,
FIPS 140-3 인증을 위한 필수 요건이기도 합니다.
SP 800-90B는 주사위 품질 검사관과 같습니다. 카지노에서 주사위가 공정한지 확인하려면 어떻게 해야 할까요? 주사위를 던져보고 결과를 분석합니다. SP 800-90B도 똑같습니다 — 하드웨어 난수 생성기(hwrng)에서 숫자를 많이 뽑아보고, 그 숫자들이 정말 예측 불가능한지 통계적으로 검사합니다.
- 엔트로피(Entropy) = "예측 불가능성" — 주사위가 얼마나 공정한지 나타내는 점수. 100점 만점에 100점이면 완벽한 주사위, 0점이면 항상 같은 숫자만 나오는 불량 주사위
- min-entropy = "최악의 경우 점수" — 평균이 아니라 가장 자주 나오는 숫자를 기준으로 계산. 보안에서는 평균이 아니라 최악을 대비해야 하므로 이 점수를 사용
- IID 검정 = "독립성 검사" — 주사위를 던질 때마다 이전 결과에 전혀 영향을 받지 않는지 확인. 매번 새로운 주사위를 던지는 것과 같아야 IID
- Non-IID 검정 = "보수적 검사" — 주사위에 약간의 편향이나 패턴이 있을 수 있다고 가정하고, 그런 경우에도 안전할 만큼의 예측 불가능성이 있는지 계산
- 재시작 테스트 = "새로 켤 때마다 다른 결과가 나오는가?" — 주사위 공장에서 주사위를 새로 만들 때마다 항상 같은 면이 위로 나온다면 문제. 전원을 껐다 켤 때마다 다른 난수가 나오는지 확인
- 컨디셔닝 = "품질 개선 공정" — 약간 편향된 주사위를 여러 개 던져서 결과를 섞으면 더 공정한 결과가 나오는 것처럼, 원시 난수를 후처리하여 품질을 높이는 단계
- 헬스 테스트 = "실시간 건강 검진" — 주사위가 도중에 고장 나서 항상 같은 숫자만 나오는지 실시간으로 감시
SP 800-90B 핵심 개념 쉬운 가이드
아래는 SP 800-90B의 핵심 개념들을 전문 용어 없이 설명한 내용입니다. 기술적 세부 사항은 뒤에 이어지는 각 섹션에서 다룹니다.
엔트로피(Entropy) — "얼마나 예측하기 어려운가?"
엔트로피는 "이 난수 생성기가 만든 숫자를 공격자가 얼마나 예측하기 어려운가"를 나타내는 점수입니다. 점수가 높을수록 예측이 어렵고, 점수가 낮을수록 쉽습니다.
- 8비트 난수(0~255 중 하나)의 경우: 완벽하게 무작위면 엔트로피 = 8.0 (만점). 항상 같은 숫자만 나오면 엔트로피 = 0
- quality 점수: 엔트로피를 0~1024 점수로 변환한 것. 1024 = 만점(완벽한 난수), 0 = 난수 아님
- 왜 "min-entropy"를 쓰는가?: 평균적인 예측 어려움이 아니라 "가장 쉽게 예측할 수 있는 경우"를 기준으로 삼습니다. 보안에서는 최악의 경우를 대비해야 하므로, 평균(Shannon entropy)이 아니라 최악(min-entropy)을 사용합니다
IID vs Non-IID — "매번 새 주사위인가, 같은 주사위인가?"
IID(Independent and Identically Distributed)는 "매번 던질 때마다 완전히 독립적이고 동일한 분포"라는 뜻입니다. 쉽게 말해 "매번 새로운 공정한 주사위를 던지는 것"과 같습니다.
- IID 검정 통과: 주사위가 완벽하게 공정하고 이전 결과에 영향을 받지 않음. 하지만 현실의 하드웨어는 거의 통과하지 못함
- Non-IID 검정: 주사위에 약간의 편향이나 이전 결과의 영향이 있을 수 있다고 가정. 대부분의 하드웨어 RNG는 이 검사를 사용
- Non-IID가 더 보수적(더 낮은 점수)이지만, 모든 소스에 안전하게 적용 가능
10가지 추정기 — "주사위의 어떤 문제를 찾는가?"
Non-IID 검사에서는 10가지 다른 방법으로 주사위의 예측 불가능성을 측정합니다. 각 방법이 찾는 문제가 다릅니다. 10가지 중 가장 낮은 점수가 최종 점수가 됩니다 — 가장 엄격한 기준을 적용하는 것입니다.
| 추정기 | 쉬운 설명 | 찾는 문제 |
|---|---|---|
| 1. Most Common Value | "어떤 숫자가 가장 많이 나왔나?" | 특정 숫자의 편향 |
| 2. Collision | "같은 숫자가 또 나올 때까지 얼마나 걸리나?" | 빈번한 반복 (바이너리 전용) |
| 3. Markov | "이번 숫자가 다음 숫자에 영향을 주나?" | 인접 숫자 간 의존성 (바이너리 전용) |
| 4. Compression | "데이터를 압축하면 줄어드나?" | 반복 패턴 = 압축 가능 (바이너리 전용) |
| 5. t-Tuple | "특정 패턴이 반복해서 나타나나?" | 다양한 길이의 패턴 반복 |
| 6. LRS | "가장 긴 반복되는 부분이 얼마나 긴가?" | 긴 반복 구조 |
| 7. MultiMCW | "최근 경향으로 다음 숫자를 예측할 수 있나?" | 단기/장기 추세 |
| 8. Lag | "일정 간격 전의 숫자로 예측할 수 있나?" | 주기적/순환 패턴 |
| 9. MultiMMC | "몇 개 연속 숫자 패턴으로 예측할 수 있나?" | 고차 순차 의존성 |
| 10. LZ78Y | "이전에 본 패턴으로 다음을 예측할 수 있나?" | 사전 기반 예측 가능성 |
재시작 테스트 — "다시 켤 때마다 다른가?"
컴퓨터를 껐다 켤 때 하드웨어 난수 생성기가 매번 다른 초기 상태에서 시작하는지 확인합니다. 만약 부팅할 때마다 항상 비슷한 난수가 나온다면, 공격자가 "이 기기는 부팅 후 항상 이런 난수를 만든다"고 예측할 수 있어 치명적입니다.
컨디셔닝 — "품질 개선 공정"
하드웨어 난수 생성기의 원시 출력에는 약간의 편향이 있을 수 있습니다. 예를 들어 0이 1보다 약간 더 자주 나올 수 있습니다. 컨디셔닝은 이 편향을 제거하고 난수 품질을 높이는 후처리 단계입니다.
헬스 테스트 — "실시간 건강 검진"
난수 생성기가 동작 중에 고장 나는 것을 실시간으로 감지합니다. 두 가지 필수 검사가 있습니다:
- RCT (반복 카운트 검정): "같은 숫자가 너무 많이 연속으로 나오지 않는가?" — 주사위가 고장 나서 한 면에 고착되었는지 즉시 감지
- APT (적응형 비율 검정): "특정 숫자가 전체적으로 너무 자주 나오지 않는가?" — 주사위가 서서히 마모되어 편향이 커지는 것을 감지
SP 800-90 시리즈 3부작
NIST SP 800-90 시리즈는 난수 생성 체계를 세 계층으로 분리하여 각각의 표준 문서로 정의합니다.
이 3부작 구조는 리눅스 커널 drivers/char/random.c의 난수 서브시스템 설계와 직접 대응됩니다.
| 표준 | 제목 | 담당 계층 | 발행일 | 커널 대응 |
|---|---|---|---|---|
| SP 800-90A Rev.1 | DRBG (Deterministic Random Bit Generator) | 결정론적 난수 확장 — 시드(Seed)에서 결정적으로 난수 출력을 생성 | 2015년 6월 | crypto/drbg.c — CTR_DRBG, HMAC_DRBG, Hash_DRBG 구현 |
| SP 800-90B | Entropy Sources | 물리적/소프트웨어 노이즈 소스 — 원시 엔트로피 수집 및 품질 검증 | 2018년 1월 | drivers/char/hw_random/ — hwrng 프레임워크, Jitter RNG |
| SP 800-90C | RBG Construction | 난수 생성기 구성 — 90A DRBG와 90B 엔트로피 소스를 조합한 RBG 구축 방법 | 2024년 8월 (Final) | random.c의 CRNG(ChaCha20) 재시드 구조가 요건 충족 |
데이터 흐름은 90B 엔트로피 소스 → 90A DRBG → 90C RBG 구성 순서입니다. 90B가 원시 엔트로피의 품질을 보증하고, 90A가 그 엔트로피를 시드로 받아 결정론적으로 확장하며, 90C가 이 둘을 조합하는 안전한 구성 패턴을 규정합니다. 90C는 2024년 8월 최종 확정될 때까지 커널은 사실상 90A+90B 조합으로 90C 요건을 선제 충족해 왔습니다.
엔트로피 소스 모델
SP 800-90B는 모든 엔트로피 소스를 4단계 파이프라인으로 모델링합니다. 이 모델은 hwrng 드라이버 설계와 1:1로 대응되며, 각 단계의 출력이 검증 대상이 됩니다.
rng->read() 콜백으로 디지털 비트열이 전달됩니다. 3단계(컨디셔닝)는 일부 하드웨어가
내장 추출기(von Neumann, Toeplitz hash 등)로 수행하거나, 커널 random.c의 Blake2s
믹싱이 담당합니다. 4단계(출력)는 add_hwgenerator_randomness()로 input_pool에 주입됩니다.
검증용 데이터 샘플 유형
SP 800-90B 검증은 세 종류의 데이터 샘플을 요구합니다. 각 샘플은 최소 1,000,000개의 연속된 표본값을 포함해야 하며, 샘플 수집 방법이 검증 결과의 유효성을 결정합니다.
| 샘플 유형 | 수집 위치 | 최소 크기 | 검증 도구 | 측정 목적 |
|---|---|---|---|---|
| 순차 데이터셋 (Sequential) | 노이즈 소스 원시 출력 (컨디셔닝 전) | 1,000,000샘플 | ea_iid / ea_non_iid |
초기 엔트로피 추정 (HI) |
| 컨디셔닝 데이터셋 (Conditioned) | non-vetted 컨디셔너 출력 (컨디셔닝 후) | 1,000,000샘플 | ea_conditioning |
컨디셔닝 후 엔트로피 감소량 산정 |
| 재시작(Reboot) 데이터셋 (Restart) | 1,000회 재시작 × 1,000샘플/회 | 1,000,000샘플 | ea_restart |
초기 조건 의존성 검증 (HI vs HR) |
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; /* 이상적 8비트 표본: 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 (이론적 최대)
* H_min_per_bit = 0.990 → quality = 1014 (검증된 고품질 TRNG)
* 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) | 인증서 발급 |
IID vs Non-IID 검증 경로
SP 800-90B는 엔트로피 소스의 통계적 특성에 따라 두 가지 검증 경로를 제공합니다. 경로 선택은 IID(Independent and Identically Distributed, 독립 동일 분포) 가설 검정으로 결정되며, 대부분의 실제 하드웨어 엔트로피 소스는 non-IID 경로를 따릅니다.
| 구분 | IID 경로 | Non-IID 경로 |
|---|---|---|
| 가정 | 각 샘플이 독립적이고 동일 분포를 따름 | 샘플 간 상관관계 또는 분포 변화 허용 |
| 판정 방법 | 순열 검정 19종(§5.1) + 카이제곱 2종(§5.2.1-5.2.4) + LRS 1종(§5.2.5) = 22종 | 10가지 추정기 중 최솟값 채택 |
| 엔트로피 추정 | HI = min(Hbitstring, bits_per_symbol × Hper_bit) | 10가지 추정기 결과의 최솟값 = HI |
| 보수성 | 덜 보수적 (IID 가정이 성립할 때만 유효) | 더 보수적 (모든 소스에 안전) |
| 적용 사례 | 양자 RNG(QRNG), 고품질 TRNG (IG 7.18 추가 근거 필요) | 대부분의 hwrng, Jitter RNG, ring oscillator TRNG |
| FIPS 140-3 요건 | IID 주장 시 추가 통계적 근거 필수 | 기본 경로 — 추가 근거 불필요 |
IID 경로를 선택하려면 ea_iid 도구의 순열 검정 19종(§5.1), 카이제곱 2종(§5.2.1-5.2.4), LRS 1종(§5.2.5) — 총 22종 검정이 모두 IID 가설을 기각하지 않아야 합니다. 하나라도 기각되면 non-IID 경로로 전환해야 합니다.
CMVP Implementation Guidance 7.18(IG 7.18)은 IID 주장에 대해 추가 통계적 근거를 요구하므로,
대부분의 엔트로피 소스는 IID가 아니며, 실제 검증에서는 대부분 non-IID 경로가 채택됩니다.
Non-IID 10가지 추정기
Non-IID 경로에서는 10가지 추정기가 각각 독립적으로 min-entropy를 추정하고, 그중 가장 보수적인(가장 낮은) 값이 해당 엔트로피 소스의 HI로 채택됩니다. 이 다중 추정기 접근은 단일 추정기가 놓칠 수 있는 다양한 형태의 구조적 편향을 포착합니다.
| # | 추정기 | SP 800-90B 절 | 적용 | 측정 방법 요약 | 탐지 대상 | 수치 범위 (8비트) |
|---|---|---|---|---|---|---|
| 1 | Most Common Value (MCV) | §6.3.1 | 전체 | 최빈값 빈도 pmax에 99.5% 신뢰구간 상한 보정 (Z=2.576) → H = −log₂(p') | 최빈값의 발생 빈도 — 가장 기본적이고 보수적인 추정기 | H ∈ [0, 8] |
| 2 | Collision Estimate | §6.3.2 | 바이너리 | 충돌(동일값 재등장) 간 평균 간격 T̄ → 99% 신뢰하한 X̄' 산출 후 이진 탐색으로 p 해석 → H (§6.3.2) | 연속 샘플 간 충돌 패턴 — 빈번한 충돌 = 낮은 엔트로피 | H ∈ [0, 1] (비트당) |
| 3 | Markov Estimate | §6.3.3 | 바이너리 | 2상태 마르코프 전이 확률 P(0→0), P(0→1), P(1→0), P(1→1) → 조건부 엔트로히 | 인접 비트 간 1차 순차 의존성 — 한 비트가 다음 비트에 미치는 영향 | H ∈ [0, 1] (비트당) |
| 4 | Compression Estimate | §6.3.4 | 바이너리 | LZ76 압축 통계량 — 평균 사전 단어 길이로 압축 가능성 측정 | Lempel-Ziv 압축 가능성 — 반복 패턴, 압축 잘 됨 = 낮은 엔트로피 | H ∈ [0, 1] (비트당) |
| 5 | t-Tuple Estimate | §6.3.5 | 전체 | 각 튜플 길이 t에서 최빈 t-튜플 빈도 pmax(t) → H(t) = −log₂(p'(t))/t, min over t | 다양한 스케일의 튜플 반복 — 특정 길이 패턴의 반복 출현 | H ∈ [0, 8] |
| 6 | Longest Repeated Substring (LRS) | §6.3.6 | 전체 | 2회 이상 출현하는 최장 부분문자열 길이 W → 접미 배열 O(n log n) 계산 | 데이터 내 가장 긴 반복 부분문자열 — 긴 반복 = 구조적 패턴 | H ∈ [0, 8] |
| 7 | MultiMCW Prediction | §6.3.7 | 전체 | 4개 윈도우(63, 255, 1023, 4095)에서 최빈값으로 예측 → 적중률 pglobal | 다중 윈도우 최빈값 기반 예측 — 단기~장기 시간 스케일 예측 가능성 | H ∈ [0, 8] |
| 8 | Lag Prediction | §6.3.8 | 전체 | lag d ∈ [1, 128]에서 d 이전 샘플로 예측 → 적중률 plag, min over d | 과거 특정 시점(lag) 샘플로 다음 값 예측 — 주기적/순환 패턴 | H ∈ [0, 8] |
| 9 | MultiMMC Prediction | §6.3.9 | 전체 | 차수 d ∈ {16, 64, 256} 마르코프 모델 → 문맥 기반 예측 적중률 | 다중 마르코프 모델 + 교정(Correction) — 고차 순차 의존성 | H ∈ [0, 8] |
| 10 | LZ78Y Prediction | §6.3.10 | 전체 | LZ78 사전 구축(최대 2¹⁶ 항목) → 가장 긴 매칭 패턴으로 예측 | LZ78 사전 기반 예측 (Y 버전) — 압축 가능성 = 예측 가능성 | H ∈ [0, 8] |
추정기 1-6은 통계적 분석 기반이며, 7-10은 예측(Prediction) 기반입니다. 예측 추정기는 이전 샘플들로 다음 샘플을 예측하는 정확도로 엔트로피를 추정합니다 — 예측이 잘 맞을수록 엔트로피가 낮다는 의미입니다. 바이너리 전용 추정기(2-4)는 비트스트링(bitstring) 모드에서만 실행되며, 다비트 심볼(8비트 등)에서는 추정기 1, 5-10만 적용됩니다.
통계적 분석 기반 추정기 (1~6)
통계적 분석 기반 추정기는 데이터의 빈도 분포, 패턴 반복, 압축 가능성 등 관측된 통계량으로부터 min-entropy를 직접 산출합니다. 데이터의 구조를 수학적 모델로 기술하고, 그 모델 하에서 최악의 경우(가장 높은 확률)를 상한으로 삼습니다.
1. Most Common Value (MCV) — §6.3.1: 모든 경로(IID/non-IID)에서 항상 계산되는 기본 추정기입니다. 데이터에서 가장 빈번한 값의 출현 비율 pmax를 구하고, 99.5% 신뢰구간 상한 보정을 적용합니다. p' = pmax + 2.576·√(pmax(1−pmax)/(n−1)), H = −log₂(p'). 완벽한 균등분포(8비트 256값)에서 pmax ≈ 1/256, H ≈ 8.0. 한 값이 지배적이면 pmax → 1.0, H → 0. 신뢰구간 보정으로 표본 크기 유한성을 보수적으로 반영하여 이론적 최댓값보다 약간 낮은 값(예: 7.999 대신 7.998)이 산출되는 것이 정상입니다. 수치 범위: H ∈ [0, 8] (8비트 심볼).
2. Collision Estimate — §6.3.2 (바이너리 전용): Hagerty와 Draper가 제안한 방법으로, 충돌(동일값 재등장)까지의 평균 간격으로 엔트로피를 추정합니다. 충돌 시간 ti의 표본평균 X̄와 표준편차 σ̂를 구하고, 99% 신뢰하한 X̄' = X̄ − 2.576·σ̂/√v를 산출한 후, 이진 탐색으로 가장 가능성 높은 p를 해석하여 H = −log₂(p)를 산출합니다. 충돌이 드물면(간격이 길면) 엔트로피가 높고, 빈번하면 낮습니다. 수치 범위: H ∈ [0, 1] (비트당, 비트스트링 모드).
3. Markov Estimate — §6.3.3 (바이너리 전용): 2상태 마르코프 체인으로 인접 비트 간 1차 의존성을 모델링합니다. 4개 전이 확률 P(0→0), P(0→1), P(1→0), P(1→1)을 추정하고, 각 상태에서의 조건부 엔트로피에 보정항을 적용합니다. P(0→0)이 0.5에 가까우면 독립적, 1.0에 가까우면 0에 고착, 0.0에 가까우면 강제 교대 패턴을 의미합니다. 수치 범위: H ∈ [0, 1] (비트당). 1차 의존성만 모델링하므로 고차 의존성은 MultiMMC(#9)에서 처리합니다.
4. Compression Estimate — §6.3.4 (바이너리 전용): LZ76 압축 알고리즘의 통계량을 활용합니다. 데이터를 순차적으로 읽으며 "이전에 본 적이 없는" 최소 길이 구문(phrase)으로 분할하고, 평균 구문 길이로부터 H = log₂(n)/avg_word_length + 보정항으로 산출합니다. 구문이 짧을수록(많을수록) 반복적이고 압축 가능 = 낮은 엔트로피. 수치 범위: H ∈ [0, 1] (비트당).
5. t-Tuple Estimate — §6.3.5: 각 튜플 길이 t = 1, 2, 3, ...에 대해 데이터에서 가장 빈번한 t-튜플의 빈도 pmax(t)를 측정하고, 신뢰구간 보정 후 H(t) = −log₂(p'(t))/t를 계산합니다. 모든 t에 대해 최솟값을 채택합니다. t = 1은 MCV와 동일하며, t가 증가할수록 더 긴 패턴의 반복을 탐지합니다. 수치 범위: H ∈ [0, 8] (8비트 심볼).
6. Longest Repeated Substring (LRS) — §6.3.6: 데이터에서 2회 이상 출현하는 가장 긴 부분문자열의 길이 W를 접미 배열(suffix array)과 LCP 배열로 O(n log n)에 계산합니다 (libdivsufsort 의존). 무작위 데이터에서 이론적 W ≈ 2·logk(n)이며, 8비트 심볼 1,000,000샘플에서 W ≈ 4~5가 예상치입니다. W가 이보다 크면 구조적 패턴이 존재 = 비-IID. 수치 범위: H ∈ [0, 8] (8비트 심볼).
예측 기반 추정기 (7~10)
예측 기반 추정기는 이전 샘플들로 다음 샘플을 예측하는 정확도(적중률)로 엔트로피를 추정합니다. 예측 적중률 p가 높을수록 H = −log₂(p)가 낮아집니다 — 즉, "미래를 예측할 수 있다"는 것은 "엔트로피가 낮다"는 의미입니다. 각 추정기는 서로 다른 예측 전략을 사용하여 다양한 형태의 구조적 의존성을 포착합니다.
7. MultiMCW Prediction — §6.3.7: 4개 윈도우 크기(W = 63, 255, 1023, 4095)에서 각각 최근 윈도우 내의 최빈값(Most Common Value)으로 다음 샘플을 예측합니다. 단기(63샘플)부터 장기(4095샘플)까지 다양한 시간 스케일의 예측 가능성을 탐지합니다. 4개 윈도우 중 가장 보수적인(가장 낮은) 추정값을 채택합니다. 수치 범위: H ∈ [0, 8] (8비트 심볼). 적중률 1/256(무작위)이면 H ≈ 8.0, 적중률 1.0(완벽 예측)이면 H = 0.
8. Lag Prediction — §6.3.8: 특정 lag d ∈ [1, 128] 이전의 샘플 값으로 현재 샘플을 예측합니다. 128개 lag 중 가장 보수적인 추정값을 채택합니다. 주기적/순환 패턴을 탐지하는 데 특화되어 있습니다 — 예를 들어 매 10번째 샘플이 항상 같은 값이면 lag = 10에서 적중률이 높게 나타납니다. 수치 범위: H ∈ [0, 8] (8비트 심볼).
9. MultiMMC Prediction — §6.3.9: 차수 d ∈ {16, 64, 256}의 마르코프 모델(Multi-Step Markov Model with Correction)을 구축합니다. 이전 d개 샘플의 패턴(문맥)으로 다음 샘플을 예측합니다. Markov Estimate(#3)가 1차(인접 비트 간)만 모델링하는 반면, MultiMMC는 고차 의존성을 탐지합니다. 차수가 높을수록 더 긴 패턴의 의존성을 탐지할 수 있지만, 가능한 상태 수가 kd로 기하급수적으로 늘어 통계적 신뢰도가 낮아집니다. 3개 차수 중 최솟값을 채택합니다. 수치 범위: H ∈ [0, 8] (8비트 심볼).
10. LZ78Y Prediction — §6.3.10: LZ78 압축 알고리즘의 사전 구축 방식을 예측에 응용합니다(Y 변형). 관찰된 패턴으로 사전(최대 216 = 65,536 항목)을 구축하고, 현재 위치에서 가장 길게 매칭되는 사전 패턴의 다음 값으로 예측합니다. 압축 가능한 데이터(반복 패턴)는 사전 기반 예측이 잘 되므로 엔트로피가 낮게 추정됩니다. 수치 범위: H ∈ [0, 8] (8비트 심볼).
보정 항과 신뢰구간
모든 추정기는 표본 크기가 유한할 때의 통계적 불확실성을 반영하기 위해 보정 항(correction term)을 적용합니다. 이는 추정값이 우연히 높게 산출되는 것을 방지하는 보수적 장치입니다.
| 추정기 | 보정 방식 | 보정 공식 | 효과 |
|---|---|---|---|
| MCV (§6.3.1) | 99.5% 신뢰구간 상한 | p' = pmax + Z·√(pmax(1−pmax)/(n−1)), Z = 2.576 (ZALPHA, utils.h 고정값) |
pmax를 위쪽으로 보정 → H를 아래로 보정 (보수적) |
| Collision (§6.3.2) | 정규 근사 보정항 | X̄' = X̄ − 2.576·σ̂/√v (99% 신뢰하한) 후 이진 탐색으로 p 해석 → H = −log₂(p) | 유한 표본에서의 생일 문제 근사 오차 보정 |
| Markov (§6.3.3) | 정규 근사 보정항 | H = −log₂(max(P₀·2−H₀, P₁·2−H₁)) + c, c ≈ 0.9968... | 전이 확률 추정의 불확실성 반영 |
| Compression (§6.3.4) | 표본 크기 편향 보정 | H = log₂(n)/avg_word_length + correction(n) | n이 클수록 보정항이 0에 수렴 |
| t-Tuple (§6.3.5) | 99.5% 신뢰구간 상한 (각 t) | p'(t) = pmax(t) + Z·√(pmax(t)(1−pmax(t))/(n−t)), Z = 2.576 | 각 튜플 길이 t마다 독립 보정, 가장 보수적 t의 값 채택 |
| LRS (§6.3.6) | 표본 크기 편향 보정 | H = f(n, W) + correction(n, W) | 반복 길이 W의 통계적 유의성 반영 |
| MultiMCW (§6.3.7) | 예측 적중률 신뢰구간 | p'(W) = pglobal(W) + correction(W, n) | 각 윈도우 크기 W마다 독립 보정 |
| Lag (§6.3.8) | 예측 적중률 신뢰구간 | p'(d) = plag(d) + correction(d, n) | 각 lag d마다 독립 보정 |
| MultiMMC (§6.3.9) | 관측되지 않은 상태 반영 | p'(d) = p(d) + correction_mmc(d, n, k), k = 알파벳 크기 | 고차 모델에서 관측되지 않은 상태의 불확실성 보정 |
| LZ78Y (§6.3.10) | 사전 크기 보정 | p' = p + correction_lz78y(n, k) | 사전이 포화되지 않은 경우의 불확실성 보정 |
# ea_non_iid 실행 시 10가지 추정기별 결과 출력
./ea_non_iid -i -v hwrng_sample.bin 8
# -i: unconditioned, -v: 상세 출력, 8 = bits_per_symbol
# 출력 예 (8비트 심볼, 바이너리 전용 추정기는 bitstring 모드에서 별도 실행):
# Most Common Value Estimate: H = 7.998417
# Collision Test Estimate: H = 7.995622 (bitstring only)
# Markov Test Estimate: H = 7.973311 (bitstring only)
# Compression Test Estimate: H = 7.989123 (bitstring only)
# t-Tuple Test Estimate: H = 7.991234
# LRS Test Estimate: H = 7.994567
# MultiMCW Prediction Estimate: H = 7.987654
# Lag Prediction Test Estimate: H = 7.992345
# MultiMMC Prediction Test Estimate: H = 7.988901
# LZ78Y Prediction Test Estimate: H = 7.990123
# ────────────────────────────────────────
# H_min = 7.973311 ← 10가지 중 최솟값이 최종 H_I
# 출력 해석:
# 1. H_I = 7.973311 bits per symbol (Markov 추정기가 가장 보수적)
# 2. H' = 7.973311 / 8 = 0.9967 bits per bit (min-entropy per bit)
# 3. quality = floor(0.9967 × 1024) = 1020 (커널 hwrng quality 파라미터)
# 4. Markov가 최소 → 인접 비트 간 1차 의존성이 가장 큰 엔트로피 감소 원인
# 5. 개선 방향: 컨디셔닝(SHA-256 등)으로 비트 간 의존성 제거 → Markov H 향상
# 비트스트링 모드 추가 실행 (바이너리 전용 추정기 포함 10종 전체):
./ea_non_iid -c -t hwrng_sample.bin 1
# -c: conditioned/bitstring, -t: 첫 1,000,000비트로 제한, 1 = 바이너리
# H_bitstring = min(10종) — H ∈ [0, 1] bits per bit
# 최종 H_I = min(8 × H_bitstring, H_symbol)
- 어느 추정기가 최소값을 산출했는지가 가장 중요 — 이는 엔트로피 소스의 주요 약점을 가리킵니다
- MCV가 최소 → 한 값의 빈도가 너무 높음 → 편향(bias) 문제, 컨디셔닝으로 해결
- Markov가 최소 → 인접 비트 간 의존성 → 비트 간 상관 제거 필요 (예: XOR 폴딩)
- Compression/LZ78Y가 최소 → 반복 패턴 존재 → 노이즈 소스 설계 개선 필요
- Lag가 최소 → 주기적 패턴 → 클럭 주파수, 샘플링 주기 조정 필요
- t-Tuple/LRS가 최소 → 특정 길이 패턴 반복 → 노이즈 소스의 결정론적 성분 제거
- 모든 추정기가 H ≈ 8.0에 가까우면 고품질 엔트로피 소스 — FIPS 140-3 인증 적합
헬스 테스트 (Health Tests)
SP 800-90B는 노이즈 소스가 정상 동작하는지 상시 감시하기 위해 두 가지 필수 헬스 테스트를 규정합니다. 이 테스트는 컨디셔닝 전의 원시 출력에 적용되며, 시작 시(Start-up), 연속(Continuous), 요청 시(On-demand) 세 가지 시점에서 수행됩니다. 헬스 테스트 실패 시 엔트로피 소스는 즉시 비활성화되어야 합니다.
반복 카운트 검정 (Repetition Count Test, RCT)
RCT는 노이즈 소스가 단일 값에 "고착(stuck)"되는 치명적 고장을 즉시 감지합니다.
연속으로 동일한 값이 나타나는 횟수가 임계값을 초과하면 알람을 발생시킵니다.
임계값 C는 표준 §4.4.1의 공식 C = 1 + ⌈(−log₂ α) / H⌉로 계산됩니다. 여기서 α는 허용 오경보율(일반적으로 2−20), H는 평가된 min-entropy입니다. 예: α = 2−20, H = 2.0 → C = 1 + ⌈20/2.0⌉ = 11.
/* SP 800-90B §4.4.1 — Repetition Count Test 구현 */
/* C = 1 + ceil(-log2(alpha) / H), alpha = 2^-20 (일반적 오경보율) */
static int sp800_90b_rct(uint8_t *samples, size_t n,
double h_target, bool *alarm)
{
double alpha = pow(2.0, -20.0); /* 오경보율 */
int c = 1 + (int)ceil(-log2(alpha) / h_target); /* 임계값 */
int repetition_count = 1;
uint8_t prev = samples[0];
for (size_t i = 1; i < n; i++) {
if (samples[i] == prev) {
repetition_count++;
if (repetition_count >= c) {
*alarm = true; /* 고착 고장 감지 */
return -EIO;
}
} else {
repetition_count = 1; /* 다른 값 → 카운터 리셋 */
prev = samples[i];
}
}
*alarm = false;
return 0;
}
적응형 비율 검정 (Adaptive Proportion Test, APT)
APT는 환경 변화나 하드웨어 노화로 인한 점진적 엔트로피 손실을 감지합니다. 고정 크기 윈도우(바이너리: 1,024, 비-바이너리: 512) 내에서 특정 값의 출현 빈도가 임계값을 초과하면 알람을 발생시킵니다. RCT가 급성 고장을 감지한다면, APT는 만성 엔트로피 저하를 감지합니다.
/* SP 800-90B §4.4.2 — Adaptive Proportion Test 구현 */
/* 윈도우 크기: 바이너리 W=1024, 비-바이너리 W=512 */
#define APT_WINDOW_BINARY 1024
#define APT_WINDOW_NONBINARY 512
static int sp800_90b_apt(uint8_t *samples, size_t n,
double h_target, bool binary, bool *alarm)
{
int window = binary ? APT_WINDOW_BINARY : APT_WINDOW_NONBINARY;
double alpha = pow(2.0, -h_target);
/* upper bound: 바이너리 = W * (p + α), 비-바이너리 = W * (p_max + α) */
int bound = (int)(window * (pow(2.0, -h_target) + alpha));
for (size_t start = 0; start + window <= n; start++) {
uint8_t target = samples[start]; /* 윈도우 첫 값이 타겟 */
int count = 0;
for (int i = 0; i < window; i++) {
if (samples[start + i] == target)
count++;
}
if (count > bound) {
*alarm = true; /* 엔트로피 손실 감지 */
return -EIO;
}
}
*alarm = false;
return 0;
}
| 헬스 테스트 시점 | 수행 조건 | 목적 | 커널 대응 |
|---|---|---|---|
| 시작 시 (Start-up) | 전원 인가 / 리부트 후 첫 사용 전 | 부팅 후 노이즈 소스가 정상 동작 확인 | hwrng_register() 시 초기 샘플 검증 |
| 연속 (Continuous) | 노이즈 소스 동작 중 상시 수행 | 실시간 고장 / 엔트로피 손실 감지 | hwrng_fillfn() 내 RCT/APT 적용 |
| 요청 시 (On-demand) | 언제든 호출 가능 (리부트로 대체 가능) | 주기적 검증 또는 이벤트 트리거 검증 | sysfs 트리거 또는 드라이버 ioctl |
hwrng_unregister() 호출로 드라이버를 제거하고, CRNG는 다른 엔트로피 소스
(Jitter RNG, IRQ 타이밍 등)로 폴백합니다. FIPS 140-3 모듈은 헬스 테스트 실패를
"오류 상태(Error State)"로 간주하고 모든 암호화 서비스를 중단해야 합니다.
재시작 테스트 (Restart Test) 상세
재시작 테스트는 엔트로피 소스의 초기 조건 의존성을 검증합니다. 전원 인가 시 항상 동일한 내부 상태에서 시작한다면, 첫 출력이 예측 가능할 수 있습니다. SP 800-90B는 1,000회 재시작으로 이 위험을 정량화합니다.
데이터 수집 절차:
- 엔트로피 소스를 1,000회 재시작 (전원 차단/복구 또는 리셋)
- 각 재시작마다 1,000개의 연속 샘플 수집 (시작 테스트 완료 후)
- 총 1,000,000개 샘플을 "행 데이터셋(row dataset)" 형식으로 저장 — 1,000행 × 1,000열
- 행 단위(단일 재시작 내)와 열 단위(재시작 간) 엔트로피를 각각 추정
재시작 테스트는 순차 데이터셋에서 얻은 HI와 재시작 데이터셋에서 얻은 HR을 비교하여, 최종 엔트로피 추정값으로 둘 중 더 작은 값을 채택합니다.
# 재시작 테스트 데이터 수집 (하드웨어별 스크립트 필요)
# 1,000회 재시작 × 1,000샘플 = 1,000,000바이트
# ea_restart 실행 (non-IID 경로, H_I는 순차 검증 결과값)
./ea_restart -n -v hwrng_restart.bin 8 7.973311
# ↑ bits_per_symbol ↑ H_I
# 출력 예:
# Row dataset analysis (within-restart): H_R_row = 7.985432
# Column dataset analysis (across-restart): H_R_col = 7.972145
# ────────────────────────────────────────
# H_R = min(H_R_row, H_R_col) = 7.972145
# Final entropy = min(H_I, H_R) = min(7.973311, 7.972145) = 7.972145
컨디셔닝 컴포넌트
컨디셔닝(Conditioning)은 노이즈 소스의 원시 출력에서 편향을 제거하고 엔트로피 밀도를 높이는 선택적 후처리 단계입니다. SP 800-90B는 컨디셔닝 컴포넌트를 두 가지로 분류합니다.
| 구분 | Vetted (승인된) | Non-Vetted (비승인) |
|---|---|---|
| 정의 | NIST가 수학적 보안성을 검증한 함수 | 검증되지 않은 임의의 후처리 함수 |
| 예시 | SHA-256, SHA-512, SHA-3 기반 해시 함수 | LFSR, XOR 믹싱, von Neumann 추출기, Toeplitz 해시 |
| 엔트로피 보증 | 출력 엔트로피 = min(n_out, n_in × h_in / n_in) | 별도 컨디셔닝 데이터셋 검증 필수 |
| 검증 요건 | 컨디셔닝 데이터셋 검증 불필요 | 컨디셔닝 출력으로 1,000,000샘플 추가 수집 후 ea_conditioning 검증 |
| 출력 엔트로피 계산 | H_out = min(n_out, h_in) — 입력 엔트로피가 그대로 전달 | H_out = min(n_out, h_in × h') — h'는 컨디셔닝 데이터셋 추정값 |
# ea_conditioning — 컨디셔닝 후 엔트로피 감소량 산정
# Vetted 컨디셔너 (SHA-256 등):
./ea_conditioning -v 256 256 256 240.0
# ↑ n_in n_out nw h_in
# 출력: H_out = min(256, 240.0) = 240.0 bits
# (입력 엔트로피 240비트가 그대로 보존됨)
# Non-vetted 컨디셔너 (LFSR 등):
./ea_conditioning -n 256 256 8 240.0 0.98
# ↑ n_in n_out nw h_in h'
# 출력: H_out = min(256, 240.0 × 0.98) = 235.2 bits
# (컨디셔닝으로 인해 4.8비트 손실)
random.c의 Blake2s 해시 믹싱은
사실상 vetted 컨디셔닝 역할을 수행합니다. 하지만 SP 800-90B 검증 관점에서는
하드웨어 RNG 자체의 내장 컨디셔닝(von Neumann 추출기 등)이 non-vetted인 경우가 많아,
별도의 컨디셔닝 데이터셋 검증이 필요합니다. Intel RDRAND는 내부에 SP 800-90B 호환
컨디셔닝을 포함하므로 vetted로 분류될 수 있습니다.
FIPS 140-3 요구사항 연관
| FIPS 140-3 요구사항 | hwrng 구현 | 관련 커널 코드 |
|---|---|---|
| 연속 RNG 테스트 (CRNGT) | 연속 동일 출력 감지 | hwrng_fillfn() 내 반복 감지 |
| 시작 업 테스트 (SURT) | 초기화 시 통계 검증 | hwrng_register() 호출 시 |
| min-entropy ≥ 목표값 | quality 파라미터 설정 |
struct hwrng.quality |
| 조건부 예외 테스트 | 오류 시 드라이버 비활성화 | hwrng_unregister() |
SP 800-90B 검증 프로세스
SP 800-90B 검증은 NIST의 암호화 모듈 검증 프로그램(CMVP) 하에서 수행됩니다. 검증 프로세스는 엔트로피 소스 설계 문서 제출 → 데이터 샘플 수집 → 통계 검증 → 독립 실험실 검토 → 인증서 발급의 단계로 진행됩니다.
| 검증 주체 | 역할 | 프로그램 |
|---|---|---|
| NIST CAVP | 암호화 알고리즘/엔트로피 소스 검증 알고리즘 및 테스트 벡터 관리 | CAVTS → ACVTS (자동화 시스템으로 전환) |
| NIST CMVP | 암호화 모듈 전체 인증 (FIPS 140-3) — 엔트로피 소스 검증 결과를 모듈 인증에 통합 | FIPS 140-3 인증서 |
| CMTL | NIST 인정 독립 검증 실험실 (Cryptographic Module Testing Lab) — 엔트로피 보고서 검토 및 검증 수행 | NVML (NIST NVLAP 인정) |
| ESV | 엔트로피 소스 검증 온라인 시스템 — 데이터 제출, 테스트 실행, 결과 관리를 웹에서 수행 | ACVTS 프레임워크 일부 |
| 개발사 | 엔트로피 소스 설계, 데이터 샘플 수집, 사전 검증 (오프라인 도구 사용), 엔트로피 보고서 작성 | — |
quality 파라미터로 검증된 H∞ 값을
정확히 반영해야 하며, 헬스 테스트(RCT/APT)를 구현해야 FIPS 140-3 모듈 인증이 가능합니다.
암호 키 생성 완전 플로우
hwrng 장치에서 생성된 엔트로피가 최종 암호 키로 변환되는 전체 흐름을 추적합니다. 커널 내부에서 여러 계층의 처리를 거쳐 안전한 키가 생성됩니다.
암호 키 생성 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;
}
SSH 키 생성과 엔트로피
ssh-keygen은 모든 키 쌍 생성 시 OpenSSL의 BN_rand() 또는
libsodium의 randombytes_buf()를 통해 커널 getrandom() 시스템 콜에서
엔트로피를 획득합니다. 키 타입에 따라 엔트로피 소비량과 품질 요구사항이 크게 다릅니다.
| 키 타입 | 엔트로피 소비 | 생성 시 getrandom() 호출 | 보안 강도 | 특징 |
|---|---|---|---|---|
| RSA 2048 | ~2048비트 (소인수 분해용) | 2회 (p, q 각각) | 112비트 | 두 개의 큰 소수 생성, Miller-Rabin 판정 반복 |
| RSA 4096 | ~4096비트 | 2회 | 152비트 | 소수 탐색 시도 증가, 엔트로피 재요청 빈번 |
| Ed25519 | 256비트 (스칼라 1개) | 1회 | 128비트 | 단일 스칼라 생성, 즉시 완료, 가장 적은 엔트로피 |
| ECDSA P-256 | 256비트 (스칼라 1개) | 1회 + RFC 6979 적용 시 추가 없음 | 128비트 | 개인 키 스칼라 1회, 서명 시 k값은 결정론적 |
| ECDSA P-384 | 384비트 | 1회 | 192비트 | P-256과 동일 구조, 스칼라 크기만 증가 |
ssh-keygen -A가 부팅 초기에
자동 실행되는 배포판(임베디드, 컨테이너 이미지 최초 부팅)에서 CRNG 미초기기 상태가 발생할 수 있습니다.
Linux 3.17+에서는 getrandom()이 블록되므로 ssh-keygen이 멈추는 현상으로 나타나며,
타임아웃 로직이 /dev/urandom 구식 경로로 폴백하는 경우 안전하지 않은 호스트 키가 생성될 수 있습니다.
컨테이너 이미지에 호스트 키를 사전 생성하거나, After=systemd-random-seed.service 의존성을
명시하여 CRNG 초기화 후 키 생성을 보장해야 합니다.
호스트 키 재생성과 Forward Secrecy: 운영 중 SSH 호스트 키를 재생성하면
이전 키로 서명된 known_hosts 항목이 무효화(Invalidation)될 뿐 아니라, 재생성 시점의 엔트로피 품질이
새 키의 장기 보안성을 결정합니다. 특히 동일한 엔트로피 상태에서 재생성하면
이론적으로 동일한 키가 다시 나올 수 있으므로, crng_ready() 확인 후
재생성하는 것이 필수입니다. ChaCha20-CRNG의 Fast Key Erasure는 이전 출력을 역추적할 수 없게 하므로,
CRNG가 초기화된 이후라면 재생성된 키의 안전성이 보장됩니다.
# 호스트 키 재생성 전 CRNG 준비 상태 확인
cat /proc/sys/kernel/random/entropy_avail # 256 이상이어야 안전
# Ed25519 호스트 키 생성 (엔트로피 소비 최소, 권장)
ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N ""
# RSA 4096 호스트 키 생성 (엔트로피 소비 최대, 구형 클라이언트 호환용)
ssh-keygen -t rsa -b 4096 -f /etc/ssh/ssh_host_rsa_key -N ""
# ssh-keygen의 getrandom() 호출 추적
strace -e trace=getrandom ssh-keygen -t ed25519 -f /tmp/test_key -N "" 2>&1
KDF와 솔트 생성
hwrng에서 추출된 원시 엔트로피는 KDF(Key Derivation Function, 키 파생 함수)를 거쳐 인증 프로토콜에 사용되는 다양한 키 자료로 확장됩니다. 원시 hwrng 출력을 인증 키로 직접 사용하면 하드웨어 편향(Bias)이 그대로 전파되므로, 반드시 KDF를 통해 균일 분포(Uniform Distribution)로 정규화해야 합니다.
HKDF와 TLS 1.3 / IKEv2 키 스케줄
HKDF(HMAC-based Key Derivation Function, RFC 5869)는 현대 인증 프로토콜의 키 파생 핵심입니다. TLS 1.3은 핸드셰이크 초기 엔트로피(ClientHello random + ServerHello random)를 HKDF-Extract로 흡수하고, HKDF-Expand로 각 단계별 키를 파생합니다.
/*
* TLS 1.3 키 스케줄에서 hwrng 엔트로피의 흐름
*
* 1. ClientHello.random ← getrandom() 32바이트
* 2. ServerHello.random ← getrandom() 32바이트
* 3. Early Secret = HKDF-Extract(0, PSK 또는 0)
* 4. Handshake Secret = HKDF-Extract(Derive-Secret(Early Secret), ECDHE shared secret)
* 5. Main Secret = HKDF-Extract(Derive-Secret(Handshake Secret), 0)
* 6. 각 키 파생: client/server traffic key, finished key, exporter key
*
* 핵심: ClientHello.random과 ServerHello.random은 hwrng 기반 CRNG에서 직접 추출.
* ECDHE 개인 키도 getrandom()에서 생성. 모든 키 자료의 근원 엔트로피는 hwrng.
*/
/* IPSec IKEv2 키 파생 — RFC 7386 (PRF = HMAC-SHA-256) */
/*
* SKEYSEED = prf(Ni | Nr, g^ir)
* Ni (initiator nonce) ← getrandom() 16~32바이트
* Nr (responder nonce) ← getrandom() 16~32바이트
* g^ir = Diffie-Hellman shared secret (개인 키도 getrandom()에서 생성)
*
* SK_d, SK_ai, SK_ar, SK_ei, SK_er = prf+(SKEYSEED, Ni | Nr | SPIi | SPIr)
* 모든 SA 키는 SKEYSEED에서 파생되며, SKEYSEED의 엔트로피는
* nonce(Ni, Nr)와 DH 일시 키에서 비롯 — 모두 hwrng 기반
*/
/dev/hwrng에서 읽은 원시 바이트를
인증 키나 세션 키로 직접 사용하면 하드웨어 편향이 그대로 키에 전파됩니다.
예를 들어 특정 비트가 0에 편향된 TRNG에서 256비트 키를 직접 생성하면
유효 엔트로피가 256비트 미만이 되어 무차별 대입(Brute Force) 공격에 더 취약해집니다.
반드시 getrandom()(CRNG 경유) 또는 KDF(HKDF/PBKDF2) 처리 후 사용해야 합니다.
커널 input_pool의 Blake2s 혼합이 이 편향 제거 역할을 수행합니다.
비밀번호 해시 솔트 생성
비밀번호 기반 인증의 핵심인 솔트(Salt) 생성도 hwrng에 의존합니다. 솔트는 각 사용자 비밀번호 해시마다 고유해야 하며, 예측 가능하면 레인보우 테이블(Rainbow Table)의 효율이 크게 높아집니다.
| 해시 알고리즘 | 솔트 길이 | 엔트로피 소스 | 커널 인터페이스 |
|---|---|---|---|
| bcrypt | 128비트 (16바이트) | getrandom() | OpenSSL RAND_bytes() |
| argon2id | 128비트 (16바이트) | getrandom() | libsodium randombytes_buf() |
| PBKDF2-HMAC-SHA256 | 128비트 (16바이트) | getrandom() | OpenSSL / glibc getrandom() |
| scrypt | 128비트 (16바이트) | getrandom() | libsodium / libscrypt |
| SHA-512 crypt (glibc) | 96비트 (12바이트, base64) | /dev/urandom | glibc getrandom() 폴백 |
/* 비밀번호 솔트 생성 — 안전한 경로 (glibc 2.25+ 또는 직접 getrandom) */
#include <sys/random.h>
#include <errno.h>
static int generate_salt(unsigned char *salt, size_t len)
{
/* getrandom()은 CRNG 초기화 전에 블록되므로 안전한 솔트 보장 */
ssize_t ret = getrandom(salt, len, 0);
if (ret < 0 || ret != (ssize_t)len)
return -1;
return 0;
}
/* 위험: time() + getpid() 기반 솔트 — 절대 사용 금지 */
/* unsigned int bad_salt = time(NULL) ^ getpid() ^ (clock() & 0xffff); */
/* → 예측 가능, 레인보우 테이블 사전 계산 가능, 인증 무력화 */
time())으로 사용하면
공격자가 특정 시간대에 가입한 사용자의 솔트를 좁힐 수 있습니다.
128비트 솔트를 getrandom()으로 생성하면 시간 추측 공격이 원천 차단됩니다.
/etc/shadow의 솔트는 glibc crypt() 함수가 내부적으로
getrandom()을 호출하여 생성하므로, 최신 시스템에서는 안전합니다.
인증 프로토콜에서의 hwrng 역할
hwrng에서 생성된 엔트로피는 커널 CRNG를 거쳐 다양한 인증 프로토콜의 안전성 근간이 됩니다. 이 섹션에서는 hwrng 품질이 직접적으로 인증 보안에 영향을 미치는 주요 프로토콜들을 분석합니다.
원격 증명과 hwrng
원격 증명(Remote Attestation)은 TPM(Trusted Platform Module)이 시스템의 무결성(Integrity) 상태를 외부 검증자(Verifier)에게 암호학적으로 증명하는 프로세스입니다. 이 프로세스에서 hwrng는 도전값(Qualifying Data)의 무작위성을 보장하는 핵심 역할을 수행합니다.
TPM2_Quote 명령의 엔트로피 흐름:
- 검증자가
getrandom()로 128~256비트 도전값(qualifying data)을 생성 - 검증자가 도전값을 증명 대상(Attestator)에 전송
- 증명 대상이
TPM2_Quote명령으로 도전값 + 선택된 PCR(Platform Configuration Register) 값을 서명 - 검증자가 서명된 응답을 검증 — 도전값이 일치하면 재생 공격(Replay Attack) 차단
drivers/char/tpm/tpm_hwrng.c는 TPM 내부 RNG를
커널 엔트로피 풀에 기여합니다. 동시에 TPM2_Quote에 사용되는 도전값은 커널 CRNG에서 생성됩니다.
이는 TPM의 엔트로피가 커널을 거쳐 다시 TPM 증명에 사용되는 순환 구조를 형성합니다.
일반적으로 input_pool의 Blake2s 혼합과 다중 엔트로피 소스 결합으로 이 순환 의존성의
위험이 완화되지만, TPM이 유일한 엔트로피 소스인 환경(가상 머신 with vTPM, 일부 임베디드)에서는
순환 의존성이 보안 약점이 될 수 있습니다. 이 경우 CONFIG_RANDOM_TRUST_BOOTLOADER 또는
Jitter RNG 보조 엔트로피 소스를 함께 활성화하는 것이 권장됩니다.
/* TPM2_Quote 도전값 생성 — 커널 CRNG에서 추출 */
#include <sys/random.h>
static int generate_attestation_challenge(uint8_t *challenge, size_t len)
{
/*
* 도전값은 검증자가 생성. 128비트(16바이트) 이상 권장.
* getrandom()은 CRNG 초기화 전에 블록되므로
* 안전한 도전값이 보장됨.
*
* 주의: time()이나 counter 기반 도전값 사용 금지.
* 예측 가능한 도전값은 재생 공격을 허용.
*/
ssize_t ret = getrandom(challenge, len, 0);
return (ret == (ssize_t)len) ? 0 : -1;
}
/*
* TPM2_Quote 호출 (tpm2-tools 예시):
* tpm2_quote -c ak.ctx -l sha256:0,1,2,3,4,5,6,7 -q $(hex_challenge)
*
* -q (qualifying data): 검증자가 생성한 무작위 도전값
* TPM이 이 값을 서명에 포함하여 재생 공격 방지
*/
도전-응답 인증과 난수 품질
도전-응답(Challenge-Response) 인증은 상대방이 비밀을 직접 전송하지 않고 도전값에 대한 응답으로 자신을 증명하는 프로토콜입니다. 도전값의 예측 불가능성이 프로토콜 전체의 보안을 결정하므로, hwrng 기반 CRNG에서 생성된 난수가 필수입니다.
| 프로토콜 | 도전값 소스 | 도전값 크기 | 예측 시 공격 |
|---|---|---|---|
| CHAP (RFC 1994) | 서버 getrandom() |
16~128바이트 | 사전 응답 계산 → 재생 |
| SCRAM-SHA-256 | 양측 getrandom() |
24바이트 (client nonce + server nonce) | nonce 충돌 → 중간자 공격 |
| IKEv2 (IPSec) | 양측 getrandom() |
16~32바이트 (Ni, Nr) | nonce 예측 → SA 키 파생 약화 |
| NTLMv2 | 클라이언트 getrandom() |
8바이트 (client challenge) | challenge 예측 → 릴레이 공격 |
| HTTP Digest Auth | 서버 getrandom() |
16바이트 (nonce) | nonce 예측 → 재생 공격 |
getrandom()으로 생성하면 충돌 확률이 2-128 이하가 되어
실용적으로 불가능합니다. 반면 32비트 도전값은 약 65,000번만 시도하면 50% 충돌 확률(생일 공격)에 도달합니다.
PKI 인증서 발급의 난수 의존성
PKI(Public Key Infrastructure, 공개 키 기반 구조) 인증서 발급에서 hwrng 기반 난수는 세 가지 핵목에 사용됩니다: 인증서 일련 번호(Serial Number), CRL 번호, OCSP nonce입니다.
인증서 일련 번호 (RFC 5280 §4.1.2.2): RFC 5280은 일련 번호를 "양의 정수"로 요구하며, CA(Browser Forum Baseline Requirements)는 최소 64비트 무작위성을 권장합니다. 일련 번호가 순차적이거나 예측 가능하면 공격자가 다음 인증서의 일련 번호를 추측하여 인증서 충돌 공격이나 추적 공격에 활용할 수 있습니다.
/* OpenSSL 기반 CA의 일련 번호 생성 경로 */
/*
* OpenSSL BN_rand() → getrandom() syscall
*
* CA 일련 번호: 20바이트(160비트) 무작위, 최상위 비트는 0 (양수 보장)
* RFC 5280: 일련 번호는 1~20바이트 octet string
* CA/B Forum: 최소 64비트 무작위성 권장 (충돌 방지)
*
* 예측 가능한 일련 번호의 위험:
* - 순차 할당(1, 2, 3...) → 발급량 추적 가능
* - 시간 기반 할당 → 발급 시점 추측 → 정밀 추적
* - 충돌 공격: 같은 발급자+일련번호로 두 인증서 생성 시 혼란
*/
/* OCSP nonce (RFC 8954) — 재생 공격 방지 */
/*
* OCSP 요청에 nonce 확장 포함:
* 클라이언트가 getrandom()로 128비트 nonce 생성 → OCSP 요청에 포함
* 서버가 응답에 동일 nonce 포함 → 재생 공격 차단
*
* nonce 부재 시: 공격자가 이전 OCSP 응답을 재생하여
* 이미 폐지된 인증서를 유효하게 속일 수 있음
*/
FIDO2 / WebAuthn 인증기 난수
FIDO2/WebAuthn은 패스워드 없는 인증(Passwordless Authentication) 표준으로, 인증기(Authenticator)가 비대칭 키 쌍을 생성하고 서명으로 사용자를 인증합니다. 이 과정에서 hwrng는 두 가지 경로로 기여합니다.
호스트 측 도전값: 의존 당사자(Relying Party, RP)가 crypto.getRandomValues()로
도전값을 생성합니다. 브라우저는 이를 getrandom() syscall로 전달하여
커널 CRNG에서 128비트 이상의 난수를 획득합니다.
인증기 내부 난수: 하드웨어 인증기(YubiKey, Titan Security Key 등)는
내장 TRNG를 사용하여 서명 논스와 키 쌍을 생성합니다. 소프트웨어 인증기(플랫폼 인증기)는
호스트의 getrandom()에 의존합니다.
| 인증기 유형 | 엔트로피 소스 | 키 생성 위치 | hwrng 의존도 |
|---|---|---|---|
| 하드웨어 인증기 (YubiKey 등) | 장치 내장 TRNG | 인증기 내부 (SE/Samsung eSE) | 호스트 hwrng: 도전값만 / 장치 TRNG: 키 생성 |
| 플랫폼 인증기 (Windows Hello, Touch ID) | TPM 2.0 또는 SE | TPM/SE 내부 | 호스트 hwrng: 도전값 / TPM RNG: 키 생성 |
| 소프트 토큰 (soft token) | 호스트 getrandom() |
사용자 공간 라이브러리 | 호스트 hwrng에 완전 의존 |
Crypto.getRandomValues()는 내부적으로 getrandom()을
호출하므로, CRNG 초기화 후에는 안전한 도전값이 보장됩니다.
하지만 서비스 워커(Service Worker)에서 페이지(Page) 로드 직후 crypto.getRandomValues()를
호출하는 경우, CRNG 미초기기 상태에서 블로킹이 발생할 수 있습니다.
Kerberos 세션 키 생성
Kerberos는 중앙 KDC(Key Distribution Center)가 TGT(Ticket Granting Ticket)와 TGS(Ticket Granting Service) 티켓에 포함되는 세션 키를 생성합니다. 이 세션 키는 클라이언트와 서비스 간 인증의 기반이 되며, KDC의 엔트로피 품질에 직접 의존합니다.
세션 키 생성 경로:
- KDC의
krb5_c_make_random_key()호출 → MIT krb5 라이브러리 - 라이브러리 내부에서
krb5_c_random_make_octets()→/dev/urandom또는getrandom() - 추출된 난수를 암호 알고리즘의 키 형식(AES-256, Camellia 등)으로 변환
- 생성된 세션 키가 TGT/TGS 티켓에 암호화되어 포함
virtio-rng 보급이 권장됩니다.
# KDC의 엔트로피 소비 추적 (MIT Kerberos)
strace -e trace=getrandom -f -p $(pgrep krb5kdc) 2>&1 | head -20
# KDC 시작 전 CRNG 준비 확인
# /etc/systemd/system/krb5kdc.service.d/override.conf
[Unit]
After=systemd-random-seed.service
[Service]
ExecStartPre=/bin/sh -c 'while [ $(cat /proc/sys/kernel/random/entropy_avail) -lt 256 ]; do sleep 0.1; done'
# Active Directory KDC의 RNG 의존성:
# Windows DC는 CryptGenRandom() → BCryptGenRandom() 호출
# 내부적으로 커널 RNG(sysgenrng)를 사용하므로 hwrng 품질에 의존
사용자 공간 프로그램의 커널 난수 공급 구조
지금까지 커널 내부의 hwrng → input_pool → ChaCha20 CRNG 경로를 살펴봤습니다.
하지만 실제 암호학적 안전성은 사용자 공간(User Space) 프로그램이 이 난수를
어떻게 획득하고 어떻게 사용하는가에 의해 최종 결정됩니다.
OpenSSL, OpenSSH, strongSwan, GnuTLS, NSS 등 주요 암호 라이브러리와 응용 프로그램은
각자 고유한 난수 공급 아키텍처를 가지며, 커널 getrandom() 시스템 콜 또는
/dev/urandom에서 시드를 받아 자체 DRBG를 운영합니다.
이 섹션에서는 각 프로그램이 커널로부터 난수를 공급받아 사용하는 전체 경로를 추적합니다.
전체 공급 계층 구조
사용자 공간 프로그램이 커널 난수를 사용하는 경로는 4계층으로 나뉩니다:
- 커널 계층(Kernel Layer) — hwrng 하드웨어 → input_pool(Blake2s) → ChaCha20 CRNG
- 시스템 콜 계층(System Call Layer) —
getrandom(2),/dev/urandom,/dev/random,AF_ALG소켓(crypto API) - 라이브러리 DRBG 계층(Library DRBG Layer) — OpenSSL, GnuTLS, NSS, libgcrypt 각자의 NIST SP 800-90A DRBG 인스턴스
- 응용 프로그램 계층(Application Layer) — OpenSSH, strongSwan/charon, Nginx, Apache, Python 등
대부분의 응용 프로그램은 라이브러리 DRBG 계층을 거쳐 난수를 획득하지만,
일부(특히 시스템 유틸리티)는 getrandom()이나 /dev/urandom을 직접 호출하기도 합니다.
다음 다이어그램은 전체 공급 경로를 보여줍니다:
OpenSSL — DRBG 기반 난수 공급 아키텍처
OpenSSL은 가장 널리 사용되는 범용 암호 라이브러리로, Nginx, Apache, OpenSSH, curl 등
수많은 프로그램이 난수 생성을 OpenSSL에 위임합니다. OpenSSL 1.1.1부터는
NIST SP 800-90A CTR-DRBG(AES-256-CTR 기반)를 기본 DRBG로 사용하며,
OpenSSL 3.0부터는 Provider 아키텍처로 전환되어 EVP_RAND / EVP_DRBG API를 통해
DRBG를 추상화합니다.
시드 획득 경로
OpenSSL은 플랫폼별로 최적의 시드 소스를 자동 선택합니다. Linux에서의 우선순위는 다음과 같습니다:
getrandom(2)시스템 콜 (OpenSSL 1.1.0+ 사용) —GRND_NONBLOCK플래그 없이 호출하여 CRNG 초기화 전 블로킹 보장. 시드로 256비트(32바이트) 이상 요청/dev/urandom폴백 —getrandom()을 사용할 수 없는 구형 커널(Linux 3.17 미만)에서 사용. OpenSSL 1.1.0 이전에는/dev/urandom이 기본 경로/dev/random추가 시드 — FIPS 모드에서drbg-seed시 사용. 일반 모드에서는 사용하지 않음
OpenSSL의 시드 획득은 rand_seed.c 또는 Provider의 seed_src에서 수행됩니다:
/* OpenSSL 3.x — default provider의 seed source (libcrypto/rand/rand_seed.c) */
/*
* Linux에서 시드를 획득하는 핵심 함수.
* 1차: getrandom(GRND_NONBLOCK=0) → CRNG 준비까지 블로킹
* 2차: /dev/urandom 읽기 (구형 커널 폴백)
*/
static int rand_pool_add_nonce_data(RAND_POOL *pool)
{
unsigned char buf[DRBG_MIN_SEED_LEN]; /* 32바이트 (256비트) */
size_t ret;
/* getrandom() 시도 — Linux 3.17+ */
ret = syscall(SYS_getrandom, buf, sizeof(buf), 0);
if (ret == sizeof(buf)) {
return rand_pool_add(pool, buf, ret, ret * 8);
}
/* /dev/urandom 폴백 */
return rand_pool_add_additional_data(pool);
}
/*
* 응용 프로그램이 호출하는 공개 API.
* 내부적으로: DRBG 인스턴스 → 시드 확인 → CTR-DRBG 출력
*/
int RAND_bytes(unsigned char *buf, int num)
{
return EVP_RAND_generate(
RAND_get0_primary(NULL), /* primary DRBG 인스턴스 */
buf, num,
DRBG_STRENGTH, /* 256비트 보안 강도 */
NULL, 0 /* 추가 입력 없음 */
);
}
DRBG 인스턴스 계층
OpenSSL 3.0은 NIST SP 800-90C의 3계층 DRBG 트리를 구현합니다:
| DRBG 인스턴스 | 역할 | 시드 소스 | 재시드 주기 |
|---|---|---|---|
| Primary DRBG (seed DRBG) | OS 시드를 직접 수신, 하위 DRBG에 시드 제공 | getrandom() / /dev/urandom |
최초 1회 + RAND_seed() 호출 시 |
| Public DRBG | RAND_bytes() 공개 난수 생성 |
Primary DRBG에서 파생 | 요청마다 자동 재시드 (reseed_interval) |
| Private DRBG | RAND_priv_bytes() 비공개 난수 (키 생성용) |
Primary DRBG에서 파생, 독립 상태 | 요청마다 자동 재시드 |
RAND_priv_bytes()는 OpenSSL 1.1.1에서 도입된 API로,
키 생성과 같은 장기 보안이 필요한 난수를 위한 독립된 DRBG 인스턴스를 사용합니다.
Public DRBG(RAND_bytes())의 상태가 노출되더라도 Private DRBG의 출력은 안전하게 유지됩니다.
이는 NIST SP 800-90C의 "prediction resistance" 요구사항을 부분적으로 만족합니다.
재시드(Reseed) 정책
OpenSSL DRBG는 다음 조건에서 자동 재시드를 수행합니다:
- 시드 풀 갱신 —
RAND_add()또는RAND_seed()호출 시 Primary DRBG가 재시드되고, 하위 DRBG도 연쇄 재시드 - 요청 횟수 임계값 —
RESEED_INTERVAL(기본값: 224 = 16,777,216회) 도달 시 - 시간 경과 —
RAND_DRBG_get_reseed_time_interval()기준 (기본 600초) - FIPS 모드 —
drbg-seed파일에서 추가 시드를 주기적으로 읽어 재시드
# OpenSSL이 getrandom()을 호출하는지 추적
strace -e trace=getrandom,openat -f openssl rand -hex 32 2>&1 | grep -E 'getrandom|urandom'
# 출력 예 (Linux 5.x + OpenSSL 3.x):
# getrandom("\x..", 48, 0) = 48 ← 384비트 시드 획득 (CRNG 준비까지 블로킹)
# (이후 CTR-DRBG 내부에서 난수 생성, 추가 시스템 콜 없음)
# FIPS 모드에서 AF_ALG 사용 확인
strace -e trace=socket,sendto,recvfrom -f openssl rand -hex 32 -provider fips 2>&1 | head -20
OpenSSH — SSH 프로토콜 난수 사용
OpenSSH는 SSH 프로토콜의 모든 단계에서 난수를 소비합니다.
OpenSSH의 난수 획득은 OpenSSL의 RAND_bytes()를 1차 경로로 사용하며,
OpenSSL이 사용 불가능한 환경에서는 자체 arc4random() 구현으로 폴백합니다.
시드 및 난수 획득 경로
OpenSSH의 seed_rng() 함수(entropy.c)는 시작 시 다음 순서로 난수 소스를 초기화합니다:
- OpenSSL
RAND_bytes()사용 가능 — OpenSSL이 내부적으로getrandom()을 호출하여 시드 획득. OpenSSH는 OpenSSL의 DRBG 출력을 그대로 사용 arc4random()폴백 — BSD 시스템 또는 OpenSSL 미연결 시.arc4random()은 ChaCha20 기반 자체 CSPRNG를 운영하며, 시드는getrandom()또는/dev/urandom에서 획득/dev/urandom직접 읽기 — 최후의 폴백. 구형 시스템에서 사용
/* OpenSSH entropy.c — seed_rng() 단순화 */
void seed_rng(void)
{
unsigned char buf[32];
/*
* 1차: OpenSSL RAND_bytes() — 내부적으로 getrandom() 호출
* OpenSSL이 컴파일 시 연결된 경우 우선 사용
*/
#ifdef WITH_OPENSSL
if (RAND_bytes(buf, sizeof(buf)) == 1)
return; /* OpenSSL DRBG에서 난수 확보 완료 */
#endif
/*
* 2차: arc4random() — libcrypto 미사용 시
* arc4random_buf()는 내부적으로 getrandom() 또는
* /dev/urandom에서 시드를 받아 ChaCha20 CSPRNG 운영
*/
arc4random_buf(buf, sizeof(buf));
}
/*
* SSH 프로토콜 쿠키 생성 — sshconnect.c / sshd.c
* 16바이트(128비트) 난수 쿠키로 서버 인증 강화
*/
static void generate_session_cookie(u_char *cookie)
{
arc4random_buf(cookie, 16); /* 128비트 SSH 쿠키 */
}
프로토콜 단계별 난수 소비
SSH 연결 수립 과정에서 난수가 사용되는 모든 지점을 추적합니다:
| SSH 프로토콜 단계 | 난수 용도 | 소비량 | 획득 경로 |
|---|---|---|---|
| 버전 교환 (Version Exchange) | 서버/클라이언트 식별 문자열의 무작위성 | 없음 (고정 문자열) | — |
| 알고리즘 협상 (KexInit) | cookie 필드 (16바이트) |
128비트 | arc4random_buf() / RAND_bytes() |
| 키 교환 (DH / ECDH / Curve25519) | DH 개인 지수 / Curve25519 스칼라 | 256~2048비트 | RAND_bytes() → OpenSSL |
| 세션 키 파생 (KDF) | 해시 계산용 K 값 (shared secret에서 파생, 난수 직접 소비 없음) | 0 (결정론적 파생) | — |
| 패킷 암호화 | 패킷 패딩 (random padding bytes) | 4~255바이트/패킷 | arc4random_buf() |
호스트 키 생성 (ssh-keygen) |
RSA 소수 p, q / Ed25519 스칼라 | 256~4096비트 | RAND_bytes() → OpenSSL |
| 재시드 (rekeying) | 새 DH 개인 지수, 새 세션 키 | 256~2048비트 | RAND_bytes() → OpenSSL |
sshd 엔트로피 기아(Starvation):
임베디드 시스템이나 컨테이너에서 sshd가 부팅 초기에 시작되면,
getrandom()이 CRNG 초기화를 기다리며 블로킹될 수 있습니다.
Linux 4.8+에서는 getrandom(GRND_NONBLOCK)이 -EAGAIN을 반환하므로
OpenSSH가 이를 감지하여 재시도하지만, 블로킹 모드 호출 시 sshd가 멈추는 현상이 발생합니다.
해결책:
systemd서비스에After=systemd-random-seed.service지정- 컨테이너 이미지에 호스트 키를 사전 생성하여 최초 부팅 시
ssh-keygen실행 회피 virtio-rng또는 hwrng 장치를 통해 초기 엔트로피 공급 가속
strongSwan — IKE/IPsec 난수 사용
strongSwan은 Linux에서 가장 널리 사용되는 IPsec/IKEv2 구현체입니다.
charon 데몬(IKE 데몬)은 libstrongswan 프레임워크의 RNG 추상화 계층을 통해
난수를 획득하며, 운영 모드에 따라 시드 소스가 다릅니다.
RNG 추상화 계층
strongSwan은 rng_t 인터페이스를 통해 세 가지 품질(Quality) 등급의 난수를 제공합니다:
| 품질 등급 | 내부 식별자 | 시드 소스 | 용도 |
|---|---|---|---|
| RNG_TRUE | RNG_QUAL_TRUE |
/dev/random (블로킹) |
장기 키 (인증서 서명 키 등). 거의 사용 안 함 |
| RNG_STRONG | RNG_QUAL_STRONG |
/dev/urandom 또는 getrandom() |
IKE nonce, DH 지수, 세션 키 — 주 사용 경로 |
| RNG_WEAK | RNG_QUAL_WEAK |
/dev/urandom (재시드 빈도 낮음) |
패딩, SPI 값 등 비보안 용도 |
/* strongSwan libstrongswan/crypto/rng.h — RNG 인터페이스 */
typedef enum {
RNG_QUAL_TRUE = 0, /* /dev/random — 블로킹, 진난수 */
RNG_QUAL_STRONG = 1, /* /dev/urandom 또는 getrandom() — CSPRNG */
RNG_QUAL_WEAK = 2, /* DRBG 내부 — 재시드 없음 */
} rng_quality_t;
/*
* charon 데몬이 IKE nonce를 생성하는 코드 경로.
* file: src/libcharon/sa/ikev2/task_v2.c
*/
static status_t build_nonce(ike_sa_t *ike_sa, chunk_t *nonce)
{
rng_t *rng;
size_t nonce_len = 32; /* 256비트 nonce (RFC 7383 권장) */
/* RNG_STRONG 품질로 RNG 인스턴스 생성 */
rng = lib->crypto->create_rng(lib->crypto, RNG_QUAL_STRONG);
if (!rng) {
return FAILED;
}
/* /dev/urandom 또는 getrandom()에서 256비트 난수 획득 */
rng->get_bytes(rng, nonce_len, nonce->ptr);
rng->destroy(rng);
return SUCCESS;
}
FIPS 모드 — AF_ALG 경로
strongSwan을 FIPS 140-2/140-3 모드로 운영할 때는 사용자 공간 라이브러리의 DRBG 대신
커널 Crypto API의 DRBG를 AF_ALG 소켓을 통해 직접 사용합니다.
이는 인증된 DRBG 구현체(crypto/drbg.c)를 사용하기 위함입니다:
socket(AF_ALG, SOCK_SEQPACKET, 0)— 알고리즘 소켓 생성bind(fd, {sa_family=AF_ALG, salg_name="drbg_nopr_ctr_aes256", ...})— DRBG 알고리즘 선택setsockopt(fd, SOL_ALG, ALG_SET_KEY, seed, seed_len)— 시드 주입 (getrandom()에서 획득)accept(fd)— operation 소켓 생성send(op_fd, input, len, MSG_MORE)+recv(op_fd, output, len, 0)— 난수 출력
# strongSwan FIPS 모드에서 AF_ALG 사용 확인
strace -e trace=socket,bind,setsockopt,accept,sendto,recvfrom \
-f charon 2>&1 | grep -E 'AF_ALG|drbg|getrandom'
# strongSwan 설정에서 RNG 품질 지정 (strongswan.conf)
# /etc/strongswan.conf
charon {
# 기본 RNG 품질 (기본값: strong)
rng_quality = strong
# FIPS 모드: 커널 crypto API DRBG 사용
# plugins/crypto/kernel-plugin 사용 시 AF_ALG 경로
crypto {
# RNG_STRONG → /dev/urandom (비FIPS) 또는 AF_ALG DRBG (FIPS)
rng_strong = "drbg_nopr_ctr_aes256"
}
}
# IKE SA 설정에서 nonce 크기 확인
# ipsec.conf 또는 swanctl.conf
connections {
ike-v2 {
version = 2
proposals = aes256gcm16-prfsha256-modp2048
# nonce_len 기본값: 32바이트 (256비트)
}
}
IKEv2 통신 단계별 난수 소비
| IKEv2 단계 | 난수 용도 | 소비량 | RNG 품질 |
|---|---|---|---|
| IKE_SA_INIT (1단계) | 개시자/응답자 nonce (Ni, Nr) | 256비트 × 2 | RNG_STRONG |
| 키 교환 (KE) | DH 개인 지수 (private exponent) | 2048비트 (MODP2048) / 256비트 (Curve25519) | RNG_STRONG |
| IKE_AUTH (2단계) | SKEYSEED 파생에 사용 (결정론적, 추가 난수 없음) | 0 (PRF 파생) | — |
| CREATE_CHILD_SA | 새 DH 지수 (PFS 사용 시), 새 nonce | 256~2048비트 | RNG_STRONG |
| INFORMATIONAL | DELETE 메시지의 SPI 값 | 32~64비트 | RNG_WEAK |
| ESP 키 파생 | KEYMAT 파생 (PRF(nonce + DH shared secret)) | 0 (결정론적 파생) | — |
| rekey (재협상) | 새 nonce + 새 DH 지수 (PFS 시) | 256~2048비트 | RNG_STRONG |
/dev/urandom에서 읽기 실패하거나 getrandom()이
-EAGAIN을 반환하면 IKE_SA_INIT 단계에서 build_nonce()가 FAILED를 반환하고
협상이 중단됩니다. 부팅 직후 컨테이너 환경에서 charon이 시작되는 경우 이 문제가 발생할 수 있으며,
systemd 서비스에 After=systemd-random-seed.service를 지정하거나
virtio-rng를 통해 초기 엔트로피를 보충해야 합니다.
기타 주요 라이브러리 — GnuTLS, NSS, libgcrypt
GnuTLS
GnuTLS는 OpenSSL의 대안으로 널리 사용되는 TLS 라이브러리로, Apache, curl, Exim 등에서 사용됩니다.
GnuTLS 3.3+에서는 getrandom(2)를 1차 시드 소스로 사용하며, 자체 DRBG 인스턴스를 운영합니다:
- 시드 소스:
getrandom()→/dev/urandom폴백 - DRBG: AES-256-CTR 기반 CTR-DRBG (NIST SP 800-90A)
- API:
gnutls_rnd(level, data, len)— 세 가지 수준 제공GNUTLS_RND_NONCE— nonce용 (재시드 빈도 낮음, 속도 우선)GNUTLS_RND_RANDOM— 일반 난수 (균형)GNUTLS_RND_KEY— 키 생성용 (매번 재시드, 보안 우선)
- 재시드:
GNUTLS_RND_KEY호출 시마다getrandom()에서 새 시드 획득
/* GnuTLS — gnutls_rnd() 사용 예 */
#include <gnutls/gnutls.h>
uint8_t session_key[32];
uint8_t nonce[12];
/* 키 생성 — 매번 getrandom()에서 재시드 (가장 안전) */
gnutls_rnd(GNUTLS_RND_KEY, session_key, sizeof(session_key));
/* nonce — DRBG 출력 (빠른 경로) */
gnutls_rnd(GNUTLS_RND_NONCE, nonce, sizeof(nonce));
NSS (Network Security Services)
NSS는 Mozilla Firefox, Thunderbird, Red Hat Directory Server 등에서 사용하는 암호 라이브러리입니다. 과거 Hash-DRBG(SHA-256)를 사용했으나, 현재는 CTR-DRBG(AES-256-CTR)로 전환했습니다:
- 시드 소스:
getrandom()(Linux 3.17+) →/dev/urandom폴백 - DRBG: CTR-DRBG (AES-256-CTR), NIST SP 800-90A 준수
- API:
PK11_GenerateRandom(data, len)— PKCS#11 인터페이스 - 특징: NSS는 자체 "freebl" 라이브러리에서 DRBG를 운영하며, 별도의 시드 데몬 없이
getrandom()에서 직접 시드 획득
libgcrypt (GnuPG)
libgcrypt는 GnuPG, dirmngr, gpg-agent 등에서 사용하는 암호 라이브러리입니다:
- 시드 소스:
getrandom()→/dev/urandom폴백 - DRBG: CTR-DRBG (AES-256-CTR) 또는 HMAC-DRBG (SHA-256), 설정에 따라 선택
- API:
gcry_random_bytes(len, level)— 세 가지 수준GCRY_WEAK_RANDOM— nonce/padding용GCRY_STRONG_RANDOM— 일반 암호학적 용도GCRY_VERY_STRONG_RANDOM— 장기 키 생성 (매번 재시드)
- 특징:
GCRY_VERY_STRONG_RANDOM사용 시/dev/random에서 추가 엔트로피를 읽어 재시드. FIPS 모드에서는rngd와 연동
공급 경로 종합 비교
| 프로그램/라이브러리 | 1차 시드 소스 | 폴백 | 내부 DRBG | 재시드 정책 | FIPS 경로 |
|---|---|---|---|---|---|
| OpenSSL 3.x | getrandom(2) |
/dev/urandom |
CTR-DRBG (AES-256) | Primary → Public/Private 분리, 요청마다 자동 | drbg-seed 파일 + FIPS provider |
| OpenSSH | OpenSSL RAND_bytes() |
arc4random() → /dev/urandom |
위임 (OpenSSL DRBG) | OpenSSL 정책 따름 | OpenSSL FIPS provider 사용 |
| strongSwan (charon) | /dev/urandom 또는 getrandom() |
/dev/random (RNG_TRUE) |
libstrongswan RNG 추상화 | RNG_STRONG: SA 설정 시, rekey 시 | AF_ALG → 커널 crypto/drbg.c |
| GnuTLS | getrandom(2) |
/dev/urandom |
CTR-DRBG (AES-256) | RND_KEY 호출마다 재시드 |
FIPS provider (커널 AF_ALG 연동) |
| NSS | getrandom(2) |
/dev/urandom |
CTR-DRBG (AES-256) | 주기적 재시드 (내부 카운터 기반) | NSS FIPS 모드 |
| libgcrypt | getrandom(2) |
/dev/urandom |
CTR-DRBG / HMAC-DRBG | VERY_STRONG 시마다 재시드 |
rngd 연동 |
| Python (os.urandom) | getrandom(2) |
/dev/urandom |
없음 (직접 CRNG 출력) | 매 호출마다 getrandom() |
— |
| Node.js (crypto) | OpenSSL RAND_bytes() |
/dev/urandom |
위임 (OpenSSL DRBG) | OpenSSL 정책 따름 | OpenSSL FIPS provider |
getrandom()은 매 시스템 콜마다 커널 CRNG에서 직접 난수를 가져오므로,
고빈도 난수 요청(TLS 핸드셰이크 폭주 등)에서 시스템 콜 오버헤드가 병목이 됩니다.
라이브러리 DRBG는 getrandom()에서 256비트 시드를 한 번 받아온 후
사용자 공간에서 결정론적으로 난수를 확장하므로, 시스템 콜 횟수를 수천 배로 줄일 수 있습니다.
단, DRBG 상태가 노출되면 이후 출력이 예측 가능해지므로, 재시드 주기와 forward secrecy 설계가 중요합니다.
실제 난수 획득 추적 — strace 실습
각 프로그램이 실제로 어떤 시스템 콜을 통해 커널에서 난수를 획득하는지 strace로 확인할 수 있습니다:
# OpenSSL: getrandom() 호출 1회 후 DRBG에서 확장
strace -e trace=getrandom,openat -f openssl rand -hex 64 2>&1 | grep -v ENOENT
# 예상 출력:
# [pid 12345] getrandom("\x..", 32, GRND_NONBLOCK) = 32 ← CRNG 준비 확인
# [pid 12345] getrandom("\x..", 48, 0) = 48 ← DRBG 시드 (256+128비트)
# (이후 64바이트는 CTR-DRBG 내부에서 생성, 추가 getrandom 없음)
# OpenSSH: ssh-keygen Ed25519 키 생성
strace -e trace=getrandom -f ssh-keygen -t ed25519 -f /tmp/test_key -N "" 2>&1
# 예상 출력:
# getrandom("\x..", 32, 0) = 32 ← OpenSSL을 통한 256비트 시드
# (Ed25519 스칼라 1개 = 256비트, DRBG에서 1회 출력)
# strongSwan: charon 데몬의 IKE nonce 생성
strace -e trace=getrandom,openat -f -p $(pgrep charon) 2>&1 | head -10
# 예상 출력 (비FIPS 모드):
# openat(AT_FDCWD, "/dev/urandom", O_RDONLY|O_CLOEXEC) = 8
# read(8, "\x..", 32) = 32 ← 256비트 nonce (Ni)
# read(8, "\x..", 32) = 32 ← 256비트 nonce (Nr)
# strongSwan FIPS 모드: AF_ALG 소켓 사용
strace -e trace=socket,bind,setsockopt,accept,sendto,recvfrom -f -p $(pgrep charon) 2>&1 | head -20
# 예상 출력:
# socket(AF_ALG, SOCK_SEQPACKET, 0) = 9
# bind(9, {sa_family=AF_ALG, salg_type="rng", salg_name="drbg_nopr_ctr_aes256"}, ...)
# setsockopt(9, SOL_ALG, ALG_SET_KEY, "\x..", 32) ← getrandom() 시드
# accept(9) = 10
# recvfrom(10, "\x..", 32, 0, ...) = 32 ← DRBG 출력
# Python: 매번 getrandom() 직접 호출 (DRBG 없음)
strace -e trace=getrandom python3 -c 'import os; os.urandom(32)' 2>&1
# 예상 출력:
# getrandom("\x..", 32, 0) = 32 ← 직접 CRNG 출력, DRBG 거치지 않음
# Node.js: crypto.randomBytes()는 OpenSSL RAND_bytes() 경유
strace -e trace=getrandom node -e 'require("crypto").randomBytes(32)' 2>&1
# 예상 출력:
# getrandom("\x..", 48, 0) = 48 ← OpenSSL DRBG 시드 (내부적으로 getrandom 호출)
# (이후 난수는 OpenSSL CTR-DRBG에서 생성)
os.urandom()과 secrets 모듈은 자체 DRBG 없이
매 호출마다 getrandom()을 직접 호출합니다. 이는 보안상 안전하지만(매번 커널 CRNG에서 직접 난수 획득),
고빈도 호출 시 시스템 콜 오버헤드가 발생합니다. 반면 OpenSSL(및 OpenSSL을 사용하는 Node.js, OpenSSH)과 GnuTLS는
DRBG를 통해 시스템 콜 횟수를 최소화합니다. Python에서 DRBG 기반 난수가 필요한 경우
cryptography 라이브러리(OpenSSL 바인딩)를 사용하면 됩니다. secrets 모듈은 os.urandom() 래퍼이므로 DRBG를 사용하지 않습니다.
테스트 및 검증
hwrng 드라이버를 로드한 후 다양한 수준의 검증을 수행해야 합니다. 기본적인 sysfs 확인부터 NIST 통계 배터리까지, 검증 단계는 드라이버 로드 확인 → 바이트 스트림 읽기 → FIPS 빠른 검증 → 정밀 통계 검증 순서로 진행합니다. 다음 다이어그램은 전체 테스트 워크플로우를 보여줍니다.
sysfs 인터페이스
# 현재 활성 hwrng 확인
cat /sys/class/misc/hw_random/rng_current
# 출력: myrng (또는 usb-hwrng)
# 사용 가능한 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
# 예상 출력 (정상 hwrng):
# 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 → 이상적 난수 표본
# Chi-square: 초과 확률 > 1%이면 양호
ent는 John Walker(Fourmilab)가 작성한 의사난수 수열 검정 프로그램(Pseudorandom Number Sequence Test Program)입니다. Debian/Ubuntu에서는 apt-get install ent로, macOS(Homebrew)에서는 brew install ent로 설치할 수 있으며 Gentoo는 sci-mathematics/ent, NixOS는 nixpkgs.ent 패키지로 제공됩니다. 소스는 공식 홈페이지(fourmilab.ch/random)에서 random.zip으로 배포되고, Fourmilab이 관리하는 최신 GitHub 저장소(github.com/Fourmilab/ent_random_sequence_tester)에서 전체 이력과 빌드 지침을 확인할 수 있습니다. 표준 ANSI C 소스이므로 아카이브를 풀고 make 한 번으로 빌드됩니다.
커널 내부 엔트로피 디버그
# 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 hwrng
# 1048576 bytes copied, 1.05 s, 998 kB/s ← USB hwrng
NIST SP 800-90B 오픈소스 검증 도구
# NIST SP 800-90B 공식 검증 도구 설치 (C++11 + OpenMP 기반)
git clone https://github.com/usnistgov/SP800-90B_EntropyAssessment.git
cd SP800-90B_EntropyAssessment
# 의존 라이브러리 설치 (Ubuntu/Debian)
apt-get install libbz2-dev libdivsufsort-dev libjsoncpp-dev libssl-dev libmpfr-dev
# 빌드 — 전체 도구 한 번에 컴파일
make
# 또는 개별 빌드: make iid / make non_iid / make restart / make conditioning
# 셀프 테스트로 컴파일 검증
cd selftest && ./selftest
# delta < 1.0E-6 이면 PASS
# 1,000,000바이트 원시 엔트로피 수집
dd if=/dev/hwrng bs=1M count=1 of=hwrng_sample.bin
# IID 추정 (독립-동일 분포 가정)
./ea_iid -i hwrng_sample.bin 8
# -i: unconditioned 초기 엔트로피 추정, 8 = bits_per_symbol
# 출력: min-entropy estimate = 7.999 bits per symbol → quality=1023 적합
# Non-IID 추정 (보수적 추정, FIPS 140-3 요건) — 대부분의 소스는 이 경로 사용
./ea_non_iid -i hwrng_sample.bin 8
# 10가지 추정기 중 최솟값이 실제 min-entropy (H_I)
# -v 옵션 추가 시 각 추정기별 상세 결과 출력
# 출력 예: min-entropy = 7.998 → H_min_per_bit = 0.9998 → quality=1023
# 컨디셔닝된 데이터 검증 (-c 옵션, bitstring 모드)
./ea_non_iid -c -t conditioned_sample.bin 1
# -c: conditioned 데이터, -t: 첫 1,000,000비트로 제한, 1 = 바이너리
# 재시작 테스트 (Restart Test) — 하드웨어 초기 조건 의존성 검증
./ea_restart -n hwrng_restart.bin 8 7.998
# -n: non-IID 경로, 마지막 인자 H_I는 순차 검증 결과값
# 파일은 "row dataset" 형식: 1,000행 × 1,000열 = 1,000,000바이트
# 컨디셔닝 컴포넌트 엔트로피 감소량 산정
./ea_conditioning -v 256 256 256 240.0
# -v: vetted, n_in=256 n_out=256 nw=256 h_in=240.0
# non-vetted: ./ea_conditioning -n 256 256 8 240.0 0.98 (마지막은 h')
-i: unconditioned 데이터 (초기 엔트로피 추정, 기본값)-c: conditioned 데이터 (bitstring 모드로만 평가)-a: 전체 데이터 사용하여 H_bitstring 평가-t: H_bitstring 평가를 첫 1,000,000비트로 제한-v: 상세 출력 (여러 번 사용 가능)-l <index>,<samples>: 오프셋에서 지정 샘플 수만 읽기
make ARCH=aarch64 CROSS_COMPILE=aarch64-linux-gnu-
SP800-90B_EntropyAssessment 툴킷 구성 (5종)
위 코드에서 사용한 ea_* 명령들은 모두 NIST 공식 C++ 툴킷 SP800-90B_EntropyAssessment(C++11 + OpenMP)에서 빌드되는 하위 도구들입니다. 툴킷은 SP 800-90B의 min-entropy 평가 방법을 충실하게 구현하며, 관리자는 NIST의 Chris Celi, 라이선스는 NIST public domain(NIST 개발 소프트웨어 공공 서비스 조건)입니다.
| 하위 도구 | 빌드 타겟 | 역할 | 해당 SP 800-90B 절 |
|---|---|---|---|
ea_iid |
make iid |
IID(독립 동일 분포) 경로 엔트로피 추정. 순열 검정 19종(§5.1) + 카이제곱 2종(§5.2.1-5.2.4) + LRS 1종(§5.2.5) = 총 22종 검정으로 IID 가설 검정 후 H_I 산출. IID 가설이 기각되면 non-IID 경로로 전환해야 함. | §3.1.3 (H_I 산출), §5.1 (순열 검정 19종), §5.2 (카이제곱 + LRS), §6.1 (IID 트랙) |
ea_non_iid |
make non_iid |
Non-IID 경로 min-entropy 추정. 10가지 추정기가 각각 독립 추정하고 그 최솟값이 실제 min-entropy(H_I)가 됨. FIPS 140-3 요건상 대부분의 하드웨어 소스는 이 경로 사용. | §3.1.3 (H_I 산출), §6.2 (Non-IID 트랙), §6.3.1~§6.3.10 (MCV, Collision, Markov, Compression, t-Tuple, LRS, MultiMCW, Lag, MultiMMC, LZ78Y) |
ea_restart |
make restart |
재시작 테스트(Restart Test). 1,000회 재시작 × 1,000샘플 row dataset로 초기 조건 의존성 정량화. 인자로 ea_non_iid 결과 H_I를 전달받아 순차 검증 수행. |
§3.1.4 Restart Testing, §3.1.4.1 Row Dataset |
ea_conditioning |
make conditioning |
컨디셔닝(Conditioning) 컴포넌트 엔트로피 감소량 산정. vetted(-v) / non-vetted(-n) 분기로 n_in·n_out·nw·h_in을 입력받아 출력 엔트로피 계산. libmpfr·libgmp 의존. |
§3.1.5 Conditioning Component |
ea_transpose |
make transpose |
데이터 전치(Data Transposition) 유틸리티. 재시작 테스트용 row dataset(1,000행 × 1,000열) 형식으로 원시 샘플을 변환. ea_restart 입력 전처리용 보조 도구. |
§3.1.4.1 Row Dataset 포맷 변환 |
SP800-90B_EntropyAssessment 세부 평가 항목 상세 해설
SP800-90B_EntropyAssessment 툴킷은 단일 프로그램이 아니라 5개 하위 도구(ea_iid, ea_non_iid, ea_restart, ea_conditioning, ea_transpose)로 구성되며, 각 하위 도구가 SP 800-90B 표준의 서로 다른 절(Section)에 대응하는 평가 항목들을 수행합니다. 아래는 각 하위 도구가 평가하는 모든 세부 항목을 방법·의미·수치 범위와 함께 열거한 해설입니다.
① ea_iid — IID 검정 세부 항목 (카이제곱 + LRS + 순열 검정 19종)
ea_iid는 엔트로피 소스가 IID(Independent and Identically Distributed, 독립 동일 분포) 특성을 만족하는지 검정합니다. IID 가설이 유지되면 Most Common Value 추정기로 HI를 산출하고, 기각되면 ea_non_iid 경로로 전환해야 합니다. ea_iid의 검정은 SP 800-90B §3.1.3에 기술된 절차에 따라 세 단계로 구성됩니다 — 순열 검정(§5.1), 카이제곱 검정(§5.2), LRS 검정(§5.2.5) 순서로 수행되며, 모든 단계에서 PASS해야 IID 가설이 유지됩니다.
iid_main.cpp 기준):
- Most Common Value 추정 — Horiginal (다비트 심볼) 및 Hbitstring (비트스트링) 산출. HI = min(Horiginal, bits_per_symbol × Hbitstring)
- 카이제곱 검정 (§5.2) — 독립성 검정 + 적합도 검정, 각각 p-value < 0.001이면 FAIL
- LRS 검정 (§5.2.5) — 최장 반복 부분문자열, Pr(X ≥ 1) ≥ 1/1000이면 PASS
- 순열 검정 (§5.1) — 19종 통계량 × 10,000회 순열, p-value < 0.001이면 FAIL
카이제곱 검정 (§5.2) — 2종
카이제곱 검정은 순열 검정과 별도로 수행되는 독립된 검정 범주입니다. 소스 코드 chi_square_tests.h에 구현되어 있으며, 두 가지 하위 검정을 수행합니다.
| # | 검정 | SP 800-90B 절 | 측정 방법 | 의미 | 판정 |
|---|---|---|---|---|---|
| 1 | Chi-Square Independence (카이제곱 독립성 검정) | §5.2.1/§5.2.3 | 인접 샘플 쌍 (xi, xi+1)의 출현 빈도로 k×k 분할표(contingency table)를 구성하고, 관측 빈도 Oij와 기대 빈도 Eij = n/(k²)의 차이를 검정: χ² = ΣΣ(Oij − Eij)²/Eij | 인접 샘플 간 독립성 검증. χ²가 크면 인접 샘플 간 상관관계 존재 = 비-IID | p-value < 0.001 → FAIL (고정 임계값, 조정 불가) |
| 2 | Chi-Square Goodness-of-Fit (카이제곱 적합도 검정) | §5.2.2/§5.2.4 | 각 값의 관측 빈도 Oi와 기대 빈도 Ei = n/k의 차이를 검정: χ² = Σ(Oi − Ei)²/Ei. 바이너리의 경우 __builtin_popcount()로 최적화된 binary_goodness_of_fit() 사용 |
균등분포 적합성 검증. 한 값이 과도하게 많거나 적으면 χ²가 커짐 = 편향 = 비-IID | p-value < 0.001 → FAIL (고정 임계값, 조정 불가) |
LRS 검정 (§5.2) — 1종
최장 반복 부분문자열(Longest Repeated Substring) 검정은 순열 검정과 별도로 수행됩니다. 소스 코드 lrs_test.h에 구현되어 있으며, 접미 배열(suffix array, libdivsufsort)과 Kasai 등의 LCP(Longest Common Prefix) O(n) 알고리즘으로 최장 반복 부분문자열의 길이 W를 계산합니다.
| 검정 | SP 800-90B 절 | 측정 방법 | 의미 | 판정 |
|---|---|---|---|---|
| LRS (Longest Repeated Substring) | §5.2.5 | 데이터에서 2회 이상 출현하는 가장 긴 부분문자열의 길이 W를 접미 배열로 계산. W와 데이터 길이 n으로부터 충돌 확률 pcolW를 산출하고, 반복이 우연히 발생할 확률 Pr(X ≥ 1) = 1 − (1 − pcolW)N을 계산. 판정 조건: log(0.999) ≥ N·log(1 − pcolW) | 긴 반복 부분문자열 = 구조적 패턴 = 비-IID. 무작위 데이터에서 W ≈ 2·logk(n) | Pr(X ≥ 1) ≥ 1/1000 → PASS. Pr(X ≥ 1) < 1/1000 → FAIL |
순열 검정 (§5.1) — 19종
순열 검정(Permutation Test)은 원본 데이터에서 각 통계량을 계산한 후, 데이터를 무작위로 섞어(shuffle) 동일 통계량을 10,000회(PERMS = 10000, utils.h에 고정) 반복 계산하여 참조 분포(reference distribution)를 구성합니다. 원본 통계량이 순열 분포에서 극단적인 영역에 위치하면 p-value가 낮아지고, p-value < 0.001이면 해당 통계량에 대해 IID 가설을 기각합니다. OpenMP 병렬 처리로 10,000회 순열을 다중 코어에서 동시에 수행합니다.
- 원본 데이터에서 19종 통계량 Tobs,j 계산 (j = 0..18)
- 데이터를 무작위로 순열(shuffle)하여 새 시퀀스 생성
- 순열된 시퀀스에서 동일 19종 통계량 Ti,j 계산
- 2~3을 10,000회 반복 (PERMS = 10000, 고정값)
- 각 통계량별로 카운터 C[j][0], C[j][1], C[j][2] 누적 — 원본보다 크/같/작 경우의 수
- p-value = min(C[j][0], C[j][2]) / (C[j][0] + C[j][1] + C[j][2])
- p-value < 0.001 → 해당 통계량에 대해 IID 가설 기각
아래 표는 소스 코드 permutation_tests.h의 test_names[] 배열에서 직접 확인한 19종 순열 검정 통계량의 정확한 목록입니다. 모든 통계량은 바이너리 및 다비트 심볼 모두에 적용되며, 바이너리 데이터의 경우 방향성/중앙값 기반 검정(2~6)은 Conversion I을, 충돌 검정(7~8)은 Conversion II를 통해 전처리됩니다.
| # | 순열 검정 통계량 (소스 코드명) | SP 800-90B 절 | 측정 방법 | 의미 | 수치 범위 |
|---|---|---|---|---|---|
| 1 | excursion (편차) | §5.1.1 | 각 지점에서의 누적 합과 평균의 편차: T = max|Σi≤k(xi − x̄)|, k = 1..n. 데이터가 평균에서 한쪽으로 치우쳐 있는지 측정 | IID 소스에서는 편차가 무작위적으로 분산해야 함. 한쪽으로 치우치면 추세 또는 편향 존재 | T ∈ [0, n×range]. p-value ∈ [0, 1] |
| 2 | numDirectionalRuns (방향성 연속 구간 수) | §5.1.2 | 연속 값들이 이전 값보다 크거나(≥) 작거나(<) 같은 방향으로 달리는 구간(run)의 총 개수. 방향 배열은 alt_sequence1()로 생성 (바이너리는 Conversion I 전처리) | 방향 변화 빈도. IID 소스에서는 약 (n+1)/2개 예상. 너무 적으면 단조 추세, 너무 많으면 과도한 교차 | T ∈ [1, n−1]. p-value ∈ [0, 1] |
| 3 | lenDirectionalRuns (방향성 연속 최장 길이) | §5.1.3 | 동일 방향(증가 또는 감소)이 연속하는 최장 구간의 길이. alt_sequence1() 기반 | 단조 증가/감소 추세 탐지. IID 소스에서는 긴 방향 연속이 극히 드묾 | T ∈ [1, n−1]. p-value ∈ [0, 1] |
| 4 | numIncreasesDecreases (증감 변화 수) | §5.1.4 | 연속 값 간 증가 횟수와 감소 횟수 중 최댓값: T = max(#increases, #decreases). alt_sequence1() 기반 | 증가/감소의 균형. IID 소스에서는 증가와 감소가 약 n/2씩 균등해야 함. 한쪽으로 치우치면 추세 | T ∈ [0, n−1]. p-value ∈ [0, 1] |
| 5 | numRunsMedian (중앙값 기준 연속 구간 수) | §5.1.5 | 데이터의 중앙값(median) 기준으로 각 값이 중앙값 이상(≥) 또는 미만(<)인지 분류하고, 동일 부호가 연속하는 구간의 총 개수. alt_sequence2() 기반 | 중앙값 기반 변동성. IID 소스에서는 중앙값 상/하 교차가 빈번해야 함 | T ∈ [1, n]. p-value ∈ [0, 1] |
| 6 | lenRunsMedian (중앙값 기준 연속 최장 길이) | §5.1.6 | 중앙값 기준 상/하 방향이 연속하는 최장 구간의 길이. alt_sequence2() 기반 | 중앙값 편향의 지속성. 긴 연속 = 한쪽으로 편향된 구간 존재 | T ∈ [1, n]. p-value ∈ [0, 1] |
| 7 | avgCollision (평균 충돌 간격) | §5.1.7 | 데이터를 순회하며 동일한 값이 등장할 때까지의 샘플 수를 기록하고, 그 평균을 산출. 충돌 = 같은 값이 다시 나타나는 지점. 바이너리는 Conversion II 전처리 | 생일 문제 기반. IID 소스에서 충돌 간격은 알파벳 크기에 따라 결정. 간격이 짧으면 빈번한 반복 = 낮은 엔트로피 | T ∈ [1, n]. p-value ∈ [0, 1] |
| 8 | maxCollision (최대 충돌 간격) | §5.1.8 | 동일한 값이 재등장할 때까지의 최대 샘플 수 (가장 긴 비충돌 구간) | 가장 긴 비충돌 구간. IID 소스에서는 이론적 범위 내에 있어야 함. #7의 극단 케이스 | T ∈ [1, n]. p-value ∈ [0, 1] |
| 9 | periodicity(1) (주기성, lag=1) | §5.1.9 | lag d=1에서 xi = xi+d인 샘플 쌍의 수를 카운트. 주기적 구조의 존재를 lag 1에서 검출 | IID 소스에서 lag d의 일치 수는 (n−d)/k에 가까워야 함. 유의하게 높으면 lag d에서의 의존성 | T ∈ [0, n−d]. p-value ∈ [0, 1] |
| 10 | periodicity(2) (주기성, lag=2) | §5.1.9 | lag d=2에서의 주기성 검정. xi = xi+2인 샘플 쌍의 수 | lag 2에서의 주기적 패턴 탐지 | T ∈ [0, n−d]. p-value ∈ [0, 1] |
| 11 | periodicity(8) (주기성, lag=8) | §5.1.9 | lag d=8에서의 주기성 검정. xi = xi+8인 샘플 쌍의 수 | lag 8에서의 주기적 패턴 탐지 (바이트 경계 등) | T ∈ [0, n−d]. p-value ∈ [0, 1] |
| 12 | periodicity(16) (주기성, lag=16) | §5.1.9 | lag d=16에서의 주기성 검정 | lag 16에서의 주기적 패턴 탐지 (워드 경계 등) | T ∈ [0, n−d]. p-value ∈ [0, 1] |
| 13 | periodicity(32) (주기성, lag=32) | §5.1.9 | lag d=32에서의 주기성 검정 | lag 32에서의 주기적 패턴 탐지 (긴 주기 구조) | T ∈ [0, n−d]. p-value ∈ [0, 1] |
| 14 | covariance(1) (공분산, lag=1) | §5.1.10 | lag d=1에서의 lagged correlation: T = Σ(xi − x̄)(xi+d − x̄). 인접 샘플 간 상관관계의 강도 측정 | IID 소스에서는 모든 lag에서 공분산이 0에 가까워야 함. 유의하게 크면 lag d에서의 선형 의존성 | T ∈ [−n×var, n×var]. p-value ∈ [0, 1] |
| 15 | covariance(2) (공분산, lag=2) | §5.1.10 | lag d=2에서의 lagged correlation | lag 2에서의 선형 의존성 탐지 | T ∈ [−n×var, n×var]. p-value ∈ [0, 1] |
| 16 | covariance(8) (공분산, lag=8) | §5.1.10 | lag d=8에서의 lagged correlation | lag 8에서의 선형 의존성 탐지 | T ∈ [−n×var, n×var]. p-value ∈ [0, 1] |
| 17 | covariance(16) (공분산, lag=16) | §5.1.10 | lag d=16에서의 lagged correlation | lag 16에서의 선형 의존성 탐지 | T ∈ [−n×var, n×var]. p-value ∈ [0, 1] |
| 18 | covariance(32) (공분산, lag=32) | §5.1.10 | lag d=32에서의 lagged correlation | lag 32에서의 선형 의존성 탐지 | T ∈ [−n×var, n×var]. p-value ∈ [0, 1] |
| 19 | compression (압축) | §5.1.11 | 데이터를 bzip2(Burrows-Wheeler Transform 기반)로 압축하고 압축된 데이터의 길이를 측정. libbz2 의존 | 압축이 잘 되면 반복/구조적 패턴 존재 = 낮은 엔트로피. 무작위 데이터는 압축이 되지 않아 길이가 원본과 비슷 | T ∈ [0, n]. p-value ∈ [0, 1] |
- 유의수준 α = 0.001 (고정값,
chi_square_tests.h및permutation_tests.h에 하드코딩). 명령행 옵션으로 조정 불가 - 순열 검정 19종(§5.1) + 카이제곱 2종(§5.2.1-5.2.4) + LRS 1종(§5.2.5) = 총 22종 검정 모두 PASS → IID 가설 유지
- 하나라도 FAIL → non-IID 경로 전환 →
ea_non_iid의 10가지 추정기 최솟값이 HI - 순열 검정 반복 수 PERMS = 10,000 (
utils.h고정값, 조정 불가) - 최소 샘플 수 MIN_SIZE = 1,000,000 (
utils.h고정값) - 바이너리 데이터는 Conversion I(방향성/중앙값 검정용) 또는 Conversion II(충돌 검정용) 전처리 후 19종 모두 적용
- 다비트 심볼(8비트 등)에서도 19종 모두 적용 — "바이너리 전용" 순열 검정은 존재하지 않음
# ea_iid 상세 실행 — 모든 검정 결과 개별 출력
./ea_iid -v -v -v hwrng_sample.bin 8
# -v 3회: 가장 상세한 출력 (각 검정별 통계량, p-value)
# -q: quiet mode (출력 감소, verbose 플래그 무시). 유의수준 조정 아님
# 검정 수행 순서 (iid_main.cpp 기준):
# 1. Most Common Value → H_original, H_bitstring 산출
# 2. Chi-Square Tests (§5.1) → independence + goodness-of-fit
# 3. LRS Test (§5.2) → longest repeated substring
# 4. Permutation Tests (§5.1) → 19종 × 10,000회 순열
# 출력 예 (8비트 심볼, n = 1,000,000):
# H_original = 7.999124
# H_bitstring = 0.999891
# Assessed min entropy: 7.999124
# ** Passed chi square tests
# ** Passed length of longest repeated substring test
# ** Passed IID permutation tests
# ─────────────────────────────────────────
# IID 검정 결과: PASS (22종 검정 모두 통과)
# H_I = 7.999124 bits per symbol (Most Common Value 추정)
# 순열 검정 19종 개별 결과 (-v -v -v 출력):
# statistic C[i][0] C[i][1] C[i][2]
# excursion 4987 101 4912
# numDirectionalRuns 5003 98 4899
# lenDirectionalRuns 4950 103 4947
# numIncreasesDecreases 4991 95 4914
# numRunsMedian 4978 110 4912
# lenRunsMedian 4985 99 4916
# avgCollision 5012 87 4901
# maxCollision 4977 105 4918
# periodicity(1) 4993 97 4910
# periodicity(2) 5001 100 4899
# periodicity(8) 4962 94 4944
# periodicity(16) 4988 103 4909
# periodicity(32) 4995 98 4907
# covariance(1) 4970 99 4931
# covariance(2) 5005 96 4899
# covariance(8) 4989 101 4910
# covariance(16) 4956 94 4950
# covariance(32) 4991 100 4909
# compression 4988 103 4909
# 각 행: C[i][0]=원본보다 큰 순열 수, C[i][1]=같은 수, C[i][2]=작은 수
# p-value = min(C[i][0], C[i][2]) / (C[i][0]+C[i][1]+C[i][2]) < 0.001 → 기각
- Conversion I: 바이너리 시퀀스를 8비트 블록으로 분할하고 각 블록의 1의 개수를 카운트하여 0~8 범위 값으로 변환. 방향성 연속 구간(#2, #3), 증감 변화(#4), 중앙값 기반 검정(#5, #6)에 사용
- Conversion II: 바이너리 시퀀스를 8비트 블록으로 분할하고 이진수로 해석하여 0~255 범위 값으로 변환. 충돌 검정(#7, #8)에 사용
- 비전환 검정(#1 excursion, #9-13 periodicity, #14-18 covariance, #19 compression)은 원본 바이너리 데이터에 직접 적용
- 다비트 심볼(8비트 등)은 변환 없이 19종 모두 원본 데이터에 직접 적용
② ea_non_iid — 10가지 추정기 상세 해설
ea_non_iid는 IID 가설이 기각되었거나, FIPS 140-3 요건상 보수적 경로를 선택하는 경우에 사용합니다. 10가지 추정기가 각각 독립적으로 min-entropy H를 추정하고, 그중 가장 낮은 값(최소값)이 해당 엔트로피 소스의 HI로 채택됩니다. 이 "최소값 채택" 원칙은 어떤 단일 추정기도 놓칠 수 없도록 모든 형태의 구조적 편향을 포착하는 다중 방어선 전략입니다.
아래는 10가지 추정기 각각의 평가 방법, 수학적 공식, 의미, 수치 범위를 상세히 해설한 내용입니다.
1. Most Common Value (MCV) Estimate — §6.3.1
적용 범위: 모든 알파벳 크기 (전체). IID 및 Non-IID 양 경로에서 항상 계산됩니다.
평가 방법: 데이터에서 가장 빈번하게 나타나는 값(최빈값)의 출현 횟수를 세고, 그 비율 pmax에 99.5% 신뢰구간 상한 보정(Z = 2.576, ZALPHA)을 적용하여 min-entropy를 산출합니다. 가장 단순하면서도 가장 보수적인 추정기로, 모든 경로의 기준이 됩니다.
수학적 공식:
p_max = count(most_common_value) / n
/* 99.5% 신뢰구간 상한 (단측) 보정, ZALPHA = 2.576 */
p' = min(1.0, p_max + ZALPHA * sqrt(p_max * (1 - p_max) / (n - 1)))
/* Z = 2.576 (ZALPHA, utils.h 고정값, 99.5% 신뢰수준) */
H = -log2(p')
/* n = 샘플 수 (1,000,000), p_max = 최빈값 비율 */
의미: 한 값이 전체 데이터에서 차지하는 비율이 높을수록 엔트로피가 낮습니다. 극단적으로 모든 샘플이 동일한 값이면 pmax = 1.0, H = 0 bits가 됩니다. 완벽한 균등분포(8비트, 256값)에서 pmax ≈ 1/256, H ≈ 8.0 bits가 됩니다. 신뢰구간 보정은 표본 크기가 유한할 때의 통계적 불확실성을 보수적으로 반영합니다.
수치 범위: H ∈ [0, log₂(k)] (k = 알파벳 크기). 8비트 심볼: H ∈ [0, 8]. H' = H / 8 ∈ [0, 1]. 신뢰구간 보정으로 인해 이론적 최댓값보다 약간 낮은 값이 산출되는 것이 정상입니다 (예: 7.999 대신 7.998).
2. Collision Estimate — §6.3.2 (바이너리 전용)
적용 범위: 비트스트링(bitstring) 모드 전용. 다비트 심볼에는 적용되지 않습니다.
평가 방법: Hagerty와 Draper[HD12]가 제안한 방법으로, 충돌(동일값 재등장)까지의 간격으로 엔트로피를 추정합니다. 비트 시퀀스에서 충돌 간격 ti를 기록하고, 표본평균 X̄와 표준편차 σ̂를 구하여 99% 신뢰하한을 산출한 후, 이진 탐색으로 가장 가능성 높은 p를 해석합니다.
수학적 공식:
/* §6.3.2 — Collision Estimate (Hagerty & Draper) */
/* 1. 충돌격 t_i 기록: 같은 값이 다시 나타날 때까지의 샘플 수 */
/* 2. 표본평균 X̄와 표준편차 σ̂ 산출 */
X_bar = (1/v) * sum(t_i);
sigma_hat = sqrt((1/(v-1)) * sum((t_i - X_bar)^2));
/* 3. 99% 신뢰하한 (Z = 2.576) */
X_prime = X_bar - 2.576 * sigma_hat / sqrt(v);
/* 4. 이진 탐색으로 p 해석: X̄' = f(p)를 만족하는 p 찾기 */
/* f(p)는 Gamma 함수 포함 복잡 수식 (표준 §6.3.2 참조) */
H = -log2(p); /* 최종 min-entropy 추정값 */
의미: 충돌 간격이 길수록 엔트로피가 높습니다. 무작위 비트열에서 충돌은 기하급수적으로 드물게 발생하며, 편향된 소스에서는 특정 비트 패턴이 빠르게 재등장합니다. 표준 §6.3.2에서 명시적으로 “only applied to binary inputs”라고 규정합니다.
수치 범위: H ∈ [0, 1] (비트당, 비트스트링 모드). 충돌 간격이 길수록 H가 1.0에 가까워집니다. 모든 샘플이 동일하면 충돌 간격 = 1, H ≈ 0.
3. Markov Estimate — §6.3.3 (바이너리 전용)
적용 범위: 비트스트링 모드 전용. 1차 마르코프 체인을 모델링합니다.
평가 방법: 비트 시퀀스를 2상태 마르코프 체인으로 모델링하여, 4가지 전이 확률 P(0→0), P(0→1), P(1→0), P(1→1)을 추정합니다. 인접 비트 간의 순차적 의존성을 정량화합니다.
수학적 공식:
/* 전이 확률 추정 */
P00 = count(0→0) / count(0→*)
P01 = count(0→1) / count(0→*)
P10 = count(1→0) / count(1→*)
P11 = count(1→1) / count(1→*)
P0 = count(0) / n; P1 = count(1) / n
/* 각 상태에서의 조건부 엔트로피 */
H0 = -P00*log2(P00) - P01*log2(P01)
H1 = -P10*log2(P10) - P11*log2(P11)
/* 보정 적용 후 min-entropy */
H = -log2(max(P0 * pow(2, -H0), P1 * pow(2, -H1))) + correction
/* correction = 0.9968... (정규 근사 보정항) */
의미: 현재 비트가 다음 비트에 영향을 미치는 정도를 측정합니다. P(0→0)이 0.5에 가까우면 독립적, 1.0에 가까우면 0에 고착, 0.0에 가까우면 강제 교대 패턴을 의미합니다. 마르코프 추정기는 1차(인접 비트 간) 의존성만 모델링하며, 고차 의존성은 MultiMMC 추정기(#9)에서 처리합니다.
수치 범위: H ∈ [0, 1] (비트당). 독립적 비트열에서 H ≈ 1.0. 완전 상관(P00=P11=1.0)이면 H = 0.0. 강제 교대(P01=P10=1.0)이면 H = 0.0 (다음 비트가 완벽히 예측 가능).
4. Compression Estimate — §6.3.4 (바이너리 전용)
적용 범위: 비트스트링 모드 전용. Lempel-Ziv 76 압축 알고리즘 기반.
평가 방법: LZ76 압축 통계량을 활용하여 데이터의 압축 가능성으로 엔트로피를 추정합니다. 압축이 잘 되는 데이터는 반복 패턴이 존재하여 엔트로피가 낮고, 무작위 데이터는 압축이 되지 않아 엔트로피가 높습니다.
수학적 공식:
/* LZ76 사전 단어 수 계산 */
num_dictionary_words = count_distinct_phrases(LZ76_parse)
/* 평균 사전 단어 길이 */
avg_word_length = n / num_dictionary_words
/* 정규 근사를 통한 엔트로피 추정 */
H = log2(n) / avg_word_length + correction
/* correction: 표본 크기에 따른 편향 보정항 */
/* n이 클수록 보정항이 0에 수렴 */
의미: 압축 가능성 = 예측 가능성 = 낮은 엔트로피. LZ76 파싱은 데이터를 순차적으로 읽으며 "이전에 본 적이 없는" 최소 길이의 구문(phrase)으로 분할합니다. 구문이 짧을수록(많을수록) 데이터가 반복적이고 압축 가능합니다. 반대로 구문이 길수록(적을수록) 데이터가 무작위적입니다.
수치 범위: H ∈ [0, 1] (비트당). 무작위 비트열에서 H ≈ 1.0. 완전히 반복적인 패턴(예: 01010101...)에서는 H가 0에 가까워집니다. avg_word_length가 log₂(n)에 가까우면 H ≈ 1.0.
5. t-Tuple Estimate — §6.3.5
적용 범위: 모든 알파벳 크기 (전체). 다비트 심볼에도 적용 가능.
평가 방법: 각 튜플 길이 t = 1, 2, 3, ...에 대해, 데이터에서 가장 빈번하게 출현하는 t-튜플(t개 연속 샘플)의 빈도를 측정합니다. 다양한 스케일에서 반복 패턴을 탐지합니다.
수학적 공식:
/* 각 튜플 길이 t에 대해 */
for t = 1 to max_t:
p_max(t) = max_count_of_any_t_tuple / (n - t + 1)
H(t) = -log2(p_max(t)) / t
/* 보정: 신뢰구간 상한 적용 */
p'(t) = p_max(t) + Z(0.99) * sqrt(p_max(t)*(1-p_max(t))/(n-t))
H'(t) = -log2(p'(t)) / t
H = min(H'(t)) over all t
의미: 특정 길이의 비트 패턴이 반복적으로 출현하면 엔트로피가 저하됩니다. t = 1은 Most Common Value와 동일하고, t = 2, 3, ...은 더 긴 패턴의 반복을 탐지합니다. t가 증가할수록 가능한 튜플 종류가 기하급수적으로 늘어나므로, 의미 있는 최대 t는 데이터 크기에 의해 제한됩니다.
수치 범위: H ∈ [0, log₂(k)] (k = 알파벳 크기). 8비트: H ∈ [0, 8]. 모든 t에 대해 H'(t)를 계산하고 최솟값을 취하므로, 어느 스케일에서든 구조가 발견되면 전체 엔트로피 추정값이 낮아집니다.
6. Longest Repeated Substring (LRS) Estimate — §6.3.6
적용 범위: 모든 알파벳 크기 (전체). 접미 배열(suffix array) 기반 고속 계산.
평가 방법: 데이터에서 2회 이상 출현하는 가장 긴 부분문자열의 길이 W를 찾습니다. 접미 배열(suffix array)과 LCP(Longest Common Prefix) 배열을 구성하여 O(n log n) 시간에 계산합니다 (libdivsufsort 라이브러리 의존).
수학적 공식:
/* W = 최장 반복 부분문자열 길이 */
W = longest_repeated_substring_length(data, n)
/* 엔트로피 추정 (정규화) */
if (W < sqrt(n))
H = n * log2(n) / (W * (n - W))
else
H = log2(n) - log2(W)
/* 보정: 표본 크기 편향 보정항 적용 */
H_corrected = H + correction_term(n, W)
의미: 긴 반복 부분문자열은 데이터에 구조적 패턴이 존재한다는 증거입니다. 무작위 데이터에서 이론적 최장 반복 길이는 W ≈ 2·logk(n) (k = 알파벳 크기)이며, 8비트 심볼 1,000,000샘플에서 W ≈ 2·log₂(1,000,000)/8 ≈ 4~5 정도가 예상치입니다. W가 이보다 유의하게 크면 비-IID입니다.
수치 범위: H ∈ [0, log₂(k)]. 8비트: H ∈ [0, 8]. W가 작을수록 H가 높고, W가 클수록 H가 낮습니다. W = 0 (반복 없음)이면 이론적 최대 H.
7. MultiMCW Prediction Estimate — §6.3.7
적용 범위: 모든 알파벳 크기 (전체). 예측 기반 추정기.
평가 방법: 4개의 서로 다른 윈도우 크기(W = 63, 255, 1023, 4095)에서 각각 가장 최근 윈도우 내의 최빈값(Most Common Value)으로 다음 샘플을 예측합니다. 예측이 적중한 비율로 엔트로피를 추정합니다.
수학적 공식:
/* 4개 윈도우 크기에 대해 독립 예측 */
windows = {63, 255, 1023, 4095}
for W in windows:
for i = W to n-1:
prediction = most_common_value(data[i-W..i-1])
if (data[i] == prediction) correct[W]++
p_global(W) = correct[W] / (n - W)
/* 보정: 예측 적중률에 대한 신뢰구간 상한 */
p'(W) = p_global(W) + correction(W, n)
H(W) = -log2(p'(W))
H = min(H(W)) over all W
의미: 최근 관찰 윈도우의 최빈값으로 다음 값을 예측합니다. 예측이 잘 맞으면(적중률이 높으면) 최근 경향이 다음 샘플을 결정한다는 의미로 엔트로피가 낮습니다. 4개 윈도우 크기로 단기(63샘플)부터 장기(4095샘플)까지 다양한 시간 스케일의 예측 가능성을 탐지합니다.
수치 범위: H ∈ [0, log₂(k)]. 8비트: H ∈ [0, 8]. 적중률 1/k (무작위)이면 H ≈ log₂(k). 적중률 1.0 (완벽 예측)이면 H = 0.
8. Lag Prediction Estimate — §6.3.8
적용 범위: 모든 알파벳 크기 (전체). 예측 기반 추정기.
평가 방법: 특정 lag d 이전의 샘플 값으로 현재 샘플을 예측합니다. lag d ∈ [1, 128] 범위에서 최적 lag를 탐색하여 가장 보수적인 추정값을 채택합니다.
수학적 공식:
/* 각 lag d에 대해 */
for d = 1 to 128:
for i = d to n-1:
prediction = data[i - d]
if (data[i] == prediction) correct[d]++
p_lag(d) = correct[d] / (n - d)
p'(d) = p_lag(d) + correction(d, n)
H(d) = -log2(p'(d))
H = min(H(d)) over d ∈ [1, 128]
의미: 주기적/순환 패턴을 탐지합니다. 특정 간격 d 이전의 값이 현재 값을 잘 예측한다면, 데이터에 주기 d의 순환 구조가 존재한다는 의미입니다. 예: 매 10번째 샘플이 항상 같은 값이면 lag = 10에서 적중률이 높게 나타납니다.
수치 범위: H ∈ [0, log₂(k)]. 8비트: H ∈ [0, 8]. 128개 lag 중 가장 보수적인 값이 채택되므로, 어느 주기에서든 순환 구조가 발견되면 전체 추정값이 낮아집니다.
9. MultiMMC Prediction Estimate — §6.3.9
적용 범위: 모든 알파벳 크기 (전체). 예측 기반 추정기. Markov Estimate(#3)의 일반화.
평가 방법: 다양한 차수 d ∈ {16, 64, 256}의 마르코프 모델(Multi-Step Markov Model with Correction)을 구축합니다. 이전 d개 샘플이 다음 샘플을 예측하는 정도를 측정합니다. Markov Estimate가 1차(인접 비트 간)만 모델링하는 반면, MultiMMC는 고차 의존성을 탐지합니다.
수학적 공식:
/* 차수 d의 마르코프 모델 구축 */
orders = {16, 64, 256}
for d in orders:
/* d개 이전 샘플로 구성된 상태 → 다음 샘플 예측 */
for i = d to n-1:
context = data[i-d..i-1]
prediction = most_common_next_value(context)
if (data[i] == prediction) correct[d]++
p(d) = correct[d] / (n - d)
/* Correction: 관측되지 않은 상태의 불확실성 반영 */
p'(d) = p(d) + correction_mmc(d, n, alphabet_size)
H(d) = -log2(p'(d))
H = min(H(d)) over d ∈ {16, 64, 256}
의미: 이전 d개 샘플의 패턴(문맥)이 다음 샘플을 예측하는 정도를 측정합니다. 차수가 높을수록 더 긴 패턴의 의존성을 탐지할 수 있지만, 동시에 가능한 상태 수가 kd로 기하급수적으로 늘어나 통계적 신뢰도가 낮아집니다. 3개 차수 중 가장 보수적인 값을 채택합니다.
수치 범위: H ∈ [0, log₂(k)]. 8비트: H ∈ [0, 8]. 고차 의존성이 존재하면 특정 차수에서 예측 적중률이 높아져 H가 낮아집니다.
10. LZ78Y Prediction Estimate — §6.3.10
적용 범위: 모든 알파벳 크기 (전체). 예측 기반 추정기. LZ78 압축 알고리즘 변형.
평가 방법: LZ78 사전(Dictionary) 구축 방식을 예측에 응용합니다(Y 변형). 관찰된 패턴으로 사전을 구축하고, 사전에서 가장 길게 매칭되는 패턴의 다음 값으로 다음 샘플을 예측합니다. Compression Estimate(#4)의 압축 기반 접근을 예측 기반으로 전환한 것입니다.
수학적 공식:
/* LZ78Y 사전 구축 + 예측 */
dictionary = {} /* LZ78 사전 */
max_dict_size = pow(2, 16) /* 최대 사전 크기 */
for i = 0 to n-1:
/* 현재 위치에서 가장 길게 매칭되는 사전 패턴 탐색 */
match = longest_match(data[0..i-1], data[i..])
if (match.next_value == data[i]) correct++
/* 사전 갱신 (Y 변형: preload length 조정) */
if (dictionary.size < max_dict_size)
dictionary.insert(data[i..i+match.length])
p = correct / n
p' = p + correction_lz78y(n, alphabet_size)
H = -log2(p')
의미: LZ78 압축 알고리즘의 사전 구축 방식을 예측에 응용합니다. 압축 가능한 데이터(반복 패턴 존재)는 사전 기반 예측이 잘 되므로 엔트로피가 낮게 추정됩니다. LZ78Y는 LZ78의 변형으로, 사전 preload 길이와 매칭 전략을 조정하여 예측 정확도를 높입니다. 사전 크기가 216 = 65,536에 도달하면 더 이상 갱신하지 않습니다.
수치 범위: H ∈ [0, log₂(k)]. 8비트: H ∈ [0, 8]. 예측 적중률이 1/k(무작위)이면 H ≈ log₂(k). 적중률 1.0(완벽 예측)이면 H = 0.
- 다비트 심볼 모드(예: 8비트 심볼,
./ea_non_iid -i data.bin 8): 추정기 1, 5, 6, 7, 8, 9, 10번 적용 (7종) - 비트스트링 모드(예:
-c -t옵션, bits_per_symbol = 1): 모든 10종 적용. 바이너리 전용 추정기 2, 3, 4번 추가 실행 - HI = min(모든 적용 추정기 결과) — 가장 보수적인 값이 최종 min-entropy
- 추정기 1~6: 통계적 분석 기반 (빈도, 패턴, 압축성). 추정기 7~10: 예측 기반 (과거 관찰로 미래 예측)
③ ea_restart — 재시작 테스트 세부 항목
ea_restart는 엔트로피 소스가 재시작(전원 차단 후 재인가, 리부트 등) 후에도 동일한 엔트로피 품질을 유지하는지 검증합니다. 핵심 우려는 초기 조건 의존성 — 하드웨어 엔트로피 소스가 부팅 시 항상 비슷한 초기 상태에서 시작한다면, 재시작 후 첫 출력이 매번 비슷해지는 치명적 보안 취약점이 됩니다.
데이터 형식 (§3.1.4.1): 재시작 테스트는 1,000×1000 재시작 행렬 M을 요구합니다. 엔트로피 소스를 r = 1,000회 재시작하고, 각 재시작에서 c = 1,000연속 샘플을 수집합니다. M[i][j]는 i번째 재시작의 j번째 샘플입니다. 이 행렬로부터 두 개의 데이터셋을 구성합니다:
- Row dataset: 행을 순서대로 연결 — M[1][1]‖...‖M[1][1000]‖M[2][1]‖...‖M[2][1000]‖...‖M[1000][1]‖...‖M[1000][1000]
- Column dataset: 열을 순서대로 연결 — M[1][1]‖...‖M[1000][1]‖M[1][2]‖...‖M[1000][2]‖...‖M[1][1000]‖...‖M[1000][1000]
ea_transpose 유틸리티로 순차 데이터를 row dataset 형식으로 변환할 수 있습니다. 단, 실제 인증에서는 진짜 재시작 데이터(1,000회 재부팅)를 수집해야 합니다.
평가 항목 3종 (§3.1.4.2~§3.1.4.3):
| # | 검정 항목 | SP 800-90B 절 | 평가 방법 | 의미 | 판정 |
|---|---|---|---|---|---|
| 1 | Sanity Check (정합성 검정) | §3.1.4.3 | 행렬 M의 행과 열에서 최빈값(Most Common Value)의 출현 빈도를 검사. HI에 기반한 기대 빈도와 비교하여 유의하게 높은지 검정 | 재시작 데이터에서 특정 값이 비정상적으로 빈번하게 출현하는지 탐지. Sanity check 실패 시 즉시 검증 실패 | 빈도가 HI 기준 유의수준 내 → PASS. 유의하게 높으면 → FAIL (엔트로피 평가 0) |
| 2 | Row dataset 엔트로피 추정 | §3.1.4.2 | Row dataset(1,000,000샘플)에 §6.1(IID 트랙) 또는 §6.2(non-IID 트랙)의 추정 방법을 적용하여 Hr 산출 | 재시작 후 연속 시퀀스의 엔트로피가 순차 데이터와 동일한지 검증 | Hr ≥ HI/2 → PASS. Hr < HI/2 → FAIL |
| 3 | Column dataset 엔트로피 추정 | §3.1.4.2 | Column dataset(1,000,000샘플)에 동일한 추정 방법을 적용하여 Hc 산출. Column dataset는 각 재시작의 동일 순서 샘플들을 모은 것 — 재시작 간 유사성 탐지 | 재시작 간 동일 시점의 샘플 분포가 순차 데이터와 동일한지 검증. Column dataset 엔트로피가 낮으면 재시작마다 비슷한 패턴이 발생한다는 의미 | Hc ≥ HI/2 → PASS. Hc < HI/2 → FAIL |
# 1단계: 1,000회 재시작 데이터 수집 (각 1,000샘플)
# 하드웨어를 1,000회 재부팅하며 각 부팅 후 1,000바이트씩 수집
# → restart_data.bin (1,000,000바이트 = 1,000행 × 1,000열 행렬 M)
# 2단계: ea_non_iid로 순차 데이터 H_I 산출 (별도 수행)
./ea_non_iid -i sequential_data.bin 8
# → H_I = 7.998 bits per symbol
# 3단계: ea_restart 실행 — H_I를 인자로 전달
./ea_restart -n restart_data.bin 8 7.998
# -n: non-IID 경로, 8 = bits_per_symbol, 7.998 = H_I (순차 검증 결과)
# 수행 순서 (표준 §3.1.4.2):
# 1. Sanity check (§3.1.4.3) — 행/열 최빈값 빈도 검정
# 2. Row dataset 엔트로피 추정 → H_r
# 3. Column dataset 엔트로피 추정 → H_c
# 출력 예:
# Sanity check: Passed
# Row dataset entropy: H_r = 7.876 bits per symbol
# Column dataset entropy: H_c = 7.912 bits per symbol
# min(H_r, H_c) = 7.876, H_I / 2 = 3.999
# ─────────────────────────────────────────
# Restart test result: PASS (min(H_r, H_c) ≥ H_I / 2)
# Final entropy = min(H_r, H_c, H_I) = min(7.876, 7.912, 7.998) = 7.876
# IID 경로로 재시작 테스트 (ea_iid 통과한 소스만)
./ea_restart -i restart_data.bin 8 7.999
# -i: IID 경로, 마지막 인자는 ea_iid 결과 H_I
- 1단계 — Sanity check (§3.1.4.3): 행렬 M의 행과 열에서 최빈값 빈도가 HI 기준 유의수준 내인지 검정. 실패 시 즉시 검증 실패, 엔트로피 평가 0
- 2단계 — Row + Column dataset 추정: 두 dataset에 §6.1(IID) 또는 §6.2(non-IID) 추정 방법 적용 → Hr, Hc 산출
- 판정: min(Hr, Hc) < HI/2 → FAIL (엔트로피 평가 0, 인증 불가)
- PASS 시 최종 엔트로피: H = min(Hr, Hc, HI) — 세 값 중 최솟값이 최종 엔트로피 평가값
- Row dataset에서 Hr이 낮으면: 재시작 후 연속 시퀀스의 엔트로피가 부족 = 워밍업 부족 등
- Column dataset에서 Hc가 낮으면: 재시작 간 동일 시점의 샘플이 유사 = 초기 조건 의존성 (치명적 취약점)
- 인자로 전달하는 HI는
ea_non_iid(또는ea_iid)의 최종 산출값이어야 함 — 임의 값 사용 시 결과 무효
④ ea_conditioning — 컨디셔닝 검증 세부 항목
ea_conditioning은 컨디셔닝(Conditioning) 컴포넌트 — 원시 엔트로피 출력의 편향을 제거하고 엔트로피를 집중시키는 후처리 단계 — 의 엔트로피 감소량을 산정합니다. SP 800-90B §3.1.5는 컨디셔닝을 vetted(검증됨, §3.1.5.1)와 non-vetted(검증되지 않음, §3.1.5.2) 두 종류로 분류합니다.
- Keyed (키 기반): HMAC (FIPS 198, FIPS 180/202 해시 함수), CMAC (SP 800-38B, AES 블록 암호), CBC-MAC (Appendix F, AES 블록 암호)
- Unkeyed (비키 기반): FIPS 180/202 승인 해시 함수 (SHA-256, SHA-512 등), Hash_df (SP 800-90A), Block_Cipher_df (SP 800-90A, AES)
| # | 검증 항목 | SP 800-90B 절 | 입력 매개변수 | 산출 공식 | 의미 |
|---|---|---|---|---|---|
| 1 | Vetted Conditioning (-v) | §3.1.5.1.2 | nin: 입력 비트 수 nout: 출력 비트 수 nw: 최소 내부 폭 (narrowest width) hin: 입력 엔트로피 (bits) |
hout = Output_Entropy(nin, nout, nw, hin) Output_Entropy()는 다단계 수식: ① Phigh = 2−hin, Plow = (1−Phigh)/(2nin−1) ② n = min(nout, nw) ③ ψ = 2nin−n·Plow + Phigh ④ U = 2nin−n + √(2n·2nin−n·ln2) ⑤ ω = U·Plow ⑥ hout = −log₂(max(ψ, ω)) |
검증된 알고리즘의 엔트로피 보존성을 수학적으로 산정. 입력 엔트로피와 내부 폭을 기반으로 출력 엔트로피의 하한을 보장. Vetted 컨디셔너는 full entropy 출력 주장 허용 |
| 2 | Non-vetted Conditioning (-n) | §3.1.5.2 | nin, nout, nw, hin h': 컨디셔닝된 순차 데이터셋의 비트당 엔트로피 추정값 (§6.1 또는 §6.2 방법으로 산출) |
hout = min(Output_Entropy(nin, nout, nw, hin), 0.999·nout, h'·nout) 0.999 곱셈항은 non-vetted 컨디셔너의 full entropy 주장을 금지하기 위한 상한. h'는 컨디셔닝된 출력을 비트스트링으로 평가하여 산출 |
검증되지 않은 컨디셔닝의 엔트로피 손실 정량화. Output_Entropy() + 0.999 상한 + 관측 엔트로피 h'·nout 중 최솟값 채택. Non-vetted는 full entropy 주장 불가 |
# Vetted 컨디셔닝 검증 (예: HMAC-SHA-256 기반)
./ea_conditioning -v 256 256 256 240.0
# -v: vetted 모드
# n_in=256, n_out=256, n_w=256, h_in=240.0
# → h_out = Output_Entropy(256, 256, 256, 240.0)
# Output_Entropy() 다단계 수식으로 정확한 h_out 산출 (libmpfr·libgmp 임의 정밀도 연산)
# 출력 예:
# Vetted Conditioning Component
# n_in = 256, n_out = 256, n_w = 256
# h_in = 240.000 bits
# h_out = 239.987 bits (Output_Entropy 산출값)
# Non-vetted 컨디셔닝 검증 (예: von Neumann 추출기)
./ea_conditioning -n 256 256 8 240.0 0.98
# -n: non-vetted 모드, h'=0.98 (컨디셔닝된 데이터셋에서 산출한 비트당 엔트로피)
# → h_out = min(Output_Entropy(256,256,8,240.0), 0.999×256, 0.98×256)
# = min(Output_Entropy(...), 255.744, 250.88)
# 0.999×n_out 항으로 인해 full entropy(256비트) 주장 불가
# h' 추정: 별도로 컨디셔닝된 데이터 샘플 수집 후
./ea_non_iid -c -t conditioned_sample.bin 1
# 컨디셔닝된 데이터를 비트스트링으로 평가 → h' = H_bitstring 산출
- Vetted 컨디셔너 (§3.1.5.1): HMAC, CMAC, CBC-MAC, 승인된 해시 함수, Hash_df, Block_Cipher_df만 vetted로 인정. Output_Entropy() 공식으로 엔트로피 산정. Full entropy 출력 주장 허용
- Non-vetted 컨디셔너 (§3.1.5.2): 위 목록 외의 모든 알고리즘. h_out = min(Output_Entropy(), 0.999·nout, h'·nout). 0.999 곱셈항으로 full entropy 주장 불가. h'는 컨디셔닝된 출력을 비트스트링으로 §6.1/§6.2 방법으로 평가하여 산출
- 추출(Truncation): Vetted 컨디셔너의 출력 절단은 허용되며 엔트로피는 비례 감소. Non-vetted 컨디셔너의 출력 절단은 엔트로피 소스 출력 전에 수행되어서는 안 됨
- 컨디셔닝을 사용하지 않는 엔트로피 소스는 이 검증이 불필요 (원시 출력 그대로 DRBG 시드로 사용)
- 추가 노이즈 소스(§3.1.6) 사용 시 반드시 vetted 컨디셔닝 사용해야 하며, 추가 소스의 엔트로피는 인정하지 않음
⑤ ea_transpose — 데이터 전치 유틸리티
ea_transpose는 평가 도구가 아닌 전처리 유틸리티입니다. ea_restart가 요구하는 row dataset(1,000행 × 1,000열) 형식으로 순차 원시 데이터를 변환합니다. 별도의 평가 항목은 없으며, 1,000,000바이트 순차 데이터를 1,000바이트씩 1,000행으로 재구성합니다.
# 순차 데이터를 row dataset으로 변환
./ea_transpose sequential_data.bin restart_data.bin
# 입력: 1,000,000바이트 순차 데이터
# 출력: 1,000행 × 1,000열 row dataset (동일 1,000,000바이트, 2차원 재구성)
# 실제 재시작 데이터를 수집한 경우 변환 불필요
# (각 재부팅 후 1,000바이트씩 순차 저장하면 이미 row dataset 형식)
- H' = 1.000 (H = 8.000, quality = 1024): 이론적 최대. 양자 RNG(QRNG) 이상적 조건. SP 800-90B 인증에서 매우 드묾
- H' ≥ 0.999 (H ≥ 7.992, quality ≥ 1022): 검증된 고품질 TRNG. Intel RDRAND/ARM FEAT_RNG 수준
- H' ≥ 0.9375 (H ≥ 7.5, quality ≥ 960): 양호한 하드웨어 RNG. FIPS 140-3 인증 적합
- H' ≥ 0.750 (H ≥ 6.0, quality ≥ 768): 보통 품질. 컨디셔닝 후 DRBG 시드로 사용 가능
- H' ≥ 0.500 (H ≥ 4.0, quality ≥ 512): 낮은 품질. 링 오실레이터 TRNG, Jitter RNG 등. 컨디셔닝 필수
- H' < 0.500 (H < 4.0, quality < 512): 위험. 엔트로피 소스로 부적합. 설계 재검토 필요
- H' = 0.000 (H = 0, quality = 0): 엔트로피 없음. PRNG이거나 고장난 소스
- Git 저장소: github.com/usnistgov/SP800-90B_EntropyAssessment (C++ 76%, C 22%, NIST public domain)
- 표준 문서: NIST SP 800-90B Final — 각 추정기·검정의 수학적 정의와 절 번호 원문
- DeepWiki 자동 문서: deepwiki.com/usnistgov/SP800-90B_EntropyAssessment — 5개 하위 도구별 알고리즘 해설
- Debian 패키지:
sp800-90b-entropy-assessment— manpage(man ea_iid등 5종) 제공
- 발행일: 2018년 1월 10일 (January 2018)
- 저자: Meltem Sönmez Turan (NIST), Elaine Barker (NIST), John Kelsey (NIST), Kerry McKay (NIST), Mary Baish (NSA), Michael Boyle (NSA)
- DOI: 10.6028/NIST.SP.800-90B
- Errata: 2025-05-29 기준 2개 errata 식별 — 향후 업데이트/개정에서 수정 예정 (상세: CSRC errata file)
- 관련 표준: SP 800-90A Rev. 1 (DRBG), SP 800-90C (RBG 구성)
- 관리자: Chris Celi (NIST), 최신 릴리스 v1.1.8 (2024-07-12)
- IG 7.18: IID 주장 시 추가 통계적 근거 필수 — 대부분의 엔트로피 소스는 IID가 아니며, IID 경로 선택은 예외적
utils.h):
ZALPHA = 2.5758293035489008— 99.5% 신뢰수준 Z값 (MCV, 예측 추정기 보정항에 공통 사용)PERMS = 10000— 순열 검정 반복 수 (고정값, 조정 불가)MIN_SIZE = 1000000— 최소 샘플 수 (100만)- 유의수준
0.001— 카이제곱·순열 검정 임계값 (chi_square_tests.h,permutation_tests.h에 고정)
NIST ESV Server (공식 검증 서버)
SP 800-90B 인증서를 실제로 발급받으려면 NIST의 ESVTS(Entropy Source Validation Test System)에 엔트로피 소스 데이터를 제출해야 합니다. ESVTS는 ACVP(Automated Cryptographic Validation Protocol) 기반의 ESVP(Entropy Source Validation Protocol) API를 제공하며, 내부적으로 위 SP 800-90B Entropy Assessment Library를 실행해 엔트로피 추정치를 산출합니다. 같은 서버가 SP 800-90C RBG(Random Bit Generator) 구성 적합성 제출도 처리합니다.
| 구성 요소 | 설명 |
|---|---|
| ESVTS 서버 | Demo(https://demo.esvts.nist.gov:7443, mTLS+TOTP 자격증명 필요)와 Prod(NVLAP 17ESV 인증 랩 전용) 환경으로 분리. 서버 코드 자체는 비공개이나 프로토콜·이슈 트래커는 GitHub에서 공개. |
| Python 클라이언트 | ESV-Server 저장소의 client/ 디렉토리에 ESVP API 호출용 Python 클라이언트 제공. 웹 기반 클라이언트도 API URL에서 이용 가능. |
| 검증 절차 | 인증 랩이 엔트로피 소스 데이터 + 설계 문서를 ESVP로 제출 → ESVTS가 SP 800-90B 평가 도구 실행 → CMVP 검토자가 적합성 판정 → CSRC에 검증 certificate 게시. |
- Git 저장소(프로토콜·클라이언트): github.com/usnistgov/ESV-Server (Python 100%, ESVP 프로토콜 문서 + Python 클라이언트)
- 프로그램 홈페이지: NIST CMVP — Entropy Validation Server
- Demo 접근:
esv-demo@nist.gov에 CSR(Certificate Signing Request) 요청, 관리자 Christopher Celi(NIST)
Python 포트 — sp800_90b
sp800_90b는 NIST 공식 C++ 코드를 pybind11로 래핑한 Python 패키지로, 오프라인 개발·CI(Continuous Integration) 자동화에서 SP 800-90B 평가를 스크립트로 호출할 때 사용합니다. NIST 원본 코드를 nist_impl 서브모듈로 포함해 동일한 알고리즘 결과를 내며, sp800_90b.Data 클래스로 .iid_tests(), .h_initial() 등을 직접 호출할 수 있습니다. PyPI 패키지명은 sp800-90b입니다.
# Python 포트 설치 (PyPI)
pip install sp800-90b
# 또는 소스 빌드 — NIST C++ 서브모듈 포함
git clone https://github.com/hnj2/sp800_90b.git
cd sp800_90b
git submodule init && git submodule update
pip install setuptools pybind11
python setup.py build && python setup.py install
import random, sp800_90b
# 1,000,000바이트 난수 샘플 생성
r = bytes(random.getrandbits(8) for _ in range(1000000))
rd = sp800_90b.Data(r)
# IID 검정 — 순열 검정 19종 수행
print(rd.iid_tests()) # True = IID 가설 기각 불가
print(rd.h_initial()) # 7.189... (bits per symbol)
- Git 저장소: github.com/hnj2/sp800_90b (C++ 72% / Python 19%, pybind11 래핑)
- PyPI: pypi.org/project/sp800-90b —
pip install sp800-90b - 문서: hnj2.github.io/sp800_90b — Sphinx API 레퍼런스
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 → 이상적 난수 표본
# 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.6 이전에는 input_pool과 blocking_pool 두 개의 풀이 존재했지만,
5.6에서 blocking_pool이 제거되어 input_pool 단일 구조가 되었고,
5.17에서는 기존 LFSR + SHA-1 기반 믹싱이 Blake2s 해시 함수로 완전 교체되었습니다.
이 섹션에서는 풀의 내부 구조, 믹싱 알고리즘, 엔트로피 추정 메커니즘을 심층적으로 분석합니다.
풀 구조의 진화: LFSR에서 Blake2s로
c1ea38a5e7c(v5.17)에서 Jason Donenfeld가 Blake2s 해시 함수로 완전 교체했으며,
이로써 풀 크기가 4096비트(512바이트)에서 256비트(32바이트)로 축소되었지만 암호학적 강도는 크게 향상되었습니다.
별도로 blocking_pool은 이미 v5.6(2020)에서 제거되어 /dev/random과 /dev/urandom이 동일한 CRNG 출력 단계를 공유하게 되었습니다.
| 항목 | 레거시 (~5.5) | 과도기 (5.6~5.16) | 현대 (5.17+) |
|---|---|---|---|
| 풀 구조 | input_pool + blocking_pool (2개) | input_pool 단일 (blocking_pool 제거) | input_pool 단일 (Blake2s 재작업) |
| 믹싱 함수 | LFSR (비트 연산 기반) | LFSR (비트 연산 기반) | Blake2s (암호학적 해시) |
| 풀 크기 | 4096비트 (512바이트) | 4096비트 (512바이트) | 256비트 (32바이트 해시 상태) |
| 추출 함수 | SHA-1 기반 extract | SHA-1 기반 extract | Blake2s final + re-absorb |
| 엔트로피 카운터 | 정수 비트 카운터 (정밀) | 정수 비트 카운터 (정밀) | init_bits 256비트 목표 |
| /dev/random 블록킹 | 엔트로피 고갈 시 블록 | CRNG 초기화 전에만 블록 | CRNG 초기화 전에만 블록 |
| forward secrecy | 미보장 (SHA-1 출력 재사용) | 미보장 (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();
}
- 정보이론적 안전성 (Information-theoretic security): XOR 혼합은 소스 중 1 개라도 예측 불가능하면 출력도 예측 불가능함을 정보이론적으로 증명합니다. 그러나 XOR 만으로는 편향 (bias) 이 제거되지 않을 수 있습니다.
- 계산적 안전성 (Computational security): Blake2s 는 계산적 안전성 가정에 의존합니다. 즉, Blake2s 의 충돌 저항성 (collision resistance) 을 깨는 것이 계산적으로 불가능하다고 가정합니다. 이는 정보이론적 증명이 아니지만, Blake2s 가 깨지지 않는 한 실용적으로 안전합니다.
- Leftover Hash Lemma: 해시 기반 추출기 (extractor) 는 엔트로피 소스의 편향을 균등 분포에 가깝게 만듭니다. Blake2s 가 강한 무작위성 추출기 (strong randomness extractor) 로 동작하여, 입력 엔트로피의 편향을 제거합니다.
- 엔트로피 보존?: 해시 함수는 정보를 축약하므로 엔트로피 1.0 이 그대로 유지된다고 할 수 없습니다. 예를 들어 고품질 하드웨어 RNG를 낮은 엔트로피 소스와 혼합해도 출력 엔트로피가 정확히 1.0 이 된다는 보장은 없습니다.
- 실용적 결론: 현대 리눅스 커널 (5.18+) 은 Blake2s 를 사용하여, 다중 엔트로피 소스의 결함이 전체 시스템 안전성을 훼손하지 않도록 합니다. 커널의 목표는 "엔트로피 1.0 유지"가 아닌 "출력의 예측 불가능성 보장"입니다. RDRAND 이 Intel 에 의해 조작되더라도, 다른 소스 (IRQ 지터, 디스크 I/O) 와 혼합되면 출력은 여전히 예측 불가능합니다.
- quality 파라미터의 한계: hwrng 드라이버의
quality값 (0~1024) 은 드라이버 개발자가 수동으로 설정합니다. NIST SP 800-90B 검증 없이quality=1024를 설정할 수 있으며, 이는 실제 엔트로피 품질을 보장하지 않습니다. - CRNG 의 결정론적 확장:
get_random_bytes()는 ChaCha20 기반 PRNG 입니다. 시드 (최대 256 비트) 의 엔트로피를 확장할 뿐, 새로운 엔트로피를 생성하지 않습니다. 예를 들어 시드 256 비트로 1000 바이트를 생성하면, 정보이론적 엔트로피는 여전히 256 비트 이하이며 비트당 약 0.032 비트입니다. - 하드웨어 RNG 의 불완전성: 실제 하드웨어 RNG는 완벽하지 않습니다. 편향 (bias), 비트 간 상관관계, 온도/전압 변동, 초기 불안정성 등이 발생할 수 있습니다.
- 결론: "단일 하드웨어 RNG = 엔트로피 1.0"은 잘못된 이해입니다. 커널은 다중 소스 혼합을 통해 단일 소스의 결함을 방어하며, 목표는 "엔트로피 1.0 유지"가 아닌 "계산적 예측 불가능성 보장"입니다.
- quality 파라미터는 검증되지 않은 주장:
quality=1024는 드라이버 개발자가 수동으로 설정한 값일 뿐, NIST SP 800-90B 검증을 거친 실제 엔트로피 품질이 아닙니다. - hwrng 원시 출력과 get_random_bytes() 는 다름: hwrng 에서 직접 읽은 원시 바이트는 quality=1024 일 수 있지만,
get_random_bytes()는 CRNG(ChaCha20 PRNG) 를 통과한 출력입니다. PRNG 는 시드 엔트로피를 결정론적으로 확장할 뿐, 새 엔트로피를 생성하지 않습니다. - 정보이론적 계산: CRNG 시드 256 비트로 1000 바이트 (8000 비트) 를 생성하면, 비트당 엔트로피는 256/8000 ≈ 0.032 비트/비트 입니다. "1.0 비트/비트"가 아닙니다.
- 올바른 이해: 커널 난수의 목표는 "엔트로피 1.0 유지"가 아닌 "계산적 예측 불가능성 보장"입니다. ChaCha20 이 깨지지 않는 한 출력은 실용적으로 안전하지만, 정보이론적 엔트로피는 1.0 이 아닙니다.
엔트로피 추정과 크레딧 정책
커널은 각 소스별로 다른 엔트로피 크레딧 정책을 적용합니다.
hwrng의 경우 quality 파라미터가 직접적으로 크레딧 계산에 사용됩니다.
| 엔트로피 소스 | 크레딧 정책 | 기여 조건 | 최대 기여량 |
|---|---|---|---|
| hwrng | bytes × quality × 8 / 1024 |
hwrng_fillfn() 스레드 주기적 호출 | quality 의존 (최대 무제한) |
| 인터럽트 지터 | fast_pool 64회/1초마다 input_pool 혼합 (크레딧 별도 산정) | TSC/사이클 카운터 차이 | 초당 수십 비트 |
| 디스크 I/O | 타이밍 델타 기반 | 자동 수집 (별도 설정 불필요) | 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 초기 시딩 |
| 지연 | ~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 | 60초 또는 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() |
KCMVP(한국 암호모듈 검증프로그램)와 커널 RNG
KCMVP(Korea Cryptographic Module Validation Program)는 국가정보원(NIS, National Intelligence Service)이 운영하는 한국의 암호모듈 검증 제도입니다. 미국 FIPS 140과 동일한 ISO/IEC 19790을 기반 표준으로 채택하면서, 한국 고유 암호 알고리즘(ARIA, SEED, LEA, LSH, KCDSA, EC-KCDSA)과 한국 표준(TTAK)을 검증 대상으로 추가한 것이 핵심 특징입니다. 이 섹션에서는 KCMVP의 난수발생기 관련 요구사항과 Linux 커널 hwrng 서브시스템의 관계를 분석합니다.
KCMVP 법적 근거 및 기반 표준
KCMVP는 국가·공공망에서 사용되는 암호모듈의 안전성과 구현 적합성을 검증합니다. 정보보호시스템(DB 암호화, VPN, SSO, DRM 등)에 KCMVP 검증필 암호모듈 탑재가 필수이며, 이는 FIPS 140이 미국 연방 정부에 요구하는 것과 동일한 구조입니다.
| 항목 | KCMVP | FIPS 140-3 (비교) |
|---|---|---|
| 법적 근거 | 전자정부법 제56조, 시행령 제69조, 사이버안보 업무규정 제9조, 암호모듈 시험 및 검증지침 | FISMA, FIPS 140-3 표준 |
| 모듈 보안 요구사항 | KS X ISO/IEC 19790:2015 (2025-11-18 확인) | ISO/IEC 19790:2025 (동일 기반) |
| 모듈 시험 요구사항 | KS X ISO/IEC 24759:2015 | ISO/IEC 24759:2025 |
| 난수발생기 표준 | KS X ISO/IEC 18031 (2023-12-08 확인, ISO/IEC 18031:2011 IDT) | NIST SP 800-90A/B/C |
| 검증기관 | 국가정보원 (NIS) | NIST + CSEC (캐나다) |
| 시험기관 | 공공: NSR(국가보안기술연구소), KISA / 민간: 한국시스템보증, 한국정보보안기술원, 한국정보통신기술협회 | NVML (NIST 인정 민간 검증 실험실) |
| 보안 등급 | 1~4등급 (KS X ISO/IEC 19790 기반) | Level 1~4 (ISO/IEC 19790 기반, 동일) |
| 구현안내서 | GVI(Guideline for Validation Implementation) Part 1 | Implementation Guidance (IG) |
검증대상 난수발생기 알고리즘
KCMVP는 3종 DRBG(Deterministic Random Bit Generator)를 검증대상으로 지정합니다. NIST SP 800-90A와 알고리즘 구조가 동일하지만, 한국 해시함수 LSH를 지원하는 것이 핵심 차이점입니다. KS X ISO/IEC 18031은 ISO/IEC 18031:2011과 일치(IDT)하는 한국 표준으로, 난수발생기의 개념적 모델과 DRBG 메커니즘을 규정합니다.
| 분류 | 알고리즘 | 국내 참조표준 | 지원 해시/암호 | NIST 대응 |
|---|---|---|---|---|
| 해시함수 기반 (Hash_DRBG) |
Hash_DRBG (SHA-2) | TTAK.KO-12.0331-Part2 | SHA-224/256/384/512 | SP 800-90A Hash_DRBG |
| Hash_DRBG (LSH) | TTAK.KO-12.0331-Part3 | LSH-224/256/384/512 | KCMVP 고유 (NIST에 없음) | |
| Hash_DRBG (SHA-3) | TTAK.KO-12.0331-Part4 | SHA3-224/256/384/512 | SP 800-90A Hash_DRBG | |
| HMAC 기반 (HMAC_DRBG) |
HMAC_DRBG (SHA-2) | TTAK.KO-12.0332-Part2 | HMAC-SHA-2 | SP 800-90A HMAC_DRBG |
| HMAC_DRBG (LSH) | TTAK.KO-12.0332-Part3 | HMAC-LSH | KCMVP 고유 (NIST에 없음) | |
| HMAC_DRBG (SHA-3) | TTAK.KO-12.0332-Part4 | HMAC-SHA-3 | SP 800-90A HMAC_DRBG | |
| 블록암호 기반 (CTR_DRBG) |
CTR_DRBG | TTAK.KO-12.0189/R1 | ARIA, SEED, LEA, AES | SP 800-90A CTR_DRBG |
난수발생기 신규 시험방법론 (2022년 6월 의무 적용)
2022년 1월 5일 KISA는 "난수발생기 신규 시험방법론"을 발표하고, 2022년 6월 1일부터 의무 적용했습니다. 이 방법론은 종전의 DRBG 알고리즘 구현 적합성 검증만 수행하던 방식에서, 잡음원(Noise Source) → 엔트로피 소스(Entropy Source) → DRBG 3단계 전체를 검증하는 방식으로 전환했습니다. 이는 NIST SP 800-90B의 엔트로피 소스 검증 접근법과 동일한 방향입니다.
적용 기술표준 문서:
| 표준번호 | 명칭 | 역할 |
|---|---|---|
| TTAK.KO-12.0235 | 운영체제별 잡음원 수집 및 운용지침 | 잡음원, 엔트로피 소스, DRBG 운영방법 규정. OS별 사용 가능한 잡음원(RTC 지터, 디스크 타이밍, 프로세스 스케줄링 등)과 수집 방식 정의 |
| TTAK.KO-12.0341/R1 | 소프트웨어 암호모듈에 사용되는 잡음원 시험평가 지침 | 잡음원 엔트로피 평가 절차. 통계 검사(randomness) + 엔트로피 추정(entropy estimation) 수행. NIST SP 800-90B 알고리즘 활용 |
| TTAK.KO-12.0306/R1 | 소프트웨어 환경에서의 잡음원 엔트로피 평가 알고리즘 | 엔트로피 추정 알고리즘 규정. TTAK.KO-12.0341에서 참조 |
GVI Part 1 개정 (2025년 12월)
2025년 12월 10일, 국가정보원과 NSR은 KCMVP 기준 문서인 "암호모듈 구현안내서(GVI) Part 1"을 개정 공개했습니다. 난수발생기 보안강도 요건 강화와 PQC(양자내성암호, Post-Quantum Cryptography) 하이브리드 방식 허용이 핵심 내용입니다.
RBG 보안강도 요건 (신설 조항 9.5)
GVI Part 1 신설 조항 9.5는 암호모듈에 탑재된 난수발생기(RBG)가 해당 모듈이 제공하는 암호 서비스 중 최대 보안강도 이상을 지원해야 한다고 규정합니다.
| 암호 서비스 | 보안강도 | RBG 요구 보안강도 | 적용 예시 |
|---|---|---|---|
| ARIA-128 | 128비트 | ≥ 128비트 | Hash_DRBG(SHA-256) — 256비트 지원, 요건 충족 |
| ARIA-256 | 256비트 | ≥ 256비트 | Hash_DRBG(SHA-256) — 256비트, 요건 충족 |
| ARIA-256 | 256비트 | ≥ 256비트 | Hash_DRBG(SHA-224) — 224비트, 요건 미충족 |
| RSA-2048 | 112비트 | ≥ 112비트 | CTR_DRBG(ARIA-128) — 128비트, 요건 충족 |
| EC-KCDSA P-256 | 128비트 | ≥ 128비트 | HMAC_DRBG(LSH-256) — 256비트, 요건 충족 |
자가시험 요건 강화
GVI 개정으로 암호모듈은 주기적 시험 요청 시 다음 두 가지 자가시험을 모두 수행할 수 있는 수단을 제공해야 합니다:
| 자가시험 유형 | 시점 | 검사 내용 | NIST 대응 |
|---|---|---|---|
| 동작 전 자가시험 (Pre-operational Self-test) |
모듈 초기화 시 | DRBG KAT(Known Answer Test), 알고리즘 무결성(Integrity) 검증 | SP 800-90A Section 4 — Power-up test |
| 조건부 자가시험 (Conditional Self-test) |
특정 조건 발생 시 | DRBG 상태 비일관성 감지, 연속 동일 출력 감지(CRNGT) | SP 800-90A Section 4 — Conditional test |
PQC 하이브리드 방식 허용 (부속서 C.5)
GVI Part 1 부속서 C.5에 따라, 개발업체는 기존 검증대상 암호알고리즘인 키 설정(DH/ECDH)이나 전자서명(RSA-PSS/KCDSA/EC-KCDSA/ECDSA) 기술에 PQC 알고리즘(ML-KEM, ML-DSA 등)을 결합하여 운용할 수 있습니다.
Linux 커널과 KCMVP의 관계
Linux 커널의 hwrng 서브시스템과 KCMVP의 관계는 FIPS 140과 동일한 구조를 가집니다. 커널 자체는 KCMVP 검증 대상이 아니며, 검증 대상은 사용자 공간 암호모듈 또는 하드웨어 보안 모듈(HSM)입니다.
| KCMVP 모듈 유형 | 엔트로피 획득 경로 | 커널 hwrng 의존도 | 엔트로피 평가 대상 |
|---|---|---|---|
| 소프트웨어 모듈 (Linux 사용자 공간) |
getrandom() 또는 /dev/urandom → 커널 CRNG |
높음 — 커널 hwrng 품질이 직접 영향 | TTAK.KO-12.0341: OS 잡음원 평가 |
| 하드웨어 모듈 (HSM, 보안칩) |
내장 TRNG (SoC TRNG, PUF 등) | 낮음 — 자체 TRNG 사용 | 내장 TRNG의 엔트로피 평가 |
| 혼합형 모듈 (소프트웨어 + hwrng) |
커널 hwrng + 자체 잡음원 결합 | 중간 — hwrng가 보조 엔트로피 소스 | 양쪽 모두 평가 (TTAK.KO-12.0341) |
getrandom()로 엔트로피를 획득하는 경우,
TTAK.KO-12.0235(운영체제별 잡음원 수집 및 운용지침)에 따라 Linux 커널의 잡음원 구성을
문서화하고 TTAK.KO-12.0341/R1로 엔트로피를 평가해야 합니다.
커널 hwrng의 quality 파라미터, input_pool의 Blake2s 혼합, ChaCha20-CRNG의
Fast Key Erasure 메커니즘이 엔트로피 평가 결과에 직접 영향을 미칩니다.
특히 임베디드 Linux에서 hwrng가 없는 경우, TTAK.KO-12.0235에 명시된 OS 잡음원
(RTC 지터, 디스크 I/O 타이밍, 프로세스 스케줄링 지터 등)만으로 충분한 엔트로피를
확보해야 하므로, 엔트로피 평가 통과가 더 어려울 수 있습니다.
/*
* KCMVP 검증 소프트웨어 모듈의 난수 획득 경로 (Linux 환경)
*
* 1. 모듈 내부 DRBG (Hash_DRBG/HMAC_DRBG/CTR_DRBG)
* - TTAK.KO-12.0331/0332/0189 표준 알고리즘 구현
* - KS X ISO/IEC 18031 준거
*
* 2. DRBG 시드 = 엔트로피 소스 출력
* - 경로 A: getrandom() → 커널 ChaCha20-CRNG (hwrng 기반)
* - 경로 B: 모듈 자체 잡음원 (TTAK.KO-12.0235 지침 준수)
* - 경로 C: 하드웨어 TRNG (내장 칩)
*
* 3. 엔트로피 평가 (TTAK.KO-12.0341/R1)
* - NIST SP 800-90B 알고리즘 사용
* - 통계 검사: monobit, runs, poker, IID/non-IID
* - 엔트로피 추정: min-entropy H_∞ 산출
*
* 4. 자가시험 (GVI 9.5)
* - 동작 전: DRBG KAT 수행
* - 조건부: 연속 동일 출력 감지 (CRNGT)
* - 보안강도: 모듈 최대 보안강도 ≥ RBG 보안강도
*/
/* KCMVP 모듈에서 커널 getrandom() 호출 예시 */
static int kcmvp_get_entropy(uint8_t *buf, size_t len)
{
/*
* TTAK.KO-12.0235에 따른 잡음원 수집 경로.
* getrandom()은 CRNG 초기화 전에 블록되므로
* 안전한 엔트로피가 보장됨.
*
* 주의: /dev/urandom 직접 open + read 대신
* getrandom() syscall 사용 권장 (파일 디스크립터
* 남용 및 경쟁 조건 방지).
*/
ssize_t ret = syscall(SYS_getrandom, buf, len, 0);
if (ret != (ssize_t)len)
return -1;
/* 수집된 엔트로피를 모듈 내부 DRBG 시드로 사용 */
return 0;
}
KCMVP 난수 관련 참고 자료
공식 기관 및 제도
- KISA 암호모듈검증 개요 — KCMVP 제도 개요, 검증필 암호모듈 활용, 법적 근거
- KISA 시험·검증절차 — 7단계 검증 절차, 제출물 목록 (엔트로피 평가 결과서 포함)
- KISA 검증대상 암호알고리즘 — 난수발생기 검증대상 알고리즘 (Hash_DRBG, HMAC_DRBG, CTR_DRBG) 및 참조표준 전체 목록
- KISA 암호모듈검증 자료실 — 암호모듈 제출물 작성 안내서, 난수발생기 신규 시험방법론 FAQ, 검증제도 FAQ
- KISA 난수발생기 신규 시험방법론 FAQ (2022.1.5) — 2022년 6월 1일 의무 적용, TTAK.KO-12.0235/12.0341 적용 기술표준
- 암호모듈 검증 관리 및 사전검증 서비스 — KCMVP 온라인 검증 관리 포털
- 국가사이버안보센터 — 암호모듈 검증 — 검증필 암호모듈 탑재 필수 제품 유형, 보안적합성 검증
기반 표준
- KS X ISO/IEC 19790 — 암호모듈 보안 요구사항 — KCMVP 모듈 보안 요구사항 기반 표준 (2025-11-18 확인)
- KS X ISO/IEC 18031 — 난수발생기 — KCMVP 난수발생기 기반 표준 (ISO/IEC 18031:2011 IDT, 2023-12-08 확인)
- TTAK.KO-12.0341/R1 — 소프트웨어 암호모듈 잡음원 시험평가 지침 — NIST SP 800-90B 활용 잡음원 엔트로피 평가 절차 (2020-12-10)
- ISO/IEC 18031:2025 — Random bit generation (제3판) — RBG2/RBG3 구조, 난수발생기 국제 표준 최신판
- ISO/IEC 19790:2025 — Cryptographic module security requirements — 암호모듈 보안 요구사항 국제 표준
GVI 개정 및 PQC 전환
- 국정원 암호모듈 구현안내서(GVI) Part 1 개정 보도 (2025.12.10) — RBG 보안강도 요건 강화(9.5항), PQC 하이브리드 방식 허용(부속서 C.5), 자가시험 요건 구체화
- KISA 암호모듈 제출물 작성 안내서 (2025.9. 개정) — GVI 개정에 따른 제출물 작성 가이드라인 최신판
최초 인증 사례
- ICTK G3K 보안칩 KCMVP 인증 (2022.7.13) — 신규 시험방법론으로 검증된 국내 최초 사례, PUF 기술 접목, 2등급
- ICTK G3K 칩 KCMVP 획득 (데이터넷) — 난수발생기 신규 시험방법론 의무화 후 최초 검증 상세 보도
관련 본 문서 섹션
- NIST SP 800-90B 인증 — KCMVP 엔트로피 평가가 활용하는 NIST SP 800-90B 검증 절차 상세
- FIPS 140-2/140-3 인증과 커널 RNG — KCMVP와 동일한 ISO/IEC 19790 기반의 미국 인증 제도
- 최신 동향 (2024-2026) — PQC 하이브리드 TLS, ML-KEM/ML-DSA 난수 소비 증가
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)로 게스트에 공급하는 난수 대역폭을 제한할 수 있습니다.
-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바이트)
인증 서비스 부팅 시 엔트로피 기아(Starvation)
부팅 직후 인증 서비스가 시작될 때 CRNG 미초기기 상태와 충돌하는 현상은
운영 환경에서 빈번하게 발생합니다. sshd, Kerberos KDC, OpenVPN, IPsec IKE 데몬 등은
시작 시점에 즉시 난수가 필요하며, CRNG가 준비되지 않았으면 getrandom() 호출이 블록됩니다.
이 블로킹은 서비스 시작 지연으로 나타나거나, 타임아웃 로직이 잘못 작성된 경우
안전하지 않은 난수로 폴백하는 치명적 보안 결함으로 이어질 수 있습니다.
| 인증 서비스 | 엔트로피 사용 시점 | 블로킹 영향 | 권장 조치 |
|---|---|---|---|
| sshd | 호스트 키 로드, 세션 키 교환 | 첫 연결 수락 지연, sshd: getrandom() blocked 로그 |
ConditionPathExists=/proc/sys/kernel/random/entropy_avail |
| Kerberos KDC | TGT/TGS 세션 키 생성 | 인증 요청 타임아웃, 대규모 인프라에서 장애 확산 | KDC를 systemd-random-seed.service 이후 시작 |
| OpenVPN / IKEv2 | Diffie-Hellman 일시 키, IKE SA nonce | 터널 설정 실패, 피어 타임아웃 | After=systemd-random-seed.service 의존성 명시 |
| nginx / Apache (TLS) | 세션 티켓 키 회전, TLS 1.3 key schedule | 첫 요청 핸드셰이크 지연, 503 오류 | hwrng 장치 또는 virtio-rng 보장 |
| HashiCorp Vault | Seal/Unseal 키, 봉인 키(Seal Key) 생성 | 초기화 실패, 자동 봉인 해제(Unseal) 불가 | HSM 또는 hwrng 기반 엔트로피 보강 |
getrandom() 블로킹을 회피하기 위해
time(), getpid(), clock_gettime() 조합으로 난수를 "생성"하는 폴백 경로를 가집니다.
이 경로가 활성화되면 도전값(Challenge)이 예측 가능해져 인증 전체가 무력화됩니다.
Linux 3.17 이전에는 /dev/urandom이 CRNG 초기화 전에도 난수를 반환하여 이 문제가 널리 퍼져 있었습니다.
현대 getrandom()은 이 폴백을 원천 차단하지만, 서비스 코드가 GRND_NONBLOCK으로
-EAGAIN을 받은 후 안전하지 않은 경로로 빠지는 경우는 여전히 발생합니다.
# 인증 서비스의 getrandom() 블로킹 추적 (strace)
strace -e trace=getrandom -f -p $(pgrep -o sshd) 2>&1 | head -20
# CRNG 초기화 완료 대기 후 서비스 시작 (systemd 유닛 예시)
# /etc/systemd/system/sshd.service.d/override.conf
[Unit]
After=systemd-random-seed.service
Wants=systemd-random-seed.service
[Service]
# CRNG 준비 전까지 시작 지연 (타임아웃 방지)
ExecStartPre=/bin/sh -c 'while [ $(cat /proc/sys/kernel/random/entropy_avail) -lt 256 ]; do sleep 0.1; done'
# 컨테이너 환경: 호스트 virtio-rng 또는 hwrng 확인
ls -la /dev/hwrng 2>/dev/null && echo "hwrng available" || echo "hwrng missing — 엔트로피 보강 필요"
# random.trust_cpu=1 부트 파라미터: RDRAND를 즉시 credit
# 장점: CRNG 초기화 시간 < 100ms (인증 서비스 즉시 시작 가능)
# 단점: CPU RNG가 신뢰할 수 없는 경우 가정 하에 설계 (Intel RDRAND 신뢰 논쟁)
cat /proc/cmdline | grep -o 'random.trust_cpu=[0-9]'
컨테이너 환경 인증 서비스 특별 고려사항
OCI 컨테이너(Docker, Podman, containerd)는 호스트 커널의 CRNG를 공유하므로,
호스트 CRNG가 초기화된 이후에 시작되는 컨테이너는 즉시 안전한 난수를 얻을 수 있습니다.
하지만 컨테이너 오케스트레이터가 수백 개 TLS 엔드포인트를 동시에 초기화하는 경우,
모든 컨테이너가 같은 CRNG에서 동시에 난수를 추출하여 per-CPU ChaCha20 키 회전이 집중됩니다.
이는 CRNG 자체의 안전성에는 영향을 주지 않지만, crng_reseed() 주기(60초) 내에
재시드가 발생하지 않아 일시적 처리율 저하가 나타날 수 있습니다.
After=systemd-random-seed.service + Wants=systemd-random-seed.service 조합으로
부팅 시 저장된 엔트로피 시드가 복원된 후 인증 서비스가 시작됩니다.
임베디드/IoT에서는 After=systemd-random-seed.service만으로 부족할 수 있으며,
hwrng 드라이버 로드 완료를 보장하는 ConditionPathExists=/dev/hwrng 추가를 권장합니다.
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 bbbbbbbb nnnnnnnn nnnnnnnn ← 64비트 카운터 + 64비트 논스
*/
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)
위 코드에서 chacha20_block_generic()는 인자로 받은 64바이트 state를 그대로 입력으로
사용합니다(memcpy(x, state, 64)). 그렇다면 이 64바이트 state는 어디서, 무엇으로
채워지는가가 핵심 질문입니다. 정답은 crng_fast_key_erasure()가 매 난수 요청마다 state를
4가지 요소로 조립한다는 것입니다. 아래 다이어그램은 이 조립 과정과 각 요소의 출처를
추적합니다.
crng_fast_key_erasure()가 매 난수 요청마다
chacha_init_consts()로 상수를, crng->key로 256비트 키를,
memset(&x[12], 0, 16)으로 64비트 카운터와 64비트 논스를 0으로 채워 64바이트 state를 만듭니다. 이 state가
chacha20_block()의 유일한 입력이며, 함수는 이를 20라운드 처리해 64바이트 키스트림을
출력합니다. Key와 Seed를 별도로 설명하는 이유는 SEED → Key → (상수+카운터+논스와 함께)
블록 입력이라는 파이프라인을 분해해 보여주기 위함입니다.
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);
}
/* crng_fast_key_erasure()가 state 조립 + 블록 생성 + 키 교체를 모두 수행:
* chacha_init_consts() → 상수(word 0-3), crng->key → 키(word 4-11),
* memset(&x[12], 0, 16) → 64비트 카운터(word 12-13) + 64비트 논스(word 14-15) */
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만+ 호출/초 처리율을 달성합니다.
SEED 주입 파이프라인 심층 분석
ChaCha20 CRNG가 안전한 난수를 생성하려면 시드(Seed) — 즉 256비트 암호학적 엔트로피 — 가 정확한 경로로 주입되어야 합니다. 시드가 없는 CRNG는 결정론적 기계에 불과하므로, 시드 주입 파이프라인은 CRNG 보안의 출발점이자 기반입니다. 이 절에서는 엔트로피 소스에서 출발하여 input_pool(Blake2s)을 거쳐 per-CPU CRNG 키까지 도달하는 전체 시드 주입 흐름을 단계별로 추적하고, 부팅 시 초기 시드 주입과 런타임 재시드가 각각 어떻게 Forward Secrecy의 기반을 형성하는지 분석합니다.
시드 주입 파이프라인 전체 구조
Linux 커널 RNG의 시드 주입은 3단계 파이프라인으로 구성됩니다. 각 단계는 서로 다른 역할을 담당하며, 단계 사이의 데이터 이동은 모두 단방향으로 설계되어 역추적을 차단합니다.
| 단계 | 컴포넌트 | 역할 | 출력 |
|---|---|---|---|
| 1단계: 수집 | 엔트로피 소스 (hwrng, RDSEED, 인터럽트 타이밍, jitterentropy) | 물리적 비결정론적 비트를 수집하여 커널에 전달 | 원시 엔트로피 비트 (품질 변동) |
| 2단계: 컨디셔닝 | input_pool (Blake2s 해시 상태) | 다양한 소스의 원시 비트를 암호학적 해시로 혼합·압축하여 균일한 256비트 시드 생성 | 256비트 시드 (압축된 엔트로피) |
| 3단계: 주입 | base_crng → per-CPU CRNG | 컨디셔닝된 시드를 ChaCha20 키로 로드하여 CRNG 상태 초기화 또는 갱신 | 32바이트 ChaCha20 키 (CRNG 운용 키) |
extract_entropy() 호출 시 풀 상태를 변경(re-keying)하여
동일한 시드가 두 번 추출되지 않습니다. 이 단방향성이 Forward Secrecy의 전제 조건입니다.
부팅 시 초기 시드 주입 — crng_reseed()
커널 부팅 초기에는 엔트로피가 거의 없으므로, CRNG는 미초기화(uninitialized)
상태로 시작합니다. crng_ready 플래그가 false인 동안
getrandom() 시스템 콜은 블로킹되거나 GRND_NONBLOCK 플래그가
있으면 -EAGAIN을 반환합니다. 이 게이팅 메커니즘이 낮은 엔트로피
상태에서 예측 가능한 키가 생성되는 것을 방지합니다.
부팅 시 시드 주입은 다음 순서로 진행됩니다:
/* drivers/char/random.c — 부팅 시 초기 시드 주입 순서 */
/*
* 1단계: 아주 초기 부팅 (init/main.c → start_kernel())
* - random_init() 호출
* - 아키텍처 의존 시드: RDSEED/RDRAND에서 가능한 만큼 수집
* - random-seed 파일(UEFI Runtime Services 또는 bootloader 전달) 로드
* - 아직 input_pool의 엔트로피 크레딧은 0에 가까움
*/
void random_init(const char *command_line)
{
unsigned long random_seed;
/* 아키텍처 RNG에서 초기 시드 획득 (RDSEED 우선, RDRAND 차선) */
if (!arch_get_random_seed_long(&random_seed))
arch_get_random_long(&random_seed);
/* 명령줄 해시 + 아키텍처 시드를 input_pool에 혼합 */
mix_pool_bytes(command_line, strlen(command_line));
add_early_randomness(command_line);
}
/*
* 2단계: 엔트로피 누적 대기
* - 인터럽트 발생마다 add_interrupt_randomness()가 input_pool에 기여
* - hwrng 드라이버 로드 시 hwrng_init() → add_early_randomness()
* - jitterentropy 모듈이 CPU 지터를 수집하여 input_pool에 추가
* - input_pool.init_bits가 256(=32바이트)에 도달할 때까지 대기
*/
/*
* 3단계: 256비트 엔트로피 누적 달성 → crng_reseed() 호출
* - _credit_init_bits()가 try_cmpxchg로 256비트 도달 감지
* - crng_reseed() → extract_entropy()로 input_pool에서 32바이트 시드 추출
* - 추출된 시드를 base_crng.key에 로드
* - crng_init = CRNG_READY 설정 → getrandom() 블로킹 해제
* - pr_notice("crng init done") 출력
*/
/* crng_reseed() — 초기화와 런타임 재시드 모두 이 함수 하나가 처리 */
static void crng_reseed(struct work_struct *work)
{
static DECLARE_DELAYED_WORK(next_reseed, crng_reseed);
u8 key[CHACHA_KEY_SIZE]; /* 32바이트 */
/* 다음 재시드 예약 (60초 후) */
if (likely(system_dfl_wq))
queue_delayed_work(system_dfl_wq, &next_reseed, crng_reseed_interval());
/* input_pool에서 256비트 시드 추출 (Blake2s HKDF 확장) */
extract_entropy(key, sizeof(key));
/* 시드를 마스터 CRNG 키로 로드 → 이 시점부터 CRNG 가동 */
spin_lock_irqsave(&base_crng.lock, flags);
memcpy(base_crng.key, key, sizeof(base_crng.key));
WRITE_ONCE(base_crng.generation, next_gen);
/* 스택 잔류 시드 소거 — Forward Secrecy의 첫 번째 조치 */
memzero_explicit(key, sizeof(key));
/* CRNG_READY 전환 → getrandom() 블로킹 해제 */
if (!static_branch_likely(&crng_is_ready))
crng_init = CRNG_READY;
spin_unlock_irqrestore(&base_crng.lock, flags);
wake_up_interruptible(&crng_init_wait);
pr_notice("crng init done\n");
}
/*
* 4단계: per-CPU CRNG 최초 동기화
* - 최초 get_random_bytes() 호출 시 crng_make_state()가 실행
* - per-CPU crng->generation(초기값 0)이 base_crng.generation(≥1)과 불일치
* - base_crng.key를 per-CPU crng->key로 복사 → 모든 CPU에 시드 전파
* - 이후 각 CPU는 자체 Fast Key Erasure로 독립적으로 키 갱신
*/
init_bits가 256에 도달하는 데
수 초에서 수 분이 걸릴 수 있습니다. 이 기간 동안 getrandom()이 블로킹되면
부팅이 지연되는 "엔트로피 기아(Starvation)" 현상이 발생합니다. 최신 커널(5.18+)은
RANDOM_DEFAULT_INIT_CRNG와 BLAKE2s 기반 초기 시드를 통해 이 문제를
완화하며, virtio-rng로 호스트 엔트로피를 조기에 주입하는 것이 권장됩니다.
런타임 시드 갱신 — crng_reseed()와 엔트로피 크레딧
초기 시드 주입 후에도 CRNG는 주기적으로 새로운 시드를 주입받아야 합니다. 이는 Forward Secrecy의 파트너 속성인 Prediction resistance(예측 저항)을 회복하는 메커니즘입니다. 재시드가 일어나지 않으면 공격자가 한 시점의 키를 덤프한 뒤 정방향으로 모든 미래 출력을 예측할 수 있기 때문입니다.
재시드는 두 가지 조건 중 하나가 충족되면 발생합니다:
| 재시드 조건 | 상수 | 의미 | 엔트로피 소스 |
|---|---|---|---|
| 시간 기반 | CRNG_RESEED_INTERVAL (약 60초) |
마지막 재시드로부터 60초 경과 시 강제 재시드 | 해당 시간 동안 input_pool에 누적된 모든 엔트로피 |
| 엔트로피 기반 | CRNG_RESEED_THRESH |
input_pool에 새로운 충분한 엔트로피가 누적된 경우 조기 재시드 | hwrng 대량 기여, RDSEED 고속 수집 등 |
/* drivers/char/random.c — 런타임 재시드 흐름 */
#define CRNG_RESEED_INTERVAL (60 * HZ) /* 약 60초 (CONFIG_HZ=1000 기준 60000 ticks) */
/*
* 재시드 필요성 판단 — crng_make_state() 내에서 호출
* 1) 시간 조건: 현재 시각 - base_crng.init_time이 CRNG_RESEED_INTERVAL 초과
* 2) 세대 조건: per-CPU crng->generation != base_crng.generation
* 둘 중 하나라도 true면 crng_reseed() 수행
*/
static void crng_reseed(struct crng *crng)
{
u8 key[CHACHA_KEY_SIZE];
unsigned long flags;
/* input_pool에서 새 256비트 시드 추출
* extract_entropy()는 Blake2s 파이널화 + 출력을 풀에 재혼합
* → 풀 상태가 변경되므로 동일한 시드가 두 번 나오지 않음 */
extract_entropy(key, sizeof(key));
/* 새 시드로 base_crng 키 교체
* 이전 키는 새 키로 덮어쓰기 → 메모리에서 소거
* → 이전 키로는 미래 출력 예측 불가 (Prediction resistance 회복) */
spin_lock_irqsave(&base_crng.lock, flags);
memcpy(base_crng.key, key, sizeof(base_crng.key));
WRITE_ONCE(base_crng.generation, ++base_crng_generation);
spin_unlock_irqrestore(&base_crng.lock, flags);
/* 스택 잔류 시드 소거 */
memzero_explicit(key, sizeof(key));
/* 재시드 시각 기록 → 다음 60초 카운트 시작 */
base_crng.init_time = jiffies;
}
/*
* per-CPU CRNG로의 시드 전파 (lazy synchronization)
* 재시드 직후 즉시 모든 CPU에 전파하지 않고, 각 CPU의 최초 난수 요청 시
* generation 불일치를 감지하여 그때 키를 복사 → "지연 동기화"
*
* 이 설계의 이점:
* - 재시드 시 전역 락을 짧게 유지 (base_crng.lock만)
* - 각 CPU는 자신의 local_lock만으로 독립 동작
* - 유휴 CPU는 재시드 비용을 지불하지 않음
*/
/* crng_make_state() 내부 (앞선 NUMA 절의 코드 참조):
* if (crng->generation != base_crng.generation) {
* memcpy(crng->key, base_crng.key, sizeof(crng->key));
* crng->generation = base_crng.generation;
* }
*/
input_pool의 시드 컨디셔너 역할
input_pool은 시드 주입 파이프라인에서 컨디셔너(Conditioner) 역할을 담당합니다. 엔트로피 소스들이 제공하는 원시 비트는 품질이 균일하지 않고, 일부 소스는 편향(Bias)을 가질 수 있습니다. input_pool은 Blake2s 암호학적 해시 함수를 사용하여 이러한 불균일한 입력을 균일한 256비트 시드로 압축합니다.
컨디셔닝 과정이 중요한 이유는 다음과 같습니다:
- 편향 제거: 인터럽트 타이밍의 하위 비트는 특정 패턴을 가질 수 있으나, Blake2s의 혼합 연산(Add-Rotate-XOR)이 편향을 분산시켜 균일한 출력 생성
- 엔트로피 농축: 다수의 낮은 품질 소스를 조합하여 높은 품질의 시드를 생성 — "엔트로피 풀"이라는 이름의 유래
- 단방향성: 추출된 시드에서 input_pool의 원시 엔트로피를 역산할 수 없음 — Forward Secrecy의 제1단계 보장
- 재추출 방지:
extract_entropy()호출 시 풀 상태를 변경(re-keying)하여 동일한 시드가 두 번 생성되지 않음
/* drivers/char/random.c — input_pool의 시드 컨디셔닝 과정 */
/* input_pool은 Blake2s 해시 상태로 구현 (Sponge 구조와 유사) */
static struct blake2s_state input_pool;
/*
* 엔트로피 주입 — 모든 소스가 이 함수를 통해 input_pool에 기여
* add_interrupt_randomness() → 인터럽트 타이밍
* add_hwgenerator_randomness() → hwrng 드라이버
* add_device_randomness() → 부팅 시 아키텍처 시드
* add_disk_randomness() → 디스크 I/O 타이밍
* add_input_randomness() → 입력 장치 이벤트
*/
void mix_pool_bytes(const void *buf, size_t len)
{
unsigned long flags;
spin_lock_irqsave(&input_pool.lock, flags);
/* Blake2s 업데이트 — 입력을 해시 상태에 혼합
* 이 연산은 단방향이므로 입력을 역추적할 수 없음 */
blake2s_update(&input_pool, buf, len);
spin_unlock_irqrestore(&input_pool.lock, flags);
}
/*
* 엔트로피 크레딧 부여 — 소스가 자신의 엔트로피 기여량을 선언
* quality 파라미터(0~1024)가 높을수록 더 많은 비트 크레딧을 받음
* init_bits가 256에 도달하면 crng_reseed() 트리거
* CRNG ready 후에는 매크로 가드로 no-op → 런타임 재시드는 60초 타이머가 처리
*/
#define credit_init_bits(bits) if (!crng_ready()) _credit_init_bits(bits)
static void _credit_init_bits(size_t nbits)
{
unsigned int new, orig, add;
add = min_t(size_t, nbits, POOL_BITS);
orig = READ_ONCE(input_pool.init_bits);
do {
new = min_t(unsigned int, POOL_BITS, orig + add);
} while (!try_cmpxchg(&input_pool.init_bits, &orig, new));
/* 128비트 도달: CRNG_EARLY / 256비트 도달: CRNG_READY + crng_reseed() */
if (orig < POOL_READY_BITS && new >= POOL_READY_BITS)
crng_reseed(NULL); /* input_pool에서 시드 추출 → CRNG 초기화 */
}
/*
* 시드 추출 — input_pool에서 컨디셔닝된 32바이트 시드를 꺼냄
* 1) Blake2s 파이널화로 256비트 해시 출력 생성
* 2) 출력을 input_pool에 재혼합 → 풀 상태 변경 (재추출 방지)
* 3) 반환된 시드는 base_crng.key 또는 재시드 키로 사용
*/
static void extract_entropy(void *buf, size_t nbytes)
{
struct blake2s_state recovery;
spin_lock_irqsave(&input_pool.lock, flags);
/* 1) 현재 풀 상태 스냅샷 */
recovery = input_pool;
/* 2) Blake2s 파이널화 → 256비트 해시 출력 (시드) */
blake2s_final(&recovery, buf);
/* 3) 출력을 풀에 재혼합 → 동일한 상태에서 같은 시드 재추출 방지
* 이 "재혼합" 단계가 Forward Secrecy의 핵심:
* 시드 추출 후 풀 상태가 변경되므로, 이전 시드를 알아도
* 다음 시드를 예측할 수 없음 */
blake2s_update(&input_pool, buf, nbytes);
spin_unlock_irqrestore(&input_pool.lock, flags);
memzero_explicit(&recovery, sizeof(recovery));
}
extract_entropy()는 시드를 반환한 직후, 그 시드를 다시 input_pool에
혼합합니다. 이로 인해 풀 상태가 변경되므로, 공격자가 이전 시드를 알더라도
다음 시드를 예측할 수 없습니다. 이는 재시드 간의 독립성을 보장하여,
한 재시드 시점의 키가 유출되어도 다음 재시드의 키는 안전하게 유지됩니다.
"출력을 다시 입력으로" 순환시키는 이 구조는 Sponge 구조의 영감을 받았습니다.
hwrng/RDSEED의 시드 기여 구조
하드웨어 RNG는 시드 주입 파이프라인에서 가장 신뢰할 수 있는 엔트로피 소스입니다. 소프트웨어 기반 엔트로피(인터럽트 타이밍, jitterentropy)는 환경에 따라 품질이 변동하지만, 하드웨어 TRNG는 물리적 과정(열 잡음, 광자 양자 효과, 발생기 잡음)에 기반하므로 환경에 독립적인 엔트로피를 제공합니다.
| 하드웨어 소스 | 엔트로피 경로 | quality | 시드 주입 시점 | Forward Secrecy 기여 |
|---|---|---|---|---|
| hwrng 드라이버 (TPM, PCIe RNG, virtio-rng) |
hwrng_get_data() → add_hwgenerator_randomness() → mix_pool_bytes() + credit_init_bits() |
드라이버 선언 (보통 512~1024) | 부팅 시 조기 시드 가속 + 런타임 재시드 크레딧 | 독립 물리 엔트로피 주입 → Prediction resistance 회복 가속 |
| RDSEED (x86 온다이 TRNG) |
arch_get_random_seed_long() → add_device_randomness() → mix_pool_bytes() + credit_init_bits() |
전체 비트 (원시 엔트로피) | 부팅 최초 시드 (random_init()에서 즉시 사용) |
상태 없음 → FS 직접 기여 없음, 단 시드 독립성에 직접 기여 |
| RDRAND (x86 온다이 DRBG) |
arch_get_random_long() → mix_pool_bytes() (크레딧 제한적) |
제한적 (DRBG 출력이므로 원시 엔트로피 아님) | 보조 엔트로피 — 단독으로 init_bits 달성 불가 | RDRAND 자체 FS(내부 키 교체)가 있으나 커널은 보수적으로 취급 |
| ARM RNG (AArch64 RNDR/RNDRRS) |
arch_get_random_seed_long() → RDSEED와 동일 경로 |
전체 비트 (원시 엔트로피) | 부팅 시드 + 런타임 재시드 | RDSEED와 동일 — 시드 독립성에 직접 기여 |
/* drivers/char/hw_random/core.c — hwrng에서 input_pool로의 시드 주입 */
/*
* hwrng 드라이버가 하드웨어에서 읽은 난수를 커널 RNG에 주입
* 이 함수는 hwrng kthread에 의해 주기적으로 호출되거나
* rng-tools 데몬이 /dev/hwrng를 읽을 때 트리거됨
*/
void add_hwgenerator_randomness(const char *buffer, size_t count,
size_t entropy)
{
/* 1) 원시 난수를 input_pool에 혼합 (Blake2s) */
mix_pool_bytes(buffer, count);
/* 2) 엔트로피 크레딧 부여 — quality에 기반하여 init_bits 증가
* hwrng의 quality가 높을수록 더 많은 비트가 크레딧되어
* 빠른 시드 주입(부팅)에 기여. CRNG ready 후에는 no-op */
if (entropy)
credit_init_bits(entropy);
/* 3) throttle: CRNG ready 후에는 재시드 간격만큼 대기
* 런타임 재시드는 60초 주기 delayed_work가 독립 처리 */
if (sleep_after && !kthread_should_stop() &&
(crng_ready() || !entropy))
schedule_timeout_interruptible(crng_reseed_interval());
}
/* hwrng kthread — 주기적으로 하드웨어에서 난수를 읽어 input_pool에 주입
* 기본 주기: 1초 (HWRNG_DEFAULT_INTERVAL)
* input_pool의 엔트로피가 부족하면 더 자주, 충분하면 덜 자주 호출 */
static int hwrng_fillfn(void *unused)
{
while (!kthread_should_stop()) {
size_t entropy, bytes_read;
u8 buffer[32];
bytes_read = hwrng_get_data(current_rng, buffer, sizeof(buffer), ...);
entropy = min_t(size_t, bytes_read * 8,
current_rng->quality * bytes_read / 1024);
add_hwgenerator_randomness(buffer, bytes_read, entropy);
memzero_explicit(buffer, bytes_read);
schedule_timeout_interruptible(HWRNG_KTHREAD_INTERVAL);
}
return 0;
}
quality
파라미터는 시드 주입 속도에 직접 영향을 미칩니다. quality=1024인
TPM RNG는 읽은 바이트 수만큼의 엔트로피 비트를 크레딧받아 init_bits가
빠르게 256에 도달하지만, quality=100인 저품질 RNG는 10배 더 많은
데이터를 읽어야 같은 크레딧을 받습니다. quality가 0이면 엔트로피 크레딧 없이
mix_pool_bytes()만 수행하여 시드 주입 속도에 기여하지 못합니다.
SEED 주입과 Forward Secrecy의 관계 정리
시드 주입과 Forward Secrecy는 서로 보완하는 두 축으로 CRNG 보안을 구성합니다. 시드 주입이 "독립성"을 제공한다면, Fast Key Erasure는 "소거"를 제공합니다. 두 메커니즘이 함께 작동해야 완전한 Forward Secrecy + Prediction resistance가 달성됩니다.
| 구분 | 시드 주입 (Seed Injection) | Fast Key Erasure |
|---|---|---|
| 담당 메커니즘 | extract_entropy() + crng_reseed() | crng_fast_key_erasure() |
| 보안 속성 | Prediction resistance (미래 보호) — 새 독립 엔트로피로 키 교체 | Forward Secrecy (과거 보호) — 출력마다 키 소거 |
| 발생 빈도 | 약 60초 (CRNG_RESEED_INTERVAL) 또는 엔트로피 누적 시 | 매 64바이트 출력마다 |
| 엔트로피 소비 | input_pool에서 32바이트 소비 (재혼합으로 풀 상태 변경) | 엔트로피 소비 없음 — 기존 키의 ChaCha20 출력으로 자체 갱신 |
| 독립성 보장 | O — 새 시드는 이전 키와 독립 (input_pool의 새 엔트로피에 기반) | X — 새 키는 이전 키의 함수 (ChaCha20 출력) → 정방향 계산 가능 |
| 소거 보장 | X — 이전 키를 새 키로 덮어쓰지만, 역산성 자체는 ChaCha20에 의존 | O — 이전 키를 memzero_explicit()로 명시적 소거 |
| 단독 달성 | Prediction resistance만 달성 — 과거 출력은 여전히 역산 가능 | Forward Secrecy만 달성 — 미래 출력은 예측 가능 |
| 협력 달성 | 둘 다 있을 때 완전한 보안: 과거 출력 역산 불가(FS) + 미래 출력 예측 불가(PR) | |
SEED/Re-SEED → Key → 출력 전체 데이터 흐름
지금까지 개별 절에서 시드 주입, 키 관리, Fast Key Erasure를 각각 살펴보았습니다. 이 절에서는 이 세 메커니즘이 하나의 파이프라인으로 어떻게 연결되어 동작하는지 단일 다이어그램으로 정리합니다. 이 흐름은 ChaCha20-CRNG의 전체 구현 골격이며, 각 단계가 어떤 데이터를 소비하고 어떤 데이터를 출력하는지 추적할 수 있습니다.
Forward Secrecy 심층 분석
Forward Secrecy(전방 비밀성)는 hwrng → input_pool → ChaCha20 CRNG 파이프라인이 제공하는 가장 중요한 보안 속성 중 하나입니다. 앞선 절들에서 Fast Key Erasure와 reseed가 각각 이 속성에 기여하는 모습을 산발적으로 살펴보았지만, 이 절에서는 Forward Secrecy를 단일 주제로 모아 위협 모델, 수학적 근거, 실패 시나리오, 그리고 상위 프로토콜의 PFS(Perfect Forward Secrecy)와의 연결까지 체계적으로 정리합니다.
Forward Secrecy vs Backward Secrecy — 정확한 구분
NIST SP 800-90A는 DRBG의 상태 노출 시나리오에서 두 가지 저항성(Resistance)을 정의합니다. 두 용어는 방향이 반대이므로 혼동하기 쉽지만, 커널 RNG 설계에서는 서로 다른 메커니즘이 담당합니다.
| 속성 | NIST 표기 | 의미 | 커널 RNG 담당 메커니즘 | 적용 단위 |
|---|---|---|---|---|
| 전방 비밀성 | Backtracking resistance | 현재 내부 상태가 공격자에게 노출되어도 과거에 이미 생성된 출력을 역산할 수 없음 | Fast Key Erasure (출력마다 키 교체) | 매 64바이트 출력 블록 |
| 후방 비밀성 | Prediction resistance | 현재 내부 상태가 노출된 뒤 미래 출력이 예측 불가능해지는 성질 (재시드 후 회복) | reseed (CRNG_RESEED_INTERVAL, 약 60초) — input_pool의 새 엔트로피로 키 갱신 | 재시드 주기 |
위협 모델 — 메모리 덤프 공격자
Forward Secrecy의 위협 모델은 전체 메모리 스냅샷을 확보한 공격자입니다. 구체적으로 다음 능력을 가정합니다:
- 커널 메모리 덤프(
/proc/kcore,crash유틸리티, kdump, 하이퍼바이저(Hypervisor) 메모리 introspection)로 per-CPU CRNG의key[32]와generation, 그리고 ChaCha20 카운터를 읽을 수 있음 - input_pool의 Blake2s 해시 상태(
h[8],t[2])와init_bits크레딧도 함께 노출 - 과거 출력 스트림(네트워크 패킷의 nonce, TLS 핸드셰이크 random, 세션 키 등)을 수집하고 있음
이 공격자 모델에서 Forward Secrecy가 보장하는 것은 "덤프 시점의 키로는 이미 반환된 과거 출력을 재현하거나 그 키를 역산할 수 없다"는 점입니다. 반면 후방 비밀성(Prediction resistance)은 보장하지 않으므로, 덤프 시점의 키로 다음 reseed 전까지의 미래 출력은 계산 가능합니다. 이 차이가 reseed 주기 설계의 핵심 동기입니다.
Fast Key Erasure의 수학적 근거
Fast Key Erasure는 Daniel J. Bernstein이 스트림 암호 설계에서 제안한 기법으로, 출력을 생성할 때마다 키를 "소거(Erasure)"하여 단방향성을 확보합니다. 핵심 논증은 다음과 같습니다:
/*
* Fast Key Erasure의 단방향성 근거
*
* 상태 전이: K_i --[ChaCha20_block]--> (K_{i+1} || O_i)
* K_{i+1} = ChaCha20(K_i)[0..31] (앞 32바이트, 새 키)
* O_i = ChaCha20(K_i)[32..63] (뒤 32바이트, 사용자 출력)
* 이후 K_i는 memzero_explicit()로 메모리에서 소거
*
* 공격자가 시점 i+1의 메모리를 덤프해 K_{i+1}을 얻었다고 가정.
* 과거 출력 O_i를 재현하려면 K_i가 필요하지만, K_i는 이미 소거됨.
* K_{i+1}로부터 K_i를 역산하려면 ChaCha20의 역함수가 필요한데,
* ChaCha20은 ARX(Add-Rotate-XOR) 20라운드로 구성된 의사난수 함수이므로
* 실용적으로 역산 불가능 (one-way). 따라서 O_i는 복구되지 않음 → Forward Secrecy.
*
* 반면 K_{i+1}로부터 K_{i+2}, O_{i+1}은 정방향 계산 가능 → Prediction resistance
* 실패. 이는 다음 reseed가 K를 새 엔트로피로 갱신할 때까지 지속됨.
*/
리시드 주기와 Forward Secrecy의 세분성
Fast Key Erasure는 출력 블록 단위(64바이트마다) Forward Secrecy를 제공하지만, Prediction resistance는 재시드 주기 단위(약 60초)로만 회복됩니다. 즉 공격자가 시점 t에 키를 덤프하면, t 이전의 모든 출력은 안전하지만 t부터 다음 재시드까지의 출력은 예측 가능합니다. 이를 "세분성(Granularity)의 비대칭"이라 부릅니다.
/* drivers/char/random.c — 재시드가 Prediction resistance를 회복하는 지점 */
/* CRNG_RESEED_INTERVAL: 약 60초 (CONFIG) */
#define CRNG_RESEED_INTERVAL (60 * HZ)
static void crng_reseed(struct crng *crng)
{
u8 key[CHACHA_KEY_SIZE];
/* input_pool에서 32바이트 신규 엔트로피 추출 (Blake2s 압축) */
extract_entropy(key, sizeof(key));
/* 새 엔트로피로 키를 갱신 → 노출된 구 키로는 미래 출력 예측 불가 */
memcpy(crng->key, key, sizeof(key));
WRITE_ONCE(crng->generation, ++base_crng_generation);
memzero_explicit(key, sizeof(key));
}
/*
* 재시드 직후 Prediction resistance 회복 흐름:
* 시점 t: 공격자가 K_t 덤프 → O_t, O_{t+1}, ... 정방향 예측 가능
* 시점 t+r: crng_reseed() 실행 → K_{t+r} = extract_entropy(input_pool)
* K_{t+r}는 input_pool의 신규 엔트로피에 의존하므로
* K_t로부터 독립 → 이후 출력 예측 불가능 (Prediction resistance 회복)
* 단, O_t..O_{t+r-1} 구간은 여전히 예측 가능했음 (회복 불가)
*/
이 비대칭 때문에 GRND_RANDOM이나 높은 보안 등급을 요구하는 환경에서는
DRBG의 Prediction Resistance 모드(drbg_pr_*)처럼 매 요청마다
재시드하는 옵션을 고려할 수 있습니다. 하지만 Linux CRNG는 기본적으로
60초 재시드를 채택하여 성능과 보안의 균형을 잡습니다. FIPS 140-3 Level 4
(물리적 보안)처럼 Prediction resistance가 강제되는 환경에서는 DRBG PR 모드를
별도로 사용합니다.
Forward Secrecy 실패 시나리오
Forward Secrecy는 메모리에서 키가 "소거"되고 "역산 불가능"하다는 전제 위에 성립합니다. 다음 시나리오는 이 전제를 깨뜨려 Forward Secrecy를 무력화하거나 약화시킬 수 있으므로 운영 시 주의가 필요합니다.
| 시나리오 | Forward Secrecy 영향 | 완화 방법 |
|---|---|---|
| VM 스냅샷/롤백(Rollback) | 게스트 CRNG 상태를 스냅샷으로 보존한 뒤 롤백하면, 롤백 직후 동일한 키로 동일한 출력이 재생성됨. Forward Secrecy뿐 아니라 예측 저항도 붕괴 | VIRTIO_RNG(virtio-rng)로 호스트 엔트로피 주입; 게스트는 롤백 후 vm.genid 감지 시 CRNG 재초기화; 클라우드 이미지는 random-seed 재생성 |
| 최대절전(Hibernate)/Suspend-to-disk | 메모리 이미지가 디스크에 저장되고 부팅 시 복원되면 CRNG 키가 그대로 이어짐. 복원 직후 과거/미래 출력이 예측 가능해지는 창 발생 | 커널은 resume 시 crng_reseed()를 강제 수행하여 키를 갱신; resume notifier로 별도 처리 |
| random-seed 파일 재사용 | /var/lib/systemd/random-seed가 동일한 내용으로 두 번 로드되면(예: 읽기 전용 루트, 컨테이너 이미지 복제) 동일한 초기 키가 재생성되어 Forward Secrecy 무력화 |
systemd-random-seed.service는 로드 직후 파일을 덮어쓰기/삭제; 읽기 전용 루트에서는 seed를 매 부팅마다 고유하게 생성하거나 hwrng/RDSEED 의존 |
| kdump / 크래시 덤프 | 크래시 덤프에 CRNG 키가 포함되면, 덤프 시점의 키로 그 이전 출력은 안전하지만 이후(덤프 캡처 중 생성된) 출력은 노출될 수 있음. 덤프 파일 자체가 민감 자료 | kdump 이미지에서 CRNG 메모리 영역 마스킹 또는 덤프 암호화; 덤프는 신뢰할 수 있는 보관소만 취급 |
| fork()/COW 후 자식 프로세스 | 사용자 공간 DRBG(OpenSSL 등) 상태가 COW로 복제되면 부모·자식이 동일한 난수 스트림을 생성 — 커널 CRNG 자체는 영향이 없으나 응용 단 Forward Secrecy 붕괴 | OpenSSL 1.1.0+는 fork() 감지 시 DRBG 자동 재시드; 커널 getrandom()는 매 호출이 독립 키이므로 커널 단은 안전 |
| 메모리 잔류(Stale memory) | memset()은 컴파일러 최적화로 제거될 수 있어 키가 메모리에 잔류 → 덤프로 과거 키 복구 가능 → Forward Secrecy 위협 |
커널은 항상 memzero_explicit() 사용(최적화 방지); 사용자 공간도 explicit_bzero() 또는 OPENSSL_cleanse() 사용 |
vm.genid 또는 virtio-rng를 통해 게스트가 롤백을 감지하고 CRNG를
재초기화하도록 구성해야 합니다. 게스트가 롤백을 감지하지 못하면 Forward Secrecy와
Prediction resistance가 동시에 붕괴합니다.
RDSEED vs RDRAND와 Forward Secrecy
x86 하드웨어 RNG 명령인 RDSEED와 RDRAND는 Forward Secrecy에 서로 다른 기여를 합니다. 두 명령의 차이가 커널 RNG의 Forward Secrecy 보장에 어떻게 연결되는지 정리합니다.
| 구분 | RDRAND | RDSEED |
|---|---|---|
| 출력 성격 | DRBG 출력(가공된 난수) | 원시 엔트로피(비가공) |
| Forward Secrecy | 보장 — 매 출력마다 내부 DRBG 키 교체(공개 문서 기준) | 해당 없음 — 엔트로피 자체는 상태가 없음 |
| 커널 사용 위치 | arch_get_random_long() → input_pool 보조 엔트로피 + CRNG 시드 보강 | arch_get_random_seed_long() → input_pool의 시드 크레딧으로만 사용 (DRBG를 거치지 않음) |
| 예측 저항 기여 | 간접 — DRBG 재시드를 통해 | 직접 — 새로운 독립 엔트로피 주입으로 reseed 시 Prediction resistance 회복 |
즉 RDRAND는 자체적으로 Forward Secrecy를 가지지만 커널은 이를 엔트로피 소스로만 취급하고, 실제 Forward Secrecy는 커널 CRNG의 Fast Key Erasure가 담당합니다. RDSEED는 상태가 없는 원시 엔트로피이므로 Forward Secrecy 개념이 적용되지 않으며, 대신 reseed 시 input_pool에 신규 독립 엔트로피를 공급하여 Prediction resistance 회복을 가속합니다. 두 명령을 조합 사용하는 이유는 RDSEED의 원시 엔트로피(독립성)와 RDRAND의 가공된 난수(처리율) 장점을 모두 취하기 위해서입니다.
커널 RNG Forward Secrecy → 프로토콜 PFS
커널 CRNG의 Forward Secrecy는 그 자체로 끝나지 않고, 상위 인증·키 교환
프로토콜의 PFS(Perfect Forward Secrecy)를 뒷받침하는 기반이
됩니다. 프로토콜 PFS는 "장기 개인 키가 유출되어도 과거 세션 키가 노출되지
않는다"는 속성인데, 이는 매 세션마다 새로운 일시 키(Ephemeral Key)를
getrandom()에서 생성하고 세션 종료 후 소거하는 방식으로
달성됩니다. 커널 CRNG가 이 일시 키를 안전하게 공급하지 못하면 프로토콜 PFS도
무의미해집니다.
| 프로토콜 | PFS 달성 방식 | 커널 RNG Forward Secrecy의 역할 |
|---|---|---|
| TLS 1.3 | 매 핸드셰이크마다 ECDHE 일시 키 쌍 생성, 세션 종료 시 소거 | ECDHE 개인 키 + ClientHello/ServerHello random이 CRNG에서 추출 — Fast Key Erasure가 이전 세션의 일시 키를 역추적 불가하게 만들어 장기 키 유출 시에도 과거 세션 보호 |
| SSH (curve25519-sha256) | 매 연결마다 새로운 DH 일시 키 생성 | ssh-keygen 및 연결 시 DH 스칼라가 CRNG에서 생성 — 호스트 키 재생성 절(SSH 키 생성과 엔트로피) 참조 |
| IPSec/IKEv2 | DH 일시 키 + nonce(Ni, Nr) 매 SA 재협상 시 갱신 | nonce와 DH 개인 키가 CRNG에서 추출 — reseed로 갱신된 독립 엔트로피가 SA 재협상마다 새 PFS 기반 제공 |
| WireGuard | 매 핸드셰이크(약 2분/메시지)마다 새 ephemeral 키 + nonce | 모든 키 자료가 get_random_bytes()에서 직접 생성 — 커널 내부에서 CRNG를 곧바로 소비하므로 라이브러리 DRBG 계층 생략 |
① hwrng/엔트로피 소스 → ② input_pool(Blake2s) → ③ per-CPU ChaCha20 CRNG(Fast Key Erasure) →
④ 응용 일시 키 생성 → ⑤ 프로토콜 PFS. 어느 한 계층이라도 Forward Secrecy를
깨뜨리면 상위 PFS도 무력화됩니다. 특히 ③번 계층의 Fast Key Erasure가 없으면
메모리 덤프 한 번으로 수많은 과거 세션 키가 노출될 수 있으므로, 커널 CRNG의
Forward Secrecy는 프로토콜 PFS의 필요 조건입니다.
Forward Secrecy 검증 방법
Forward Secrecy는 부정적 속성("역산할 수 없다")이므로 직접 증명하기 어렵지만, 다음 관측으로 설계가 올바르게 동작하는지 확인할 수 있습니다.
# 1. Fast Key Erasure가 실제로 키를 소거하는지 확인 (ftrace)
# crng_fast_key_erasure 호출마다 키가 교체되는지 추적
echo 1 > /sys/kernel/debug/tracing/events/random/enable
cat /sys/kernel/debug/tracing/trace_pipe | grep -i crng
# 2. 재시드 주기 확인 (60초마다 generation 증가)
# /proc/sys/kernel/random/random_uuid는 매 호출마다 달라져야 함
watch -n 1 'cat /proc/sys/kernel/random/boot_id; cat /proc/sys/kernel/random/uuid'
# 3. 메모리 잔류 키 검사 — 크래시 덤프에서 CRNG 키 영역 덤프
# get_random_bytes() 호출 전후로 per-CPU crng.key가 달라야 함
crash > struct crng crngs
crash > rd -8 <per_cpu crngs.key 주소> 4
# 4. VM 롤백 감지 — vm.genid 변경 시 CRNG 재초기화 확인
# 롤백 후 entropy_avail이 0에 가깝다가 다시 오르는지, 동일 UUID 재사용 여부
cat /sys/class/dmi/id/product_uuid
cat /proc/sys/kernel/random/entropy_avail
엔트로피 소스 완전 가이드
Linux 커널은 다양한 엔트로피 소스를 수집하여 input_pool에 혼합합니다. 각 소스는 서로 다른 품질과 수집 속도를 가지며, 이들의 조합으로 전체 엔트로피 품질을 보장합니다. 이 섹션에서는 각 소스의 동작 원리, 수집 경로, 환경별 가용성을 분석합니다.
인터럽트 타이밍 지터
인터럽트 타이밍은 가장 풍부한 소프트웨어 엔트로피 소스입니다. 하드웨어 인터럽트(IRQ) 도착 시각의 나노초 단위 변동(지터)은 CPU 캐시(Cache) 상태, 메모리 접근 패턴, 버스(Bus) 경합(Contention) 등에 의해 결정되며 외부에서 예측하기 매우 어렵습니다.
/* 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/ 인터페이스를 활용한 모니터링 기법을 다룹니다.
kprobe로 난수 생성 추적
# kprobe 설정: random 서브시스템 함수 추적 (5.18+에서 tracepoint 제거됨)
cd /sys/kernel/tracing
# kprobe 이벤트 등록 — 주요 RNG 함수에 프로브 설치
echo 'p:mix mix_pool_bytes' > kprobe_events
echo 'p:cred _credit_init_bits bits=%x0:u64' >> kprobe_events
echo 'p:extr extract_entropy' >> kprobe_events
echo 'p:reseed crng_reseed' >> kprobe_events
# 엔트로피 크레딧 + 시드 추출 추적 활성화
echo 1 > events/kprobes/cred/enable
echo 1 > events/kprobes/extr/enable
echo 1 > events/kprobes/reseed/enable
# 트레이스 시작
echo 1 > tracing_on
# 트레이스 확인 (엔트로피 수집/소비 패턴)
cat trace_pipe | head -50
# 출력 예:
# [hwrng]-123 cred: (_credit_init_bits+0x0/0x120) bits=256
# sshd-456 extr: (extract_entropy+0x0/0xf0)
# kworker-78 reseed: (crng_reseed+0x0/0x1a0)
# 트레이스 종료
echo 0 > tracing_on
# kprobe 제거
echo > kprobe_events
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비트 (단일) | 다중 풀 (비공개) |
| 리시드 주기 | 60초 또는 수요 기반 | 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 hwrng: ~1 MB/s
# PCIe hwrng: ~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) | 제한적 (전역 잠금(Lock)) | 선형 (per-CPU) | Linux, OpenBSD 우수 |
설계 철학 비교
arc4random()은 항상 비블록킹이며 libc에서 직접 제공합니다.
getentropy()의 최대 크기가 256바이트로 제한됩니다.
최신 동향 (2024-2026)
2024년 이후 리눅스 난수(Random) 서브시스템은 vDSO 기반 getrandom(), NIST SP 800-90C 확정, 하드웨어 RNG 명령 확장의 세 축으로 크게 움직였습니다.
rngd 같은 사용자 공간 엔트로피 데몬(Daemon)이 더 이상 필요하지 않다는 공식 기준선이 분명해진 시기이기도 합니다.
| 시점 | 변경 | 영향 |
|---|---|---|
| Linux 6.11 (2024-09) | getrandom() vDSO 구현 병합 (x86-64) | 2년 넘는 패치 리뷰 끝에 Jason A. Donenfeld의 제안이 통합. 특정 워크로드에서 최대 15배 성능 향상, 사용자 공간이 시스템 콜(System Call) 없이 스레드별 opaque state로 ChaCha20 스트림을 받습니다. |
| Linux 6.12 (2024-11) | PowerPC64, s390 vDSO 확장 준비 | x86 외 아키텍처(Architecture)로 vgetrandom() 구현이 확대되어, 데이터센터 표준 ISA 대부분에서 같은 프로그램 인터페이스로 고속 난수를 얻을 수 있게 됩니다. |
| Linux 6.15 (2025-04) | jitterentropy-rng 메모리 접근 패턴 갱신 | LRNG 계열에서 제안된 jitter 소스 강화 패치(Patch)가 일부 반영돼 부팅 초기 엔트로피 확보 시간이 단축됩니다. 가상화 환경에서 특히 체감됩니다. |
| glibc 2.41 (2025-02) | getrandom() vDSO 사용자 공간 통합 | Adhemerval Zanella와 Jason Donenfeld가 glibc에 vDSO getrandom 지원을 병합. 표준 getrandom() 호출이 커널 6.11+에서 자동으로 vDSO 빠른 경로(Fast Path)를 사용합니다. x86-64는 6.11, aarch64·powerpc·loongarch64·s390x는 6.12 이상이 필요합니다. |
| Linux 7.0 (2026-04) | ML-DSA 서명 지원, Rust 코어 통합, lazy preemption | x509·pkcs7·crypto 서브시스템에 ML-DSA(포스트 양자 서명) 서명 검증이 추가되어 키 생성·서명 난수 소비 경로가 확장됩니다. lazy preemption 모델은 인터럽트 타이밍 기반 엔트로피 수집 패턴에 영향을 줄 수 있어 jitterentropy 검증이 재수행되었습니다. |
| NIST SP 800-90C (2024-08) | 엔트로피 검증 구조 공식 확정 | DRBG 구성과 엔트로피 소스 조합 요건이 분명해져, 커널 random.c가 위임하는 시험 조건이 표준과 정렬됩니다. |
| FIPS 140-3 Implementation Guide (2025) | SHA-1 HMAC DRBG 등 레거시 조건 축소 | 배포판 FIPS 모듈이 ChaCha20 CRNG 기반 경로를 1차 DRBG로 삼는 설정이 표준화되어 갑니다. |
| 2026-09-21 | FIPS 140-2 인증 완전 이전 | CMVP가 남은 140-2 모듈을 Historical로 이동. 리눅스 배포판의 FIPS 모듈은 이 시점까지 140-3로 재인증을 마쳐야 합니다. |
vDSO 기반 getrandom() — vgetrandom()
사용자 공간 난수 생성은 전통적으로 getrandom() 시스템 콜에 의존했고,
초당 수백만 건을 처리하는 워크로드(Workload)에서는 커널 진입 비용 자체가 병목(Bottleneck)이었습니다.
Linux 6.11부터 vDSO에 올라온 vgetrandom()은 커널이 관리하는 opaque 상태(entropy state)를 스레드마다 한 벌씩 매핑해 주고,
사용자 공간은 이 상태에서 ChaCha20 스트림을 직접 소비합니다. 커널 재시드(Reseed)가 필요할 때만 실제 시스템 콜로 내려갑니다.
/* glibc 2.41 이상이 제공하는 관용적 사용법 */
#include <sys/random.h>
uint8_t buf[32];
if (getrandom(buf, sizeof buf, 0) != sizeof buf)
abort();
/* 가능하다면 glibc가 자동으로 vgetrandom()을 선택해 시스템 콜을
* 거치지 않고 반환합니다. 애플리케이션 코드는 바꿀 필요가 없습니다. */
/* 벤치: 시스템 콜 경로 vs vDSO 경로 */
/* perf stat -e syscalls:sys_enter_getrandom ./app */
/* getrandom syscall 수가 거의 0이면 vDSO 경로가 동작하는 것입니다. */
# vDSO가 실제로 getrandom을 노출했는지 커널 설정에서 확인
scripts/config --state GENERIC_VDSO_GETRANDOM
scripts/config --state VDSO_GETRANDOM
# 런타임에 vvar 페이지 매핑 상태
cat /proc/self/maps | grep -E "vvar|vdso"
# 사용자 공간에서 시스템 콜(System Call)이 얼마나 줄었는지
strace -c -e getrandom ./your-app 2>&1 | tail -5
perf trace -e random:* -a sleep 5 | awk '{print $NF}' | sort | uniq -c
vgetrandom_alloc()을 호출해 각 스레드에 state를 한 번씩 매핑해 줘야 합니다.
rngd 필요성 재점검
과거 리눅스 환경에서는 rngd 데몬이 /dev/hwrng에서 바이트를 읽어 /dev/random 풀에 주입해 주는 역할을 했습니다.
그러나 5.17 이후 커널 스스로 /dev/hwrng를 직접 읽어 엔트로피 풀에 혼합합니다.
rngd를 여전히 돌릴 이유는 특정 구형 하드웨어, TPM 난수(TPM RNG) 보강, 인증서 요구 정도로 좁혀졌습니다.
# 커널이 자동으로 hwrng를 신뢰하는지 확인
cat /proc/sys/kernel/random/entropy_avail
cat /sys/class/misc/hw_random/rng_available
cat /sys/class/misc/hw_random/rng_current
# 부팅 시 CPU RNG 명령(RDRAND/RDSEED) 신뢰 설정
cat /proc/cmdline | grep -E "random.trust_cpu|random.trust_bootloader"
# rngd가 중복 동작 중이라면 로그에서 확인
systemctl status rngd.service 2>/dev/null
journalctl -u rngd.service --since "1 hour ago" 2>/dev/null | head
CPU·가속기 난수 명령 현황
| 아키텍처 | 명령/인터페이스 | 상태 (2026) |
|---|---|---|
| x86-64 | RDRAND, RDSEED | 기본 제공. random.trust_cpu=on이 기본값이며, Intel은 DRNG 2.0 계열로 전력 관리와 속도를 조정합니다. |
| AMD Zen 4/5 | RDRAND, RDSEED + CCP 내부 RNG | CCP(보안 프로세서)가 TRNG 기반 엔트로피를 kernel hwrng로 내보냅니다. drivers/crypto/ccp/가 담당. |
| ARM v8.5+ | RNDR, RNDRRS | Cortex-A76 이후 일부 코어와 Neoverse V1/V2/V3가 지원. 펌웨어(Firmware)에서 활성화 여부가 다릅니다. |
| RISC-V | Zkr 난수 확장 (csrr seed) | Zkr 확장을 지원하는 SoC에서 seed CSR(0x015)이 엔트로피 소스로 동작합니다. arch_get_random_seed_longs() archrandom 인터페이스로 구현되어 hwrng 프레임워크가 아닌 커널 RNG에 직접 기여합니다. 패치는 2023년 제출되어 6.7~6.8 사이에 머지되었습니다. |
| POWER (POWER10) | DARN 명령 | PowerVM KVM 게스트까지 엔트로피 경로가 통합되어 있습니다. |
| 전용 하드웨어 RNG | USB / PCIe / 네트워크 | 전용 TRNG/QRNG 장치는 보통 제조사 SDK, out-of-tree 드라이버, 사용자 공간 데몬 형태로 제공되며, 메인라인 통합 시 quality 파라미터를 통해 NIST SP 800-90B 시험 보고값을 등록할 수 있습니다. |
포스트 양자 시대의 난수 요구 사항
ML-KEM, ML-DSA 같은 포스트 양자(Post-Quantum) 알고리즘은 고전 ECDH나 ECDSA보다 훨씬 많은 난수를 소비합니다. 한 번의 하이브리드 TLS 핸드셰이크가 수 KB 수준의 균일(Uniform) 난수를 요구하는 것이 일반적이며, 키 확장 함수와 서명 생성 과정에서 엔트로피 소비가 가파르게 늘어납니다. 이 때문에 vDSO 경로와 하드웨어 RNG가 단순한 성능 문제를 넘어 운영 안정성의 문제가 됐습니다.
-
엔트로피 폭증 시점 — 컨테이너 오케스트레이터가 수백 개 TLS 엔드포인트를 동시에 초기화할 때, ML-KEM 키쌍 생성이 겹치면 초기 엔트로피 풀이 일시적으로 고갈될 수 있습니다.
crng_init상태 전에 서비스가 시작하지 않도록 systemdConditionCapability=CAP_SYS_BOOT대신ConditionPathExists=/dev/random와Wants=systemd-random-seed.service를 조합하는 구성이 권장됩니다. - 하드웨어 RNG의 역할 — 전용 하드웨어 RNG는 초기 엔트로피 확보에는 유용하지만, 핸드셰이크마다 직접 소비하기에는 처리량이 부족할 수 있습니다. 현재 권장 구성은 hwrng로 시드만 주입하고, 실제 소비는 vDSO + ChaCha20 경로로 처리하는 것입니다.
-
NIST SP 800-175B — 연방 시스템에서 PQC 사용 시 엔트로피 요구치를 명시합니다. 리눅스
/dev/random경로가 이 요구를 자동으로 충족하도록 설정하는 것이 FIPS 140-3 인증 프로세스의 표준 체크 항목이 되었습니다. -
Re-seed 정책 — OpenSSL 3.5 provider는 자체 DRBG를 유지하면서 일정 주기마다
getrandom()으로 재시드합니다. vDSO 경로가 기본이 되면 이 재시드 비용이 낮아져, 기본 재시드 간격을 좁히는 것이 권장됩니다(OSSL_PARAM "reseed-interval").
/dev/urandom을 바인드하거나 /dev/random을 urandom으로 심볼릭 링크해 놓습니다. vDSO 경로가 활성화된 커널에서는 이런 우회가 오히려 엔트로피 상태 추적을 막아 PQC 키 생성 품질 보증을 어렵게 만듭니다. 기본 장치를 유지하고 glibc가 알아서 vDSO 경로를 선택하도록 두는 것이 바람직합니다.
관련 문서
- 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 — 엔트로피 소스 요구사항 및 검증 표준입니다
- NIST SP800-90B_EntropyAssessment — 공식 C++ 툴킷 — ea_iid·ea_non_iid·ea_restart·ea_conditioning·ea_transpose 5종 하위 도구를 포함한 NIST 공식 min-entropy 평가 구현체입니다
- NIST ESV-Server — 엔트로피 소스 검증 서버 — CMVP 인증 랩이 엔트로피 소스를 제출해 SP 800-90B/90C 검증 certificate을 받는 ESVP 프로토콜과 Python 클라이언트입니다
- sp800_90b — NIST 툴킷 Python 포트 — NIST C++ 코드를 pybind11로 래핑한 Python 패키지(PyPI
sp800-90b), 오프라인 자동화용입니다 - BSI — Random Number Generators — 독일 BSI의 난수 생성기 보안 요구사항입니다
- Intel DRNG Implementation Guide — Intel RDRAND/RDSEED 구현 가이드입니다
- Kernel Keys Documentation — 난수로 생성한 키의 관리에 사용되는 키링(Keyring) 문서입니다
- LWN — The rest of the 5.17 merge window (random) — 커널 /dev/random 서브시스템 대규모 리팩토링 기사입니다
- Jitter Entropy — CPU Jitter RNG — 소프트웨어 기반 엔트로피 수집기 Jitterentropy 프로젝트입니다
- Dieharder — 공식 홈페이지 (Robert G. Brown, Duke University) — 난수 통계 검정 배터리 dieharder의 원저자 배포처로 소스 tarball과 RPM, GPL v2b 라이선스 정보를 제공합니다
- Dieharder — Debian 유지보수 업스트림 git 저장소 — 공동 저자 Dirk Eddelbuettel이 관리하는 git 저장소로 원저자의 옛 Google Code SVN 이력과 최신 Debian 패치를 포함합니다