하드웨어 난수 생성기 (hwrng / TRNG)

hwrng 경로를 커널 엔트로피 품질과 암호학적 안전성 관점에서 심층 분석합니다. hwrng 프레임워크와 드라이버 등록(Driver Registration) 모델, 하드웨어 난수 소스 신뢰도 평가, 엔트로피 풀 혼합과 credit 정책, 커널 CSPRNG(ChaCha20) 초기화·재시드 동작, 부팅 초기 entropy starvation 대응, 가상화(Virtualization)/임베디드 환경의 난수 공급 이슈, rngd와 userspace 연계, 품질 검증과 장애 진단 절차까지 안전한 난수 인프라 구축에 필요한 핵심을 다룹니다.

전제 조건:
일상 비유: 난수 생성기는 거대한 동전 던지기 기계와 비슷합니다.

컴퓨터는 본질적으로 결정론적 기계라 "진정한 우연"을 만들지 못합니다. 하드웨어 난수 생성기(hwrng)는 열잡음, 방사선 감쇠, 양자 효과 등 물리 세계의 예측 불가한 현상을 측정해 진짜 우연한 비트를 공급합니다. 커널은 이 원시 엔트로피를 ChaCha20 암호화(Encryption) 알고리즘으로 정제해 빠르고 안전한 난수 스트림(/dev/urandom)을 만듭니다.

더 쉽게 비유하자면, hwrng 시스템은 제빵소와 같습니다: 하드웨어 난수 생성기(TRNG)는 신선한 재료를 공급하는 농장이고, 엔트로피 풀(input_pool)은 모든 재료를 섞어 보관하는 큰 그릇이며, ChaCha20 CSPRNG는 반죽을 일정한 품질로 빵으로 만드는 제빵기이고, getrandom()는 고객에게 빵을 배달하는 창구입니다.

왜 난수가 중요한가?

우리가 매일 사용하는 기술에 난수가 숨어 있습니다:

  • 비밀번호와 인증 키 — 예측 가능한 키는 해커가 쉽게 뚫을 수 있습니다. 강력한 난수가 있어야 안전한 키를 만들 수 있습니다.
  • HTTPS/TLS 암호화 — 웹사이트 접속 시 매번 새로운 암호화 키를 생성하는데, 이 키가 예측 가능하면 통신 내용이 노출됩니다.
  • SSH 접속 — 서버에 원격 접속할 때 생성되는 세션 키도 난수에서 비롯됩니다.
  • 게임 — 주사위, 카드 섞기, 아이템 드롭 확률 등 게임의 공정성은 난수 품질에 달려 있습니다.
  • 가상화/컨테이너 — VM과 컨테이너의 ASLR(주소 공간 랜덤화)도 커널 난수를 사용합니다.

난수가 약하면 2012년 Debian OpenSSL 버그(CVE-2008-0166)처럼 전체 시스템 보안이 무너질 수 있습니다.

난수 생성 전체 여정: 하드웨어에서 사용자까지 1단계: 하드웨어 TRNG / QRNG 열잡음, 양자효과 클록 지터 비유: 신선한 재료를 키우는 농장 원시 난수 2단계: 엔트로피 풀 input_pool (Blake2s) 모든 소스를 섞음 되돌릴 수 없음 비유: 모든 재료를 섞는 큰 그릇 시드 추출 3단계: CSPRNG ChaCha20 CRNG 고속 난수 생성 60초마다 재시드 비유: 반죽을 빵으로 만드는 제빵기 암호학적 난수 4단계: 사용자 프로그램 getrandom() / /dev/urandom get_random_bytes() (커널) 암호 키, SSH, HTTPS, 게임... 비유: 빵을 받아가는 고객 (응용 프로그램) 핵심: 하드웨어는 느리지만 진짜 난수를 만들고, CSPRNG는 빠르지만 시드가 필요합니다 hwrng의 역할 = CSPRNG에 "진짜 난수 시드"를 공급하는 것. CSPRNG가 직접 난수를 만들어 사용자에게 전달합니다. 주의: hwrng는 getrandom()에 직접 데이터를 주지 않습니다! hwrng는 엔트로피 풀에 시드를 주입하기만 하고, 실제 난수 생성은 ChaCha20 CSPRNG가 담당합니다.

핵심 요약

  • 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 직접 읽기로 더 높은 처리율을 달성

단계별 이해

  1. 엔트로피 풀과 CSPRNG
    엔트로피 풀 메커니즘과 커널 CSPRNG 내부(add_hwgenerator_randomness())를 함께 이해합니다.
  2. hwrng API
    struct hwrng 인터페이스를 익히고 드라이버 등록 경로를 분석합니다.
  3. hwrng 드라이버 구현
    SoC 내장 TRNG, USB/PCIe 외장 RNG, 보안 모듈 RNG 같은 장치가 같은 struct hwrng 인터페이스로 통합되는 방식을 살펴봅니다.
  4. NIST 인증과 키 생성
    NIST SP 800-90B min-entropy 평가와 암호 키 생성 전체 플로우를 연결합니다.
  5. 테스트·검증
    dieharder/TestU01 등 통계 테스트와 커널 내부 self-test로 품질을 지속 검증합니다.

초보자 용어집

