乱数 API が提供する契約には、異なる 2 種類があります。決定論的な疑似乱数生成器は、再現可能なテストやシミュレーションに役立ちます。セキュリティー上重要なキー、ノンス、トークン、ソルト、およびシードには、カーネルの暗号論的擬似乱数生成器(CSPRNG)から得たバイト列が必要です。
C ライブラリの生成器 #
rand() はプロセス内の隠れた状態を進め、0 から RAND_MAX までの整数を返します。srand(seed) は、特定の C ライブラリ実装における再現可能なシーケンスを選択します。省略した場合の動作は、シードに 1 を指定した場合と同じです。ISO C はアルゴリズムを規定しておらず、実装間で同じシーケンスになることも要求していません。したがって、クロスプラットフォームでの再現性が必要なテストでは、明示的なアルゴリズムを選択して記録する必要があります。
random() と srandom() は、別の決定論的な関数群を提供します。glibc は加算フィードバック方式の状態を使用して random() を実装しており、呼び出し元は initstate() と setstate() で状態配列を切り替えられます。random_r() では、その状態が明示的になります。rand_r() が保持する状態は unsigned int 1 個だけであり、POSIX では廃止予定です。そのため、独立した高品質のストリームを生成する設計には適していません。
drand48() 関数群では、48 ビットの線形合同漸化式が規定されています。
X(n + 1) = (0x5DEECE66D * X(n) + 0xB) mod 2^48drand48() は [0.0, 1.0) の範囲の double を返し、lrand48() は [0, 2^31) の範囲の long を返し、mrand48() は [-2^31, 2^31) の範囲の符号付きの値を返します。erand48()、nrand48()、jrand48() は、呼び出し元が提供する 3 要素の状態を使用します。_r インターフェースは、明示的なリエントラント状態構造体を使用します。
攻撃者が十分な状態または出力を把握すると、上記の関数群はいずれも予測可能になります。現在時刻、PID、またはカーネルから得た 1 個の乱数値をシードとしても、それ以降の出力が暗号論的乱数生成器によるものになるわけではありません。
カーネルのエントロピーと CSPRNG #
Linux はタイミング情報やデバイスデータを入力プールに混合し、初期化中に予測不能な寄与分を保守的にエントロピーとして加算します。十分なエントロピーが加算されると、カーネルはプールの状態から CSPRNG キーを確定します。現在のアップストリームコードは ChaCha ベースの高速鍵消去型生成器を使用し、そのキーをプールから定期的に更新します。アプリケーションが消費するのは CSPRNG の出力であり、出力 1 バイトごとに生のハードウェアノイズを 1 バイト消費するわけではありません。
getrandom(2) はパス名もファイルディスクリプターも必要としないため、Linux の主要なインターフェースです。
getrandom(buf, len, 0)は CSPRNG が初期化されるまで待機します。GRND_NONBLOCKは、起動直後の初期化前の状態で待機する代わりにEAGAINを返します。- 初期化済みの urandom ソースに対する 256 バイト以下の要求は、要求された長さを返し、シグナルによって中断されません。より大きな要求では、短い結果を返したり
EINTRで失敗したりする可能性があります。 getentropy(3)は 256 バイトに制限された libc インターフェースです。バッファー全体を埋めた場合にのみ 0 を返し、それ以外の場合は-1を返します。
GRND_RANDOM は従来の互換性フラグであり、セキュリティーを強化するモードではありません。Linux 5.6 より前では、random ソースからの読み取りはエントロピー推定値に応じてブロックする可能性がありました。現在のアップストリームの random.c は /dev/random を getrandom(..., 0) と同様に扱い、初期化までの間だけブロックします。古いカーネルでは以前の動作が維持されます。移植可能な新規コードではフラグ 0 を使用してください。
デバイスのセマンティクスと起動直後 #
/dev/urandom は初期化を待機しません。現在のカーネルソースでは GRND_INSECURE と同等に扱われているため、起動後の極めて早い段階では、CSPRNG に安全なシードが設定される前に読み取られる可能性があります。getrandom(..., 0) は初期化を待機するため、この競合を回避できます。初期化後は、/dev/urandom、/dev/random、および通常の getrandom の出力はいずれもカーネルの CSPRNG から生成されます。エントロピーカウンターが再充填されるまで待機しても、生成されるキーの品質は向上しません。
Linux 5.6 以降、/dev/random は初期化後にブロックしなくなり、その O_NONBLOCK 設定が影響するのは初期化が未完了の間だけです。古いカーネル上でもソフトウェアを実行する必要がある場合、歴史的な相違は依然として重要です。いずれかのデバイスへの書き込みによってデータはプールに混合されますが、特権を必要とする乱数デバイス ioctl で明示的に指定しない限り、エントロピーとしては加算されません。
安全なキー生成 #
エラーと長さを必ず確認してください。後からバッファーが小規模要求の保証範囲を超えるサイズに拡大されても、ループを使用すれば正しく動作します。
#include <errno.h>
#include <stddef.h>
#include <sys/random.h>
static int
random_bytes(void *buffer, size_t length)
{
unsigned char *p = buffer;
while (length != 0) {
ssize_t n = getrandom(p, length, 0);
if (n > 0) {
p += (size_t)n;
length -= (size_t)n;
continue;
}
if (n < 0 && errno == EINTR)
continue;
return -1;
}
return 0;
}
unsigned char key[32];
if (random_bytes(key, sizeof key) < 0) {
/* Abort key creation; never continue with an unfilled key. */
}キーのバイト列は、rand() を介さずに直接生成してください。上限未満の偏りのない整数を得るには、棄却サンプリングを使用します。ソースの範囲が上限の正確な倍数でない限り、value % bound には偏りがあります。生成後も、秘密情報の有効期間、安全な消去、ノンスの一意性、およびファイルのパーミッションは、それぞれ別個に対応すべき事項です。
アップストリームの getrandom(2) および random(4) のページでは、ユーザー空間 ABI を定義しています。現在の実装の詳細と 5.6 以降におけるデバイスの等価性は、カーネルの drivers/char/random.c ソースに記載されています。
コメント
GitHub で閲覧・コメントする(投稿にはログインが必要です)