Dynamic Assurance (3 of 3)
You may have noticed a little module (scope of the mod
keyword) hanging out at the bottom of crypto_tool/rc4/src/lib.rs
:
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
let result = 2 + 2;
assert_eq!(result, 4);
}
}
This is unit test boilerplate, it was filled in when we ran cargo new crypto_tool/rc4 --lib
earlier.
We're going to replace it with our own unit test now.
The first test we'll write is essentially a "sanity check". At a bare minimum, our library should be able to transform plaintext into something different (presumably an encrypted form) and back into the original. That's what this test checks for:
#[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()
);
}
}
We print our initial plaintext, use apply_keystream_static
to encrypt it and print the result, then similarly decrypt it and print the result.
-
key
is a random, 16-byte key we've chosen arbitrarily for testing purposes. -
msg
is the raw bytes for the ASCII1 string "Hello World!". -
String::from_utf8(msg.to_vec()).unwrap()
converts the raw bytes into a printable string.- This is a fallible operation (we could have provided non-printable bytes as input!) so an "operation result" has to be "unwrapped" (
.unwrap()
is like anassert!
here). We'll discussResult
and error handling in Chapter 3.
- This is a fallible operation (we could have provided non-printable bytes as input!) so an "operation result" has to be "unwrapped" (
-
#[rustfmt::skip]
tells our code formatter (invoked viacargo fmt
) not to change the indentation of the variable it appears above. It's not pertinent to this test, but you may have been curious what it's for. Rust supports configurable code formatting and linting to make style consistent for large, multi-developer codebases.
You can run this test with cargo test
command, from the crypto_tool/rc4
directory.
By default, cargo test
prints only test results, not their console output, unless a test fails.
To see our println!
statements, we need to use cargo test -- --show-output
.
Then output will include the following:
---- 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!
Our simple dynamic test demonstrates we have a runnable program capable of scrambling and unscrambling a message!
Note how we don't print the ciphertext as a string, since it contains non-printable characters.
We display the raw hexadecimal bytes instead.
You can take a second to write a similar test for the chunk API, apply_keystream
, now.
1st-party unit test support, via cargo test
, is a major strength of Rust relative to C and C++.
We didn't need to learn/configure/build/import any 3rd party test frameworks to get a modern development experience.
While our methodology is powerful, our actual test was not. This "sanity check" doesn't actually prove that we implemented RC4 correctly - just that our code can transform the data and reverse the change. There's a risk that the generated cipher text is incorrect for the given key, potentially in some way that makes it "crackable" - maybe an attacker can leverage some flaw and extract plaintext without knowledge of the key.
To ensure that's not the case, we need to validate our implementation dynamically. Create a runnable test against ground truth. For cryptographic ciphers, this often means comparing against official "test vectors" (known-good input-output pairs).
Dynamic Validation
RC4 was, in the not-so-distant-past, a critical part of internet security. Almost every TLS connection on the internet once used, or could choose to use, the algorithm. Thus, a leading internet standards body - the Internet Engineering Task Force (IETF) - released official test vectors2 to help protocol implementers validate their RC4 libraries.
We're going to leverage those official vectors now! Justifiable confidence is the hallmark of high assurance programming.
The IETF document2 contains over a dozen tables of test vector data. Here's the first one:
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
We're given a key (line 2) and 18 samples from the keystream a valid RC4 implementation should produce (the subsequent rows). Each sample is 16 bytes long and preceded by its offset into the keystream (given in both decimal and hex).
Translating every sample from every table into a test suite would be important for a real library, but tedious for our example. So we'll use just the first 4 rows of the table above:
#[cfg(test)]
mod tests {
use super::Rc4;
// ..sanity_check_static_api() omitted..
// 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
is an array for storing the first 272 bytes of the keystream (just enough to slice out the first four samples for comparison). It starts initialized to all zeros. Instead of initializing it in a loop, we use the shorthand[0x0; 272]
.- Any byte XORed with
0x00
is itself. So encrypting a zero-buffer means we're just extracting our implementation's keystream. In any secure cipher, this keystream should be indistinguishable from a random sequence of bytes. For RC4, the values should match the official vectors.
- Any byte XORed with
-
Each
assert_eq!
checks a slice of the keystream (a subset ofout_buf
) against the corresponding test vector (test_stream_*
).- Notice we use slicing notation to grab 16-byte chunks at an offset corresponding to the document's table (e.g.
out_buf[240..256]
means bytes in the range[240, 256)
of our272
).
- Notice we use slicing notation to grab 16-byte chunks at an offset corresponding to the document's table (e.g.
If you run cargo test
from the crypto_tool/rc4
directory, you should now see both unit tests pass:
running 2 tests
test tests::ietf_40_bit_key_first_4_vectors ... ok
test tests::sanity_check_static_api ... ok
Takeaway
You've now built your first piece of high assurance software (sans the RC4 algorithm itself). Your RC4 library is:
- Fully memory-safe, hence
#![forbid(unsafe_code)]
- Stand-alone and capable for running almost anywhere, hence
#![no_std]
- Functionally validated, using official IETF test vectors
Before we get to the fun and tangible part - writing a command line tool that uses this library to encrypt local files - we need to take a step back and understand the limitations of all the static and dynamic assurance topics discussed so far.
Is Rust a good choice for cryptographic libraries?
A study of C and C++ cryptographic libraries found that only 27.2% of reported vulnerabilities were caused by flaws related to the cryptography itself, but 37.2% were memory safety issues3.
Because both performance and security are core requirements, cryptography is a prime use case for Rust (pun intended). The language has a thriving cryptographic ecosystem.
rustls
4, a pure-Rust TLS library, is one notable project. In 2019, it outperformed OpenSSL by significant margins5.
ASCII. Wikipedia (Accessed 2022).
Test Vectors for the Stream Cipher RC4. Internet Engineering Task Force (2011).
You Really Shouldn’t Roll Your Own Crypto: An Empirical Study of Vulnerabilities in Cryptographic Libraries. Jenny Blessing, Michael A. Specter, Daniel J. Weitzner (2021). Please note that, at the time of this writing, this paper has not yet been accepted to a peer-reviewed conference.
rustls
. rustls Contributors (Accessed 2022).
A Rust-based TLS library outperformed OpenSSL in almost every category. Catalin Cimpanu (2019).