용어쉬운 설명비유
엔트로피 (Entropy)무작위성의 양을 나타내는 척도. 높을수록 예측하기 어려움주사위를 굴릴 때 나올 수 있는 경우의 수가 많을수록 엔트로피가 높음
TRNG물리적 현상(열잡음, 양자효과)으로 진짜 난수를 만드는 하드웨어진짜 주사위를 굴리는 것
PRNG수학 공식으로 난수처럼 보이는 수를 만드는 소프트웨어. 씨앗(seed)이 필요계산기로 주사위 결과를 흉내 내는 것
CSPRNG암호학적으로 안전한 PRNG. 과거 출력으로 미래 출력을 예측할 수 없음아무리 빵을 먹어봐도 반죽 비법을 알 수 없는 제빵소
시드 (Seed)PRNG/CSPRNG의 시작 값. 엔트로피 풀에서 추출제빵사가 반죽을 시작할 때 넣는 첫 재료
input_pool커널의 엔트로피 저장소. 모든 난수 소스가 여기에 섞임모든 재료를 한데 섞어 보관하는 큰 그릇
ChaCha20Linux 커널이 사용하는 암호화 알고리즘. CSPRNG의 핵심 엔진일정한 품질로 빵을 찍어내는 제빵기
qualityhwrng 드라이버의 엔트로피 품질 지수 (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 장치까지 모든 경로를 총망라합니다.

커널 RNG 전체 파이프라인 — 엔트로피 수집 → Blake2s 믹싱 → ChaCha20 난수 생성 → 소비 ① 엔트로피 수집 → ② input_pool (Blake2s) → ③ ChaCha20 CRNG (Fast Key Erasure · batched cache) → ④ 인터페이스 → 사용자 공간 ① 하드웨어 엔트로피 소스 RDRAND / RDSEED (x86) — arch_get_random_long() ARM RNDR (v8.5+) · RISC-V Zkr (seed CSR) hwrng 드라이버 (struct hwrng) → add_hwgenerator_randomness() TPM 2.0 · QRNG · virtio-rng 원리: 물리적 비결정성 (열잡음·양자·지터) → 디지털 비트 ① 소프트웨어 엔트로피 소스 IRQ 타이밍 지터 — add_interrupt_randomness() 디스크 I/O · 입력 이벤트 (키보드/마우스) jitterentropy — CPU 클록 지터 (부팅 초기) random.trust_cpu=on → RDRAND 256비트 즉시 시드 원리: 예측 불가능한 타이밍 변동 → 엔트로피 비트 mix_pool_bytes() credit_init_bits() ② input_pool — Blake2s 엔트로피 혼합 메커니즘 (256비트 해시 상태) v5.17 LFSR 4096비트 → Blake2s 32바이트 · XOR + 체인 압축으로 되돌릴 수 없는 혼합 IRQ 타이밍 지터 hwrng 하드웨어 디스크 / I/O jitterentropy 모든 소스 수렴 Blake2s 압축 함수 — 내부 동작 ⊕ XOR — 이전 해시 상태 ⊕ 새 엔트로피 입력 G 함수 + 10라운드 압축 (Blake2s compress) 256비트 해시 상태 (32바이트 = 8워드 체인) 되돌릴 수 없는 압축 — 역추적 불가 state_new = compress(state_old ⊕ input) extract_entropy() Blake2s final + re-absorb → 256비트 시드 추출 credit_init_bits() 각 소스 min-entropy 추정·누적 crng_ready() — 누적 256비트 → true 256비트 시드 → CRNG로 ↓ 시드 주입 재시드 ③ ChaCha20 CRNG — 출력 풀 (per-CPU key) · Fast Key Erasure 순환 메커니즘 32바이트 키 + 16바이트 논스/카운터 · quarter-round × 4 × 10 double-round = 20라운드 · forward secrecy 보장 ChaCha20 State — 16 워드 (64바이트) 상수 (4워드) 키 (8워드 / 32바이트) ← crng_make_state() per-CPU 선택 카운터 논스 quarter-round 회전: a b c d × 4 그룹 (10 double-round) a+=b; d^=a; d<<<16; c+=d; b^=c; b<<<12; a+=b; d^=a; d<<<8; c+=d; b^=c; b<<<7 ↓ chacha20_block() — 20라운드 → 64바이트 출력 블록 앞 32바이트 → 새 키 ↑ 키 교체 (Fast Key Erasure) → 다음 블록 입력 키 뒤 32바이트 → 난수 출력 ↓ get_random_bytes() / _user() 커널·사용자 공간으로 분배 순환: 새 키 → 다음 State 키 이전 키 소거 memzero_explicit() 이전 키 + 출력 잔류 → 역추적 영구 불가 난수 출력 분기 per-CPU batched cache get_random_u32() / u64() / canary() 소량 난수 고속 경로 (cache 즉시 반환) ↑ cache 소진 시 CRNG에서 재충전 직접 CRNG 경로 get_random_bytes() / _user() 대량 난수, cache 거치지 않음 KASLR · 네트워크 · 암호 · 사용자 crng_reseed() ← extract_entropy() — 60초 주기 타이머 (delayed_work) 기반 input_pool에서 재갱신 get_random_bytes() 커널 내부 직접 per-CPU 구조 crng[NR_CPUS] 각 코어별 독립 키 + local_lock batched cache도 per-CPU ④ 인터페이스 — 사용자 공간 진입점 getrandom() 시스템 콜 #318 · GRND_* 플래그 CRNG 준비 전 블록 vDSO vgetrandom() ★ v6.11+ · 시스템 콜 진입 없음 스레드별 opaque state /dev/random v5.6+ urandom과 동일 GRND_RANDOM (레거시) /dev/urandom CRNG 준비 후 nonblock 권장 기본 경로 /dev/hwrng 원시 HW 바이트 직접 읽기 rng-tools (레거시) /dev/hwrng (점선 테두리): CRNG 우회 HW 원시 바이트 직접 반환 커널 내부 소비자 — get_random_bytes() 계열 KASLR · ASLR · 스택 카나리 · UUID 네트워크 (TCP seq / SYN cookie / IPv4 ID) 암호 키 (RSA / ECDSA / AES 키 생성) 커널 키링 키 생성 · SLUB 랜덤화 주소 공간 랜덤화 · IRQ 벡터 랜덤화 KASLR mem · FG-KASLR · struct 랜덤화 OpenSSL ← getrandom() 주기적 재시드 → 자체 DRBG (CTR/HMAC) TLS 핸드셰이크 · RSA/ECDSA/AES 키 · 서명 · PKCS OpenSSH ← getrandom() 직접 호출 (ssh-keygen, sshd) ssh-keygen (RSA/Ed25519) · SSH 핸드셰이크 nonce · 키 교환 strongSwan ← getrandom() → IKEv2 핸드셰이크 nonce DH 키 · 세션 키 · IPsec SA · X.509 인증서 난수 glibc ← vDSO vgetrandom() ★ (6.11+, 시스템 콜 없음) PQC (ML-KEM/ML-DSA) · 컨테이너 · UUID · 모든 C 프로그램 레거시 ← /dev/urandom read() · /dev/hwrng + rng-tools 과거 호환성 — 현대 코드는 getrandom() 권장
커널 RNG 전체 파이프라인 — 엔트로피 수집(①) → Blake2s 믹싱(②) → ChaCha20 CRNG(③) → 인터페이스·소비자(④)

난수 생성기 분류

유형전체 명칭특징예시
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
난수 생성기 (RNG) 비결정론적 (Non-Deterministic) TRNG 열잡음 (Johnson-Nyquist) 클록 지터 (RDRAND) 준안정 플립플롭 방사성 붕괴 QRNG 빔 분리기 (BS 50:50) 진공 요동 (Vacuum) TOAD / 위상 잡음 원리적 예측 불가 결정론적 (Deterministic, Seed 기반) PRNG LCG (rand(), glibc) Mersenne Twister xoshiro256** / xorshift ⚠ 보안 용도 부적합 CSPRNG / DRBG ChaCha20-CRNG (Linux) CTR-DRBG (NIST AES) HMAC-DRBG / Hash-DRBG ✓ 암호학적 안전 엔트로피 풀 (input_pool, Blake2s) 하드웨어 RNG + 소프트웨어 소스 → 혼합 씨드(Seed) 공급 CSPRNG 출력 (실제 난수 생성 담당) /dev/urandom, getrandom(), get_random_bytes() ChaCha20: per-CPU 키 상태 + Fast Key Erasure 핵심 관계: 비결정론적 소스가 엔트로피 씨드를 공급 → CSPRNG가 고속 암호 난수 생성 PRNG(단순 의사난수)는 시뮬레이션/게임에만 사용 | DRBG는 NIST 표준 인증이 필요한 환경에서 CSPRNG 대체 Linux 커널의 실제 난수 파이프라인: hwrng → input_pool → ChaCha20-CRNG(CSPRNG) → 사용자

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 환경에서 독립 엔트로피 소스로 인정
 */
TRNG의 한계: TRNG는 비트 생성 속도가 물리적 현상에 의해 제한됩니다(수 Mbps 이하). 또한 온도 변화, 전압 변동, 전자기 간섭(EMI)에 의해 편향(Bias)이 발생할 수 있으므로 반드시 후처리(Post-Processing)를 거쳐야 합니다. Linux 커널에서는 TRNG 출력을 직접 사용하지 않고 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 백도어 사례 — RNG가 인증을 무력화한 역사적 교훈: NIST SP 800-90A는 과거 4번째 DRBG 메커니즘으로 Dual_EC_DRBG를 포함했습니다. 이 알고리즘은 타원 곡선(Elliptic Curve) 점 연산을 기반으로 설계되었으며, 2013년 Edward Snowden 문서 공개로 NSA가 표준화 과정에 백도어를 심었다는 사실이 확인되었습니다. 백도어의 핵심은 알고리즘에 하드코딩된 두 개의 곡선 점(P, Q) 중 Q가 NSA의 비밀 키와 관련되어 있다는 점이었습니다. 비밀 키를 아는 공격자는 DRBG 출력 일부만 관찰하여 내부 상태를 역추적할 수 있었고, 이후 모든 출력을 예측할 수 있었습니다.

인증 시스템에 미친 영향: 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(물리적 보안) 환경에서 요구됩니다.

CSPRNG vs DRBG 선택 기준: 일반 Linux 환경에서는 ChaCha20-CRNG(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 위치 — 아키텍처 다이어그램

커널 RNG 파이프라인 — 엔트로피 수집 → 시드/재시드 → 난수 생성 → 소비자 Blake2s input_pool + ChaCha20 per-CPU CRNG + vDSO getrandom 빠른 경로 엔트로피 원천 (Entropy Sources) 수집 / 입력 풀 CRNG (난수 엔진) 사용자 / 커널 인터페이스 x86 RDRAND / RDSEED arch_get_random_long() ARM RNDR / RNDRRS + TRNG FEAT_RNG (v8.5+), NIST SP800-90B RISC-V Zkr (CSR seed) riscv_isa_extension_available v6.12+ USB / PCIe hwrng virtio-rng, TPM hwrng 등 TPM 2.0 RNG /dev/tpmrm0, tpm_get_random() QRNG (양자 난수 생성기) 광자·진공·위상잡음·방사성 붕괴 virtio-rng (게스트) 호스트 엔트로피 → 게스트 — 소프트웨어 엔트로피 — IRQ 타이밍 지터 디스크 / 네트워크 I/O CPU 카운터 / TSC 지터 hwrng 서브시스템 drivers/char/hw_random/ hwrng_register() / unregister() hwrng core + 드라이버 ops (data_read, data_present) /dev/hwrng (문자 디바이스) rng-tools 데몬 (user-space) add_hwgenerator_randomness() → input_pool 주입 input_pool (엔트로피 풀) Blake2s 256비트 해시 상태 mix_pool_bytes() — 혼합 credit_init_bits() — 추정 add_interrupt_randomness() add_input_randomness() add_disk_randomness() add_hwgenerator_randomness() random_get_entropy_fallback() crng_ready() 게이트 early → ready 전환 extract_entropy() → 시드 blocking / non-blocking v5.17+ /dev/random == /dev/urandom ChaCha20 CRNG crng[NR_CPUS] per-CPU 64바이트 key + 카운터 crng_reseed() 갱신 · 60초 주기 · 512B 소비 후 · input_pool 준비 시 arch_get_random_*() 빠른 시드 (RDRAND/RNDR 직접 시드) extract_crng() — per-CPU 난수 출력 getrandom() 시스템 콜 GRND_RANDOM / _INSECURE / _SEED vDSO getrandom() (빠른 경로) v6.11+ — 시스템 콜 진입 없음 /dev/urandom /dev/random /dev/hwrng (직접 읽기) — 커널 내부 API — get_random_bytes() get_random_bytes_arch() get_random_u32() / u64() get_random_u32_below(n) get_random_canary() get_random_u32_below() 균등분포 net_random() / prandom_u32() (레거시 별칭) 커널 난수 소비자 (Consumers) — CRNG 출력 분배 KASLR 커널 이미지 기준 주소 ASLR (사용자 영역) mmap / 스택 / PIE 베이스 네트워크 키 TCP seq / SYN cookie / IPv4 ID 암호화 키 kTLS / IPSec / dm-crypt / F2FS SLUB 랜덤화 freelist / 카나리 초기 부팅 — crng_ready() 전까지 blocking wait_for_random_bytes() / getrandom() 대기 또는 -ENOSYS (GRND_INSECURE 제외) ready 후 — non-blocking, per-CPU CRNG에서 직접 분배 vDSO getrandom() / get_random_bytes() 모두 동일한 ChaCha20 출력 흐름 범례: 엔트로피 / 난수 데이터 흐름 (실선) CPU RNG → CRNG 직접 시드 (점선) CPU 명령어 RNG 외부 hwrng (USB/TPM/QRNG/virtio) 소프트웨어 엔트로피 커널 난수 소비자 blocking / early 제약 extract_entropy() arch_get_random_*() 직접 시드 (점선 빠른 경로)

QRNG와 양자 기반 TRNG 물리 원리

QRNG(Quantum Random Number Generator)는 양자 역학의 고유한 비결정성(inherent randomness)을 활용하여 원리적으로 예측 불가능한 진난수(True Random Number)를 생성합니다. 고전적 TRNG(열잡음, 지터)는 환경 조건에 따라 편향이 발생할 수 있지만, QRNG는 양자역학 법칙 자체가 비결정성을 보장하므로 어떤 물리적 모델로도 예측할 수 없습니다. 상용 QRNG는 주로 광자·진공 요동·레이저 위상 잡음 같은 광학적 양자 현상을 엔트로피 원천으로 사용합니다. 방사성 붕괴도 양자역학적 비결정성을 이용하는 진난수 원천이지만, 일반적으로 광학 기반 QRNG 제품군보다는 별도의 양자 기반 TRNG 또는 연구용 난수 소스로 구분하는 편이 정확합니다.

용어 구분: 열잡음, 링 오실레이터 지터, 애벌랜치/제너 다이오드 노이즈 같은 일반 하드웨어 TRNG는 미시적으로 양자 효과가 섞일 수 있어도 보통 QRNG라고 부르지 않습니다. 이 절에서 QRNG는 양자 측정 원리를 장치 설계와 보안 모델의 핵심으로 명시한 난수 생성기를 뜻합니다.
빔 분리기 (BS 50:50) 단일 광자 소스 (Single Photon Emitter) 50:50 빔 스플리터 |ψ⟩ = (|0⟩ + |1⟩)/√2 하이젠베르크 불확정성 검출기 A 검출기 B → "0" → "1" 1~10 Mbps ID Quantique Quantis 진공 요동 (Vacuum) 진공 상태 전자기장 (Zero-Point Energy) 호모다인 검출기 ΔE·Δt ≥ ℏ/2 에너지-시간 불확정성 ADC → 진폭 디지털화 → 비트 스트림 100+ Mbps QuintessenceLabs qStream 광자 도착 시간 (TOAD) 약한 코히어런트 광원 (Weak Coherent Source) 단일 광자 검출기 P(n) = e^(-μ)·μ^n/n! 파동-입자 이중성 Δt 타임스탬프 → 비트 → 비트 스트림 10~100 Mbps EYL QRNG 칩 레이저 위상 잡음 반도체 레이저 다이오드 (자발 방출 위상 확산) 간섭계 검출 Δφ → ΔI (위상→진폭) Schawlow-Townes 선폭 고속 ADC → 비트 → 비트 스트림 1~10 Gbps Toshiba, Quside 방사성 붕괴 TRNG 방사성 동위원소 (알파/베타 붕괴) 가이거-뮐러 계수관 양자 터널링 붕괴 시점 완전 비결정적 이벤트 간격 → 비트 → 비트 스트림 ~kbps Hotbits (연구용) 후처리 파이프라인 (Post-Processing) Toeplitz Hashing / von Neumann Extractor / NIST SP 800-90B 컨디셔닝 → 편향 제거 → 균일 비트열 커널 hwrng 서브시스템 add_hwgenerator_randomness() → 엔트로피 풀

빔 분리기(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
커널 통합 관점: QRNG든 방사성 붕괴 기반 TRNG든 전용 장치가 난수 바이트를 제공한다면, Linux 커널에서는 동일한 struct hwrng 인터페이스를 통해 등록됩니다. quality 파라미터에 장치의 min-entropy 수준을 반영하면 커널이 엔트로피 크레딧을 적절히 계산하여 input_pool에 기여합니다.

엔트로피 풀 메커니즘

초보자 비유: 엔트로피 풀은 큰 믹싱 보울과 같습니다. 밀가루, 계란, 설탕, 소금 등 여러 재료를 넣고 섞으면 나중에 어떤 재료가 들어갔는지 구분할 수 없습니다. 커널도 하드웨어 난수, 인터럽트 타이밍, 디스크 I/O 등 여러 소스를 input_pool에 섞어 저장합니다. 이 되돌릴 수 없는 혼합이 암호학적 안전성의 출발점입니다. 엔트로피가 256비트 이상 모이면 "그릇이 충분히 섞였다"고 판단하여 CSPRNG가 본격 가동됩니다.

커널 엔트로피 풀은 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)을 형성합니다.

엔트로피 풀 → 난수 인터페이스 데이터 흐름 input_pool (Blake2s) h[0..7]: 256비트 상태 buf[64]: 입력 버퍼 init_bits: 0 → 256 엔트로피 소스: hwrng IRQ 지터 디스크 I/O 키보드/마우스 단방향 혼합 extract_entropy() 32바이트 키 추출 ChaCha20 CRNG per-CPU key[32] 60초마다 재시드 암호학적 난수 출력 /dev/random 초기화 전 블록 → 후 non-block /dev/urandom 항상 non-block (레거시 호환) getrandom() — 권장 초기화 전 블록 (GRND_NONBLOCK 제외) get_random_bytes() — 커널 내부 항상 non-block (드라이버용) Linux 5.18+ 핵심: /dev/random과 /dev/urandom은 동일한 CSPRNG 백엔드 사용 블록킹은 CRNG 초기화 완료(256비트 엔트로피 수집) 전에만 발생 — 이후 모든 인터페이스가 동일한 품질의 난수 제공 사용자 공간: getrandom() 권장 | 커널 내부: get_random_bytes() / get_random_u32() / get_random_u64()

/dev/random vs /dev/urandom — 현대 커널의 통합

Linux 5.18+ 변경사항: /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 완료 타이밍 */
핵심 요약 (2026-06-24 kernel.org 소스 검증 완료): hwrng 드라이버는 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회 미만
51250% 효율 (Intel RDRAND)16,384 비트1회 미만
25625% 효율 (열잡음 TRNG)8,192 비트1회 미만
12812.5% 효율 (FPGA 지터)4,096 비트1회 미만
0엔트로피 기여 없음0 비트∞ (초기화 불가)

hwrng 엔트로피 공급 주기

hwrng_fillfn() 커널 스레드는 읽기 성공 시 add_hwgenerator_randomness(..., true) 를 호출합니다. 현대 커널에서는 CRNG 초기화 이후 또는 엔트로피 credit 이 없는 경우 crng_reseed_interval() 만큼 throttle 되므로, 실제 공급 주기는 하드웨어 처리율과 커널 재시드 정책의 영향을 함께 받습니다:

이와 별개로 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는 증가하지 않음
분리 설계의 보안 의미: 공격자가 hwrng 드라이버의 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비트) 필요 바이트
10241.0 비트/비트256 비트 (1회 충분)4,096 비트32,768 비트32바이트
10000.976 비트/비트250 비트3,906 비트31,250 비트33바이트
5120.5 비트/비트128 비트2,048 비트16,384 비트64바이트
2560.25 비트/비트64 비트1,024 비트8,192 비트128바이트
1280.125 비트/비트32 비트512 비트4,096 비트256바이트
320.031 비트/비트8 비트128 비트1,024 비트1,024바이트
00 비트/비트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)만 제공
엔트로피 credit 누적과 CRNG 상태 전이 상태 1: 미초기화 init_bits = 0 crng_ready() = false getrandom() 블록 상태 2: 축적 중 0 < init_bits < 256 crng_ready() = false getrandom() 여전히 블록 상태 3: CRNG 준비됨 init_bits >= 256 crng_ready() = true getrandom() 즉시 반환 credit 누적 시작 256 도달 → crng_reseed() credit를 부여하는 엔트로피 소스들 hwrng 드라이버 credit_init_bits( bytes×q×8/1024) quality 기반 전량 인터럽트 지터 credit_init_bits( max(1u, ...)) 64회마다 최소 1비트 RDSEED / ARM RNG credit_init_bits( 전체 비트) 원시 엔트로피 전량 RDRAND credit_init_bits( 제한적) 단독 256 달성 불가 input_pool.init_bits 누적 카운터 0 ─────────────── 128 ─────────────── 256 (POOL_READY_BITS) 현재 credit 진행도 (예: 128/256 비트) init_bits ≥ 256 crng_reseed() — 1회 트리거 extract_entropy(32바이트) → base_crng.key 갱신 런타임 재시드 약 60초 주기 credit 없이 mix만 수행: quality=0 hwrng, add_device_randomness() (MAC/시리얼), RDRAND (제한적) → input_pool 해시 상태는 변경되지만 init_bits는 증가하지 않음 — CRNG 초기화에 기여하지 않음
quality=1024 ≠ 검증된 엔트로피 (중요 오해):
  • 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 누적까지의 전체 파이프라인을 보여줍니다. 각 단계에서 수행되는 연산과 데이터 변환을 추적할 수 있습니다.

quality → credit 변환 파이프라인 (hwrng 경로) 1. read() 호출 hwrng_fillfn() rng→read(buf, max 4096바이트) 2. 바이트 수 rc = read() 반환값 예: rc = 32 예: rc = 4096 3. quality 적용 rng→quality 예: quality = 1024 예: quality = 512 4. entropy_bits 계산 (rc × q × 8) >> 10 32×1024×8>>10=256 4096×512×8>>10=16384 5. 주입 함수 add_hwgenerator_ randomness( buf, rc, entropy) 분기 mix_pool_bytes(buf, rc) Blake2s 해시 상태 갱신 무조건 수행 — 신뢰도 무관 credit_init_bits(entropy) init_bits += entropy (max 256) entropy=0이면 스킵 (quality=0) wake_up_interruptible() getrandom() 대기 프로세스 깨움 quality = 0 경로 entropy = 0 → credit 스킵 mix만 수행 — init_bits 불변 보안 효과 풀 상태는 오염(taint)되지만 CRNG 초기화 가속 안 됨 mix만

보수적 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 부팅 가속
운영 권장사항: 클라우드 VM(QEMU/KVM with virtio-rng)에서는 호스트가 물리적 hwrng를 통해 게스트에게 엔트로피를 공급하므로 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바이트 읽기)
credit 관측으로 알 수 있는 것:
  • 어떤 소스가 CRNG 초기화에 기여했는가_credit_init_bits kprobe의 프로세스 이름으로 판별 (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 용어 정리

초보자 비유: 난수 생성기 용어는 자동차 분류와 비슷합니다. "자동차(Car)"라는 넓은 개념 아래에 "세단(Sedan)", "SUV", "트럭(Truck)" 등 용도와 형태에 따라 이름이 다르듯, 난수 생성기도 안전성 요구사항표준 기관에 따라 DRBG, CRNG, CSRNG, 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 메커니즘을 정의합니다:

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)"하려면 다음 두 가지 속성을 만족해야 합니다:

DRBG는 NIST 표준 관점에서 CSPRNG의 구체적 구현 명세로 볼 수 있습니다. 즉, 모든 DRBG는 CSPRNG의 요구사항을 만족하지만, 모든 CSPRNG가 NIST 승인 DRBG인 것은 아닙니다 (Linux ChaCha20 CRNG가 그 예입니다).

