From 2b3c7afc620b19cfe536c27253c4662ba8dca79d Mon Sep 17 00:00:00 2001 From: David Drysdale Date: Sun, 25 Jul 2021 12:34:52 +0000 Subject: [PATCH] Tool to convert Wycheproof test vectors to .blb files (#280) --- Cargo.lock | 53 ++++++++++++ Cargo.toml | 1 + blobby/examples/convert.rs | 77 ++--------------- blobby/src/lib.rs | 96 +++++++++++++++++++++- wycheproof2blb/Cargo.toml | 16 ++++ wycheproof2blb/src/aead.rs | 89 ++++++++++++++++++++ wycheproof2blb/src/aes_siv.rs | 60 ++++++++++++++ wycheproof2blb/src/ecdsa.rs | 73 ++++++++++++++++ wycheproof2blb/src/ed25519.rs | 64 +++++++++++++++ wycheproof2blb/src/hkdf.rs | 68 +++++++++++++++ wycheproof2blb/src/mac.rs | 66 +++++++++++++++ wycheproof2blb/src/main.rs | 137 +++++++++++++++++++++++++++++++ wycheproof2blb/src/wycheproof.rs | 123 +++++++++++++++++++++++++++ 13 files changed, 850 insertions(+), 73 deletions(-) create mode 100644 wycheproof2blb/Cargo.toml create mode 100644 wycheproof2blb/src/aead.rs create mode 100644 wycheproof2blb/src/aes_siv.rs create mode 100644 wycheproof2blb/src/ecdsa.rs create mode 100644 wycheproof2blb/src/ed25519.rs create mode 100644 wycheproof2blb/src/hkdf.rs create mode 100644 wycheproof2blb/src/mac.rs create mode 100644 wycheproof2blb/src/main.rs create mode 100644 wycheproof2blb/src/wycheproof.rs diff --git a/Cargo.lock b/Cargo.lock index f0b2b0af..66e6ed8f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -200,6 +200,12 @@ dependencies = [ "digest", ] +[[package]] +name = "itoa" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" + [[package]] name = "libc" version = "0.2.98" @@ -294,6 +300,12 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + [[package]] name = "salsa20" version = "0.8.0" @@ -315,6 +327,37 @@ dependencies = [ "sha2", ] +[[package]] +name = "serde" +version = "1.0.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d5161132722baa40d802cc70b15262b98258453e85e5d1d365c757c73869ae" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9391c295d64fc0abb2c556bad848f33cb8296276b1ad2677d1ae1ace4f258f31" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea1c6153794552ea7cf7cf63b1231a25de00ec90db326ba6264440fa08e31486" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "sha2" version = "0.9.5" @@ -382,6 +425,16 @@ version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" +[[package]] +name = "wycheproof2blb" +version = "0.1.0" +dependencies = [ + "blobby", + "hex", + "serde", + "serde_json", +] + [[package]] name = "x509" version = "0.0.0" diff --git a/Cargo.toml b/Cargo.toml index 4af73eab..a87631eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,5 +18,6 @@ members = [ "pkcs5", "pkcs8", "spki", + "wycheproof2blb", "x509" ] diff --git a/blobby/examples/convert.rs b/blobby/examples/convert.rs index 35039585..3e91264e 100644 --- a/blobby/examples/convert.rs +++ b/blobby/examples/convert.rs @@ -1,35 +1,8 @@ //! Convert utility -use blobby::BlobIterator; -use std::collections::HashMap; +use blobby::{encode_blobs, BlobIterator}; use std::io::{self, BufRead, BufReader, BufWriter, Write}; use std::{env, error::Error, fs::File}; -const NEXT_MASK: u8 = 0b1000_0000; -const VAL_MASK: u8 = 0b0111_1111; - -fn encode_vlq(mut val: usize, buf: &mut [u8; 4]) -> &[u8] { - macro_rules! step { - ($n:expr) => { - buf[$n] = if $n == 3 { - (val & (VAL_MASK as usize)) as u8 - } else { - val -= 1; - NEXT_MASK | (val & (VAL_MASK as usize)) as u8 - }; - val >>= 7; - if val == 0 { - return &buf[$n..]; - } - }; - } - - step!(3); - step!(2); - step!(1); - step!(0); - panic!("integer is too big") -} - fn encode(reader: impl BufRead, mut writer: impl Write) -> io::Result { let mut blobs = Vec::new(); for line in reader.lines() { @@ -37,50 +10,10 @@ fn encode(reader: impl BufRead, mut writer: impl Write) -> io::Result { .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; blobs.push(blob); } - - let mut idx_map = HashMap::new(); - for blob in blobs.iter().filter(|b| b.len() != 0) { - let v = idx_map.entry(blob.as_slice()).or_insert(0); - *v += 1; - } - - let mut idx: Vec<&[u8]> = idx_map - .iter() - .filter(|(_, &v)| v > 1) - .map(|(&k, _)| k) - .collect(); - idx.sort_by_key(|e| { - let k = match e { - &[0] => 2, - &[1] => 1, - _ => 0, - }; - (k, idx_map.get(e).unwrap()) - }); - idx.reverse(); - - let rev_idx: HashMap<&[u8], usize> = idx.iter().enumerate().map(|(i, &e)| (e, i)).collect(); - - println!("Index len: {:?}", idx.len()); - let mut buf = [0u8; 4]; - writer.write_all(encode_vlq(idx.len(), &mut buf))?; - for e in idx { - writer.write_all(encode_vlq(e.len(), &mut buf))?; - writer.write_all(e)?; - } - - for blob in blobs.iter() { - if let Some(dup_pos) = rev_idx.get(blob.as_slice()) { - let n = (dup_pos << 1) + 1usize; - writer.write_all(encode_vlq(n, &mut buf))?; - } else { - let n = blob.len() << 1; - writer.write_all(encode_vlq(n, &mut buf))?; - writer.write_all(blob)?; - } - } - - Ok(blobs.len()) + let (data, idx_len) = encode_blobs(&blobs); + let data_len = data.len(); + println!("Index len: {:?}", idx_len); + writer.write_all(&data).map(|_| data_len) } fn decode(mut reader: R, mut writer: W) -> io::Result { diff --git a/blobby/src/lib.rs b/blobby/src/lib.rs index f74c9c45..4fa74b21 100644 --- a/blobby/src/lib.rs +++ b/blobby/src/lib.rs @@ -51,7 +51,7 @@ )] extern crate alloc; -use alloc::{boxed::Box, vec, vec::Vec}; +use alloc::{boxed::Box, collections::BTreeMap, vec, vec::Vec}; use core::iter::Iterator; /// Iterator over binary blobs @@ -114,6 +114,100 @@ fn read_vlq(data: &[u8], pos: &mut usize) -> Result { Ok(val) } +/// Write a git-flavoured VLQ value into `buf`. +/// +/// Returns the slice within `buf` that holds the value. +fn encode_vlq(mut val: usize, buf: &mut [u8; 4]) -> &[u8] { + macro_rules! step { + ($n:expr) => { + buf[$n] = if $n == 3 { + (val & (VAL_MASK as usize)) as u8 + } else { + val -= 1; + NEXT_MASK | (val & (VAL_MASK as usize)) as u8 + }; + val >>= 7; + if val == 0 { + return &buf[$n..]; + } + }; + } + + step!(3); + step!(2); + step!(1); + step!(0); + panic!("integer is too big") +} + +/// Encode the given collection of binary blobs in .blb format into `writer`. +/// Returns the encoded data together with a count of the number of blobs included in the index. +/// +/// The encoded file format is: +/// - count of index entries=N +/// - N x index entries, each encoded as: +/// - size L of index entry (VLQ) +/// - index blob contents (L bytes) +/// - repeating encoded blobs, each encoded as: +/// - VLQ value that is either: +/// - (J << 1) & 0x01: indicates this blob is index entry J +/// - (L << 1) & 0x00: indicates an explicit blob of len L +/// - (in the latter case) explicit blob contents (L bytes) +pub fn encode_blobs<'a, I, T: 'a>(blobs: &'a I) -> (Vec, usize) +where + &'a I: IntoIterator, + T: AsRef<[u8]>, +{ + let mut idx_map = BTreeMap::new(); + blobs + .into_iter() + .map(|v| v.as_ref()) + .filter(|blob| !blob.is_empty()) + .for_each(|blob| { + let v = idx_map.entry(blob.as_ref()).or_insert(0); + *v += 1; + }); + + let mut idx: Vec<&[u8]> = idx_map + .iter() + .filter(|(_, &v)| v > 1) + .map(|(&k, _)| k) + .collect(); + idx.sort_by_key(|e| { + let k = match e { + [0] => 2, + [1] => 1, + _ => 0, + }; + (k, idx_map.get(e).unwrap()) + }); + idx.reverse(); + let idx_len = idx.len(); + + let rev_idx: BTreeMap<&[u8], usize> = idx.iter().enumerate().map(|(i, &e)| (e, i)).collect(); + + let mut out_buf = Vec::new(); + let mut buf = [0u8; 4]; + out_buf.extend_from_slice(encode_vlq(idx.len(), &mut buf)); + for e in idx { + out_buf.extend_from_slice(encode_vlq(e.len(), &mut buf)); + out_buf.extend_from_slice(e); + } + + for blob in blobs.into_iter().map(|v| v.as_ref()) { + if let Some(dup_pos) = rev_idx.get(blob) { + let n = (dup_pos << 1) + 1usize; + out_buf.extend_from_slice(encode_vlq(n, &mut buf)); + } else { + let n = blob.len() << 1; + out_buf.extend_from_slice(encode_vlq(n, &mut buf)); + out_buf.extend_from_slice(blob); + } + } + + (out_buf, idx_len) +} + impl<'a> BlobIterator<'a> { /// Create new `BlobIterator` for given `data`. pub fn new(data: &'a [u8]) -> Result { diff --git a/wycheproof2blb/Cargo.toml b/wycheproof2blb/Cargo.toml new file mode 100644 index 00000000..cb3a07a7 --- /dev/null +++ b/wycheproof2blb/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "wycheproof2blb" +version = "0.1.0" +authors = ["RustCrypto Developers"] +license = "MIT OR Apache-2.0" +description = "A utility for converting Wycheproof test vectors to the blobby format" +repository = "https://github.com/RustCrypto/utils" +keywords = ["test", "Wycheproof"] +categories = ["cryptography", "no-std"] +edition = "2018" + +[dependencies] +blobby = { version = "*", path = "../blobby" } +hex = "*" +serde = { version = "*", features = ["derive"] } +serde_json = "*" diff --git a/wycheproof2blb/src/aead.rs b/wycheproof2blb/src/aead.rs new file mode 100644 index 00000000..d8de26b8 --- /dev/null +++ b/wycheproof2blb/src/aead.rs @@ -0,0 +1,89 @@ +use crate::wycheproof; +use crate::wycheproof::{case_result, description, hex_string}; +use crate::TestInfo; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct TestSuite { + #[serde(flatten)] + pub suite: wycheproof::Suite, + #[serde(rename = "testGroups")] + pub test_groups: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct TestGroup { + #[serde(flatten)] + pub group: wycheproof::Group, + #[serde(rename = "ivSize")] + pub iv_size: u32, + #[serde(rename = "keySize")] + pub key_size: u32, + #[serde(rename = "tagSize")] + pub tag_size: u32, + pub tests: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct TestCase { + #[serde(flatten)] + pub case: wycheproof::Case, + #[serde(with = "hex_string")] + pub aad: Vec, + #[serde(with = "hex_string")] + pub ct: Vec, + #[serde(with = "hex_string")] + pub iv: Vec, + #[serde(with = "hex_string")] + pub key: Vec, + #[serde(with = "hex_string")] + pub msg: Vec, + #[serde(with = "hex_string")] + pub tag: Vec, +} + +pub fn aes_gcm_generator(data: &[u8], algorithm: &str, key_size: u32) -> Vec { + generator(data, algorithm, key_size, 12 * 8) +} + +pub fn chacha20_poly1305(data: &[u8], algorithm: &str, _key_size: u32) -> Vec { + generator(data, algorithm, 256, 12 * 8) +} + +pub fn xchacha20_poly1305(data: &[u8], algorithm: &str, _key_size: u32) -> Vec { + generator(data, algorithm, 256, 24 * 8) +} + +fn generator(data: &[u8], algorithm: &str, key_size: u32, iv_size: u32) -> Vec { + let suite: TestSuite = serde_json::from_slice(data).unwrap(); + assert_eq!(algorithm, suite.suite.algorithm); + + let mut infos = vec![]; + for g in &suite.test_groups { + for tc in &g.tests { + if key_size != 0 && g.key_size != key_size { + continue; + } + if g.iv_size != iv_size { + println!(" skipping tests for iv_size={}", g.iv_size); + continue; + } + let mut combined_ct = Vec::new(); + combined_ct.extend_from_slice(&tc.ct); + combined_ct.extend_from_slice(&tc.tag); + + infos.push(TestInfo { + data: vec![ + tc.key.clone(), + tc.iv.clone(), + tc.aad.clone(), + tc.msg.clone(), + combined_ct, + vec![case_result(&tc.case)], + ], + desc: description(&suite.suite, &tc.case), + }); + } + } + infos +} diff --git a/wycheproof2blb/src/aes_siv.rs b/wycheproof2blb/src/aes_siv.rs new file mode 100644 index 00000000..e4d399d5 --- /dev/null +++ b/wycheproof2blb/src/aes_siv.rs @@ -0,0 +1,60 @@ +use crate::wycheproof; +use crate::wycheproof::{case_result, description, hex_string}; +use crate::TestInfo; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +struct TestSuite { + #[serde(flatten)] + pub suite: wycheproof::Suite, + #[serde(rename = "testGroups")] + pub test_groups: Vec, +} + +#[derive(Debug, Deserialize)] +struct TestGroup { + #[serde(flatten)] + pub group: wycheproof::Group, + #[serde(rename = "keySize")] + pub key_size: u32, + pub tests: Vec, +} + +#[derive(Debug, Deserialize)] +struct TestCase { + #[serde(flatten)] + pub case: wycheproof::Case, + #[serde(with = "hex_string")] + pub key: Vec, + #[serde(with = "hex_string")] + pub aad: Vec, + #[serde(with = "hex_string")] + pub msg: Vec, + #[serde(with = "hex_string")] + pub ct: Vec, +} + +pub fn generator(data: &[u8], algorithm: &str, key_size: u32) -> Vec { + let suite: TestSuite = serde_json::from_slice(data).unwrap(); + assert_eq!(algorithm, suite.suite.algorithm); + + let mut infos = vec![]; + for g in &suite.test_groups { + if key_size != 0 && g.key_size != key_size { + continue; + } + for tc in &g.tests { + infos.push(TestInfo { + data: vec![ + tc.key.clone(), + tc.aad.clone(), + tc.msg.clone(), + tc.ct.clone(), + vec![case_result(&tc.case)], + ], + desc: description(&suite.suite, &tc.case), + }); + } + } + infos +} diff --git a/wycheproof2blb/src/ecdsa.rs b/wycheproof2blb/src/ecdsa.rs new file mode 100644 index 00000000..c9877d99 --- /dev/null +++ b/wycheproof2blb/src/ecdsa.rs @@ -0,0 +1,73 @@ +use crate::wycheproof; +use crate::wycheproof::{case_result, description, hex_string}; +use crate::TestInfo; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +struct TestSuite { + #[serde(flatten)] + pub suite: wycheproof::Suite, + #[serde(rename = "testGroups")] + pub test_groups: Vec, +} + +#[derive(Debug, Deserialize)] +struct TestGroup { + #[serde(flatten)] + pub group: wycheproof::Group, + #[serde(rename = "keyDer")] + pub key_der: String, + #[serde(rename = "keyPem")] + pub key_pem: String, + pub sha: String, + pub key: TestKey, + pub tests: Vec, +} + +#[derive(Debug, Deserialize)] +struct TestKey { + curve: String, + #[serde(rename = "type")] + key_type: String, + #[serde(with = "hex_string")] + wx: Vec, + #[serde(with = "hex_string")] + wy: Vec, +} + +#[derive(Debug, Deserialize)] +struct TestCase { + #[serde(flatten)] + pub case: wycheproof::Case, + #[serde(with = "hex_string")] + pub msg: Vec, + #[serde(with = "hex_string")] + pub sig: Vec, +} + +pub fn generator(data: &[u8], algorithm: &str, _key_size: u32) -> Vec { + let suite: TestSuite = serde_json::from_slice(data).unwrap(); + + let mut infos = vec![]; + for g in &suite.test_groups { + assert_eq!(g.key.curve, algorithm); + assert_eq!(g.sha, "SHA-256"); + for tc in &g.tests { + if tc.case.result == crate::wycheproof::CaseResult::Acceptable { + // TODO: figure out what to do with test cases that pass but which have weak params + continue; + } + infos.push(TestInfo { + data: vec![ + g.key.wx.clone(), + g.key.wy.clone(), + tc.msg.clone(), + tc.sig.clone(), + vec![case_result(&tc.case)], + ], + desc: description(&suite.suite, &tc.case), + }); + } + } + infos +} diff --git a/wycheproof2blb/src/ed25519.rs b/wycheproof2blb/src/ed25519.rs new file mode 100644 index 00000000..9ae1e235 --- /dev/null +++ b/wycheproof2blb/src/ed25519.rs @@ -0,0 +1,64 @@ +use crate::wycheproof; +use crate::wycheproof::{case_result, description, hex_string}; +use crate::TestInfo; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +struct TestSuite { + #[serde(flatten)] + pub suite: wycheproof::Suite, + #[serde(rename = "testGroups")] + pub test_groups: Vec, +} + +#[derive(Debug, Deserialize)] +struct TestGroup { + #[serde(flatten)] + pub group: wycheproof::Group, + #[serde(rename = "keyDer")] + pub key_der: String, + #[serde(rename = "keyPem")] + pub key_pem: String, + pub key: TestKey, + pub tests: Vec, +} + +#[derive(Debug, Deserialize)] +struct TestKey { + #[serde(with = "hex_string")] + sk: Vec, + #[serde(with = "hex_string")] + pk: Vec, +} + +#[derive(Debug, Deserialize)] +struct TestCase { + #[serde(flatten)] + pub case: wycheproof::Case, + #[serde(with = "hex_string")] + pub msg: Vec, + #[serde(with = "hex_string")] + pub sig: Vec, +} + +pub fn generator(data: &[u8], algorithm: &str, _key_size: u32) -> Vec { + let suite: TestSuite = serde_json::from_slice(data).unwrap(); + assert_eq!(algorithm, suite.suite.algorithm); + + let mut infos = vec![]; + for g in &suite.test_groups { + for tc in &g.tests { + infos.push(TestInfo { + data: vec![ + g.key.sk.clone(), + g.key.pk.clone(), + tc.msg.clone(), + tc.sig.clone(), + vec![case_result(&tc.case)], + ], + desc: description(&suite.suite, &tc.case), + }); + } + } + infos +} diff --git a/wycheproof2blb/src/hkdf.rs b/wycheproof2blb/src/hkdf.rs new file mode 100644 index 00000000..4901d92c --- /dev/null +++ b/wycheproof2blb/src/hkdf.rs @@ -0,0 +1,68 @@ +use crate::wycheproof; +use crate::wycheproof::{description, hex_string}; +use crate::TestInfo; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +struct TestSuite { + #[serde(flatten)] + pub suite: wycheproof::Suite, + #[serde(rename = "testGroups")] + pub test_groups: Vec, +} + +#[derive(Debug, Deserialize)] +struct TestGroup { + #[serde(flatten)] + pub group: wycheproof::Group, + #[serde(rename = "keySize")] + pub key_size: u32, + pub tests: Vec, +} + +#[derive(Debug, Deserialize)] +struct TestCase { + #[serde(flatten)] + pub case: wycheproof::Case, + #[serde(with = "hex_string")] + pub ikm: Vec, + #[serde(with = "hex_string")] + pub salt: Vec, + #[serde(with = "hex_string")] + pub info: Vec, + pub size: usize, + #[serde(with = "hex_string")] + pub okm: Vec, +} + +pub fn generator(data: &[u8], algorithm: &str, _key_size: u32) -> Vec { + let suite: TestSuite = serde_json::from_slice(data).unwrap(); + assert_eq!(algorithm, suite.suite.algorithm); + + let mut infos = vec![]; + for g in &suite.test_groups { + for tc in &g.tests { + if tc.case.result != crate::wycheproof::CaseResult::Valid { + continue; + } + if tc.okm.len() != tc.size { + eprintln!( + "Skipping case {} with size={} != okm.len()={}", + tc.case.case_id, + tc.size, + tc.okm.len() + ); + } + infos.push(TestInfo { + data: vec![ + tc.ikm.clone(), + tc.salt.clone(), + tc.info.clone(), + tc.okm.clone(), + ], + desc: description(&suite.suite, &tc.case), + }); + } + } + infos +} diff --git a/wycheproof2blb/src/mac.rs b/wycheproof2blb/src/mac.rs new file mode 100644 index 00000000..4def17f2 --- /dev/null +++ b/wycheproof2blb/src/mac.rs @@ -0,0 +1,66 @@ +use crate::wycheproof; +use crate::wycheproof::{description, hex_string, CaseResult}; +use crate::TestInfo; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct TestSuite { + #[serde(flatten)] + pub suite: wycheproof::Suite, + #[serde(rename = "testGroups")] + pub test_groups: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct TestGroup { + #[serde(flatten)] + pub group: wycheproof::Group, + #[serde(rename = "keySize")] + pub key_size: u32, + #[serde(rename = "tagSize")] + pub tag_size: u32, + pub tests: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct TestCase { + #[serde(flatten)] + pub case: wycheproof::Case, + #[serde(with = "hex_string")] + pub key: Vec, + #[serde(with = "hex_string")] + pub msg: Vec, + #[serde(with = "hex_string")] + pub tag: Vec, +} + +pub fn generator(data: &[u8], algorithm: &str, key_size: u32) -> Vec { + let suite: TestSuite = serde_json::from_slice(data).unwrap(); + assert_eq!(algorithm, suite.suite.algorithm); + + let mut infos = vec![]; + for g in &suite.test_groups { + for tc in &g.tests { + if key_size != 0 && g.key_size != key_size { + continue; + } + if tc.case.result != CaseResult::Valid { + // TODO: adapt HMAC tests for invalid cases + continue; + } + assert_eq!(tc.key.len() * 8, g.key_size as usize); + assert_eq!(g.tag_size % 8, 0); + infos.push(TestInfo { + data: vec![ + tc.key.clone(), + tc.msg.clone(), + // `tc.tag` holds the MAC output truncated to `(g.tag_size / 8)` bytes + // (when this is smaller than MAC output size) + tc.tag.clone(), + ], + desc: description(&suite.suite, &tc.case), + }); + } + } + infos +} diff --git a/wycheproof2blb/src/main.rs b/wycheproof2blb/src/main.rs new file mode 100644 index 00000000..889626ed --- /dev/null +++ b/wycheproof2blb/src/main.rs @@ -0,0 +1,137 @@ +//! Tool to convert Wycheproof test vectors to raw hex format +use std::io::Write; + +mod aead; +mod aes_siv; +mod ecdsa; +mod ed25519; +mod hkdf; +mod mac; +mod wycheproof; + +/// Test information +pub struct TestInfo { + /// Raw data for the tests. + pub data: Vec>, + /// Test case description. + pub desc: String, +} + +/// Generator function which takes input parameters: +/// - contents of Wycheproof test data file +/// - algorithm name +/// - key size (in bits) to include +/// and returns the raw contents, together with a list of test identifiers (one per entry). +type BlbGenerator = fn(&[u8], &str, u32) -> Vec; + +struct Algorithm { + pub file: &'static str, + pub generator: BlbGenerator, +} + +fn main() { + let args: Vec = std::env::args().collect(); + let wycheproof_dir = args + .get(1) + .expect("Provide directory with wycheproof vectors"); + let algorithm = args.get(2).expect("Provide algorithm family"); + let key_size = args + .get(3) + .expect("Provide key size in bits, or 0 for all sizes") + .parse::() + .expect("Key size needs to be a number of bits"); + let out_path = args.get(4).expect("Provide path for output blobby file"); + let descriptions_path = args.get(5).expect("Provide path for descriptions file"); + + let algo = match algorithm.as_str() { + "AES-GCM" => Algorithm { + file: "aes_gcm_test.json", + generator: aead::aes_gcm_generator, + }, + "AES-GCM-SIV" => Algorithm { + file: "aes_gcm_siv_test.json", + generator: aead::aes_gcm_generator, + }, + "CHACHA20-POLY1305" => Algorithm { + file: "chacha20_poly1305_test.json", + generator: aead::chacha20_poly1305, + }, + "XCHACHA20-POLY1305" => Algorithm { + file: "xchacha20_poly1305_test.json", + generator: aead::xchacha20_poly1305, + }, + "AES-SIV-CMAC" => Algorithm { + file: "aes_siv_cmac_test.json", + generator: aes_siv::generator, + }, + "AES-CMAC" => Algorithm { + file: "aes_cmac_test.json", + generator: mac::generator, + }, + "HKDF-SHA-1" => Algorithm { + file: "hkdf_sha1_test.json", + generator: hkdf::generator, + }, + "HKDF-SHA-256" => Algorithm { + file: "hkdf_sha256_test.json", + generator: hkdf::generator, + }, + "HKDF-SHA-384" => Algorithm { + file: "hkdf_sha384_test.json", + generator: hkdf::generator, + }, + "HKDF-SHA-512" => Algorithm { + file: "hkdf_sha512_test.json", + generator: hkdf::generator, + }, + "HMACSHA1" => Algorithm { + file: "hmac_sha1_test.json", + generator: mac::generator, + }, + "HMACSHA224" => Algorithm { + file: "hmac_sha224_test.json", + generator: mac::generator, + }, + "HMACSHA256" => Algorithm { + file: "hmac_sha256_test.json", + generator: mac::generator, + }, + "HMACSHA384" => Algorithm { + file: "hmac_sha384_test.json", + generator: mac::generator, + }, + "HMACSHA512" => Algorithm { + file: "hmac_sha512_test.json", + generator: mac::generator, + }, + "EDDSA" => Algorithm { + file: "eddsa_test.json", + generator: ed25519::generator, + }, + "secp256r1" => Algorithm { + file: "ecdsa_secp256r1_sha256_test.json", + generator: ecdsa::generator, + }, + // There's also "ecdsa_secp256r1_sha256_p1363_test.json" with a different signature encoding. + "secp256k1" => Algorithm { + file: "ecdsa_secp256k1_sha256_test.json", + generator: ecdsa::generator, + }, + _ => panic!("Unrecognized algorithm '{}'", algorithm), + }; + + let data = wycheproof::data(wycheproof_dir, algo.file); + + let infos = (algo.generator)(&data, algorithm, key_size); + println!("Emitting {} test cases", infos.len()); + + let mut txt_file = std::fs::File::create(descriptions_path).unwrap(); + for info in &infos { + writeln!(&mut txt_file, "{}", info.desc).unwrap(); + } + + let mut out_file = std::fs::File::create(out_path).unwrap(); + let blobs: Vec> = infos.into_iter().map(|info| info.data).flatten().collect(); + let (blb_data, _) = blobby::encode_blobs(&blobs); + out_file.write_all(&blb_data).unwrap(); +} diff --git a/wycheproof2blb/src/wycheproof.rs b/wycheproof2blb/src/wycheproof.rs new file mode 100644 index 00000000..2b464a62 --- /dev/null +++ b/wycheproof2blb/src/wycheproof.rs @@ -0,0 +1,123 @@ +//! Helpers for retrieving Wycheproof test vectors. + +use serde::Deserialize; + +/// `Suite` represents the common elements of the top level object in a Wycheproof json +/// file. Implementations should embed (using `#[serde(flatten)]`) `Suite` in a struct +/// that strongly types the `testGroups` field. +#[derive(Debug, Deserialize)] +pub struct Suite { + pub algorithm: String, + #[serde(rename = "generatorVersion")] + pub generator_version: String, + #[serde(rename = "numberOfTests")] + pub number_of_tests: i32, + pub notes: std::collections::HashMap, +} + +/// `Group` represents the common elements of a testGroups object in a Wycheproof suite. +/// Implementations should embed (using `#[serde(flatten)]`) Group in a struct that +/// strongly types its list of cases. +#[derive(Debug, Deserialize)] +pub struct Group { + #[serde(rename = "type")] + pub group_type: String, +} + +/// `Result` represents the possible result values for a Wycheproof test case. +#[derive(Debug, PartialEq, Eq)] +pub enum CaseResult { + /// Test case is valid, the crypto operation should succeed. + Valid, + /// Test case is invalid; the crypto operation should fail. + Invalid, + /// Test case is valid, but uses weak parameters; the crypto operation might succeed + /// or fail depending on how strict the library is. + Acceptable, +} + +impl std::fmt::Display for CaseResult { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + CaseResult::Valid => "valid", + CaseResult::Invalid => "invalid", + CaseResult::Acceptable => "acceptable", + } + ) + } +} + +/// `Case` represents the common elements of a tests object in a Wycheproof group. +/// Implementations should embed (using `#[serde(flatten)]`) `Case` in a struct that +/// contains fields specific to the test type. +#[derive(Debug, Deserialize)] +pub struct Case { + #[serde(rename = "tcId")] + pub case_id: i32, + pub comment: String, + #[serde(with = "case_result")] + pub result: CaseResult, + #[serde(default)] + pub flags: Vec, +} + +pub mod hex_string { + //! Manual JSON deserialization implementation for hex strings. + use serde::Deserialize; + pub fn deserialize<'de, D: serde::Deserializer<'de>>( + deserializer: D, + ) -> Result, D::Error> { + let s = String::deserialize(deserializer)?; + ::hex::decode(&s).map_err(|_e| { + serde::de::Error::invalid_value(serde::de::Unexpected::Str(&s), &"hex data expected") + }) + } +} + +pub mod case_result { + //! Manual JSON deserialization for a `result` enum. + use serde::Deserialize; + pub fn deserialize<'de, D: serde::Deserializer<'de>>( + deserializer: D, + ) -> Result { + let s = String::deserialize(deserializer)?; + match s.as_ref() { + "valid" => Ok(super::CaseResult::Valid), + "invalid" => Ok(super::CaseResult::Invalid), + "acceptable" => Ok(super::CaseResult::Acceptable), + _ => Err(serde::de::Error::invalid_value( + serde::de::Unexpected::Str(&s), + &"unexpected result value", + )), + } + } +} + +/// Convert a `result` enum to a byte. +pub fn case_result(case: &Case) -> u8 { + match case.result { + CaseResult::Invalid => 0u8, + CaseResult::Valid => 1u8, + _ => panic!("Unexpected case result {}", case.result), + } +} + +/// Retrieve Wycheproof test vectors from the given filename in a Wycheproof repo. +pub fn data(wycheproof_dir: &str, filename: &str) -> Vec { + let path = std::path::Path::new(&wycheproof_dir) + .join("testvectors") + .join(filename); + std::fs::read(&path) + .unwrap_or_else(|_| panic!("Test vector file {} not found at {:?}", filename, path)) +} + +/// Build a description for a test case in a suite +pub fn description(suite: &Suite, case: &Case) -> String { + format!( + "{} case {} [{}] {}", + suite.algorithm, case.case_id, case.result, case.comment + ) +}