Arduinoで真に乱数を取得する


13

Arduinoで真の(擬似ではなく)乱数を取得するための最良の方法、または少なくとも可能な限り最良の近似は何ですか?私の理解では、関数randomSeed(analogRead(x))は十分にランダムではありません。

可能であれば、メソッドは基本的なArduinoセットアップのみを活用する必要があります(追加のセンサーはありません)。外部センサーを使用したソリューションは、基本的なセットアップよりもランダム性が大幅に改善される場合に歓迎されます。


アプリケーションは何ですか?暗号的に安全である必要がありますか?そのとき、あなたはランダムネスで何をしているのですか?次に、物理エントロピーソースからTRNGを実装する外部チップがないと、運が悪くなります。HMAC DRBGのような決定論的なRNGを実装し、静的なものと低品質のエントロピーソースからシードすることもできますが、それでも暗号的に安全ではありません。
マクシミリアンゲルハルト

はい、暗号で保護されたアプリケーションには乱数が必要です。
レクシラス

回答:


10

エントロピーライブラリの使用:

真の乱数の信頼できるストリームを生成するウォッチドッグタイマーの自然なジッター

マイクロコントローラのピンを使い果たしず、外部回路を必要としないため、このソリューションが気に入っています。また、これにより、外部障害の影響を受けにくくなります。

ライブラリに加えて、ライブラリなしでマイクロコントローラのPRNGのランダムシードを生成するために使用されるのと同じ手法の使用を示すスケッチも提供します:https : //sites.google.com/site/astudyofentropy/project-definition / timer-jitter-entropy-sources / entropy-library / arduino-random-seed


8

randomSeed(analogRead(x))は、255の数値シーケンスのみを生成します。これにより、すべてのコンボを試行し、出力ストリームに結合できるオラクルを生成して、すべての出力を100%予測するのは簡単です。あなたは正しい軌道に乗っていますが、それは単なる数字のゲームであり、もっとたくさん必要です。たとえば、4つのADCから100のアナログ読み取り値を取得し、それらをすべて加算して、それを供給するrandomSeed方がはるかに良いでしょう。最大限のセキュリティを確保するには、予測不能な入力と非決定的な混合の両方が必要です。

私は暗号作成者ではありませんが、ハードウェアとソフトウェアのランダムジェネレーターの研究と構築に何千時間も費やしてきたので、学んだことを共有しましょう。

予測不能な入力:

  • analogRead()(フローティングピン上)
  • GetTemp()

潜在的に予測不可能な入力:

  • micros()(非決定的なサンプル期間付き)
  • クロックジッター(低帯域幅、使用可能)
  • readVCC()(バッテリー駆動でない場合)

外部予測不能入力:

  • 温度、湿度、および圧力センサー
  • マイク
  • LDR分圧器
  • 逆バイアストランジスタノイズ
  • コンパス/加速ジッタ
  • esp8266 wifiホットスポットスキャン(ssid、dbなど)
  • esp8266タイミング(バックグラウンドWiFiタスクにより、スケジュールされたmicros()フェッチが不確定になります)
  • esp8266 HWRNG--非常にRANDOM_REG32高速で予測不可能なワンストップ

収集 最後に行うことは、エントロピーをそのまま吐き出すことです。コインのバケツよりもコインの反転を推測する方が簡単です。合計は良いです。unsigned long bank;その後bank+= thisSample;は良いです。ロールオーバーします。bank[32]さらに良いです、読んでください。出力のチャンクごとに、少なくとも8個の入力サンプルを収集するのが理想的です。

ポイズニングに対する保護 ボードを加熱すると、特定の最大クロックジッターが発生する場合、それは攻撃ベクトルです。analogRead()入力に向けてRFIをブラストするのと同じです。別の一般的な攻撃は、ユニットを単に抜いて、蓄積されたすべてのエントロピーをダンプします。速度を犠牲にしても、安全であるとわかるまでは数値を出力しないでください。

EEPROM、SDなどを使用して、長期にわたってエントロピーを維持したいのはこのためです。32バンクを使用するFortuna PRNGを調べてください。各バンクは前のバンクの半分の頻度で更新されます。そのため、妥当な時間内に32銀行すべてを攻撃することは困難です。

