fix(csprng): make Seed interface less confusing wrt endianness

- From a user perspective giving the same u128 seed e.g. 1u128 should have
the same behavior no matter the endianness of the system
This commit is contained in:
Arthur Meyre
2025-09-04 11:23:00 +02:00
parent 9c0d078e1a
commit a78d5cc57b
7 changed files with 76 additions and 11 deletions

View File

@@ -1,8 +1,16 @@
use crate::generators::aes_ctr::{AES_CALLS_PER_BATCH, BYTES_PER_AES_CALL, BYTES_PER_BATCH};
/// Represents a key used in the AES block cipher.
///
/// The u128 endianness should be ignored by implementations and the u128 should be seen as a simple
/// [u8; 16].
///
/// Therefore, except when loading the key from a [`Seed`](`crate::seeders::Seed`), whose bytes
/// needs to be loaded with [u128::from_le] (to keep consistency of the loaded bytes across systems
/// endianness), the rest of the code should use the [`AesKey`] with native endian ordering such
/// that the internal u128 is equivalent to [u8; 16].
#[derive(Clone, Copy)]
pub struct AesKey(pub u128);
pub(crate) struct AesKey(pub(crate) u128);
/// A trait for AES block ciphers.
///

View File

@@ -85,7 +85,15 @@ impl<BlockCipher: AesBlockCipher> AesCtrGenerator<BlockCipher> {
pub(crate) fn from_seed(seed: impl Into<SeedKind>) -> Self {
match seed.into() {
SeedKind::Ctr(seed) => Self::new(AesKey(seed.0), None, None),
SeedKind::Ctr(seed) => {
// AesKey has an unspoken requirement to have bytes in an order independent of
// platform endianness, problem is the Seed(u128) has an endianness, meaning
// 1u128 == [1, 0, ..., 0] for little endian
// but
// 1u128 == [0, ..., 0, 1] for big endian
let seed_u128 = u128::from_le(seed.0);
Self::new(AesKey(seed_u128), None, None)
}
SeedKind::Xof(seed) => {
let (key, init_index) = super::xof_init(seed);
let last_index = TableIndex::LAST.decremented();

View File

@@ -117,4 +117,9 @@ mod test {
fn test_vector_xof_seed() {
generator_generic_test::test_vectors_xof_seed::<NeonAesRandomGenerator>();
}
#[test]
fn test_vector_xof_seed_bytes() {
generator_generic_test::test_vectors_xof_seed_bytes::<NeonAesRandomGenerator>();
}
}

View File

@@ -117,4 +117,9 @@ mod test {
fn test_vector_xof_seed() {
generator_generic_test::test_vectors_xof_seed::<AesniRandomGenerator>();
}
#[test]
fn test_vector_xof_seed_bytes() {
generator_generic_test::test_vectors_xof_seed_bytes::<AesniRandomGenerator>();
}
}

View File

@@ -125,4 +125,9 @@ mod test {
fn test_vector_xof_seed() {
generator_generic_test::test_vectors_xof_seed::<SoftwareRandomGenerator>();
}
#[test]
fn test_vector_xof_seed_bytes() {
generator_generic_test::test_vectors_xof_seed_bytes::<SoftwareRandomGenerator>();
}
}

View File

@@ -265,8 +265,7 @@ pub mod generator_generic_test {
70, 110, 91, 181, 229, 231, 27, 225, 185, 143, 63, 238,
];
let seed_bytes: [u8; 16] = [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
let seed = Seed(u128::from_ne_bytes(seed_bytes));
let seed = Seed(1u128);
let mut rng = G::new(seed);
let bytes = rng.take(N_BYTES).collect::<Vec<_>>();
@@ -295,9 +294,41 @@ pub mod generator_generic_test {
162, 44, 120, 67, 241, 157, 31, 162, 113, 91,
];
let seed_bytes: [u8; 16] = [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
let seed = u128::from_ne_bytes(seed_bytes);
let xof_seed = XofSeed::new_u128(seed, [b'a', b'b', b'c', b'd', b'e', b'f', b'g', b'h']);
let seed = 1u128;
let xof_seed = XofSeed::new_u128(seed, *b"abcdefgh");
let mut rng = G::new(xof_seed);
let bytes = rng.take(N_BYTES).collect::<Vec<_>>();
assert_eq!(bytes, EXPECTED_BYTE);
}
pub fn test_vectors_xof_seed_bytes<G: RandomGenerator>() {
// Number of random bytes to generate,
// this should be 2 batch worth of aes calls (where a batch is 8 aes)
const N_BYTES: usize = 16 * 2 * 8;
const EXPECTED_BYTE: [u8; N_BYTES] = [
21, 82, 236, 82, 18, 196, 63, 129, 54, 134, 70, 114, 199, 200, 11, 5, 52, 170, 218, 49,
127, 45, 5, 252, 214, 82, 127, 196, 241, 83, 161, 79, 139, 183, 33, 122, 126, 177, 23,
36, 161, 122, 7, 112, 237, 154, 195, 90, 202, 218, 64, 90, 86, 190, 139, 169, 192, 105,
248, 220, 126, 133, 60, 124, 81, 72, 183, 238, 253, 138, 141, 144, 167, 168, 94, 19,
172, 92, 235, 113, 185, 31, 150, 143, 165, 220, 115, 83, 180, 1, 10, 130, 140, 32, 74,
132, 76, 22, 120, 126, 68, 154, 95, 61, 202, 79, 126, 38, 217, 181, 243, 6, 218, 75,
232, 235, 194, 255, 254, 184, 18, 122, 51, 222, 61, 167, 175, 97, 188, 186, 217, 105,
72, 205, 130, 3, 204, 157, 252, 27, 20, 212, 136, 70, 65, 215, 164, 130, 242, 107, 214,
150, 211, 59, 92, 13, 148, 219, 96, 181, 5, 38, 170, 48, 218, 111, 131, 246, 102, 169,
17, 182, 253, 41, 209, 185, 79, 245, 30, 142, 192, 127, 78, 178, 68, 223, 89, 210, 27,
84, 164, 163, 216, 188, 190, 128, 154, 224, 160, 53, 249, 10, 250, 95, 160, 94, 28, 41,
34, 254, 232, 137, 185, 82, 82, 192, 74, 197, 19, 46, 180, 169, 182, 216, 221, 127,
196, 185, 156, 82, 32, 133, 97, 140, 183, 67, 37, 110, 31, 210, 197, 27, 81, 197, 132,
136, 98, 78, 218, 252, 247, 239, 205, 21, 166, 218,
];
let seed = vec![
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
24, 25, 26, 27, 28, 29, 30, 31,
];
let xof_seed = XofSeed::new(seed, *b"abcdefgh");
let mut rng = G::new(xof_seed);
let bytes = rng.take(N_BYTES).collect::<Vec<_>>();

View File

@@ -30,7 +30,7 @@ impl XofSeed {
pub fn new_u128(seed: u128, domain_separator: [u8; Self::DOMAIN_SEP_LEN]) -> Self {
let mut data = vec![0u8; size_of::<u128>() + domain_separator.len()];
data[..Self::DOMAIN_SEP_LEN].copy_from_slice(domain_separator.as_slice());
data[Self::DOMAIN_SEP_LEN..].copy_from_slice(seed.to_ne_bytes().as_slice());
data[Self::DOMAIN_SEP_LEN..].copy_from_slice(seed.to_le_bytes().as_slice());
Self { data }
}
@@ -134,16 +134,19 @@ mod generic_tests {
#[test]
fn test_xof_seed_getters() {
let bits = u128::from_ne_bytes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]);
let seed_bytes = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
let bits = u128::from_le_bytes(seed_bytes);
let dsep = [b't', b'f', b'h', b'e', b'k', b's', b'p', b's'];
let seed = XofSeed::new_u128(bits, dsep);
let s = u128::from_ne_bytes(seed.seed().try_into().unwrap());
let s = u128::from_le_bytes(seed.seed().try_into().unwrap());
assert_eq!(s, bits);
assert_eq!(seed.domain_separator(), dsep);
assert_eq!(seed.bit_len(), 192);
let collected_u128s = seed.iter_u128_blocks().collect::<Vec<_>>();
// Those u128 are used in AES computations and are just a way to handle a [u8; 16] so those
// are ok to check in ne_bytes
assert_eq!(
collected_u128s,
vec![
@@ -155,7 +158,7 @@ mod generic_tests {
);
// To make sure both constructors yield the same results
let seed2 = XofSeed::new(bits.to_ne_bytes().to_vec(), dsep);
let seed2 = XofSeed::new(seed_bytes.to_vec(), dsep);
assert_eq!(seed.data, seed2.data);
}
}