動的保証(3/3)
crypto_tool/rc4/src/lib.rs の末尾に小さなモジュール(mod キーワードのスコープ)があることに気づいたかもしれません。
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
let result = 2 + 2;
assert_eq!(result, 4);
}
}
これはユニットテストのボイラープレートで、以前に cargo new crypto_tool/rc4 --lib を実行したときに生成されたものです。
これから、これを独自のユニットテストに置き換えます。
最初に書くテストは、基本的には「健全性チェック」です。 最低限、私たちのライブラリは平文を別のもの(おそらく暗号化された形式)に変換し、それを元に戻せるべきです。 このテストはまさにそれを確認します。
#[cfg(test)]
mod tests {
use super::Rc4;
#[test]
fn sanity_check_static_api() {
#[rustfmt::skip]
let key: [u8; 16] = [
0x4b, 0x8e, 0x29, 0x87, 0x80, 0x95, 0x96, 0xa3,
0xbb, 0x23, 0x82, 0x49, 0x9f, 0x1c, 0xe7, 0xc2,
];
#[rustfmt::skip]
let plaintext = [
0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57, 0x6f,
0x72, 0x6c, 0x64, 0x21,
]; // "Hello World!"
let mut msg: [u8; 12] = plaintext.clone();
println!(
"Plaintext (initial): {}",
String::from_utf8(msg.to_vec()).unwrap()
);
// Encrypt in-place
Rc4::apply_keystream_static(&key, &mut msg);
assert_ne!(msg, plaintext);
println!("Ciphertext: {:x?}", msg);
// Decrypt in-place
Rc4::apply_keystream_static(&key, &mut msg);
assert_eq!(msg, plaintext);
println!(
"Plaintext (decrypted): {}",
String::from_utf8(msg.to_vec()).unwrap()
);
}
}
最初の平文を出力し、apply_keystream_static を使ってそれを暗号化して結果を出力し、同様に復号して結果を出力します。
-
keyは、テスト目的で任意に選んだランダムな 16 バイトの鍵です。 -
msgは、ASCII1 文字列 “Hello World!” の生バイトです。 -
String::from_utf8(msg.to_vec()).unwrap()は、生バイトを出力可能な文字列に変換します。- これは失敗しうる操作です(入力として出力不可能なバイトを渡すこともできたからです!)そのため、「操作結果」を「unwrap」する必要があります(ここでの
.unwrap()はassert!のようなものです)。Resultとエラーハンドリングについては第3章で説明します。
- これは失敗しうる操作です(入力として出力不可能なバイトを渡すこともできたからです!)そのため、「操作結果」を「unwrap」する必要があります(ここでの
-
#[rustfmt::skip]は、コードフォーマッター(cargo fmt経由で呼び出されます)に対して、その下に現れる変数のインデントを変更しないよう指示します。このテストには直接関係ありませんが、何のためのものか気になっていたかもしれません。Rust は、大規模な複数開発者のコードベースでスタイルを一貫させるために、設定可能なコードフォーマットと lint をサポートしています。
このテストは、crypto_tool/rc4 ディレクトリから cargo test コマンドで実行できます。
デフォルトでは、cargo test はテスト結果のみを出力し、テストが失敗しない限りコンソール出力は表示しません。
println! 文を確認するには、cargo test -- --show-output を使う必要があります。
すると、出力には以下が含まれます。
---- tests::sanity_check_static_api stdout ----
Plaintext (initial): Hello World!
Ciphertext: [d0, 1c, 95, d4, 40, c7, 3c, 53, 8a, 22, d9, a1]
Plaintext (decrypted): Hello World!
この単純な動的テストにより、メッセージをスクランブルし、元に戻すことができる実行可能なプログラムがあることが示されました!
暗号文には出力不可能な文字が含まれているため、文字列として出力していない点に注意してください。
代わりに、生の 16 進バイトを表示しています。
ここで少し時間を取って、チャンク API である apply_keystream についても同様のテストを書いてみてください。
cargo test によるファーストパーティのユニットテストサポートは、C や C++ と比較した Rust の大きな強みです。
モダンな開発体験を得るために、サードパーティのテストフレームワークを学習、設定、ビルド、インポートする必要はありませんでした。
私たちの方法論は強力ですが、実際のテストはそうではありませんでした。 この「健全性チェック」は、RC4 を正しく実装したことを実際に証明するものではありません。単に、私たちのコードがデータを変換し、その変更を元に戻せることを示しているだけです。 生成された暗号文が、与えられた鍵に対して正しくない可能性があります。場合によっては、それが「解読可能」にしてしまうような形で誤っているかもしれません。つまり、攻撃者が何らかの欠陥を利用して、鍵を知らずに平文を抽出できる可能性があります。
そうではないことを確実にするには、実装を動的に検証する必要があります。 グラウンドトゥルースに対する実行可能なテストを作成します。 暗号のサイファーでは、これは多くの場合、公式の「テストベクター」(既知の正しい入出力ペア)と比較することを意味します。
動的検証
RC4 は、そう遠くない過去において、インターネットセキュリティの重要な一部でした。 かつて、インターネット上のほぼすべての TLS 接続がこのアルゴリズムを使用していたか、使用することを選択できました。 そのため、主要なインターネット標準化団体である Internet Engineering Task Force(IETF)は、プロトコル実装者が RC4 ライブラリを検証できるよう、公式のテストベクター2 を公開しました。
これから、その公式ベクターを活用します! 正当化可能な確信こそが、高保証プログラミングの特徴です。
IETF 文書2 には、テストベクターデータの表が十数個含まれています。 以下はその最初のものです。
Key length: 40 bits.
key: 0x0102030405
DEC 0 HEX 0: b2 39 63 05 f0 3d c0 27 cc c3 52 4a 0a 11 18 a8
DEC 16 HEX 10: 69 82 94 4f 18 fc 82 d5 89 c4 03 a4 7a 0d 09 19
DEC 240 HEX f0: 28 cb 11 32 c9 6c e2 86 42 1d ca ad b8 b6 9e ae
DEC 256 HEX 100: 1c fc f6 2b 03 ed db 64 1d 77 df cf 7f 8d 8c 93
DEC 496 HEX 1f0: 42 b7 d0 cd d9 18 a8 a3 3d d5 17 81 c8 1f 40 41
DEC 512 HEX 200: 64 59 84 44 32 a7 da 92 3c fb 3e b4 98 06 61 f6
DEC 752 HEX 2f0: ec 10 32 7b de 2b ee fd 18 f9 27 76 80 45 7e 22
DEC 768 HEX 300: eb 62 63 8d 4f 0b a1 fe 9f ca 20 e0 5b f8 ff 2b
DEC 1008 HEX 3f0: 45 12 90 48 e6 a0 ed 0b 56 b4 90 33 8f 07 8d a5
DEC 1024 HEX 400: 30 ab bc c7 c2 0b 01 60 9f 23 ee 2d 5f 6b b7 df
DEC 1520 HEX 5f0: 32 94 f7 44 d8 f9 79 05 07 e7 0f 62 e5 bb ce ea
DEC 1536 HEX 600: d8 72 9d b4 18 82 25 9b ee 4f 82 53 25 f5 a1 30
DEC 2032 HEX 7f0: 1e b1 4a 0c 13 b3 bf 47 fa 2a 0b a9 3a d4 5b 8b
DEC 2048 HEX 800: cc 58 2f 8b a9 f2 65 e2 b1 be 91 12 e9 75 d2 d7
DEC 3056 HEX bf0: f2 e3 0f 9b d1 02 ec bf 75 aa ad e9 bc 35 c4 3c
DEC 3072 HEX c00: ec 0e 11 c4 79 dc 32 9d c8 da 79 68 fe 96 56 81
DEC 4080 HEX ff0: 06 83 26 a2 11 84 16 d2 1f 9d 04 b2 cd 1c a0 50
DEC 4096 HEX 1000: ff 25 b5 89 95 99 67 07 e5 1f bd f0 8b 34 d8 75
鍵(2行目)と、有効な RC4 実装が生成するはずのキーストリームからの 18 個のサンプル(それ以降の行)が与えられています。 各サンプルは 16 バイト長で、その前にキーストリーム内のオフセット(10 進数と 16 進数の両方)が付いています。
すべての表からすべてのサンプルをテストスイートに変換することは、実際のライブラリにとっては重要ですが、今回の例では手間がかかります。 そこで、上の表の最初の 4 行だけを使います。
#[cfg(test)]
mod tests {
use super::Rc4;
// ..sanity_check_static_api() は省略..
// See: https://datatracker.ietf.org/doc/html/rfc6229#section-2
#[test]
fn ietf_40_bit_key_first_4_vectors() {
let key: [u8; 5] = [0x01, 0x02, 0x03, 0x04, 0x5];
let mut out_buf: [u8; 272] = [0x0; 272];
#[rustfmt::skip]
let test_stream_0: [u8; 16] = [
0xb2, 0x39, 0x63, 0x05, 0xf0, 0x3d, 0xc0, 0x27,
0xcc, 0xc3, 0x52, 0x4a, 0x0a, 0x11, 0x18, 0xa8,
];
#[rustfmt::skip]
let test_stream_16: [u8; 16] = [
0x69, 0x82, 0x94, 0x4f, 0x18, 0xfc, 0x82, 0xd5,
0x89, 0xc4, 0x03, 0xa4, 0x7a, 0x0d, 0x09, 0x19,
];
#[rustfmt::skip]
let test_stream_240: [u8; 16] = [
0x28, 0xcb, 0x11, 0x32, 0xc9, 0x6c, 0xe2, 0x86,
0x42, 0x1d, 0xca, 0xad, 0xb8, 0xb6, 0x9e, 0xae,
];
#[rustfmt::skip]
let test_stream_256: [u8; 16] = [
0x1c, 0xfc, 0xf6, 0x2b, 0x03, 0xed, 0xdb, 0x64,
0x1d, 0x77, 0xdf, 0xcf, 0x7f, 0x8d, 0x8c, 0x93,
];
// Remaining 14 vectors in set skipped for brevity...
// Create an instance of the cipher
let mut rc4 = Rc4::new(&key);
// Output keystream
rc4.apply_keystream(&mut out_buf);
// Validate against official vectors
assert_eq!(out_buf[0..16], test_stream_0);
assert_eq!(out_buf[16..32], test_stream_16);
assert_eq!(out_buf[240..256], test_stream_240);
assert_eq!(out_buf[256..272], test_stream_256);
}
}
-
out_bufは、キーストリームの先頭 272 バイトを格納するための配列です(比較用に先頭 4 つのサンプルを切り出すのにちょうど十分な長さです)。最初はすべてゼロで初期化されています。ループで初期化する代わりに、省略記法[0x0; 272]を使用します。- 任意のバイトを
0x00と XOR すると、そのバイト自身になります。そのため、ゼロバッファを暗号化するということは、単に実装のキーストリームを抽出していることになります。安全な暗号では、このキーストリームはランダムなバイト列と識別できないはずです。RC4 の場合、値は公式ベクトルと一致するはずです。
- 任意のバイトを
-
各
assert_eq!は、キーストリームのスライス(out_bufの一部)を、対応するテストベクトル(test_stream_*)と照合します。- ドキュメントの表に対応するオフセットで 16 バイトのチャンクを取得するために、スライス記法を使用している点に注目してください(たとえば、
out_buf[240..256]は、272のうち範囲[240, 256)にあるバイトを意味します)。
- ドキュメントの表に対応するオフセットで 16 バイトのチャンクを取得するために、スライス記法を使用している点に注目してください(たとえば、
crypto_tool/rc4 ディレクトリから cargo test を実行すると、両方のユニットテストが通ることを確認できるはずです。
running 2 tests
test tests::ietf_40_bit_key_first_4_vectors ... ok
test tests::sanity_check_static_api ... ok
要点
これで、最初の高保証ソフトウェアの一部を構築できました(RC4 アルゴリズム自体は除きます)。 あなたの RC4 ライブラリは次のようなものです。
- 完全にメモリ安全です。したがって
#![forbid(unsafe_code)] - スタンドアロンで、ほぼどこでも実行できます。したがって
#![no_std] - 公式 IETF テストベクトルを使用して、機能的に検証されています
このライブラリを使用してローカルファイルを暗号化するコマンドラインツールを書くという、楽しくて具体的な部分に進む前に、いったん立ち止まり、ここまでに説明してきた静的保証と動的保証のすべてのトピックの限界を理解する必要があります。
Rust は暗号ライブラリに適した選択肢でしょうか?
C および C++ の暗号ライブラリに関するある研究では、報告された脆弱性のうち、暗号自体に関連する欠陥が原因だったものは 27.2% にすぎない一方で、37.2% はメモリ安全性の問題でした3。
パフォーマンスとセキュリティはいずれも中核的な要件であるため、暗号は Rust の主要なユースケースです(しゃれのつもりです)。 この言語には活発な暗号エコシステムがあります。 pure-Rust の TLS ライブラリである
rustls4 は、注目すべきプロジェクトの 1 つです。 2019 年には、ほぼすべてのカテゴリで OpenSSL を大きく上回りました5。
-
ストリーム暗号 RC4 のテストベクトル。Internet Engineering Task Force(2011 年)。 ↩ ↩2
-
独自暗号を作るべきではない理由:暗号ライブラリにおける脆弱性の実証的研究。Jenny Blessing、Michael A. Specter、Daniel J. Weitzner(2021 年)。本稿執筆時点では、この論文はまだ査読付きカンファレンスに採択されていないことに注意してください。 ↩
-
Rust ベースの TLS ライブラリがほぼすべてのカテゴリで OpenSSL を上回った。Catalin Cimpanu(2019 年)。 ↩