// SPDX-License-Identifier: MPL-2.0 //! Implementations of XOFs specified in [[draft-irtf-cfrg-vdaf-08]]. //! //! [draft-irtf-cfrg-vdaf-08]: https://datatracker.ietf.org/doc/draft-irtf-cfrg-vdaf/08/ /// Value of the domain separation byte "D" used by XofTurboShake128 when invoking TurboSHAKE128. const XOF_TURBO_SHAKE_128_DOMAIN_SEPARATION: u8 = 1; /// Value of the domain separation byte "D" used by XofFixedKeyAes128 when invoking TurboSHAKE128. #[cfg(all(feature = "crypto-dependencies", feature = "experimental"))] const XOF_FIXED_KEY_AES_128_DOMAIN_SEPARATION: u8 = 2; use crate::{ field::FieldElement, prng::Prng, vdaf::{CodecError, Decode, Encode}, }; #[cfg(all(feature = "crypto-dependencies", feature = "experimental"))] use aes::{ cipher::{generic_array::GenericArray, BlockEncrypt, KeyInit}, Block, }; #[cfg(feature = "crypto-dependencies")] use aes::{ cipher::{KeyIvInit, StreamCipher}, Aes128, }; #[cfg(feature = "crypto-dependencies")] use ctr::Ctr64BE; #[cfg(feature = "crypto-dependencies")] use hmac::{Hmac, Mac}; use rand_core::{ impls::{next_u32_via_fill, next_u64_via_fill}, RngCore, SeedableRng, }; #[cfg(feature = "crypto-dependencies")] use sha2::Sha256; use sha3::{ digest::{ExtendableOutput, Update, XofReader}, TurboShake128, TurboShake128Core, TurboShake128Reader, }; #[cfg(feature = "crypto-dependencies")] use std::fmt::Formatter; use std::{ fmt::Debug, io::{Cursor, Read}, }; use subtle::{Choice, ConstantTimeEq}; /// Input of [`Xof`]. #[derive(Clone, Debug)] pub struct Seed(pub(crate) [u8; SEED_SIZE]); impl Seed { /// Generate a uniform random seed. pub fn generate() -> Result { let mut seed = [0; SEED_SIZE]; getrandom::getrandom(&mut seed)?; Ok(Self::from_bytes(seed)) } /// Construct seed from a byte slice. pub(crate) fn from_bytes(seed: [u8; SEED_SIZE]) -> Self { Self(seed) } } impl AsRef<[u8; SEED_SIZE]> for Seed { fn as_ref(&self) -> &[u8; SEED_SIZE] { &self.0 } } impl PartialEq for Seed { fn eq(&self, other: &Self) -> bool { self.ct_eq(other).into() } } impl Eq for Seed {} impl ConstantTimeEq for Seed { fn ct_eq(&self, other: &Self) -> Choice { self.0.ct_eq(&other.0) } } impl Encode for Seed { fn encode(&self, bytes: &mut Vec) -> Result<(), CodecError> { bytes.extend_from_slice(&self.0[..]); Ok(()) } fn encoded_len(&self) -> Option { Some(SEED_SIZE) } } impl Decode for Seed { fn decode(bytes: &mut Cursor<&[u8]>) -> Result { let mut seed = [0; SEED_SIZE]; bytes.read_exact(&mut seed)?; Ok(Seed(seed)) } } /// Trait for deriving a vector of field elements. pub trait IntoFieldVec: RngCore + Sized { /// Generate a finite field vector from the seed stream. fn into_field_vec(self, length: usize) -> Vec; } impl IntoFieldVec for S { fn into_field_vec(self, length: usize) -> Vec { Prng::from_seed_stream(self).take(length).collect() } } /// An extendable output function (XOF) with the interface specified in [[draft-irtf-cfrg-vdaf-08]]. /// /// [draft-irtf-cfrg-vdaf-08]: https://datatracker.ietf.org/doc/draft-irtf-cfrg-vdaf/08/ pub trait Xof: Clone + Debug { /// The type of stream produced by this XOF. type SeedStream: RngCore + Sized; /// Construct an instance of [`Xof`] with the given seed. fn init(seed_bytes: &[u8; SEED_SIZE], dst: &[u8]) -> Self; /// Update the XOF state by passing in the next fragment of the info string. The final info /// string is assembled from the concatenation of sequence of fragments passed to this method. fn update(&mut self, data: &[u8]); /// Finalize the XOF state, producing a seed stream. fn into_seed_stream(self) -> Self::SeedStream; /// Finalize the XOF state, producing a seed. fn into_seed(self) -> Seed { let mut new_seed = [0; SEED_SIZE]; let mut seed_stream = self.into_seed_stream(); seed_stream.fill_bytes(&mut new_seed); Seed(new_seed) } /// Construct a seed stream from the given seed and info string. fn seed_stream(seed: &Seed, dst: &[u8], binder: &[u8]) -> Self::SeedStream { let mut xof = Self::init(seed.as_ref(), dst); xof.update(binder); xof.into_seed_stream() } } /// The key stream produced by AES128 in CTR-mode. #[cfg(feature = "crypto-dependencies")] #[cfg_attr(docsrs, doc(cfg(feature = "crypto-dependencies")))] pub struct SeedStreamAes128(Ctr64BE); #[cfg(feature = "crypto-dependencies")] impl SeedStreamAes128 { /// Construct an instance of the seed stream with the given AES key `key` and initialization /// vector `iv`. pub fn new(key: &[u8], iv: &[u8]) -> Self { SeedStreamAes128( as KeyIvInit>::new(key.into(), iv.into())) } fn fill(&mut self, buf: &mut [u8]) { buf.fill(0); self.0.apply_keystream(buf); } } #[cfg(feature = "crypto-dependencies")] impl RngCore for SeedStreamAes128 { fn fill_bytes(&mut self, dest: &mut [u8]) { self.fill(dest); } fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> { self.fill(dest); Ok(()) } fn next_u32(&mut self) -> u32 { next_u32_via_fill(self) } fn next_u64(&mut self) -> u64 { next_u64_via_fill(self) } } #[cfg(feature = "crypto-dependencies")] impl Debug for SeedStreamAes128 { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { // Ctr64BE does not implement Debug, but [`ctr::CtrCore`][1] does, and we get that // with [`cipher::StreamCipherCoreWrapper::get_core`][2]. // // [1]: https://docs.rs/ctr/latest/ctr/struct.CtrCore.html // [2]: https://docs.rs/cipher/latest/cipher/struct.StreamCipherCoreWrapper.html self.0.get_core().fmt(f) } } /// The XOF based on TurboSHAKE128 as specified in [[draft-irtf-cfrg-vdaf-08]]. /// /// [draft-irtf-cfrg-vdaf-08]: https://datatracker.ietf.org/doc/draft-irtf-cfrg-vdaf/08/ #[derive(Clone, Debug)] pub struct XofTurboShake128(TurboShake128); impl Xof<16> for XofTurboShake128 { type SeedStream = SeedStreamTurboShake128; fn init(seed_bytes: &[u8; 16], dst: &[u8]) -> Self { let mut xof = Self(TurboShake128::from_core(TurboShake128Core::new( XOF_TURBO_SHAKE_128_DOMAIN_SEPARATION, ))); Update::update( &mut xof.0, &[dst.len().try_into().expect("dst must be at most 255 bytes")], ); Update::update(&mut xof.0, dst); Update::update(&mut xof.0, seed_bytes); xof } fn update(&mut self, data: &[u8]) { Update::update(&mut self.0, data); } fn into_seed_stream(self) -> SeedStreamTurboShake128 { SeedStreamTurboShake128::new(self.0.finalize_xof()) } } /// The seed stream produced by TurboSHAKE128. pub struct SeedStreamTurboShake128(TurboShake128Reader); impl SeedStreamTurboShake128 { pub(crate) fn new(reader: TurboShake128Reader) -> Self { Self(reader) } } impl RngCore for SeedStreamTurboShake128 { fn fill_bytes(&mut self, dest: &mut [u8]) { XofReader::read(&mut self.0, dest); } fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> { XofReader::read(&mut self.0, dest); Ok(()) } fn next_u32(&mut self) -> u32 { next_u32_via_fill(self) } fn next_u64(&mut self) -> u64 { next_u64_via_fill(self) } } /// A `rand`-compatible interface to construct XofTurboShake128 seed streams, with the domain /// separation tag and binder string both fixed as the empty string. impl SeedableRng for SeedStreamTurboShake128 { type Seed = [u8; 16]; fn from_seed(seed: Self::Seed) -> Self { XofTurboShake128::init(&seed, b"").into_seed_stream() } } /// Factory to produce multiple [`XofFixedKeyAes128`] instances with the same fixed key and /// different seeds. #[cfg(all(feature = "crypto-dependencies", feature = "experimental"))] #[cfg_attr( docsrs, doc(cfg(all(feature = "crypto-dependencies", feature = "experimental"))) )] pub struct XofFixedKeyAes128Key { cipher: Aes128, } #[cfg(all(feature = "crypto-dependencies", feature = "experimental"))] impl XofFixedKeyAes128Key { /// Derive the fixed key from the domain separation tag and binder string. pub fn new(dst: &[u8], binder: &[u8]) -> Self { let mut fixed_key_deriver = TurboShake128::from_core(TurboShake128Core::new( XOF_FIXED_KEY_AES_128_DOMAIN_SEPARATION, )); Update::update( &mut fixed_key_deriver, &[dst.len().try_into().expect("dst must be at most 255 bytes")], ); Update::update(&mut fixed_key_deriver, dst); Update::update(&mut fixed_key_deriver, binder); let mut key = GenericArray::from([0; 16]); XofReader::read(&mut fixed_key_deriver.finalize_xof(), key.as_mut()); Self { cipher: Aes128::new(&key), } } /// Combine a fixed key with a seed to produce a new stream of bytes. pub fn with_seed(&self, seed: &[u8; 16]) -> SeedStreamFixedKeyAes128 { SeedStreamFixedKeyAes128 { cipher: self.cipher.clone(), base_block: (*seed).into(), length_consumed: 0, } } } /// XofFixedKeyAes128 as specified in [[draft-irtf-cfrg-vdaf-08]]. This XOF is NOT RECOMMENDED for /// general use; see Section 9 ("Security Considerations") for details. /// /// This XOF combines TurboSHAKE128 and a fixed-key mode of operation for AES-128. The key is /// "fixed" in the sense that it is derived (using TurboSHAKE128) from the domain separation tag and /// binder strings, and depending on the application, these strings can be hard-coded. The seed is /// used to construct each block of input passed to a hash function built from AES-128. /// /// [draft-irtf-cfrg-vdaf-08]: https://datatracker.ietf.org/doc/draft-irtf-cfrg-vdaf/08/ #[derive(Clone, Debug)] #[cfg(all(feature = "crypto-dependencies", feature = "experimental"))] #[cfg_attr( docsrs, doc(cfg(all(feature = "crypto-dependencies", feature = "experimental"))) )] pub struct XofFixedKeyAes128 { fixed_key_deriver: TurboShake128, base_block: Block, } #[cfg(all(feature = "crypto-dependencies", feature = "experimental"))] impl Xof<16> for XofFixedKeyAes128 { type SeedStream = SeedStreamFixedKeyAes128; fn init(seed_bytes: &[u8; 16], dst: &[u8]) -> Self { let mut fixed_key_deriver = TurboShake128::from_core(TurboShake128Core::new(2u8)); Update::update( &mut fixed_key_deriver, &[dst.len().try_into().expect("dst must be at most 255 bytes")], ); Update::update(&mut fixed_key_deriver, dst); Self { fixed_key_deriver, base_block: (*seed_bytes).into(), } } fn update(&mut self, data: &[u8]) { Update::update(&mut self.fixed_key_deriver, data); } fn into_seed_stream(self) -> SeedStreamFixedKeyAes128 { let mut fixed_key = GenericArray::from([0; 16]); XofReader::read( &mut self.fixed_key_deriver.finalize_xof(), fixed_key.as_mut(), ); SeedStreamFixedKeyAes128 { base_block: self.base_block, cipher: Aes128::new(&fixed_key), length_consumed: 0, } } } /// Seed stream for [`XofFixedKeyAes128`]. #[cfg(all(feature = "crypto-dependencies", feature = "experimental"))] #[cfg_attr( docsrs, doc(cfg(all(feature = "crypto-dependencies", feature = "experimental"))) )] pub struct SeedStreamFixedKeyAes128 { cipher: Aes128, base_block: Block, length_consumed: u64, } #[cfg(all(feature = "crypto-dependencies", feature = "experimental"))] impl SeedStreamFixedKeyAes128 { fn hash_block(&self, block: &mut Block) { let sigma = Block::from([ // hi block[8], block[9], block[10], block[11], block[12], block[13], block[14], block[15], // xor(hi, lo) block[8] ^ block[0], block[9] ^ block[1], block[10] ^ block[2], block[11] ^ block[3], block[12] ^ block[4], block[13] ^ block[5], block[14] ^ block[6], block[15] ^ block[7], ]); self.cipher.encrypt_block_b2b(&sigma, block); for (b, s) in block.iter_mut().zip(sigma.iter()) { *b ^= s; } } fn fill(&mut self, buf: &mut [u8]) { let next_length_consumed = self.length_consumed + u64::try_from(buf.len()).unwrap(); let mut offset = usize::try_from(self.length_consumed % 16).unwrap(); let mut index = 0; let mut block = Block::from([0; 16]); // NOTE(cjpatton) We might be able to speed this up by unrolling this loop and encrypting // multiple blocks at the same time via `self.cipher.encrypt_blocks()`. for block_counter in self.length_consumed / 16..(next_length_consumed + 15) / 16 { block.clone_from(&self.base_block); for (b, i) in block.iter_mut().zip(block_counter.to_le_bytes().iter()) { *b ^= i; } self.hash_block(&mut block); let read = std::cmp::min(16 - offset, buf.len() - index); buf[index..index + read].copy_from_slice(&block[offset..offset + read]); offset = 0; index += read; } self.length_consumed = next_length_consumed; } } #[cfg(all(feature = "crypto-dependencies", feature = "experimental"))] impl RngCore for SeedStreamFixedKeyAes128 { fn fill_bytes(&mut self, dest: &mut [u8]) { self.fill(dest); } fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> { self.fill(dest); Ok(()) } fn next_u32(&mut self) -> u32 { next_u32_via_fill(self) } fn next_u64(&mut self) -> u64 { next_u64_via_fill(self) } } /// XOF based on HMAC-SHA256 and AES128. This XOF is not part of the VDAF spec. #[cfg(feature = "crypto-dependencies")] #[cfg_attr(docsrs, doc(cfg(feature = "crypto-dependencies")))] #[derive(Clone, Debug)] pub struct XofHmacSha256Aes128(Hmac); #[cfg(feature = "crypto-dependencies")] impl Xof<32> for XofHmacSha256Aes128 { type SeedStream = SeedStreamAes128; fn init(seed_bytes: &[u8; 32], dst: &[u8]) -> Self { let mut mac = as Mac>::new_from_slice(seed_bytes).unwrap(); Mac::update( &mut mac, &[dst.len().try_into().expect("dst must be at most 255 bytes")], ); Mac::update(&mut mac, dst); Self(mac) } fn update(&mut self, data: &[u8]) { Mac::update(&mut self.0, data); } fn into_seed_stream(self) -> SeedStreamAes128 { let tag = Mac::finalize(self.0).into_bytes(); let (key, iv) = tag.split_at(16); SeedStreamAes128::new(key, iv) } } #[cfg(test)] mod tests { use super::*; use crate::{field::Field128, vdaf::equality_comparison_test}; use serde::{Deserialize, Serialize}; use std::{convert::TryInto, io::Cursor}; #[derive(Deserialize, Serialize)] struct XofTestVector { #[serde(with = "hex")] seed: Vec, #[serde(with = "hex")] dst: Vec, #[serde(with = "hex")] binder: Vec, length: usize, #[serde(with = "hex")] derived_seed: Vec, #[serde(with = "hex")] expanded_vec_field128: Vec, } // Test correctness of dervied methods. fn test_xof() where P: Xof, { let seed = Seed::generate().unwrap(); let dst = b"algorithm and usage"; let binder = b"bind to artifact"; let mut xof = P::init(seed.as_ref(), dst); xof.update(binder); let mut want = Seed([0; SEED_SIZE]); xof.clone().into_seed_stream().fill_bytes(&mut want.0[..]); let got = xof.clone().into_seed(); assert_eq!(got, want); let mut want = [0; 45]; xof.clone().into_seed_stream().fill_bytes(&mut want); let mut got = [0; 45]; P::seed_stream(&seed, dst, binder).fill_bytes(&mut got); assert_eq!(got, want); } #[test] fn xof_turboshake128() { let t: XofTestVector = serde_json::from_str(include_str!("test_vec/08/XofTurboShake128.json")).unwrap(); let mut xof = XofTurboShake128::init(&t.seed.try_into().unwrap(), &t.dst); xof.update(&t.binder); assert_eq!( xof.clone().into_seed(), Seed(t.derived_seed.try_into().unwrap()) ); let mut bytes = Cursor::new(t.expanded_vec_field128.as_slice()); let mut want = Vec::with_capacity(t.length); while (bytes.position() as usize) < t.expanded_vec_field128.len() { want.push(Field128::decode(&mut bytes).unwrap()) } let got: Vec = xof.clone().into_seed_stream().into_field_vec(t.length); assert_eq!(got, want); test_xof::(); } #[test] fn xof_hmac_sha256_aes128() { let t: XofTestVector = serde_json::from_str(include_str!("test_vec/XofHmacSha256Aes128.json")).unwrap(); let mut xof = XofHmacSha256Aes128::init(&t.seed.try_into().unwrap(), &t.dst); xof.update(&t.binder); assert_eq!( xof.clone().into_seed(), Seed(t.derived_seed.try_into().unwrap()) ); let mut bytes = Cursor::new(t.expanded_vec_field128.as_slice()); let mut want = Vec::with_capacity(t.length); while (bytes.position() as usize) < t.expanded_vec_field128.len() { want.push(Field128::decode(&mut bytes).unwrap()) } let got: Vec = xof.clone().into_seed_stream().into_field_vec(t.length); assert_eq!(got, want); test_xof::(); } #[cfg(feature = "experimental")] #[test] fn xof_fixed_key_aes128() { let t: XofTestVector = serde_json::from_str(include_str!("test_vec/08/XofFixedKeyAes128.json")).unwrap(); let mut xof = XofFixedKeyAes128::init(&t.seed.try_into().unwrap(), &t.dst); xof.update(&t.binder); assert_eq!( xof.clone().into_seed(), Seed(t.derived_seed.try_into().unwrap()) ); let mut bytes = Cursor::new(t.expanded_vec_field128.as_slice()); let mut want = Vec::with_capacity(t.length); while (bytes.position() as usize) < t.expanded_vec_field128.len() { want.push(Field128::decode(&mut bytes).unwrap()) } let got: Vec = xof.clone().into_seed_stream().into_field_vec(t.length); assert_eq!(got, want); test_xof::(); } #[cfg(feature = "experimental")] #[test] fn xof_fixed_key_aes128_incomplete_block() { let seed = Seed::generate().unwrap(); let mut expected = [0; 32]; XofFixedKeyAes128::seed_stream(&seed, b"dst", b"binder").fill(&mut expected); for len in 0..=32 { let mut buf = vec![0; len]; XofFixedKeyAes128::seed_stream(&seed, b"dst", b"binder").fill(&mut buf); assert_eq!(buf, &expected[..len]); } } #[cfg(feature = "experimental")] #[test] fn xof_fixed_key_aes128_alternate_apis() { let dst = b"domain separation tag"; let binder = b"AAAAAAAAAAAAAAAAAAAAAAAA"; let seed_1 = Seed::generate().unwrap(); let seed_2 = Seed::generate().unwrap(); let mut stream_1_trait_api = XofFixedKeyAes128::seed_stream(&seed_1, dst, binder); let mut output_1_trait_api = [0u8; 32]; stream_1_trait_api.fill(&mut output_1_trait_api); let mut stream_2_trait_api = XofFixedKeyAes128::seed_stream(&seed_2, dst, binder); let mut output_2_trait_api = [0u8; 32]; stream_2_trait_api.fill(&mut output_2_trait_api); let fixed_key = XofFixedKeyAes128Key::new(dst, binder); let mut stream_1_alternate_api = fixed_key.with_seed(seed_1.as_ref()); let mut output_1_alternate_api = [0u8; 32]; stream_1_alternate_api.fill(&mut output_1_alternate_api); let mut stream_2_alternate_api = fixed_key.with_seed(seed_2.as_ref()); let mut output_2_alternate_api = [0u8; 32]; stream_2_alternate_api.fill(&mut output_2_alternate_api); assert_eq!(output_1_trait_api, output_1_alternate_api); assert_eq!(output_2_trait_api, output_2_alternate_api); } #[test] fn seed_equality_test() { equality_comparison_test(&[Seed([1, 2, 3]), Seed([3, 2, 1])]) } }