- Go 1.22는
math/rand와math/rand/v2의 기본 난수 소스를 암호학적으로 강한 생성기로 바꿔,crypto/rand를 써야 할 곳에서 실수했을 때의 피해를 크게 줄임 - 기존 Go 1 생성기는 607개의
uint64상태를 쓰는 선형 피드백 시프트 레지스터라, 607개 출력만 관측해도 과거·미래 값을 복원할 수 있음 math/rand/v2의 PCG-DXSM은 통계적 난수 품질과 상태 크기를 개선했지만, 비밀값에 필요한 예측 불가능성까지 보장하지는 못함- 새 ChaCha8Rand는 32바이트 시드, 16블록마다 재키잉, 코어별 300바이트 상태를 사용하며
math/rand/v2, 일부math/rand, map 해시 시드에 적용됨 - 성능 비용은 제한적임: ChaCha8Rand는 Go 1 생성기보다 느리지만 최대 2배 이내이고, 일반 서버에서는 차이가 3ns를 넘지 않아 대부분의 프로그램에서 보안 이득이 더 큼
Go 1.22가 바꾼 난수 기본값
- Go 1.22는
math/rand와math/rand/v2에서 암호학적으로 강한 의사난수 생성기를 사용하도록 기본값을 바꿈 - 목적은
crypto/rand가 필요한 곳에 개발자가 실수로math/rand를 사용했을 때의 피해를 줄이는 데 있음 - Go의 난수 API는 전통적으로 두 범주로 나뉨
math/rand: 시뮬레이션, 샘플링, 수치해석, 비암호학적 무작위 알고리듬, 퍼징, 셔플, 지수 백오프 등에 쓰는 통계적 난수crypto/rand: 키·토큰처럼 예측 불가능성이 필요한 암호학적 난수
통계적 난수가 충분하지 않은 이유
- 통계적 난수 생성기는 기본 통계 테스트를 통과하면 여러 비암호학적 용도에 충분할 수 있음
- 그러나 알고리듬을 아는 관찰자가 충분한 출력을 보면 이후 시퀀스를 예측할 수 있는 경우가 많음
- Unix V3의
srand와rand는 이후 C와 여러 언어의 난수 API에 영향을 준 초기 형태임- 단일 정수 시드로 상태를 설정함
- 선형 합동 생성기(LCG) 방식으로 다음 값을 계산함
- 내부 상태가 단순해 출력 하나만으로도 미래 값을 쉽게 계산할 수 있음
- LCG는 가능한 출력값을 반복 전 한 번씩 내보내도록 상수를 고를 수 있지만, 낮은 비트가 짧은 주기로 반복되는 약점이 있음
Go 1 생성기의 구조와 취약성
- Go 1의
math/rand생성기는 선형 피드백 시프트 레지스터 계열임 - 내부 상태는 607개의
uint64로 된vec슬라이스임vec[606]은 “tap”vec[334]는 “feed”- 다음 값을 만들 때 tap과 feed를 더해
x를 만들고,x를 feed 위치에 저장한 뒤 반환함
- 실제 구현은 슬라이스 전체를 이동하지 않고 tap과 feed 위치만 뒤로 이동해 비용을 줄임
- 다음 값 생성에는 두 번의 뺄셈, 두 번의 조건부 덧셈, 두 번의 로드, 한 번의 덧셈, 한 번의 저장이 필요함
- 반환값이 내부 상태 벡터의 한 원소라서 607개 출력을 읽으면 전체 상태가 노출됨
- 같은
vec을 채우고 알고리듬을 실행하면 미래 값을 예측할 수 있음 - 알고리듬을 거꾸로 실행하면 과거 값도 복원 가능함
- 같은
- Go 1 생성기는 보안 목적이 아니며, 생성된 수의 품질도 초기
vec설정에 의존함
PCG가 개선한 점과 남은 한계
math/rand/v2는 더 현대적인 통계적 난수 생성기로 Melissa O’Neill의 PCG를 채택함- Go의 PCG는 128비트 LCG를 기반으로 하며,
scramble함수로 128비트 상태를 64비트 출력으로 줄임 - Go는 제안 논의 중 O’Neill의 제안을 바탕으로 곱셈 기반
scramble을 사용함- 이 형태는 PCG-DXSM으로 불림
- Numpy도 이 PCG 형태를 사용함
- PCG는 Go 1 생성기보다 상태가 훨씬 작음
- Go 1 생성기: 607개의
uint64 - PCG: 두 개의
uint64
- Go 1 생성기: 607개의
- PCG는 초기 상태 값에 덜 민감하고 여러 통계 테스트를 통과하지만, 예측 불가능성을 보장하지는 않음
- PCG-XSL-RR은 되돌릴 수 있음
- PCG-DXSM도 되돌릴 수 있어도 놀랍지 않다고 봄
- 비밀값 생성에는 PCG가 아니라 다른 생성기가 필요함
암호학적 난수와 운영체제의 역할
- 암호학적 난수는 생성 방식을 알고 과거 출력값을 많이 본 관찰자에게도 실질적으로 예측 불가능해야 함
- 암호 프로토콜, 비밀 키, 현대 상거래, 온라인 프라이버시 등은 암호학적 난수에 의존함
- 실제 난수 공급은 운영체제가 담당함
- 마우스, 키보드, 디스크, 네트워크 타이밍 등 물리적 장치에서 무작위성을 수집함
- 최근에는 CPU가 직접 측정한 전기적 노이즈도 활용됨
- 운영체제가 충분한 난수, 예를 들어 최소 256비트를 모으면 해시나 암호화 알고리듬으로 긴 난수 시퀀스를 만듦
- 과거에는
/dev/random같은 장치 파일을 주로 썼지만, 현재 운영체제는 직접 시스템 호출을 제공함 - Go의
crypto/rand는 운영체제별 차이를 감추고 동일한 인터페이스인rand.Read를 제공함
ChaCha8Rand 설계
- Go 1.22의 새 생성기 ChaCha8Rand는 Daniel J. Bernstein의 ChaCha 스트림 암호를 가볍게 수정한 형태임
- ChaCha는 TLS와 SSH에서도 쓰이는 ChaCha20 형태가 널리 사용됨
- Jean-Philippe Aumasson의 Too Much Crypto는 8라운드 형태인 ChaCha8도 안전하다고 보며, ChaCha8은 대략 2.5배 빠름
- ChaCha8Rand는 ChaCha8을
rand.Source로 쓰기 위해 생성 블록을 입력과 XOR하지 않고 직접 난수 스트림으로 사용함- 이는 모두 0인 데이터를 암호화하거나 복호화하는 것과 같음
ChaCha8Rand의 변경점
- ChaCha8Rand는 32바이트 시드를 ChaCha8 키로 사용함
- ChaCha8은 64바이트 블록을 생성하며, 계산은 블록을 16개의
uint32로 취급함 - 일반 구현은 SIMD 명령으로 4개 블록을 동시에 계산할 수 있지만, XOR 입력에 쓰려면 interleaved 블록을 다시 풀어야 함
- ChaCha8Rand는 이 interleaved 블록 자체를 난수 스트림으로 정의해 unshuffle 비용을 없앰
- ChaCha8의 블록 마무리 단계에서는 특정 값을 각
uint32에 더함- 절반은 키 재료이고 절반은 알려진 상수임
- ChaCha8Rand는 알려진 상수를 다시 더하지 않아 최종 덧셈의 절반을 제거함
- 매 16번째 생성 블록마다 마지막 32바이트를 다음 16블록의 키로 사용함
- 이 재키잉은 일종의 전방향 안전성을 제공함
- 생성기 메모리 상태 전체가 노출되어도 마지막 재키잉 이후 값만 복구 가능하고, 과거 값에는 접근할 수 없음
- Go는 ChaCha8Rand C2SP 명세와 테스트 케이스를 공개해, 같은 시드에 대해 다른 구현도 Go 구현과 반복성을 공유할 수 있게 함
표준 라이브러리 적용 위치
- Go 런타임은 운영체제가 공급한 암호학적 난수로 시드된 코어별 ChaCha8Rand 상태를 유지함
- 각 코어당 상태 크기는 300바이트임
- 16코어 시스템에서는 단일 공유 Go 1 생성기 상태 4,872바이트와 비슷한 수준임
- 코어별 상태 덕분에 락 경합 없이 빠르게 난수를 만들 수 있음
math/rand/v2의 패키지 함수는 항상 ChaCha8Rand를 사용함- 예:
rand.N,rand.Float64
- 예:
math/rand의 패키지 함수는rand.Seed가 호출되지 않은 경우 ChaCha8Rand를 사용함- 예:
rand.Intn,rand.Float64 rand.Seed가 호출되면 호환성을 위해 Go 1 생성기로 되돌아가야 함
- 예:
- 런타임은 새 map의 해시 시드를 이전의 wyrand 기반 생성기 대신 ChaCha8Rand로 선택함
- 공격자가 map 구현의 특정 해시 함수를 알면 입력을 준비해 map을 이차 시간 동작으로 몰 수 있음
- 전역 시드 하나가 아니라 map별 시드를 쓰면 다른 퇴화 동작도 피할 수 있음
- map 시드에 암호학적 난수가 반드시 필요한지는 명확하지 않지만, 전환은 간단하고 신중한 선택이었음
- 별도 ChaCha8Rand 인스턴스가 필요한 코드는
rand.ChaCha8을 직접 만들 수 있음
보안 실수의 피해 축소
- Go는 보안 문제가 있는 흔한 실수를 줄이거나 제거해 기본적으로 안전한 코드 작성을 돕는 것을 목표로 함
- Go 1.20에서
math/rand의Read가 deprecated되자, 일부 개발자는 키 재료 생성처럼crypto/rand가 필요한 위치에math/rand를 쓰고 있었다는 사실을 발견함 - Go 1.20에서는 이런 실수가 심각한 보안 문제였음
- 키가 어디에 쓰였는지
- 키가 어떻게 노출됐는지
- 다른 난수 출력이 공격자에게 키 유도 단서를 줬는지 조사해야 했음
- Go 1.22에서는 같은 실수가 여전히 실수이지만 보안 참사로 이어질 가능성은 줄어듦
- 그래도 비밀값에는
crypto/rand를 쓰는 편이 더 나음- 운영체제 커널은 난수 값을 더 잘 보호할 수 있음
- 커널은 생성기에 새 엔트로피를 계속 추가함
- 커널 구현은 더 많은 검토를 받았음
암호처럼 보이지 않는 사례
- 무작위 UUID 생성은 UUID가 비밀값이 아니므로
math/rand로 충분해 보일 수 있음 - 하지만
math/rand를 현재 시간으로 시드하면, 서로 다른 컴퓨터가 같은 순간 실행될 때 같은 값을 만들 수 있음- 현재 시간이 밀리초 정밀도만 제공되는 시스템에서는 이 가능성이 더 커짐
- Go 1.20의 OS 엔트로피 기반 자동 시딩이 있어도, Go 1 생성기의 시드는 63비트 정수뿐임
- 시작 시 UUID를 만드는 프로그램은 가능한 첫 UUID가 2⁶³개로 제한됨
- 약 2³¹개 UUID 이후 충돌 가능성이 생김
- Go 1.22의 ChaCha8Rand는 256비트 엔트로피로 시드됨
- 가능한 첫 UUID는 2²⁵⁶개임
- 충돌 걱정이 필요 없음
- 프런트엔드 서버가 요청을 백엔드 서버에 무작위 배정하는 부하 분산도 예측 불가능한 난수가 필요할 수 있음
- 공격자가 배정을 관찰하고 예측 가능한 알고리듬을 안다면, 비싼 요청을 특정 백엔드에 몰 수 있음
- Go 1 생성기에서는 드물지만 가능한 문제임
- Go 1.22에서는 문제가 되지 않음
성능 특성
- ChaCha8Rand의 보안 이점에는 작은 비용이 있지만, 성능은 Go 1 생성기와 PCG와 같은 범위에 있음
- 비교 대상 연산은 두 가지임
Uint64: 난수 스트림에서 다음uint64반환N(1000):[0, 1000)범위의 난수 반환
- 64비트 x86 칩에서
GOARCH=386으로 빌드해 32비트 모드로 실행하면, PCG의 128비트 곱셈 때문에 PCG가 ChaCha8Rand보다 느림- ChaCha8Rand는 32비트 SIMD 산술을 사용함
- 일부 시스템에서는
Go 1: Uint64가PCG: Uint64보다 빠르지만,Go 1: N(1000)은PCG: N(1000)보다 느림- Go 1의
N(1000)은 범위 축소에 64비트 정수 나눗셈 두 번을 사용함 - PCG와 ChaCha8의
N(1000)은 대부분 나눗셈을 피하는 더 빠른math/rand/v2알고리듬을 사용함
- Go 1의
- 전체적으로 ChaCha8Rand는 Go 1 생성기보다 느리지만 2배 이상 느려지지는 않음
- 일반적인 서버에서는 차이가 3ns를 넘지 않으며, 이 차이가 병목이 되는 프로그램은 매우 적음
결론
- Go 1.22는 코드 변경 없이 프로그램의 보안을 높임
- 핵심 방식은
crypto/rand대신math/rand를 실수로 쓰는 흔한 문제를 줄이기 위해math/rand자체를 강화한 것임 - npm
keypair패키지처럼 Web Crypto API가 없을 때 JavaScriptMath.random으로 RSA 키 쌍 생성을 시도하는 사례도 있음 - 시스템 보안은 개발자가 실수하지 않는다는 전제에 의존할 수 없음
- Go 1.22의 ChaCha8Rand는 “수학적” 난수에도 암호학적으로 강한 의사난수 생성기를 쓰는 접근이 다른 생성기와 경쟁 가능한 성능을 낼 수 있음을 보여줌