From d0e1a582e140a7215ab34f415672cc3b5959d170 Mon Sep 17 00:00:00 2001 From: Arthur Meyre Date: Tue, 29 Aug 2023 15:13:42 +0200 Subject: [PATCH] chore(csprng): add code base taken from concrete-core repo --- Cargo.toml | 2 +- concrete-csprng/Cargo.toml | 51 +++ concrete-csprng/LICENSE | 28 ++ concrete-csprng/README.md | 23 ++ concrete-csprng/benches/benchmark.rs | 54 +++ concrete-csprng/build.rs | 112 +++++ .../src/generators/aes_ctr/block_cipher.rs | 20 + .../src/generators/aes_ctr/generic.rs | 379 +++++++++++++++++ .../src/generators/aes_ctr/index.rs | 389 ++++++++++++++++++ concrete-csprng/src/generators/aes_ctr/mod.rs | 223 ++++++++++ .../src/generators/aes_ctr/parallel.rs | 222 ++++++++++ .../src/generators/aes_ctr/states.rs | 176 ++++++++ .../generators/implem/aarch64/block_cipher.rs | 173 ++++++++ .../generators/implem/aarch64/generator.rs | 110 +++++ .../src/generators/implem/aarch64/mod.rs | 16 + .../src/generators/implem/aarch64/parallel.rs | 95 +++++ .../generators/implem/aesni/block_cipher.rs | 226 ++++++++++ .../src/generators/implem/aesni/generator.rs | 110 +++++ .../src/generators/implem/aesni/mod.rs | 15 + .../src/generators/implem/aesni/parallel.rs | 95 +++++ concrete-csprng/src/generators/implem/mod.rs | 14 + .../generators/implem/soft/block_cipher.rs | 114 +++++ .../src/generators/implem/soft/generator.rs | 110 +++++ .../src/generators/implem/soft/mod.rs | 11 + .../src/generators/implem/soft/parallel.rs | 94 +++++ concrete-csprng/src/generators/mod.rs | 235 +++++++++++ concrete-csprng/src/lib.rs | 114 +++++ concrete-csprng/src/main.rs | 58 +++ .../implem/apple_secure_enclave_seeder.rs | 141 +++++++ concrete-csprng/src/seeders/implem/mod.rs | 14 + concrete-csprng/src/seeders/implem/rdseed.rs | 50 +++ concrete-csprng/src/seeders/implem/unix.rs | 72 ++++ concrete-csprng/src/seeders/mod.rs | 47 +++ 33 files changed, 3592 insertions(+), 1 deletion(-) create mode 100644 concrete-csprng/Cargo.toml create mode 100644 concrete-csprng/LICENSE create mode 100644 concrete-csprng/README.md create mode 100644 concrete-csprng/benches/benchmark.rs create mode 100644 concrete-csprng/build.rs create mode 100644 concrete-csprng/src/generators/aes_ctr/block_cipher.rs create mode 100644 concrete-csprng/src/generators/aes_ctr/generic.rs create mode 100644 concrete-csprng/src/generators/aes_ctr/index.rs create mode 100644 concrete-csprng/src/generators/aes_ctr/mod.rs create mode 100644 concrete-csprng/src/generators/aes_ctr/parallel.rs create mode 100644 concrete-csprng/src/generators/aes_ctr/states.rs create mode 100644 concrete-csprng/src/generators/implem/aarch64/block_cipher.rs create mode 100644 concrete-csprng/src/generators/implem/aarch64/generator.rs create mode 100644 concrete-csprng/src/generators/implem/aarch64/mod.rs create mode 100644 concrete-csprng/src/generators/implem/aarch64/parallel.rs create mode 100644 concrete-csprng/src/generators/implem/aesni/block_cipher.rs create mode 100644 concrete-csprng/src/generators/implem/aesni/generator.rs create mode 100644 concrete-csprng/src/generators/implem/aesni/mod.rs create mode 100644 concrete-csprng/src/generators/implem/aesni/parallel.rs create mode 100644 concrete-csprng/src/generators/implem/mod.rs create mode 100644 concrete-csprng/src/generators/implem/soft/block_cipher.rs create mode 100644 concrete-csprng/src/generators/implem/soft/generator.rs create mode 100644 concrete-csprng/src/generators/implem/soft/mod.rs create mode 100644 concrete-csprng/src/generators/implem/soft/parallel.rs create mode 100644 concrete-csprng/src/generators/mod.rs create mode 100644 concrete-csprng/src/lib.rs create mode 100644 concrete-csprng/src/main.rs create mode 100644 concrete-csprng/src/seeders/implem/apple_secure_enclave_seeder.rs create mode 100644 concrete-csprng/src/seeders/implem/mod.rs create mode 100644 concrete-csprng/src/seeders/implem/rdseed.rs create mode 100644 concrete-csprng/src/seeders/implem/unix.rs create mode 100644 concrete-csprng/src/seeders/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 847933b96..f1cadb064 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["tfhe", "tasks", "apps/trivium"] +members = ["tfhe", "tasks", "apps/trivium", "concrete-csprng"] [profile.bench] lto = "fat" diff --git a/concrete-csprng/Cargo.toml b/concrete-csprng/Cargo.toml new file mode 100644 index 000000000..c40f323d3 --- /dev/null +++ b/concrete-csprng/Cargo.toml @@ -0,0 +1,51 @@ +[package] +name = "concrete-csprng" +version = "0.4.0" +edition = "2021" +license = "BSD-3-Clause-Clear" +description = "Cryptographically Secure PRNG used in the TFHE-rs library." +homepage = "https://zama.ai/" +documentation = "https://docs.zama.ai/tfhe-rs" +repository = "https://github.com/zama-ai/tfhe-rs" +readme = "README.md" +keywords = ["fully", "homomorphic", "encryption", "fhe", "cryptography"] + +[dependencies] +aes = "0.8.2" +rayon = { version = "1.5.0", optional = true } + +[target.'cfg(target_os = "macos")'.dependencies] +libc = "0.2.133" + +[dev-dependencies] +rand = "0.8.3" +criterion = "0.3" + +[features] +parallel = ["rayon"] +seeder_x86_64_rdseed = [] +seeder_unix = [] +generator_x86_64_aesni = [] +generator_fallback = [] +generator_aarch64_aes = [] + +x86_64 = [ + "parallel", + "seeder_x86_64_rdseed", + "generator_x86_64_aesni", + "generator_fallback", +] +x86_64-unix = ["x86_64", "seeder_unix"] +aarch64 = ["parallel", "generator_aarch64_aes", "generator_fallback"] +aarch64-unix = ["aarch64", "seeder_unix"] + +[[bench]] +name = "benchmark" +path = "benches/benchmark.rs" +harness = false +required-features = ["seeder_x86_64_rdseed", "generator_x86_64_aesni"] + +[[bin]] +name = "generate" +path = "src/main.rs" +required-features = ["seeder_unix", "generator_fallback"] diff --git a/concrete-csprng/LICENSE b/concrete-csprng/LICENSE new file mode 100644 index 000000000..c04b2b236 --- /dev/null +++ b/concrete-csprng/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause Clear License + +Copyright © 2023 ZAMA. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or other +materials provided with the distribution. + +3. Neither the name of ZAMA nor the names of its contributors may be used to endorse +or promote products derived from this software without specific prior written permission. + +NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY THIS LICENSE. +THIS SOFTWARE IS PROVIDED BY THE ZAMA AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +ZAMA OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, +OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/concrete-csprng/README.md b/concrete-csprng/README.md new file mode 100644 index 000000000..fe3e34ce5 --- /dev/null +++ b/concrete-csprng/README.md @@ -0,0 +1,23 @@ +# Concrete CSPRNG + +This crate contains a fast *Cryptographically Secure Pseudoramdon Number Generator*, used in the +['concrete-core'](https://crates.io/crates/concrete-core) library, you can find it [here](../concrete-core/) in this repo. + +The implementation is based on the AES blockcipher used in CTR mode, as described in the ISO/IEC +18033-4 standard. + +Two implementations are available, an accelerated one on x86_64 CPUs with the `aes` feature and the `sse2` feature, and a pure software one that can be used on other platforms. + +The crate also makes two seeders available, one needing the x86_64 feature `rdseed` and another one based on the Unix random device `/dev/random` the latter requires the user to provide a secret. + +## Running the benchmarks + +To execute the benchmarks on an x86_64 platform: +```shell +RUSTFLAGS="-Ctarget-cpu=native" cargo bench --features=seeder_x86_64_rdseed,generator_x86_64_aesni +``` + +## License + +This software is distributed under the BSD-3-Clause-Clear license. If you have any questions, +please contact us at `hello@zama.ai`. diff --git a/concrete-csprng/benches/benchmark.rs b/concrete-csprng/benches/benchmark.rs new file mode 100644 index 000000000..a66b02003 --- /dev/null +++ b/concrete-csprng/benches/benchmark.rs @@ -0,0 +1,54 @@ +use concrete_csprng::generators::{ + AesniRandomGenerator, BytesPerChild, ChildrenCount, RandomGenerator, +}; +use concrete_csprng::seeders::{RdseedSeeder, Seeder}; +use criterion::{black_box, criterion_group, criterion_main, Criterion}; + +// The number of bytes to generate during one benchmark iteration. +const N_GEN: usize = 1_000_000; + +fn parent_generate(c: &mut Criterion) { + let mut seeder = RdseedSeeder; + let mut generator = AesniRandomGenerator::new(seeder.seed()); + c.bench_function("parent_generate", |b| { + b.iter(|| { + (0..N_GEN).for_each(|_| { + generator.next(); + }) + }) + }); +} + +fn child_generate(c: &mut Criterion) { + let mut seeder = RdseedSeeder; + let mut generator = AesniRandomGenerator::new(seeder.seed()); + let mut generator = generator + .try_fork(ChildrenCount(1), BytesPerChild(N_GEN * 10_000)) + .unwrap() + .next() + .unwrap(); + c.bench_function("child_generate", |b| { + b.iter(|| { + (0..N_GEN).for_each(|_| { + generator.next(); + }) + }) + }); +} + +fn fork(c: &mut Criterion) { + let mut seeder = RdseedSeeder; + let mut generator = AesniRandomGenerator::new(seeder.seed()); + c.bench_function("fork", |b| { + b.iter(|| { + black_box( + generator + .try_fork(ChildrenCount(2048), BytesPerChild(2048)) + .unwrap(), + ) + }) + }); +} + +criterion_group!(benches, parent_generate, child_generate, fork); +criterion_main!(benches); diff --git a/concrete-csprng/build.rs b/concrete-csprng/build.rs new file mode 100644 index 000000000..e6c243c48 --- /dev/null +++ b/concrete-csprng/build.rs @@ -0,0 +1,112 @@ +// To have clear error messages during compilation about why some piece of code may not be available +// we decided to check the features compatibility with the target configuration in this script. + +use std::collections::HashMap; +use std::env; + +// See https://doc.rust-lang.org/reference/conditional-compilation.html#target_arch for various +// compilation configuration + +// Can be easily extended if needed +pub struct FeatureRequirement { + pub feature_name: &'static str, + // target_arch requirement + pub feature_req_target_arch: Option<&'static str>, + // target_family requirement + pub feature_req_target_family: Option<&'static str>, +} + +// We implement a version of default that is const which is not possible through the Default trait +impl FeatureRequirement { + // As we cannot use cfg!(feature = "feature_name") with something else than a literal, we need + // a reference to the HashMap we populate with the enabled features + fn is_activated(&self, build_activated_features: &HashMap<&'static str, bool>) -> bool { + *build_activated_features.get(self.feature_name).unwrap() + } + + // panics if the requirements are not met + fn check_requirements(&self) { + let target_arch = get_target_arch_cfg(); + if let Some(feature_req_target_arch) = self.feature_req_target_arch { + if feature_req_target_arch != target_arch { + panic!( + "Feature `{}` requires target_arch `{}`, current cfg: `{}`", + self.feature_name, feature_req_target_arch, target_arch + ) + } + } + + let target_family = get_target_family_cfg(); + if let Some(feature_req_target_family) = self.feature_req_target_family { + if feature_req_target_family != target_family { + panic!( + "Feature `{}` requires target_family `{}`, current cfg: `{}`", + self.feature_name, feature_req_target_family, target_family + ) + } + } + } +} + +// const vecs are not yet a thing so use a fixed size array (update the array size when adding +// requirements) +static FEATURE_REQUIREMENTS: [FeatureRequirement; 4] = [ + FeatureRequirement { + feature_name: "seeder_x86_64_rdseed", + feature_req_target_arch: Some("x86_64"), + feature_req_target_family: None, + }, + FeatureRequirement { + feature_name: "generator_x86_64_aesni", + feature_req_target_arch: Some("x86_64"), + feature_req_target_family: None, + }, + FeatureRequirement { + feature_name: "seeder_unix", + feature_req_target_arch: None, + feature_req_target_family: Some("unix"), + }, + FeatureRequirement { + feature_name: "generator_aarch64_aes", + feature_req_target_arch: Some("aarch64"), + feature_req_target_family: None, + }, +]; + +// For a "feature_name" feature_cfg!("feature_name") expands to +// ("feature_name", cfg!(feature = "feature_name")) +macro_rules! feature_cfg { + ($feat_name:literal) => { + ($feat_name, cfg!(feature = $feat_name)) + }; +} + +// Static HashMap would require an additional crate (phf or lazy static e.g.), so we just write a +// function that returns the HashMap we are interested in +fn get_feature_enabled_status() -> HashMap<&'static str, bool> { + HashMap::from([ + feature_cfg!("seeder_x86_64_rdseed"), + feature_cfg!("generator_x86_64_aesni"), + feature_cfg!("seeder_unix"), + feature_cfg!("generator_aarch64_aes"), + ]) +} + +// See https://stackoverflow.com/a/43435335/18088947 for the inspiration of this code +fn get_target_arch_cfg() -> String { + env::var("CARGO_CFG_TARGET_ARCH").expect("CARGO_CFG_TARGET_ARCH is not set") +} + +fn get_target_family_cfg() -> String { + env::var("CARGO_CFG_TARGET_FAMILY").expect("CARGO_CFG_TARGET_FAMILY is not set") +} + +fn main() { + let feature_enabled_status = get_feature_enabled_status(); + + // This will panic if some requirements for a feature are not met + FEATURE_REQUIREMENTS + .iter() + .filter(|&req| FeatureRequirement::is_activated(req, &feature_enabled_status)) + .for_each(FeatureRequirement::check_requirements); +} diff --git a/concrete-csprng/src/generators/aes_ctr/block_cipher.rs b/concrete-csprng/src/generators/aes_ctr/block_cipher.rs new file mode 100644 index 000000000..d05aedd5c --- /dev/null +++ b/concrete-csprng/src/generators/aes_ctr/block_cipher.rs @@ -0,0 +1,20 @@ +use crate::generators::aes_ctr::index::AesIndex; +use crate::generators::aes_ctr::BYTES_PER_BATCH; + +/// Represents a key used in the AES block cipher. +#[derive(Clone, Copy)] +pub struct AesKey(pub u128); + +/// A trait for AES block ciphers. +/// +/// Note: +/// ----- +/// +/// The block cipher is used in a batched manner (to reduce amortized cost on special hardware). +/// For this reason we only expose a `generate_batch` method. +pub trait AesBlockCipher: Clone + Send + Sync { + /// Instantiate a new generator from a secret key. + fn new(key: AesKey) -> Self; + /// Generates the batch corresponding to the given index. + fn generate_batch(&mut self, index: AesIndex) -> [u8; BYTES_PER_BATCH]; +} diff --git a/concrete-csprng/src/generators/aes_ctr/generic.rs b/concrete-csprng/src/generators/aes_ctr/generic.rs new file mode 100644 index 000000000..770b62770 --- /dev/null +++ b/concrete-csprng/src/generators/aes_ctr/generic.rs @@ -0,0 +1,379 @@ +use crate::generators::aes_ctr::block_cipher::{AesBlockCipher, AesKey}; +use crate::generators::aes_ctr::index::TableIndex; +use crate::generators::aes_ctr::states::{BufferPointer, ShiftAction, State}; +use crate::generators::aes_ctr::BYTES_PER_BATCH; +use crate::generators::{ByteCount, BytesPerChild, ChildrenCount, ForkError}; + +// Usually, to work with iterators and parallel iterators, we would use opaque types such as +// `impl Iterator<..>`. Unfortunately, it is not yet possible to return existential types in +// traits, which we would need for `RandomGenerator`. For this reason, we have to use the +// full type name where needed. Hence the following trait aliases definition: + +/// A type alias for the children iterator closure type. +pub type ChildrenClosure = + fn((usize, (Box, TableIndex, BytesPerChild))) -> AesCtrGenerator; + +/// A type alias for the children iterator type. +pub type ChildrenIterator = std::iter::Map< + std::iter::Zip< + std::ops::Range, + std::iter::Repeat<(Box, TableIndex, BytesPerChild)>, + >, + ChildrenClosure, +>; + +/// A type implementing the `RandomGenerator` api using the AES block cipher in counter mode. +#[derive(Clone)] +pub struct AesCtrGenerator { + // The block cipher used in the background + pub(crate) block_cipher: Box, + // The state corresponding to the latest outputted byte. + pub(crate) state: State, + // The last legal index. This makes bound check faster. + pub(crate) last: TableIndex, + // The buffer containing the current batch of aes calls. + pub(crate) buffer: [u8; BYTES_PER_BATCH], +} + +#[allow(unused)] // to please clippy when tests are not activated +impl AesCtrGenerator { + /// Generates a new csprng. + /// + /// Note : + /// ------ + /// + /// The `start_index` given as input, points to the first byte that will be outputted by the + /// generator. If not given, this one is automatically set to the second table index. The + /// first table index is not used to prevent an edge case from happening: since `state` is + /// supposed to contain the index of the previous byte, the initial value must be decremented. + /// Using the second value prevents wrapping to the max index, which would make the bound + /// checking fail. + /// + /// The `bound_index` given as input, points to the first byte that can __not__ be legally + /// outputted by the generator. If not given, the bound is automatically set to the last + /// table index. + pub fn new( + key: AesKey, + start_index: Option, + bound_index: Option, + ) -> AesCtrGenerator { + AesCtrGenerator::from_block_cipher( + Box::new(BlockCipher::new(key)), + start_index.unwrap_or(TableIndex::SECOND), + bound_index.unwrap_or(TableIndex::LAST), + ) + } + + /// Generates a csprng from an existing block cipher. + pub fn from_block_cipher( + block_cipher: Box, + start_index: TableIndex, + bound_index: TableIndex, + ) -> AesCtrGenerator { + assert!(start_index < bound_index); + let last = bound_index.decremented(); + let buffer = [0u8; BYTES_PER_BATCH]; + let state = State::new(start_index); + AesCtrGenerator { + block_cipher, + state, + last, + buffer, + } + } + + /// Returns the table index related to the previous random byte. + pub fn table_index(&self) -> TableIndex { + self.state.table_index() + } + + /// Returns the bound of the generator if any. + /// + /// The bound is the table index of the first byte that can not be outputted by the generator. + pub fn get_bound(&self) -> TableIndex { + self.last.incremented() + } + + /// Returns whether the generator is bounded or not. + pub fn is_bounded(&self) -> bool { + self.get_bound() != TableIndex::LAST + } + + /// Computes the number of bytes that can still be outputted by the generator. + /// + /// Note : + /// ------ + /// + /// Note that `ByteCount` uses the `u128` datatype to store the byte count. Unfortunately, the + /// number of remaining bytes is in ⟦0;2¹³² -1⟧. When the number is greater than 2¹²⁸ - 1, + /// we saturate the count at 2¹²⁸ - 1. + pub fn remaining_bytes(&self) -> ByteCount { + TableIndex::distance(&self.last, &self.state.table_index()).unwrap() + } + + /// Outputs the next random byte. + pub fn generate_next(&mut self) -> u8 { + self.next() + .expect("Tried to generate a byte after the bound.") + } + + /// Tries to fork the current generator into `n_child` generators each able to output + /// `child_bytes` random bytes. + pub fn try_fork( + &mut self, + n_children: ChildrenCount, + n_bytes: BytesPerChild, + ) -> Result, ForkError> { + if n_children.0 == 0 { + return Err(ForkError::ZeroChildrenCount); + } + if n_bytes.0 == 0 { + return Err(ForkError::ZeroBytesPerChild); + } + if !self.is_fork_in_bound(n_children, n_bytes) { + return Err(ForkError::ForkTooLarge); + } + + // The state currently stored in the parent generator points to the table index of the last + // generated byte. The first index to be generated is the next one: + let first_index = self.state.table_index().incremented(); + let output = (0..n_children.0) + .zip(std::iter::repeat(( + self.block_cipher.clone(), + first_index, + n_bytes, + ))) + .map( + // This map is a little weird because we need to cast the closure to a fn pointer + // that matches the signature of `ChildrenIterator`. + // Unfortunately, the compiler does not manage to coerce this one + // automatically. + (|(i, (block_cipher, first_index, n_bytes))| { + // The first index to be outputted by the child is the `first_index` shifted by + // the proper amount of `child_bytes`. + let child_first_index = first_index.increased(n_bytes.0 * i); + // The bound of the child is the first index of its next sibling. + let child_bound_index = first_index.increased(n_bytes.0 * (i + 1)); + AesCtrGenerator::from_block_cipher( + block_cipher, + child_first_index, + child_bound_index, + ) + }) as ChildrenClosure, + ); + // The parent next index is the bound of the last child. + let next_index = first_index.increased(n_bytes.0 * n_children.0); + self.state = State::new(next_index); + + Ok(output) + } + + pub(crate) fn is_fork_in_bound( + &self, + n_child: ChildrenCount, + child_bytes: BytesPerChild, + ) -> bool { + let mut end = self.state.table_index(); + end.increase(n_child.0 * child_bytes.0); + end <= self.last + } +} + +impl Iterator for AesCtrGenerator { + type Item = u8; + + fn next(&mut self) -> Option { + if self.state.table_index() >= self.last { + None + } else { + match self.state.increment() { + ShiftAction::OutputByte(BufferPointer(ptr)) => Some(self.buffer[ptr]), + ShiftAction::RefreshBatchAndOutputByte(aes_index, BufferPointer(ptr)) => { + self.buffer = self.block_cipher.generate_batch(aes_index); + Some(self.buffer[ptr]) + } + } + } + } +} + +#[cfg(test)] +pub mod aes_ctr_generic_test { + #![allow(unused)] // to please clippy when tests are not activated + + use super::*; + use crate::generators::aes_ctr::index::{AesIndex, ByteIndex}; + use crate::generators::aes_ctr::BYTES_PER_AES_CALL; + use rand::{thread_rng, Rng}; + + const REPEATS: usize = 1_000_000; + + pub fn any_table_index() -> impl Iterator { + std::iter::repeat_with(|| { + TableIndex::new( + AesIndex(thread_rng().gen()), + ByteIndex(thread_rng().gen::() % BYTES_PER_AES_CALL), + ) + }) + } + + pub fn any_usize() -> impl Iterator { + std::iter::repeat_with(|| thread_rng().gen()) + } + + pub fn any_children_count() -> impl Iterator { + std::iter::repeat_with(|| ChildrenCount(thread_rng().gen::() % 2048 + 1)) + } + + pub fn any_bytes_per_child() -> impl Iterator { + std::iter::repeat_with(|| BytesPerChild(thread_rng().gen::() % 2048 + 1)) + } + + pub fn any_key() -> impl Iterator { + std::iter::repeat_with(|| AesKey(thread_rng().gen())) + } + + /// Output a valid fork: + /// a table index t, + /// a number of children nc, + /// a number of bytes per children nb + /// and a positive integer i such that: + /// increase(t, nc*nb+i) < MAX with MAX the largest table index. + /// + /// Put differently, if we initialize a parent generator at t and fork it with (nc, nb), our + /// parent generator current index gets shifted to an index, distant of at least i bytes of + /// the max index. + pub fn any_valid_fork( + ) -> impl Iterator { + any_table_index() + .zip(any_children_count()) + .zip(any_bytes_per_child()) + .zip(any_usize()) + .map(|(((t, nc), nb), i)| (t, nc, nb, i)) + .filter(|(t, nc, nb, i)| { + TableIndex::distance(&TableIndex::LAST, t).unwrap().0 > (nc.0 * nb.0 + i) as u128 + }) + } + + /// Check the property: + /// On a valid fork, the table index of the first child is the same as the table index of + /// the parent before the fork. + pub fn prop_fork_first_state_table_index() { + for _ in 0..REPEATS { + let (t, nc, nb, i) = any_valid_fork().next().unwrap(); + let k = any_key().next().unwrap(); + let original_generator = + AesCtrGenerator::::new(k, Some(t), Some(t.increased(nc.0 * nb.0 + i))); + let mut forked_generator = original_generator.clone(); + let first_child = forked_generator.try_fork(nc, nb).unwrap().next().unwrap(); + assert_eq!(original_generator.table_index(), first_child.table_index()); + } + } + + /// Check the property: + /// On a valid fork, the table index of the first byte outputted by the parent after the + /// fork, is the bound of the last child of the fork. + pub fn prop_fork_last_bound_table_index() { + for _ in 0..REPEATS { + let (t, nc, nb, i) = any_valid_fork().next().unwrap(); + let k = any_key().next().unwrap(); + let mut parent_generator = + AesCtrGenerator::::new(k, Some(t), Some(t.increased(nc.0 * nb.0 + i))); + let last_child = parent_generator.try_fork(nc, nb).unwrap().last().unwrap(); + assert_eq!( + parent_generator.table_index().incremented(), + last_child.get_bound() + ); + } + } + + /// Check the property: + /// On a valid fork, the bound of the parent does not change. + pub fn prop_fork_parent_bound_table_index() { + for _ in 0..REPEATS { + let (t, nc, nb, i) = any_valid_fork().next().unwrap(); + let k = any_key().next().unwrap(); + let original_generator = + AesCtrGenerator::::new(k, Some(t), Some(t.increased(nc.0 * nb.0 + i))); + let mut forked_generator = original_generator.clone(); + forked_generator.try_fork(nc, nb).unwrap().last().unwrap(); + assert_eq!(original_generator.get_bound(), forked_generator.get_bound()); + } + } + + /// Check the property: + /// On a valid fork, the parent table index is increased of the number of children + /// multiplied by the number of bytes per child. + pub fn prop_fork_parent_state_table_index() { + for _ in 0..REPEATS { + let (t, nc, nb, i) = any_valid_fork().next().unwrap(); + let k = any_key().next().unwrap(); + let original_generator = + AesCtrGenerator::::new(k, Some(t), Some(t.increased(nc.0 * nb.0 + i))); + let mut forked_generator = original_generator.clone(); + forked_generator.try_fork(nc, nb).unwrap().last().unwrap(); + assert_eq!( + forked_generator.table_index(), + // Decrement accounts for the fact that the table index stored is the previous one + t.increased(nc.0 * nb.0).decremented() + ); + } + } + + /// Check the property: + /// On a valid fork, the bytes outputted by the children in the fork order form the same + /// sequence the parent would have had yielded no fork had happened. + pub fn prop_fork() { + for _ in 0..1000 { + let (t, nc, nb, i) = any_valid_fork().next().unwrap(); + let k = any_key().next().unwrap(); + let bytes_to_go = nc.0 * nb.0; + let original_generator = + AesCtrGenerator::::new(k, Some(t), Some(t.increased(nc.0 * nb.0 + i))); + let mut forked_generator = original_generator.clone(); + let initial_output: Vec = original_generator.take(bytes_to_go).collect(); + let forked_output: Vec = forked_generator + .try_fork(nc, nb) + .unwrap() + .flat_map(|child| child.collect::>()) + .collect(); + assert_eq!(initial_output, forked_output); + } + } + + /// Check the property: + /// On a valid fork, all children got a number of remaining bytes equals to the number of + /// bytes per child given as fork input. + pub fn prop_fork_children_remaining_bytes() { + for _ in 0..REPEATS { + let (t, nc, nb, i) = any_valid_fork().next().unwrap(); + let k = any_key().next().unwrap(); + let mut generator = + AesCtrGenerator::::new(k, Some(t), Some(t.increased(nc.0 * nb.0 + i))); + assert!(generator + .try_fork(nc, nb) + .unwrap() + .all(|c| c.remaining_bytes().0 == nb.0 as u128)); + } + } + + /// Check the property: + /// On a valid fork, the number of remaining bybtes of the parent is reduced by the number + /// of children multiplied by the number of bytes per child. + pub fn prop_fork_parent_remaining_bytes() { + for _ in 0..REPEATS { + let (t, nc, nb, i) = any_valid_fork().next().unwrap(); + let k = any_key().next().unwrap(); + let bytes_to_go = nc.0 * nb.0; + let mut generator = + AesCtrGenerator::::new(k, Some(t), Some(t.increased(nc.0 * nb.0 + i))); + let before_remaining_bytes = generator.remaining_bytes(); + let _ = generator.try_fork(nc, nb).unwrap(); + let after_remaining_bytes = generator.remaining_bytes(); + assert_eq!( + before_remaining_bytes.0 - after_remaining_bytes.0, + bytes_to_go as u128 + ); + } + } +} diff --git a/concrete-csprng/src/generators/aes_ctr/index.rs b/concrete-csprng/src/generators/aes_ctr/index.rs new file mode 100644 index 000000000..e95b26f0d --- /dev/null +++ b/concrete-csprng/src/generators/aes_ctr/index.rs @@ -0,0 +1,389 @@ +use crate::generators::aes_ctr::BYTES_PER_AES_CALL; +use crate::generators::ByteCount; +use std::cmp::Ordering; + +/// A structure representing an [aes index](#coarse-grained-pseudo-random-table-lookup). +#[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq)] +pub struct AesIndex(pub u128); + +/// A structure representing a [byte index](#fine-grained-pseudo-random-table-lookup). +#[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq)] +pub struct ByteIndex(pub usize); + +/// A structure representing a [table index](#fine-grained-pseudo-random-table-lookup) +#[derive(Clone, Copy, Debug)] +pub struct TableIndex { + pub(crate) aes_index: AesIndex, + pub(crate) byte_index: ByteIndex, +} + +impl TableIndex { + /// The first table index. + pub const FIRST: TableIndex = TableIndex { + aes_index: AesIndex(0), + byte_index: ByteIndex(0), + }; + + /// The second table index. + pub const SECOND: TableIndex = TableIndex { + aes_index: AesIndex(0), + byte_index: ByteIndex(1), + }; + + /// The last table index. + pub const LAST: TableIndex = TableIndex { + aes_index: AesIndex(u128::MAX), + byte_index: ByteIndex(BYTES_PER_AES_CALL - 1), + }; + + /// Creates a table index from an aes index and a byte index. + #[allow(unused)] // to please clippy when tests are not activated + pub fn new(aes_index: AesIndex, byte_index: ByteIndex) -> Self { + assert!(byte_index.0 < BYTES_PER_AES_CALL); + TableIndex { + aes_index, + byte_index, + } + } + + /// Shifts the table index forward of `shift` bytes. + pub fn increase(&mut self, shift: usize) { + // Compute full shifts to avoid overflows + let full_aes_shifts = shift / BYTES_PER_AES_CALL; + let shift_remainder = shift % BYTES_PER_AES_CALL; + + // Get the additional shift if any + let new_byte_index = self.byte_index.0 + shift_remainder; + let full_aes_shifts = full_aes_shifts + new_byte_index / BYTES_PER_AES_CALL; + + // Store the reaminder in the byte index + self.byte_index.0 = new_byte_index % BYTES_PER_AES_CALL; + + self.aes_index.0 = self.aes_index.0.wrapping_add(full_aes_shifts as u128); + } + + /// Shifts the table index backward of `shift` bytes. + pub fn decrease(&mut self, shift: usize) { + let remainder = shift % BYTES_PER_AES_CALL; + if remainder <= self.byte_index.0 { + self.aes_index.0 = self + .aes_index + .0 + .wrapping_sub((shift / BYTES_PER_AES_CALL) as u128); + self.byte_index.0 -= remainder; + } else { + self.aes_index.0 = self + .aes_index + .0 + .wrapping_sub((shift / BYTES_PER_AES_CALL) as u128 + 1); + self.byte_index.0 += BYTES_PER_AES_CALL - remainder; + } + } + + /// Shifts the table index forward of one byte. + pub fn increment(&mut self) { + self.increase(1) + } + + /// Shifts the table index backward of one byte. + pub fn decrement(&mut self) { + self.decrease(1) + } + + /// Returns the table index shifted forward by `shift` bytes. + pub fn increased(mut self, shift: usize) -> Self { + self.increase(shift); + self + } + + /// Returns the table index shifted backward by `shift` bytes. + #[allow(unused)] // to please clippy when tests are not activated + pub fn decreased(mut self, shift: usize) -> Self { + self.decrease(shift); + self + } + + /// Returns the table index to the next byte. + pub fn incremented(mut self) -> Self { + self.increment(); + self + } + + /// Returns the table index to the previous byte. + pub fn decremented(mut self) -> Self { + self.decrement(); + self + } + + /// Returns the distance between two table indices in bytes. + /// + /// Note: + /// ----- + /// + /// This method assumes that the `larger` input is, well, larger than the `smaller` input. If + /// this is not the case, the method returns `None`. Also, note that `ByteCount` uses the + /// `u128` datatype to store the byte count. Unfortunately, the number of bytes between two + /// table indices is in ⟦0;2¹³² -1⟧. When the distance is greater than 2¹²⁸ - 1, we saturate + /// the count at 2¹²⁸ - 1. + pub fn distance(larger: &Self, smaller: &Self) -> Option { + match std::cmp::Ord::cmp(larger, smaller) { + Ordering::Less => None, + Ordering::Equal => Some(ByteCount(0)), + Ordering::Greater => { + let mut result = larger.aes_index.0 - smaller.aes_index.0; + result = result.saturating_mul(BYTES_PER_AES_CALL as u128); + result = result.saturating_add(larger.byte_index.0 as u128); + result = result.saturating_sub(smaller.byte_index.0 as u128); + Some(ByteCount(result)) + } + } + } +} + +impl Eq for TableIndex {} + +impl PartialEq for TableIndex { + fn eq(&self, other: &Self) -> bool { + matches!(self.partial_cmp(other), Some(Ordering::Equal)) + } +} + +impl PartialOrd for TableIndex { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for TableIndex { + fn cmp(&self, other: &Self) -> Ordering { + match self.aes_index.cmp(&other.aes_index) { + Ordering::Equal => self.byte_index.cmp(&other.byte_index), + other => other, + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use rand::{thread_rng, Rng}; + + const REPEATS: usize = 1_000_000; + + fn any_table_index() -> impl Iterator { + std::iter::repeat_with(|| { + TableIndex::new( + AesIndex(thread_rng().gen()), + ByteIndex(thread_rng().gen::() % BYTES_PER_AES_CALL), + ) + }) + } + + fn any_usize() -> impl Iterator { + std::iter::repeat_with(|| thread_rng().gen()) + } + + #[test] + #[should_panic] + /// Verifies that the constructor of `TableIndex` panics when the byte index is too large. + fn test_table_index_new_panic() { + TableIndex::new(AesIndex(12), ByteIndex(144)); + } + + #[test] + /// Verifies that the `TableIndex` wraps nicely with predecessor + fn test_table_index_predecessor_edge() { + assert_eq!(TableIndex::FIRST.decremented(), TableIndex::LAST); + } + + #[test] + /// Verifies that the `TableIndex` wraps nicely with successor + fn test_table_index_successor_edge() { + assert_eq!(TableIndex::LAST.incremented(), TableIndex::FIRST); + } + + #[test] + /// Check that the table index distance saturates nicely. + fn prop_table_index_distance_saturates() { + assert_eq!( + TableIndex::distance(&TableIndex::LAST, &TableIndex::FIRST) + .unwrap() + .0, + u128::MAX + ) + } + + #[test] + /// Check the property: + /// For all table indices t, + /// distance(t, t) = Some(0). + fn prop_table_index_distance_zero() { + for _ in 0..REPEATS { + let t = any_table_index().next().unwrap(); + assert_eq!(TableIndex::distance(&t, &t), Some(ByteCount(0))); + } + } + + #[test] + /// Check the property: + /// For all table indices t1, t2 such that t1 < t2, + /// distance(t1, t2) = None. + fn prop_table_index_distance_wrong_order_none() { + for _ in 0..REPEATS { + let (t1, t2) = any_table_index() + .zip(any_table_index()) + .find(|(t1, t2)| t1 < t2) + .unwrap(); + assert_eq!(TableIndex::distance(&t1, &t2), None); + } + } + + #[test] + /// Check the property: + /// For all table indices t1, t2 such that t1 > t2, + /// distance(t1, t2) = Some(v) where v is strictly positive. + fn prop_table_index_distance_some_positive() { + for _ in 0..REPEATS { + let (t1, t2) = any_table_index() + .zip(any_table_index()) + .find(|(t1, t2)| t1 > t2) + .unwrap(); + assert!(matches!(TableIndex::distance(&t1, &t2), Some(ByteCount(v)) if v > 0)); + } + } + + #[test] + /// Check the property: + /// For all table indices t, positive i such that i < distance (MAX, t) with MAX the largest + /// table index, + /// distance(t.increased(i), t) = Some(i). + fn prop_table_index_distance_increase() { + for _ in 0..REPEATS { + let (t, inc) = any_table_index() + .zip(any_usize()) + .find(|(t, inc)| { + (*inc as u128) < TableIndex::distance(&TableIndex::LAST, t).unwrap().0 + }) + .unwrap(); + assert_eq!( + TableIndex::distance(&t.increased(inc), &t).unwrap().0 as usize, + inc + ); + } + } + + #[test] + /// Check the property: + /// For all table indices t, t =? t = true. + fn prop_table_index_equality() { + for _ in 0..REPEATS { + let t = any_table_index().next().unwrap(); + assert_eq!( + std::cmp::PartialOrd::partial_cmp(&t, &t), + Some(std::cmp::Ordering::Equal) + ); + } + } + + #[test] + /// Check the property: + /// For all table indices t, positive i such that i < distance (MAX, t) with MAX the largest + /// table index, + /// t.increased(i) >? t = true. + fn prop_table_index_greater() { + for _ in 0..REPEATS { + let (t, inc) = any_table_index() + .zip(any_usize()) + .find(|(t, inc)| { + (*inc as u128) < TableIndex::distance(&TableIndex::LAST, t).unwrap().0 + }) + .unwrap(); + assert_eq!( + std::cmp::PartialOrd::partial_cmp(&t.increased(inc), &t), + Some(std::cmp::Ordering::Greater), + ); + } + } + + #[test] + /// Check the property: + /// For all table indices t, positive i such that i < distance (t, 0) with MAX the largest + /// table index, + /// t.decreased(i) = rayon::iter::Map< + rayon::iter::Zip< + rayon::range::Iter, + rayon::iter::RepeatN<(Box, TableIndex, BytesPerChild)>, + >, + fn((usize, (Box, TableIndex, BytesPerChild))) -> AesCtrGenerator, +>; + +impl AesCtrGenerator { + /// Tries to fork the current generator into `n_child` generators each able to output + /// `child_bytes` random bytes as a parallel iterator. + /// + /// # Notes + /// + /// This method necessitate the "multithread" feature. + pub fn par_try_fork( + &mut self, + n_children: ChildrenCount, + n_bytes: BytesPerChild, + ) -> Result, ForkError> + where + BlockCipher: Send + Sync, + { + use rayon::prelude::*; + + if n_children.0 == 0 { + return Err(ForkError::ZeroChildrenCount); + } + if n_bytes.0 == 0 { + return Err(ForkError::ZeroBytesPerChild); + } + if !self.is_fork_in_bound(n_children, n_bytes) { + return Err(ForkError::ForkTooLarge); + } + + // The state currently stored in the parent generator points to the table index of the last + // generated byte. The first index to be generated is the next one : + let first_index = self.state.table_index().incremented(); + let output = (0..n_children.0) + .into_par_iter() + .zip(rayon::iter::repeatn( + (self.block_cipher.clone(), first_index, n_bytes), + n_children.0, + )) + .map( + // This map is a little weird because we need to cast the closure to a fn pointer + // that matches the signature of `ChildrenIterator`. Unfortunately, + // the compiler does not manage to coerce this one automatically. + (|(i, (block_cipher, first_index, n_bytes))| { + // The first index to be outputted by the child is the `first_index` shifted by + // the proper amount of `child_bytes`. + let child_first_index = first_index.increased(n_bytes.0 * i); + // The bound of the child is the first index of its next sibling. + let child_bound_index = first_index.increased(n_bytes.0 * (i + 1)); + AesCtrGenerator::from_block_cipher( + block_cipher, + child_first_index, + child_bound_index, + ) + }) as ChildrenClosure, + ); + // The parent next index is the bound of the last child. + let next_index = first_index.increased(n_bytes.0 * n_children.0); + self.state = State::new(next_index); + + Ok(output) + } +} + +#[cfg(test)] +pub mod aes_ctr_parallel_generic_tests { + + use super::*; + use crate::generators::aes_ctr::aes_ctr_generic_test::{any_key, any_valid_fork}; + use rayon::prelude::*; + + const REPEATS: usize = 1_000_000; + + /// Check the property: + /// On a valid fork, the table index of the first child is the same as the table index of + /// the parent before the fork. + pub fn prop_fork_first_state_table_index() { + for _ in 0..REPEATS { + let (t, nc, nb, i) = any_valid_fork().next().unwrap(); + let k = any_key().next().unwrap(); + let original_generator = + AesCtrGenerator::::new(k, Some(t), Some(t.increased(nc.0 * nb.0 + i))); + let mut forked_generator = original_generator.clone(); + let first_child = forked_generator + .par_try_fork(nc, nb) + .unwrap() + .find_first(|_| true) + .unwrap(); + assert_eq!(original_generator.table_index(), first_child.table_index()); + } + } + + /// Check the property: + /// On a valid fork, the table index of the first byte outputted by the parent after the + /// fork, is the bound of the last child of the fork. + pub fn prop_fork_last_bound_table_index() { + for _ in 0..REPEATS { + let (t, nc, nb, i) = any_valid_fork().next().unwrap(); + let k = any_key().next().unwrap(); + let mut parent_generator = + AesCtrGenerator::::new(k, Some(t), Some(t.increased(nc.0 * nb.0 + i))); + let last_child = parent_generator + .par_try_fork(nc, nb) + .unwrap() + .find_last(|_| true) + .unwrap(); + assert_eq!( + parent_generator.table_index().incremented(), + last_child.get_bound() + ); + } + } + + /// Check the property: + /// On a valid fork, the bound of the parent does not change. + pub fn prop_fork_parent_bound_table_index() { + for _ in 0..REPEATS { + let (t, nc, nb, i) = any_valid_fork().next().unwrap(); + let k = any_key().next().unwrap(); + let original_generator = + AesCtrGenerator::::new(k, Some(t), Some(t.increased(nc.0 * nb.0 + i))); + let mut forked_generator = original_generator.clone(); + forked_generator + .par_try_fork(nc, nb) + .unwrap() + .find_last(|_| true) + .unwrap(); + assert_eq!(original_generator.get_bound(), forked_generator.get_bound()); + } + } + + /// Check the property: + /// On a valid fork, the parent table index is increased of the number of children + /// multiplied by the number of bytes per child. + pub fn prop_fork_parent_state_table_index() { + for _ in 0..REPEATS { + let (t, nc, nb, i) = any_valid_fork().next().unwrap(); + let k = any_key().next().unwrap(); + let original_generator = + AesCtrGenerator::::new(k, Some(t), Some(t.increased(nc.0 * nb.0 + i))); + let mut forked_generator = original_generator.clone(); + forked_generator + .par_try_fork(nc, nb) + .unwrap() + .find_last(|_| true) + .unwrap(); + assert_eq!( + forked_generator.table_index(), + // Decrement accounts for the fact that the table index stored is the previous one + t.increased(nc.0 * nb.0).decremented() + ); + } + } + + /// Check the property: + /// On a valid fork, the bytes outputted by the children in the fork order form the same + /// sequence the parent would have had outputted no fork had happened. + pub fn prop_fork() { + for _ in 0..1000 { + let (t, nc, nb, i) = any_valid_fork().next().unwrap(); + let k = any_key().next().unwrap(); + let bytes_to_go = nc.0 * nb.0; + let original_generator = + AesCtrGenerator::::new(k, Some(t), Some(t.increased(nc.0 * nb.0 + i))); + let mut forked_generator = original_generator.clone(); + let initial_output: Vec = original_generator.take(bytes_to_go).collect(); + let forked_output: Vec = forked_generator + .par_try_fork(nc, nb) + .unwrap() + .flat_map(|child| child.collect::>()) + .collect(); + assert_eq!(initial_output, forked_output); + } + } + + /// Check the property: + /// On a valid fork, all children got a number of remaining bytes equals to the number of + /// bytes per child given as fork input. + pub fn prop_fork_children_remaining_bytes() { + for _ in 0..REPEATS { + let (t, nc, nb, i) = any_valid_fork().next().unwrap(); + let k = any_key().next().unwrap(); + let mut generator = + AesCtrGenerator::::new(k, Some(t), Some(t.increased(nc.0 * nb.0 + i))); + assert!(generator + .par_try_fork(nc, nb) + .unwrap() + .all(|c| c.remaining_bytes().0 == nb.0 as u128)); + } + } + + /// Check the property: + /// On a valid fork, the number of remaining bytes of the parent is reduced by the + /// number of children multiplied by the number of bytes per child. + pub fn prop_fork_parent_remaining_bytes() { + for _ in 0..REPEATS { + let (t, nc, nb, i) = any_valid_fork().next().unwrap(); + let k = any_key().next().unwrap(); + let bytes_to_go = nc.0 * nb.0; + let mut generator = + AesCtrGenerator::::new(k, Some(t), Some(t.increased(nc.0 * nb.0 + i))); + let before_remaining_bytes = generator.remaining_bytes(); + let _ = generator.par_try_fork(nc, nb).unwrap(); + let after_remaining_bytes = generator.remaining_bytes(); + assert_eq!( + before_remaining_bytes.0 - after_remaining_bytes.0, + bytes_to_go as u128 + ); + } + } +} diff --git a/concrete-csprng/src/generators/aes_ctr/states.rs b/concrete-csprng/src/generators/aes_ctr/states.rs new file mode 100644 index 000000000..2af399cb7 --- /dev/null +++ b/concrete-csprng/src/generators/aes_ctr/states.rs @@ -0,0 +1,176 @@ +use crate::generators::aes_ctr::index::{AesIndex, TableIndex}; +use crate::generators::aes_ctr::BYTES_PER_BATCH; + +/// A pointer to the next byte to be outputted by the generator. +#[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq)] +pub struct BufferPointer(pub usize); + +/// A structure representing the current state of generator using batched aes-ctr approach. +#[derive(Debug, Clone, Copy)] +pub struct State { + table_index: TableIndex, + buffer_pointer: BufferPointer, +} + +/// A structure representing the action to be taken by the generator after shifting its state. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ShiftAction { + /// Outputs the byte pointed to by the 0-th field. + OutputByte(BufferPointer), + /// Refresh the buffer starting from the 0-th field, and output the byte pointed to by the 0-th + /// field. + RefreshBatchAndOutputByte(AesIndex, BufferPointer), +} + +impl State { + /// Creates a new state from the initial table index. + /// + /// Note : + /// ------ + /// + /// The `table_index` input, is the __first__ table index that will be outputted on the next + /// call to `increment`. Put differently, the current table index of the newly created state + /// is the predecessor of this one. + pub fn new(table_index: TableIndex) -> Self { + // We ensure that the table index is not the first one, to prevent wrapping on `decrement`, + // and outputting `RefreshBatchAndOutputByte(AesIndex::MAX, ...)` on the first increment + // (which would lead to loading a non continuous batch). + assert_ne!(table_index, TableIndex::FIRST); + State { + // To ensure that the first outputted table index is the proper one, we decrement the + // table index. + table_index: table_index.decremented(), + // To ensure that the first `ShiftAction` will be a `RefreshBatchAndOutputByte`, we set + // the buffer to the last allowed value. + buffer_pointer: BufferPointer(BYTES_PER_BATCH - 1), + } + } + + /// Shifts the state forward of `shift` bytes. + pub fn increase(&mut self, shift: usize) -> ShiftAction { + self.table_index.increase(shift); + let total_batch_index = self.buffer_pointer.0 + shift; + if total_batch_index > BYTES_PER_BATCH - 1 { + self.buffer_pointer.0 = self.table_index.byte_index.0; + ShiftAction::RefreshBatchAndOutputByte(self.table_index.aes_index, self.buffer_pointer) + } else { + self.buffer_pointer.0 = total_batch_index; + ShiftAction::OutputByte(self.buffer_pointer) + } + } + + /// Shifts the state forward of one byte. + pub fn increment(&mut self) -> ShiftAction { + self.increase(1) + } + + /// Returns the current table index. + pub fn table_index(&self) -> TableIndex { + self.table_index + } +} + +impl Default for State { + fn default() -> Self { + State::new(TableIndex::FIRST) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::generators::aes_ctr::index::ByteIndex; + use crate::generators::aes_ctr::BYTES_PER_AES_CALL; + use rand::{thread_rng, Rng}; + + const REPEATS: usize = 1_000_000; + + fn any_table_index() -> impl Iterator { + std::iter::repeat_with(|| { + TableIndex::new( + AesIndex(thread_rng().gen()), + ByteIndex(thread_rng().gen::() % BYTES_PER_AES_CALL), + ) + }) + } + + fn any_usize() -> impl Iterator { + std::iter::repeat_with(|| thread_rng().gen()) + } + + #[test] + /// Check the property: + /// For all table indices t, + /// State::new(t).increment() = RefreshBatchAndOutputByte(t.aes_index, t.byte_index) + fn prop_state_new_increment() { + for _ in 0..REPEATS { + let (t, mut s) = any_table_index() + .map(|t| (t, State::new(t))) + .next() + .unwrap(); + assert!(matches!( + s.increment(), + ShiftAction::RefreshBatchAndOutputByte(t_, BufferPointer(p_)) if t_ == t.aes_index && p_ == t.byte_index.0 + )) + } + } + + #[test] + /// Check the property: + /// For all states s, table indices t, positive integer i + /// if s = State::new(t), then t.increased(i) = s.increased(i-1).table_index(). + fn prop_state_increase_table_index() { + for _ in 0..REPEATS { + let (t, mut s, i) = any_table_index() + .zip(any_usize()) + .map(|(t, i)| (t, State::new(t), i)) + .next() + .unwrap(); + s.increase(i); + assert_eq!(s.table_index(), t.increased(i - 1)) + } + } + + #[test] + /// Check the property: + /// For all table indices t, positive integer i such as t.byte_index + i < 127, + /// if s = State::new(t), and s.increment() was executed, then + /// s.increase(i) = OutputByte(t.byte_index + i). + fn prop_state_increase_small() { + for _ in 0..REPEATS { + let (t, mut s, i) = any_table_index() + .zip(any_usize()) + .map(|(t, i)| (t, State::new(t), i % BYTES_PER_BATCH)) + .find(|(t, _, i)| t.byte_index.0 + i < BYTES_PER_BATCH - 1) + .unwrap(); + s.increment(); + assert!(matches!( + s.increase(i), + ShiftAction::OutputByte(BufferPointer(p_)) if p_ == t.byte_index.0 + i + )); + } + } + + #[test] + /// Check the property: + /// For all table indices t, positive integer i such as t.byte_index + i >= 127, + /// if s = State::new(t), and s.increment() was executed, then + /// s.increase(i) = RefreshBatchAndOutputByte( + /// t.increased(i).aes_index, + /// t.increased(i).byte_index). + fn prop_state_increase_large() { + for _ in 0..REPEATS { + let (t, mut s, i) = any_table_index() + .zip(any_usize()) + .map(|(t, i)| (t, State::new(t), i)) + .find(|(t, _, i)| t.byte_index.0 + i >= BYTES_PER_BATCH - 1) + .unwrap(); + s.increment(); + assert!(matches!( + s.increase(i), + ShiftAction::RefreshBatchAndOutputByte(t_, BufferPointer(p_)) + if t_ == t.increased(i).aes_index && p_ == t.increased(i).byte_index.0 + )); + } + } +} diff --git a/concrete-csprng/src/generators/implem/aarch64/block_cipher.rs b/concrete-csprng/src/generators/implem/aarch64/block_cipher.rs new file mode 100644 index 000000000..d28dde84f --- /dev/null +++ b/concrete-csprng/src/generators/implem/aarch64/block_cipher.rs @@ -0,0 +1,173 @@ +use crate::generators::aes_ctr::{AesBlockCipher, AesIndex, AesKey, BYTES_PER_BATCH}; +use core::arch::aarch64::{ + uint8x16_t, vaeseq_u8, vaesmcq_u8, vdupq_n_u32, vdupq_n_u8, veorq_u8, vgetq_lane_u32, + vreinterpretq_u32_u8, vreinterpretq_u8_u32, +}; +use std::arch::is_aarch64_feature_detected; +use std::mem::transmute; + +const RCONS: [u32; 10] = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1B, 0x36]; +const NUM_WORDS_IN_KEY: usize = 4; +const NUM_ROUNDS: usize = 10; +const NUM_ROUND_KEYS: usize = NUM_ROUNDS + 1; + +/// An aes block cipher implementation which uses `neon` and `aes` instructions. +#[derive(Clone)] +pub struct ArmAesBlockCipher { + round_keys: [uint8x16_t; NUM_ROUND_KEYS], +} + +impl AesBlockCipher for ArmAesBlockCipher { + fn new(key: AesKey) -> ArmAesBlockCipher { + let aes_detected = is_aarch64_feature_detected!("aes"); + let neon_detected = is_aarch64_feature_detected!("neon"); + + if !(aes_detected && neon_detected) { + panic!( + "The ArmAesBlockCipher requires both aes and neon aarch64 CPU features.\n\ + aes feature available: {}\nneon feature available: {}\n.", + aes_detected, neon_detected + ) + } + + let round_keys = unsafe { generate_round_keys(key) }; + ArmAesBlockCipher { round_keys } + } + + fn generate_batch(&mut self, AesIndex(aes_ctr): AesIndex) -> [u8; BYTES_PER_BATCH] { + let mut output = [0u8; BYTES_PER_BATCH]; + // We want 128 bytes of output, the ctr gives 128 bit message (16 bytes) + for (i, out) in output.chunks_exact_mut(16).enumerate() { + let encrypted = unsafe { + // Safe because we prevent the user from creating the Generator + // on non-supported hardware + encrypt(aes_ctr + (i as u128), &self.round_keys) + }; + out.copy_from_slice(&encrypted.to_ne_bytes()); + } + output + } +} + +/// Does the AES SubWord operation for the Key Expansion step +/// +/// # SAFETY +/// +/// You must make sure the CPU's arch is`aarch64` and has +/// `neon` and `aes` features. +unsafe fn sub_word(word: u32) -> u32 { + let data = vreinterpretq_u8_u32(vdupq_n_u32(word)); + let zero_key = vdupq_n_u8(0u8); + let temp = vaeseq_u8(data, zero_key); + // vaeseq_u8 does SubBytes(ShiftRow(XOR(data, key)) + // But because we used a zero aes key,the XOR did not alter data + // We now have temp = SubBytes(ShiftRow(data)) + + // Since in AES ShiftRow operation, the first row is not shifted + // We can just get that one to have our SubWord(word) result + vgetq_lane_u32::<0>(vreinterpretq_u32_u8(temp)) +} + +fn uint8x16_t_to_u128(input: uint8x16_t) -> u128 { + unsafe { transmute(input) } +} + +fn u128_to_uint8x16_t(input: u128) -> uint8x16_t { + unsafe { transmute(input) } +} + +unsafe fn generate_round_keys(key: AesKey) -> [uint8x16_t; NUM_ROUND_KEYS] { + let mut round_keys: [uint8x16_t; NUM_ROUND_KEYS] = std::mem::zeroed(); + round_keys[0] = u128_to_uint8x16_t(key.0); + + let words = std::slice::from_raw_parts_mut( + round_keys.as_mut_ptr() as *mut u32, + NUM_ROUND_KEYS * NUM_WORDS_IN_KEY, + ); + + debug_assert_eq!(words.len(), 44); + + // Skip the words of the first key, its already done + for i in NUM_WORDS_IN_KEY..words.len() { + if (i % NUM_WORDS_IN_KEY) == 0 { + words[i] = words[i - NUM_WORDS_IN_KEY] + ^ sub_word(words[i - 1]).rotate_right(8) + ^ RCONS[(i / NUM_WORDS_IN_KEY) - 1]; + } else { + words[i] = words[i - NUM_WORDS_IN_KEY] ^ words[i - 1]; + } + // Note: there is also a special thing to do when + // i mod SElf::NUM_WORDS_IN_KEY == 4 but it cannot happen on 128 bits keys + } + + round_keys +} + +/// Encrypts a 128-bit message +/// +/// # SAFETY +/// +/// You must make sure the CPU's arch is`aarch64` and has +/// `neon` and `aes` features. +unsafe fn encrypt(message: u128, keys: &[uint8x16_t; NUM_ROUND_KEYS]) -> u128 { + // Notes: + // According the [ARM Manual](https://developer.arm.com/documentation/ddi0487/gb/): + // `vaeseq_u8` is the following AES operations: + // 1. AddRoundKey (XOR) + // 2. ShiftRows + // 3. SubBytes + // `vaesmcq_u8` is MixColumns + let mut data: uint8x16_t = u128_to_uint8x16_t(message); + + for &key in keys.iter().take(NUM_ROUNDS - 1) { + data = vaesmcq_u8(vaeseq_u8(data, key)); + } + + data = vaeseq_u8(data, keys[NUM_ROUNDS - 1]); + data = veorq_u8(data, keys[NUM_ROUND_KEYS - 1]); + + uint8x16_t_to_u128(data) +} + +#[cfg(test)] +mod test { + use super::*; + + // Test vector for aes128, from the FIPS publication 197 + const CIPHER_KEY: u128 = u128::from_be(0x000102030405060708090a0b0c0d0e0f); + const KEY_SCHEDULE: [u128; 11] = [ + u128::from_be(0x000102030405060708090a0b0c0d0e0f), + u128::from_be(0xd6aa74fdd2af72fadaa678f1d6ab76fe), + u128::from_be(0xb692cf0b643dbdf1be9bc5006830b3fe), + u128::from_be(0xb6ff744ed2c2c9bf6c590cbf0469bf41), + u128::from_be(0x47f7f7bc95353e03f96c32bcfd058dfd), + u128::from_be(0x3caaa3e8a99f9deb50f3af57adf622aa), + u128::from_be(0x5e390f7df7a69296a7553dc10aa31f6b), + u128::from_be(0x14f9701ae35fe28c440adf4d4ea9c026), + u128::from_be(0x47438735a41c65b9e016baf4aebf7ad2), + u128::from_be(0x549932d1f08557681093ed9cbe2c974e), + u128::from_be(0x13111d7fe3944a17f307a78b4d2b30c5), + ]; + const PLAINTEXT: u128 = u128::from_be(0x00112233445566778899aabbccddeeff); + const CIPHERTEXT: u128 = u128::from_be(0x69c4e0d86a7b0430d8cdb78070b4c55a); + + #[test] + fn test_generate_key_schedule() { + // Checks that the round keys are correctly generated from the sample key from FIPS + let key = AesKey(CIPHER_KEY); + let keys = unsafe { generate_round_keys(key) }; + for (expected, actual) in KEY_SCHEDULE.iter().zip(keys.iter()) { + assert_eq!(*expected, uint8x16_t_to_u128(*actual)); + } + } + + #[test] + fn test_encrypt_message() { + // Checks that encrypting many plaintext at the same time gives the correct output. + let message = PLAINTEXT; + let key = AesKey(CIPHER_KEY); + let keys = unsafe { generate_round_keys(key) }; + let ciphertext = unsafe { encrypt(message, &keys) }; + assert_eq!(CIPHERTEXT, ciphertext); + } +} diff --git a/concrete-csprng/src/generators/implem/aarch64/generator.rs b/concrete-csprng/src/generators/implem/aarch64/generator.rs new file mode 100644 index 000000000..c79240aa5 --- /dev/null +++ b/concrete-csprng/src/generators/implem/aarch64/generator.rs @@ -0,0 +1,110 @@ +use crate::generators::aes_ctr::{AesCtrGenerator, AesKey, ChildrenIterator}; +use crate::generators::implem::aarch64::block_cipher::ArmAesBlockCipher; +use crate::generators::{ByteCount, BytesPerChild, ChildrenCount, ForkError, RandomGenerator}; +use crate::seeders::Seed; + +/// A random number generator using the `aesni` instructions. +pub struct NeonAesRandomGenerator(pub(super) AesCtrGenerator); + +/// The children iterator used by [`NeonAesRandomGenerator`]. +/// +/// Outputs children generators one by one. +pub struct ArmAesChildrenIterator(ChildrenIterator); + +impl Iterator for ArmAesChildrenIterator { + type Item = NeonAesRandomGenerator; + + fn next(&mut self) -> Option { + self.0.next().map(NeonAesRandomGenerator) + } +} + +impl RandomGenerator for NeonAesRandomGenerator { + type ChildrenIter = ArmAesChildrenIterator; + fn new(seed: Seed) -> Self { + NeonAesRandomGenerator(AesCtrGenerator::new(AesKey(seed.0), None, None)) + } + fn remaining_bytes(&self) -> ByteCount { + self.0.remaining_bytes() + } + fn try_fork( + &mut self, + n_children: ChildrenCount, + n_bytes: BytesPerChild, + ) -> Result { + self.0 + .try_fork(n_children, n_bytes) + .map(ArmAesChildrenIterator) + } +} + +impl Iterator for NeonAesRandomGenerator { + type Item = u8; + + fn next(&mut self) -> Option { + self.0.next() + } +} + +#[cfg(test)] +mod test { + use crate::generators::aes_ctr::aes_ctr_generic_test; + use crate::generators::implem::aarch64::block_cipher::ArmAesBlockCipher; + use crate::generators::{generator_generic_test, NeonAesRandomGenerator}; + + #[test] + fn prop_fork_first_state_table_index() { + aes_ctr_generic_test::prop_fork_first_state_table_index::(); + } + + #[test] + fn prop_fork_last_bound_table_index() { + aes_ctr_generic_test::prop_fork_last_bound_table_index::(); + } + + #[test] + fn prop_fork_parent_bound_table_index() { + aes_ctr_generic_test::prop_fork_parent_bound_table_index::(); + } + + #[test] + fn prop_fork_parent_state_table_index() { + aes_ctr_generic_test::prop_fork_parent_state_table_index::(); + } + + #[test] + fn prop_fork() { + aes_ctr_generic_test::prop_fork::(); + } + + #[test] + fn prop_fork_children_remaining_bytes() { + aes_ctr_generic_test::prop_fork_children_remaining_bytes::(); + } + + #[test] + fn prop_fork_parent_remaining_bytes() { + aes_ctr_generic_test::prop_fork_parent_remaining_bytes::(); + } + + #[test] + fn test_roughly_uniform() { + generator_generic_test::test_roughly_uniform::(); + } + + #[test] + fn test_generator_determinism() { + generator_generic_test::test_generator_determinism::(); + } + + #[test] + fn test_fork() { + generator_generic_test::test_fork_children::(); + } + + #[test] + #[should_panic(expected = "expected test panic")] + fn test_bounded_panic() { + generator_generic_test::test_bounded_none_should_panic::(); + } +} diff --git a/concrete-csprng/src/generators/implem/aarch64/mod.rs b/concrete-csprng/src/generators/implem/aarch64/mod.rs new file mode 100644 index 000000000..27ffba245 --- /dev/null +++ b/concrete-csprng/src/generators/implem/aarch64/mod.rs @@ -0,0 +1,16 @@ +//! A module implementing a random number generator, using the aarch64 `neon` and `aes` +//! instructions. +//! +//! This module implements a cryptographically secure pseudorandom number generator +//! (CS-PRNG), using a fast block cipher. The implementation is based on the +//! [intel aesni white paper 323641-001 revision 3.0](https://www.intel.com/content/dam/doc/white-paper/advanced-encryption-standard-new-instructions-set-paper.pdf). + +mod block_cipher; + +mod generator; +pub use generator::*; + +#[cfg(feature = "parallel")] +mod parallel; +#[cfg(feature = "parallel")] +pub use parallel::*; diff --git a/concrete-csprng/src/generators/implem/aarch64/parallel.rs b/concrete-csprng/src/generators/implem/aarch64/parallel.rs new file mode 100644 index 000000000..288cd8a67 --- /dev/null +++ b/concrete-csprng/src/generators/implem/aarch64/parallel.rs @@ -0,0 +1,95 @@ +use super::*; +use crate::generators::aes_ctr::{AesCtrGenerator, ParallelChildrenIterator}; +use crate::generators::implem::aarch64::block_cipher::ArmAesBlockCipher; +use crate::generators::{BytesPerChild, ChildrenCount, ForkError, ParallelRandomGenerator}; +use rayon::iter::plumbing::{Consumer, ProducerCallback, UnindexedConsumer}; +use rayon::prelude::*; + +/// The parallel children iterator used by [`NeonAesRandomGenerator`]. +/// +/// Outputs the children generators one by one. +#[allow(clippy::type_complexity)] +pub struct ParallelArmAesChildrenIterator( + rayon::iter::Map< + ParallelChildrenIterator, + fn(AesCtrGenerator) -> NeonAesRandomGenerator, + >, +); + +impl ParallelIterator for ParallelArmAesChildrenIterator { + type Item = NeonAesRandomGenerator; + fn drive_unindexed(self, consumer: C) -> C::Result + where + C: UnindexedConsumer, + { + self.0.drive_unindexed(consumer) + } +} + +impl IndexedParallelIterator for ParallelArmAesChildrenIterator { + fn len(&self) -> usize { + self.0.len() + } + fn drive>(self, consumer: C) -> C::Result { + self.0.drive(consumer) + } + fn with_producer>(self, callback: CB) -> CB::Output { + self.0.with_producer(callback) + } +} + +impl ParallelRandomGenerator for NeonAesRandomGenerator { + type ParChildrenIter = ParallelArmAesChildrenIterator; + + fn par_try_fork( + &mut self, + n_children: ChildrenCount, + n_bytes: BytesPerChild, + ) -> Result { + self.0 + .par_try_fork(n_children, n_bytes) + .map(|iterator| ParallelArmAesChildrenIterator(iterator.map(NeonAesRandomGenerator))) + } +} + +#[cfg(test)] + +mod test { + use crate::generators::aes_ctr::aes_ctr_parallel_generic_tests; + use crate::generators::implem::aarch64::block_cipher::ArmAesBlockCipher; + + #[test] + fn prop_fork_first_state_table_index() { + aes_ctr_parallel_generic_tests::prop_fork_first_state_table_index::(); + } + + #[test] + fn prop_fork_last_bound_table_index() { + aes_ctr_parallel_generic_tests::prop_fork_last_bound_table_index::(); + } + + #[test] + fn prop_fork_parent_bound_table_index() { + aes_ctr_parallel_generic_tests::prop_fork_parent_bound_table_index::(); + } + + #[test] + fn prop_fork_parent_state_table_index() { + aes_ctr_parallel_generic_tests::prop_fork_parent_state_table_index::(); + } + + #[test] + fn prop_fork_ttt() { + aes_ctr_parallel_generic_tests::prop_fork::(); + } + + #[test] + fn prop_fork_children_remaining_bytes() { + aes_ctr_parallel_generic_tests::prop_fork_children_remaining_bytes::(); + } + + #[test] + fn prop_fork_parent_remaining_bytes() { + aes_ctr_parallel_generic_tests::prop_fork_parent_remaining_bytes::(); + } +} diff --git a/concrete-csprng/src/generators/implem/aesni/block_cipher.rs b/concrete-csprng/src/generators/implem/aesni/block_cipher.rs new file mode 100644 index 000000000..22c604f1a --- /dev/null +++ b/concrete-csprng/src/generators/implem/aesni/block_cipher.rs @@ -0,0 +1,226 @@ +use crate::generators::aes_ctr::{AesBlockCipher, AesIndex, AesKey, BYTES_PER_BATCH}; +use std::arch::x86_64::{ + __m128i, _mm_aesenc_si128, _mm_aesenclast_si128, _mm_aeskeygenassist_si128, _mm_load_si128, + _mm_shuffle_epi32, _mm_slli_si128, _mm_store_si128, _mm_xor_si128, +}; +use std::mem::transmute; + +/// An aes block cipher implementation which uses `aesni` instructions. +#[derive(Clone)] +pub struct AesniBlockCipher { + // The set of round keys used for the aes encryption + round_keys: [__m128i; 11], +} + +impl AesBlockCipher for AesniBlockCipher { + fn new(key: AesKey) -> AesniBlockCipher { + let aes_detected = is_x86_feature_detected!("aes"); + let sse2_detected = is_x86_feature_detected!("sse2"); + + if !(aes_detected && sse2_detected) { + panic!( + "The AesniBlockCipher requires both aes and sse2 x86 CPU features.\n\ + aes feature available: {}\nsse2 feature available: {}\n.", + aes_detected, sse2_detected + ) + } + + let round_keys = generate_round_keys(key); + AesniBlockCipher { round_keys } + } + + fn generate_batch(&mut self, AesIndex(aes_ctr): AesIndex) -> [u8; BYTES_PER_BATCH] { + si128arr_to_u8arr(aes_encrypt_many( + &u128_to_si128(aes_ctr), + &u128_to_si128(aes_ctr + 1), + &u128_to_si128(aes_ctr + 2), + &u128_to_si128(aes_ctr + 3), + &u128_to_si128(aes_ctr + 4), + &u128_to_si128(aes_ctr + 5), + &u128_to_si128(aes_ctr + 6), + &u128_to_si128(aes_ctr + 7), + &self.round_keys, + )) + } +} + +fn generate_round_keys(key: AesKey) -> [__m128i; 11] { + let key = u128_to_si128(key.0); + let mut keys: [__m128i; 11] = [u128_to_si128(0); 11]; + aes_128_key_expansion(key, &mut keys); + keys +} + +// Uses aes to encrypt many values at once. This allows a substantial speedup (around 30%) +// compared to the naive approach. +#[allow(clippy::too_many_arguments)] +fn aes_encrypt_many( + message_1: &__m128i, + message_2: &__m128i, + message_3: &__m128i, + message_4: &__m128i, + message_5: &__m128i, + message_6: &__m128i, + message_7: &__m128i, + message_8: &__m128i, + keys: &[__m128i; 11], +) -> [__m128i; 8] { + unsafe { + let message_1 = _mm_load_si128(message_1 as *const __m128i); + let message_2 = _mm_load_si128(message_2 as *const __m128i); + let message_3 = _mm_load_si128(message_3 as *const __m128i); + let message_4 = _mm_load_si128(message_4 as *const __m128i); + let message_5 = _mm_load_si128(message_5 as *const __m128i); + let message_6 = _mm_load_si128(message_6 as *const __m128i); + let message_7 = _mm_load_si128(message_7 as *const __m128i); + let message_8 = _mm_load_si128(message_8 as *const __m128i); + + let mut tmp_1 = _mm_xor_si128(message_1, keys[0]); + let mut tmp_2 = _mm_xor_si128(message_2, keys[0]); + let mut tmp_3 = _mm_xor_si128(message_3, keys[0]); + let mut tmp_4 = _mm_xor_si128(message_4, keys[0]); + let mut tmp_5 = _mm_xor_si128(message_5, keys[0]); + let mut tmp_6 = _mm_xor_si128(message_6, keys[0]); + let mut tmp_7 = _mm_xor_si128(message_7, keys[0]); + let mut tmp_8 = _mm_xor_si128(message_8, keys[0]); + + for key in keys.iter().take(10).skip(1) { + tmp_1 = _mm_aesenc_si128(tmp_1, *key); + tmp_2 = _mm_aesenc_si128(tmp_2, *key); + tmp_3 = _mm_aesenc_si128(tmp_3, *key); + tmp_4 = _mm_aesenc_si128(tmp_4, *key); + tmp_5 = _mm_aesenc_si128(tmp_5, *key); + tmp_6 = _mm_aesenc_si128(tmp_6, *key); + tmp_7 = _mm_aesenc_si128(tmp_7, *key); + tmp_8 = _mm_aesenc_si128(tmp_8, *key); + } + + tmp_1 = _mm_aesenclast_si128(tmp_1, keys[10]); + tmp_2 = _mm_aesenclast_si128(tmp_2, keys[10]); + tmp_3 = _mm_aesenclast_si128(tmp_3, keys[10]); + tmp_4 = _mm_aesenclast_si128(tmp_4, keys[10]); + tmp_5 = _mm_aesenclast_si128(tmp_5, keys[10]); + tmp_6 = _mm_aesenclast_si128(tmp_6, keys[10]); + tmp_7 = _mm_aesenclast_si128(tmp_7, keys[10]); + tmp_8 = _mm_aesenclast_si128(tmp_8, keys[10]); + + [tmp_1, tmp_2, tmp_3, tmp_4, tmp_5, tmp_6, tmp_7, tmp_8] + } +} + +fn aes_128_assist(temp1: __m128i, temp2: __m128i) -> __m128i { + let mut temp3: __m128i; + let mut temp2 = temp2; + let mut temp1 = temp1; + unsafe { + temp2 = _mm_shuffle_epi32(temp2, 0xff); + temp3 = _mm_slli_si128(temp1, 0x4); + temp1 = _mm_xor_si128(temp1, temp3); + temp3 = _mm_slli_si128(temp3, 0x4); + temp1 = _mm_xor_si128(temp1, temp3); + temp3 = _mm_slli_si128(temp3, 0x4); + temp1 = _mm_xor_si128(temp1, temp3); + temp1 = _mm_xor_si128(temp1, temp2); + } + temp1 +} + +fn aes_128_key_expansion(key: __m128i, keys: &mut [__m128i; 11]) { + let (mut temp1, mut temp2): (__m128i, __m128i); + temp1 = key; + unsafe { + _mm_store_si128(keys.as_mut_ptr(), temp1); + temp2 = _mm_aeskeygenassist_si128(temp1, 0x01); + temp1 = aes_128_assist(temp1, temp2); + _mm_store_si128(keys.as_mut_ptr().offset(1), temp1); + temp2 = _mm_aeskeygenassist_si128(temp1, 0x02); + temp1 = aes_128_assist(temp1, temp2); + _mm_store_si128(keys.as_mut_ptr().offset(2), temp1); + temp2 = _mm_aeskeygenassist_si128(temp1, 0x04); + temp1 = aes_128_assist(temp1, temp2); + _mm_store_si128(keys.as_mut_ptr().offset(3), temp1); + temp2 = _mm_aeskeygenassist_si128(temp1, 0x08); + temp1 = aes_128_assist(temp1, temp2); + _mm_store_si128(keys.as_mut_ptr().offset(4), temp1); + temp2 = _mm_aeskeygenassist_si128(temp1, 0x10); + temp1 = aes_128_assist(temp1, temp2); + _mm_store_si128(keys.as_mut_ptr().offset(5), temp1); + temp2 = _mm_aeskeygenassist_si128(temp1, 0x20); + temp1 = aes_128_assist(temp1, temp2); + _mm_store_si128(keys.as_mut_ptr().offset(6), temp1); + temp2 = _mm_aeskeygenassist_si128(temp1, 0x40); + temp1 = aes_128_assist(temp1, temp2); + _mm_store_si128(keys.as_mut_ptr().offset(7), temp1); + temp2 = _mm_aeskeygenassist_si128(temp1, 0x80); + temp1 = aes_128_assist(temp1, temp2); + _mm_store_si128(keys.as_mut_ptr().offset(8), temp1); + temp2 = _mm_aeskeygenassist_si128(temp1, 0x1b); + temp1 = aes_128_assist(temp1, temp2); + _mm_store_si128(keys.as_mut_ptr().offset(9), temp1); + temp2 = _mm_aeskeygenassist_si128(temp1, 0x36); + temp1 = aes_128_assist(temp1, temp2); + _mm_store_si128(keys.as_mut_ptr().offset(10), temp1); + } +} + +fn u128_to_si128(input: u128) -> __m128i { + unsafe { transmute(input) } +} + +#[allow(unused)] // to please clippy when tests are not activated +fn si128_to_u128(input: __m128i) -> u128 { + unsafe { transmute(input) } +} + +fn si128arr_to_u8arr(input: [__m128i; 8]) -> [u8; BYTES_PER_BATCH] { + unsafe { transmute(input) } +} + +#[cfg(test)] +mod test { + use super::*; + + // Test vector for aes128, from the FIPS publication 197 + const CIPHER_KEY: u128 = u128::from_be(0x000102030405060708090a0b0c0d0e0f); + const KEY_SCHEDULE: [u128; 11] = [ + u128::from_be(0x000102030405060708090a0b0c0d0e0f), + u128::from_be(0xd6aa74fdd2af72fadaa678f1d6ab76fe), + u128::from_be(0xb692cf0b643dbdf1be9bc5006830b3fe), + u128::from_be(0xb6ff744ed2c2c9bf6c590cbf0469bf41), + u128::from_be(0x47f7f7bc95353e03f96c32bcfd058dfd), + u128::from_be(0x3caaa3e8a99f9deb50f3af57adf622aa), + u128::from_be(0x5e390f7df7a69296a7553dc10aa31f6b), + u128::from_be(0x14f9701ae35fe28c440adf4d4ea9c026), + u128::from_be(0x47438735a41c65b9e016baf4aebf7ad2), + u128::from_be(0x549932d1f08557681093ed9cbe2c974e), + u128::from_be(0x13111d7fe3944a17f307a78b4d2b30c5), + ]; + const PLAINTEXT: u128 = u128::from_be(0x00112233445566778899aabbccddeeff); + const CIPHERTEXT: u128 = u128::from_be(0x69c4e0d86a7b0430d8cdb78070b4c55a); + + #[test] + fn test_generate_key_schedule() { + // Checks that the round keys are correctly generated from the sample key from FIPS + let key = u128_to_si128(CIPHER_KEY); + let mut keys: [__m128i; 11] = [u128_to_si128(0); 11]; + aes_128_key_expansion(key, &mut keys); + for (expected, actual) in KEY_SCHEDULE.iter().zip(keys.iter()) { + assert_eq!(*expected, si128_to_u128(*actual)); + } + } + + #[test] + fn test_encrypt_many_messages() { + // Checks that encrypting many plaintext at the same time gives the correct output. + let message = u128_to_si128(PLAINTEXT); + let key = u128_to_si128(CIPHER_KEY); + let mut keys: [__m128i; 11] = [u128_to_si128(0); 11]; + aes_128_key_expansion(key, &mut keys); + let ciphertexts = aes_encrypt_many( + &message, &message, &message, &message, &message, &message, &message, &message, &keys, + ); + for ct in &ciphertexts { + assert_eq!(CIPHERTEXT, si128_to_u128(*ct)); + } + } +} diff --git a/concrete-csprng/src/generators/implem/aesni/generator.rs b/concrete-csprng/src/generators/implem/aesni/generator.rs new file mode 100644 index 000000000..e3cdf797d --- /dev/null +++ b/concrete-csprng/src/generators/implem/aesni/generator.rs @@ -0,0 +1,110 @@ +use crate::generators::aes_ctr::{AesCtrGenerator, AesKey, ChildrenIterator}; +use crate::generators::implem::aesni::block_cipher::AesniBlockCipher; +use crate::generators::{ByteCount, BytesPerChild, ChildrenCount, ForkError, RandomGenerator}; +use crate::seeders::Seed; + +/// A random number generator using the `aesni` instructions. +pub struct AesniRandomGenerator(pub(super) AesCtrGenerator); + +/// The children iterator used by [`AesniRandomGenerator`]. +/// +/// Outputs children generators one by one. +pub struct AesniChildrenIterator(ChildrenIterator); + +impl Iterator for AesniChildrenIterator { + type Item = AesniRandomGenerator; + + fn next(&mut self) -> Option { + self.0.next().map(AesniRandomGenerator) + } +} + +impl RandomGenerator for AesniRandomGenerator { + type ChildrenIter = AesniChildrenIterator; + fn new(seed: Seed) -> Self { + AesniRandomGenerator(AesCtrGenerator::new(AesKey(seed.0), None, None)) + } + fn remaining_bytes(&self) -> ByteCount { + self.0.remaining_bytes() + } + fn try_fork( + &mut self, + n_children: ChildrenCount, + n_bytes: BytesPerChild, + ) -> Result { + self.0 + .try_fork(n_children, n_bytes) + .map(AesniChildrenIterator) + } +} + +impl Iterator for AesniRandomGenerator { + type Item = u8; + + fn next(&mut self) -> Option { + self.0.next() + } +} + +#[cfg(test)] +mod test { + use crate::generators::aes_ctr::aes_ctr_generic_test; + use crate::generators::implem::aesni::block_cipher::AesniBlockCipher; + use crate::generators::{generator_generic_test, AesniRandomGenerator}; + + #[test] + fn prop_fork_first_state_table_index() { + aes_ctr_generic_test::prop_fork_first_state_table_index::(); + } + + #[test] + fn prop_fork_last_bound_table_index() { + aes_ctr_generic_test::prop_fork_last_bound_table_index::(); + } + + #[test] + fn prop_fork_parent_bound_table_index() { + aes_ctr_generic_test::prop_fork_parent_bound_table_index::(); + } + + #[test] + fn prop_fork_parent_state_table_index() { + aes_ctr_generic_test::prop_fork_parent_state_table_index::(); + } + + #[test] + fn prop_fork() { + aes_ctr_generic_test::prop_fork::(); + } + + #[test] + fn prop_fork_children_remaining_bytes() { + aes_ctr_generic_test::prop_fork_children_remaining_bytes::(); + } + + #[test] + fn prop_fork_parent_remaining_bytes() { + aes_ctr_generic_test::prop_fork_parent_remaining_bytes::(); + } + + #[test] + fn test_roughly_uniform() { + generator_generic_test::test_roughly_uniform::(); + } + + #[test] + fn test_generator_determinism() { + generator_generic_test::test_generator_determinism::(); + } + + #[test] + fn test_fork() { + generator_generic_test::test_fork_children::(); + } + + #[test] + #[should_panic(expected = "expected test panic")] + fn test_bounded_panic() { + generator_generic_test::test_bounded_none_should_panic::(); + } +} diff --git a/concrete-csprng/src/generators/implem/aesni/mod.rs b/concrete-csprng/src/generators/implem/aesni/mod.rs new file mode 100644 index 000000000..c169cbb98 --- /dev/null +++ b/concrete-csprng/src/generators/implem/aesni/mod.rs @@ -0,0 +1,15 @@ +//! A module implementing a random number generator, using the x86_64 `aesni` instructions. +//! +//! This module implements a cryptographically secure pseudorandom number generator +//! (CS-PRNG), using a fast block cipher. The implementation is based on the +//! [intel aesni white paper 323641-001 revision 3.0](https://www.intel.com/content/dam/doc/white-paper/advanced-encryption-standard-new-instructions-set-paper.pdf). + +mod block_cipher; + +mod generator; +pub use generator::*; + +#[cfg(feature = "parallel")] +mod parallel; +#[cfg(feature = "parallel")] +pub use parallel::*; diff --git a/concrete-csprng/src/generators/implem/aesni/parallel.rs b/concrete-csprng/src/generators/implem/aesni/parallel.rs new file mode 100644 index 000000000..7da9b5c88 --- /dev/null +++ b/concrete-csprng/src/generators/implem/aesni/parallel.rs @@ -0,0 +1,95 @@ +use super::*; +use crate::generators::aes_ctr::{AesCtrGenerator, ParallelChildrenIterator}; +use crate::generators::implem::aesni::block_cipher::AesniBlockCipher; +use crate::generators::{BytesPerChild, ChildrenCount, ForkError, ParallelRandomGenerator}; +use rayon::iter::plumbing::{Consumer, ProducerCallback, UnindexedConsumer}; +use rayon::prelude::*; + +/// The parallel children iterator used by [`AesniRandomGenerator`]. +/// +/// Outputs the children generators one by one. +#[allow(clippy::type_complexity)] +pub struct ParallelAesniChildrenIterator( + rayon::iter::Map< + ParallelChildrenIterator, + fn(AesCtrGenerator) -> AesniRandomGenerator, + >, +); + +impl ParallelIterator for ParallelAesniChildrenIterator { + type Item = AesniRandomGenerator; + fn drive_unindexed(self, consumer: C) -> C::Result + where + C: UnindexedConsumer, + { + self.0.drive_unindexed(consumer) + } +} + +impl IndexedParallelIterator for ParallelAesniChildrenIterator { + fn len(&self) -> usize { + self.0.len() + } + fn drive>(self, consumer: C) -> C::Result { + self.0.drive(consumer) + } + fn with_producer>(self, callback: CB) -> CB::Output { + self.0.with_producer(callback) + } +} + +impl ParallelRandomGenerator for AesniRandomGenerator { + type ParChildrenIter = ParallelAesniChildrenIterator; + + fn par_try_fork( + &mut self, + n_children: ChildrenCount, + n_bytes: BytesPerChild, + ) -> Result { + self.0 + .par_try_fork(n_children, n_bytes) + .map(|iterator| ParallelAesniChildrenIterator(iterator.map(AesniRandomGenerator))) + } +} + +#[cfg(test)] + +mod test { + use crate::generators::aes_ctr::aes_ctr_parallel_generic_tests; + use crate::generators::implem::aesni::block_cipher::AesniBlockCipher; + + #[test] + fn prop_fork_first_state_table_index() { + aes_ctr_parallel_generic_tests::prop_fork_first_state_table_index::(); + } + + #[test] + fn prop_fork_last_bound_table_index() { + aes_ctr_parallel_generic_tests::prop_fork_last_bound_table_index::(); + } + + #[test] + fn prop_fork_parent_bound_table_index() { + aes_ctr_parallel_generic_tests::prop_fork_parent_bound_table_index::(); + } + + #[test] + fn prop_fork_parent_state_table_index() { + aes_ctr_parallel_generic_tests::prop_fork_parent_state_table_index::(); + } + + #[test] + fn prop_fork_ttt() { + aes_ctr_parallel_generic_tests::prop_fork::(); + } + + #[test] + fn prop_fork_children_remaining_bytes() { + aes_ctr_parallel_generic_tests::prop_fork_children_remaining_bytes::(); + } + + #[test] + fn prop_fork_parent_remaining_bytes() { + aes_ctr_parallel_generic_tests::prop_fork_parent_remaining_bytes::(); + } +} diff --git a/concrete-csprng/src/generators/implem/mod.rs b/concrete-csprng/src/generators/implem/mod.rs new file mode 100644 index 000000000..5ce7cd10f --- /dev/null +++ b/concrete-csprng/src/generators/implem/mod.rs @@ -0,0 +1,14 @@ +#[cfg(feature = "generator_x86_64_aesni")] +mod aesni; +#[cfg(feature = "generator_x86_64_aesni")] +pub use aesni::*; + +#[cfg(feature = "generator_aarch64_aes")] +mod aarch64; +#[cfg(feature = "generator_aarch64_aes")] +pub use aarch64::*; + +#[cfg(feature = "generator_fallback")] +mod soft; +#[cfg(feature = "generator_fallback")] +pub use soft::*; diff --git a/concrete-csprng/src/generators/implem/soft/block_cipher.rs b/concrete-csprng/src/generators/implem/soft/block_cipher.rs new file mode 100644 index 000000000..7ff3551d3 --- /dev/null +++ b/concrete-csprng/src/generators/implem/soft/block_cipher.rs @@ -0,0 +1,114 @@ +use crate::generators::aes_ctr::{ + AesBlockCipher, AesIndex, AesKey, AES_CALLS_PER_BATCH, BYTES_PER_AES_CALL, BYTES_PER_BATCH, +}; +use aes::cipher::generic_array::GenericArray; +use aes::cipher::{BlockEncrypt, KeyInit}; +use aes::Aes128; + +#[derive(Clone)] +pub struct SoftwareBlockCipher { + // Aes structure + aes: Aes128, +} + +impl AesBlockCipher for SoftwareBlockCipher { + fn new(key: AesKey) -> SoftwareBlockCipher { + let key: [u8; BYTES_PER_AES_CALL] = key.0.to_ne_bytes(); + let key = GenericArray::clone_from_slice(&key[..]); + let aes = Aes128::new(&key); + SoftwareBlockCipher { aes } + } + + fn generate_batch(&mut self, AesIndex(aes_ctr): AesIndex) -> [u8; BYTES_PER_BATCH] { + aes_encrypt_many( + aes_ctr, + aes_ctr + 1, + aes_ctr + 2, + aes_ctr + 3, + aes_ctr + 4, + aes_ctr + 5, + aes_ctr + 6, + aes_ctr + 7, + &self.aes, + ) + } +} + +// Uses aes to encrypt many values at once. This allows a substantial speedup (around 30%) +// compared to the naive approach. +#[allow(clippy::too_many_arguments)] +fn aes_encrypt_many( + message_1: u128, + message_2: u128, + message_3: u128, + message_4: u128, + message_5: u128, + message_6: u128, + message_7: u128, + message_8: u128, + cipher: &Aes128, +) -> [u8; BYTES_PER_BATCH] { + let mut b1 = GenericArray::clone_from_slice(&message_1.to_ne_bytes()[..]); + let mut b2 = GenericArray::clone_from_slice(&message_2.to_ne_bytes()[..]); + let mut b3 = GenericArray::clone_from_slice(&message_3.to_ne_bytes()[..]); + let mut b4 = GenericArray::clone_from_slice(&message_4.to_ne_bytes()[..]); + let mut b5 = GenericArray::clone_from_slice(&message_5.to_ne_bytes()[..]); + let mut b6 = GenericArray::clone_from_slice(&message_6.to_ne_bytes()[..]); + let mut b7 = GenericArray::clone_from_slice(&message_7.to_ne_bytes()[..]); + let mut b8 = GenericArray::clone_from_slice(&message_8.to_ne_bytes()[..]); + + cipher.encrypt_block(&mut b1); + cipher.encrypt_block(&mut b2); + cipher.encrypt_block(&mut b3); + cipher.encrypt_block(&mut b4); + cipher.encrypt_block(&mut b5); + cipher.encrypt_block(&mut b6); + cipher.encrypt_block(&mut b7); + cipher.encrypt_block(&mut b8); + + let output_array: [[u8; BYTES_PER_AES_CALL]; AES_CALLS_PER_BATCH] = [ + b1.into(), + b2.into(), + b3.into(), + b4.into(), + b5.into(), + b6.into(), + b7.into(), + b8.into(), + ]; + + unsafe { *{ output_array.as_ptr() as *const [u8; BYTES_PER_BATCH] } } +} + +#[cfg(test)] +mod test { + use super::*; + use std::convert::TryInto; + + // Test vector for aes128, from the FIPS publication 197 + const CIPHER_KEY: u128 = u128::from_be(0x000102030405060708090a0b0c0d0e0f); + const PLAINTEXT: u128 = u128::from_be(0x00112233445566778899aabbccddeeff); + const CIPHERTEXT: u128 = u128::from_be(0x69c4e0d86a7b0430d8cdb78070b4c55a); + + #[test] + fn test_encrypt_many_messages() { + // Checks that encrypting many plaintext at the same time gives the correct output. + let key: [u8; BYTES_PER_AES_CALL] = CIPHER_KEY.to_ne_bytes(); + let aes = Aes128::new(&GenericArray::from(key)); + let ciphertexts = aes_encrypt_many( + PLAINTEXT, PLAINTEXT, PLAINTEXT, PLAINTEXT, PLAINTEXT, PLAINTEXT, PLAINTEXT, PLAINTEXT, + &aes, + ); + let ciphertexts: [u8; BYTES_PER_BATCH] = ciphertexts[..].try_into().unwrap(); + for i in 0..8 { + assert_eq!( + u128::from_ne_bytes( + ciphertexts[BYTES_PER_AES_CALL * i..BYTES_PER_AES_CALL * (i + 1)] + .try_into() + .unwrap() + ), + CIPHERTEXT + ); + } + } +} diff --git a/concrete-csprng/src/generators/implem/soft/generator.rs b/concrete-csprng/src/generators/implem/soft/generator.rs new file mode 100644 index 000000000..0b022dabc --- /dev/null +++ b/concrete-csprng/src/generators/implem/soft/generator.rs @@ -0,0 +1,110 @@ +use crate::generators::aes_ctr::{AesCtrGenerator, AesKey, ChildrenIterator}; +use crate::generators::implem::soft::block_cipher::SoftwareBlockCipher; +use crate::generators::{ByteCount, BytesPerChild, ChildrenCount, ForkError, RandomGenerator}; +use crate::seeders::Seed; + +/// A random number generator using a software implementation. +pub struct SoftwareRandomGenerator(pub(super) AesCtrGenerator); + +/// The children iterator used by [`SoftwareRandomGenerator`]. +/// +/// Outputs children generators one by one. +pub struct SoftwareChildrenIterator(ChildrenIterator); + +impl Iterator for SoftwareChildrenIterator { + type Item = SoftwareRandomGenerator; + + fn next(&mut self) -> Option { + self.0.next().map(SoftwareRandomGenerator) + } +} + +impl RandomGenerator for SoftwareRandomGenerator { + type ChildrenIter = SoftwareChildrenIterator; + fn new(seed: Seed) -> Self { + SoftwareRandomGenerator(AesCtrGenerator::new(AesKey(seed.0), None, None)) + } + fn remaining_bytes(&self) -> ByteCount { + self.0.remaining_bytes() + } + fn try_fork( + &mut self, + n_children: ChildrenCount, + n_bytes: BytesPerChild, + ) -> Result { + self.0 + .try_fork(n_children, n_bytes) + .map(SoftwareChildrenIterator) + } +} + +impl Iterator for SoftwareRandomGenerator { + type Item = u8; + + fn next(&mut self) -> Option { + self.0.next() + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::generators::aes_ctr::aes_ctr_generic_test; + use crate::generators::generator_generic_test; + + #[test] + fn prop_fork_first_state_table_index() { + aes_ctr_generic_test::prop_fork_first_state_table_index::(); + } + + #[test] + fn prop_fork_last_bound_table_index() { + aes_ctr_generic_test::prop_fork_last_bound_table_index::(); + } + + #[test] + fn prop_fork_parent_bound_table_index() { + aes_ctr_generic_test::prop_fork_parent_bound_table_index::(); + } + + #[test] + fn prop_fork_parent_state_table_index() { + aes_ctr_generic_test::prop_fork_parent_state_table_index::(); + } + + #[test] + fn prop_fork() { + aes_ctr_generic_test::prop_fork::(); + } + + #[test] + fn prop_fork_children_remaining_bytes() { + aes_ctr_generic_test::prop_fork_children_remaining_bytes::(); + } + + #[test] + fn prop_fork_parent_remaining_bytes() { + aes_ctr_generic_test::prop_fork_parent_remaining_bytes::(); + } + + #[test] + fn test_roughly_uniform() { + generator_generic_test::test_roughly_uniform::(); + } + + #[test] + fn test_fork() { + generator_generic_test::test_fork_children::(); + } + + #[test] + fn test_generator_determinism() { + generator_generic_test::test_generator_determinism::(); + } + + #[test] + #[should_panic(expected = "expected test panic")] + fn test_bounded_panic() { + generator_generic_test::test_bounded_none_should_panic::(); + } +} diff --git a/concrete-csprng/src/generators/implem/soft/mod.rs b/concrete-csprng/src/generators/implem/soft/mod.rs new file mode 100644 index 000000000..602ba02fc --- /dev/null +++ b/concrete-csprng/src/generators/implem/soft/mod.rs @@ -0,0 +1,11 @@ +//! A module using a software fallback implementation of random number generator. + +mod block_cipher; + +mod generator; +pub use generator::*; + +#[cfg(feature = "parallel")] +mod parallel; +#[cfg(feature = "parallel")] +pub use parallel::*; diff --git a/concrete-csprng/src/generators/implem/soft/parallel.rs b/concrete-csprng/src/generators/implem/soft/parallel.rs new file mode 100644 index 000000000..cb3012925 --- /dev/null +++ b/concrete-csprng/src/generators/implem/soft/parallel.rs @@ -0,0 +1,94 @@ +use super::*; +use crate::generators::aes_ctr::{AesCtrGenerator, ParallelChildrenIterator}; +use crate::generators::implem::soft::block_cipher::SoftwareBlockCipher; +use crate::generators::{BytesPerChild, ChildrenCount, ForkError, ParallelRandomGenerator}; +use rayon::iter::plumbing::{Consumer, ProducerCallback, UnindexedConsumer}; +use rayon::prelude::*; + +/// The parallel children iterator used by [`SoftwareRandomGenerator`]. +/// +/// Outputs the children generators one by one. +#[allow(clippy::type_complexity)] +pub struct ParallelSoftwareChildrenIterator( + rayon::iter::Map< + ParallelChildrenIterator, + fn(AesCtrGenerator) -> SoftwareRandomGenerator, + >, +); + +impl ParallelIterator for ParallelSoftwareChildrenIterator { + type Item = SoftwareRandomGenerator; + fn drive_unindexed(self, consumer: C) -> C::Result + where + C: UnindexedConsumer, + { + self.0.drive_unindexed(consumer) + } +} + +impl IndexedParallelIterator for ParallelSoftwareChildrenIterator { + fn len(&self) -> usize { + self.0.len() + } + fn drive>(self, consumer: C) -> C::Result { + self.0.drive(consumer) + } + fn with_producer>(self, callback: CB) -> CB::Output { + self.0.with_producer(callback) + } +} + +impl ParallelRandomGenerator for SoftwareRandomGenerator { + type ParChildrenIter = ParallelSoftwareChildrenIterator; + + fn par_try_fork( + &mut self, + n_children: ChildrenCount, + n_bytes: BytesPerChild, + ) -> Result { + self.0 + .par_try_fork(n_children, n_bytes) + .map(|iterator| ParallelSoftwareChildrenIterator(iterator.map(SoftwareRandomGenerator))) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::generators::aes_ctr::aes_ctr_parallel_generic_tests; + + #[test] + fn prop_fork_first_state_table_index() { + aes_ctr_parallel_generic_tests::prop_fork_first_state_table_index::(); + } + + #[test] + fn prop_fork_last_bound_table_index() { + aes_ctr_parallel_generic_tests::prop_fork_last_bound_table_index::(); + } + + #[test] + fn prop_fork_parent_bound_table_index() { + aes_ctr_parallel_generic_tests::prop_fork_parent_bound_table_index::(); + } + + #[test] + fn prop_fork_parent_state_table_index() { + aes_ctr_parallel_generic_tests::prop_fork_parent_state_table_index::(); + } + + #[test] + fn prop_fork() { + aes_ctr_parallel_generic_tests::prop_fork::(); + } + + #[test] + fn prop_fork_children_remaining_bytes() { + aes_ctr_parallel_generic_tests::prop_fork_children_remaining_bytes::(); + } + + #[test] + fn prop_fork_parent_remaining_bytes() { + aes_ctr_parallel_generic_tests::prop_fork_parent_remaining_bytes::(); + } +} diff --git a/concrete-csprng/src/generators/mod.rs b/concrete-csprng/src/generators/mod.rs new file mode 100644 index 000000000..3d0d861d9 --- /dev/null +++ b/concrete-csprng/src/generators/mod.rs @@ -0,0 +1,235 @@ +//! A module containing random generators objects. +//! +//! See [crate-level](`crate`) explanations. +use crate::seeders::Seed; +use std::error::Error; +use std::fmt::{Display, Formatter}; + +/// The number of children created when a generator is forked. +#[derive(Debug, Copy, Clone)] +pub struct ChildrenCount(pub usize); + +/// The number of bytes each child can generate, when a generator is forked. +#[derive(Debug, Copy, Clone)] +pub struct BytesPerChild(pub usize); + +/// A structure representing the number of bytes between two table indices. +#[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq)] +pub struct ByteCount(pub u128); + +/// An error occuring during a generator fork. +#[derive(Debug)] +pub enum ForkError { + ForkTooLarge, + ZeroChildrenCount, + ZeroBytesPerChild, +} + +impl Display for ForkError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + ForkError::ForkTooLarge => { + write!( + f, + "The children generators would output bytes after the parent bound. " + ) + } + ForkError::ZeroChildrenCount => { + write!( + f, + "The number of children in the fork must be greater than zero." + ) + } + ForkError::ZeroBytesPerChild => { + write!( + f, + "The number of bytes per child must be greater than zero." + ) + } + } + } +} +impl Error for ForkError {} + +/// A trait for cryptographically secure pseudo-random generators. +/// +/// See the [crate-level](#crate) documentation for details. +pub trait RandomGenerator: Iterator { + /// The iterator over children generators, returned by `try_fork` in case of success. + type ChildrenIter: Iterator; + + /// Creates a new generator from a seed. + /// + /// This operation is usually costly to perform, as the aes round keys need to be generated from + /// the seed. + fn new(seed: Seed) -> Self; + + /// Returns the number of bytes that can still be outputted by the generator before reaching its + /// bound. + /// + /// Note: + /// ----- + /// + /// A fresh generator can generate 2¹³² bytes. Unfortunately, no rust integer type in is able + /// to encode such a large number. Consequently [`ByteCount`] uses the largest integer type + /// available to encode this value: the `u128` type. For this reason, this method does not + /// effectively return the number of remaining bytes, but instead + /// `min(2¹²⁸-1, remaining_bytes)`. + fn remaining_bytes(&self) -> ByteCount; + + /// Returns the next byte of the stream, if the generator did not yet reach its bound. + fn next_byte(&mut self) -> Option { + self.next() + } + + /// Tries to fork the generator into an iterator of `n_children` new generators, each able to + /// output `n_bytes` bytes. + /// + /// Note: + /// ----- + /// + /// To be successful, the number of remaining bytes for the parent generator must be larger than + /// `n_children*n_bytes`. + fn try_fork( + &mut self, + n_children: ChildrenCount, + n_bytes: BytesPerChild, + ) -> Result; +} + +/// A trait extending [`RandomGenerator`] to the parallel iterators of `rayon`. +#[cfg(feature = "parallel")] +pub trait ParallelRandomGenerator: RandomGenerator + Send { + /// The iterator over children generators, returned by `par_try_fork` in case of success. + type ParChildrenIter: rayon::prelude::IndexedParallelIterator; + + /// Tries to fork the generator into a parallel iterator of `n_children` new generators, each + /// able to output `n_bytes` bytes. + /// + /// Note: + /// ----- + /// + /// To be successful, the number of remaining bytes for the parent generator must be larger than + /// `n_children*n_bytes`. + fn par_try_fork( + &mut self, + n_children: ChildrenCount, + n_bytes: BytesPerChild, + ) -> Result; +} + +mod aes_ctr; + +mod implem; +pub use implem::*; + +#[cfg(test)] +pub mod generator_generic_test { + #![allow(unused)] // to please clippy when tests are not activated + use super::*; + use rand::Rng; + + const REPEATS: usize = 1_000; + + fn any_seed() -> impl Iterator { + std::iter::repeat_with(|| Seed(rand::thread_rng().gen())) + } + + fn some_children_count() -> impl Iterator { + std::iter::repeat_with(|| ChildrenCount(rand::thread_rng().gen::() % 16 + 1)) + } + + fn some_bytes_per_child() -> impl Iterator { + std::iter::repeat_with(|| BytesPerChild(rand::thread_rng().gen::() % 128 + 1)) + } + + /// Checks that the PRNG roughly generates uniform numbers. + /// + /// To do that, we perform an histogram of the occurences of each byte value, over a fixed + /// number of samples and check that the empirical probabilities of the bins are close to + /// the theoretical probabilities. + pub fn test_roughly_uniform() { + // Number of bins to use for the histogram. + const N_BINS: usize = u8::MAX as usize + 1; + // Number of samples to use for the histogram. + let n_samples = 10_000_000_usize; + // Theoretical probability of a each bins. + let expected_prob: f64 = 1. / N_BINS as f64; + // Absolute error allowed on the empirical probabilities. + // This value was tuned to make the test pass on an arguably correct state of + // implementation. 10^-4 precision is arguably pretty fine for this rough test, but it would + // be interesting to improve this test. + let precision = 10f64.powi(-3); + + for _ in 0..REPEATS { + // We instantiate a new generator. + let seed = any_seed().next().unwrap(); + let mut generator = G::new(seed); + // We create a new histogram + let mut counts = [0usize; N_BINS]; + // We fill the histogram. + for _ in 0..n_samples { + counts[generator.next_byte().unwrap() as usize] += 1; + } + // We check that the empirical probabilities are close enough to the theoretical one. + counts + .iter() + .map(|a| (*a as f64) / (n_samples as f64)) + .for_each(|a| assert!((a - expected_prob).abs() < precision)) + } + } + + /// Checks that given a state and a key, the PRNG is determinist. + pub fn test_generator_determinism() { + for _ in 0..REPEATS { + let seed = any_seed().next().unwrap(); + let mut first_generator = G::new(seed); + let mut second_generator = G::new(seed); + for _ in 0..1024 { + assert_eq!(first_generator.next(), second_generator.next()); + } + } + } + + /// Checks that forks returns a bounded child, and that the proper number of bytes can be + /// generated. + pub fn test_fork_children() { + for _ in 0..REPEATS { + let ((seed, n_children), n_bytes) = any_seed() + .zip(some_children_count()) + .zip(some_bytes_per_child()) + .next() + .unwrap(); + let mut gen = G::new(seed); + let mut bounded = gen.try_fork(n_children, n_bytes).unwrap().next().unwrap(); + assert_eq!(bounded.remaining_bytes(), ByteCount(n_bytes.0 as u128)); + for _ in 0..n_bytes.0 { + bounded.next().unwrap(); + } + + // Assert we are at the bound + assert!(bounded.next().is_none()); + } + } + + /// Checks that a bounded prng returns none when exceeding the allowed number of bytes. + /// + /// To properly check for panic use `#[should_panic(expected = "expected test panic")]` as an + /// attribute on the test function. + pub fn test_bounded_none_should_panic() { + let ((seed, n_children), n_bytes) = any_seed() + .zip(some_children_count()) + .zip(some_bytes_per_child()) + .next() + .unwrap(); + let mut gen = G::new(seed); + let mut bounded = gen.try_fork(n_children, n_bytes).unwrap().next().unwrap(); + assert_eq!(bounded.remaining_bytes(), ByteCount(n_bytes.0 as u128)); + for _ in 0..n_bytes.0 { + assert!(bounded.next().is_some()); + } + + // One call too many, should panic + bounded.next().ok_or("expected test panic").unwrap(); + } +} diff --git a/concrete-csprng/src/lib.rs b/concrete-csprng/src/lib.rs new file mode 100644 index 000000000..57c9cb1d4 --- /dev/null +++ b/concrete-csprng/src/lib.rs @@ -0,0 +1,114 @@ +#![deny(rustdoc::broken_intra_doc_links)] +//! Cryptographically secure pseudo random number generator. +//! +//! Welcome to the `concrete-csprng` documentation. +//! +//! This crate provides a fast cryptographically secure pseudo-random number generator, suited to +//! work in a multithreaded setting. +//! +//! Random Generators +//! ================= +//! +//! The central abstraction of this crate is the [`RandomGenerator`](generators::RandomGenerator) +//! trait, which is implemented by different types, each supporting a different platform. In +//! essence, a type implementing [`RandomGenerator`](generators::RandomGenerator) is a type that +//! outputs a new pseudo-random byte at each call to +//! [`next_byte`](generators::RandomGenerator::next_byte). Such a generator `g` can be seen as +//! enclosing a growing index into an imaginary array of pseudo-random bytes: +//! ```ascii +//! 0 1 2 3 4 5 6 7 8 9 M-1 │ +//! ┏━┯━┯━┯━┯━┯━┯━┯━┯━┯━┯━━━┯━┓ │ +//! ┃ │ │ │ │ │ │ │ │ │ │...│ ┃ │ +//! ┗↥┷━┷━┷━┷━┷━┷━┷━┷━┷━┷━━━┷━┛ │ +//! g │ +//! │ +//! g.next_byte() │ +//! │ +//! 0 1 2 3 4 5 6 7 8 9 M-1 │ +//! ┏━┯━┯━┯━┯━┯━┯━┯━┯━┯━┯━━━┯━┓ │ +//! ┃╳│ │ │ │ │ │ │ │ │ │...│ ┃ │ +//! ┗━┷↥┷━┷━┷━┷━┷━┷━┷━┷━┷━━━┷━┛ │ +//! g │ +//! │ +//! g.next_byte() │ legend: +//! │ ------- +//! 0 1 2 3 4 5 6 7 8 9 M-1 │ ↥ : next byte to be outputted by g +//! ┏━┯━┯━┯━┯━┯━┯━┯━┯━┯━┯━━━┯━┓ │ │ │: byte not yet outputted by g +//! ┃╳│╳│ │ │ │ │ │ │ │ │...│ ┃ │ │╳│: byte already outputted by g +//! ┗━┷━┷↥┷━┷━┷━┷━┷━┷━┷━┷━━━┷━┛ │ +//! g 🭭 +//! ``` +//! +//! While being large, this imaginary array is still bounded to M = 2¹³² bytes. Consequently, a +//! generator is always bounded to a maximal index. That is, there is always a max amount of +//! elements of this array that can be outputted by the generator. By default, generators created +//! via [`new`](generators::RandomGenerator::new) are always bounded to M-1. +//! +//! Tree partition of the pseudo-random stream +//! ========================================== +//! +//! One particularity of this implementation is that you can use the +//! [`try_fork`](generators::RandomGenerator::try_fork) method to create an arbitrary partition tree +//! of a region of this array. Indeed, calling `try_fork(nc, nb)` outputs `nc` new generators, each +//! able to output `nb` bytes. The `try_fork` method ensures that the states and bounds of the +//! parent and children generators are set so as to prevent the same substream to be outputted +//! twice: +//! ```ascii +//! 0 1 2 3 4 5 6 7 8 9 M │ +//! ┏━┯━┯━┯━┯━┯━┯━┯━┯━┯━┯━━━┯━┓ │ +//! ┃P│P│P│P│P│P│P│P│P│P│...│P┃ │ +//! ┗↥┷━┷━┷━┷━┷━┷━┷━┷━┷━┷━━━┷━┛ │ +//! p │ +//! │ +//! (a,b) = p.fork(2,4) │ +//! │ +//! 0 1 2 3 4 5 6 7 8 9 M │ +//! ┏━┯━┯━┯━┯━┯━┯━┯━┯━┯━┯━━━┯━┓ │ +//! ┃A│A│A│A│B│B│B│B│P│P│...│P┃ │ +//! ┗↥┷━┷━┷━┷↥┷━┷━┷━┷↥┷━┷━━━┷━┛ │ +//! a b p │ +//! │ legend: +//! (c,d) = b.fork(2, 1) │ ------- +//! │ ↥ : next byte to be outputted by p +//! 0 1 2 3 4 5 6 7 8 9 M │ │P│: byte to be outputted by p +//! ┏━┯━┯━┯━┯━┯━┯━┯━┯━┯━┯━━━┯━┓ │ │╳│: byte already outputted +//! ┃A│A│A│A│C│D│B│B│P│P│...│P┃ │ +//! ┗↥┷━┷━┷━┷↥┷↥┷↥┷━┷↥┷━┷━━━┷━┛ │ +//! a c d b p 🭭 +//! ``` +//! +//! This makes it possible to consume the stream at different places. This is particularly useful in +//! a multithreaded setting, in which we want to use the same generator from different independent +//! threads: +//! +//! ```ascii +//! 0 1 2 3 4 5 6 7 8 9 M │ +//! ┏━┯━┯━┯━┯━┯━┯━┯━┯━┯━┯━━━┯━┓ │ +//! ┃A│A│A│A│C│D│B│B│P│P│...│P┃ │ +//! ┗↥┷━┷━┷━┷↥┷↥┷↥┷━┷↥┷━┷━━━┷━┛ │ +//! a c d b p │ +//! │ +//! a.next_byte() │ +//! │ +//! 0 1 2 3 4 5 6 7 8 9 M │ +//! ┏━┯━┯━┯━┯━┯━┯━┯━┯━┯━┯━━━┯━┓ │ +//! ┃╳│A│A│A│C│D│B│B│P│P│...│P┃ │ +//! ┗━┷↥┷━┷━┷↥┷↥┷↥┷━┷↥┷━┷━━━┷━┛ │ +//! a c d b p │ +//! │ legend: +//! b.next_byte() │ ------- +//! │ ↥ : next byte to be outputted by p +//! 0 1 2 3 4 5 6 7 8 9 M │ │P│: byte to be outputted by p +//! ┏━┯━┯━┯━┯━┯━┯━┯━┯━┯━┯━━━┯━┓ │ │╳│: byte already outputted +//! ┃╳│A│A│A│C│D│╳│B│P│P│...│P┃ │ +//! ┗━┷↥┷━┷━┷↥┷↥┷━┷↥┷↥┷━┷━━━┷━┛ │ +//! a c d b p 🭭 +//! ``` +//! +//! Implementation +//! ============== +//! +//! The implementation is based on the AES blockcipher used in counter (CTR) mode, as presented +//! in the ISO/IEC 18033-4 document. +pub mod generators; +pub mod seeders; diff --git a/concrete-csprng/src/main.rs b/concrete-csprng/src/main.rs new file mode 100644 index 000000000..343d1bc39 --- /dev/null +++ b/concrete-csprng/src/main.rs @@ -0,0 +1,58 @@ +//! This program uses the concrete csprng to generate an infinite stream of random bytes on +//! the program stdout. For testing purpose. +#[cfg(feature = "generator_x86_64_aesni")] +use concrete_csprng::generators::AesniRandomGenerator as ActivatedRandomGenerator; +#[cfg(feature = "generator_aarch64_aes")] +use concrete_csprng::generators::NeonAesRandomGenerator as ActivatedRandomGenerator; +#[cfg(all( + not(feature = "generator_x86_64_aesni"), + not(feature = "generator_aarch64_aes"), + feature = "generator_fallback" +))] +use concrete_csprng::generators::SoftwareRandomGenerator as ActivatedRandomGenerator; + +use concrete_csprng::generators::RandomGenerator; + +#[cfg(target_os = "macos")] +use concrete_csprng::seeders::AppleSecureEnclaveSeeder as ActivatedSeeder; +#[cfg(all(not(target_os = "macos"), feature = "seeder_x86_64_rdseed"))] +use concrete_csprng::seeders::RdseedSeeder as ActivatedSeeder; +#[cfg(all( + not(target_os = "macos"), + not(feature = "seeder_x86_64_rdseed"), + feature = "seeder_unix" +))] +use concrete_csprng::seeders::UnixSeeder as ActivatedSeeder; + +use concrete_csprng::seeders::Seeder; + +use std::io::prelude::*; +use std::io::stdout; + +pub fn main() { + // Ugly hack to be able to use UnixSeeder + #[cfg(all( + not(target_os = "macos"), + not(feature = "seeder_x86_64_rdseed"), + feature = "seeder_unix" + ))] + let new_seeder = || ActivatedSeeder::new(0); + #[cfg(not(all( + not(target_os = "macos"), + not(feature = "seeder_x86_64_rdseed"), + feature = "seeder_unix" + )))] + let new_seeder = || ActivatedSeeder; + + let mut seeder = new_seeder(); + let mut generator = ActivatedRandomGenerator::new(seeder.seed()); + let mut stdout = stdout(); + let mut buffer = [0u8; 16]; + loop { + buffer + .iter_mut() + .zip(&mut generator) + .for_each(|(b, g)| *b = g); + stdout.write_all(&buffer).unwrap(); + } +} diff --git a/concrete-csprng/src/seeders/implem/apple_secure_enclave_seeder.rs b/concrete-csprng/src/seeders/implem/apple_secure_enclave_seeder.rs new file mode 100644 index 000000000..8328c8269 --- /dev/null +++ b/concrete-csprng/src/seeders/implem/apple_secure_enclave_seeder.rs @@ -0,0 +1,141 @@ +use crate::seeders::{Seed, Seeder}; +use libc; +use std::cmp::Ordering; + +/// There is no `rseed` equivalent in the ARM specification until `ARMv8.5-A`. +/// However it seems that these instructions are not exposed in `core::arch::aarch64`. +/// +/// Our primary interest for supporting aarch64 targets is AppleSilicon support +/// which for the M1 macs available, they are based on the `ARMv8.4-A` set. +/// +/// So we fall back to using a function from Apple's API which +/// uses the [Secure Enclave] to generate cryptographically secure random bytes. +/// +/// [Secure Enclave]: https://support.apple.com/fr-fr/guide/security/sec59b0b31ff/web +mod secure_enclave { + pub enum __SecRandom {} + pub type SecRandomRef = *const __SecRandom; + use libc::{c_int, c_void}; + + #[link(name = "Security", kind = "framework")] + extern "C" { + pub static kSecRandomDefault: SecRandomRef; + + pub fn SecRandomCopyBytes(rnd: SecRandomRef, count: usize, bytes: *mut c_void) -> c_int; + } + + pub fn generate_random_bytes(bytes: &mut [u8]) -> std::io::Result<()> { + // As per Apple's documentation: + // - https://developer.apple.com/documentation/security/randomization_services?language=objc + // - https://developer.apple.com/documentation/security/1399291-secrandomcopybytes?language=objc + // + // The `SecRandomCopyBytes` "Generate cryptographically secure random numbers" + unsafe { + let res = SecRandomCopyBytes( + kSecRandomDefault, + bytes.len(), + bytes.as_mut_ptr() as *mut c_void, + ); + if res != 0 { + Err(std::io::Error::last_os_error()) + } else { + Ok(()) + } + } + } +} + +/// A seeder which uses the `SecRandomCopyBytes` function from Apple's `Security` framework. +/// +/// +pub struct AppleSecureEnclaveSeeder; + +impl Seeder for AppleSecureEnclaveSeeder { + fn seed(&mut self) -> Seed { + // 16 bytes == 128 bits + let mut bytes = [0u8; 16]; + secure_enclave::generate_random_bytes(&mut bytes) + .expect("Failure while using Apple secure enclave: {err:?}"); + + Seed(u128::from_le_bytes(bytes)) + } + + fn is_available() -> bool { + let os_version_sysctl_name = match std::ffi::CString::new("kern.osproductversion") { + Ok(c_str) => c_str, + _ => return false, + }; + + // Big enough buffer to get a version output as an ASCII string + const OUTPUT_BUFFER_SIZE: usize = 64; + let mut output_buffer_size = OUTPUT_BUFFER_SIZE; + let mut output_buffer = [0u8; OUTPUT_BUFFER_SIZE]; + let res = unsafe { + libc::sysctlbyname( + os_version_sysctl_name.as_ptr() as *const _ as *const _, + &mut output_buffer as *mut _ as *mut _, + &mut output_buffer_size as *mut _ as *mut _, + std::ptr::null_mut(), + 0, + ) + }; + + if res != 0 { + return false; + } + + let result_c_str = + match std::ffi::CStr::from_bytes_with_nul(&output_buffer[..output_buffer_size]) { + Ok(c_str) => c_str, + _ => return false, + }; + + let result_string = match result_c_str.to_str() { + Ok(str) => str, + _ => return false, + }; + + // Normally we get a major version and minor version + let split_string: Vec<&str> = result_string.split('.').collect(); + + let mut major = -1; + let mut minor = -1; + + // Major part of the version string + if !split_string.is_empty() { + major = match split_string[0].parse() { + Ok(major_from_str) => major_from_str, + _ => return false, + }; + } + + // SecRandomCopyBytes is available starting with mac OS 10.7 + // https://developer.apple.com/documentation/security/1399291-secrandomcopybytes?language=objc + // This match pattern is recommended by clippy, so we oblige here + match major.cmp(&10) { + Ordering::Greater => true, + Ordering::Equal => { + // Minor part of the version string + if split_string.len() >= 2 { + minor = match split_string[1].parse() { + Ok(minor_from_str) => minor_from_str, + _ => return false, + }; + } + minor >= 7 + } + Ordering::Less => false, + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::seeders::generic_tests::check_seeder_fixed_sequences_different; + + #[test] + fn check_bounded_sequence_difference() { + check_seeder_fixed_sequences_different(|_| AppleSecureEnclaveSeeder); + } +} diff --git a/concrete-csprng/src/seeders/implem/mod.rs b/concrete-csprng/src/seeders/implem/mod.rs new file mode 100644 index 000000000..2d11d9959 --- /dev/null +++ b/concrete-csprng/src/seeders/implem/mod.rs @@ -0,0 +1,14 @@ +#[cfg(target_os = "macos")] +mod apple_secure_enclave_seeder; +#[cfg(target_os = "macos")] +pub use apple_secure_enclave_seeder::AppleSecureEnclaveSeeder; + +#[cfg(feature = "seeder_x86_64_rdseed")] +mod rdseed; +#[cfg(feature = "seeder_x86_64_rdseed")] +pub use rdseed::RdseedSeeder; + +#[cfg(feature = "seeder_unix")] +mod unix; +#[cfg(feature = "seeder_unix")] +pub use unix::UnixSeeder; diff --git a/concrete-csprng/src/seeders/implem/rdseed.rs b/concrete-csprng/src/seeders/implem/rdseed.rs new file mode 100644 index 000000000..518a4fe24 --- /dev/null +++ b/concrete-csprng/src/seeders/implem/rdseed.rs @@ -0,0 +1,50 @@ +use crate::seeders::{Seed, Seeder}; + +/// A seeder which uses the `rdseed` x86_64 instruction. +/// +/// The `rdseed` instruction allows to deliver seeds from a hardware source of entropy see +/// . +pub struct RdseedSeeder; + +impl Seeder for RdseedSeeder { + fn seed(&mut self) -> Seed { + Seed(rdseed_random_m128()) + } + + fn is_available() -> bool { + is_x86_feature_detected!("rdseed") + } +} + +// Generates a random 128 bits value from rdseed +fn rdseed_random_m128() -> u128 { + let mut rand1: u64 = 0; + let mut rand2: u64 = 0; + let mut output_bytes = [0u8; 16]; + unsafe { + loop { + if core::arch::x86_64::_rdseed64_step(&mut rand1) == 1 { + break; + } + } + loop { + if core::arch::x86_64::_rdseed64_step(&mut rand2) == 1 { + break; + } + } + } + output_bytes[0..8].copy_from_slice(&rand1.to_ne_bytes()); + output_bytes[8..16].copy_from_slice(&rand2.to_ne_bytes()); + u128::from_ne_bytes(output_bytes) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::seeders::generic_tests::check_seeder_fixed_sequences_different; + + #[test] + fn check_bounded_sequence_difference() { + check_seeder_fixed_sequences_different(|_| RdseedSeeder); + } +} diff --git a/concrete-csprng/src/seeders/implem/unix.rs b/concrete-csprng/src/seeders/implem/unix.rs new file mode 100644 index 000000000..f52953235 --- /dev/null +++ b/concrete-csprng/src/seeders/implem/unix.rs @@ -0,0 +1,72 @@ +use crate::seeders::{Seed, Seeder}; +use std::fs::File; +use std::io::Read; + +/// A seeder which uses the `/dev/random` source on unix-like systems. +pub struct UnixSeeder { + counter: u128, + secret: u128, + file: File, +} + +impl UnixSeeder { + /// Creates a new seeder from a user defined secret. + /// + /// Important: + /// ---------- + /// + /// This secret is used to ensure the quality of the seed in scenarios where `/dev/random` may + /// be compromised. + /// + /// The attack hypotheses are as follow: + /// - `/dev/random` output can be predicted by a process running on the machine by just + /// observing various states of the machine + /// - The attacker cannot read data from the process where `concrete-csprng` is running + /// + /// Using a secret in `concrete-csprng` allows to generate values that the attacker cannot + /// predict, making this seeder secure on systems were `/dev/random` outputs can be + /// predicted. + pub fn new(secret: u128) -> UnixSeeder { + let file = std::fs::File::open("/dev/random").expect("Failed to open /dev/random ."); + let counter = std::time::UNIX_EPOCH + .elapsed() + .expect("Failed to initialize unix seeder.") + .as_nanos(); + UnixSeeder { + secret, + counter, + file, + } + } +} + +impl Seeder for UnixSeeder { + fn seed(&mut self) -> Seed { + let output = self.secret ^ self.counter ^ dev_random(&mut self.file); + self.counter = self.counter.wrapping_add(1); + Seed(output) + } + + fn is_available() -> bool { + cfg!(target_family = "unix") + } +} + +fn dev_random(random: &mut File) -> u128 { + let mut buf = [0u8; 16]; + random + .read_exact(&mut buf[..]) + .expect("Failed to read from /dev/random ."); + u128::from_ne_bytes(buf) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::seeders::generic_tests::check_seeder_fixed_sequences_different; + + #[test] + fn check_bounded_sequence_difference() { + check_seeder_fixed_sequences_different(UnixSeeder::new); + } +} diff --git a/concrete-csprng/src/seeders/mod.rs b/concrete-csprng/src/seeders/mod.rs new file mode 100644 index 000000000..5048fef94 --- /dev/null +++ b/concrete-csprng/src/seeders/mod.rs @@ -0,0 +1,47 @@ +//! A module containing seeders objects. +//! +//! When initializing a generator, one needs to provide a [`Seed`], which is then used as key to the +//! AES blockcipher. As a consequence, the quality of the outputs of the generator is directly +//! conditioned by the quality of this seed. This module proposes different mechanisms to deliver +//! seeds that can accomodate varying scenarios. + +/// A seed value, used to initialize a generator. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct Seed(pub u128); + +/// A trait representing a seeding strategy. +pub trait Seeder { + /// Generates a new seed. + fn seed(&mut self) -> Seed; + + /// Check whether the seeder can be used on the current machine. This function may check if some + /// required CPU features are available or if some OS features are availble for example. + fn is_available() -> bool + where + Self: Sized; +} + +mod implem; +pub use implem::*; + +#[cfg(test)] +mod generic_tests { + use crate::seeders::Seeder; + + /// Naively verifies that two fixed-size sequences generated by repeatedly calling the seeder + /// are different. + #[allow(unused)] // to please clippy when tests are not activated + pub fn check_seeder_fixed_sequences_different S>( + construct_seeder: F, + ) { + const SEQUENCE_SIZE: usize = 500; + const REPEATS: usize = 10_000; + for i in 0..REPEATS { + let mut seeder = construct_seeder(i as u128); + let orig_seed = seeder.seed(); + for _ in 0..SEQUENCE_SIZE { + assert_ne!(seeder.seed(), orig_seed); + } + } + } +}