処理 「エントロピー」を収集したら、それをクリーンアップし、逆戻りが困難な方法で入力から離す必要があります。SHA / 1/256はこれに適しています。プレーンテキストの脆弱性がないため、速度を上げるためにSHA1(または実際にはMD5でも)を使用できます。収穫するには、完全なエントピーバンクを使用しないでください。output = sha1( String(micros()) + String(bank[0]) + [...] );また、エントロピーバンクの変更がない場合、常に異なる出力に「塩」を追加して、同一の出力を防止します。低蓄積された耳鼻咽喉科、およびその他の一般的な問題。

タイマー入力を使用するには、それらを不確定にする必要があります。これは単純delayMicroseconds(lastSample % 255)です; 予測できない時間を一時停止し、「連続した」クロック読み取りの差を不均一にします。if(analogRead(A1)>200){...}A1がうるさい場合、または動的入力にフックされている場合、のように半規則的に行います。フローの各分岐を判別するのがかなり困難になると、逆コンパイル/リッピングされた出力での暗号分析を防ぐことができます。

本当のセキュリティとは、攻撃者がシステム全体を知っていて、それを克服するのに無力な場合です。

最後に、作業内容を確認してください。ENT.EXE(nix / macでも利用可能)を使用して出力を実行し、適切かどうかを確認します。最も重要なのは、カイ二乗分布で、通常は33%〜66%のはずです。1.43%、99.999%、またはそのようなエッジの効いた、連続した複数のテストを取得した場合、ランダムはくだらないです。また、エントロピーENTレポートを1バイトあたり可能な限り8ビットに近いものにする必要があります。

TLDR:最も簡単なフールプルーフ方法は、ESP8266のHWRNGを使用することです。速く、均一で、予測不可能です。Ardunioコアを実行しているESP8266で次のようなものを実行し、シリアルを使用してAVRと通信します。

// ESP8266 Arduino core code:
void setup(){
 Serial.begin(9600); // or whatever
}

void loop() {
  // Serial.write((char)(RANDOM_REG32 % 256)); // "bin"
  Serial.print( String(RANDOM_REG32, HEX).substring(1)); // "hex"
}

**編集

これは私がしばらく前に書いたベアボードHWRNGスケッチで、単なるコレクターとしてではなく、シリアルポートから吐き出されるCSPRNG全体として動作します。プロミニ向けに構築されていますが、他のボードに簡単に適応できるはずです。フローティングアナログピンだけを使用することもできますが、それらに何かを追加することをお勧めします。マイク、LDR、サーミスタ(室温付近で最大に広がるようにトリミングされている)、さらには長いワイヤのように。耳障りな音さえあれば、耳鼻咽喉科でかなりうまくいきます。

このスケッチには、回答とフォローアップコメントで述べたいくつかの概念が統合されています。エントロピーの蓄積、理想的ではないエントロピーのオーバーサンプリングによるストレッチ(フォンノイマンによるとクール)、均一性へのハッシングです。エントロピー品質の推定は行わず、「おそらく動的なものをギミックする」ことと、暗号プリミティブを使用して混合することを優先します。

// AVR (ardunio) HWRNG by dandavis. released to public domain by author.
#include <Hash.h> 

unsigned long read[8] = {0, 0, 0, 0, 0, 0, 0, 0};
const int pincount = 9; // adjust down for non pro-mini boards
int pins[9] = {A0, A1, A2, A3, A4, A5, A6, A7, A0}; // adjust for board, name analog inputs to be sampled
unsigned int ticks = 0;
String buff = ""; // holds one round of derivation tokens to be hashed.
String cache; // the last read hash



void harvest() { // String() slows down the processing, making micros() calls harder to recreate
  unsigned long tot = 0; // the total of all analog reads
  buff = String(random(2147483647)) + String(millis() % 999);
  int seed =  random(256) + (micros() % 32);
  int offset =  random(2147483647) % 256;

  for (int i = 0; i < 8; i++) {
    buff += String( seed + read[i] + i + (ticks % 65), HEX );
    buff += String(random(2147483647), HEX);
    tot += read[i];
  }//next i

  buff += String( (micros() + ticks + offset) % 99999, HEX);
  if (random(10) < 3) randomSeed(tot + random(2147483647) + micros()); 
  buff = sha1( String(random(2147483647)) + buff + (micros()%64) + cache); // used hash to uniform output and waste time
  Serial.print( buff ); // output the hash
  cache = buff;
  spin();
}//end harvest()