CRNG (Cryptographic Random Number Generator)

CRNG(암호학적 난수 생성기)는 "Pseudo" 수식어가 생략된 용어입니다. 이 생략은 의도적일 수도 있고 단순한 관행일 수도 있습니다:

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는 가장 넓은 의미의 용어로, 다음을 모두 포함합니다:

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(가장 구체적인 표준 명세)입니다:

CSRNG (Cryptographically Secure RNG) TRNG + CSPRNG 모두 포함 — 가장 넓은 의미 TRNG (비결정론적) 물리적 엔트로피 소스 RDRAND, RDSEED, QRNG, hwrng 장치 CSPRNG에 포함되지 않음 (Pseudo가 없으므로) CSPRNG (결정론적, "Pseudo" 포함) 암호학적으로 안전한 의사난수 생성기 CRNG 구현체 명칭 Linux: ChaCha20 CRNG (struct crng) CSPRNG와 동의어 또는 TRNG를 포함한 넓은 의미 DRBG NIST SP 800-90A Hash / HMAC / CTR 표준 명세 CSPRNG의 NIST 표준화 버전 시드 ← 결정론적 확장 (알고리즘 기반) → CRNG와 DRBG는 CSPRNG의 두 가지 명명 관점 CRNG = 구현체 명칭, DRBG = 표준 명세
그림: DRBG / CSPRNG / CRNG / CSRNG 개념 계층 — 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.cstruct 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.ccrypto/ 프레임워크를 통해 DRBG 알고리즘을 명시적으로 요청하는 경우(tls, ipsec 등)에만 사용됩니다. 두 경로는 모두 CSPRNG의 요구사항을 만족하지만, 구현체와 인증 상태가 다릅니다.

핵심 요약

커널 CSPRNG 내부

초보자 비유: CSPRNG는 제빵사와 같습니다. 제빵사가 반죽(시드)을 받아 일정한 방법으로 빵(난수)을 만들지만, 빵을 맛본 사람이 반죽의 정확한 성분을 역추적할 수 없는 것처럼, CSPRNG의 출력을 아무리 많이 가져도 내부 상태를 알아낼 수 없습니다. 또한 매번 새로운 빵을 만들 때마다 비법(키)을 바꾸므로(forward secrecy), 과거 빵을 분석해도 현재 빵의 비법을 알 수 없습니다. Linux 커널은 이 역할을 ChaCha20 알고리즘으로 수행하며, CPU마다 독립된 제빵기(per-CPU CRNG)를 두어 성능을 높입니다.

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));
}
ChaCha20 Fast Key Erasure — 키 교체를 통한 forward secrecy 기존 키 (old) key[32] chacha_state chacha20_block() ChaCha20 블록 출력 (64바이트) 앞 32바이트 → 새 키 (new key) 뒤 32바이트 → 난수 출력 memcpy(key, ...) 새 키로 교체 다음 블록의 chacha_state random_data 출력 사용자에게 반환 get_random_bytes() 등 memzero_explicit() 출력의 키 부분(앞 32바이트) 메모리에서 소거 결과: 메모리 덤프를 해도 이전 출력으로부터 현재 키를 역추적할 수 없음 → forward secrecy (전방 비밀성) 매 출력마다 키가 교체되므로, 한 번의 키 유출이 과거 출력에 영향을 주지 않음
코드 설명
  • 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 드라이버 개발

초보자 비유: hwrng 드라이버는 식재료 배달부와 같습니다. 배달부가 농장에서 식재료를 수집해 주방에 가져다주기만 하면 되지, 요리를 직접 하지 않는 것처럼, 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;
};
struct hwrng 콜백 API와 프레임워크 호출 흐름 드라이버 콜백 (struct hwrng) init() — 선택사항 하드웨어 초기화 read() — 필수 난수 바이트 읽기 data_present() — 선택 데이터 준비 여부 data_read() — 구형 4바이트 읽기 cleanup() — 선택사항 하드웨어 정리 quality: 0~1024 엔트로피 품질 지수 개발자가 구현 hwrng_register() init() 호출 + rng_list 추가 hwrng_fillfn() 커널 스레드 (주기적) rng→read() 호출 바이트 + quality add_hwgenerator_randomness() 엔트로피 credit 계산 bits = bytes × quality × 8 / 1024 input_pool 엔트로피 혼합 자동 관리 항목 /dev/hwrng 장치 sysfs 엔트리 rng_current 선택 kref 참조 카운트 엔트로피 주입 CRNG 재시드 트리거 개발자 부담 최소화 read() 콜백만 구현하면 됨

quality 파라미터 가이드

quality 값엔트로피 품질해당 하드웨어
10241.0 비트/비트 (이론적 최대)검증된 고품질 TRNG
512~10230.5~1.0 비트/비트Intel RDRAND, ARM TRNG, TPM RNG
256~5110.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 제품: 전용 TRNG, 보안 모듈 RNG, 일부 QRNG 제품은 USB/PCIe/칩 폼팩터로 제공되며, 각 제조사별로 속도, 헬스 테스트, 인증 현황이 다릅니다.
초보자 비유: 외장 RNG는 택배로 식재료를 받는 것과 같습니다. 직접 텃밭에서 키운 재료(SoC 내장 TRNG)와 달리, 택배(USB)로 받으면 느리지만 설치가 간단하고, 전문 냉장 배송(PCIe)으로 받으면 빠르지만 설정이 복잡합니다. 두 방식 모두 최종적으로 같은 주방(엔트로피 풀)에 재료를 전달합니다.

외장 하드웨어 RNG 장치는 USB 또는 PCIe 인터페이스로 연결되어 hwrng 드라이버로 구현되는 경우가 많습니다. 장치 내부의 엔트로피 원천은 열잡음 TRNG, 애벌랜치 노이즈, 링 오실레이터 지터, 양자 광학 소스 등으로 다양하며, 커널 드라이버 관점에서는 모두 struct hwrngread() 콜백으로 난수 바이트를 제공하는 장치입니다. 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 같은 엔트로피 평가 결과를 기준으로 정해야 합니다.

외장 hwrng 데이터 경로 비교: USB vs PCIe USB 경로 (Bulk Transfer) HW TRNG 칩 난수 생성 Bulk IN EP 엔드포인트 0x81 usb_submit_urb() 비동기 전송 요청 URB 완료 콜백 인터럽트 컨텍스트 complete() 대기 해제 신호 wait_for_completion() 슬립 가능 컨텍스트 DMA 버퍼 → data memcpy 복사 read() 반환 읽은 바이트 수 USB 2.0: ~40 MB/s | 지연: ms 단위 | quality: 600~800 | 구현: 단순 (벌크 전송) mutex + completion 동기화 | usb_alloc_coherent() DMA 버퍼 PCIe 경로 (MMIO + MSI) HW TRNG 칩 FIFO 채움 MMIO BAR0 레지스터 매핑 MSI 인터럽트 FIFO 준비 통지 pcie_rng_irq() atomic_set + wake_up wait_event() data_ready 대기 readl(DATA_REG) 32비트 FIFO 읽기 *out++ = 데이터 직접 메모리 쓰기 read() 반환 읽은 바이트 수 PCIe: 수 Gbps | 지연: us 단위 | quality: 900~1024 | 구현: 복잡 (MMIO + MSI + wait_queue) atomic_t + wait_queue_head_t 동기화 | pcim_iomap() devm 자동 해제 | pci_alloc_irq_vectors(MSI) 공통: 두 경로 모두 hwrng 코어의 hwrng_fillfn() 스레드가 주기적으로 read() 콜백을 호출 → add_hwgenerator_randomness()로 input_pool에 주입

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 hwrngstruct usb_rng_dev에 내장되어 있으므로, container_of()로 부모 구조체 포인터를 얻습니다. 별도 할당보다 메모리 효율적입니다.

메인라인 hwrng/TRNG 드라이버 예시

아래 항목은 Linux 메인라인 커널에서 확인할 수 있는 hwrng/TRNG 계열 드라이버 예시입니다. 상용 QRNG 전용 드라이버 목록이 아니며, 대부분 SoC 내장 TRNG, CPU RNG 명령, TPM, 가상 RNG 인터페이스입니다.

대상커널 소스 위치분류인터페이스
BCM2835 / BCM63xxdrivers/char/hw_random/bcm2835-rng.cSoC 내장 RNG/TRNGMMIO
Intel 하드웨어 RNGdrivers/char/hw_random/intel-rng.c칩셋/플랫폼 RNGMMIO/I/O 포트
ARM SMCCC TRNGdrivers/char/hw_random/arm_smccc_trng.c펌웨어 TRNG 인터페이스SMC/HVC 콜
TPM 2.0 RNGdrivers/char/tpm/TPM 내부 RNGTPM 커맨드
VirtIO RNG (KVM 게스트)drivers/char/hw_random/virtio-rng.c가상 RNGvirtqueue
EXYNOS (삼성)drivers/char/hw_random/exynos-trng.cSoC 내장 TRNGMMIO

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 인증

초보자 비유: NIST 인증은 식품 안전 검사와 같습니다. 식품이 안전한지 규격에 맞춰 검사하는 것처럼, 난수 생성기의 출력이 진짜 무작위인지 통계적으로 검증합니다. "이 주사위가 공정한가?"를 수백만 번 던져보며 확인하는 것과 같습니다. 이 검사를 통과한 난수 생성기만이 정부, 금융, 의료 등 보안이 중요한 시스템에서 사용될 수 있습니다. 커널 hwrng의 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는 주사위 품질 검사관과 같습니다. 카지노에서 주사위가 공정한지 확인하려면 어떻게 해야 할까요? 주사위를 던져보고 결과를 분석합니다. SP 800-90B도 똑같습니다 — 하드웨어 난수 생성기(hwrng)에서 숫자를 많이 뽑아보고, 그 숫자들이 정말 예측 불가능한지 통계적으로 검사합니다.

  • 엔트로피(Entropy) = "예측 불가능성" — 주사위가 얼마나 공정한지 나타내는 점수. 100점 만점에 100점이면 완벽한 주사위, 0점이면 항상 같은 숫자만 나오는 불량 주사위
  • min-entropy = "최악의 경우 점수" — 평균이 아니라 가장 자주 나오는 숫자를 기준으로 계산. 보안에서는 평균이 아니라 최악을 대비해야 하므로 이 점수를 사용
  • IID 검정 = "독립성 검사" — 주사위를 던질 때마다 이전 결과에 전혀 영향을 받지 않는지 확인. 매번 새로운 주사위를 던지는 것과 같아야 IID
  • Non-IID 검정 = "보수적 검사" — 주사위에 약간의 편향이나 패턴이 있을 수 있다고 가정하고, 그런 경우에도 안전할 만큼의 예측 불가능성이 있는지 계산
  • 재시작 테스트 = "새로 켤 때마다 다른 결과가 나오는가?" — 주사위 공장에서 주사위를 새로 만들 때마다 항상 같은 면이 위로 나온다면 문제. 전원을 껐다 켤 때마다 다른 난수가 나오는지 확인
  • 컨디셔닝 = "품질 개선 공정" — 약간 편향된 주사위를 여러 개 던져서 결과를 섞으면 더 공정한 결과가 나오는 것처럼, 원시 난수를 후처리하여 품질을 높이는 단계
  • 헬스 테스트 = "실시간 건강 검진" — 주사위가 도중에 고장 나서 항상 같은 숫자만 나오는지 실시간으로 감시
SP 800-90B 검증 — 주사위 품질 검사에 비유 1. 데이터 수집 주사위 100만 번 던지기 hwrng에서 1,000,000샘플 → "이 주사위가 공정한가?" 2. 검사 방식 선택 IID: 완벽한 주사위인가? Non-IID: 약간 편향이 있어도 안전한가? 3. 점수 계산 min-entropy H 산출 "예측 불가능성 점수" 0~8점 (8비트 기준) 4. 커널 등록 quality = H/8 × 1024 0~1024점 → /dev/hwrng 높을수록 좋은 난수 점수 해석 — "이 주사위를 카지노에서 사용해도 될까?" 위험 (H < 4) 불량 주사위 — 사용 불가 보통 (H = 4~7.5) 약간 편향 — 컨디셔닝 필수 양호 (H > 7.5) 좋은 주사위 — FIPS 적합 우수 (H ≈ 8.0) 완벽 — 양자 RNG 수준 추가 검사 1: 재시작 테스트 "주사위를 1,000번 새로 만들 때마다 다른 결과가 나오는가?" 전원을 1,000번 껐다 켜면서 각각 1,000샘플씩 수집 → 검사 점수가 원래의 절반 이상이면 합격 min(H_r, H_c) ≥ H_I / 2 → PASS 추가 검사 2: 컨디셔닝 "약간 편향된 주사위를 섞어서 더 공정하게 만들 수 있는가?" vetted: NIST 검증 알고리즘 (HMAC, 해시 함수 등) non-vetted: 사용자 정의 알고리즘 (엔트로피 손실 산정 필수)

SP 800-90B 핵심 개념 쉬운 가이드

아래는 SP 800-90B의 핵심 개념들을 전문 용어 없이 설명한 내용입니다. 기술적 세부 사항은 뒤에 이어지는 각 섹션에서 다룹니다.

엔트로피(Entropy) — "얼마나 예측하기 어려운가?"

엔트로피는 "이 난수 생성기가 만든 숫자를 공격자가 얼마나 예측하기 어려운가"를 나타내는 점수입니다. 점수가 높을수록 예측이 어렵고, 점수가 낮을수록 쉽습니다.

비유: 시험 점수로 비유하면, Shannon entropy는 "반 평균 점수"이고, min-entropy는 "반에서 가장 낮은 점수"입니다. 안전한 난수 생성기가 되려면 "반에서 가장 낮은 점수"도 높아야 합니다 — 한 명이라도 0점이면 전체가 위험하기 때문입니다.

IID vs Non-IID — "매번 새 주사위인가, 같은 주사위인가?"

IID(Independent and Identically Distributed)는 "매번 던질 때마다 완전히 독립적이고 동일한 분포"라는 뜻입니다. 쉽게 말해 "매번 새로운 공정한 주사위를 던지는 것"과 같습니다.

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"이전에 본 패턴으로 다음을 예측할 수 있나?"사전 기반 예측 가능성
비유: 주사위를 10명의 검사관이 각자 다른 각도에서 검사한다고 상상하십시오. 한 명은 "숫자 분포가 균등한가?"를 보고, 다른 한 명은 "패턴이 반복되는가?"를 보고, 또 다른 한 명은 "이전 결과가 다음에 영향을 주는가?"를 봅니다. 10명 중 가장 엄격한 검사관이 준 점수가 최종 점수입니다.

재시작 테스트 — "다시 켤 때마다 다른가?"

