보안 토큰에서 “문자열이 길고 모양이 랜덤해 보인다”는 관찰은 충분한 근거가 아닙니다. 공격자가 다음 값을 예측하거나 후보를 좁힐 수 없어야 하며, 서버는 그 후보에 대한 시도 횟수와 유효 기간도 제한해야 합니다. RFC 4086은 보안 용도의 무작위성과 통계적으로 고른 출력이 다르다는 점, 시계처럼 제한된 범위의 입력과 약한 seed가 위험하다는 점을 설명합니다. 이 장은 비밀번호 재설정·세션·초대 코드 같은 자격과, 재현 가능한 시뮬레이션 난수를 한 경계에서 섞지 않는 방법을 다룹니다.
엔트로피라는 말을 사용할 때도 무엇을 측정하는지 분리해야 합니다. 균등하게 128비트 공간에서 하나를 뽑는다면 이상적인 후보 수는 2^128이고, 한 번의 최선 추측 확률은 2^-128입니다. 그러나 128비트 출력이 8비트 seed로 결정되면 공격자는 256개 seed를 시험해 전체를 재현할 수 있습니다. 출력 길이와 실제 비밀 불확실성은 같은 값이 아닙니다.
예측 불가능성과 통계적 무작위성
통계 검정은 출력이 편향되었는지 보는 데 유용하지만, 공격자가 seed나 상태를 알고 다음 출력을 계산할 수 있는지 답하지 못합니다. RFC 4086은 암호학적 용도에서 예측 불가능성이 핵심이며, 전통적인 pseudo-random sequence나 clock 기반 값을 그대로 자격으로 쓰지 말아야 한다는 관점을 취합니다. 반대로 시뮬레이션은 같은 seed로 같은 결과를 재현하는 것이 장점입니다. 요구되는 속성이 반대이므로 API와 상태를 분리하는 것이 먼저입니다.
비밀번호 재설정 토큰을 token_bytes(32)처럼 32개의 CSPRNG 바이트로 만든다고 하겠습니다. 이상적인 균등 가정에서 원시 후보 공간은 256^32 = 2^256입니다. 이를 URL-safe base64로 인코딩하면 약 43문자에 해당하지만, 길이가 43이라는 사실보다 원시 바이트가 예측 불가능한 보안 난수원에서 나왔는지가 핵심입니다. base64가 일부 padding을 생략하는 표현은 후보 공간을 늘리거나 줄이는 보안 근거가 아닙니다.
토큰 후보 공간과 온라인 예산
공격 성공 확률은 후보 공간만으로 결정되지 않습니다. 단일 토큰의 후보 수를 N, 공격자가 허용받는 검증 시도 횟수를 q라고 하면 균등한 추측의 대략적인 상한은 q/N입니다. 예를 들어 6자리 숫자 코드는 N=1,000,000이고, 계정별 5회 시도만 허용하면 한 토큰에 대한 단순 추측 확률은 약 5/1,000,000입니다. 이 코드를 이메일 링크의 256비트 토큰과 같은 취급해서는 안 됩니다.
32 random bytes -> N = 2^256
100 attempts -> success bound ≈ 100 / 2^256
6 decimal digits -> N = 10^6
5 attempts -> success bound ≈ 5 / 10^6
실제 설계에는 토큰 만료, 단일 사용, 사용자·IP·디바이스별 rate limit, 실패 시 계정 열거 방지, 유출 가능한 URL·Referer·로그 경로가 함께 들어갑니다. 짧은 코드는 서버가 온라인 검증을 엄격히 제한할 때만 별도 위험 수준으로 운용할 수 있고, 제한이 풀리면 후보 공간 자체가 부족해집니다. 토큰 원문을 DB에 저장하지 않고 해시해 두면 DB 읽기 접근이 곧바로 계정 변경으로 이어지는 것을 줄일 수 있지만, 전송 중 탈취나 메일함 침해를 막아 주지는 않습니다.
편향 없는 범위 선택
CSPRNG에서 바이트를 얻어 x % n으로 코드를 만들 때 원시 범위가 n으로 나누어떨어지지 않으면 modulo bias가 생깁니다. 0..7에서 하나를 뽑아 0..2로 줄이면 0과 1은 각각 세 번, 2는 두 번 나타납니다. 균등한 선택을 원하면 6과 7을 버리고 0..5에서 다시 선택합니다. rejection sampling은 출력 난수의 질과 별개로 범위 변환의 분포를 교정합니다.
보안 토큰을 0..999999로 바꾸는 경우도 같은 원리입니다. 32비트 unsigned 범위를 M = 2^32라고 하고 limit = M - (M mod 1,000,000)을 계산한 뒤, x < limit인 샘플만 받아 x mod 1,000,000을 취합니다. 여기서 M을 signed 32비트로 잘못 계산하면 상한이 음수가 되거나 큰 값이 잘립니다. 언어의 정수 표현과 API가 반환하는 범위를 먼저 확인해야 합니다. 다만 토큰의 보안 핵심이 CSPRNG라면 구현 언어가 제공하는 randomInt 또는 OS-backed API가 범위 변환까지 보장하는지 확인하고 직접 비트 연산을 최소화합니다.
상태 복제와 재시드 경계
사용자 공간 DRBG는 내부 상태로부터 다음 출력을 결정합니다. 컨테이너나 VM snapshot 시점에 두 인스턴스의 상태가 복제되고, 복원 뒤 같은 상태를 독립 프로세스가 이어서 사용하면 동일한 토큰열이 나올 수 있습니다. “시간이 조금 다르니 다르겠지”는 방어가 아닙니다. 두 프로세스가 복원 직후 같은 clock·PID·환경을 볼 수도 있고, 시간 값은 공격자가 추정하기 쉽습니다.
그림을 글로 읽기
- 토큰 발급 요청: 비밀번호 재설정·세션
- OS CSPRNG API: 예측 불가능한 상태
- 토큰·해시·만료: 온라인 시도 제한
- 재현 PRNG 경로: 시뮬레이션 전용
- 토큰 발급 요청 → OS CSPRNG API: 보안 자격만 연결
- OS CSPRNG API → 토큰·해시·만료: 원시 바이트 생성
- 토큰 발급 요청 → 재현 PRNG 경로: 다른 API·다른 상태
OS CSPRNG가 fork·snapshot·resume 뒤 어떻게 상태를 재동기화하는지는 운영체제와 런타임 API의 계약을 확인해야 합니다. 이 장의 근거인 RFC 4086은 일반적인 난수 요구사항과 entropy 문제를 설명하지만 특정 언어의 randomBytes가 모든 VM 복원 시나리오를 해결한다고 보증하지 않습니다. 문서에 fork 안전·재시드 동작이 명시되지 않은 사용자 공간 DRBG를 보안 토큰에 직접 쓰지 않는 편이 안전합니다.
재시드가 필요하다면 예측 가능한 시간·PID를 seed로 쓰는 fallback을 두지 않습니다. 프로세스 시작 시 보안 난수 초기화가 실패하면 readiness를 실패시키고 자격 발급 endpoint를 차단하는 fail-closed 경로를 설계합니다. 일시적인 OS 오류에는 유한 횟수의 backoff 재시도와 명확한 운영 알람을 둘 수 있지만, 계속된 실패 뒤 시간 기반 토큰을 발급하는 순간 장애가 취약점으로 바뀝니다.
용도별 상태와 키 분리
게임 replay, 부하 테스트, 샘플 데이터처럼 재현성이 필요한 난수는 seed를 로그와 테스트 fixture에 남길 수 있습니다. 세션 ID·비밀번호 재설정·CSRF nonce처럼 보유 자체가 권한인 값은 그 상태와 API가 같은 프로세스 안에서라도 분리되어야 합니다. 하나의 전역 RNG 객체를 모든 호출자가 사용하면 테스트가 seed를 고정하는 순간 보안 경로까지 예측 가능해질 수 있습니다.
이 선택은 단순한 보안 설정이 아니라 접근 제어입니다. 시뮬레이션 코드는 임의 seed와 상태 snapshot을 읽을 수 있어야 하지만, 자격 생성기는 상태를 애플리케이션 로그나 디버거에 노출하지 않아야 합니다. 보안 토큰 API에서 seed 인자를 받거나 “재현을 위해 seed를 로그에 남기는 옵션”을 두지 않습니다. 필요한 재현성은 실제 자격값이 아니라 발급 결과의 메타데이터와 테스트 전용 RNG로 확보합니다.
실패 처리와 운영 검증
발급 실패는 “토큰 생성 시도”와 “사용자에게 성공 응답”을 구분합니다. CSPRNG 호출이 실패했는데 회원가입이나 재설정 요청을 성공으로 응답하면 사용자는 사용할 수 없는 자격을 받거나, 구현자가 fallback을 추가할 유인이 생깁니다. 서비스 시작 시 난수원 self-check를 하더라도 실제 발급 시의 오류 반환을 별도로 처리해야 합니다. 오류 메시지에는 내부 장치나 seed를 노출하지 않고 추적 ID만 제공합니다.
검증 사례는 다음처럼 구성합니다. CSPRNG 정상 반환, 호출 오류, 경계값·거절 샘플, snapshot 복원 뒤 두 worker, 토큰 만료 직전, 동일 토큰 동시 소비, rate limit 초과를 각각 관찰합니다. 복원 시나리오는 이 환경에서 실제로 실행하지 않았다면 예상 결과로 표시해야 하며, 플랫폼 문서가 없는 동작을 성공 사실처럼 기록하지 않습니다. 분포 테스트는 수백만 회 표본에서 극단적 편향을 찾는 보조 수단일 뿐 예측 가능성의 증명이 아닙니다.
비용과 한계
높은 entropy의 토큰도 이메일 계정, 브라우저 history, 프록시 로그, 화면 캡처가 노출되면 안전하지 않습니다. CSPRNG를 사용하면 시스템 호출과 운영 장애 처리가 추가되고, 긴 URL 토큰은 전달 채널의 길이·로그 정책을 고려해야 합니다. 짧은 코드와 강한 rate limit은 사용자 경험과 운영 복잡성을 맞바꾸는 선택이며, 보안 토큰을 두 용도로 재사용하는 비용 절감과 비교할 수 없습니다.
또한 entropy 추정치를 과장하지 않아야 합니다. 실제 입력 분포가 균등하지 않거나 seed가 알려졌다면 Shannon entropy가 높아 보여도 최악의 추측 저항은 낮을 수 있습니다. RFC 4086이 설명하는 min-entropy 관점처럼 가장 가능성 높은 값의 확률을 확인하고, 토큰 후보 공간·시도 예산·수명·회수를 함께 검토합니다. 운영체제와 런타임의 현재 API, snapshot 정책, rate-limit 저장소의 원자성은 이 장의 일반 원리만으로 확정할 수 없는 결정 사항입니다.
구현 상태와 검산
토큰 발급 함수의 반환 계약을 issue(kind)처럼 좁히고 seed나 RNG 객체를 인자로 받지 않도록 하면 테스트용 상태가 보안 경계로 들어오는 길이 줄어듭니다. 발급 직후 DB에는 hash(token), 목적, 사용자 식별자, 만료 시각, 소비 시각만 저장하고 원문은 메일 전송 계층을 떠난 뒤 다시 읽지 않습니다. 소비는 UPDATE ... WHERE consumed_at IS NULL AND expires_at > now() 같은 원자적 조건으로 한 행만 성공시킨 뒤 비밀번호 변경과 같은 트랜잭션에 묶어야 합니다.
범위 변환은 작은 값으로 검산할 수 있습니다. 3비트 원천 0..7을 3개 값에 매핑하면 0과 1은 각각 세 번, 2는 두 번 선택되므로 6과 7을 거절하고 0..5만 사용해야 균등합니다. 이는 원천 DRBG 상태가 복제되는 문제와 독립적입니다. snapshot 시점 상태를 S라고 할 때 A와 B가 모두 다음 블록 T(S)를 받는 것은 분포가 아니라 상태 동일성의 실패입니다. 복원 뒤 OS CSPRNG를 다시 읽는다는 보장을 대상 런타임 문서가 말하지 않으면 발급을 막는 편이 정직합니다.
운영 검증에는 난수원 오류, rate-limit 저장소 장애, 동시 소비, 재발급 무효화, 로그 필터를 각각 넣습니다. 분포 표본이 균일해 보여도 8비트 seed를 가진 생성기는 예측 가능할 수 있고, 반대로 강한 토큰도 URL·메일함이 노출되면 탈취됩니다. 따라서 비용은 시스템 호출과 장애 대응뿐 아니라 원자적 저장소, 만료 정리, 모니터링까지 포함해 산정합니다.
참고 자료와 확인 범위
- RFC 4086, Randomness Requirements for Security. 예측 불가능성과 통계적 무작위성의 차이, 제한된 seed와 clock 사용의 문제, min-entropy 설명을 확인했습니다.
- RFC 2104는 HMAC 비밀키 자체를 다루는 별도 근거이며, 이 장의 토큰 생성 규칙을 직접 정의하지 않습니다.
- OS CSPRNG의 fork·snapshot·reseed 보장은 선택한 운영체제와 런타임 문서를 추가로 고정해야 합니다. 본문에서 해당 동작을 특정 제품의 보장으로 주장하지 않았습니다.
- 코드와 숫자 추적은 설명용이며 이 환경에서 토큰 발급·snapshot 복원을 실행한 결과가 아닙니다.
참고 경로
위 링크는 개념별 참고 경로이며, 본문에서 명시한 확인 범위와 미확인 구현 조건을 함께 적용합니다.