void spin() { // add entropy and mix
  ticks++;
  int sample = 128;
  for (int i = 0; i < 8; i++) { // update ~6/8 banks 8 times
    read[ read[i] % 8] += (micros() % 128);
    sample = analogRead(  pins[i] ); // a read from each analog pin
    read[ micros() % 8] += ( read[i] % 64 ); // mix timing and 6LSBs from read
    read[i] += sample; // mix whole raw sample
    read[(i + 1) % 8] += random(2147483647) % 1024; // mix prng
    read[ticks % 8] += sample % 16; // mix the best nibble of the read
    read[sample % 8] += read[ticks % 8] % 2147483647; // intra-mix banks
  }

}//end spin()



void setup() {
  Serial.begin(9600);
  delay(222);
  int mx = 2028 + ((analogRead(A0)  + analogRead(A1) + analogRead(A2)  + analogRead(A3)) % 256);  
  while (ticks < mx) {
    spin();
    delay(1);
    randomSeed(read[2] + read[1] + read[0] + micros() + random(4096) + ticks);
  }// wend
}// end setup()



void loop() {
  spin();
  delayMicroseconds((read[ micros() % 8] %  2048) + 333  );
  delay(random(10));
  //if (millis() < 500) return;
  if ((ticks % 16) == (millis() % 16) ) harvest();
}// end loop()

(私はここのキャラクターが不足しています、ごめんなさい。)良い概要!塩のカウンターを使用することをお勧めします。micros()は、呼び出しの間にいくつかのステップでジャンプする可能性があるため、ビットの無駄です。アナログ入力の上位ビットを避け、最下位の1ビットまたは2ビットに制限します。標的型攻撃であっても、それらを特定するのは困難です(入力にワイヤを配置できない場合)。「非決定論的ミキシング」は、ソフトウェアでできることではありません。SHA-1ミキシングが標準化されています:crypto.stackexchange.com/a/6232。インデット。提案するタイマーは、既に持っているソースと同じくらいランダムです。ここではあまり得られません。
ジョナスシェーファー

shaは単純化して保護するため、たとえばアナログ入力から取得するビット数を心配する必要はありません。アナログ(または曲がりくねったPCBトレース)に接続された数インチのワイヤは、数ビット以上それを揺らします。蓄積された値のサブサンプルでハッシュに供給される未保存の塩と未知の塩のおかげで、混合は非決定的です。micros()は、特に非決定的な間隔で起動された場合、カウンターよりも再生が困難です。
ダンダビス

1
質問があります。あなたは、100の措置を取る方が良いと言った。しかし、これらの「ランダムな」データを取得することの有効性を制限する一種の「平均的な」手段をたくさん取っているのではありませんか?
つまり

絶えずサンプリングすることをお勧めします。100が1よりも優れていると言っただけです。Yarrow / Fortunaのような蓄積モデルは、はるかに優れています。ハッシュする前に、これらの100個のアナログサンプルを連結する(合計しない)ことを検討してください。これは、サンプルの順序を重要にし、1文字オフするとまったく異なるハッシュになるためです。したがって、ノイズを減らすためにサンプルを平均化できたとしても、攻撃者はすべての値を逐語的に暗唱するか、一致しない必要があります。
ダンダビス

4

私の経験から、analogRead()フローティングピンのエントロピーは非常に低くなっています。呼び出しごとに1ビットまたは2ビットのランダム性があります。あなたは間違いなくより良いものが欲しいです。per1234の回答で提案されているように、ウォッチドッグタイマーのジッターは、適切な代替手段です。ただし、非常に遅いレートでエントロピーを生成します。これは、プログラムの開始時に必要な場合に問題になる可能性があります。dandavisにはいくつかの優れた提案がありますが、通常はESP8266または外部ハードウェアが必要です。