컴퓨터를 껐다 켤 때 하드웨어 난수 생성기가 매번 다른 초기 상태에서 시작하는지 확인합니다. 만약 부팅할 때마다 항상 비슷한 난수가 나온다면, 공격자가 "이 기기는 부팅 후 항상 이런 난수를 만든다"고 예측할 수 있어 치명적입니다.

비유: 주사위 공장에서 주사위를 만들 때마다 항상 같은 면이 위를 향하게 나온다면 문제가 있습니다. 1,000개의 주사위를 새로 만들어보고, 각 주사위의 첫 결과가 서로 다른지 확인하는 것과 같습니다.

컨디셔닝 — "품질 개선 공정"

하드웨어 난수 생성기의 원시 출력에는 약간의 편향이 있을 수 있습니다. 예를 들어 0이 1보다 약간 더 자주 나올 수 있습니다. 컨디셔닝은 이 편향을 제거하고 난수 품질을 높이는 후처리 단계입니다.

비유: 약간 기울어진 주사위를 여러 번 던져서 결과를 수학적으로 섞으면, 원래 주사위의 기울어짐이 상쇄되어 더 공정한 결과를 얻을 수 있습니다. SHA-256 해시 함수 같은 vetted(검증된) 컨디셔너는 NIST가 수학적으로 품질을 보증하는 "믿을 수 있는 섞기 기계"이고, 사용자가 직접 만든 알고리즘은 non-vetted로 분류되어 품질을 별도로 증명해야 합니다.

헬스 테스트 — "실시간 건강 검진"

난수 생성기가 동작 중에 고장 나는 것을 실시간으로 감지합니다. 두 가지 필수 검사가 있습니다:

비유: RCT는 "주사위가 갑자기 멈춰서 항상 6만 나오는가?"를 즉시 발견하는 비상 벨이고, APT는 "시간이 지나면서 6이 점점 더 자주 나오고 있지 않은가?"를 감시하는 온도계입니다. 비상 벨이 울리면 즉시 주사위를 폐기하고, 온도계가 임계치를 넘으면 주사위를 교체해야 합니다.
min-entropy vs Shannon entropy: Shannon entropy H = -Σ pi log2 pi는 평균 정보량을 나타내지만, 암호학에서는 단일 최악값(min-entropy H = -log2 pmax)이 보안 강도를 결정합니다. 예측 가능성은 평균이 아닌 최빈값에 의해 좌우되기 때문입니다. SP 800-90B는 모든 추정에서 min-entropy를 사용합니다.

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로 대응되며, 각 단계의 출력이 검증 대상이 됩니다.

1. 노이즈 소스 (Noise Source) 물리적 무작위성 발생 열 잡음 / 발진기 지터 / 양자 효과 아날로그 2. 디지털화 (Digitization) 아날로그 → 디지털 변환 샘플링 + 양자화 (선택적 단계) 원시 비트 3. 컨디셔닝 (Conditioning) 편향 제거 + 엔트로피 집중 (선택적 단계) vetted / non-vetted 처리된 4. 엔트로피 출력 (Output) DRBG 시드로 전달 또는 직접 난수 사용 헬스 테스트 (Health Tests) — 노이즈 소스 출력에 상시 적용 Repetition Count Test (RCT): 고장 감지 | Adaptive Proportion Test (APT): 엔트로피 손실 감지 시작 시 (Start-up) / 연속 (Continuous) / 요청 시 (On-demand) 수행 SP 800-90B 검증 데이터 샘플 (각 1,000,000샘플 이상) 순차 데이터셋 (Sequential) 노이즈 소스 원시 출력 컨디셔닝 전 단계에서 수집 → ea_iid / ea_non_iid 검증 컨디셔닝 데이터셋 (Conditioned) non-vetted 컨디셔너 출력 컨디셔닝 후 연속 출력 → ea_conditioning 검증 재시작 데이터셋 (Restart) 1,000회 재시작 × 1,000샘플 초기 조건 의존성 검증 → ea_restart 검증
커널 대응: 1단계(노이즈 소스)는 hwrng 하드웨어(TRNG 칩, TPM, RDRAND/RDSEED) 또는 Jitter RNG(소프트웨어 노이즈)에 해당합니다. 2단계(디지털화)는 하드웨어 내부에서 수행되며 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)
원시 데이터 접근 주의: 순차 데이터셋은 컨디셔닝 전의 원시 출력이어야 합니다. 많은 하드웨어 RNG가 내부 컨디셔닝을 수행하므로 원시 데이터 접근을 위해 특수 커널 빌드, 하드웨어 디버그 포트, 또는 제조사 SDK가 필요할 수 있습니다. 원시 데이터에 접근할 수 없다면 IID 주장을 포기하고 non-IID 경로로 검증해야 합니다 (IG 7.18 참조).

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]
10가지 추정기 독립 추정 → 최소값 채택 = H_I 원시 엔트로피 데이터 (1,000,000샘플) 1. MCV 7.998 2. Collision 7.996 (비트스트링) 3. Markov 7.973 (비트스트링) ← 최소값 4. Compress 7.989 (비트스트링) 5. t-Tuple 7.991 6. LRS 7.995 7. MultiMCW 7.988 8. Lag 7.992 9. MultiMMC 7.989 10. LZ78Y 7.990 0 4 8 H_I = min = 7.973 (Markov) ■ 통계적 분석 (1~6) ■ 예측 기반 (7~10) ■ 최소값 = H_I (가장 보수적)

추정기 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비트 심볼).

추정기 적용 모드: 비트스트링 vs 다비트 심볼 다비트 심볼 모드 (예: ./ea_non_iid -i data.bin 8) bits_per_symbol = 8, 알파벳 크기 = 256, 추정기 7종 적용 1. MCV 5. t-Tuple 6. LRS 7. MultiMCW 8. Lag 9. MultiMMC 10. LZ78Y H_I = min(7종 추정값) — H ∈ [0, 8] bits per symbol 바이너리 전용 추정기(2, 3, 4번)는 이 모드에서 실행되지 않음 비트스트링 모드 (예: ./ea_non_iid -c -t data.bin 1) bits_per_symbol = 1, 알파벳 크기 = 2, 추정기 10종 모두 적용 1. MCV 2. Collision 3. Markov 4. Compress 5. t-Tuple 6. LRS 7. MultiMCW 8. Lag 9. MultiMMC 10.LZ78Y 바이너리 전용 추정기 2, 3, 4번이 추가 실행 (테두리 강조) H_bitstring = min(10종 추정값) — H ∈ [0, 1] bits per bit H_I = min(bits_per_symbol × H_bitstring, H_symbol) — 비트스트링과 심볼 추정값 중 최소값

보정 항과 신뢰구간

모든 추정기는 표본 크기가 유한할 때의 통계적 불확실성을 반영하기 위해 보정 항(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) 사전이 포화되지 않은 경우의 불확실성 보정
보정 항의 철학: SP 800-90B의 모든 보정 항은 보수적 하향 방향으로 작용합니다. 즉, 추정값이 우연히 높게 산출되는 것을 방지하고, 실제 엔트로피가 추정값보다 낮을 위험을 최소화합니다. 이는 암호화 응용에서 엔트로피를 과대평가하는 것이 과소평가하는 것보다 훨씬 위험하기 때문입니다. 표본 크기 n이 증가할수록 보정항의 영향이 감소하여 추정값이 참값에 수렴합니다.
# 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 인증 적합
각 추정기의 수학적 공식·동작 원리·수치 범위 상세 해설은 SP800-90B_EntropyAssessment 세부 평가 항목 상세 해설의 ② ea_non_iid 섹션을 참조하십시오.

헬스 테스트 (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
헬스 테스트 실패 시: RCT 또는 APT가 알람을 발생하면 엔트로피 소스는 즉시 비활성화되어야 하며, 이미 생성된 출력은 폐기해야 합니다. 커널 hwrng 프레임워크에서는 hwrng_unregister() 호출로 드라이버를 제거하고, CRNG는 다른 엔트로피 소스 (Jitter RNG, IRQ 타이밍 등)로 폴백합니다. FIPS 140-3 모듈은 헬스 테스트 실패를 "오류 상태(Error State)"로 간주하고 모든 암호화 서비스를 중단해야 합니다.

재시작 테스트 (Restart Test) 상세

재시작 테스트는 엔트로피 소스의 초기 조건 의존성을 검증합니다. 전원 인가 시 항상 동일한 내부 상태에서 시작한다면, 첫 출력이 예측 가능할 수 있습니다. SP 800-90B는 1,000회 재시작으로 이 위험을 정량화합니다.

데이터 수집 절차:

  1. 엔트로피 소스를 1,000회 재시작 (전원 차단/복구 또는 리셋)
  2. 각 재시작마다 1,000개의 연속 샘플 수집 (시작 테스트 완료 후)
  3. 총 1,000,000개 샘플을 "행 데이터셋(row dataset)" 형식으로 저장 — 1,000행 × 1,000열
  4. 행 단위(단일 재시작 내)와 열 단위(재시작 간) 엔트로피를 각각 추정

재시작 테스트는 순차 데이터셋에서 얻은 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
재시작 시뮬레이션 주의: 재시작은 실제 사용 환경의 재시작 과정을 시뮬레이션해야 합니다. 시작 테스트(Start-up Test)가 완료된 후 샘플을 수집해야 하며, 정상 작동 조건에서 수행해야 합니다. 단순히 소프트웨어 리셋만으로는 하드웨어 초기 상태가 재현되지 않을 수 있으므로, 전원 차단/복구를 포함한 물리적 재시작이 권장됩니다.

컨디셔닝 컴포넌트

컨디셔닝(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) 하에서 수행됩니다. 검증 프로세스는 엔트로피 소스 설계 문서 제출 → 데이터 샘플 수집 → 통계 검증 → 독립 실험실 검토 → 인증서 발급의 단계로 진행됩니다.

1. 설계 문서 제출 엔트로피 소스 아키텍처 노이즈 소스 명세 2. 데이터 샘플 수집 순차 / 컨디셔닝 / 재시작 데이터셋 3. 통계 검증 ea_iid / ea_non_iid ea_restart / ea_conditioning 4. 독립 실험실 검토 CMTL (CMVP Testing Lab) 엔트로피 보고서 검토 5. ACVTS / ESV 인증서 발급 Entropy Source Validation Certificate — CMVP 모듈 인증에 통합 NIST 공식 검증 도구 SP800-90B_EntropyAssessment (C++/OpenMP, GitHub: usnistgov) — 오프라인 테스트용 ESV (Entropy Source Validation) — 차세대 온라인 검증 시스템 GitHub: usnistgov/ESV-Server — 웹 기반 검증 제출 및 결과 관리 ACVTS (Automated Cryptographic Validation Testing System) 프레임워크의 엔트로피 소스 검증 모듈
검증 주체역할프로그램
NIST CAVP 암호화 알고리즘/엔트로피 소스 검증 알고리즘 및 테스트 벡터 관리 CAVTS → ACVTS (자동화 시스템으로 전환)
NIST CMVP 암호화 모듈 전체 인증 (FIPS 140-3) — 엔트로피 소스 검증 결과를 모듈 인증에 통합 FIPS 140-3 인증서
CMTL NIST 인정 독립 검증 실험실 (Cryptographic Module Testing Lab) — 엔트로피 보고서 검토 및 검증 수행 NVML (NIST NVLAP 인정)
ESV 엔트로피 소스 검증 온라인 시스템 — 데이터 제출, 테스트 실행, 결과 관리를 웹에서 수행 ACVTS 프레임워크 일부
개발사 엔트로피 소스 설계, 데이터 샘플 수집, 사전 검증 (오프라인 도구 사용), 엔트로피 보고서 작성
커널 드라이버와 검증의 관계: 커널 hwrng 드라이버 자체는 SP 800-90B 인증 대상이 아닙니다. 인증 대상은 하드웨어 엔트로피 소스(칩 자체)이며, 드라이버는 검증된 엔트로피 소스의 출력을 커널에 전달하는 역할을 합니다. 다만 드라이버가 quality 파라미터로 검증된 H 값을 정확히 반영해야 하며, 헬스 테스트(RCT/APT)를 구현해야 FIPS 140-3 모듈 인증이 가능합니다.

암호 키 생성 완전 플로우

hwrng 장치에서 생성된 엔트로피가 최종 암호 키로 변환되는 전체 흐름을 추적합니다. 커널 내부에서 여러 계층의 처리를 거쳐 안전한 키가 생성됩니다.

hwrng 하드웨어 TRNG / TPM / 외장 RNG 기타 엔트로피 소스 IRQ 지터 / 디스크 I/O / 키보드 hwrng 서브시스템 hwrng_fillfn() 커널 스레드 input_pool Blake2s 256비트 해시 상태 add_hwgenerator_randomness() ChaCha20-CRNG per-CPU 32바이트 키 상태 crng_reseed() 60초 주기 갱신 extract_entropy() AES 세션 키 (128/256비트) TLS 핸드셰이크 논스 ECDH 개인 키 (256비트) 디스크 암호화 마스터 키 SSH 호스트 키 쌍 IPSec SA 키 get_random_bytes() 키 소거 (Key Zeroization) — 사용 후 필수 memzero_explicit(key, sizeof(key)) — 컴파일러 최적화 무력화, 스택/힙 잔류 키 소거 kfree_sensitive(ptr) — 해제 전 자동 제로화, 슬랩 캐시 재사용 방지 Forward Secrecy — Fast Key Erasure 보장 crng_fast_key_erasure(): ChaCha20 출력 앞 32바이트를 즉시 새 키로 교체 → 메모리 탈취되어도 이전 난수 출력 역추적 불가 (Perfect Forward Secrecy)

암호 키 생성 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 기반
 */
원시 hwrng 출력 직접 사용 금지: /dev/hwrng에서 읽은 원시 바이트를 인증 키나 세션 키로 직접 사용하면 하드웨어 편향이 그대로 키에 전파됩니다. 예를 들어 특정 비트가 0에 편향된 TRNG에서 256비트 키를 직접 생성하면 유효 엔트로피가 256비트 미만이 되어 무차별 대입(Brute Force) 공격에 더 취약해집니다. 반드시 getrandom()(CRNG 경유) 또는 KDF(HKDF/PBKDF2) 처리 후 사용해야 합니다. 커널 input_pool의 Blake2s 혼합이 이 편향 제거 역할을 수행합니다.

비밀번호 해시 솔트 생성

비밀번호 기반 인증의 핵심인 솔트(Salt) 생성도 hwrng에 의존합니다. 솔트는 각 사용자 비밀번호 해시마다 고유해야 하며, 예측 가능하면 레인보우 테이블(Rainbow Table)의 효율이 크게 높아집니다.

해시 알고리즘솔트 길이엔트로피 소스커널 인터페이스
bcrypt128비트 (16바이트)getrandom()OpenSSL RAND_bytes()
argon2id128비트 (16바이트)getrandom()libsodium randombytes_buf()
PBKDF2-HMAC-SHA256128비트 (16바이트)getrandom()OpenSSL / glibc getrandom()
scrypt128비트 (16바이트)getrandom()libsodium / libscrypt
SHA-512 crypt (glibc)96비트 (12바이트, base64)/dev/urandomglibc 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); */
/* → 예측 가능, 레인보우 테이블 사전 계산 가능, 인증 무력화 */
솔트 예측 가능성의 영향: 솔트가 32비트만 되어도 전체 사용자에 대한 레인보우 테이블 크기가 232배 증가하여 사전 계산이 비현실적이 됩니다. 하지만 솔트 생성 시간을 추측 가능한 값(time())으로 사용하면 공격자가 특정 시간대에 가입한 사용자의 솔트를 좁힐 수 있습니다. 128비트 솔트를 getrandom()으로 생성하면 시간 추측 공격이 원천 차단됩니다. /etc/shadow의 솔트는 glibc crypt() 함수가 내부적으로 getrandom()을 호출하여 생성하므로, 최신 시스템에서는 안전합니다.

