2P by GN⁺ | ★ favorite | 댓글 1개
  • Go 1.22는 math/randmath/rand/v2의 기본 난수 소스를 암호학적으로 강한 생성기로 바꿔, crypto/rand를 써야 할 곳에서 실수했을 때의 피해를 크게 줄임
  • 기존 Go 1 생성기는 607개의 uint64 상태를 쓰는 선형 피드백 시프트 레지스터라, 607개 출력만 관측해도 과거·미래 값을 복원할 수 있음
  • math/rand/v2PCG-DXSM은 통계적 난수 품질과 상태 크기를 개선했지만, 비밀값에 필요한 예측 불가능성까지 보장하지는 못함
  • ChaCha8Rand는 32바이트 시드, 16블록마다 재키잉, 코어별 300바이트 상태를 사용하며 math/rand/v2, 일부 math/rand, map 해시 시드에 적용됨
  • 성능 비용은 제한적임: ChaCha8Rand는 Go 1 생성기보다 느리지만 최대 2배 이내이고, 일반 서버에서는 차이가 3ns를 넘지 않아 대부분의 프로그램에서 보안 이득이 더 큼

Go 1.22가 바꾼 난수 기본값

  • Go 1.22는 math/randmath/rand/v2에서 암호학적으로 강한 의사난수 생성기를 사용하도록 기본값을 바꿈
  • 목적은 crypto/rand가 필요한 곳에 개발자가 실수로 math/rand를 사용했을 때의 피해를 줄이는 데 있음
  • Go의 난수 API는 전통적으로 두 범주로 나뉨
    • math/rand: 시뮬레이션, 샘플링, 수치해석, 비암호학적 무작위 알고리듬, 퍼징, 셔플, 지수 백오프 등에 쓰는 통계적 난수
    • crypto/rand: 키·토큰처럼 예측 불가능성이 필요한 암호학적 난수

통계적 난수가 충분하지 않은 이유

  • 통계적 난수 생성기는 기본 통계 테스트를 통과하면 여러 비암호학적 용도에 충분할 수 있음
  • 그러나 알고리듬을 아는 관찰자가 충분한 출력을 보면 이후 시퀀스를 예측할 수 있는 경우가 많음
  • Unix V3의 srandrand는 이후 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
  • 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/randRead가 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: Uint64PCG: Uint64보다 빠르지만, Go 1: N(1000)PCG: N(1000)보다 느림
    • Go 1의 N(1000)은 범위 축소에 64비트 정수 나눗셈 두 번을 사용함
    • PCG와 ChaCha8의 N(1000)은 대부분 나눗셈을 피하는 더 빠른 math/rand/v2 알고리듬을 사용함
  • 전체적으로 ChaCha8Rand는 Go 1 생성기보다 느리지만 2배 이상 느려지지는 않음
  • 일반적인 서버에서는 차이가 3ns를 넘지 않으며, 이 차이가 병목이 되는 프로그램은 매우 적음

결론

  • Go 1.22는 코드 변경 없이 프로그램의 보안을 높임
  • 핵심 방식은 crypto/rand 대신 math/rand를 실수로 쓰는 흔한 문제를 줄이기 위해 math/rand 자체를 강화한 것임
  • npm keypair 패키지처럼 Web Crypto API가 없을 때 JavaScript Math.random으로 RSA 키 쌍 생성을 시도하는 사례도 있음
  • 시스템 보안은 개발자가 실수하지 않는다는 전제에 의존할 수 없음
  • Go 1.22의 ChaCha8Rand는 “수학적” 난수에도 암호학적으로 강한 의사난수 생성기를 쓰는 접근이 다른 생성기와 경쟁 가능한 성능을 낼 수 있음을 보여줌

댓글과 토론

