- Go 1.22では、既存の
math/rand パッケージと新たに導入された math/rand/v2 パッケージの両方で、暗号学的に安全な乱数生成器を使うように変更された。これにより、より優れたランダム性を提供し、開発者が誤って crypto/rand の代わりに math/rand を使ってしまったときに生じうる被害を大幅に減らせるようになった。
統計的ランダム性と暗号学的ランダム性の違い
- 統計的ランダム性は、シミュレーション、サンプリング、数値解析、非暗号用途のランダムアルゴリズム、ランダムテスト、入力のシャッフル、ランダムな指数バックオフなどに適している。
- ごく基本的で計算しやすい数学的な式でも、こうした用途には十分うまく機能する。だが、使用アルゴリズムを知る観測者は、ある程度の値を見れば今後の系列を予測できる。
- 暗号学的ランダム性は、どれだけ過去に生成された値を観測していても、実際には完全に予測不能でなければならない。
- 安全な暗号プロトコル、秘密鍵、現代の商取引、オンラインプライバシーなどは、暗号学的ランダム性に大きく依存している。
Go 1 の math/rand 生成器
- Linear-feedback shift register(LFSR)方式を使用する。
- 内部状態が 607 個の
uint64 からなるベクトルとして完全に露出してしまう問題がある。
- 生成器から 607 個の値を読み取ると、すべての状態が露出し、その後の値を予測できる。
math/rand/v2 の PCG 生成器
- Melissa O'Neill の PCG アルゴリズムを使用。128 ビット LCG に後処理を適用したもの。
- 全状態は 128 ビットの数値 1 つで、更新は 128 ビットの乗算と加算で行われる。
- Go では O'Neill の提案に従い、XOR ベースではなく乗算ベースの scramble 関数を使って、ビットをより積極的に混ぜる。
- Go 1 の生成器より計算量は多いが、状態保存に必要なメモリははるかに少なく、初期状態値への感度も低く、他の生成器が通過できない統計テストにも合格する。
- ただし、PCG も依然として予測不能ではない。
暗号学的ランダム性
- 最終的には、OS が物理デバイスのノイズから真のランダム性を収集する必要がある。
- 十分なランダム性(256 ビット以上)を収集できれば、暗号学的ハッシュや暗号アルゴリズムでそれを引き伸ばし、任意長の乱数列を作れる。
- Go の
crypto/rand パッケージは、こうした OS インターフェースの違いを抽象化し、rand.Read という同一インターフェースを提供する。
ChaCha8Rand 生成器
- DJB の ChaCha ストリーム暗号を変形して作られた新しい生成器。
- 8 ラウンド版の ChaCha8 を使用する。ChaCha20 より 2.5 倍高速でありながら安全。
- 32 バイトのシードを ChaCha8 のキーとして使用。16 ブロックごとに、生成されたブロックの後ろ 32 バイトを次の 16 ブロックのキーとして使い、前方秘匿性を提供する。
math/rand/v2 の rand.Float64、rand.N などは常にこの生成器を使う。
math/rand もこの生成器を使う。ただし、rand.Seed が呼ばれた場合は Go 1 の生成器を使う。
- ランタイムも、新しいマップのハッシュシードを選ぶ際に ChaCha8Rand を使う。
セキュリティ上のミスへの対処
- Go 1.22 は
math/rand を強化することで、コード変更なしでもプログラムをより安全にする。
- たとえば
math/rand の Read を誤用して鍵生成などに使った場合、Go 1.20 では深刻なセキュリティ問題だったが、Go 1.22 では単なるミスで済む。
- UUID 生成やフロントエンドサーバーの負荷分散など、一見「暗号」とは見えない用途でも、ChaCha8Rand を使えば Go 1 の生成器よりはるかに堅牢になる。
性能
- ChaCha8Rand は Go 1 の生成器や PCG と同程度の性能を示す。
- 32 ビットコードでは、128 ビット乗算が必要な PCG より ChaCha8Rand の方が速い。
- 64 ビット除算を避ける
math/rand/v2 のアルゴリズムのおかげで、N(1000) 演算では Go 1 の生成器より ChaCha8Rand や PCG の方が速い場合もある。
- 全体として ChaCha8Rand は Go 1 の生成器より遅いが、2 倍以上遅くなることはなく、一般的なサーバーでは差が 3ns を超えない。
GN⁺の見解
- Go 1.22 での ChaCha8Rand の採用は、セキュリティを大きく高めつつ性能低下を最小限に抑えた、言語レベルの模範的な改善事例と言える。開発者が頻繁に犯すミスを、言語レベルで根本的に防いだ点が印象的だ。
- 本文でも触れられているように、この種のミスは Go に限らず他の言語でもよく見られる。開発者のミスにシステムの安全性が左右されるべきではないため、他の言語も Go のように「数学的」乱数生成に対しても暗号学的に強力な疑似乱数生成器を使う方向へ進むべきだろう。
- ただし ChaCha8Rand は、
crypto_box や xchacha20poly1305 のような暗号プリミティブに使うには不適切である。こうした用途では、引き続き crypto/rand を直接使う必要がある。
- Go ランタイムがマップのハッシュシード選択にも ChaCha8Rand を使うよう変更したのは、やや意外だった。ハッシュシードに暗号学的乱数が必須かは明確ではないが、厄介な攻撃可能性を根本から封じようとする開発チームのセキュリティ意識が際立つ。
- 言語標準の基本パッケージである
math/rand の品質が向上したぶん、今後はアプリケーションで math/rand を直接使う場面も増えそうだ。これまで math/rand の予測可能性のために別の乱数生成ライブラリを使っていたプロジェクトであれば、今回の変更の恩恵を受けられるだろう。
1件のコメント
Hacker News のコメント
要約すると次のとおり:
math/randパッケージのRead関数が deprecated になったことで、これをcrypto/randの代わりに誤って使っていた事例が見つかった。これは、セキュリティに脆弱な決定論的乱数生成器を使ってしまうミスにつながる。gosecやgolangci-lintのような静的解析ツールは、math/randの使用に対して警告を出す。math/rand/v2パッケージは ChaCha8 暗号を使用し、システムエントロピーでシードされるため「安全そうな」印象を与えるが、それでもセキュリティに敏感な処理には不適切である。この場合はcrypto/randを使うべきである。math/randは、正確には additive lagged Fibonacci generator と見なせる。math/randは、最悪の場合でも従来の非安全な乱数生成器の半分程度の速度を示し、ほとんどのベンチマークでは差がほぼなかった。Go は標準ライブラリにおいて、安全性と性能の適切なバランスを取っている。java.util.Randomのような失敗を防ぐ、開発者フレンドリーなアプローチとして評価されている。