인증 프로토콜에서의 hwrng 역할

hwrng에서 생성된 엔트로피는 커널 CRNG를 거쳐 다양한 인증 프로토콜의 안전성 근간이 됩니다. 이 섹션에서는 hwrng 품질이 직접적으로 인증 보안에 영향을 미치는 주요 프로토콜들을 분석합니다.

인증 프로토콜과 hwrng 엔트로피 흐름 hwrng 하드웨어 TRNG / TPM / RDRAND ChaCha20 CRNG getrandom() / get_random_bytes() 원격 증명 (TPM Quote) qualifying data (nonce) 도전-응답 (CHAP/SCRAM) challenge nonce PKI 인증서 발급 serial number / OCSP nonce FIDO2 / WebAuthn authenticator challenge Kerberos 세션 키 (session key) 엔트로피 품질 저하가 인증에 미치는 영향 원격 증명: 도전값 예측 → 공격자가 사전에 준비한 PCR 값을 재생 → 증명 무력화 도전-응답: challenge 예측 → 응답 사전 계산 → 재생 공격(Replay Attack) 성공 PKI: 일련 번호 추측 → 다음 인증서 번호 예측 → 인증서 충돌 공격 가능 FIDO2: authenticator nonce 예측 → 서명 재사용 → 2단계 인증(2FA) 우회 Kerberos: 세션 키 예측 → 티켓 위조 → 도메인 전체 인증 무력화

원격 증명과 hwrng

원격 증명(Remote Attestation)은 TPM(Trusted Platform Module)이 시스템의 무결성(Integrity) 상태를 외부 검증자(Verifier)에게 암호학적으로 증명하는 프로세스입니다. 이 프로세스에서 hwrng는 도전값(Qualifying Data)의 무작위성을 보장하는 핵심 역할을 수행합니다.

TPM2_Quote 명령의 엔트로피 흐름:

  1. 검증자가 getrandom()로 128~256비트 도전값(qualifying data)을 생성
  2. 검증자가 도전값을 증명 대상(Attestator)에 전송
  3. 증명 대상이 TPM2_Quote 명령으로 도전값 + 선택된 PCR(Platform Configuration Register) 값을 서명
  4. 검증자가 서명된 응답을 검증 — 도전값이 일치하면 재생 공격(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 예측 → 재생 공격
도전값 재사용 공격: 동일한 도전값이 반복 사용되면 공격자가 이전 응답을 재생하여 인증을 통과할 수 있습니다. hwrng 품질이 낮아 도전값의 엔트로피가 부족하면 도전값 공간이 좁아져 우연한 충돌 또는 의도적 충돌(Collision) 공격이 가능해집니다. 128비트 이상의 도전값을 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에 완전 의존
WebAuthn 도전값 최소 크기: W3C WebAuthn 규격은 도전값을 최소 16바이트(128비트)를 권장합니다. 브라우저의 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의 엔트로피 품질에 직접 의존합니다.

세션 키 생성 경로:

  1. KDC의 krb5_c_make_random_key() 호출 → MIT krb5 라이브러리
  2. 라이브러리 내부에서 krb5_c_random_make_octets()/dev/urandom 또는 getrandom()
  3. 추출된 난수를 암호 알고리즘의 키 형식(AES-256, Camellia 등)으로 변환
  4. 생성된 세션 키가 TGT/TGS 티켓에 암호화되어 포함
교차 영역 인증의 엔트로피 의존성: 교차 영역(Cross-Realm) 인증에서는 두 개의 KDC가 각각 자체 세션 키를 생성합니다. 한쪽 KDC의 엔트로피가 약하면 교차 영역 세션 키 전체의 보안이 그 수준으로 떨어집니다. 예를 들어 본부 KDC는 hwrng가 있지만 분사 KDC가 임베디드 장치로 엔트로피가 부족하면, 분사 영역을 경유하는 모든 교차 영역 인증의 세션 키가 예측 가능해집니다. 분산 KDC에도 hwrng 또는 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 품질에 의존

사용자 공간 프로그램의 커널 난수 공급 구조

초보자 비유: 커널 RNG는 정수장(Water Purification Plant)과 같습니다. 정수장이 원수(하드웨어 엔트로피)를 정제하여 깨끗한 수돗물(난수)을 만들면, 각 가정(프로그램)은 수도관(시스템 콜)을 통해 물을 받아 각자의 용도에 맞게 사용합니다. 어떤 가정은 정수기(OpenSSL DRBG)를 한 번 더 거쳐 마시고, 어떤 가정은 바로 수돗물을 사용합니다. 핵심은 모든 가정의 물이 동일한 정수장에서 오며, 정수장의 원수 품질(hwrng)이 모든 가정의 물 품질을 결정한다는 점입니다.

지금까지 커널 내부의 hwrng → input_pool → ChaCha20 CRNG 경로를 살펴봤습니다. 하지만 실제 암호학적 안전성은 사용자 공간(User Space) 프로그램이 이 난수를 어떻게 획득하고 어떻게 사용하는가에 의해 최종 결정됩니다. OpenSSL, OpenSSH, strongSwan, GnuTLS, NSS 등 주요 암호 라이브러리와 응용 프로그램은 각자 고유한 난수 공급 아키텍처를 가지며, 커널 getrandom() 시스템 콜 또는 /dev/urandom에서 시드를 받아 자체 DRBG를 운영합니다. 이 섹션에서는 각 프로그램이 커널로부터 난수를 공급받아 사용하는 전체 경로를 추적합니다.

전체 공급 계층 구조

사용자 공간 프로그램이 커널 난수를 사용하는 경로는 4계층으로 나뉩니다:

  1. 커널 계층(Kernel Layer) — hwrng 하드웨어 → input_pool(Blake2s) → ChaCha20 CRNG
  2. 시스템 콜 계층(System Call Layer)getrandom(2), /dev/urandom, /dev/random, AF_ALG 소켓(crypto API)
  3. 라이브러리 DRBG 계층(Library DRBG Layer) — OpenSSL, GnuTLS, NSS, libgcrypt 각자의 NIST SP 800-90A DRBG 인스턴스
  4. 응용 프로그램 계층(Application Layer) — OpenSSH, strongSwan/charon, Nginx, Apache, Python 등

대부분의 응용 프로그램은 라이브러리 DRBG 계층을 거쳐 난수를 획득하지만, 일부(특히 시스템 유틸리티)는 getrandom()이나 /dev/urandom을 직접 호출하기도 합니다. 다음 다이어그램은 전체 공급 경로를 보여줍니다:

1. 커널 계층 (Kernel Layer) hwrng HW RDRAND/TPM/QRNG input_pool Blake2s 혼합 ChaCha20 CRNG per-CPU, Fast Key Erasure 2. 시스템 콜 계층 (System Call Layer) getrandom(2) /dev/urandom /dev/random AF_ALG 소켓 crypto API (DRBG 직접 호출) 3. 라이브러리 DRBG 계층 (Library DRBG Layer) OpenSSL CTR-DRBG (AES-256) RAND_bytes() / EVP_DRBG GnuTLS DRBG (AES-256-CTR) gnutls_rnd() NSS CTR-DRBG (AES-256) PK11_GenerateRandom() libgcrypt CTR-DRBG / HMAC-DRBG gcry_random_bytes() 4. 응용 프로그램 계층 (Application Layer) OpenSSH (sshd / ssh) • 호스트 키 생성 (RSA/Ed25519) • 세션 키 (KDF: HKDF/SHA-256) • SSH 쿠키 (cookie exchange) • 패킷 패딩 (random padding) • 재시드 시 getrandom() 직접 → OpenSSL RAND_bytes() 경유 또는 arc4random() 폴백 엔트로피 소비: 키 생성 시 256~4096비트, 세션당 ~512비트 요청 빈도: 연결 설정 시 strongSwan (charon) • IKE nonce (Ni, Nr) 256비트 • DH 개인 지수 (DH exponent) • ESP 키 파생 (PRF + nonces) • CHILD_SA 키 재생성 (rekey) • 인증서 serial / nonce → rng_create(RNG_STRONG) FIPS 모드: AF_ALG → DRBG 엔트로피 소비: SA 설정 시 ~512비트, rekey마다 ~256비트 요청 빈도: SA 수명 주기마다 기타 응용 프로그램 • Nginx/Apache: TLS 세션 키 → OpenSSL/GnuTLS RAND • Python: os.urandom() → getrandom • Node.js: crypto.randomBytes() → getrandom() / /dev/urandom • dnssec-keygen: ZSK/KSK 생성 • GnuPG: 세션 키, 서명 nonce → libgcrypt gcry_random_bytes • curl: TLS 핸드셰이크 난수 → OpenSSL/GnuTLS/NSS 위임 FIPS 모드 직접 경로
그림: 커널 hwrng → 시스템 콜 → 라이브러리 DRBG → 응용 프로그램 4계층 난수 공급 구조. 점선은 보조/FIPS 경로

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에서의 우선순위는 다음과 같습니다:

  1. getrandom(2) 시스템 콜 (OpenSSL 1.1.0+ 사용) — GRND_NONBLOCK 플래그 없이 호출하여 CRNG 초기화 전 블로킹 보장. 시드로 256비트(32바이트) 이상 요청
  2. /dev/urandom 폴백getrandom()을 사용할 수 없는 구형 커널(Linux 3.17 미만)에서 사용. OpenSSL 1.1.0 이전에는 /dev/urandom이 기본 경로
  3. /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에서 파생, 독립 상태 요청마다 자동 재시드
Private 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는 다음 조건에서 자동 재시드를 수행합니다:

# 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)는 시작 시 다음 순서로 난수 소스를 초기화합니다:

  1. OpenSSL RAND_bytes() 사용 가능 — OpenSSL이 내부적으로 getrandom()을 호출하여 시드 획득. OpenSSH는 OpenSSL의 DRBG 출력을 그대로 사용
  2. arc4random() 폴백 — BSD 시스템 또는 OpenSSL 미연결 시. arc4random()은 ChaCha20 기반 자체 CSPRNG를 운영하며, 시드는 getrandom() 또는 /dev/urandom에서 획득
  3. /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의 DRBGAF_ALG 소켓을 통해 직접 사용합니다. 이는 인증된 DRBG 구현체(crypto/drbg.c)를 사용하기 위함입니다:

  1. socket(AF_ALG, SOCK_SEQPACKET, 0) — 알고리즘 소켓 생성
  2. bind(fd, {sa_family=AF_ALG, salg_name="drbg_nopr_ctr_aes256", ...}) — DRBG 알고리즘 선택
  3. setsockopt(fd, SOL_ALG, ALG_SET_KEY, seed, seed_len) — 시드 주입 (getrandom()에서 획득)
  4. accept(fd) — operation 소켓 생성
  5. 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
엔트로피 부족 시 IKE 협상 실패: strongSwan charon이 /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 인스턴스를 운영합니다:

/* 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)로 전환했습니다:

libgcrypt (GnuPG)

libgcrypt는 GnuPG, dirmngr, gpg-agent 등에서 사용하는 암호 라이브러리입니다:

공급 경로 종합 비교

프로그램/라이브러리 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
왜 라이브러리가 자체 DRBG를 운영하는가: 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에서 생성)
Python vs OpenSSL/Node.js의 차이: Python의 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 빠른 검증 → 정밀 통계 검증 순서로 진행합니다. 다음 다이어그램은 전체 테스트 워크플로우를 보여줍니다.

/dev/hwrng 원시 바이트 스트림 1단계: 기본 확인 rng_current 확인 rng_available 목록 dd if=/dev/hwrng 읽기 2단계: FIPS 빠른 검증 rngtest -c 1000 FIPS 140-2 4가지 검정 (모노비트/포커/런/롱런) 3단계: NIST SP 800-90B ea_iid (IID 검정) ea_non_iid (비IID 검정) min-entropy 추정 4단계: 정밀 통계 배터리 dieharder -a (전체 스위트) TestU01 BigCrush PractRand (장시간 검증) PASS FAIL PASS FAIL PASS FAIL FAIL 시 점검 항목 하드웨어 연결 확인 → 후처리 파이프라인 점검 → quality 파라미터 하향 → 드라이버 로그 분석 (dmesg) quality 파라미터 결정 NIST min-entropy H∞ 기반: quality = H∞ × 1024 (예: H∞ = 0.98 → quality = 1003) hwrng_register(&my_rng) — 커널 등록 완료

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 도구 설치 및 소스: 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>: 오프셋에서 지정 샘플 수만 읽기
크로스 컴파일(Cross Compilation): 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)에 대응하는 평가 항목들을 수행합니다. 아래는 각 하위 도구가 평가하는 모든 세부 항목을 방법·의미·수치 범위와 함께 열거한 해설입니다.

SP800-90B_EntropyAssessment ea_iid IID 검정 경로 ea_non_iid Non-IID 검정 경로 ea_restart 재시작 테스트 ea_conditioning 컨디셔닝 검증 ea_transpose 데이터 전치 유틸 평가 항목 (22종) 카이제곱 2종 (§5.2) · 독립성, 적합도 LRS 1종 (§5.2) 순열 검정 19종 (§5.1) · Excursion, Runs · Collision, Periodicity · Covariance, Compression → H_I 산출 p-value ≥ α → IID 가설 유지 추정기 10종 1. Most Common Value 2. Collision (바이너리) 3. Markov (바이너리) 4. Compression (바이너리) 5. t-Tuple 6. LRS 7. MultiMCW 8. Lag 9. MultiMMC 10. LZ78Y min(10종) = H_I 평가 항목 (2종) 행간 검정 (Between-row) 1,000행 첫 샘플 분포 일치성 행내 검정 (Within-row) 각 행 내부 엔트로피 일치성 H_r ≥ H_I 평가 항목 (2종) Vetted (-v) SHA-256, AES 등 검증된 알고리즘 h_out=min(h_in,n_out) Non-vetted (-n) 검증되지 않은 알고리즘 h_out=h_in×h' 엔트로피 손실 산정 유틸리티 순차 데이터 → Row dataset 변환 1,000행 × 1,000열 = 1,000,000샘플 ea_restart 전처리 별도 평가 없음 평가 데이터 흐름 원시 샘플 (1,000,000+) → ea_transpose(재시작용) → ea_iid 또는 ea_non_iid → ea_restart → ea_conditioning 수치 범위 요약 ea_iid p-value: [0, 1] α = 0.001 (기준) ea_non_iid H: [0, bits_per_symbol] H' = H / bits_per_symbol ea_restart H_r: [0, H_I] min(H_r,H_c)≥H_I/2 ea_conditioning h_out: [0, n_out] h': (0, 1] (non-vetted) quality = floor(H' × 1024), 범위 [0, 1024] — H' = min-entropy per bit, 범위 [0.0, 1.0] IID 검정 유의수준 α: 0.001 (고정값, 조정 불가), 순열 검정 반복 수: 10,000회 (PERMS 고정값) 재시작 횟수: 1,000회 × 1,000샘플/행 = 1,000,000샘플 row dataset
① 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 가설이 유지됩니다.

ea_iid 검정 수행 순서 (소스 코드 iid_main.cpp 기준):
  1. Most Common Value 추정 — Horiginal (다비트 심볼) 및 Hbitstring (비트스트링) 산출. HI = min(Horiginal, bits_per_symbol × Hbitstring)
  2. 카이제곱 검정 (§5.2) — 독립성 검정 + 적합도 검정, 각각 p-value < 0.001이면 FAIL
  3. LRS 검정 (§5.2.5) — 최장 반복 부분문자열, Pr(X ≥ 1) ≥ 1/1000이면 PASS
  4. 순열 검정 (§5.1) — 19종 통계량 × 10,000회 순열, p-value < 0.001이면 FAIL
세 단계 모두 PASS → IID 가설 유지 → HI = Most Common Value 추정값. 하나라도 FAIL → non-IID 경로 전환.
카이제곱 검정 (§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회 순열을 다중 코어에서 동시에 수행합니다.

순열 검정 원리 (§5.1):
  1. 원본 데이터에서 19종 통계량 Tobs,j 계산 (j = 0..18)
  2. 데이터를 무작위로 순열(shuffle)하여 새 시퀀스 생성
  3. 순열된 시퀀스에서 동일 19종 통계량 Ti,j 계산
  4. 2~3을 10,000회 반복 (PERMS = 10000, 고정값)
  5. 각 통계량별로 카운터 C[j][0], C[j][1], C[j][2] 누적 — 원본보다 크/같/작 경우의 수
  6. p-value = min(C[j][0], C[j][2]) / (C[j][0] + C[j][1] + C[j][2])
  7. p-value < 0.001 → 해당 통계량에 대해 IID 가설 기각
유의수준 0.001은 소스 코드에 고정되어 있으며 명령행 옵션으로 조정할 수 없습니다.

아래 표는 소스 코드 permutation_tests.htest_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]
IID 검정 판정 기준 (소스 코드 검증):
  • 유의수준 α = 0.001 (고정값, chi_square_tests.hpermutation_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 / Conversion II (§5.1):
  • 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로 채택됩니다. 이 "최소값 채택" 원칙은 어떤 단일 추정기도 놓칠 수 없도록 모든 형태의 구조적 편향을 포착하는 다중 방어선 전략입니다.

Non-IID 10가지 추정기 — 분류, 적용 범위, 수치 범위 통계적 분석 기반 (1~6) 예측 기반 (7~10) 1. Most Common Value 전체 | [0, 8] 2. Collision 바이너리 | [0, 1] 3. Markov 바이너리 | [0, 1] 4. Compression 바이너리 | [0, 1] 5. t-Tuple 전체 | [0, 8] 6. LRS 전체 | [0, 8] 7. MultiMCW Prediction 전체 | [0, 8] 8. Lag Prediction 전체 | [0, 8] 9. MultiMMC Prediction 전체 | [0, 8] 10. LZ78Y Prediction 전체 | [0, 8] 최소값 채택 H_I = min(H₁, H₂, ..., H₁₀) 가장 보수적인 추정값이 실제 min-entropy로 보증됨 min-entropy H 수치 범위 (8비트 심볼 기준) 0 위험 (<4) 4 주의 (4~7.5) 7.5 양호 (>7.5) 8.0 quality=0 quality=512 quality=960 quality=1024 H' = H / bits_per_symbol, quality = floor(H' × 1024) — H' ∈ [0.0, 1.0], quality ∈ [0, 1024]

아래는 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(00) / count(0→*)
P01 = count(01) / count(0→*)
P10 = count(10) / count(1→*)
P11 = count(11) / 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.

10가지 추정기 요약 — 적용 모드 정리:
  • 다비트 심볼 모드(예: 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번째 샘플입니다. 이 행렬로부터 두 개의 데이터셋을 구성합니다:

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
재시작 테스트 판정 기준 (표준 §3.1.4.2 직접 인용):
  • 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가 낮으면: 재시작 간 동일 시점의 샘플이 유사 = 초기 조건 의존성 (치명적 취약점)
  • 인자로 전달하는 HIea_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) 두 종류로 분류합니다.

표준 §3.1.5.1.1 — Vetted 컨디셔닝 알고리즘 목록 (표준에서 명시):
  • 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)
위 알고리즘 외의 컨디셔닝(von Neumann 추출기, XOR 폴딩, 사용자 정의 등)은 non-vetted로 분류됩니다.
#검증 항목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 산출
컨디셔닝 검증 핵심 규칙 (표준 §3.1.5 직접 인용):
  • 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 형식)
SP 800-90B 평가 수치 범위 종합 해석 가이드 H' (min-entropy per bit) 0.0 0.5 0.75 0.9375 1.0 위험 주의 양호 quality (커널 파라미터) 0 512 768 960 1024 p-value (IID 순열 검정) 기각 유지 (IID 가설) 0.0 α=0.001 | 1.0 H_r / H_I (재시작 검정) FAIL PASS 0.0 0.95 (임계) 1.0+ quality = floor(H' × 1024) | H' = H_I / bits_per_symbol | 임계값 0.95는 권장 보수적 기준 (표준은 1.0)
실제 hwrng 품질 등급별 수치 기준 (8비트 심볼, H' = H/8):
  • 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이거나 고장난 소스
툴킷 공식 소스 및 표준 문서 메타데이터: 표준 문서 메타데이터:
  • 발행일: 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 게시.
ESV Server 공식 소스:

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)
Python 포트 공식 소스: 주의: 이 패키지는 NIST 원본 코드를 재포장한 비공식 포트입니다. CMVP 인증 제출용 공식 검증은 ESVTS 서버를 통해서만 수행되며, 오프라인 도구 결과는 사전 자체 평가용으로 활용됩니다.

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  → 독립성 확인
dieharder 소스 코드: dieharder는 GPL v2b 오픈소스 프로젝트입니다. 원저자 Robert G. Brown(Duke University)의 공식 홈페이지(webhome.phy.duke.edu/~rgb/General/dieharder.php)에서 소스 tarball과 RPM을 배포하며, 공동 저자 Dirk Eddelbuettel이 관리하는 Debian 유지보수 업스트림 git 저장소(github.com/eddelbuettel/dieharder)에서 전체 이력과 최신 패치를 확인할 수 있습니다. 새 통계 검정이나 RNG(Random Number Generator)를 직접 추가하려면 소스를 빌드해 테스트 플러그인을 작성할 수 있습니다.

드라이버 로드/언로드 검증 체크리스트

단계확인 명령정상 기대값
드라이버 로드 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_poolblocking_pool 두 개의 풀이 존재했지만, 5.6에서 blocking_pool이 제거되어 input_pool 단일 구조가 되었고, 5.17에서는 기존 LFSR + SHA-1 기반 믹싱이 Blake2s 해시 함수로 완전 교체되었습니다. 이 섹션에서는 풀의 내부 구조, 믹싱 알고리즘, 엔트로피 추정 메커니즘을 심층적으로 분석합니다.

풀 구조의 진화: LFSR에서 Blake2s로

역사적 배경: Linux 5.16 이전까지 엔트로피 풀은 LFSR(Linear Feedback Shift Register) 기반 믹싱 함수와 SHA-1 기반 추출(Extraction) 함수를 사용했습니다. 2022년, 커밋 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 기반 extractSHA-1 기반 extractBlake2s 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();
}
Blake2s 믹싱의 보안 증명:
  • 정보이론적 안전성 (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) 와 혼합되면 출력은 여전히 예측 불가능합니다.
단일 하드웨어 RNG 소스 사용 시 주의사항:
  • 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=1024 ≠ 엔트로피 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 드라이버 IRQ 타이밍 지터 디스크/네트워크 I/O 키보드/마우스 이벤트 add_hwgenerator_randomness() add_interrupt_randomness() add_disk_randomness() add_input_randomness() input_pool (Blake2s) h[0..7]: 256비트 해시 상태 t[0..1]: 바이트 카운터 buf[64]: 입력 버퍼 init_bits: 0 → 256 (CRNG) _mix_pool_bytes() extract_entropy() Blake2s final + re-absorb 재혼합 (backtrack resistance) ChaCha20-CRNG (per-CPU) 32바이트 키 + Fast Key Erasure crng_reseed() /dev/urandom getrandom() hwrng (하드웨어) 소프트웨어 엔트로피 핵심 풀 상태 재혼합 (비가역)

엔트로피 추정과 크레딧 정책

커널은 각 소스별로 다른 엔트로피 크레딧 정책을 적용합니다. 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 설정 시에만 크레딧 부여 */
    }
}
Intel RDRAND vs RDSEED 아키텍처 비교 열잡음 디지털 RNG (ES, Entropy Source) 실리콘 트랜지스터 열적 요동 → 비결정적 비트 생성 RDSEED 경로 (순수 엔트로피) ES 컨디셔너 (AES-CBC-MAC) RDSEED 출력 (64비트) DRBG 미경유 → 원시 시드 실패 확률 높음 (ES 고갈 시) 용도: DRBG 시드, 커널 풀 시딩 처리율: ~70 MB/s (변동) 엔트로피 크레딧: 64비트/호출 RDRAND 경로 (DRBG 출력) CTR-DRBG (AES-256-CTR) RDRAND 출력 (64비트) DRBG 경유 → 결정론적 확장 실패 확률 매우 낮음 (버퍼링) 용도: 빠른 난수, ASLR, 스택 카나리 처리율: ~500 MB/s (일정) 엔트로피 크레딧: 조건부 직접 출력 시드 공급 커널 권장: RDSEED 우선 → 실패 시 RDRAND 폴백 (try_to_generate_entropy)