まだ言及されていない興味深いエントロピーソースが1つあります。それは、初期化されていないRAMの内容です。MCUの電源を入れると、そのRAMビットの一部(たぶん最も対称的なトランジスタを持つもの)がランダムな状態で起動します。このhackadayの記事で説明したように 、これはエントロピーソースとして使用できます。コールドブートでのみ利用できるため、初期エントロピープールを満たすために使用できます。その後、他の低速ソースから定期的に補充します。このようにして、プールがゆっくりいっぱいになるのを待たずに、プログラムが作業を開始できます。

これは、AVRベースのArduinoでこれをどのように収集できるかの例です。以下のコードスニペットは、後でフィードするシードを構築するためにRAM全体をXORしsrandom()ます。トリッキーな部分は、Cランタイムが.dataおよび.bssメモリセクションを初期化する前にハーベストを行う必要があり、Cランタイムが上書きしない場所にシードを保存する必要があることです。これは、特定のメモリセクションを使用して行われ ます

uint32_t __attribute__((section(".noinit"))) random_seed;

void __attribute__((naked, section(".init3"))) seed_from_ram()
{
    const uint32_t * const ramstart = (uint32_t *) RAMSTART;
    const uint32_t * const ramend   = (uint32_t *) RAMEND;
    uint32_t seed = 0;
    for (const uint32_t *p = ramstart; p <= ramend; p++)
        seed ^= *p;
    random_seed = seed;
}

void setup()
{
    srandom(random_seed);
}

ウォームリセットでは、SRAMが保持されるため、エントロピープールの内容全体が保持されることに注意してください。この同じコードを使用して、リセット後も収集されたエントロピーを保持できます。

編集:ローカルの代わりにseed_from_ram()グローバルで機能する私の最初のバージョンの問題を修正しました。これにより、シードがそれ自体とXORされ、これまでに収集されたすべてのエントロピーが破壊される可能性があります。random_seedseed


よくやった!盗むことはできますか?re:ピン:1ビットまたは2ビットの未知数で十分です。それは完全な秘密(出力)の出力速度を制限するだけで、必要な計算上の秘密は制限しません
...-dandavis

1
@dandavis:はい、再利用できます。analogRead()自分が何をしているのかを知っていれば、あなたは使用できることについて正しいです。プールのエントロピーの推定値を更新するときに、そのランダム性を過大評価しないように注意する必要があります。私のポイントはanalogRead()、主に貧しいまだの批判として意味されることが多い繰り返さ「レシピ」randomSeed(analogRead(0)) 一度だけsetup()、それの十分を前提としています。
エドガーボネット

analogRead(0)呼び出しごとに1ビットのエントロピーがある場合、繰り返し呼び出すと、エントロピーの10000/8 = 1.25 KBytes /秒、エントロピーライブラリの150倍になります。
ドミトリーグリゴリエフ

0

エントロピーを実際に必要とせず、スタートアップごとに異なる擬似乱数のシーケンスを取得したいだけであれば、EEPROMを使用して連続したシードを反復処理できます。技術的にはプロセスは完全に確定的ですが、実際randomSeed(analogRead(0))には、接続されていないピンよりもはるかに優れているため、多くの場合、0または1023の同じシードで開始します。EEPROMに次のシードを保存すると、別の毎回シード。

#include <EEPROM.h>

const int seed_addr = 0;
unsigned long seed;

void setup() {
    seed = EEPROM.read(seed_addr);
    EEPROM.write(seed_addr, seed+1);
    randomSeed(seed);
}

実際のエントロピーが必要な場合は、クロックドリフトから収集するか、外部ノイズを増幅して収集します。また、多くのエントロピーが必要な場合、外部ノイズが唯一の実行可能なオプションです。ツェナーダイオードは、特に5〜6Vを超える電圧源がある場合によく選択されます(適切なツェナーダイオードでも5Vで動作しますが、エントロピーが少なくなります)。

ここに画像の説明を入力してください

ソース)。

アンプの出力はアナログピンに接続する必要があります。アナログピンはそれぞれanalogRead()最大数10 MHz のエントロピーを数ビット生成します(Arduinoがサンプリングできるよりも高速です)。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.