Linux 하드웨어 난수 생성기 (hwrng & QRNG)
hwrng 및 QRNG 경로를 커널 엔트로피 품질과 암호학적 안전성 관점에서 심층 분석합니다. hwrng 프레임워크와 드라이버 등록 모델, 하드웨어 난수 소스 신뢰도 평가, 엔트로피 풀 혼합과 credit 정책, 커널 CSPRNG(ChaCha20) 초기화·재시드 동작, 부팅 초기 entropy starvation 대응, 가상화/임베디드 환경의 난수 공급 이슈, rngd와 userspace 연계, 품질 검증과 장애 진단 절차까지 안전한 난수 인프라 구축에 필요한 핵심을 다룹니다.
- 커널 모듈 개발 —
struct hwrng등록/해제 패턴 이해 필요 - Linux Crypto Framework — 암호학적 기초 (CSPRNG, ChaCha20)
- 메모리 관리 — 엔트로피 풀의 메모리 레이아웃 이해
컴퓨터는 본질적으로 결정론적 기계라 "진정한 우연"을 만들지 못합니다. 하드웨어 난수 생성기(hwrng)는 열잡음, 방사선 감쇠, 양자 효과 등 물리 세계의 예측 불가한 현상을 측정해 진짜 우연한 비트를 공급합니다. 커널은 이 원시 엔트로피를 ChaCha20 암호화 알고리즘으로 정제해 빠르고 안전한 난수 스트림(/dev/urandom)을 만듭니다.
핵심 요약
- TRNG vs CSPRNG: 하드웨어 TRNG는 물리적 엔트로피 원천, CSPRNG는 그것을 씨앗 삼아 대량 난수 생성
- /dev/random vs /dev/urandom: 현대 커널(5.18+)에서 두 인터페이스는 동일하게 동작 (CSPRNG 기반)
- getrandom() 시스템 콜: 커널 초기화 완료 전에는 블록, 이후 절대 블록하지 않음
- hwrng 드라이버:
struct hwrng하나만 구현하면 커널 엔트로피 풀에 자동 기여 - quality 파라미터: 하드웨어 품질을 0~1024로 표현 (1024 = 비트당 1비트 엔트로피)
- QRNG 물리 원리 3종: 빔 분리기(편광), 진공 요동(ΔEΔt≥ℏ/2), 광자 도착 시간(TOAD)은 모두 양자 역학적 비결정성 활용
- NIST SP 800-90B: min-entropy 기반 통계 검증으로 hwrng quality 파라미터를 인증, FIPS 140-3 요구사항과 연동
- PCIe vs USB QRNG: PCIe는 MSI 인터럽트+MMIO 직접 읽기로 더 높은 처리율 달성, USB는 Bulk 전송으로 단순 구현
단계별 이해
- 엔트로피 풀 개념 파악 → 엔트로피 풀 메커니즘
- 커널 CSPRNG 내부 구조 이해 → 커널 CSPRNG 내부
- hwrng API 학습 → hwrng 드라이버 개발
- QRNG 물리 원리 심화 → QRNG 물리적 원리 심화
- 실제 QRNG 드라이버 분석 → QRNG 드라이버 구현
- NIST 800-90B 인증 이해 → NIST SP 800-90B 인증
- 암호 키 생성 플로우 → 암호 키 생성 완전 플로우
- 테스트/검증 방법 숙지 → 테스트 및 검증
hwrng 서브시스템 개요
Linux 커널의 hwrng(Hardware Random Number Generator) 서브시스템은
다양한 하드웨어 난수 생성기를 통일된 인터페이스로 추상화합니다.
Intel RDRAND, ARM TRNG, TPM 2.0, USB QRNG 등 모든 하드웨어 RNG는
동일한 struct hwrng API를 통해 커널 엔트로피 풀에 기여합니다.
난수 생성기 분류
| 유형 | 전체 명칭 | 특징 | 예시 |
|---|---|---|---|
| TRNG | True Random Number Generator | 물리적 현상 기반, 느리지만 진정한 무작위성 | 열잡음, 방사선, 광자 분리 |
| QRNG | Quantum Random Number Generator | 양자 역학 기반, 원리적으로 예측 불가 | ID Quantique, QuintessenceLabs |
| PRNG | Pseudo Random Number Generator | 결정론적, 빠름, 씨앗(seed)에 의존 | rand(), Mersenne Twister |
| CSPRNG | Cryptographically Secure PRNG | 역추적 불가, 예측 저항성, 암호화 안전 | Linux ChaCha20-CRNG, /dev/urandom |
| DRBG | Deterministic Random Bit Generator | NIST SP 800-90A 표준, 재현 가능 | CTR-DRBG(AES), Hash-DRBG |
QRNG 방식별 비교
| 방식 | 물리 현상 | 처리율 | 대표 제품 | 양자 원리 |
|---|---|---|---|---|
| 빔 분리기 | 광자 편광 불확정성 | 1~10 Mbps | ID Quantique Quantis | 하이젠베르크 불확정성 원리 |
| 진공 요동 | 진공 상태 전자기장 ΔE | 100+ Mbps | QuintessenceLabs qStream | ΔEΔt ≥ ℏ/2 (에너지-시간 불확정성) |
| 광자 도착 시간 (TOAD) | 광자 방출 시각 양자 요동 | 10~100 Mbps | Comscope, Toshiba QKD | 파동-입자 이중성 |
| 방사성 붕괴 | 핵 붕괴 타이밍 | 낮음 (kbps) | HotBits, Geiger counter | 양자 터널링 |
| 위상 잡음 | 레이저 위상 자연 선 폭 | 1~10 Gbps | 연구 단계 (대규모) | 자연 선 폭 불확정성 |
커널 내 hwrng 위치 — 아키텍처 다이어그램
QRNG 물리적 원리 심화
QRNG는 양자 역학의 고유한 비결정성(inherent randomness)을 활용합니다. 고전 물리계와 달리, 양자 시스템의 측정 결과는 원리적으로 예측 불가능하며 이 특성이 진정한 엔트로피 원천이 됩니다. 대표적인 세 가지 방식을 심층 분석합니다.
방식 1: 광자 빔 분리기 (Beam Splitter, BS50:50)
단일 광자를 50:50 빔 분리기(반투명 거울)에 통과시키면, 광자는 양자 중첩 상태로 투과와 반사 두 경로를 동시에 진행합니다. 검출기가 측정하는 순간 파동함수가 붕괴되어 0 또는 1이 결정됩니다. 이 결정은 하이젠베르크 불확정성 원리에 의해 원천적으로 예측 불가능합니다.
단일 광자 소스 → [50:50 빔 분리기] → 검출기 A (반사) → 0비트
↘ 검출기 B (투과) → 1비트
양자 중첩: |광자⟩ = (1/√2)|반사⟩ + (1/√2)|투과⟩
측정 붕괴: P(0) = |1/√2|² = 0.5, P(1) = |1/√2|² = 0.5
min-entropy: H_min = -log₂(0.5) = 1.0 비트/비트 ← 이론적 최대
방식 2: 진공 요동 (Vacuum Fluctuation)
진공 상태는 빈 공간이 아닙니다. 하이젠베르크 에너지-시간 불확정성 원리(ΔEΔt ≥ ℏ/2)에 의해 진공에서도 전자기장이 끊임없이 요동칩니다. 이 진공 요동을 ADC(아날로그-디지털 변환기)로 측정하면 양자 무작위 비트를 추출할 수 있습니다. 처리율이 높아(100+ Mbps) 상용 QRNG에 많이 사용됩니다.
/* 진공 요동 QRNG 원리 — ADC 기반 비트 추출 */
/* 호모다인 검출(Homodyne Detection): 진공 상태 X 직교 성분 측정 */
/* 1. 레이저 로컬 오실레이터와 진공 입력 포트 간섭 */
/* 2. 밸런스드 포토다이오드로 차동 광전류 측정 */
/* 3. ADC 샘플링 → 가우시안 분포 데이터 획득 */
/* 4. 추출기(Extractor)로 균등 분포 비트 변환 */
struct vacuum_qrng_sample {
int16_t adc_raw; /* 진공 요동 ADC 값: 가우시안 분포 */
uint8_t lsb_bits; /* LSB 추출: 가우시안 → 균등 분포 근사 */
};
/* 토플리츠 해시 추출기 (Toeplitz Hash Extractor) */
static int vacuum_extract_bits(const int16_t *adc_samples, size_t n,
uint8_t *output, size_t out_bytes)
{
/* 토플리츠 행렬 곱으로 입력 편향 제거 */
/* min-entropy 추정에 따라 압축 비율 결정 */
/* NIST SP 800-90B 3.1.3절의 추출기 요구사항 준수 */
return toeplitz_hash(adc_samples, n, output, out_bytes);
}
방식 3: 광자 도착 시간 (TOAD — Time Of Arrival Detection)
레이저 다이오드가 방출하는 광자들의 도착 시각은 양자 역학적 파동-입자 이중성으로 인해 원천적으로 불확정적입니다. 각 광자 도착 이벤트의 타임스탬프에서 LSB(최하위 비트)만 추출하면 빠르고 품질 높은 양자 난수를 얻을 수 있습니다.
/* TOAD 방식 hwrng read 콜백 예제 */
/* 1. SPAD(Single Photon Avalanche Diode)로 광자 도착 시각 측정 */
/* 2. TDC(Time-to-Digital Converter)로 ps 정밀도 타임스탬프 획득 */
/* 3. 연속 타임스탬프 간 차이(Δt) 계산 */
/* 4. Δt의 LSB 비트 추출 → 균등 분포 난수 */
static int toad_read(struct hwrng *rng, void *data, size_t max, bool wait)
{
struct toad_dev *dev = container_of(rng, struct toad_dev, hwrng);
uint64_t t1, t2, delta;
size_t bytes = 0;
uint8_t *out = data;
while (bytes < max) {
t1 = readq(dev->tdc_base + TDC_TIMESTAMP_REG); /* 광자 n 도착 시각 */
t2 = readq(dev->tdc_base + TDC_TIMESTAMP_REG); /* 광자 n+1 도착 시각 */
delta = t2 - t1; /* 광자 간격 Δt */
/* LSB 8비트 추출 — 가장 빠르게 변하는 비트 */
*out++ = (uint8_t)(delta & 0xFF);
bytes++;
}
return bytes;
}
엔트로피 풀 메커니즘
커널 엔트로피 풀은 drivers/char/random.c에 구현된 핵심 난수 인프라입니다.
Linux 5.18 이후 단일 input_pool 구조로 단순화되었습니다.
/dev/random vs /dev/urandom — 현대 커널의 통합
/dev/random과 /dev/urandom은 이제 동일한 CSPRNG 백엔드를 사용합니다.
/dev/random은 더 이상 엔트로피 고갈로 블록하지 않습니다.
블록킹 동작은 초기화 완료 전에만 발생합니다.
| 인터페이스 | 블록킹 조건 | 권장 용도 | 커널 구현 |
|---|---|---|---|
/dev/random |
CRNG 초기화 전 | 레거시 호환성 | random_read() |
/dev/urandom |
절대 블록 안 함 | 일반 용도 | urandom_read() |
getrandom() |
CRNG 초기화 전 (GRND_NONBLOCK 아닐 때) | 권장 — 파일 디스크립터 불필요 | sys_getrandom() |
get_random_bytes() |
없음 (커널 내부용) | 커널 드라이버 | lib/random.c |
엔트로피 소스와 수집 경로
/* drivers/char/random.c — 엔트로피 수집 함수들 */
/* 1. 인터럽트 지터 — 가장 풍부한 소프트웨어 엔트로피 소스 */
void add_interrupt_randomness(int irq)
{
struct fast_pool *fast_pool = this_cpu_ptr(&irq_randomness);
struct pt_regs *regs = get_irq_regs();
unsigned long now = jiffies;
cycles_t cycles = random_get_entropy(); /* TSC or 아키텍처별 타이머 */
fast_mix(fast_pool->pool, irq, now ^ cycles,
regs ? instruction_pointer(regs) : _RET_IP_);
/* 64회 인터럽트마다 input_pool에 기여 */
if (++fast_pool->count < 64 && !time_after(now, fast_pool->last + HZ))
return;
add_interrupt_bench(cycles);
mix_interrupt_randomness(fast_pool);
}
/* 2. hwrng 기여 — hw_random 코어가 자동으로 호출 */
void add_hwgenerator_randomness(const void *buf, size_t len, size_t entropy)
{
_mix_pool_bytes(buf, len); /* input_pool에 혼합 */
credit_init_bits(entropy); /* 엔트로피 추정값 증가 */
wake_up_interruptible(&random_wait); /* 대기 중인 getrandom() 깨움 */
}
/* 3. 시스템 이벤트 기여 */
void add_device_randomness(const void *buf, size_t len) /* MAC 주소, 시리얼 번호 등 */
void add_input_randomness(unsigned int type, unsigned int code, unsigned int val) /* 키보드/마우스 */
void add_disk_randomness(struct gendisk *disk) /* 디스크 I/O 완료 타이밍 */
코드 설명
-
cycles_t cycles
random_get_entropy()는 x86에서 TSC(Time Stamp Counter), ARM에서 CNTVCT를 읽습니다. 인터럽트 도착 시각의 나노초 단위 지터가 실제 엔트로피가 됩니다. - fast_pool per-CPU 고속 풀입니다. 모든 인터럽트마다 input_pool에 직접 쓰면 락 경합이 심해지므로, 64회마다 한 번씩 배치 처리합니다.
-
credit_init_bits(entropy)
엔트로피 추정값을 비트 단위로 누적합니다. CRNG 초기화에는 256비트 이상이 필요합니다. hwrng의
quality값이 이 계산에 사용됩니다.
getrandom() 시스템 콜 흐름
/* 사용자 공간에서 사용 */
#include <sys/random.h>
unsigned char key[32];
ssize_t ret = getrandom(key, sizeof(key), 0); /* 0 = 블록킹 모드 */
if (ret < 0) {
perror("getrandom"); /* EINTR, EFAULT 가능 */
return -1;
}
/* GRND_NONBLOCK: CRNG 초기화 전에도 블록하지 않음 (EAGAIN 반환) */
ret = getrandom(key, sizeof(key), GRND_NONBLOCK);
/* GRND_RANDOM: /dev/random 동작 에뮬레이션 (레거시, 권장 안 함) */
ret = getrandom(key, sizeof(key), GRND_RANDOM);
/* 커널 내부: get_random_bytes() 계열 */
get_random_bytes(buf, nbytes); /* 범용 */
u32 r = get_random_u32(); /* 32비트 */
u64 r = get_random_u64(); /* 64비트 */
u32 r = get_random_u32_below(100); /* 0~99, 편향 없음 */
u32 r = get_random_u32_inclusive(1, 6); /* 주사위 1~6 */
엔트로피 예산 계산 — quality 파라미터의 역할
hwrng 코어는 hwrng_fillfn() 커널 스레드를 통해 지속적으로 rng->read()를 호출하고,
읽은 바이트 수와 quality 값을 곱해 엔트로피 기여량을 계산합니다.
/* drivers/char/hw_random/core.c — hwrng_fillfn() 핵심 로직 */
static int hwrng_fillfn(void *unused)
{
long rc;
while (!kthread_should_stop()) {
struct hwrng *rng;
mutex_lock(&rng_mutex);
rng = get_current_rng_nolock();
mutex_unlock(&rng_mutex);
if (!rng) { msleep(1000); continue; }
/* read() 호출 — 최대 RNGD_BUFF_SIZE(4096)바이트 */
rc = rng->read(rng, rng_buffer, rng_buffer_size, true);
if (rc <= 0) { msleep(500); continue; }
/* 엔트로피 기여량 계산:
* entropy_bits = bytes_read × quality × 8 / 1024
* 예) 4096바이트 × quality=1024 × 8 / 1024 = 32768비트
* 예) 4096바이트 × quality=512 × 8 / 1024 = 16384비트
* 예) 4096바이트 × quality=0 × 8 / 1024 = 0비트 (기여 없음) */
add_hwgenerator_randomness(rng_buffer, rc,
(rc * rng->quality * 8) >> 10);
}
hwrng_put(rng);
return 0;
}
| quality 값 | 의미 | 4096바이트 읽기 시 기여 엔트로피 | CRNG 초기화(256비트) 위한 읽기 횟수 |
|---|---|---|---|
| 1024 | 이론적 최대 (검증 QRNG) | 32,768 비트 | 1회 미만 |
| 512 | 50% 효율 (Intel RDRAND) | 16,384 비트 | 1회 미만 |
| 256 | 25% 효율 (열잡음 TRNG) | 8,192 비트 | 1회 미만 |
| 128 | 12.5% 효율 (FPGA 지터) | 4,096 비트 | 1회 미만 |
| 0 | 엔트로피 기여 없음 | 0 비트 | ∞ (초기화 불가) |
커널 CSPRNG 내부
Linux 커널은 ChaCha20 기반 CRNG(Cryptographically secure Random Number Generator)를 사용합니다.
커밋 1e7f583903b(v5.17)부터 per-CPU ChaCha20 CRNG로 완전히 전환되었습니다.
ChaCha20-CRNG 상태 구조
/* include/linux/random.h, drivers/char/random.c */
/* per-CPU CRNG 상태 — 64바이트 (ChaCha20 키 크기) */
struct crng {
u8 key[CHACHA20_KEY_SIZE]; /* 32바이트 키 */
unsigned long generation; /* 리시드 세대 번호 */
local_lock_t lock; /* per-CPU 경량 락 */
};
/* 전역 초기화 완료 플래그 */
static bool crng_ready = false; /* 256비트 엔트로피 수집 후 true */
static u64 base_crng_generation; /* 전역 세대 카운터 */
/* CRNG 초기화 — 부팅 시 엔트로피 수집 */
static void crng_initialize_primary(void)
{
extract_entropy(base_crng.key, sizeof(base_crng.key));
crng_ready = true;
pr_notice("random: crng init done\n");
}
crng_reseed() — 주기적 갱신 메커니즘
/* 5분마다 또는 충분한 엔트로피 누적 시 호출 */
static void crng_reseed(struct crng *crng)
{
u8 key[CHACHA20_KEY_SIZE];
/* input_pool에서 32바이트 추출 (Blake2b 해시 압축) */
extract_entropy(key, sizeof(key));
/* 기존 키와 새 키를 XOR하여 forward secrecy 보장 */
memcpy(&crng->key, key, sizeof(key));
WRITE_ONCE(crng->generation, base_crng_generation);
memzero_explicit(key, sizeof(key)); /* 스택 잔류 키 소거 */
}
/* 난수 생성 — ChaCha20 스트림으로 64바이트씩 출력 */
static void crng_fast_key_erasure(u8 *key, struct chacha20_ctx *chacha_state,
u8 *random_data, size_t random_data_len)
{
/* ChaCha20 블록으로 난수 스트림 생성 */
chacha20_block(chacha_state, random_data);
/* Fast Key Erasure: 출력의 앞 32바이트를 새 키로 사용 */
/* → 이전 출력으로 키를 역추적 불가 (forward secrecy) */
memcpy(key, random_data, CHACHA20_KEY_SIZE);
memzero_explicit(random_data, CHACHA20_KEY_SIZE); /* 키 부분 소거 */
}
코드 설명
- extract_entropy() input_pool의 Blake2b 해시 상태를 압축해 32바이트 키를 추출합니다. 이 과정은 풀 상태를 변경(absorb)하므로 같은 키가 두 번 출력되지 않습니다.
- Fast Key Erasure Daniel Bernstein이 설계한 기법입니다. ChaCha20 블록 출력의 첫 32바이트를 즉시 새 키로 교체합니다. 메모리가 탈취되어도 이전 출력을 재현할 수 없습니다.
-
memzero_explicit()
컴파일러 최적화로 소거 코드가 제거되지 않도록 강제합니다. 일반
memset()은 최적화 옵션에 따라 제거될 수 있습니다.
input_pool — Blake2b 압축 함수
/* input_pool: Blake2b 해시 상태 기반 엔트로피 수집 */
struct blake2s_state {
u32 h[8]; /* 256비트 해시 상태 */
u32 t[2]; /* 처리된 바이트 카운터 */
u32 f[2]; /* 최종화 플래그 */
u8 buf[BLAKE2S_BLOCK_SIZE]; /* 입력 버퍼 (64바이트) */
size_t buflen;
u8 outlen;
};
/* 엔트로피 혼합 — Blake2s 압축으로 되돌릴 수 없음 */
static void _mix_pool_bytes(const void *buf, size_t len)
{
blake2s_update(&input_pool, buf, len); /* 단방향 해시 업데이트 */
}
/* 엔트로피 추출 — 풀 상태를 변경하며 키 생성 */
static void extract_entropy(void *buf, size_t nbytes)
{
struct blake2s_state recovery = input_pool; /* 풀 스냅샷 */
blake2s_final(&recovery, buf); /* 최종 해시 출력 */
blake2s_update(&input_pool, buf, nbytes); /* 출력을 풀에 재혼합 */
}
hwrng 드라이버 개발
hwrng 서브시스템의 드라이버 API는 단순합니다.
struct hwrng 구조체를 초기화하고 hwrng_register()를 호출하면 됩니다.
struct hwrng — 핵심 API
/* include/linux/hw_random.h */
struct hwrng {
const char *name; /* 드라이버 이름 (/sys/class/misc/hw_random/rng_current) */
int (*init)( struct hwrng *rng); /* 선택사항: 초기화 */
void (*cleanup)(struct hwrng *rng); /* 선택사항: 정리 */
int (*data_present)(struct hwrng *rng, int wait); /* 데이터 준비 여부 */
int (*data_read)( struct hwrng *rng, u32 *data); /* 구형 API, 4바이트 */
int (*read)( struct hwrng *rng, void *data, /* 권장 API */
size_t max, bool wait);
unsigned long priv; /* 드라이버 사용 개인 데이터 (구형) */
unsigned short quality; /* 엔트로피 품질: 0(최저)~1024(최고) */
/* 1024 = 비트당 1.0비트 엔트로피 (물리적 최대) */
/* 내부 사용 — 드라이버가 초기화 금지 */
struct list_head list;
struct kref ref;
struct completion cleanup_done;
struct completion dying;
};
quality 파라미터 가이드
| quality 값 | 엔트로피 품질 | 해당 하드웨어 |
|---|---|---|
| 1024 | 1.0 비트/비트 (이론적 최대) | 검증된 QRNG, 방사선 기반 |
| 512~1023 | 0.5~1.0 비트/비트 | Intel RDRAND, ARM TRNG, TPM RNG |
| 256~511 | 0.25~0.5 비트/비트 | 열잡음 기반 아날로그 TRNG |
| 128~255 | 보통 품질 | FPGA 링 오실레이터 |
| 0~127 | 낮은 품질 | 지터 기반, 환경 의존적 |
| 0 | 엔트로피 기여 없음 | 테스트용 또는 PRNG |
최소 hwrng 드라이버 골격
#include <linux/module.h>
#include <linux/hw_random.h>
#include <linux/platform_device.h>
struct myrng_priv {
void __iomem *base; /* MMIO 기주소 */
struct clk *clk;
};
/* 필수: 난수 읽기 콜백 */
static int myrng_read(struct hwrng *rng, void *data, size_t max, bool wait)
{
struct myrng_priv *priv = (struct myrng_priv *)dev_get_drvdata(rng->dev);
size_t read = 0;
/* HW가 준비될 때까지 폴링 (실제는 인터럽트 권장) */
while (read + 4 <= max) {
/* 레지스터에서 FIFO 가득 찼는지 확인 */
if (!(readl(priv->base + MYRNG_STATUS_REG) & MYRNG_READY_BIT)) {
if (!wait)
break;
cpu_relax();
continue;
}
/* 32비트 난수 읽기 */
*(u32 *)((u8 *)data + read) = readl(priv->base + MYRNG_DATA_REG);
read += 4;
}
return read; /* 실제 읽은 바이트 수 반환 */
}
static int myrng_probe(struct platform_device *pdev)
{
struct myrng_priv *priv;
struct hwrng *rng;
struct resource *res;
int ret;
priv = devm_kzalloc(&pdev->dev, sizeof(*priv), GFP_KERNEL);
if (!priv)
return -ENOMEM;
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
priv->base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(priv->base))
return PTR_ERR(priv->base);
rng = devm_kzalloc(&pdev->dev, sizeof(*rng), GFP_KERNEL);
if (!rng)
return -ENOMEM;
rng->name = "myrng";
rng->read = myrng_read;
rng->quality = 900; /* 하드웨어 스펙에 따라 조정 */
platform_set_drvdata(pdev, priv);
ret = devm_hwrng_register(&pdev->dev, rng); /* devm 버전: 해제 자동화 */
if (ret)
return dev_err_probe(&pdev->dev, ret, "hwrng_register 실패\n");
dev_info(&pdev->dev, "myrng 등록 완료 (quality=%u)\n", rng->quality);
return 0;
}
static const struct of_device_id myrng_of_match[] = {
{ .compatible = "myvendor,myrng" },
{}
};
MODULE_DEVICE_TABLE(of, myrng_of_match);
static struct platform_driver myrng_driver = {
.probe = myrng_probe,
.driver = {
.name = "myrng",
.of_match_table = myrng_of_match,
},
};
module_platform_driver(myrng_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Example hwrng driver");
코드 설명
- myrng_read 반환값 실제 읽은 바이트 수를 반환해야 합니다. 0 이상이면 성공, 음수면 오류입니다. hw_random 코어는 이 값으로 엔트로피 기여량을 계산합니다.
-
devm_hwrng_register()
디바이스 관리 버전으로, 드라이버 언로드 시 자동으로
hwrng_unregister()를 호출합니다. 수동hwrng_unregister()와 달리remove()콜백에 별도 코드가 필요 없습니다. - quality = 900 88% 효율의 엔트로피 품질을 의미합니다. 하드웨어 데이터시트의 min-entropy 스펙(예: 0.875 비트/비트)을 ×1024로 환산합니다.
QRNG 드라이버 구현
QRNG(Quantum Random Number Generator)는 양자 역학적 현상(광자 방향, 진공 요동 등)을 이용해 원리적으로 예측 불가능한 비트를 생성합니다. 대부분의 상용 QRNG는 USB 또는 PCIe 인터페이스로 연결되어 hwrng 드라이버로 구현됩니다.
USB QRNG 드라이버 완전 예제
아래 코드는 USB HID 기반 QRNG 장치 (예: ID Quantique Quantis USB)의 드라이버 구조를 보여줍니다.
#include <linux/module.h>
#include <linux/usb.h>
#include <linux/hw_random.h>
#include <linux/slab.h>
#include <linux/completion.h>
#define QRNG_VENDOR_ID 0x1d50 /* 가상의 벤더 ID */
#define QRNG_PRODUCT_ID 0x6021
#define QRNG_BULK_IN_EP 0x81 /* Bulk IN 엔드포인트 */
#define QRNG_BUFFER_SIZE 4096 /* 한 번에 읽는 양자 난수 버퍼 크기 */
#define QRNG_QUALITY 1024 /* 양자 소스 — 이론적 최대 엔트로피 */
struct qrng_dev {
struct usb_device *udev;
struct usb_interface *interface;
struct hwrng hwrng;
/* 비동기 Bulk 전송용 */
struct urb *urb;
u8 *buf; /* DMA 가능 버퍼 */
size_t buf_len; /* 사용 가능한 바이트 수 */
size_t buf_off; /* 현재 읽기 오프셋 */
struct mutex lock;
struct completion urb_done;
bool disconnecting;
};
/* URB 완료 콜백 — 인터럽트 컨텍스트에서 호출됨 */
static void qrng_urb_complete(struct urb *urb)
{
struct qrng_dev *qdev = urb->context;
if (urb->status == 0) {
qdev->buf_len = urb->actual_length;
qdev->buf_off = 0;
} else if (urb->status == -ENOENT || urb->status == -ECONNRESET ||
urb->status == -ESHUTDOWN) {
qdev->buf_len = 0; /* 연결 해제 시 정상 종료 */
}
complete(&qdev->urb_done);
}
/* hwrng read 콜백 — 슬립 가능 컨텍스트 */
static int qrng_read(struct hwrng *rng, void *data, size_t max, bool wait)
{
struct qrng_dev *qdev = container_of(rng, struct qrng_dev, hwrng);
size_t available, to_copy;
int ret;
mutex_lock(&qdev->lock);
/* 버퍼에 데이터가 없으면 USB 전송 시작 */
if (qdev->buf_off >= qdev->buf_len) {
if (!wait) {
mutex_unlock(&qdev->lock);
return 0;
}
reinit_completion(&qdev->urb_done);
usb_fill_bulk_urb(qdev->urb, qdev->udev,
usb_rcvbulkpipe(qdev->udev, QRNG_BULK_IN_EP),
qdev->buf, QRNG_BUFFER_SIZE,
qrng_urb_complete, qdev);
qdev->urb->transfer_flags |= URB_NO_TRANSFER_DMA_MAP;
ret = usb_submit_urb(qdev->urb, GFP_KERNEL);
if (ret) {
mutex_unlock(&qdev->lock);
return ret;
}
mutex_unlock(&qdev->lock);
wait_for_completion(&qdev->urb_done); /* USB 전송 완료 대기 */
mutex_lock(&qdev->lock);
if (qdev->buf_len == 0) {
mutex_unlock(&qdev->lock);
return -EIO;
}
}
/* 버퍼에서 요청량만큼 복사 */
available = qdev->buf_len - qdev->buf_off;
to_copy = min(available, max);
memcpy(data, qdev->buf + qdev->buf_off, to_copy);
qdev->buf_off += to_copy;
mutex_unlock(&qdev->lock);
return to_copy;
}
static int qrng_probe(struct usb_interface *interface,
const struct usb_device_id *id)
{
struct qrng_dev *qdev;
int ret;
qdev = kzalloc(sizeof(*qdev), GFP_KERNEL);
if (!qdev)
return -ENOMEM;
qdev->udev = interface_to_usbdev(interface);
qdev->interface = interface;
mutex_init(&qdev->lock);
init_completion(&qdev->urb_done);
/* DMA 가능 버퍼 할당 */
qdev->buf = usb_alloc_coherent(qdev->udev, QRNG_BUFFER_SIZE,
GFP_KERNEL, &qdev->urb->transfer_dma);
if (!qdev->buf) {
ret = -ENOMEM;
goto err_free;
}
qdev->urb = usb_alloc_urb(0, GFP_KERNEL);
if (!qdev->urb) {
ret = -ENOMEM;
goto err_free_buf;
}
/* hwrng 등록 */
qdev->hwrng.name = "qrng-quantum";
qdev->hwrng.read = qrng_read;
qdev->hwrng.quality = QRNG_QUALITY; /* 1024 — 양자 소스 */
usb_set_intfdata(interface, qdev);
ret = hwrng_register(&qdev->hwrng);
if (ret) {
dev_err(&interface->dev, "hwrng 등록 실패: %d\n", ret);
goto err_free_urb;
}
dev_info(&interface->dev, "QRNG 연결됨 (quality=1024)\n");
return 0;
err_free_urb:
usb_free_urb(qdev->urb);
err_free_buf:
usb_free_coherent(qdev->udev, QRNG_BUFFER_SIZE, qdev->buf,
qdev->urb->transfer_dma);
err_free:
kfree(qdev);
return ret;
}
static void qrng_disconnect(struct usb_interface *interface)
{
struct qrng_dev *qdev = usb_get_intfdata(interface);
qdev->disconnecting = true;
hwrng_unregister(&qdev->hwrng); /* read() 완료 후 반환 보장 */
usb_kill_urb(qdev->urb); /* 진행 중인 URB 취소 */
usb_free_urb(qdev->urb);
usb_free_coherent(qdev->udev, QRNG_BUFFER_SIZE, qdev->buf,
qdev->urb->transfer_dma);
kfree(qdev);
dev_info(&interface->dev, "QRNG 분리됨\n");
}
static const struct usb_device_id qrng_id_table[] = {
{ USB_DEVICE(QRNG_VENDOR_ID, QRNG_PRODUCT_ID) },
{}
};
MODULE_DEVICE_TABLE(usb, qrng_id_table);
static struct usb_driver qrng_driver = {
.name = "qrng",
.probe = qrng_probe,
.disconnect = qrng_disconnect,
.id_table = qrng_id_table,
};
module_usb_driver(qrng_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("USB QRNG hwrng driver");
MODULE_AUTHOR("Example Author");
코드 설명
- QRNG_QUALITY = 1024 양자 소스는 원리적으로 예측 불가능하므로 최대 품질 1024를 사용합니다. 실제 하드웨어는 제조사 인증서(NIST SP 800-90B)를 확인한 후 설정하세요.
- usb_fill_bulk_urb() USB Bulk 전송 요청을 초기화합니다. QRNG는 대량 바이트를 전송하므로 Bulk 엔드포인트가 적합합니다. 인터럽트 엔드포인트는 소량(64B 이하)에만 적합합니다.
-
hwrng_unregister() 순서
hwrng_unregister()는 진행 중인read()콜백이 완료될 때까지 블록합니다. 그 후usb_kill_urb()를 호출해야 URB 사용 후 해제 버그를 피할 수 있습니다. -
container_of()
struct hwrng가struct qrng_dev에 내장되어 있으므로,container_of()로 부모 구조체 포인터를 얻습니다. 별도 할당보다 메모리 효율적입니다.
알려진 상용 QRNG 드라이버 위치
| 벤더/제품 | 커널 소스 위치 | 인터페이스 | quality |
|---|---|---|---|
| BCM2835 (라즈베리파이) | drivers/char/hw_random/bcm2835-rng.c | MMIO | 200 |
| Intel RDRAND | arch/x86/kernel/cpu/rdrand.c | CPU 명령어 | 1024 |
| ARM TRNG (v8.5+) | drivers/char/hw_random/arm-smccc-trng.c | SMC 콜 | 1024 |
| TPM 2.0 RNG | drivers/char/tpm/tpm-chip.c | TPM 커맨드 | 질문에 따라 |
| VirtIO RNG (KVM 게스트) | drivers/char/hw_random/virtio-rng.c | virtqueue | 1024 |
| EXYNOS (삼성) | drivers/char/hw_random/exynos-trng.c | MMIO + 인터럽트 | 없음(기본) |
PCIe QRNG 드라이버
PCIe 기반 QRNG는 USB보다 높은 처리율과 낮은 지연을 제공합니다. 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_QRNG_VENDOR 0x1234
#define PCIE_QRNG_DEVICE 0xABCD
#define QRNG_REG_STATUS 0x00 /* 상태 레지스터 */
#define QRNG_REG_DATA 0x04 /* 난수 데이터 레지스터 (32비트) */
#define QRNG_REG_FIFO_COUNT 0x08 /* FIFO 내 사용 가능 워드 수 */
#define QRNG_STATUS_READY BIT(0) /* FIFO에 데이터 있음 */
#define QRNG_QUALITY 1024
struct pcie_qrng_dev {
void __iomem *mmio_base; /* BAR0 MMIO 기주소 */
struct hwrng hwrng; /* hwrng 서브시스템 핸들 */
struct pci_dev *pdev;
wait_queue_head_t wait_q; /* 인터럽트 대기 큐 */
atomic_t data_ready; /* MSI 수신 플래그 */
};
/* MSI 인터럽트 핸들러 — 하드웨어가 FIFO에 데이터 채웠을 때 호출 */
static irqreturn_t pcie_qrng_irq(int irq, void *dev_id)
{
struct pcie_qrng_dev *qdev = dev_id;
u32 status = readl(qdev->mmio_base + QRNG_REG_STATUS);
if (!(status & QRNG_STATUS_READY))
return IRQ_NONE;
atomic_set(&qdev->data_ready, 1);
wake_up_interruptible(&qdev->wait_q); /* read()에서 대기 중인 스레드 깨움 */
return IRQ_HANDLED;
}
/* hwrng read 콜백 — MMIO BAR 직접 읽기 + MSI 인터럽트 대기 */
static int pcie_qrng_read(struct hwrng *rng, void *data, size_t max, bool wait)
{
struct pcie_qrng_dev *qdev = container_of(rng, struct pcie_qrng_dev, hwrng);
size_t bytes = 0;
u32 *out = data;
int ret;
while (bytes + sizeof(u32) <= max) {
/* FIFO에 데이터가 없으면 MSI 인터럽트 대기 */
if (!atomic_read(&qdev->data_ready)) {
if (!wait)
break;
ret = wait_event_interruptible(qdev->wait_q,
atomic_read(&qdev->data_ready));
if (ret)
return bytes ? (int)bytes : -EINTR;
}
/* FIFO에서 32비트 읽기 */
*out++ = readl(qdev->mmio_base + QRNG_REG_DATA);
bytes += sizeof(u32);
if (readl(qdev->mmio_base + QRNG_REG_FIFO_COUNT) == 0)
atomic_set(&qdev->data_ready, 0);
}
return (int)bytes;
}
static int pcie_qrng_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
struct pcie_qrng_dev *qdev;
int ret;
qdev = devm_kzalloc(&pdev->dev, sizeof(*qdev), GFP_KERNEL);
if (!qdev)
return -ENOMEM;
init_waitqueue_head(&qdev->wait_q);
atomic_set(&qdev->data_ready, 0);
qdev->pdev = pdev;
/* PCIe 활성화 + DMA 마스크 설정 */
ret = pcim_enable_device(pdev);
if (ret)
return dev_err_probe(&pdev->dev, ret, "PCIe 활성화 실패\n");
ret = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
if (ret)
return dev_err_probe(&pdev->dev, ret, "DMA 마스크 설정 실패\n");
/* BAR0 MMIO 매핑 (devm: 언로드 시 자동 해제) */
qdev->mmio_base = pcim_iomap(pdev, 0, 0);
if (IS_ERR(qdev->mmio_base))
return PTR_ERR(qdev->mmio_base);
/* MSI 인터럽트 1개 할당 */
ret = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI);
if (ret < 0)
return dev_err_probe(&pdev->dev, ret, "MSI 할당 실패\n");
ret = devm_request_irq(&pdev->dev, pci_irq_vector(pdev, 0),
pcie_qrng_irq, 0, "pcie-qrng", qdev);
if (ret)
return dev_err_probe(&pdev->dev, ret, "IRQ 등록 실패\n");
/* hwrng 등록 */
qdev->hwrng.name = "pcie-qrng";
qdev->hwrng.read = pcie_qrng_read;
qdev->hwrng.quality = QRNG_QUALITY;
pci_set_drvdata(pdev, qdev);
ret = devm_hwrng_register(&pdev->dev, &qdev->hwrng);
if (ret)
return dev_err_probe(&pdev->dev, ret, "hwrng 등록 실패\n");
dev_info(&pdev->dev, "PCIe QRNG 등록 완료 (quality=%u)\n", QRNG_QUALITY);
return 0;
}
static const struct pci_device_id pcie_qrng_ids[] = {
{ PCI_DEVICE(PCIE_QRNG_VENDOR, PCIE_QRNG_DEVICE) },
{}
};
MODULE_DEVICE_TABLE(pci, pcie_qrng_ids);
static struct pci_driver pcie_qrng_driver = {
.name = "pcie-qrng",
.id_table = pcie_qrng_ids,
.probe = pcie_qrng_probe,
};
module_pci_driver(pcie_qrng_driver);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("PCIe QRNG hwrng driver with MSI interrupt");
코드 설명
-
pcim_enable_device()
devm 기반 PCIe 초기화 함수입니다. 드라이버 언로드 시 자동으로
pci_disable_device()를 호출합니다.pci_enable_device()+pci_set_master()를 자동 포함합니다. - pcim_iomap(pdev, 0, 0) BAR0을 전체 크기(0)로 MMIO 매핑합니다. devm 기반이라 언로드 시 자동 해제됩니다. 두 번째 인수 0은 BAR 인덱스, 세 번째 인수 0은 전체 크기를 의미합니다.
- pci_alloc_irq_vectors(MSI) 레거시 INTx 대신 MSI를 요청합니다. MSI는 공유 인터럽트 선 없이 메시지 방식으로 동작해 지연이 낮고 공유 IRQ 문제가 없습니다.
- wait_event_interruptible() 신호(SIGINT 등)에 의해 인터럽트될 수 있는 대기입니다. hwrng 코어 스레드가 이 콜백을 호출하므로 -EINTR 처리가 필요합니다.
NIST SP 800-90B 인증
NIST SP 800-90B는 하드웨어 엔트로피 소스(TRNG/QRNG)의 통계적 품질을 검증하는 미국 표준입니다.
커널 hwrng의 quality 파라미터는 이 표준의 min-entropy 측정값에서 직접 유도됩니다.
FIPS 140-3 인증을 위한 필수 요건이기도 합니다.
min-entropy와 quality 파라미터 연관성
/* NIST SP 800-90B — min-entropy H_min 계산 및 quality 변환 */
/* 1단계: 가장 자주 나타나는 심볼의 확률 p_max 측정 */
/* 테스트 데이터 1,000,000비트에서 각 바이트값 출현 빈도 집계 */
double p_max = max_count / total_samples; /* 이상적 QRNG: p_max ≈ 1/256 */
/* 2단계: min-entropy 계산 (비트 단위) */
double H_min = -log2(p_max); /* 이상적: -log2(1/256) = 8.0 비트/바이트 */
/* 3단계: 비트당 min-entropy */
double H_min_per_bit = H_min / 8.0; /* 이상적: 1.0 비트/비트 */
/* 4단계: hwrng quality 값으로 변환 (0~1024 스케일) */
unsigned short quality = (unsigned short)(H_min_per_bit * 1024);
/* 예시:
* H_min_per_bit = 1.000 → quality = 1024 (검증된 QRNG, 빔 분리기)
* H_min_per_bit = 0.990 → quality = 1014 (실용적 QRNG, 진공 요동)
* H_min_per_bit = 0.875 → quality = 896 (Intel RDRAND 실측치)
* H_min_per_bit = 0.500 → quality = 512 (FPGA 링 오실레이터)
* H_min_per_bit = 0.000 → quality = 0 (PRNG, 엔트로피 없음) */
NIST SP 800-90B 검증 6단계
| 단계 | 검증 항목 | 도구/방법 | 판정 기준 |
|---|---|---|---|
| 1. 데이터 수집 | 1,000,000비트 원시 샘플 수집 | dd if=/dev/hwrng bs=1M count=1 |
데이터 손실 없음 |
| 2. IID 검증 | 독립-동일 분포(IID) 여부 확인 | ea_iid (NIST 공식 도구) |
IID 가설 기각 불가 시 IID 경로 |
| 3. min-entropy 추정 | 10가지 추정기로 최소값 결정 | ea_non_iid |
H_min > 목표값 (예: 0.9 비트/비트) |
| 4. 압축 검증 | Markov/LZ 압축 테스트 | 내장 압축기 테스트 | 예측 가능 패턴 없음 |
| 5. 적응형 비율 검증 | 하드웨어 고장 감지 임계값 설정 | Repetition Count, Adaptive Proportion | 오경보율 α < 2⁻²⁰ |
| 6. 보고서 제출 | 독립 실험실 검증 및 CAVP 등록 | NIST CAVP (Cryptographic Algorithm Validation Program) | 인증서 발급 |
FIPS 140-3 요구사항 연관
| FIPS 140-3 요구사항 | hwrng 구현 | 관련 커널 코드 |
|---|---|---|
| 연속 RNG 테스트 (CRNGT) | 연속 동일 출력 감지 | hwrng_fillfn() 내 반복 감지 |
| 시작 업 테스트 (SURT) | 초기화 시 통계 검증 | hwrng_register() 호출 시 |
| min-entropy ≥ 목표값 | quality 파라미터 설정 |
struct hwrng.quality |
| 조건부 예외 테스트 | 오류 시 드라이버 비활성화 | hwrng_unregister() |
암호 키 생성 완전 플로우
QRNG에서 생성된 엔트로피가 최종 암호 키로 변환되는 전체 흐름을 추적합니다. 커널 내부에서 여러 계층의 처리를 거쳐 안전한 키가 생성됩니다.
암호 키 생성 3가지 방식
/* 방식 1: get_random_bytes() — 권장 (CRNG 경유, 항상 안전) */
static int generate_aes_key(u8 *key, size_t keylen)
{
get_random_bytes(key, keylen); /* CRNG 초기화 후 항상 블록하지 않음 */
/* keylen = 16(AES-128), 24(AES-192), 32(AES-256) */
return 0;
}
/* 사용 후 반드시 소거 */
memzero_explicit(key, keylen); /* 스택 키: 컴파일러 최적화 무력화 */
kfree_sensitive(heap_key); /* 힙 키: 해제 전 자동 제로화 */
/* 방식 2: /dev/hwrng 직접 읽기 — 사용자 공간 (원시 엔트로피, CRNG 미경유) */
#include <fcntl.h>
#include <unistd.h>
static int read_hwrng_key(unsigned char *key, size_t len)
{
int fd = open("/dev/hwrng", O_RDONLY);
if (fd < 0) return -1;
ssize_t n = read(fd, key, len);
close(fd);
return (n == (ssize_t)len) ? 0 : -1;
/* 주의: 추출기(extractor) 미적용 시 편향 가능, NIST 권장은 getrandom() */
}
/* 방식 3: rng->read() — 커널 드라이버 내부 직접 접근 */
static int kernel_rng_keygen(struct hwrng *rng, u8 *key, size_t len)
{
int ret = rng->read(rng, key, len, true);
if (ret != (int)len) return -EIO;
/* 주의: 원시 엔트로피 — 반드시 KDF(HKDF/PBKDF2)로 처리 후 사용 */
return 0;
}
테스트 및 검증
hwrng 드라이버를 로드한 후 다음 방법으로 동작을 검증합니다.
sysfs 인터페이스
# 현재 활성 hwrng 확인
cat /sys/class/misc/hw_random/rng_current
# 출력: myrng (또는 qrng-quantum)
# 사용 가능한 hwrng 목록
cat /sys/class/misc/hw_random/rng_available
# 출력: myrng virtio_rng intel-rng
# 활성 hwrng 변경 (우선순위: quality 높은 것이 자동 선택)
echo "myrng" > /sys/class/misc/hw_random/rng_current
# hwrng에서 직접 읽기 (원시 바이트)
dd if=/dev/hwrng bs=1 count=32 | xxd
rng-tools를 사용한 엔트로피 공급
# rng-tools 설치 (Ubuntu/Debian)
apt-get install rng-tools5
# rngd 데몬 실행 — /dev/hwrng → /dev/random 엔트로피 공급
rngd -r /dev/hwrng -o /dev/random -f
# 엔트로피 풀 현재 크기 확인 (레거시, 5.18+에서는 의미 감소)
cat /proc/sys/kernel/random/entropy_avail
# CRNG 초기화 완료 여부 확인 (dmesg)
dmesg | grep -E "crng|random"
# 정상: "random: crng init done"
# 경고: "random: get_random_u32 called from ... with crng_init=0" → 초기화 전 사용
rngtest — NIST SP 800-22 통계 검증
# NIST 통계 테스트 실행 (20000비트 × 200 블록)
cat /dev/hwrng | rngtest -c 200
# 예상 출력 (정상 QRNG):
# rngtest: bits received from input: 4000032
# rngtest: FIPS 140-2 success: 196
# rngtest: FIPS 140-2 failures: 4
# rngtest: bits discarded due to failures: 80032
# → 200블록 중 ~4개 실패는 통계적으로 정상 (5% 이내)
# 실패율이 너무 높으면 (10% 초과) 드라이버 또는 하드웨어 문제
# ENT (entropy estimation 도구)
dd if=/dev/hwrng bs=1M count=1 | ent
# Entropy: 7.9999 bits per byte → 이상적 QRNG
# Chi-square: 초과 확률 > 1%이면 양호
커널 내부 엔트로피 디버그
# CRNG 초기화 타임스탬프 확인
dmesg --ctime | grep "crng init"
# hwrng 스레드 동작 확인 (hw_random 코어가 kthread로 지속 수집)
ps aux | grep hwrng
# 출력: ... [hwrng] (커널 스레드)
# /proc/sys/kernel/random/ 파라미터
ls /proc/sys/kernel/random/
# boot_id — 부팅 고유 UUID (변경 불가)
# uuid — 읽을 때마다 새 UUID 생성
# entropy_avail — 현재 엔트로피 추정값 (비트)
# poolsize — 입력 풀 크기 (항상 256)
# urandom_min_reseed_secs — 재시드 최소 간격
# 엔트로피 소모 시뮬레이션 (테스트 전용)
dd if=/dev/urandom of=/dev/null bs=1M count=100 &
watch -n 0.5 'cat /proc/sys/kernel/random/entropy_avail'
엔트로피 기여량 실시간 모니터링
# hwrng 스레드 동작 및 엔트로피 기여량 실시간 모니터링
# (1초 간격으로 entropy_avail 변화 추적)
watch -n 1 'echo "=== $(date +%T) ===" ;
echo "활성 hwrng: $(cat /sys/class/misc/hw_random/rng_current)";
echo "엔트로피 추정: $(cat /proc/sys/kernel/random/entropy_avail) bits";
echo "CRNG 재시드: $(cat /proc/sys/kernel/random/urandom_min_reseed_secs)s 간격"'
# hwrng 커널 스레드 CPU 사용률 확인
ps -eo pid,comm,pcpu | grep hwrng
# 1MB 샘플 수집 속도 측정 (처리율 벤치마크)
time dd if=/dev/hwrng of=/dev/null bs=4k count=256 2>&1
# 출력 예: 1048576 bytes copied, 0.08 s, 12.6 MB/s ← PCIe QRNG
# 1048576 bytes copied, 1.05 s, 998 kB/s ← USB QRNG
NIST SP 800-90B 오픈소스 검증 도구
# NIST SP 800-90B 공식 검증 도구 설치 (Python 3 기반)
git clone https://github.com/usnistgov/SP800-90B_EntropyAssessment.git
cd SP800-90B_EntropyAssessment
pip3 install -r requirements.txt
# 1,000,000바이트 원시 엔트로피 수집
dd if=/dev/hwrng bs=1M count=1 of=qrng_sample.bin
# IID 추정 (독립-동일 분포 가정)
python3 ./src/ea_iid.py qrng_sample.bin 8
# 출력: min-entropy estimate = 7.999 bits per symbol → quality=1023 적합
# Non-IID 추정 (보수적 추정, FIPS 140-3 요건)
python3 ./src/ea_non_iid.py qrng_sample.bin 8
# 10가지 추정기 중 최솟값이 실제 min-entropy
# 출력 예: min-entropy = 7.998 → H_min_per_bit = 0.9998 → quality=1023
# 재시작 테스트 (Restart Test) — 하드웨어 초기 조건 의존성 검증
python3 ./src/ea_restart.py qrng_restart.bin 8 1000
# 1000회 재시작 × 1000샘플 = 1,000,000바이트 필요
dieharder 통계 배터리 전체 실행
# dieharder 설치
apt-get install dieharder # Ubuntu/Debian
dnf install dieharder # Fedora/RHEL
# 전체 배터리 실행 (114개 통계 검정, 수 시간 소요)
cat /dev/hwrng | dieharder -a -g 200
# -a: 모든 테스트, -g 200: stdin 입력
# 결과: PASSED/WEAK/FAILED — WEAK는 5% 이내 정상
# 특정 테스트만 실행 (빠른 검증)
cat /dev/hwrng | dieharder -d 0 -g 200 # Birthday Spacings
cat /dev/hwrng | dieharder -d 15 -g 200 # RGB Bit Distribution
cat /dev/hwrng | dieharder -d 100 -g 200 # STS Monobit
# ENT 도구 — 빠른 엔트로피 추정
dd if=/dev/hwrng bs=1M count=1 | ent
# Entropy: 7.9999 bits per byte → 이상적 QRNG
# Chi-square: 적합 확률 50% → 정상 분포
# Arithmetic mean: 127.49 → 균등 분포 (이론: 127.5)
# Serial correlation: 0.000001 → 독립성 확인
드라이버 로드/언로드 검증 체크리스트
| 단계 | 확인 명령 | 정상 기대값 |
|---|---|---|
| 드라이버 로드 | insmod myrng.ko && dmesg | tail -5 |
"myrng 등록 완료" 메시지 |
| hwrng 인식 | cat /sys/class/misc/hw_random/rng_available |
목록에 "myrng" 포함 |
| 난수 읽기 | dd if=/dev/hwrng bs=16 count=1 | xxd |
16바이트 난수 출력 |
| 엔트로피 기여 | watch cat /proc/sys/kernel/random/entropy_avail |
값이 증가 |
| NIST 테스트 | cat /dev/hwrng | rngtest -c 100 |
실패율 5% 이하 |
| 드라이버 언로드 | rmmod myrng && dmesg | tail -3 |
정상 언로드 메시지, 크래시 없음 |
엔트로피 풀 구조 심층 분석
Linux 커널의 엔트로피 풀은 drivers/char/random.c에 구현된 핵심 난수 인프라의 심장부입니다.
커널 5.18 이전에는 input_pool과 blocking_pool 두 개의 풀이 존재했지만,
현대 커널에서는 Blake2s 기반의 단일 input_pool로 통합되었습니다.
이 섹션에서는 풀의 내부 구조, 믹싱 알고리즘, 엔트로피 추정 메커니즘을 심층적으로 분석합니다.
풀 구조의 진화: LFSR에서 Blake2s로
c1ea38a5e7c(v5.17)에서 Blake2s 해시 함수로 완전 교체되었으며,
이로써 풀 크기가 4096비트(512바이트)에서 256비트(32바이트)로 축소되었지만 암호학적 강도는 크게 향상되었습니다.
| 항목 | 레거시 (5.17 이전) | 현대 (5.18+) |
|---|---|---|
| 풀 구조 | input_pool + blocking_pool (2개) | input_pool 단일 |
| 믹싱 함수 | LFSR (비트 연산 기반) | Blake2s (암호학적 해시) |
| 풀 크기 | 4096비트 (512바이트) | 256비트 (32바이트 해시 상태) |
| 추출 함수 | SHA-1 기반 extract | Blake2s final + re-absorb |
| 엔트로피 카운터 | 정수 비트 카운터 (정밀) | init_bits 256비트 목표 |
| /dev/random 블록킹 | 엔트로피 고갈 시 블록 | CRNG 초기화 전에만 블록 |
| forward secrecy | 미보장 (SHA-1 출력 재사용) | Fast Key Erasure 보장 |
믹싱 알고리즘 상세
/* drivers/char/random.c — 현대 커널 엔트로피 풀 구조 */
/* 전역 엔트로피 풀 — Blake2s 해시 상태 */
static struct {
struct blake2s_state hash; /* 256비트 내부 상태 */
spinlock_t lock; /* 동시 접근 보호 */
unsigned int init_bits; /* 누적 엔트로피 비트 수 */
} input_pool = {
.hash.h = { BLAKE2S_IV0 ^ (0x01010000 | 32),
BLAKE2S_IV1, BLAKE2S_IV2, BLAKE2S_IV3,
BLAKE2S_IV4, BLAKE2S_IV5, BLAKE2S_IV6, BLAKE2S_IV7 },
.hash.outlen = 32,
.lock = __SPIN_LOCK_UNLOCKED(input_pool.lock),
};
/* 믹싱 함수 — 엔트로피를 풀에 혼합 (비가역적) */
static void _mix_pool_bytes(const void *buf, size_t len)
{
unsigned long flags;
spin_lock_irqsave(&input_pool.lock, flags);
blake2s_update(&input_pool.hash, buf, len);
spin_unlock_irqrestore(&input_pool.lock, flags);
}
/* 엔트로피 추출 — 풀 상태를 변경하며 32바이트 키 생성 */
static void extract_entropy(void *buf, size_t len)
{
unsigned long flags;
u8 hash[BLAKE2S_HASH_SIZE];
struct blake2s_state state;
spin_lock_irqsave(&input_pool.lock, flags);
/* 1. 현재 풀 상태의 스냅샷 생성 */
state = input_pool.hash;
/* 2. 스냅샷에서 최종 해시 계산 (256비트) */
blake2s_final(&state, hash);
/* 3. 출력 해시를 풀에 재혼합 → backtrack resistance */
blake2s_update(&input_pool.hash, hash, sizeof(hash));
spin_unlock_irqrestore(&input_pool.lock, flags);
memcpy(buf, hash, len);
memzero_explicit(hash, sizeof(hash));
memzero_explicit(&state, sizeof(state));
}
/* 엔트로피 크레딧 — init_bits 누적 (256비트 목표) */
static void credit_init_bits(size_t bits)
{
unsigned int new_bits, orig;
unsigned long flags;
if (!bits)
return;
spin_lock_irqsave(&input_pool.lock, flags);
orig = input_pool.init_bits;
new_bits = min_t(unsigned int, POOL_READY_BITS,
orig + bits); /* 최대 256비트까지만 */
WRITE_ONCE(input_pool.init_bits, new_bits);
spin_unlock_irqrestore(&input_pool.lock, flags);
/* 256비트 도달 시 CRNG 초기화 트리거 */
if (orig < POOL_READY_BITS && new_bits >= POOL_READY_BITS)
crng_reseed();
}
엔트로피 추정과 크레딧 정책
커널은 각 소스별로 다른 엔트로피 크레딧 정책을 적용합니다.
hwrng의 경우 quality 파라미터가 직접적으로 크레딧 계산에 사용됩니다.
| 엔트로피 소스 | 크레딧 정책 | 기여 조건 | 최대 기여량 |
|---|---|---|---|
| hwrng | bytes × quality × 8 / 1024 |
hwrng_fillfn() 스레드 주기적 호출 | quality 의존 (최대 무제한) |
| 인터럽트 지터 | fast_pool 64회마다 1비트 | TSC/사이클 카운터 차이 | 초당 수십 비트 |
| 디스크 I/O | 타이밍 델타 기반 | CONFIG_RANDOM_TRUST_BOOTLOADER |
I/O 빈도에 비례 |
| 입력 이벤트 | 이벤트 타이밍 + 코드 | 키보드/마우스 존재 시 | 서버에서는 거의 0 |
| CPU RDRAND | CONFIG_RANDOM_TRUST_CPU=y 시 256비트 |
부팅 초기 즉시 | 256비트 (일회성) |
RDSEED vs RDRAND 비교 분석
Intel x86 프로세서는 두 가지 하드웨어 난수 명령어를 제공합니다:
RDRAND(Ivy Bridge, 2012~)와 RDSEED(Broadwell, 2014~).
두 명령어는 같은 하드웨어 엔트로피 소스(열잡음 기반 디지털 난수 생성기)를 공유하지만,
출력 방식과 암호학적 보증이 근본적으로 다릅니다.
RDRAND 내부 동작
/* arch/x86/include/asm/archrandom.h — RDRAND 인라인 어셈블리 */
static inline bool rdrand_long(unsigned long *v)
{
bool ok;
unsigned int retry = RDRAND_RETRY_LOOPS; /* 기본 10회 */
do {
asm volatile(RDRAND_LONG
"\n\t"
CC_SET(c) /* CF=1이면 유효한 난수 */
: CC_OUT(c)(ok), "=a"(*v));
if (ok)
return true;
} while (--retry);
return false; /* DRNG 내부 고갈 — 매우 드뭄 */
}
/* RDSEED — 순수 하드웨어 엔트로피 (DRBG 미경유) */
static inline bool rdseed_long(unsigned long *v)
{
bool ok;
asm volatile(RDSEED_LONG
"\n\t"
CC_SET(c) /* CF=1이면 유효한 시드 */
: CC_OUT(c)(ok), "=a"(*v));
return ok; /* 실패 확률이 RDRAND보다 높음 */
}
/* 커널 엔트로피 수집에서의 사용 — random.c */
static void try_to_generate_entropy(void)
{
unsigned long v;
/* RDSEED 우선 시도 (순수 엔트로피) */
if (rdseed_long(&v)) {
_mix_pool_bytes(&v, sizeof(v));
credit_init_bits(sizeof(v) * 8); /* 64비트 크레딧 */
return;
}
/* RDSEED 실패 시 RDRAND 폴백 (DRBG 경유) */
if (rdrand_long(&v)) {
_mix_pool_bytes(&v, sizeof(v));
/* CONFIG_RANDOM_TRUST_CPU=y 설정 시에만 크레딧 부여 */
}
}
RDRAND vs RDSEED 상세 비교표
| 항목 | RDRAND | RDSEED |
|---|---|---|
| 도입 시기 | Ivy Bridge (2012, 3세대) | Broadwell (2014, 5세대) |
| CPUID 플래그 | CPUID.01H:ECX.RDRAND[30] | CPUID.07H:EBX.RDSEED[18] |
| 엔트로피 출처 | CTR-DRBG (AES-256-CTR) 출력 | ES 컨디셔너 직접 출력 |
| NIST 준수 | SP 800-90A (DRBG) | SP 800-90B (Entropy Source) |
| 실패 확률 | 매우 낮음 (내부 버퍼링) | 상대적으로 높음 (ES 직접) |
| 처리율 (Skylake) | ~500 MB/s | ~70 MB/s (변동 있음) |
| 예측 저항성 | 재시드 간격 내 예측 가능 | 매 출력마다 독립 |
| 커널 사용처 | 보조 엔트로피 믹싱 | input_pool 초기 시딩 |
| 지연(latency) | ~400 사이클 | ~800 사이클 (변동) |
| AMD 명칭 | 동일 (Zen+) | 동일 (Zen 2+) |
RDRAND/RDSEED는 CPU 마이크로코드 수준에서 구현되어
독립적 검증이 어렵습니다. 커널의 CONFIG_RANDOM_TRUST_CPU 옵션이 n이면
RDRAND 출력을 엔트로피로 신뢰하지 않고, 믹싱만 수행합니다.
nordrand 부트 파라미터로 RDRAND 사용을 완전히 비활성화할 수 있습니다.
FIPS 140-2/140-3 인증과 커널 RNG
FIPS 140(Federal Information Processing Standard 140)은 미국 연방 정부가 요구하는
암호 모듈 보안 표준입니다. Linux 커널은 CONFIG_CRYPTO_FIPS 옵션과 fips=1 부트 파라미터를 통해
FIPS 모드를 지원하며, 이 모드에서는 RNG 관련 추가 요구사항이 활성화됩니다.
FIPS 모드 커널 설정
/* crypto/fips.c — FIPS 모드 전역 플래그 */
int fips_enabled;
EXPORT_SYMBOL_GPL(fips_enabled);
/* 부팅 파라미터: fips=1 → FIPS 모드 활성화 */
static int __init fips_enable(char *str)
{
fips_enabled = simple_strtol(str, NULL, 0);
printk(KERN_INFO "fips mode is %s\n",
fips_enabled ? "enabled" : "disabled");
return 1;
}
__setup("fips=", fips_enable);
/* FIPS 모드에서의 RNG 셀프 테스트 */
/* crypto/testmgr.c — alg_test_drbg() */
static int alg_test_drbg(const struct alg_test_desc *desc,
const char *driver, u32 type, u32 mask)
{
int err = 0;
int i;
const struct drbg_testvec *tv;
/* 알려진 응답 테스트(KAT): 고정 시드 → 고정 출력 비교 */
for (i = 0; i < desc->suite.drbg.count; i++) {
tv = &desc->suite.drbg.vecs[i];
err = drbg_cavs_test(tv, desc->alg_common.cra_driver_name);
if (err) {
pr_err("DRBG KAT 실패 #%d: %s\n", i, driver);
return err;
}
}
return 0;
}
FIPS DRBG 요구사항과 커널 구현 매핑
| FIPS 요구사항 | SP 800-90A 조항 | 커널 구현 | 코드 위치 |
|---|---|---|---|
| 시드 소스 품질 | Section 8.6.1 | hwrng quality >= 256 |
drivers/char/hw_random/core.c |
| 재시드 주기 | Section 9.3.2 | 5분 또는 2^48 블록 | drivers/char/random.c crng_reseed() |
| 예측 저항 | Section 11.3 | Fast Key Erasure | crng_fast_key_erasure() |
| 상태 소거 | Section 11.4 | memzero_explicit() |
모든 키 사용 후 호출 |
| 건강 테스트 | Section 11.3.3 | 연속 출력 비교 | hwrng_fillfn() |
HW RNG 드라이버 구조 심화
커널의 hwrng 서브시스템(drivers/char/hw_random/)은 다양한 하드웨어 RNG를 통일된
프레임워크로 관리합니다. 이 섹션에서는 코어 프레임워크의 내부 동작, 등록/해제 흐름,
주요 드라이버(virtio-rng, TPM RNG, Intel DRNG)의 구현 차이를 분석합니다.
hwrng 코어 프레임워크 내부
/* drivers/char/hw_random/core.c — hwrng_register() 흐름 */
int hwrng_register(struct hwrng *rng)
{
int err = -EINVAL;
struct hwrng *tmp;
bool is_new_current = false;
/* 필수 콜백 검증: read() 또는 data_read() 중 하나 필수 */
if (!rng->name || (!rng->data_read && !rng->read))
return -EINVAL;
mutex_lock(&rng_mutex);
/* 이름 중복 검사 */
list_for_each_entry(tmp, &rng_list, list) {
if (strcmp(tmp->name, rng->name) == 0) {
err = -EEXIST;
goto out_unlock;
}
}
/* 초기화 콜백 호출 (있으면) */
if (rng->init) {
err = rng->init(rng);
if (err)
goto out_unlock;
}
kref_init(&rng->ref);
init_completion(&rng->cleanup_done);
init_completion(&rng->dying);
/* 우선순위: quality가 높은 드라이버가 current_rng */
if (!current_rng || rng->quality > current_rng->quality) {
current_rng = rng;
is_new_current = true;
}
list_add_tail(&rng->list, &rng_list);
/* hwrng_fillfn 스레드 시작 (최초 등록 시) */
if (is_new_current || !hwrng_fill)
start_khwrngd();
mutex_unlock(&rng_mutex);
return 0;
out_unlock:
mutex_unlock(&rng_mutex);
return err;
}
virtio-rng 드라이버 상세
가상화 환경에서 게스트 OS의 엔트로피 부족은 심각한 보안 문제를 야기합니다.
virtio-rng 드라이버는 호스트의 /dev/urandom에서 생성된 난수를
virtqueue를 통해 게스트에 공급합니다.
/* drivers/char/hw_random/virtio-rng.c — 핵심 구조 */
struct virtrng_info {
struct hwrng hwrng;
struct virtqueue *vq; /* virtio 큐 */
struct completion have_data; /* 데이터 수신 완료 */
unsigned int data_avail; /* 사용 가능 바이트 */
unsigned int data_idx; /* 현재 읽기 인덱스 */
u8 *data; /* 수신 버퍼 */
bool busy;
bool hwrng_removed;
};
/* virtio 콜백 — 호스트에서 데이터 도착 */
static void random_recv_done(struct virtqueue *vq)
{
struct virtrng_info *vi = vq->vdev->priv;
vi->data_avail = virtqueue_get_buf(vq, &vi->data_avail);
complete(&vi->have_data);
}
/* hwrng read — virtqueue에서 난수 수신 */
static int virtio_read(struct hwrng *hwrng, void *buf,
size_t size, bool wait)
{
struct virtrng_info *vi = (struct virtrng_info *)hwrng->priv;
int ret;
if (!vi->busy) {
vi->busy = true;
reinit_completion(&vi->have_data);
register_buffer(vi); /* virtqueue에 수신 버퍼 등록 */
}
if (!wait)
return 0;
ret = wait_for_completion_killable(&vi->have_data);
if (ret < 0)
return ret;
vi->busy = false;
memcpy(buf, vi->data, vi->data_avail);
return vi->data_avail;
}
-device virtio-rng-pci,max-bytes=1024,period=1000으로
가상 RNG를 추가합니다. max-bytes와 period(ms)로 게스트에 공급하는 난수 대역폭을 제한할 수 있습니다.
-object rng-random,filename=/dev/urandom,id=rng0으로 호스트 소스를 지정합니다.
getrandom() 시스템 콜 심층 분석
getrandom()(syscall #318, x86-64)은 Linux 3.17에서 도입된 현대적 난수 획득 인터페이스입니다.
파일 디스크립터가 필요 없고, CRNG 초기화 상태를 정확히 반영하며,
/dev/urandom의 초기 부팅 시 안전하지 않은 난수 반환 문제를 해결합니다.
커널 내부 실행 경로
/* drivers/char/random.c — getrandom() 시스템 콜 구현 */
SYSCALL_DEFINE3(getrandom, char __user *, ubuf,
size_t, len, unsigned int, flags)
{
struct iov_iter iter;
int ret;
/* 플래그 유효성 검사 */
if (flags & ~(GRND_NONBLOCK | GRND_RANDOM | GRND_INSECURE))
return -EINVAL;
/* 크기 제한: INT_MAX (사실상 무제한) */
len = min_t(size_t, len, INT_MAX);
/* GRND_INSECURE: CRNG 초기화 전에도 즉시 반환 (안전하지 않음) */
if (flags & GRND_INSECURE)
goto insecure;
/* CRNG 초기화 대기 */
if (!crng_ready()) {
if (flags & GRND_NONBLOCK)
return -EAGAIN; /* 즉시 반환: 아직 준비 안 됨 */
/* 블록: CRNG 초기화(256비트 엔트로피 수집) 완료까지 대기 */
ret = wait_for_random_bytes();
if (ret)
return ret; /* -EINTR: 시그널에 의해 인터럽트 */
}
insecure:
/* ChaCha20-CRNG에서 난수 생성 → 사용자 공간으로 복사 */
import_ubuf(ITER_DEST, ubuf, len, &iter);
ret = get_random_bytes_user(&iter);
return ret;
}
/* wait_for_random_bytes() — CRNG 초기화 대기 */
int wait_for_random_bytes(void)
{
while (!crng_ready()) {
int ret;
/* jitterentropy 보조 시딩 시도 */
try_to_generate_entropy();
ret = wait_event_interruptible_timeout(
crng_init_wait, crng_ready(), HZ);
if (ret)
return ret > 0 ? 0 : ret;
}
return 0;
}
부팅 초기 블로킹 문제와 대응
getrandom()은 블록됩니다.
| 환경 | CRNG 초기화 시간 | 원인 | 해결책 |
|---|---|---|---|
| 물리 서버 (hwrng 있음) | < 1초 | hwrng가 즉시 엔트로피 공급 | 기본 설정으로 충분 |
| 물리 서버 (hwrng 없음) | 1~5초 | 인터럽트 지터 + 디스크 I/O | jitterentropy-rngd 설치 |
| KVM 가상머신 | 1~30초 | 가상화된 타이머 정밀도 감소 | virtio-rng 장치 추가 |
| Docker 컨테이너 | 호스트 의존 | 호스트 CRNG 공유 | 호스트의 hwrng 보장 |
| 임베디드/IoT | 10초~수 분 | 최소 하드웨어, 입력 없음 | SoC 내장 TRNG 드라이버 활성화 |
# 부팅 초기 블로킹 진단
dmesg | grep -E "crng|random|getrandom"
# "random: crng init done" 메시지 확인 (타임스탬프 = 초기화 소요 시간)
# 커널 명령줄로 엔트로피 소스 신뢰 설정
# random.trust_cpu=1 — RDRAND 출력을 엔트로피로 신뢰
# random.trust_bootloader=1 — 부트로더 제공 시드 신뢰
# systemd-random-seed.service: 이전 부팅의 엔트로피 저장/복원
systemctl status systemd-random-seed.service
# 저장 위치: /var/lib/systemd/random-seed (32바이트)
ChaCha20 CRNG 내부 심층 분석
Linux 커널의 CRNG(Cryptographically secure Random Number Generator)는 Daniel Bernstein의 ChaCha20 스트림 암호를 핵심으로 사용합니다. 이 섹션에서는 per-CPU CRNG 구조, 리시드 메커니즘, NUMA 최적화, Fast Key Erasure 기법을 상세히 분석합니다.
ChaCha20 블록 함수 상세
/* lib/crypto/chacha20-generic.c — ChaCha20 핵심 */
/* ChaCha20 상태: 4×4 = 16개의 32비트 워드 (512비트 = 64바이트) */
/*
* 상태 레이아웃:
* cccccccc cccccccc cccccccc cccccccc ← 상수 "expand 32-byte k"
* kkkkkkkk kkkkkkkk kkkkkkkk kkkkkkkk ← 키 (256비트, 8워드)
* kkkkkkkk kkkkkkkk kkkkkkkk kkkkkkkk
* bbbbbbbb nnnnnnnn nnnnnnnn nnnnnnnn ← 블록 카운터 + 논스
*/
static void chacha20_block_generic(u32 *state, u8 *stream)
{
u32 x[16];
int i;
memcpy(x, state, 64);
/* 20라운드 (10 double-round) */
for (i = 0; i < 10; i++) {
/* 열(column) 라운드 */
QUARTERROUND(x[0], x[4], x[8], x[12]);
QUARTERROUND(x[1], x[5], x[9], x[13]);
QUARTERROUND(x[2], x[6], x[10], x[14]);
QUARTERROUND(x[3], x[7], x[11], x[15]);
/* 대각선(diagonal) 라운드 */
QUARTERROUND(x[0], x[5], x[10], x[15]);
QUARTERROUND(x[1], x[6], x[11], x[12]);
QUARTERROUND(x[2], x[7], x[8], x[13]);
QUARTERROUND(x[3], x[4], x[9], x[14]);
}
/* 최종 더하기: 원본 상태와 XOR → 역연산 방지 */
for (i = 0; i < 16; i++)
x[i] += state[i];
memcpy(stream, x, 64); /* 64바이트 키스트림 출력 */
state[12]++; /* 블록 카운터 증가 */
}
/* Quarter Round 매크로 — ChaCha20의 핵심 연산 */
#define QUARTERROUND(a, b, c, d) \
a += b; d ^= a; d = rol32(d, 16); \
c += d; b ^= c; b = rol32(b, 12); \
a += b; d ^= a; d = rol32(d, 8); \
c += d; b ^= c; b = rol32(b, 7)
NUMA per-node CRNG 최적화
/* drivers/char/random.c — per-CPU CRNG 키 갱신 로직 */
/* 난수 요청 시 per-CPU CRNG 세대 확인 */
static void crng_make_state(u32 chacha_state[CHACHA_STATE_WORDS],
u8 *random_data, size_t random_data_len)
{
unsigned long flags;
struct crng *crng;
/* preempt 비활성화 + per-CPU CRNG 접근 */
local_lock_irqsave(&crngs.lock, flags);
crng = raw_cpu_ptr(&crngs);
/* 세대(generation) 비교 → 불일치 시 base_crng에서 새 키 복사 */
if (unlikely(crng->generation != READ_ONCE(base_crng.generation))) {
spin_lock(&base_crng.lock);
memcpy(crng->key, base_crng.key, sizeof(crng->key));
crng->generation = base_crng.generation;
spin_unlock(&base_crng.lock);
}
/* per-CPU 키로 ChaCha20 상태 초기화 */
chacha_init_consts(chacha_state);
memcpy(&chacha_state[4], crng->key, CHACHA20_KEY_SIZE);
memset(&chacha_state[14], 0, 8); /* 논스 = 0 */
++chacha_state[12]; /* 블록 카운터 증가 */
/* Fast Key Erasure: 출력의 앞 32바이트 → 새 키 */
crng_fast_key_erasure(crng->key, chacha_state,
random_data, random_data_len);
local_unlock_irqrestore(&crngs.lock, flags);
}
get_random_bytes()는
다른 CPU와 락 경합 없이 동작합니다. NUMA 시스템에서 원격 노드 메모리 접근도 발생하지 않아
1000만+ 호출/초 처리율을 달성합니다.
엔트로피 소스 완전 가이드
Linux 커널은 다양한 엔트로피 소스를 수집하여 input_pool에 혼합합니다. 각 소스는 서로 다른 품질과 수집 속도를 가지며, 이들의 조합으로 전체 엔트로피 품질을 보장합니다. 이 섹션에서는 각 소스의 동작 원리, 수집 경로, 환경별 가용성을 분석합니다.
인터럽트 타이밍 지터
인터럽트 타이밍은 가장 풍부한 소프트웨어 엔트로피 소스입니다. 하드웨어 인터럽트(IRQ) 도착 시각의 나노초 단위 변동(지터)은 CPU 캐시 상태, 메모리 접근 패턴, 버스 경합 등에 의해 결정되며 외부에서 예측하기 매우 어렵습니다.
/* drivers/char/random.c — 인터럽트 엔트로피 수집 상세 */
/* per-CPU 고속 풀 — 인터럽트 컨텍스트에서 락 없이 동작 */
struct fast_pool {
union {
u32 pool32[4]; /* 128비트 풀 (SipHash 스타일) */
u64 pool64[2];
};
unsigned long last; /* 마지막 기여 시각 (jiffies) */
u16 count; /* 인터럽트 카운터 (64회마다 기여) */
u16 reg_idx; /* 레지스터 인덱스 */
};
/* fast_mix() — 128비트 풀에 빠르게 혼합 (SipHash 유사) */
static void fast_mix(u32 pool[4], u32 a, u32 b, u32 c)
{
pool[0] ^= a;
pool[1] ^= b;
pool[2] ^= c;
pool[3] ^= pool[0];
/* 4라운드 혼합 — 확산(diffusion) 보장 */
pool[0] = rol32(pool[0], 7) + pool[3];
pool[1] = rol32(pool[1], 13) + pool[2];
pool[2] = rol32(pool[2], 11) + pool[1];
pool[3] = rol32(pool[3], 17) + pool[0];
}
/* mix_interrupt_randomness() — fast_pool → input_pool 전이 */
static void mix_interrupt_randomness(struct work_struct *work)
{
struct fast_pool *fast_pool = container_of(work, ...);
u32 pool[4];
memcpy(pool, fast_pool->pool32, sizeof(pool));
memset(fast_pool->pool32, 0, sizeof(fast_pool->pool32));
_mix_pool_bytes(pool, sizeof(pool));
credit_init_bits(max(1u, ...) ); /* 최소 1비트 크레딧 */
}
jitterentropy — CPU 지터 엔트로피
/* crypto/jitterentropy.c — 소프트웨어 기반 엔트로피 생성 */
/* CPU 명령어 실행 시간의 미세 변동을 엔트로피로 활용 */
/* 하드웨어 RNG가 없는 환경에서 보조 엔트로피 소스 역할 */
static u64 jent_measure_jitter(struct rand_data *ec)
{
u64 time, delta;
/* 메모리 접근 루프: 캐시 라인 경합으로 지터 유발 */
jent_memaccess(ec, 0);
/* 고정밀 타임스탬프 측정 */
time = jent_get_nstime(); /* rdtsc / cntvct / clock_gettime */
delta = time - ec->prev_time;
ec->prev_time = time;
/* 1차 미분: 타임스탬프 간 차이 */
/* 2차 미분: 차이의 차이 → 진정한 지터 추출 */
return delta;
}
/* 커널 부팅 시 jitterentropy 자동 활성화 조건 */
/* CONFIG_CRYPTO_JITTERENTROPY=y (대부분 배포판 기본) */
/* CRNG 초기화 대기 중 try_to_generate_entropy()에서 호출 */
ftrace/bpftrace 엔트로피 모니터링
hwrng 서브시스템과 엔트로피 풀의 동작을 실시간으로 관찰하는 것은
드라이버 디버깅과 성능 분석에 필수적입니다. 이 섹션에서는 ftrace, bpftrace,
/proc/sys/kernel/random/ 인터페이스를 활용한 모니터링 기법을 다룹니다.
ftrace로 난수 생성 추적
# ftrace 설정: random 서브시스템 이벤트 활성화
cd /sys/kernel/tracing
# 사용 가능한 random 이벤트 확인
ls events/random/
# add_device_randomness credit_entropy_bits
# debit_entropy extract_entropy
# get_random_bytes mix_pool_bytes
# push_to_pool random_read
# urandom_read getrandom
# 엔트로피 크레딧 추적 활성화
echo 1 > events/random/credit_entropy_bits/enable
echo 1 > events/random/mix_pool_bytes/enable
echo 1 > events/random/extract_entropy/enable
# 트레이스 시작
echo 1 > tracing_on
# 트레이스 확인 (엔트로피 수집/소비 패턴)
cat trace_pipe | head -50
# 출력 예:
# [hwrng]-123 credit_entropy_bits: bits 256
# sshd-456 extract_entropy: nbytes 32
# nginx-789 get_random_bytes: nbytes 16
# 트레이스 종료
echo 0 > tracing_on
bpftrace 엔트로피 모니터링 스크립트
/* bpftrace: 엔트로피 수집 함수 호출 빈도 히스토그램 */
/* 파일명: entropy_monitor.bt */
/* hwrng에서 엔트로피 수집 추적 */
kprobe:add_hwgenerator_randomness
{
@hw_count = count();
@hw_bytes = hist(arg1); /* 수집 바이트 수 분포 */
}
/* 인터럽트 엔트로피 수집 추적 */
kprobe:add_interrupt_randomness
{
@irq_count = count();
@irq_num[arg0] = count(); /* IRQ 번호별 빈도 */
}
/* 엔트로피 추출(소비) 추적 */
kprobe:extract_entropy
{
@extract_count = count();
@extract_bytes = hist(arg1); /* 추출 바이트 수 분포 */
}
/* get_random_bytes 호출자 추적 */
kprobe:get_random_bytes
{
@callers[kstack(3)] = count(); /* 상위 3프레임 콜스택 */
}
/* getrandom 시스콜 추적 */
tracepoint:syscalls:sys_enter_getrandom
{
@getrandom_size = hist(args->count);
@getrandom_flags[args->flags] = count();
}
interval:s:10
{
printf("\n--- %s 엔트로피 통계 ---\n", strftime("%H:%M:%S", nsecs));
print(@hw_count);
print(@irq_count);
print(@extract_count);
}
# bpftrace 스크립트 실행
bpftrace entropy_monitor.bt
# /proc/sys/kernel/random/ 파라미터 종합 분석
echo "=== 커널 난수 시스템 상태 ==="
echo "엔트로피 추정: $(cat /proc/sys/kernel/random/entropy_avail) bits"
echo "풀 크기: $(cat /proc/sys/kernel/random/poolsize) bits"
echo "부팅 UUID: $(cat /proc/sys/kernel/random/boot_id)"
echo "재시드 간격: $(cat /proc/sys/kernel/random/urandom_min_reseed_secs)초"
echo "읽기 깨움: $(cat /proc/sys/kernel/random/read_wakeup_threshold) bits"
echo "쓰기 깨움: $(cat /proc/sys/kernel/random/write_wakeup_threshold) bits"
# hwrng 드라이버 상태 종합
echo ""
echo "=== hwrng 드라이버 상태 ==="
echo "활성 RNG: $(cat /sys/class/misc/hw_random/rng_current)"
echo "가용 RNG: $(cat /sys/class/misc/hw_random/rng_available)"
# hwrng 처리율 벤치마크
echo ""
echo "=== hwrng 처리율 벤치마크 ==="
dd if=/dev/hwrng of=/dev/null bs=4096 count=1024 2>&1 | tail -1
# 4194304 bytes (4.2 MB, 4.0 MiB) copied, 0.05 s, 83.9 MB/s
# perf stat으로 RDRAND 명령어 실행 횟수 측정
perf stat -e instructions,cycles,cache-misses \
dd if=/dev/urandom of=/dev/null bs=4096 count=256 2>/dev/null
/proc/sys/kernel/random/ 파라미터 레퍼런스
| 파라미터 | 읽기/쓰기 | 기본값 | 설명 |
|---|---|---|---|
entropy_avail |
읽기 전용 | 0~256 | 현재 엔트로피 추정값 (비트). 5.18+에서는 0 또는 256 |
poolsize |
읽기 전용 | 256 | input_pool 크기 (비트). Blake2s 해시 상태 크기 |
boot_id |
읽기 전용 | UUID | 부팅마다 새로 생성되는 128비트 UUID |
uuid |
읽기 전용 | UUID | 읽을 때마다 새 UUID 생성 (UUID v4) |
urandom_min_reseed_secs |
읽기/쓰기 | 60 | CRNG 재시드 최소 간격 (초). 0이면 항상 재시드 |
read_wakeup_threshold |
읽기/쓰기 | 64 | /dev/random 블로킹 해제 엔트로피 임계값 |
write_wakeup_threshold |
읽기/쓰기 | 896 | 엔트로피 부족 시 poll() 이벤트 발생 임계값 |
RNG 구현 비교: Linux vs BSD vs Windows
운영체제마다 난수 생성 서브시스템의 설계 철학과 구현이 크게 다릅니다. 이 섹션에서는 Linux, FreeBSD, OpenBSD, Windows의 RNG 구현을 비교하고, 각각의 장단점과 성능 특성을 분석합니다.
운영체제별 RNG 구현 비교표
| 항목 | Linux (5.18+) | FreeBSD (14) | OpenBSD (7.x) | Windows (11) |
|---|---|---|---|---|
| 핵심 알고리즘 | ChaCha20 (CRNG) | Fortuna (AES-256-CTR) | ChaCha20 (arc4random) | AES-256-CTR (BCryptGenRandom) |
| 엔트로피 풀 | Blake2s 256비트 | 32개 풀 (Fortuna) | 256비트 (단일) | 다중 풀 (비공개) |
| 리시드 주기 | 5분 또는 수요 기반 | 10초 ~ 풀 로테이션 | 매 1.6MB 출력마다 | 자동 (비공개) |
| per-CPU 구조 | per-CPU ChaCha20 키 | 단일 전역 Fortuna | per-CPU arc4random | per-프로세서 (비공개) |
| 사용자 API | getrandom() |
getentropy() |
getentropy() |
BCryptGenRandom() |
| hwrng 프레임워크 | struct hwrng (다중 드라이버) | random_harvest (단일) | 없음 (RDRAND 직접) | CNG Provider |
| forward secrecy | Fast Key Erasure | Fortuna 풀 교체 | 매 블록 키 교체 | 미공개 |
| FIPS 인증 | CONFIG_CRYPTO_FIPS | 미인증 | 미인증 | FIPS 140-2 Level 1 |
| 부팅 시 블로킹 | getrandom() 블록 | getentropy() 블록 | 항상 비블록 | 항상 비블록 |
| 엔트로피 고갈 시 | CRNG 계속 출력 (안전) | Fortuna 계속 출력 | ChaCha20 계속 출력 | 계속 출력 |
성능 벤치마크 비교
# Linux /dev/urandom 처리율 측정
dd if=/dev/urandom of=/dev/null bs=4096 count=262144 2>&1 | tail -1
# 1073741824 bytes (1.1 GB) copied, 0.95 s, 1.1 GB/s ← ChaCha20 per-CPU
# getrandom() 레이턴시 측정 (C 프로그램)
# 32바이트 × 100만 회 → 평균 ~150ns/호출 (x86-64, ChaCha20 SIMD)
# /dev/hwrng 처리율 (하드웨어 의존)
dd if=/dev/hwrng of=/dev/null bs=4096 count=256 2>&1 | tail -1
# Intel RDRAND: ~500 MB/s
# virtio-rng: 호스트 의존 (~100 MB/s)
# USB QRNG: ~1 MB/s
# PCIe QRNG: ~100 MB/s
# TPM RNG: ~1 MB/s
| 인터페이스 | Linux (ChaCha20) | FreeBSD (Fortuna) | OpenBSD (arc4random) | 비고 |
|---|---|---|---|---|
| 커널 내부 (32B) | ~50ns | ~120ns | ~60ns | per-CPU 최적화 효과 |
| 시스콜 (32B) | ~150ns | ~200ns | ~100ns | 시스콜 오버헤드 포함 |
| 벌크 처리율 | ~1.1 GB/s | ~400 MB/s | ~800 MB/s | AVX2/NEON SIMD 활용 |
| 다중 스레드 확장성 | 선형 (per-CPU) | 제한적 (전역 락) | 선형 (per-CPU) | Linux, OpenBSD 우수 |
설계 철학 비교
arc4random()은 항상 비블록킹이며 libc에서 직접 제공합니다.
getentropy()의 최대 크기가 256바이트로 제한됩니다.
관련 문서
- Linux Crypto Framework (Crypto API) — ChaCha20, AES-NI, 커널 암호화 프레임워크 전반
- 커널 보안 — 커널 하드닝, 랜덤화(ASLR, stack canary), 보안 모델
- LSM / Seccomp — getrandom() 시스템 콜 필터링, seccomp 정책
- 커널 모듈 개발 —
struct hwrng기반 드라이버 기초 - 커널 디버깅 — 드라이버 크래시 분석, dmesg 해석