RDRAND vs RDSEED 상세 비교표

항목RDRANDRDSEED
도입 시기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 140 인증 프로세스와 커널 RNG FIPS 140-2 (레거시, 2001~2026) 셀프 테스트: 전원 투입 시 Known Answer Test 연속 RNG 테스트: 연속 동일 출력 감지 통계 테스트: Monobit, Poker, Runs, Long Runs DRBG: SP 800-90A (CTR-DRBG, Hash-DRBG) 엔트로피 소스: SP 800-90B 필수 아님 보안 레벨: 1~4 (Level 1 = 소프트웨어만) 폐지 예정: 2026년 9월 이후 신규 인증 불가 FIPS 140-3 (현행, 2019~) 셀프 테스트: 사전 운용 + 조건부 테스트 CRNGT: 연속 난수 생성 테스트 강화 건강 테스트: Repetition Count + Adaptive Proportion DRBG: SP 800-90A Rev.1 (예측 저항 필수) 엔트로피 소스: SP 800-90B 필수 보안 레벨: 1~4 (ISO 19790 정렬) Linux: CONFIG_CRYPTO_FIPS + fips=1 부팅 전환 커널 부팅 시 FIPS 셀프 테스트 순서 (fips=1) 1. 무결성 검증 HMAC-SHA256 커널 이미지 검증 2. 암호 알고리즘 KAT AES, SHA, HMAC Known Answer Test 3. DRBG 셀프 테스트 CTR-DRBG KAT alg_test_drbg() 4. RNG 연속 테스트 CRNGT 활성화 연속 동일 출력 감지 모든 테스트 통과 → 정상 부팅 계속 테스트 실패 → kernel panic() 호출 FIPS 모드에서는 인증되지 않은 암호 알고리즘 사용이 차단됩니다 (crypto_unregister_alg 호출) Red Hat, Ubuntu Pro, SUSE SLE 등 상용 배포판이 FIPS 인증된 커널 패키지를 제공합니다

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이 미국 연방 정부에 요구하는 것과 동일한 구조입니다.

항목KCMVPFIPS 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
LSH 해시함수 지원의 의미: KCMVP 검증 DRBG는 국산 해시함수 LSH(Lightweight Secure Hash)를 내부 압축 함수로 사용할 수 있습니다. LSH는 한국 인터넷진흥원(KISA)이 개발한 해시함수로 KS X 3262로 표준화되어 있으며, SHA-3와 경쟁하는 한국 고유 알고리즘입니다. LSH 기반 DRBG는 NIST SP 800-90A에 대응하는 알고리즘이 없으므로, FIPS 140 인증과 KCMVP 인증을 동시에 받으려면 SHA-2 또는 SHA-3 기반 DRBG를 사용해야 합니다.

난수발생기 신규 시험방법론 (2022년 6월 의무 적용)

2022년 1월 5일 KISA는 "난수발생기 신규 시험방법론"을 발표하고, 2022년 6월 1일부터 의무 적용했습니다. 이 방법론은 종전의 DRBG 알고리즘 구현 적합성 검증만 수행하던 방식에서, 잡음원(Noise Source) → 엔트로피 소스(Entropy Source) → DRBG 3단계 전체를 검증하는 방식으로 전환했습니다. 이는 NIST SP 800-90B의 엔트로피 소스 검증 접근법과 동일한 방향입니다.

KCMVP 난수발생기 신규 시험방법론 — 3단계 검증 구조 1단계: 잡음원 Noise Source 물리적/소프트웨어 비결정론 소스 TTAK.KO-12.0235 OS별 잡음원 수집 및 운용지침 2단계: 엔트로피 소스 Entropy Source 통계 검사 + 엔트로피 추정 TTAK.KO-12.0341/R1 NIST SP 800-90B 활용 3단계: DRBG 결정론적 난수발생기 KAT + 구현 적합성 검증 TTAK.KO-12.0331/0332/0189 KS X ISO/IEC 18031 난수 출력 검증된 난수 스트림 종전 (2022년 6월 이전) DRBG 알고리즘 구현 적합성 검증만 잡음원/엔트로피 소스 평가 없음 신규 (2022년 6월 이후) 잡음원 → 엔트로피 소스 → DRBG 3단계 전체 검증 NIST SP 800-90B 엔트로피 평가 방법론 채택 적용 시점 및 대상 2022년 6월 1일부터 의무 적용 — 신규 신청 및 보안기능 변경/유효기간 만료 재검증 최초 인증 사례: ICTK G3K 보안칩 (2022.7.13) — PUF 기술 접목, 2등급 획득

적용 기술표준 문서:

