From 2a7c73bf60a5864a331a0d147839cfe3fa87dac9 Mon Sep 17 00:00:00 2001 From: apoorvko Date: Wed, 1 May 2024 14:28:25 -0700 Subject: [PATCH] feat[s2n-quic]: provider-fips-crypto feature flag --- .github/workflows/ci.yml | 19 +++ .../src/crypto/application/keyset.rs | 26 +-- .../src/crypto/application/limited.rs | 4 +- quic/s2n-quic-core/src/crypto/key.rs | 4 +- quic/s2n-quic-core/src/crypto/mod.rs | 2 +- quic/s2n-quic-core/src/crypto/tests.rs | 4 +- quic/s2n-quic-core/src/crypto/tls/null.rs | 2 +- quic/s2n-quic-core/src/crypto/tls/testing.rs | 20 +-- quic/s2n-quic-core/src/packet/encoding.rs | 2 +- quic/s2n-quic-core/src/packet/tests.rs | 8 +- quic/s2n-quic-crypto/Cargo.toml | 1 + .../src/{aead.rs => aead/default.rs} | 27 +--- quic/s2n-quic-crypto/src/aead/fips.rs | 95 +++++++++++ quic/s2n-quic-crypto/src/aead/mod.rs | 26 +++ quic/s2n-quic-crypto/src/cipher_suite.rs | 2 +- .../src/cipher_suite/negotiated.rs | 2 +- quic/s2n-quic-crypto/src/cipher_suite/ring.rs | 150 ++++++++++++------ quic/s2n-quic-crypto/src/initial.rs | 8 +- quic/s2n-quic-crypto/src/lib.rs | 3 + quic/s2n-quic-crypto/src/negotiated.rs | 4 +- quic/s2n-quic-crypto/src/one_rtt.rs | 2 +- quic/s2n-quic-crypto/src/retry.rs | 1 - quic/s2n-quic-crypto/src/zero_rtt.rs | 2 +- quic/s2n-quic-rustls/src/cipher_suite.rs | 6 +- quic/s2n-quic-tls-default/Cargo.toml | 16 ++ quic/s2n-quic-tls/Cargo.toml | 1 + .../s2n-quic-transport/src/space/handshake.rs | 4 +- quic/s2n-quic-transport/src/space/initial.rs | 4 +- quic/s2n-quic/Cargo.toml | 5 +- quic/s2n-quic/src/lib.rs | 31 ++++ 30 files changed, 354 insertions(+), 127 deletions(-) rename quic/s2n-quic-crypto/src/{aead.rs => aead/default.rs} (82%) create mode 100644 quic/s2n-quic-crypto/src/aead/fips.rs create mode 100644 quic/s2n-quic-crypto/src/aead/mod.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 66dcd5f8c7..fe211058ad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -269,6 +269,25 @@ jobs: run: | ${{ matrix.target != 'native' && 'cross' || 'cargo' }} test --workspace ${{ matrix.exclude }} ${{ matrix.target != 'native' && format('--target {0}', matrix.target) || '' }} ${{ matrix.args }} + fips: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - name: Install rust stable toolchain + id: stable-toolchain + run: | + rustup toolchain install stable + rustup override set stable + + - uses: camshaft/rust-cache@v1 + + - name: Run test + run: | + cargo test --features provider-tls-fips + miri: # miri needs quite a bit of memory so use a larger instance runs-on: diff --git a/quic/s2n-quic-core/src/crypto/application/keyset.rs b/quic/s2n-quic-core/src/crypto/application/keyset.rs index f79caf6127..f2aca0ee5e 100644 --- a/quic/s2n-quic-core/src/crypto/application/keyset.rs +++ b/quic/s2n-quic-core/src/crypto/application/keyset.rs @@ -142,7 +142,7 @@ impl KeySet { let key = &mut self.crypto[phase_to_use.into()]; - let result = packet.decrypt(key.key()); + let result = packet.decrypt(key.key_mut()); key.on_packet_decryption(&self.limits); @@ -229,7 +229,7 @@ impl KeySet { where F: FnOnce( EncoderBuffer<'a>, - &K, + &mut K, KeyPhase, ) -> Result<(ProtectedPayload<'a>, EncoderBuffer<'a>), PacketEncodingError<'a>>, @@ -249,7 +249,7 @@ impl KeySet { return Err(PacketEncodingError::AeadLimitReached(buffer)); } - let r = f(buffer, self.crypto[phase].key(), phase)?; + let r = f(buffer, self.crypto[phase].key_mut(), phase)?; //= https://www.rfc-editor.org/rfc/rfc9001#section-6.6 //# Endpoints MUST count the number of encrypted packets for each set of @@ -288,8 +288,8 @@ impl KeySet { self.packet_decryption_failures } - pub fn cipher_suite(&self) -> crate::crypto::tls::CipherSuite { - self.crypto.0[0].key().cipher_suite() + pub fn cipher_suite(&mut self) -> crate::crypto::tls::CipherSuite { + self.crypto.0[0].key_mut().cipher_suite() } } @@ -355,7 +355,7 @@ mod tests { //# An endpoint SHOULD //# retain old keys for some time after unprotecting a packet sent using //# the new keys. - assert_eq!(keyset.crypto[KeyPhase::Zero].key().derivations, 0); + assert_eq!(keyset.crypto[KeyPhase::Zero].key_mut().derivations, 0); clock.inc_by(Duration::from_millis(8)); keyset.on_timeout(clock.get_time()); @@ -364,7 +364,7 @@ mod tests { //= type=test //# After this period, old read keys and their corresponding secrets //# SHOULD be discarded. - assert_eq!(keyset.crypto[KeyPhase::Zero].key().derivations, 2); + assert_eq!(keyset.crypto[KeyPhase::Zero].key_mut().derivations, 2); } #[test] @@ -374,19 +374,19 @@ mod tests { //# For this reason, endpoints MUST be able to retain two sets of packet //# protection keys for receiving packets: the current and the next. - let keyset = KeySet::new(TestKey::default(), Default::default()); + let mut keyset = KeySet::new(TestKey::default(), Default::default()); - assert_eq!(keyset.crypto[KeyPhase::Zero].key().derivations, 0); - assert_eq!(keyset.crypto[KeyPhase::One].key().derivations, 1); + assert_eq!(keyset.crypto[KeyPhase::Zero].key_mut().derivations, 0); + assert_eq!(keyset.crypto[KeyPhase::One].key_mut().derivations, 1); } #[test] fn test_phase_rotation() { let mut keyset = KeySet::new(TestKey::default(), Default::default()); - assert_eq!(keyset.active_key().key().derivations, 0); + assert_eq!(keyset.active_key_mut().key_mut().derivations, 0); keyset.rotate_phase(); - assert_eq!(keyset.active_key().key().derivations, 1); + assert_eq!(keyset.active_key_mut().key_mut().derivations, 1); } #[test] @@ -396,7 +396,7 @@ mod tests { keyset.rotate_phase(); keyset.derive_and_store_next_key(); keyset.rotate_phase(); - assert_eq!(keyset.active_key().key().derivations, 2); + assert_eq!(keyset.active_key_mut().key_mut().derivations, 2); } //= https://www.rfc-editor.org/rfc/rfc9001#section-6.6 diff --git a/quic/s2n-quic-core/src/crypto/application/limited.rs b/quic/s2n-quic-core/src/crypto/application/limited.rs index 1a0cce5acf..097007ab94 100644 --- a/quic/s2n-quic-core/src/crypto/application/limited.rs +++ b/quic/s2n-quic-core/src/crypto/application/limited.rs @@ -102,7 +102,7 @@ impl Key { } #[inline] - pub fn key(&self) -> &K { - &self.key + pub fn key_mut(&mut self) -> &mut K { + &mut self.key } } diff --git a/quic/s2n-quic-core/src/crypto/key.rs b/quic/s2n-quic-core/src/crypto/key.rs index d0927da656..823b21bebd 100644 --- a/quic/s2n-quic-core/src/crypto/key.rs +++ b/quic/s2n-quic-core/src/crypto/key.rs @@ -16,7 +16,7 @@ pub trait Key: Send { /// Encrypt a payload fn encrypt( - &self, + &mut self, packet_number: u64, header: &[u8], payload: &mut scatter::Buffer, @@ -88,7 +88,7 @@ pub mod testing { /// Encrypt a payload fn encrypt( - &self, + &mut self, _packet_number: u64, _header: &[u8], payload: &mut scatter::Buffer, diff --git a/quic/s2n-quic-core/src/crypto/mod.rs b/quic/s2n-quic-core/src/crypto/mod.rs index 9656dd552b..8a3bdcfcac 100644 --- a/quic/s2n-quic-core/src/crypto/mod.rs +++ b/quic/s2n-quic-core/src/crypto/mod.rs @@ -206,7 +206,7 @@ pub fn unprotect<'a, K: HeaderKey>( /// Encrypts a cleartext payload with a crypto key into a `EncryptedPayload` #[inline] pub fn encrypt<'a, K: Key>( - key: &K, + key: &mut K, packet_number: PacketNumber, packet_number_len: PacketNumberLen, header_len: usize, diff --git a/quic/s2n-quic-core/src/crypto/tests.rs b/quic/s2n-quic-core/src/crypto/tests.rs index fa3c176cc0..b13b71ff50 100644 --- a/quic/s2n-quic-core/src/crypto/tests.rs +++ b/quic/s2n-quic-core/src/crypto/tests.rs @@ -86,7 +86,7 @@ fn fuzz_protect( let packet_number_len = truncated_packet_number.len(); let (payload, _remaining) = crate::crypto::encrypt( - &FuzzCrypto, + &mut FuzzCrypto, packet_number, packet_number_len, header_len, @@ -117,7 +117,7 @@ impl Key for FuzzCrypto { } fn encrypt<'a>( - &self, + &mut self, packet_number: u64, _header: &[u8], payload: &mut scatter::Buffer, diff --git a/quic/s2n-quic-core/src/crypto/tls/null.rs b/quic/s2n-quic-core/src/crypto/tls/null.rs index 42b6d23d69..e6b521e99c 100644 --- a/quic/s2n-quic-core/src/crypto/tls/null.rs +++ b/quic/s2n-quic-core/src/crypto/tls/null.rs @@ -310,7 +310,7 @@ mod key { #[inline(always)] fn encrypt( - &self, + &mut self, _packet_number: u64, _header: &[u8], payload: &mut scatter::Buffer, diff --git a/quic/s2n-quic-core/src/crypto/tls/testing.rs b/quic/s2n-quic-core/src/crypto/tls/testing.rs index f4878c44dd..91060f5a2a 100644 --- a/quic/s2n-quic-core/src/crypto/tls/testing.rs +++ b/quic/s2n-quic-core/src/crypto/tls/testing.rs @@ -281,8 +281,8 @@ impl Pair { } /// Finished the test - pub fn finish(&self) { - self.client.context.finish(&self.server.context); + pub fn finish(&mut self) { + self.client.context.finish(&mut self.server.context); assert_eq!( self.client.context.transport_parameters.as_ref().unwrap(), @@ -442,7 +442,7 @@ where } /// Finishes the test and asserts consistency - pub fn finish(&self, other: &Context) + pub fn finish(&mut self, other: &mut Context) where for<'a> OP: DecoderValue<'a>, { @@ -464,9 +464,9 @@ where "0-rtt keys are not consistent between endpoints" ); - self.initial.finish(&other.initial); - self.handshake.finish(&other.handshake); - self.application.finish(&other.application); + self.initial.finish(&mut other.initial); + self.handshake.finish(&mut other.handshake); + self.application.finish(&mut other.application); } fn assert_done(&self) { @@ -566,9 +566,9 @@ impl Space { } } - fn finish(&self, other: &Space) { - let (crypto_a, crypto_a_hk) = self.crypto.as_ref().expect("missing crypto"); - let (crypto_b, crypto_b_hk) = other.crypto.as_ref().expect("missing crypto"); + fn finish(&mut self, other: &mut Space) { + let (crypto_a, crypto_a_hk) = self.crypto.as_mut().expect("missing crypto"); + let (crypto_b, crypto_b_hk) = other.crypto.as_mut().expect("missing crypto"); // ensure payloads can be encrypted and decrypted in both directions seal_open(crypto_a, crypto_b); @@ -582,7 +582,7 @@ impl Space { } } -fn seal_open(sealer: &S, opener: &O) { +fn seal_open(sealer: &mut S, opener: &O) { let packet_number = 123; let header = &[1, 2, 3, 4, 5, 6]; diff --git a/quic/s2n-quic-core/src/packet/encoding.rs b/quic/s2n-quic-core/src/packet/encoding.rs index 906ba99014..992255da20 100644 --- a/quic/s2n-quic-core/src/packet/encoding.rs +++ b/quic/s2n-quic-core/src/packet/encoding.rs @@ -114,7 +114,7 @@ pub trait PacketEncoder( mut self, - key: &K, + key: &mut K, header_key: &H, largest_acknowledged_packet_number: PacketNumber, min_packet_len: Option, diff --git a/quic/s2n-quic-core/src/packet/tests.rs b/quic/s2n-quic-core/src/packet/tests.rs index 932cbd9d12..50ae4be4f9 100644 --- a/quic/s2n-quic-core/src/packet/tests.rs +++ b/quic/s2n-quic-core/src/packet/tests.rs @@ -125,14 +125,14 @@ fn encode_packet<'a>(packet: CleartextPacket, mut encoder: EncoderBuffer<'a>) -> use CleartextPacket::*; let result = match packet { Handshake(packet) => packet.encode_packet( - &testing::Key::new(), + &mut testing::Key::new(), &testing::HeaderKey::new(), PacketNumberSpace::Handshake.new_packet_number(Default::default()), None, encoder, ), Initial(packet) => packet.encode_packet( - &testing::Key::new(), + &mut testing::Key::new(), &testing::HeaderKey::new(), PacketNumberSpace::Initial.new_packet_number(Default::default()), None, @@ -143,14 +143,14 @@ fn encode_packet<'a>(packet: CleartextPacket, mut encoder: EncoderBuffer<'a>) -> return encoder; } Short(packet) => packet.encode_packet( - &testing::Key::new(), + &mut testing::Key::new(), &testing::HeaderKey::new(), PacketNumberSpace::ApplicationData.new_packet_number(Default::default()), None, encoder, ), ZeroRtt(packet) => packet.encode_packet( - &testing::Key::new(), + &mut testing::Key::new(), &testing::HeaderKey::new(), PacketNumberSpace::ApplicationData.new_packet_number(Default::default()), None, diff --git a/quic/s2n-quic-crypto/Cargo.toml b/quic/s2n-quic-crypto/Cargo.toml index 52b6a5e646..e5445e90a6 100644 --- a/quic/s2n-quic-crypto/Cargo.toml +++ b/quic/s2n-quic-crypto/Cargo.toml @@ -13,6 +13,7 @@ exclude = ["corpus.tar.gz"] [features] default = [] aws-lc-bindgen = ["aws-lc-rs/bindgen"] +fips = ["aws-lc-rs/fips"] testing = [] [dependencies] diff --git a/quic/s2n-quic-crypto/src/aead.rs b/quic/s2n-quic-crypto/src/aead/default.rs similarity index 82% rename from quic/s2n-quic-crypto/src/aead.rs rename to quic/s2n-quic-crypto/src/aead/default.rs index 67f5915c2e..070d73c0f8 100644 --- a/quic/s2n-quic-crypto/src/aead.rs +++ b/quic/s2n-quic-crypto/src/aead/default.rs @@ -1,24 +1,11 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -use crate::ring_aead::{Aad, LessSafeKey, Nonce, MAX_TAG_LEN, NONCE_LEN}; -pub use s2n_quic_core::crypto::{packet_protection::Error, scatter}; -pub type Result = core::result::Result; - -pub trait Aead { - type Nonce; - type Tag; - - fn encrypt(&self, nonce: &Self::Nonce, aad: &[u8], payload: &mut scatter::Buffer) -> Result; - - fn decrypt( - &self, - nonce: &Self::Nonce, - aad: &[u8], - payload: &mut [u8], - tag: &Self::Tag, - ) -> Result; -} +use crate::{ + aead::{Aead, Result}, + ring_aead::{Aad, LessSafeKey, Nonce, MAX_TAG_LEN, NONCE_LEN}, +}; +use s2n_quic_core::crypto::{packet_protection::Error, scatter}; impl Aead for LessSafeKey { type Nonce = [u8; NONCE_LEN]; @@ -27,7 +14,7 @@ impl Aead for LessSafeKey { #[inline] #[cfg(target_os = "windows")] fn encrypt( - &self, + &mut self, nonce: &[u8; NONCE_LEN], aad: &[u8], payload: &mut scatter::Buffer, @@ -55,7 +42,7 @@ impl Aead for LessSafeKey { #[inline] #[cfg(not(target_os = "windows"))] fn encrypt( - &self, + &mut self, nonce: &[u8; NONCE_LEN], aad: &[u8], payload: &mut scatter::Buffer, diff --git a/quic/s2n-quic-crypto/src/aead/fips.rs b/quic/s2n-quic-crypto/src/aead/fips.rs new file mode 100644 index 0000000000..c5733243fc --- /dev/null +++ b/quic/s2n-quic-crypto/src/aead/fips.rs @@ -0,0 +1,95 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + aead::{Aead, Result}, + ring_aead::{ + self, Aad, Nonce, TlsProtocolId, TlsRecordOpeningKey, TlsRecordSealingKey, MAX_TAG_LEN, + NONCE_LEN, + }, +}; +use s2n_quic_core::crypto::{packet_protection::Error, scatter}; + +/// Encryption keys backed by FIPS certified cryptography. +/// +/// FipsKey is backed by [`TlsRecordSealingKey`], which enforces that nonces used with `seal_*` +/// operations are unique. +pub struct FipsKey { + opener: TlsRecordOpeningKey, + sealer: TlsRecordSealingKey, +} + +impl FipsKey { + #[inline] + pub fn new(algorithm: &'static ring_aead::Algorithm, key_bytes: &[u8]) -> Result { + let opener = TlsRecordOpeningKey::new(algorithm, TlsProtocolId::TLS13, key_bytes) + .expect("key size verified"); + let sealer = TlsRecordSealingKey::new(algorithm, TlsProtocolId::TLS13, key_bytes) + .expect("key size verified"); + Ok(FipsKey { opener, sealer }) + } +} + +impl Aead for FipsKey { + type Nonce = [u8; NONCE_LEN]; + type Tag = [u8; MAX_TAG_LEN]; + + #[inline] + fn encrypt( + &mut self, + nonce: &[u8; NONCE_LEN], + aad: &[u8], + payload: &mut scatter::Buffer, + ) -> Result { + use s2n_codec::Encoder; + + let nonce = Nonce::assume_unique_for_key(*nonce); + let aad = Aad::from(aad); + + let buffer = payload.flatten(); + + let tag = { + let (input, _) = buffer.split_mut(); + + self.sealer + .seal_in_place_separate_tag(nonce, aad, input) + .map_err(|_| Error::INTERNAL_ERROR)? + }; + + buffer.write_slice(tag.as_ref()); + + Ok(()) + } + + #[inline] + fn decrypt( + &self, + nonce: &[u8; NONCE_LEN], + aad: &[u8], + input: &mut [u8], + tag: &[u8; MAX_TAG_LEN], + ) -> Result { + let nonce = Nonce::assume_unique_for_key(*nonce); + let aad = Aad::from(aad); + let input = unsafe { + // ring requires that the input and tag be passed as a single slice + // so we extend the input slice here. + // This is only safe if they are contiguous + debug_assert_eq!( + if input.is_empty() { + (*input).as_ptr() + } else { + (&input[input.len() - 1] as *const u8).add(1) + }, + (*tag).as_ptr() + ); + let ptr = input.as_mut_ptr(); + let len = input.len() + MAX_TAG_LEN; + core::slice::from_raw_parts_mut(ptr, len) + }; + self.opener + .open_in_place(nonce, aad, input) + .map_err(|_| Error::DECRYPT_ERROR)?; + Ok(()) + } +} diff --git a/quic/s2n-quic-crypto/src/aead/mod.rs b/quic/s2n-quic-crypto/src/aead/mod.rs new file mode 100644 index 0000000000..35b08b6615 --- /dev/null +++ b/quic/s2n-quic-crypto/src/aead/mod.rs @@ -0,0 +1,26 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +use s2n_quic_core::crypto::{packet_protection::Error, scatter}; + +mod default; +#[cfg(feature = "fips")] +pub mod fips; + +pub type Result = core::result::Result; + +pub trait Aead { + type Nonce; + type Tag; + + fn encrypt(&mut self, nonce: &Self::Nonce, aad: &[u8], payload: &mut scatter::Buffer) + -> Result; + + fn decrypt( + &self, + nonce: &Self::Nonce, + aad: &[u8], + payload: &mut [u8], + tag: &Self::Tag, + ) -> Result; +} diff --git a/quic/s2n-quic-crypto/src/cipher_suite.rs b/quic/s2n-quic-crypto/src/cipher_suite.rs index d2aac84a7b..360ea6f33f 100644 --- a/quic/s2n-quic-crypto/src/cipher_suite.rs +++ b/quic/s2n-quic-crypto/src/cipher_suite.rs @@ -150,7 +150,7 @@ macro_rules! impl_cipher_suite { #[inline] fn encrypt( - &self, + &mut self, packet_number: u64, header: &[u8], payload: &mut scatter::Buffer, diff --git a/quic/s2n-quic-crypto/src/cipher_suite/negotiated.rs b/quic/s2n-quic-crypto/src/cipher_suite/negotiated.rs index 80038ba40a..796a1fa982 100644 --- a/quic/s2n-quic-crypto/src/cipher_suite/negotiated.rs +++ b/quic/s2n-quic-crypto/src/cipher_suite/negotiated.rs @@ -95,7 +95,7 @@ impl crypto::Key for NegotiatedCipherSuite { #[inline] fn encrypt( - &self, + &mut self, packet_number: u64, header: &[u8], payload: &mut scatter::Buffer, diff --git a/quic/s2n-quic-crypto/src/cipher_suite/ring.rs b/quic/s2n-quic-crypto/src/cipher_suite/ring.rs index f563a2bddc..9e37b04947 100644 --- a/quic/s2n-quic-crypto/src/cipher_suite/ring.rs +++ b/quic/s2n-quic-crypto/src/cipher_suite/ring.rs @@ -1,84 +1,130 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -macro_rules! key { +macro_rules! fips_supported_key { ($name:ident, $ring_cipher:path, $key_size:expr, $tag_len:expr) => { pub mod $name { use super::super::$name::{KEY_LEN, NONCE_LEN, TAG_LEN}; - use crate::ring_aead::{self as aead, LessSafeKey, UnboundKey}; + use crate::ring_aead::{self as aead}; use s2n_quic_core::crypto::scatter; use zeroize::Zeroize; pub struct Key { - key: LessSafeKey, + #[cfg(feature = "fips")] + key: crate::aead::fips::FipsKey, + #[cfg(not(feature = "fips"))] + key: aead::LessSafeKey, } impl Key { - #[inline] + #[cfg(feature = "fips")] pub fn new(secret: &[u8; KEY_LEN]) -> Self { - let unbound_key = - UnboundKey::new(&$ring_cipher, secret).expect("key size verified"); - let key = LessSafeKey::new(unbound_key); + let key = crate::aead::fips::FipsKey::new(&$ring_cipher, secret) + .expect("key successfully created"); Self { key } } - #[inline] - #[allow(dead_code)] // this is to maintain compatibility between implementations - pub fn should_update_pmtu(&self, _mtu: u16) -> bool { - // ring doesn't implement precomputed tables - false + #[cfg(not(feature = "fips"))] + pub fn new(secret: &[u8; KEY_LEN]) -> Self { + let unbound_key = + aead::UnboundKey::new(&$ring_cipher, secret).expect("key size verified"); + let key = aead::LessSafeKey::new(unbound_key); + Self { key } } + } - #[inline] - #[allow(dead_code)] // this is to maintain compatibility between implementations - pub fn update(&self, secret: &[u8; KEY_LEN]) -> Self { - // no state to persist - Self::new(secret) - } + key_impl!($name, $ring_cipher, $key_size, $tag_len); + } + }; +} - #[inline] - #[allow(dead_code)] // this is to maintain compatibility between implementations - pub fn update_pmtu(&mut self, _secret: &[u8; KEY_LEN], _mtu: u16) { - unimplemented!(); - } +macro_rules! key { + ($name:ident, $ring_cipher:path, $key_size:expr, $tag_len:expr) => { + pub mod $name { + use super::super::$name::{KEY_LEN, NONCE_LEN, TAG_LEN}; + use crate::ring_aead::{self as aead}; + use s2n_quic_core::crypto::scatter; + use zeroize::Zeroize; + + pub struct Key { + key: aead::LessSafeKey, } - impl Zeroize for Key { - fn zeroize(&mut self) { - // ring doesn't provide a way to zeroize keys currently - // https://github.com/briansmith/ring/issues/15 + impl Key { + pub fn new(secret: &[u8; KEY_LEN]) -> Self { + let unbound_key = + aead::UnboundKey::new(&$ring_cipher, secret).expect("key size verified"); + let key = aead::LessSafeKey::new(unbound_key); + Self { key } } } - impl crate::aead::Aead for Key { - type Nonce = [u8; NONCE_LEN]; - type Tag = [u8; TAG_LEN]; - - #[inline] - fn encrypt( - &self, - nonce: &[u8; NONCE_LEN], - aad: &[u8], - payload: &mut scatter::Buffer, - ) -> crate::aead::Result { - self.key.encrypt(nonce, aad, payload) - } + key_impl!($name, $ring_cipher, $key_size, $tag_len); + } + }; +} - #[inline] - fn decrypt( - &self, - nonce: &[u8; NONCE_LEN], - aad: &[u8], - input: &mut [u8], - tag: &[u8; TAG_LEN], - ) -> crate::aead::Result { - self.key.decrypt(nonce, aad, input, tag) - } +macro_rules! key_impl { + ($name:ident, $ring_cipher:path, $key_size:expr, $tag_len:expr) => { + impl Key { + #[inline] + #[allow(dead_code)] // this is to maintain compatibility between implementations + pub fn should_update_pmtu(&self, _mtu: u16) -> bool { + // ring doesn't implement precomputed tables + false + } + + #[inline] + #[allow(dead_code)] // this is to maintain compatibility between implementations + pub fn update(&self, secret: &[u8; KEY_LEN]) -> Self { + // no state to persist + Self::new(secret) + } + + #[inline] + #[allow(dead_code)] // this is to maintain compatibility between implementations + pub fn update_pmtu(&mut self, _secret: &[u8; KEY_LEN], _mtu: u16) { + unimplemented!(); + } + } + + impl Zeroize for Key { + fn zeroize(&mut self) { + // ring doesn't provide a way to zeroize keys currently + // https://github.com/briansmith/ring/issues/15 + } + } + + impl crate::aead::Aead for Key { + type Nonce = [u8; NONCE_LEN]; + type Tag = [u8; TAG_LEN]; + + #[inline] + fn encrypt( + &mut self, + nonce: &[u8; NONCE_LEN], + aad: &[u8], + payload: &mut scatter::Buffer, + ) -> crate::aead::Result { + self.key.encrypt(nonce, aad, payload) + } + + #[inline] + fn decrypt( + &self, + nonce: &[u8; NONCE_LEN], + aad: &[u8], + input: &mut [u8], + tag: &[u8; TAG_LEN], + ) -> crate::aead::Result { + self.key.decrypt(nonce, aad, input, tag) } } }; } -key!(aes128_gcm, aead::AES_128_GCM, 128 / 8, 16); -key!(aes256_gcm, aead::AES_256_GCM, 256 / 8, 16); +fips_supported_key!(aes128_gcm, aead::AES_128_GCM, 128 / 8, 16); +fips_supported_key!(aes256_gcm, aead::AES_256_GCM, 256 / 8, 16); +// Don't create a FipsKey for CHACHA20_POLY1305 since TlsRecordSealingKey and +// TlsRecordOpeningKey don't support CHACHA20_POLY1305 key!(chacha20_poly1305, aead::CHACHA20_POLY1305, 256 / 8, 16); diff --git a/quic/s2n-quic-crypto/src/initial.rs b/quic/s2n-quic-crypto/src/initial.rs index a2cec38928..dd5cd7b80e 100644 --- a/quic/s2n-quic-crypto/src/initial.rs +++ b/quic/s2n-quic-crypto/src/initial.rs @@ -92,7 +92,7 @@ impl Key for InitialKey { #[inline] fn encrypt( - &self, + &mut self, packet_number: u64, header: &[u8], payload: &mut scatter::Buffer, @@ -142,7 +142,7 @@ mod tests { #[test] fn rfc_example_server_test() { test_round_trip( - &InitialKey::new_client(&EXAMPLE_DCID), + &mut InitialKey::new_client(&EXAMPLE_DCID), &InitialKey::new_server(&EXAMPLE_DCID), &EXAMPLE_CLIENT_INITIAL_PROTECTED_PACKET, &EXAMPLE_CLIENT_INITIAL_PAYLOAD, @@ -152,7 +152,7 @@ mod tests { #[test] fn rfc_example_client_test() { test_round_trip( - &InitialKey::new_server(&EXAMPLE_DCID), + &mut InitialKey::new_server(&EXAMPLE_DCID), &InitialKey::new_client(&EXAMPLE_DCID), &EXAMPLE_SERVER_INITIAL_PROTECTED_PACKET, &EXAMPLE_SERVER_INITIAL_PAYLOAD, @@ -160,7 +160,7 @@ mod tests { } fn test_round_trip( - sealer: &(InitialKey, InitialHeaderKey), + sealer: &mut (InitialKey, InitialHeaderKey), opener: &(InitialKey, InitialHeaderKey), protected_packet: &[u8], cleartext_payload: &[u8], diff --git a/quic/s2n-quic-crypto/src/lib.rs b/quic/s2n-quic-crypto/src/lib.rs index f0ce340884..d9b78604f6 100644 --- a/quic/s2n-quic-crypto/src/lib.rs +++ b/quic/s2n-quic-crypto/src/lib.rs @@ -1,6 +1,9 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 +#[cfg(all(feature = "fips", target_os = "windows"))] +std::compile_error!("feature `fips` is not supported on windows"); + #[macro_use] mod negotiated; #[macro_use] diff --git a/quic/s2n-quic-crypto/src/negotiated.rs b/quic/s2n-quic-crypto/src/negotiated.rs index ff4bbb1d8c..637e0b998b 100644 --- a/quic/s2n-quic-crypto/src/negotiated.rs +++ b/quic/s2n-quic-crypto/src/negotiated.rs @@ -63,7 +63,7 @@ impl Key for KeyPair { #[inline] fn encrypt( - &self, + &mut self, packet_number: u64, header: &[u8], payload: &mut scatter::Buffer, @@ -150,7 +150,7 @@ macro_rules! negotiated_crypto { #[inline] fn encrypt( - &self, + &mut self, packet_number: u64, header: &[u8], payload: &mut s2n_quic_core::crypto::scatter::Buffer, diff --git a/quic/s2n-quic-crypto/src/one_rtt.rs b/quic/s2n-quic-crypto/src/one_rtt.rs index 9662b1d173..000e48d2df 100644 --- a/quic/s2n-quic-crypto/src/one_rtt.rs +++ b/quic/s2n-quic-crypto/src/one_rtt.rs @@ -95,7 +95,7 @@ mod tests { ]; for (secret, ku_secret, should_match) in tests { - let (next_cipher, expected_next_cipher) = generate_ciphers(secret, ku_secret); + let (mut next_cipher, mut expected_next_cipher) = generate_ciphers(secret, ku_secret); // Encrypt two empty blocks to verify the ciphers are the same let mut next_cipher_output = [0; 32]; diff --git a/quic/s2n-quic-crypto/src/retry.rs b/quic/s2n-quic-crypto/src/retry.rs index 50f077664e..cfff5774f7 100644 --- a/quic/s2n-quic-crypto/src/retry.rs +++ b/quic/s2n-quic-crypto/src/retry.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{constant_time, ring_aead as aead}; -use core::convert::TryInto; use s2n_quic_core::crypto::{ self, packet_protection, retry::{IntegrityTag, NONCE_BYTES, SECRET_KEY_BYTES}, diff --git a/quic/s2n-quic-crypto/src/zero_rtt.rs b/quic/s2n-quic-crypto/src/zero_rtt.rs index 3004ee8ab7..ad68cdd4d5 100644 --- a/quic/s2n-quic-crypto/src/zero_rtt.rs +++ b/quic/s2n-quic-crypto/src/zero_rtt.rs @@ -30,7 +30,7 @@ impl Key for ZeroRttKey { } fn encrypt( - &self, + &mut self, packet_number: u64, header: &[u8], payload: &mut scatter::Buffer, diff --git a/quic/s2n-quic-rustls/src/cipher_suite.rs b/quic/s2n-quic-rustls/src/cipher_suite.rs index 84b2512d86..9af342ac54 100644 --- a/quic/s2n-quic-rustls/src/cipher_suite.rs +++ b/quic/s2n-quic-rustls/src/cipher_suite.rs @@ -41,7 +41,7 @@ impl crypto::Key for PacketKey { #[inline] fn encrypt( - &self, + &mut self, packet_number: u64, header: &[u8], payload: &mut scatter::Buffer, @@ -128,7 +128,7 @@ impl crypto::Key for PacketKeys { #[inline] fn encrypt( - &self, + &mut self, packet_number: u64, header: &[u8], payload: &mut scatter::Buffer, @@ -268,7 +268,7 @@ impl crypto::Key for OneRttKey { #[inline] fn encrypt( - &self, + &mut self, packet_number: u64, header: &[u8], payload: &mut scatter::Buffer, diff --git a/quic/s2n-quic-tls-default/Cargo.toml b/quic/s2n-quic-tls-default/Cargo.toml index e18e9879f5..fa96ca33a6 100644 --- a/quic/s2n-quic-tls-default/Cargo.toml +++ b/quic/s2n-quic-tls-default/Cargo.toml @@ -10,6 +10,22 @@ license = "Apache-2.0" # Exclude corpus files when publishing to crates.io exclude = ["corpus.tar.gz"] +[features] +# The [`?`](https://doc.rust-lang.org/cargo/reference/features.html?highlight=addative#dependency-features) +# syntax only enable `fips` for `s2n-quic-tls` if something else enables `s2n-quic-tls`. This +# preserves the selective compilation of the two tls crates. +fips = ["s2n-quic-tls?/fips"] + +# Declare `s2n-quic-tls` as an optional dependency since the `?` syntax for features requires +# the dependency be optional. +# +# It is not possible to enable a feature flag based on target since Cargo currently doesn't +# support platform specific feature flags: https://github.com/rust-lang/cargo/issues/1197. In +# order to support the `?` syntax, we declare s2n-quic-tls as an optional dependency. +# `s2n-quic-tls` only gets enabled based on the target. +[dependencies] +s2n-quic-tls = { path = "../s2n-quic-tls", optional = true } + [target.'cfg(unix)'.dependencies] s2n-quic-tls = { version = "=0.37.0", path = "../s2n-quic-tls" } diff --git a/quic/s2n-quic-tls/Cargo.toml b/quic/s2n-quic-tls/Cargo.toml index a223318f4a..981e54f2f9 100644 --- a/quic/s2n-quic-tls/Cargo.toml +++ b/quic/s2n-quic-tls/Cargo.toml @@ -11,6 +11,7 @@ license = "Apache-2.0" exclude = ["corpus.tar.gz"] [features] +fips = ["s2n-quic-crypto/fips"] unstable_client_hello = [] unstable_private_key = [] diff --git a/quic/s2n-quic-transport/src/space/handshake.rs b/quic/s2n-quic-transport/src/space/handshake.rs index aa2a22462d..d41bb69fce 100644 --- a/quic/s2n-quic-transport/src/space/handshake.rs +++ b/quic/s2n-quic-transport/src/space/handshake.rs @@ -157,7 +157,7 @@ impl HandshakeSpace { }; let (_protected_packet, buffer) = packet.encode_packet( - &self.key, + &mut self.key, &self.header_key, packet_number_encoder, context.min_packet_len, @@ -243,7 +243,7 @@ impl HandshakeSpace { }; let (_protected_packet, buffer) = packet.encode_packet( - &self.key, + &mut self.key, &self.header_key, packet_number_encoder, context.min_packet_len, diff --git a/quic/s2n-quic-transport/src/space/initial.rs b/quic/s2n-quic-transport/src/space/initial.rs index 69156a97b1..bae1bfad47 100644 --- a/quic/s2n-quic-transport/src/space/initial.rs +++ b/quic/s2n-quic-transport/src/space/initial.rs @@ -206,7 +206,7 @@ impl InitialSpace { }; let (_protected_packet, buffer) = packet.encode_packet( - &self.key, + &mut self.key, &self.header_key, packet_number_encoder, context.min_packet_len, @@ -293,7 +293,7 @@ impl InitialSpace { }; let (_protected_packet, buffer) = packet.encode_packet( - &self.key, + &mut self.key, &self.header_key, packet_number_encoder, context.min_packet_len, diff --git a/quic/s2n-quic/Cargo.toml b/quic/s2n-quic/Cargo.toml index 4194c280ab..c1db00effb 100644 --- a/quic/s2n-quic/Cargo.toml +++ b/quic/s2n-quic/Cargo.toml @@ -15,7 +15,10 @@ default = [ "provider-address-token-default", "provider-tls-default", ] - +provider-tls-fips = [ + "s2n-quic-tls-default?/fips", + "s2n-quic-tls?/fips", +] provider-address-token-default = [ "cuckoofilter", "hash_hasher", diff --git a/quic/s2n-quic/src/lib.rs b/quic/s2n-quic/src/lib.rs index b2fd037594..ccf4658131 100644 --- a/quic/s2n-quic/src/lib.rs +++ b/quic/s2n-quic/src/lib.rs @@ -50,6 +50,37 @@ //! //! **NOTE**: this will override the platform detection and always use [`s2n-tls`][s2n-tls] by default. //! +//! ### `provider-tls-fips` +//! +//! FIPS mode is currently only supported with the [`s2n-tls`][s2n-tls] TLS provider. +//! Applications wanting to use FIPS-approved cryptography with s2n-quic should: +//! +//! 1. Use enable the following features: +//! +//! ``` +//! s2n-quic = { version = "1", features = ["provider-tls-fips", "provider-tls-s2n"] } +//! ``` +//! +//! 2. Build a custom s2n-tls TLS provider configured with a FIPS approved +//! [security policy](https://aws.github.io/s2n-tls/usage-guide/ch06-security-policies.html): +//! +//! ```no_run +//! use s2n_quic::provider::tls::s2n_tls; +//! +//! let mut tls = s2n_tls::Server::builder(); +//! let policy = s2n_tls::security::Policy::from_version("select_a_fips_security_policy")?; +//! tls.config_mut().set_security_policy(&policy)?; +//! tls +//! .with_certificate(...)? +//! ... +//! .build()?; +//! +//! let mut server = Server::builder() +//! .with_tls(tls)? +//! ... +//! .start()?; +//! ``` +//! //! [s2n-tls]: https://github.com/aws/s2n-tls //! [rustls]: https://github.com/rustls/rustls