組み込みでの乱数生成

通信リトライのランダムバックオフ、テスト用のデータ生成、そして暗号鍵やトークンの生成。乱数の用途は幅広いのに、「とりあえずrand()」で済ませてしまう実装が後を絶ちません。本記事では、乱数の2つの種類(PRNGとTRNG)の違いを整理し、rand()を使ってはいけない理由、組み込み特有のシード問題、用途別の正しい選び方を解説します。

乱数は2種類ある:PRNGとTRNG

  • PRNG(疑似乱数生成器):計算アルゴリズムで乱数「らしい」数列を作る方式です。初期値(シード)が同じなら毎回同じ数列になります(再現可能)。高速で、ソフトウェアだけで動きます。
  • TRNG(真性乱数生成器):回路の熱雑音などの物理現象を源にした、予測も再現もできない乱数。専用ハードウェアが必要で、近年のマイコンには内蔵品が増えています。

「再現できる」ことはテストでは長所ですが、鍵・トークン生成などでは重大なリスクになります。この性質の違いが、用途別の使い分けの出発点です。

rand()を使ってはいけない理由

C標準ライブラリのrand()は、CERT C Secure Coding Standard(JPCERT/CCが日本語版を公開しています)の MSC30-C(「疑似乱数の生成にrand()関数を使用しない」)で使用を避けるべき関数とされています。理由は、生成される数列が予測可能で、乱数としての品質も処理系任せで保証がないためです。C標準はrand()の生成アルゴリズムの品質を規定しておらず、RAND_MAXも実装依存です(実装によっては32767ということもあります)。攻撃者に次の値を予測されては困る用途(鍵・トークン・認証。セッションID、パスワードリセットトークン、認証用ノンスなどが該当し、予測されると乗っ取りにつながります)には適していません。また、品質が求められる統計的用途にも推奨できません。

さらに同標準(JPCERT/CCが日本語版を公開しています)のMSC32-C(「乱数生成器には適切なシード値を与える」)が示すとおり、シードを与えないPRNGは呼び出しのたびに同じ数列を生成します。なお同ルールには、物理現象を使うTRNGはそもそもシードを必要としない(再現性を与えるという意味でのシードは、設定できる仕様になっていない)ことも明記されています。ただし、TRNGなら何も設定しなくてよいという意味ではありません。初期化、自己診断、後段のコンディショニング(偏り除去の後処理、いわゆるホワイトニング)やDRBGとの一体化は実装によって異なるため、使用するマイコンのリファレンスに従って初期化し、エラー時の扱いもあわせて設計してください。

組み込み特有の「シード問題」

PCならOSが提供するエントロピー源(Linuxの/dev/urandom、WindowsのCSPRNG API(例:BCryptGenRandom)など)を利用できますが、組み込み機器は事情が違います。なおtime(NULL)は秒単位の解像度しかなく、攻撃者にとって予測が容易な低エントロピー値です。PCであっても、セキュリティ用途のシード源としては推奨されません(低エントロピーで推測されやすい)。非セキュリティ用途でも、単独のシード源にするのは避け、複数ソースを混ぜるのが安全です。電源投入直後の状態が毎回ほぼ同じで、時計(RTC)を持たない機器も多いためです。結果として、電源を入れるたびに同じ乱数列で動き始める機器が簡単に構築されてしまいます。複数台が同時に起動し、全台が同じタイミングで再送をぶつけ合う、といった事故はこの典型です。組み込みでのシードのエントロピー源には次のようなものが使われます。

  • ハードウェアTRNG:あればこれが本命。PRNGのシードにも、乱数そのものにも使えます。
  • ADCの最下位ビット(LSB):開放端子や温度センサのノイズを複数回読んで混ぜます。
  • 発振器のジッタ:独立したクロック源同士のずれを計数します。
  • ユーザ操作や通信のタイミング:ボタン押下時刻のタイマ値などの外部イベントの揺らぎを使います。