표준번호명칭역할
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에서 참조
엔트로피 평가 결과서 제출 의무: 신규 시험방법론 적용 후, 시험 신청 시 "엔트로피 평가 결과서"를 제출해야 합니다. 이 결과서는 TTAK.KO-12.0341/R1에 따라 잡음원에서 수집한 데이터에 대해 통계 검사와 엔트로피 추정을 수행한 결과를 포함하며, NIST SP 800-90B의 IID/non-IID 검증과 유사한 절차를 따릅니다. 하드웨어 TRNG가 내장된 모듈은 해당 TRNG의 엔트로피 평가를, 소프트웨어 잡음원만 사용하는 모듈은 OS에서 수집한 잡음원 데이터의 엔트로피 평가를 수행해야 합니다.

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비트, 요건 충족
보안강도 미충족 시 조치: RBG가 모듈의 최대 보안강도를 지원하지 못하는 경우, 보안정책서(Security Policy Statement)에 해당 사실을 명시하고 암호 서비스의 보안강도를 RBG의 보안강도로 제한해야 합니다. 예를 들어 ARIA-256을 사용하더라도 RBG가 128비트만 지원하면 모듈 전체의 보안강도는 128비트로 제한됩니다.

자가시험 요건 강화

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 등)을 결합하여 운용할 수 있습니다.

하이브리드 키 설정 방식: 검증대상 알고리즘으로 생성된 공유 비밀값과 PQC로 생성된 값을 연접(concatenation)하여 새로운 공유 비밀값을 구성합니다. 이때 PQC는 비보안 구성요소로 분류되며, 암호모듈의 검증대상 동작을 방해하거나 손상시키지 않아야 합니다. NIST나 ETSI 등 국제 표준 방식을 준용하거나 자체 방식을 구현할 수 있습니다. 이는 NIST FIPS 140-3 IG의 PQC 전환 가이드라인과 동일한 방향으로, 한국이 선제적으로 PQC 전환을 지원하는 조치입니다.

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)
소프트웨어 KCMVP 모듈의 엔트로피 평가와 커널 hwrng: 소프트웨어 기반 KCMVP 모듈이 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 난수 관련 참고 자료

공식 기관 및 제도

기반 표준

GVI 개정 및 PQC 전환

최초 인증 사례

관련 본 문서 섹션

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;
}
hwrng 코어 프레임워크 구조 intel-rng quality=1024 virtio-rng quality=1024 tpm-rng quality=0~1024 bcm2835-rng quality=200 외장 hwrng quality=설정값 hwrng_register() 이름 검증 + 초기화 quality 비교 → current rng_list (연결 리스트) intel-rng (current_rng) virtio-rng tpm-rng, bcm2835, ... [hwrng] kthread hwrng_fillfn() current_rng->read() 주기 호출 4096바이트씩 수집 get_current_rng() /dev/hwrng (misc 10,183) 사용자 공간 직접 읽기 rng_dev_read() → rng->read() sysfs 인터페이스 rng_current: 활성 드라이버 rng_available: 등록 목록 rng_quality: quality 값 조회 input_pool add_hwgenerator_randomness() 주요 hwrng 드라이버 비교 드라이버 인터페이스 read() 구현 특이사항 처리율 intel-rng RDRAND/RDSEED CPU 명령어 직접 CONFIG_RANDOM_TRUST_CPU 500 MB/s virtio-rng virtqueue I/O 호스트에서 공급 KVM/QEMU 게스트 필수 호스트 의존 tpm-rng TPM 커맨드 TPM2_GetRandom 느리지만 독립 하드웨어 ~1 MB/s bcm2835 MMIO 레지스터 FIFO 폴링 라즈베리파이 전용 ~10 MB/s exynos-trng MMIO + IRQ 인터럽트 대기 삼성 Exynos SoC ~5 MB/s

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;
}
QEMU 설정: -device virtio-rng-pci,max-bytes=1024,period=1000으로 가상 RNG를 추가합니다. max-bytesperiod(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() 시스템 콜 실행 흐름 getrandom(buf, len, flags) syscall #318 flags 검사 GRND_INSECURE 즉시 반환 (안전하지 않음) CRNG 초기화 무시 crng_ready()? 아니오 GRND_NONBLOCK 검사 NONBLOCK=1 → -EAGAIN 반환 NONBLOCK=0 → 블록 대기 wait_for_random_bytes() ChaCha20-CRNG 난수 생성 get_random_bytes_user() → copy_to_user() len 바이트 반환 flags=0 CRNG 준비 전 블록 권장 기본값 GRND_NONBLOCK 미준비 시 -EAGAIN 비동기 처리용 GRND_RANDOM /dev/random 에뮬레이션 레거시, 권장 안 함 GRND_INSECURE 항상 즉시 반환 보안 용도 부적합

부팅 초기 블로킹 문제와 대응

부팅 초기 엔트로피 기근: 헤드리스 서버, 임베디드 장치, 컨테이너(Container)에서는 키보드/마우스 입력이 없고, 디스크 I/O가 적어 CRNG 초기화(256비트)에 수십 초가 걸릴 수 있습니다. 이 기간 동안 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초) 내에 재시드가 발생하지 않아 일시적 처리율 저하가 나타날 수 있습니다.

systemd 의존성 권장 조합: 인증 서비스 유닛에 다음 의존성을 명시하여 CRNG 초기화와 엔트로피 복원이 완료된 후 시작되도록 보장합니다. 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가지 요소로 조립한다는 것입니다. 아래 다이어그램은 이 조립 과정과 각 요소의 출처를 추적합니다.

ChaCha20 블록 입력 state 조립 — 16워드(64바이트)의 구성과 출처 crng_fast_key_erasure()가 매 호출마다 64바이트 state를 조립 → chacha20_block() 입력 ① 상수 (word 0-3) chacha_init_consts() — "expand 32-byte k" 고정값 (변경 없음) ② 키 (word 4-11, 256비트) crng->key (32바이트) ← SEED: input_pool(Blake2s) → extract_entropy() → base_crng.key → per-CPU 복사 (세대 불일치 시) Fast Key Erasure로 매 출력마다 갱신 ③ 카운터 (word 12-13): 64비트, 0에서 시작 ④ 논스 (word 14-15): memset 0 (고정) ChaCha20 state (4×4 = 16 워드 = 64바이트) word 0 const word 1 const word 2 const word 3 const word 4 key[0] word 5 key[1] word 6 key[2] word 7 key[3] word 8 key[4] word 9 key[5] word 10 key[6] word 11 key[7] word 12 counter word 13 ctr hi word 14 nonce=0 word 15 nonce=0 chacha20_block(state, stream) chacha20_block() — 20라운드 (10 double-round) 64바이트 state → 64바이트 키스트림 출력 앞 32바이트 → 새 키 (Fast Key Erasure) 뒤 32바이트 → 난수 출력 (get_random_bytes) state = 상수(고정) + 키(SEED 유래) + 64비트 카운터(0 시작) + 64비트 논스(0 고정) → 블록 함수 통과 → 64바이트 출력 CRNG에서 논스를 0으로 고정하는 이유: 키 자체가 SEED에서 갱신되므로 키마다 고유한 스트림이 보장됨
이 다이어그램이 답하는 질문: "ChaCha20 블록의 입력은 무엇으로 채우는가?" — 정답은 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 → (상수+카운터+논스와 함께) 블록 입력이라는 파이프라인을 분해해 보여주기 위함입니다.
ChaCha20 CRNG: per-CPU 구조와 리시드 흐름 input_pool (Blake2s) 엔트로피 수집 + 혼합 base_crng (전역) key[32]: 마스터 키 generation: 리시드 세대 extract_entropy() 리시드 조건 60초 경과 (CRNG_RESEED_INTERVAL) 또는 충분한 엔트로피 누적 per-CPU CRNG 인스턴스 (NUMA 최적화) CPU 0 (Node 0) key[32]: per-CPU 키 generation: 세대 동기 local_lock: 경량 락 No lock contention CPU 1 (Node 0) key[32]: per-CPU 키 generation: 세대 동기 local_lock: 경량 락 No lock contention CPU 2 (Node 1) key[32]: per-CPU 키 generation: 세대 동기 local_lock: 경량 락 NUMA-local 접근 CPU N (Node M) key[32]: per-CPU 키 generation: 세대 동기 local_lock: 경량 락 ... 세대 불일치 시 키 갱신 Fast Key Erasure (crng_fast_key_erasure) 앞 32바이트 → 새 키로 교체 뒤 32바이트 → 난수 출력 이전 키를 즉시 덮어쓰므로 메모리 탈취 시에도 과거 출력 역추적 불가 → Perfect Forward Secrecy + Information-theoretic security

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);
}
성능 이점: per-CPU CRNG 구조 덕분에 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 운용 키)
핵심 설계 원칙: 각 단계는 단방향 흐름으로 설계됩니다. 엔트로피 소스 → input_pool은 Blake2s의 단방향 해시 특성으로 역추적이 불가능하며, input_pool → CRNG 키는 extract_entropy() 호출 시 풀 상태를 변경(re-keying)하여 동일한 시드가 두 번 추출되지 않습니다. 이 단방향성이 Forward Secrecy의 전제 조건입니다.
SEED 주입 파이프라인 — 엔트로피 소스 → input_pool → CRNG 키 1단계: 엔트로피 수집 hwrng 드라이버 HW TRNG (TPM, PCIe RNG) RDSEED / RDRAND CPU 온다이 DRBG / 원시 엔트로피 인터럽트 타이밍 지터 add_interrupt_randomness() jitterentropy CPU 스케줄링 지터 기반 random-seed 파일 부팅 시 저장된 시드 복원 2단계: 컨디셔닝 input_pool Blake2s 해시 상태 h[8] + t[2] (256비트 상태) mix_pool_bytes() — 단방향 혼합 init_bits: 엔트로피 크레딧 누적 extract_entropy() 32바이트 3단계: 시드 주입 base_crng (전역 마스터) key[32] ← 추출된 시드 generation++ (세대 증가) 세대 전파 per-CPU CRNG 인스턴스 CPU 0: key[32] (독립 키) CPU 1: key[32] (독립 키) CPU N: key[32] (독립 키) generation 동기화 시 키 복사 이후 Fast Key Erasure로 자체 갱신 단방향 흐름 — Forward Secrecy의 전제 조건 수집 → 컨디셔닝 Blake2s 단방향 해시 → 역추적 불가 컨디셔닝 → 주입 extract_entropy() 시 풀 재키잉 → 중복 시드 방지 주입 → 출력 Fast Key Erasure → 키 소거 후 역산 불가 엔트로피 크레딧(init_bits)과 시드 주입 타이밍 0 ~ 255비트: CRNG 미초기화 256비트: crng_ready=true 추가 누적: 재시드 크레딧 60초 경과: crng_reseed() 강제 부팅 시 256비트 누적 전까지 getrandom()은 블로킹 → 시드 주입은 보안적 "게이트" 역할 이 게이트가 없으면 낮은 엔트로피 상태에서 예측 가능한 키가 생성되어 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;
 *   }
 */
SEED 주입 라이프사이클 — 부팅부터 런타임 재시드까지 시간 → 부팅 초기 init_bits < 256 random_init() RDSEED/RDRAND 수집 random-seed 로드 인터럽트 엔트로피 누적 crng_ready = false getrandom() 블로킹 CRNG 키 없음 엔트로피: ~180/256 비트 init_bits ≥ 256 초기 시드 주입 crng_reseed(NULL) extract_entropy(32B) → base_crng.key 로드 generation = 1 crng_ready = true getrandom() 해제 memzero_explicit(key) wake_up(crng_init_wait) 엔트로피: 256/256 비트 → 시드 추출 후 리셋 런타임 — 60초 재시드 주기 정상 운용 구간 get_random_bytes() → Fast Key Erasure 매 64바이트 출력마다 키 자체 갱신 Forward Secrecy 보장 (과거 보호) Prediction resistance 미보장 (덤프 시 정방향 예측 가능 구간) 60초 경과 재시드 crng_reseed() 새 시드 주입 키 교체 gen++ 엔트로피: 60초 동안 누적 → 재시드 시 32바이트 추출 후 재누적 시드 주입과 Forward Secrecy의 연결 초기 시드 주입 → CRNG 가동의 출발점 256비트 독립 엔트로피로 키 생성 과거 출력 없음 → FS 의미 없음 단, 이 키의 품질이 모든 FS의 기반 Fast Key Erasure (출력마다) → Forward Secrecy (과거 보호) 매 64바이트마다 키 소거 + 교체 시드 없이도 자체 키 갱신으로 FS 유지 단, 미래 예측은 막지 못함 재시드 (60초마다) → Prediction resistance (미래 보호) 새 시드로 키 교체 → 덤프 키 무효화 과거 FS는 유지, 미래 예측 저항 회복 시드 주입이 PR의 유일한 회복 수단

input_pool의 시드 컨디셔너 역할

input_pool은 시드 주입 파이프라인에서 컨디셔너(Conditioner) 역할을 담당합니다. 엔트로피 소스들이 제공하는 원시 비트는 품질이 균일하지 않고, 일부 소스는 편향(Bias)을 가질 수 있습니다. input_pool은 Blake2s 암호학적 해시 함수를 사용하여 이러한 불균일한 입력을 균일한 256비트 시드로 압축합니다.

컨디셔닝 과정이 중요한 이유는 다음과 같습니다:

/* 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));
}
시드 추출의 "재혼합"이 Forward Secrecy에 기여하는 방식: 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;
}
hwrng 품질과 시드 주입의 관계: hwrng 드라이버의 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)
시드 주입 없는 CRNG는 Forward Secrecy만 있고 Prediction resistance 없음: 시드가 한 번 주입된 후 재시드가 영원히 일어나지 않는다면, Fast Key Erasure로 인해 과거 출력은 안전하지만, 공격자가 어느 시점이든 키를 덤프하면 그 이후 모든 미래 출력이 예측 가능합니다. 이것이 재시드가 "선택 사항"이 아니라 필수 보안 메커니즘인 이유입니다. hwrng가 input_pool에 지속적으로 엔트로피를 공급하지 않으면, 재시드의 시드 품질이 저하되어 Prediction resistance가 약해집니다.

SEED/Re-SEED → Key → 출력 전체 데이터 흐름

지금까지 개별 절에서 시드 주입, 키 관리, Fast Key Erasure를 각각 살펴보았습니다. 이 절에서는 이 세 메커니즘이 하나의 파이프라인으로 어떻게 연결되어 동작하는지 단일 다이어그램으로 정리합니다. 이 흐름은 ChaCha20-CRNG의 전체 구현 골격이며, 각 단계가 어떤 데이터를 소비하고 어떤 데이터를 출력하는지 추적할 수 있습니다.

ChaCha20-CRNG 전체 데이터 흐름 — SEED/Re-SEED → Key → 출력 → 키 소거 ① SEED / Re-SEED 주입 input_pool (Blake2s) h[8] 해시 상태 + init_bits 엔트로피 소스 혼합 (단방향) extract_entropy(32B) 256비트 시드 추출 출력 후 풀에 재혼합 → 재추출 방지 초기 시드 부팅 시 1회 crng_init_primary Re-SEED 60초마다 crng_reseed() memzero_explicit(key) 스택 잔류 시드 소거 ② Key 관리 base_crng (전역 마스터) key[32] ← 추출된 시드 generation: 세대 번호 spinlock: 전역 보호 generation 불일치 시 per-CPU crng (지연 동기화) CPU 0: key[32] + generation CPU 1: key[32] + generation CPU N: key[32] + generation local_lock (경합 없음) crng_make_state() ChaCha20 상태 조립 word[0-3]: 상수 "expand 32-byte k" word[4-11]: key[32] 복사 word[12]: 카운터 · word[13-15]: 논스(0) ③ ChaCha20 출력 + Fast Key Erasure chacha20_block(state) 10 double-round (총 20라운드) QUARTERROUND: ARX 연산 (Add-Rotate-XOR) 64바이트 키스트림 출력 state[12]++ (블록 카운터 증가) 앞 32바이트 → 새 key[32] crng->key 교체 Fast Key Erasure 뒤 32바이트 → 사용자 출력 get_random_bytes() getrandom() 등 구 키 memzero_explicit 시드 주입 상태 전달 다음 64바이트 블록 — 새 키로 상태 재조립 단계별 데이터 추적 — 무엇이 들어가고 무엇이 나오는가 단계 ① SEED 주입 입력: 엔트로피 소스 (hwrng, RDSEED, IRQ) 출력: 256비트 시드 (32바이트) 소거: 스택 시드 → memzero_explicit 단계 ② Key 관리 입력: 256비트 시드 → base_crng.key 전파: generation 불일치 시 per-CPU 복사 출력: ChaCha20 상태 (16 워드 = 64바이트) 단계 ③ 출력 + 키 소거 입력: ChaCha20 상태 → 20라운드 출력: 뒤 32바이트 → 사용자 난수 키 교체: 앞 32바이트 → 새 키 (구 키 소거) 각 단계가 담당하는 보안 속성 ① SEED 주입 → Prediction resistance (미래 보호) 새 독립 엔트로피로 키 교체 재시드 후 덤프 키 무효화 주기: 60초 (CRNG_RESEED_INTERVAL) ② Key 관리 → 독립성 보장 (키 분리) base_crng → per-CPU 키 전파 CPU별 독립 키 → lock 경합 없음 지연 동기화 (lazy sync) ③ 출력 + 키 소거 → Forward Secrecy (과거 보호) 매 64바이트마다 키 교체 구 키 소거 → 역산 불가 주기: 매 출력 블록 (64바이트)
한눈에 보는 핵심: 위 다이어그램의 좌→중→우 흐름은 시드가 키가 되고, 키가 난수가 되는 정방향 파이프라인입니다. 우측 하단의 순환 화살표는 Fast Key Erasure 사이클 — 한 번의 64바이트 출력 후 새 키로 상태를 재조립하여 다음 블록을 생성하는 무한 루프입니다. 이 루프는 60초마다 좌측의 Re-SEED가 새 시드를 주입하여 키를 "갈아끼울" 때까지 자체적으로 순환하며, 그 사이에 Forward Secrecy는 매 블록마다, Prediction resistance는 매 재시드마다 갱신됩니다.

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"는 시간적으로 과거를 보호하는 속성입니다. 공격자가 "앞으로(forward in time)" 시점의 상태를 덤프해도 "뒤로(back) 추적"할 수 없다는 뜻에서 Backtracking resistance라는 표현과 같은 개념입니다. 반대로 "미래"를 보호하는 Prediction resistance는 후방(Backward) 비밀성에 해당합니다. 두 용어의 방향이 반대이므로 문서마다 혼용되지만, 본 문서에서는 과거 보호 = Forward Secrecy, 미래 보호 = Prediction resistance로 일관되게 사용합니다.

위협 모델 — 메모리 덤프 공격자

Forward Secrecy의 위협 모델은 전체 메모리 스냅샷을 확보한 공격자입니다. 구체적으로 다음 능력을 가정합니다:

이 공격자 모델에서 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를 새 엔트로피로 갱신할 때까지 지속됨.
 */
직관: 키를 일회용 영수증(한 번 쓰고 태우는 종이)처럼 취급합니다. 현재 영수증(K_{i+1})을 들고 있어도 이미 태운 이전 영수증(K_i)의 내용은 복원할 수 없지만, 현재 영수증으로 다음 영수증(K_{i+2})을 발급하는 것까지는 막을 수 없습니다. 재시드는 "새 영수증책을 새로 깔아주는" 작업에 해당합니다.

리시드 주기와 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 / Prediction resistance 위협 모델 — 시간선 출력 스트림 O_1 … O_n 과거 덤프 시점 t 재시드 t+r 공격자 메모리 덤프 crng_reseed() Forward Secrecy 보장 구간 과거 출력 O_1..O_t — K_t로 역산 불가 (ChaCha20 one-way) Prediction resistance 실패 구간 O_t..O_{t+r-1} — K_t로 정방향 예측 가능 회복 K_{t+r} 독립 핵심: Forward Secrecy는 매 출력마다 자동 보장, Prediction resistance는 재시드 주기(약 60초)마다 회복 세분성(Granularity) 비대칭 Forward Secrecy 세분성 64바이트 출력 블록마다 키 교체 → 과거 보호 Prediction resistance 세분성 CRNG_RESEED_INTERVAL(약 60초)마다 회복 → 미래 보호 → 성능과 보안의 균형: 빈번한 재시드는 Prediction resistance를 높이나 get_random_bytes() 처리율 저하 → FIPS 140-3 Level 4 등 강제 환경은 DRBG PR 모드(drbg_pr_*)로 매 요청 재시드 채택 기본 CRNG는 60초 재시드로 초당 수천만 건의 get_random_bytes()를 Forward Secrecy와 함께 보장

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 롤백은 가장 심각한 Forward Secrecy 위협: 하이퍼바이저 스냅샷은 CRNG 키를 그대로 보존하므로, 롤백 후 생성되는 모든 난수(SSH 호스트 키, TLS 세션 키, 디스크 암호화 키 등)가 이전 부팅과 동일하게 재생성될 수 있습니다. 클라우드 환경에서는 반드시 vm.genid 또는 virtio-rng를 통해 게스트가 롤백을 감지하고 CRNG를 재초기화하도록 구성해야 합니다. 게스트가 롤백을 감지하지 못하면 Forward Secrecy와 Prediction resistance가 동시에 붕괴합니다.

RDSEED vs RDRAND와 Forward Secrecy

x86 하드웨어 RNG 명령인 RDSEED와 RDRAND는 Forward Secrecy에 서로 다른 기여를 합니다. 두 명령의 차이가 커널 RNG의 Forward Secrecy 보장에 어떻게 연결되는지 정리합니다.

구분RDRANDRDSEED
출력 성격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 계층 생략
계층적 Forward Secrecy: 보안은 계층적으로 쌓입니다. ① 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
한계: Forward Secrecy의 핵심인 "ChaCha20 역산 불가"는 계산 복잡성 가정에 의존하므로 수학적으로 완전히 증명된 것이 아닙니다. 다만 ChaCha20은 현재 8라운드까지만 공격이 알려져 있고 커널은 20라운드를 사용하므로, 실용적으로 충분한 보안 마진(Security Margin)을 가집니다. 양자 컴퓨터에 대해서도 대칭 암호 스트림의 키 공간은 Grover 알고리즘으로 절반(2128)만 줄어들므로 여전히 안전합니다.

엔트로피 소스 완전 가이드

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()에서 호출 */
엔트로피 소스 분류와 수집 경로 하드웨어 엔트로피 소스 RDRAND/RDSEED (CPU 내장) TPM 2.0 RNG (독립 칩) QRNG (양자 디바이스) SoC 내장 TRNG (ARM 등) 크레딧: quality 파라미터 기반 소프트웨어 엔트로피 소스 인터럽트 타이밍 지터 (TSC) 디스크 I/O 완료 타이밍 키보드/마우스 이벤트 코드+타이밍 jitterentropy (CPU 실행 지터) 크레딧: 이벤트 빈도 + 델타 기반 수집 함수 (API 계층) add_hwgenerator_randomness() add_interrupt_randomness() add_disk_randomness() add_input_randomness() add_device_randomness() add_bootloader_randomness() 공통: _mix_pool_bytes() 호출 + credit_init_bits() 크레딧 부여 (소스별 크레딧 정책 상이) input_pool Blake2s 256비트 상태 init_bits: 0 → 256 256비트 도달 → CRNG 초기화 완료! 환경별 주요 소스 물리 서버: RDRAND + IRQ + 디스크 VM/KVM: virtio-rng + IRQ 임베디드: SoC TRNG + jitter 컨테이너: 호스트 CRNG 공유 IoT/MCU: 전용 TRNG + 외장 hwrng

ftrace/bpftrace 엔트로피 모니터링

hwrng 서브시스템과 엔트로피 풀의 동작을 실시간으로 관찰하는 것은 드라이버 디버깅(Debugging)과 성능 분석에 필수적입니다. 이 섹션에서는 ftrace, bpftrace, /proc/sys/kernel/random/ 인터페이스를 활용한 모니터링 기법을 다룹니다.

엔트로피 모니터링 도구와 관찰 지점 hwrng 드라이버 input_pool 믹싱 ChaCha20 CRNG 사용자 공간 출력 kprobe 관찰 지점 (5.18+: tracepoint 제거, kprobe 사용) mix_pool_bytes credit_init_bits extract_entropy getrandom / urandom_read bpftrace kprobe 관찰 지점 kprobe:add_hwgenerator_randomness kprobe:add_interrupt_randomness 수집 빈도 + 크기 히스토그램 kprobe:extract_entropy kprobe:crng_reseed 추출/리시드 주기 분석 kprobe:get_random_bytes tracepoint:sys_enter_getrandom 호출자 + 크기 분석 /proc/sys/kernel/random/ entropy_avail, poolsize 실시간 상태 조회 모니터링 전략: ftrace(이벤트 추적) + bpftrace(통계 집계) + /proc(실시간 상태) perf stat으로 RDRAND 명령어 실행 횟수, dd로 처리율 벤치마크 병행

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 우수

설계 철학 비교

Linux: "믹싱 우선" — 모든 가능한 엔트로피 소스를 Blake2s 풀에 혼합합니다. hwrng 프레임워크로 다양한 하드웨어를 지원하되, 소프트웨어 지터만으로도 안전하게 동작합니다. FIPS 모드를 선택적으로 지원합니다.
OpenBSD: "단순성 우선" — 별도의 hwrng 프레임워크 없이 RDRAND과 인터럽트 지터만 사용합니다. arc4random()은 항상 비블록킹이며 libc에서 직접 제공합니다. getentropy()의 최대 크기가 256바이트로 제한됩니다.
FreeBSD: "Fortuna 설계" — Niels Ferguson과 Bruce Schneier의 Fortuna 알고리즘을 충실히 구현합니다. 32개의 분리된 엔트로피 풀과 지수적 리시드 스케줄로 높은 복원력을 제공합니다.
RNG 아키텍처 비교: Linux vs OpenBSD vs FreeBSD Linux (5.18+) hwrng (다중 드라이버) input_pool (Blake2s 256b) ChaCha20-CRNG (per-CPU) getrandom() /dev/urandom 처리율: ~1.1 GB/s SIMD: AVX2, NEON, SSE 확장성: per-CPU 선형 FIPS: CONFIG_CRYPTO_FIPS 부팅 블록: getrandom() 대기 OpenBSD (7.x) RDRAND + IRQ 지터 (직접) (단순 구조) ChaCha20 (per-CPU) getentropy() arc4random() 처리율: ~800 MB/s getentropy() 최대 256B 확장성: per-CPU 선형 FIPS: 미지원 부팅 블록: 절대 블록 안 함 FreeBSD (14) random_harvest (단일 API) Fortuna 32개 풀 AES-256-CTR (전역) getentropy() /dev/random 처리율: ~400 MB/s AES-NI 하드웨어 가속 확장성: 전역 락 제한 FIPS: 미지원 부팅 블록: getentropy() 대기

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 preemptionx509·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-21FIPS 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)가 필요할 때만 실제 시스템 콜로 내려갑니다.

사용자 공간 vgetrandom() 경로 사용자 스레드 스레드별 opaque state ChaCha20 직접 소비 vDSO 매핑 rng_base 페이지 공유 세대 번호 검사 커널 CRNG 재시드 요청 시에만 진입 ChaCha20 seed 발급 엔트로피 소스 jitter, hwrng, IRQ pool 주입 재시드 시만 핵심 자료 구조 struct vgetrandom_state — 스레드별 opaque 블록. ChaCha20 키, 세대 번호(generation), 캐시된 출력 버퍼. rng_vdso_data — 커널이 vvar 페이지에 두는 CRNG 세대 번호. seqcount_t로 보호. crng_init — 부팅 초기 엔트로피 수집 완료 상태. vDSO는 이 조건이 찰 때까지 커널로 폴백합니다. getrandom(2) — 사용자 공간에서 직접 호출해도 여전히 가능. vDSO는 빠른 경로일 뿐입니다.
/* 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
구현 포인트: vDSO 경로는 페이지 폴트(Page Fault), fork(), pthread 생성에 영향을 줍니다. 자식 프로세스는 새로운 opaque state를 받아야 하고, glibc는 이를 자동으로 갱신합니다. 커스텀 런타임(Runtime)이나 직접 스레드를 생성하는 언어 런타임은 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-64RDRAND, RDSEED기본 제공. random.trust_cpu=on이 기본값이며, Intel은 DRNG 2.0 계열로 전력 관리와 속도를 조정합니다.
AMD Zen 4/5RDRAND, RDSEED + CCP 내부 RNGCCP(보안 프로세서)가 TRNG 기반 엔트로피를 kernel hwrng로 내보냅니다. drivers/crypto/ccp/가 담당.
ARM v8.5+RNDR, RNDRRSCortex-A76 이후 일부 코어와 Neoverse V1/V2/V3가 지원. 펌웨어(Firmware)에서 활성화 여부가 다릅니다.
RISC-VZkr 난수 확장 (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 게스트까지 엔트로피 경로가 통합되어 있습니다.
전용 하드웨어 RNGUSB / 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가 단순한 성능 문제를 넘어 운영 안정성의 문제가 됐습니다.

함정: 일부 컨테이너 이미지는 /dev/urandom을 바인드하거나 /dev/randomurandom으로 심볼릭 링크해 놓습니다. vDSO 경로가 활성화된 커널에서는 이런 우회가 오히려 엔트로피 상태 추적을 막아 PQC 키 생성 품질 보증을 어렵게 만듭니다. 기본 장치를 유지하고 glibc가 알아서 vDSO 경로를 선택하도록 두는 것이 바람직합니다.

외부 참고 자료