Hacker News 의견들
  • 기사에 나온 내용처럼, rclone에서 정확히 이 실수를 저질렀음
    crypto/randRead를 쓰던 코드를 리팩터링하다가 import가 자동으로 바뀌었고, 아마 math/rand를 쓰는 코드와 섞이면서 goimportsmath/rand로 바꾼 듯함
    그 결과 보안 난수 생성기가 아니라, rclone이 시각으로 시드한 결정적 생성기를 쓰게 됐고 diff에서 알아채지 못했음 :-(
    https://www.cvedetails.com/cve/CVE-2020-28924/
    그래서 이번 변경에는 크게 찬성함

    • 아프네요, 미안합니다. 2016년에 goimportscrypto/rand를 선호하도록 바꿨기 때문에 리팩터링 중 무슨 일이 있었는지는 확실하지 않음
      아마 같은 파일에 math/rand 전용 API를 쓰는 코드가 들어갔을 수도 있음
      https://go-review.googlesource.com/24847
      어쨌든 이런 부분을 정리하게 되어 다행임
    • 나도 math/rand가 쓰였다고 착각해서 취약점을 제보받은 적이 있음. 실제로는 아니었고, 여러 파일을 헷갈린 것뿐이라 큰일은 아니었지만, 이 전체가 얼마나 혼란스러운지 보여줌
      text/templatehtml/template도 비슷함. 돌이켜보면 이런 패키지 이름 가리기는 나쁜 아이디어였음
    • "secure password generation golang"으로 검색해보면 거의 모든 예제가 math/rand를 쓰는 것도 봤음
      더 나쁘게는 전부 비밀번호를 만들기 직전에 현재 시각으로 시드를 초기화함
      우리 코드에서 누군가 math/rand를 쓴 걸 발견한 뒤, 어디서 복사했는지 궁금해서 찾아보다 알게 됐음
    • goimports는 거의 처음부터 math/rand.Readcrypto/rand.Read를 특별 처리했음
      다만 2016년의 https://github.com/golang/tools/commit/0835c735343e0d8e375f0...를 보면 "rand.Read""math/rand"로 해석될 수 있던 시기가 언급됨
      아마 그 시기에 걸렸던 것일 수도 있음
    • "PredictableRand" 같은 이름의 API 호출을 제공하는 게 그렇게 어렵지는 않을 것 같음
  • 지난주에도 spacey가 https://news.ycombinator.com/item?id=40237491에 올렸지만, 그 글은 https://news.ycombinator.com/item?id=40224864의 중복으로 잘못 묻힌 듯함
    두 go.dev 블로그 글은 같은 시리즈의 두 편이지만 꽤 다름. 이번 글은 효율적인 보안 난수 생성 알고리즘에 관한 것이고, 이전 글은 Go API 설계에 관한 글임

  • Russell Cox는 꾸준히 훌륭한 기술 블로그와 제안서, 그리고 작업물을 내놓음
    글쓰기와 사고의 명료함을 높이고 싶다면 Russell Cox부터 보는 게 좋은 출발점임

    • 그의 유한 상태 오토마타와 정규식 시리즈 덕분에 그 분야에 푹 빠졌음
      당시에는 Russ Cox가 누구인지도 몰랐지만, 그 글 시리즈는 정말 대단했음
      정규식 구현에 관해 무료로 볼 수 있는 자료 중 아마 최고 품질이고, 그다음은 여러 컴파일러 중심 책들이지만 무료도 아니고 웹에서 쉽게 검색하기도 어려움
    • 영상 데모도 잘 만듦 https://research.swtch.com/acme
  • crypto/rand가 반드시 필요한 곳에서 math/rand를 써버린 적이 있음
    그 결과 dnscrypt-proxy2 초기 버전에서 정적 키가 사용됐음
    원인은 import를 자동으로 추가하는 VSCode 확장 기능이었음. 보안 난수가 필요한 모든 소스 파일에는 crypto/rand를 직접 조심해서 import했지만, 파일 하나에서 빠뜨렸고, 모든 것이 컴파일되고 잘 동작했으며, 그 특정 파일에 확장이 조용히 math/rand import를 추가한 걸 알아채지 못했음
    그 뒤로는 잘못된 rand가 자동 import되는 걸 피하려고 crypto/randcryptorand라는 별칭으로 import함
    참고로 Zig도 ChaCha8 기반 난수 생성기를 쓰며, 암호 연산에서는 사용자가 자기 생성기를 공급할 수 없고 항상 안전한 생성기가 사용됨. 테스트용으로는 일부 함수가 명시적 시드를 받음
    제약 환경을 위해 표준 라이브러리에는 Ascon 순열과 Reverie 구성에 기반한 더 작은 생성기도 들어 있음

    • 당신의 경우 정확히 무슨 일이 있었는지는 모르겠지만, 아마 설명한 것과는 달랐을 가능성이 큼
      2016년에 goimportsmath/rand보다 crypto/rand를 선호하도록 바꿨고(https://go-review.googlesource.com/24847), 그때는 Go용 VSCode 지원이 나오기 전이었음
    • 다른 사람도 같은 얘기를 했음. 솔직히 import를 자동으로 추가하는 관행은 완전히 이상해 보이고, 이름을 서로 다른 이름공간으로 분리하는 목적 자체를 무너뜨림
  • 2020년대에도 여러 프로그래밍 언어의 기본 난수 구현이 왜 LFSR, MT 같은 빠른 난수 생성기를 쓰는지 자주 생각했음
    사람들이 의사 난수 생성기가 필요한지 암호학적으로 안전한 의사 난수 생성기가 필요한지 모른다고 보는 쪽으로 보수적으로 잡고, 기본값을 후자로 바꾸며 전자가 필요한 사람만 명시적으로 선택하게 하는 편이 더 좋아 보임

    • PHP 8.2의 새 객체지향 난수 API에서는 정확히 그렇게 했음
      개발자가 사용할 난수 엔진을 명시적으로 고르지 않으면 암호학적으로 안전한 생성기를 받게 됨
      이제 어려운 부분은 사람들을 새 API로 옮기도록 설득하는 것임. 더 나아가 전역 Mt19937 인스턴스를 쓰는 mt_rand()에서, PHP 7.0부터 이미 제공되는 CSPRNG 기반 random_int()로 옮기는 것조차 쉽지 않음
      [1] https://www.php.net/releases/8.2/en.php#random_extension
    • 최근 복잡한 자료 구조의 여러 구성요소에 임의 ID를 생성하는 새 Go 라이브러리를 쓰기 시작했음
      내 사용 사례에는 구성요소가 수만 개 있었고, 프로파일링해보니 자료 구조 초기화 시간의 상당 부분이 crypto/randRead()에 쓰였으며, 내 MacBook에서는 시스템 호출을 실행하고 있었음
      라이브러리를 패치해 math/randRead()를 쓰게 하자 성능이 크게 좋아졌음
      math/rand가 더 빠른 것 외에도, 별 이유 없이 시스템의 엔트로피 풀을 고갈시킬까 걱정됐음. 이 경우 ID가 임의적이어야 할 유일한 이유는 자료 구조를 직렬화/역직렬화한 뒤 나중에 구성요소를 더 추가하는 것뿐인데, 나는 그럴 생각이 없었음
      이 블로그에 나온 변경 시점이 내 경험과 정확히 어떻게 맞물리는지는 모르겠음. 아마 내가 예전 버전 라이브러리를 썼고, 지금은 crypto/rand가 사실상 math/rand와 구분 안 될 정도라면, 뭐 좋다고 봄 :-)
    • CSPRNG, 여기서는 ChaCha8을 쓰는 더 좋은 논거 중 하나는 벤치마크에서 PCG 대비 2배 이내라는 점임
      상태 크기는 여전히 비교적 큼(64바이트 대 16바이트)이지만, mt19937이나 예전 Go PRNG 같은 것보다는 훨씬 낫음
      CSPRNG가 훨씬 느리다면, 축소 라운드 ChaCha 변형이 아닌 일반 CSPRNG에서는 대체로 그렇듯, 기본값으로서 매력이 줄어듦
    • 전자가 필요한 다른 경우가 뭐가 있을까? 재현 가능한 결과가 필요한 고정 시드 정도만 떠오름. 예를 들면 테스트나 검증 같은 것
      시드가 필요 없는데도 사람들이 PRNG 쪽으로 밀리는 작은 요인이 하나 더 있음. CSPRNG API에는 시스템 호출 실패나 엔트로피 부족에 대비해 항상 처리해야 할 오류가 들어감
      crypto/rand 읽기는 실제로 얼마나 자주 실패할까? 현대 시스템에서 엔트로피를 고갈시키려면 얼마나 많이 읽어야 할까? 수십억 요청에서도 실패를 본 적이 없고, dd도 잘 됨
      대부분의 사용 사례에는 Must/panic 스타일 API가 기본값으로 맞을지도 궁금함
      덧붙여 Python의 secrets 패키지(https://docs.python.org/3/library/secrets.html)를 봤는데, 예외를 던질 수 있다는 언급이 하나도 없음. 실무에서는 그냥 안 일어나는 일인가?
    • “시스템의 모든 난수는 명시적으로 빠져나가지 않는 한 CSPRNG에서 와야 한다”는 접근이 마음에 듦
      성능을 조금 잃는 대신 잘못된 난수 생성기를 써서 재앙을 일으키지 않는다는 훨씬 강한 보장을 얻는 쪽임
      거의 모든 언어에서 개발자가 아직도 이 날카로운 모서리를 신경 써야 한다는 게 아쉬움
  • 모르는 사람을 위해 덧붙이면, gosec와 그 확장인 golangci-lintmath/rand 사용을 경고해 줌
    https://github.com/securego/gosec/blob/d3b2359ae29fe344f4df5...

    • math/rand/v2에서 가장 마음에 드는 점 중 하나는 회사에서 nolint 지시어와 그 뒤의 PR 토론 없이 쓸 수 있다는 것임
  • 보안과 새 v2 옵션에 대한 권고를 아직 해석 중임
    블로그 글은 “비밀값에는 다른 것이 필요하다” 같은 문장을 쓰고, 이어서 암호학적 난수성, ChaCha8, 시스템 난수로 시드되는 방식까지 자세히 다뤄서 매우 “안전”하다는 인상을 줌
    그런데 패키지 문서에는 이렇게 되어 있음
    ... but it should not be used for security-sensitive work ... This package's outputs might be easily predictable regardless of how it's seeded. For random numbers suitable for security-sensitive work, see the crypto/rand package.
    그렇다면 왜 블로그 글에서 math/rand/v2를 “비밀값”에 쓰는 듯한 암시를 주는 걸까?
    짧게 말하면 민감한 것은 여전히 모두 crypto/rand를 써야 하고, 여기 설명된 개선은 누군가 math/rand/v2를 부적절하게 썼을 때의 안전망이라는 뜻인가?

    • 맞음. math/rand/v2가 최적은 아니지만, 더 이상 crypto/rand를 써야 할 곳에 실수로 썼다고 즉시 치명적인 보안 결함이 되지는 않음
      글에도 이렇게 나옴
      crypto/rand를 쓰는 편이 여전히 더 낫다. 운영체제 커널은 여러 엿보기 공격으로부터 난수 값을 비밀로 유지하는 일을 더 잘할 수 있고, 커널은 생성기에 계속 새 엔트로피를 더하며, 더 많은 검토를 받아왔기 때문임. 하지만 실수로 math/rand를 쓰는 일이 더 이상 보안 재앙은 아님
  • 최악의 벤치마크에서도 새 전략은 안전하지 않은 난수 생성기보다 대략 절반 정도 느릴 뿐이고, 대부분의 벤치마크는 훨씬 더 가까웠음
    Go는 표준 라이브러리와 그 위에 만들어지는 앱을 위해 안전성과 성능의 균형을 잘 잡고 있음. 다른 생태계도 따라오면 좋겠음
    애플리케이션에 빠르고 안전하지 않은 난수가 필요하다면 내부 생성기를 직접 구현해야 함
    손쉽게 닿는 곳에 안전하지 않은 난수를 두는 것은 치워둘 수 있는 발등 찍는 도구

    • 솔직히 이건 더 나빠 보임
      사람들이 "random" 원시 기능이 암호학적으로 안전하다고 가정하도록 부추기는 건 나쁜 관행을 장려하는 것임
      math/rand/v2를 암호학적으로 안전하게 만드는 것이 한 문제를 해결할 수는 있지만, 이제 보안을 약속하는 것처럼 보이지 않는 것이 “괜찮은” 상태가 됨
      일반적으로 math/rand 함수에는 암호학적으로 안전하다는 관례가 없음. 그것을 바꿔서 나쁜 코드가 우연히 올바르게 동작하게 만들면, 우리가 이런 명백한 실수를 하고 있다면 다른 어떤 실수도 하고 있는지 가려질 수 있음
  • Go 1의 math/rand는 더 정확히는 가산 지연 피보나치 생성기라고 부르는 편이 맞음
    최초 발표는 Green, Smith, Klem의 논문임
    [1] https://doi.org/10.1145/320998.321006

    • 그 논문에는 “지연” 부분이 언급되지 않는 것 같음. 아니면 내가 놓쳤을 수도 있음
      https://www.leviathansecurity.com/blog/attacking-gos-lagged-...도 알고 있고, 여기서도 지연 피보나치 생성기라고 부름
      Rob Pike와 나는 몇 달 전 Go 1 생성기의 원래 C 버전을 작성한 Don Mitchell과 메일을 주고받으며 그가 이 알고리즘을 어떻게 설명할지 물었고, 그는 “기억하기로 Jim과 나는 Marsaglia의 LFSR 비슷한 생성기를 구현했다”고 답했음
      두 설명, 즉 지연 피보나치와 LFSR 유사 생성기는 서로 다른 관점에서 모두 정확하다고 봄. 어느 쪽도 괜찮지만, 글에서는 원저자의 설명을 쓰기로 했음
  • 작은 흠을 하나 꼽자면, 여기서는 통계적 무작위성의사 난수 생성기가 섞여 쓰인 것 같음
    위키의 통계적 무작위성 정의는 “숫자열에 알아볼 수 있는 패턴이나 규칙성이 없을 때 통계적으로 무작위라고 한다”임
    이 정의가 진정한 난수 생성기(TRNG)에도 적용될까? 적용되기를 바라야 함. 적어도 장기적으로나 극한에서는 그래야 함. 그렇지 않다면 TRNG가 아님
    TRNG는 장기적으로 “알아볼 수 있는 패턴이나 규칙성이 없는 숫자열”을 생성해야 함
    따라서 통계적 무작위성이 PRNG를 뜻하지는 않지만, TRNG에도 적용될 수 있다고 말할 수 있음
    문제는 PRNG가 제한된 형태의 통계적 무작위성을 갖는지 검증하기 위한 통계적 무작위성 테스트가 많이 있다는 데서 오는 듯함
    그래서 PRNG를 식별하려면 “통계적 무작위성”보다 “의사 난수 생성기”라는 표현이 더 적절했을 것 같음. 그래도 아주 작은 흠임