いずれも単独では偏りがあり得るため、複数の源を混ぜ合わせて使うのが安全側の設計です。ADCや外部イベント由来の揺らぎは、環境や攻撃者の影響で偏り/相関が出ることがあります。可能ならTRNGの自己診断付き出力を用い、複数ソース混合+健全性チェック(health test)を設計に含めてください。

用途別の正しい選び方

用途 要求 適した方式
再送バックオフ、テストデータ、演出など 衝突しにくい分散(同時起動でも偏らない)と実行速度 軽量PRNG(xorshift32など)+適切なシード
暗号鍵、ノンス、トークン、チャレンジ 予測不可能性が必須 TRNG、またはTRNGをシードにしたCSPRNG(暗号論的疑似乱数)

セキュリティ用途では、自作PRNGで対処しようとせず、ハードウェアTRNGか、暗号ライブラリが提供するCSPRNG(暗号論的疑似乱数生成器)を使う必要があります。CSPRNGの代表的な実装規格として、NIST SP 800-90Aに定めるDRBG(Deterministic Random Bit Generator)があります。TLSスタックを使う構成なら、そのエントロピー源の初期化を正しくおこなうことがそのまま通信の安全性につながります。非セキュリティ用途には、次のような軽量PRNGで十分です。ただしxorshift(この記事では32ビット版のxorshift32を使用)は暗号用途には適しません。識別子・トークン・認証に関わる処理へ転用しないでください。また、次の例は状態をグローバル変数で持つ最小構成です。割り込みやマルチタスクから呼ぶ場合は排他制御するか、状態を呼び出し元に持たせる形(uint32_t prng_next(uint32_t *state))にして再入可能にしてください。

/* 非セキュリティ用途向けの軽量PRNG(xorshift32) */
static uint32_t prng_state = 1u;          /* 0以外で初期化すること */
void prng_seed(uint32_t seed)
{
    prng_state = (seed != 0u) ? seed : 1u;
}
uint32_t prng_next(void)
{
    uint32_t x = prng_state;
    x ^= x << 13;
    x ^= x >> 17;
    x ^= x << 5;
    prng_state = x;
    return x;
}

ハードウェアTRNGを使うときの注意

  • エラー処理を省かない:TRNGには故障や偏りを検出する自己診断が付いていることが多く、エラー時の値を破棄してリトライする処理が必須です。
  • 生成速度に注意:TRNGは一般にPRNGより低速です。大量に必要なら、TRNGで初期化したCSPRNGから取り出す構成にします。
  • 出荷テストに検査を組み込む:起動ごとに最初の乱数をログに記録し、毎回同じ値が出ていないかを確認するだけでもシード事故を防げます。

ありがちな失敗

  • シード固定のまま量産:全台が同じ「乱数」で動く。バックオフの一斉衝突、予測可能なID生成につながります。
  • シリアル番号やMACアドレスをそのまま鍵やトークンに使う:一意ではあっても秘密ではありません。
  • rand() % Nによる範囲生成の偏り:RAND_MAX+1がNで割り切れないと、一部の値が他より多く出現します。広い乱数からのスケーリングや棄却法で範囲を作ります。例:上限を M = ⌊(RAND_MAX+1)/N⌋ × N とし、rand() が M−1 以下のときのみ採用する(棄却法)。

まとめ

乱数設計の要点は「セキュリティ用途かどうかで方式を分ける」「PRNGはシードがすべて」「rand()は使わない」の3つです。特にシード問題は組み込み特有の落とし穴なので、自分の機器のエントロピー源がどこにあるかを一度言語化しておくとよいでしょう。なお「非セキュリティ用途」であっても、後の仕様変更でトークンや識別子に流用されることがあります。用途が拡大しない前提を置けない場合は、最初からCSPRNGを選ぶのが安全です。

組み込みソフトの世界 トップへ戻る