diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 29ccbbd..541e743 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,9 +9,10 @@ on: env: CARGO_TERM_COLOR: always RUSTFLAGS: -Dwarnings + FEATURES: "logging,tls12" jobs: - fmt: + build: runs-on: ubuntu-latest steps: @@ -20,95 +21,9 @@ jobs: run: sudo apt-get install -y cmake clang - name: Check fmt run: make fmt - - test-default: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - name: Install dependencies - run: sudo apt-get install -y cmake clang - - name: Lint (default features) - run: make lint FEATURES="" - - name: Test (default features) - run: make test FEATURES="" - - name: Build (default features) - run: make build FEATURES="" - - test-tls12: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - name: Install dependencies - run: sudo apt-get install -y cmake clang - - name: Lint (tls12) - run: make lint FEATURES="tls12" - - name: Test (tls12) - run: make test FEATURES="tls12" - - name: Build (tls12) - run: make build FEATURES="tls12" - - test-logging-tls12: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - name: Install dependencies - run: sudo apt-get install -y cmake clang - - name: Lint (logging,tls12) - run: make lint FEATURES="logging,tls12" - - name: Test (logging,tls12) - run: make test FEATURES="logging,tls12" - - name: Build (logging,tls12) - run: make build FEATURES="logging,tls12" - - test-mlkem: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - name: Install dependencies - run: sudo apt-get install -y cmake clang - - name: Lint (mlkem) - run: make lint FEATURES="mlkem" - - name: Test (mlkem) - run: make test FEATURES="mlkem" - - name: Build (mlkem) - run: make build FEATURES="mlkem" - - test-mlkem-tls12: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - name: Install dependencies - run: sudo apt-get install -y cmake clang - - name: Lint (mlkem,tls12) - run: make lint FEATURES="mlkem,tls12" - - name: Test (mlkem,tls12) - run: make test FEATURES="mlkem,tls12" - - name: Build (mlkem,tls12) - run: make build FEATURES="mlkem,tls12" - - test-fips: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - name: Install dependencies - run: sudo apt-get install -y cmake clang - - name: Test (fips) - run: cargo test -p boring-rustls-provider --all-targets --features fips - - check-fips: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - name: Install dependencies - run: sudo apt-get install -y cmake clang - - name: Check (fips) - run: cargo check -p boring-rustls-provider --all-targets --features fips - - name: Check (fips-precompiled) - run: cargo check -p boring-rustls-provider --all-targets --features fips-precompiled + - name: Lint + run: make lint + - name: Tests usual + run: make test + - name: Build usual + run: make build diff --git a/Cargo.toml b/Cargo.toml index 62f9a8e..0fd565e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,10 +2,10 @@ members = [ # things that should probably be in boring crate "boring-additions", - # the main library and tests - "boring-rustls-provider", # things that should probably be in boring-sys crate "boring-sys-additions", + # the main library and tests + "boring-rustls-provider", # tests and example code "examples", ] @@ -17,11 +17,11 @@ default-members = [ resolver = "2" [workspace.dependencies] -boring = { version = "5", default-features = false } -boring-sys = { version = "5", default-features = false } -rustls = { version = "0.23", default-features = false } -rustls-pemfile = { version = "2" } -rustls-pki-types = { version = "1" } -tokio-rustls = { version = "0.26", default-features = false } -webpki = { package = "rustls-webpki", version = "0.103", default-features = false } -webpki-roots = { version = "1.0" } +boring = { version = "4", default-features = false } +boring-sys = { version = "4", default-features = false } +rustls = { version = "=0.22.0-alpha.6", default-features = false } +rustls-pemfile = { version = "=2.0.0-alpha.2" } +rustls-pki-types = { version = "0.2.3" } +tokio-rustls = { version = "0.25.0-alpha.4" } +webpki = { package = "rustls-webpki", version = "0.102.0-alpha.7", default-features = false } +webpki-roots = { version = "=0.26.0-alpha.2" } diff --git a/Makefile b/Makefile index cfa515a..5189d1e 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,4 @@ FEATURES ?= logging,tls12 -CARGO_FEATURES := $(if $(strip $(FEATURES)),-F "$(FEATURES)",) .PHONY: fmt @@ -8,16 +7,12 @@ fmt: .PHONY: lint lint: - cargo clippy --workspace --all-targets $(CARGO_FEATURES) - -.PHONY: check -check: - cargo check --workspace --all-targets $(CARGO_FEATURES) + cargo clippy --workspace --all-targets -F "$(FEATURES)" .PHONY: test test: - cargo test --all-targets $(CARGO_FEATURES) + cargo test --all-targets -F "$(FEATURES)" .PHONY: build build: - cargo build --all-targets $(CARGO_FEATURES) + cargo build --all-targets -F "$(FEATURES)" \ No newline at end of file diff --git a/Readme.md b/Readme.md index 578f0b6..f44e007 100644 --- a/Readme.md +++ b/Readme.md @@ -2,59 +2,55 @@ [![Build Status](https://github.com/janrueth/boring-rustls-provider/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/janrueth/boring-rustls-provider/actions/workflows/ci.yml?query=branch%3Amain) -A [BoringSSL](https://github.com/cloudflare/boring)-based [rustls](https://github.com/rustls/rustls) crypto provider. +This is supposed to be the start to a [boringssl](https://github.com/cloudflare/boring)-based [rustls](https://github.com/rustls/rustls) crypto provider. -Built on `boring` v5 and `rustls` 0.23. +## Status +This is just a dump of me figuring out how to interface with boring and rustls. +It works to establish a connection and exchange data but I haven't written real tests yet, nor did I cleanup the code or made the effort to make it look nice. +There is probably some code in here that should rather live in the `boring` crate. -## Features +Further, the rustls crypto provider API is still not stable it seems. This works currently with `rustls = 0.22.0-alpha.5`. -No features are enabled by default. The provider ships with TLS 1.3 support -out of the box; additional capabilities are opt-in. - -| Feature | Description | -|---|---| -| `fips` | Build against FIPS-validated BoringSSL and restrict the provider to FIPS-approved algorithms only (SP 800-52r2). Implies `mlkem`. See [FIPS mode](#fips-mode) below. | -| `fips-precompiled` | Deprecated alias for `fips`. Matches the `boring` crate's feature name. | -| `mlkem` | Enable the X25519MLKEM768 post-quantum hybrid key exchange group (`draft-ietf-tls-ecdhe-mlkem-00`). Uses ML-KEM-768 (FIPS 203) combined with X25519. See [Post-quantum key exchange](#post-quantum-key-exchange). | -| `tls12` | Enable TLS 1.2 cipher suites (`ECDHE-ECDSA` and `ECDHE-RSA` with AES-GCM and ChaCha20-Poly1305). Without this only TLS 1.3 is available. | -| `logging` | Enable debug logging of BoringSSL errors and provider internals via the `log` crate. | - -## Supported Algorithms - -### Cipher Suites - -TLS 1.3 (always available): +### Supported ciphers +Currently, supports only TLS 1.3: ``` AES_128_GCM_SHA256 AES_256_GCM_SHA384 CHACHA20_POLY1305_SHA256 ``` -TLS 1.2 (requires `tls12` feature): +QUIC: not yet supported + +TLS 1.2: ``` ECDHE_ECDSA_AES128_GCM_SHA256 ECDHE_RSA_AES128_GCM_SHA256 + ECDHE_ECDSA_AES256_GCM_SHA384 ECDHE_RSA_AES256_GCM_SHA384 + ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 ``` -### Key Exchange Groups - -Matches boring's default supported group list: - +### Key Exchange Algorithms + +`ECDHE` with curves: ``` -X25519MLKEM768 (0x11ec, requires mlkem feature, TLS 1.3 only) X25519 -secp256r1 (P-256) -secp384r1 (P-384) +X448 +secp256r1 +secp384r1 +secp521r1 ``` -When `mlkem` is enabled, X25519MLKEM768 is the preferred (first) group in both -FIPS and non-FIPS configurations. -### Signature Algorithms +`FFDHE` with: +``` +ffdhe2048 +``` + +### Signature Generation / Verification ``` RSA_PKCS1_SHA256 @@ -70,47 +66,6 @@ ED25519 ED448 ``` -## Post-Quantum Key Exchange - -The `mlkem` feature enables the **X25519MLKEM768** hybrid key exchange group -per `draft-ietf-tls-ecdhe-mlkem-00`. This combines classical X25519 -Diffie-Hellman with ML-KEM-768 (FIPS 203) post-quantum key encapsulation, -ensuring that connections are secure against both classical and quantum -adversaries. - -The `fips` feature implies `mlkem`, so X25519MLKEM768 is always available -in FIPS mode. - -Wire format (ML-KEM component first in all encodings): -- Client key share: `mlkem_pk(1184) || x25519_pk(32)` = 1216 bytes -- Server key share: `mlkem_ct(1088) || x25519_pk(32)` = 1120 bytes -- Shared secret: `mlkem_ss(32) || x25519_ss(32)` = 64 bytes - -Interoperability has been verified against Cloudflare's PQ endpoints -(`pq.cloudflareresearch.com`). - -## FIPS Mode - -When the `fips` feature is enabled the provider builds against a FIPS-validated -version of BoringSSL and restricts all algorithm selections to those approved -under [SP 800-52r2](https://doi.org/10.6028/NIST.SP.800-52r2), aligned with -boring's `fips202205` compliance policy: - -- **Cipher suites**: AES-GCM only (no ChaCha20-Poly1305). -- **Key exchange groups**: X25519MLKEM768 (preferred), P-256, and P-384 only - (no standalone X25519). -- **Signature algorithms**: RSA PKCS#1 / PSS and ECDSA with P-256 or P-384 only - (no P-521, Ed25519, or Ed448). - -## Workspace Structure - -| Crate | Purpose | -|---|---| -| `boring-rustls-provider` | The main rustls crypto provider. | -| `boring-additions` | Safe Rust wrappers for BoringSSL APIs not yet exposed by the `boring` crate (AEAD, EVP_PKEY_CTX, HMAC_CTX). Intended for upstreaming. | -| `boring-sys-additions` | Raw FFI binding for `CRYPTO_tls1_prf` (internal BoringSSL symbol used for FIPS-compliant TLS 1.2 PRF). Intended for upstreaming. | -| `examples` | Example client binary. | ## License - MIT diff --git a/boring-additions/Cargo.toml b/boring-additions/Cargo.toml index 388984f..ef0d341 100644 --- a/boring-additions/Cargo.toml +++ b/boring-additions/Cargo.toml @@ -8,6 +8,7 @@ description = "Boring additions" publish = false [dependencies] +aead = { version = "0.5", default_features = false, features = ["alloc"] } boring = { workspace = true } boring-sys = { workspace = true } foreign-types = "0.5" diff --git a/boring-additions/src/aead/mod.rs b/boring-additions/src/aead/mod.rs index 2803393..353d440 100644 --- a/boring-additions/src/aead/mod.rs +++ b/boring-additions/src/aead/mod.rs @@ -78,7 +78,11 @@ impl Crypter { /// /// # Errors /// Returns the `BoringSSL` error in case of an internal error + /// + /// # Panics + /// * If the key length mismatches the `aead_alg` required key length pub fn new(aead_alg: &Algorithm, key: &[u8]) -> Result { + assert_eq!(aead_alg.key_length(), key.len()); boring_sys::init(); let this = unsafe { @@ -112,6 +116,10 @@ impl Crypter { /// /// # Errors /// In case of an error, returns the `BoringSSL` error + /// + /// # Panics + /// * If the `nonce` is not the expected lenght + /// * If the `tag` has not enough space pub fn seal_in_place( &self, nonce: &[u8], @@ -119,9 +127,8 @@ impl Crypter { buffer: &mut [u8], tag: &mut [u8], ) -> Result { - if tag.len() < self.max_overhead || nonce.len() != self.nonce_len { - return Err(ErrorStack::get()); - } + assert!(tag.len() >= self.max_overhead); + assert_eq!(nonce.len(), self.nonce_len); let mut tag_len = tag.len(); unsafe { @@ -149,6 +156,9 @@ impl Crypter { /// /// # Errors /// In case of an error, returns the `BoringSSL` error + /// + /// # Panics + /// * if the nonce has the wrong lenght pub fn open_in_place( &self, nonce: &[u8], @@ -156,9 +166,7 @@ impl Crypter { buffer: &mut [u8], tag: &[u8], ) -> Result<(), ErrorStack> { - if nonce.len() != self.nonce_len { - return Err(ErrorStack::get()); - } + assert_eq!(nonce.len(), self.nonce_len); unsafe { cvt(boring_sys::EVP_AEAD_CTX_open_gather( @@ -180,11 +188,11 @@ impl Crypter { #[cfg(test)] mod tests { - use super::{Algorithm, Crypter}; + use super::Crypter; #[test] fn in_out() { - let key = Crypter::new(&Algorithm::aes_128_gcm(), &[0u8; 16]).unwrap(); + let key = Crypter::new(&super::Algorithm::aes_128_gcm(), &[0u8; 16]).unwrap(); let nonce = [0u8; 12]; let associated_data = b"this is authenticated"; let mut buffer = Vec::with_capacity(26); @@ -199,35 +207,4 @@ mod tests { assert_eq!(b"ABCDE", buffer.as_slice()); } - - #[test] - fn new_rejects_invalid_key_length() { - let result = Crypter::new(&Algorithm::aes_128_gcm(), &[0u8; 15]); - - assert!(result.is_err()); - } - - #[test] - fn seal_rejects_invalid_nonce_and_tag_lengths() { - let key = Crypter::new(&Algorithm::aes_128_gcm(), &[0u8; 16]).unwrap(); - let mut payload = [0u8; 8]; - - let mut short_tag = [0u8; 8]; - let short_tag_result = key.seal_in_place(&[0u8; 12], b"", &mut payload, &mut short_tag); - assert!(short_tag_result.is_err()); - - let mut tag = [0u8; 16]; - let wrong_nonce_result = key.seal_in_place(&[0u8; 11], b"", &mut payload, &mut tag); - assert!(wrong_nonce_result.is_err()); - } - - #[test] - fn open_rejects_invalid_nonce_length() { - let key = Crypter::new(&Algorithm::aes_128_gcm(), &[0u8; 16]).unwrap(); - let mut payload = [0u8; 8]; - let tag = [0u8; 16]; - - let result = key.open_in_place(&[0u8; 11], b"", &mut payload, &tag); - assert!(result.is_err()); - } } diff --git a/boring-additions/src/hmac/types.rs b/boring-additions/src/hmac/types.rs index ad3661c..7601eb9 100644 --- a/boring-additions/src/hmac/types.rs +++ b/boring-additions/src/hmac/types.rs @@ -5,6 +5,8 @@ use std::{ use foreign_types::{ForeignType, ForeignTypeRef, Opaque}; +use crate::helper::{cvt, cvt_p}; + pub struct HmacCtxRef(Opaque); unsafe impl ForeignTypeRef for HmacCtxRef { @@ -33,6 +35,20 @@ unsafe impl ForeignType for HmacCtx { } } +impl Clone for HmacCtx { + fn clone(&self) -> Self { + unsafe { + cvt_p(boring_sys::HMAC_CTX_new()) + .map(|ctx| HmacCtx::from_ptr(ctx)) + .and_then(|ctx| { + cvt(boring_sys::HMAC_CTX_copy(ctx.as_ptr(), self.0.as_ptr()))?; + Ok(ctx) + }) + } + .expect("failed cloning hmac ctx") + } +} + impl Drop for HmacCtx { fn drop(&mut self) { unsafe { diff --git a/boring-rustls-provider/Cargo.toml b/boring-rustls-provider/Cargo.toml index 9956c2b..53ac217 100644 --- a/boring-rustls-provider/Cargo.toml +++ b/boring-rustls-provider/Cargo.toml @@ -8,52 +8,31 @@ description = "Boringssl rustls provider" publish = false [features] -default = [] - -# Build against a FIPS-validated version of BoringSSL and restrict the -# provider to FIPS-approved algorithms only. This affects: -# - Cipher suites: AES-GCM only (no ChaCha20-Poly1305). -# - Key exchange groups: X25519MLKEM768 (preferred), P-256, and P-384 -# only (no X25519, X448, P-521, or FFDHE). -# - Signature algorithms: RSA PKCS#1 / PSS and ECDSA with P-256/P-384 -# only (no P-521, Ed25519, or Ed448). -# Implies `mlkem` to ensure the post-quantum hybrid group is available. -# Aligned with boring's `fips202205` compliance policy (SP 800-52r2). -fips = ["boring/fips", "mlkem"] - -# Enable X25519MLKEM768 post-quantum hybrid key exchange group -# (draft-ietf-tls-ecdhe-mlkem-00). Uses ML-KEM-768 (FIPS 203) combined -# with X25519 for hybrid post-quantum/classical key agreement. -mlkem = ["boring/mlkem"] - -# Deprecated alias for `fips`. Matches the boring crate's feature name -# for backwards compatibility. -fips-precompiled = ["fips"] - -# Enable TLS 1.2 cipher suites (ECDHE-ECDSA and ECDHE-RSA with AES-GCM -# and ChaCha20-Poly1305). Without this feature only TLS 1.3 is available. +default = ["tls12"] +# Use a FIPS-validated version of boringssl. +fips = ["boring/fips", "boring-sys/fips"] +logging = ["log"] +fips-only = [] tls12 = ["rustls/tls12"] -# Enable debug logging of BoringSSL errors and provider internals via -# the `log` crate. Useful for diagnosing handshake failures. -logging = ["log"] [dependencies] -aead = {version = "0.5", default-features = false, features = ["alloc"] } +aead = {version = "0.5", default_features = false, features = ["alloc"] } boring = { workspace = true } boring-additions = { path = "../boring-additions" } boring-sys = { workspace = true } boring-sys-additions = { path = "../boring-sys-additions" } foreign-types = "0.5" +lazy_static = "1.4" log = { version = "0.4.4", optional = true } +once_cell = "1" rustls = { workspace = true } rustls-pki-types = { workspace = true } spki = "0.7" -zeroize = "1" +webpki = { workspace = true, features = ["alloc", "std"] } [dev-dependencies] -hex-literal = "1" -rcgen = "0.12" +hex-literal = "0.4" +rcgen = "0.11.3" tokio = { version = "1.34", features = ["macros", "rt", "net", "io-util", "io-std"] } tokio-rustls = { workspace = true } -webpki-roots = { workspace = true } diff --git a/boring-rustls-provider/src/aead.rs b/boring-rustls-provider/src/aead.rs index 6bcd3ee..cf93908 100644 --- a/boring-rustls-provider/src/aead.rs +++ b/boring-rustls-provider/src/aead.rs @@ -1,11 +1,9 @@ use std::marker::PhantomData; -use aead::{AeadCore, AeadInPlace, Buffer, Nonce, Tag}; +use aead::{AeadCore, AeadInPlace, Nonce, Tag}; use boring::error::ErrorStack; use boring_additions::aead::Algorithm; -#[cfg(feature = "tls12")] -use rustls::crypto::cipher::make_tls12_aad; -use rustls::crypto::cipher::{self, make_tls13_aad, BorrowedPayload, Iv, PrefixedPayload}; +use rustls::crypto::cipher::{self, make_tls12_aad, make_tls13_aad, Iv}; use rustls::{ConnectionTrafficSecrets, ContentType, ProtocolVersion}; use crate::helper::log_and_map; @@ -20,8 +18,6 @@ pub(crate) trait BoringCipher { /// The IV's fixed length (Not the full IV length, only the part that doesn't change). /// Together with [`BoringCipher::explicit_nonce_len`] it determines the total /// lengths of the used nonce. - /// Used only by TLS 1.2 code paths. - #[cfg(feature = "tls12")] const FIXED_IV_LEN: usize; /// The key size in bytes const KEY_SIZE: usize; @@ -29,15 +25,6 @@ pub(crate) trait BoringCipher { /// The length of the authentication tag const TAG_LEN: usize; - /// integrity limit - const INTEGRITY_LIMIT: u64; - - /// confidentiality limit - const CONFIDENTIALITY_LIMIT: u64; - - /// Whether this algorithm is FIPS-approved. - const FIPS_APPROVED: bool; - /// Constructs a new instance of this cipher as an AEAD algorithm fn new_cipher() -> Algorithm; @@ -45,14 +32,14 @@ pub(crate) trait BoringCipher { fn extract_keys(key: cipher::AeadKey, iv: cipher::Iv) -> ConnectionTrafficSecrets; } -pub(crate) trait QuicCipher: Send + Sync { +pub(crate) trait QuicCipher { /// The key size in bytes const KEY_SIZE: usize; /// the expected length of a sample const SAMPLE_LEN: usize; - fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> Result<[u8; 5], rustls::Error>; + fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> [u8; 5]; } pub(crate) trait BoringAead: BoringCipher + AeadCore + Send + Sync {} @@ -77,21 +64,19 @@ impl AeadCore for BoringAeadCrypter { impl BoringAeadCrypter { /// Creates a new aead crypter pub fn new(iv: Iv, key: &[u8], tls_version: ProtocolVersion) -> Result { - let tls_version_supported = match tls_version { + assert!(match tls_version { #[cfg(feature = "tls12")] ProtocolVersion::TLSv1_2 => true, ProtocolVersion::TLSv1_3 => true, _ => false, - }; - if !tls_version_supported { - return Err(ErrorStack::get()); - } + }); let cipher = ::new_cipher(); - if cipher.nonce_len() != rustls::crypto::cipher::Nonce::new(&iv, 0).0.len() { - return Err(ErrorStack::get()); - } + assert_eq!( + cipher.nonce_len(), + rustls::crypto::cipher::Nonce::new(&iv, 0).0.len() + ); let crypter = BoringAeadCrypter { crypter: boring_additions::aead::Crypter::new(&cipher, key)?, @@ -138,10 +123,11 @@ where { fn encrypt( &mut self, - msg: cipher::OutboundPlainMessage, + msg: cipher::BorrowedPlainMessage, seq: u64, - ) -> Result { + ) -> Result { let nonce = cipher::Nonce::new(&self.iv, seq); + match self.tls_version { #[cfg(feature = "tls12")] ProtocolVersion::TLSv1_2 => { @@ -150,57 +136,50 @@ where let total_len = self.encrypted_payload_len(msg.payload.len()); - let mut full_payload = PrefixedPayload::with_capacity(total_len); + let mut full_payload = Vec::with_capacity(total_len); full_payload.extend_from_slice(&nonce.0.as_ref()[fixed_iv_len..]); - full_payload.extend_from_chunks(&msg.payload); + full_payload.extend_from_slice(msg.payload); full_payload.extend_from_slice(&vec![0u8; self.crypter.max_overhead()]); - let (_, payload) = full_payload.as_mut().split_at_mut(explicit_nonce_len); + let (_, payload) = full_payload.split_at_mut(explicit_nonce_len); let (payload, tag) = payload.split_at_mut(msg.payload.len()); let aad = cipher::make_tls12_aad(seq, msg.typ, msg.version, msg.payload.len()); self.crypter .seal_in_place(&nonce.0, &aad, payload, tag) .map_err(|_| rustls::Error::EncryptError) - .map(|_| cipher::OutboundOpaqueMessage::new(msg.typ, msg.version, full_payload)) + .map(|_| cipher::OpaqueMessage::new(msg.typ, msg.version, full_payload)) } ProtocolVersion::TLSv1_3 => { let total_len = self.encrypted_payload_len(msg.payload.len()); - let mut payload = PrefixedPayload::with_capacity(total_len); - payload.extend_from_chunks(&msg.payload); - payload.extend_from_slice(&msg.typ.to_array()); + let mut payload = Vec::with_capacity(total_len); + payload.extend_from_slice(msg.payload); + payload.push(msg.typ.get_u8()); let aad = cipher::make_tls13_aad(total_len); - self.encrypt_in_place( - Nonce::::from_slice(&nonce.0), - &aad, - &mut EncryptBufferAdapter(&mut payload), - ) - .map_err(|_| rustls::Error::EncryptError) - .map(|_| { - cipher::OutboundOpaqueMessage::new( - ContentType::ApplicationData, - ProtocolVersion::TLSv1_2, - payload, - ) - }) + self.encrypt_in_place(Nonce::::from_slice(&nonce.0), &aad, &mut payload) + .map_err(|_| rustls::Error::EncryptError) + .map(|_| { + cipher::OpaqueMessage::new( + ContentType::ApplicationData, + ProtocolVersion::TLSv1_2, + payload, + ) + }) } - _ => Err(rustls::Error::EncryptError), + _ => unimplemented!(), } } fn encrypted_payload_len(&self, payload_len: usize) -> usize { - let per_record_overhead = match self.tls_version { - ProtocolVersion::TLSv1_2 => self - .crypter - .max_overhead() - .saturating_add(::EXPLICIT_NONCE_LEN), - ProtocolVersion::TLSv1_3 => 1usize.saturating_add(self.crypter.max_overhead()), - _ => return payload_len, - }; - - payload_len.saturating_add(per_record_overhead) + match self.tls_version { + ProtocolVersion::TLSv1_2 => { + payload_len + self.crypter.max_overhead() + ::EXPLICIT_NONCE_LEN + } + ProtocolVersion::TLSv1_3 => payload_len + 1 + self.crypter.max_overhead(), + _ => unimplemented!(), + } } } @@ -208,30 +187,23 @@ impl cipher::MessageDecrypter for BoringAeadCrypter where T: BoringAead, { - fn decrypt<'a>( + fn decrypt( &mut self, - mut m: cipher::InboundOpaqueMessage<'a>, + mut m: cipher::OpaqueMessage, seq: u64, - ) -> Result, rustls::Error> { + ) -> Result { match self.tls_version { #[cfg(feature = "tls12")] ProtocolVersion::TLSv1_2 => { let explicit_nonce_len = ::EXPLICIT_NONCE_LEN; - let tag_len = self.crypter.max_overhead(); - let min_payload_len = explicit_nonce_len - .checked_add(tag_len) - .ok_or(rustls::Error::DecryptError)?; - - if m.payload.len() < min_payload_len { - return Err(rustls::Error::DecryptError); - } // payload is: [nonce] | [ciphertext] | [auth tag] - let actual_payload_length = m.payload.len() - tag_len - explicit_nonce_len; + let actual_payload_length = + m.payload().len() - self.crypter.max_overhead() - explicit_nonce_len; let aad = make_tls12_aad(seq, m.typ, m.version, actual_payload_length); - let payload = &mut m.payload; + let payload = m.payload_mut(); // get the nonce let (explicit_nonce, payload) = payload.split_at_mut(explicit_nonce_len); @@ -239,9 +211,7 @@ where let nonce = { let fixed_iv_len = ::FIXED_IV_LEN; - if explicit_nonce_len + fixed_iv_len != 12 { - return Err(rustls::Error::DecryptError); - } + assert_eq!(explicit_nonce_len + fixed_iv_len, 12); // grab the IV by constructing a nonce, this is just an xor let iv = cipher::Nonce::new(&self.iv, seq).0; @@ -252,33 +222,30 @@ where }; // split off the authentication tag - let (payload, tag) = payload.split_at_mut(payload.len() - tag_len); + let (payload, tag) = + payload.split_at_mut(payload.len() - self.crypter.max_overhead()); self.crypter .open_in_place(&nonce, &aad, payload, tag) .map_err(|e| log_and_map("open_in_place", e, rustls::Error::DecryptError)) .map(|_| { // rotate the nonce to the end - m.payload.rotate_left(explicit_nonce_len); + m.payload_mut().rotate_left(explicit_nonce_len); // truncate buffer to the actual payload - m.payload.truncate(actual_payload_length); + m.payload_mut().truncate(actual_payload_length); m.into_plain_message() }) } ProtocolVersion::TLSv1_3 => { let nonce = cipher::Nonce::new(&self.iv, seq); - let aad = make_tls13_aad(m.payload.len()); - self.decrypt_in_place( - Nonce::::from_slice(&nonce.0), - &aad, - &mut DecryptBufferAdapter(&mut m.payload), - ) - .map_err(|_| rustls::Error::DecryptError) - .and_then(|_| m.into_tls13_unpadded_message()) + let aad = make_tls13_aad(m.payload().len()); + self.decrypt_in_place(Nonce::::from_slice(&nonce.0), &aad, m.payload_mut()) + .map_err(|_| rustls::Error::DecryptError) + .and_then(|_| m.into_tls13_unpadded_message()) } - _ => Err(rustls::Error::DecryptError), + _ => unimplemented!(), } } } @@ -311,10 +278,6 @@ where let associated_data = header; let nonce = cipher::Nonce::new(&self.iv, packet_number); - if payload.len() < self.crypter.max_overhead() { - return Err(rustls::Error::DecryptError); - } - let (buffer, tag) = payload.split_at_mut(payload.len() - self.crypter.max_overhead()); self.crypter @@ -327,76 +290,6 @@ where fn tag_len(&self) -> usize { ::TAG_LEN } - - fn confidentiality_limit(&self) -> u64 { - ::CONFIDENTIALITY_LIMIT - } - - fn integrity_limit(&self) -> u64 { - ::INTEGRITY_LIMIT - } -} - -struct InvalidMessageEncrypter; - -impl cipher::MessageEncrypter for InvalidMessageEncrypter { - fn encrypt( - &mut self, - _msg: cipher::OutboundPlainMessage<'_>, - _seq: u64, - ) -> Result { - Err(rustls::Error::EncryptError) - } - - fn encrypted_payload_len(&self, payload_len: usize) -> usize { - payload_len - } -} - -struct InvalidMessageDecrypter; - -impl cipher::MessageDecrypter for InvalidMessageDecrypter { - fn decrypt<'a>( - &mut self, - _msg: cipher::InboundOpaqueMessage<'a>, - _seq: u64, - ) -> Result, rustls::Error> { - Err(rustls::Error::DecryptError) - } -} - -struct InvalidPacketKey(PhantomData); - -impl rustls::quic::PacketKey for InvalidPacketKey { - fn encrypt_in_place( - &self, - _packet_number: u64, - _header: &[u8], - _payload: &mut [u8], - ) -> Result { - Err(rustls::Error::EncryptError) - } - - fn decrypt_in_place<'a>( - &self, - _packet_number: u64, - _header: &[u8], - _payload: &'a mut [u8], - ) -> Result<&'a [u8], rustls::Error> { - Err(rustls::Error::DecryptError) - } - - fn tag_len(&self) -> usize { - ::TAG_LEN - } - - fn confidentiality_limit(&self) -> u64 { - 0 - } - - fn integrity_limit(&self) -> u64 { - 0 - } } pub(crate) struct Aead(PhantomData); @@ -407,31 +300,17 @@ impl Aead { impl cipher::Tls13AeadAlgorithm for Aead { fn encrypter(&self, key: cipher::AeadKey, iv: cipher::Iv) -> Box { - match BoringAeadCrypter::::new(iv, key.as_ref(), ProtocolVersion::TLSv1_3) { - Ok(crypter) => Box::new(crypter), - Err(err) => { - log_and_map( - "Tls13AeadAlgorithm::encrypter BoringAeadCrypter::new", - err, - (), - ); - Box::new(InvalidMessageEncrypter) - } - } + Box::new( + BoringAeadCrypter::::new(iv, key.as_ref(), ProtocolVersion::TLSv1_3) + .expect("failed to create AEAD crypter"), + ) } fn decrypter(&self, key: cipher::AeadKey, iv: cipher::Iv) -> Box { - match BoringAeadCrypter::::new(iv, key.as_ref(), ProtocolVersion::TLSv1_3) { - Ok(crypter) => Box::new(crypter), - Err(err) => { - log_and_map( - "Tls13AeadAlgorithm::decrypter BoringAeadCrypter::new", - err, - (), - ); - Box::new(InvalidMessageDecrypter) - } - } + Box::new( + BoringAeadCrypter::::new(iv, key.as_ref(), ProtocolVersion::TLSv1_3) + .expect("failed to create AEAD crypter"), + ) } fn key_len(&self) -> usize { @@ -445,10 +324,6 @@ impl cipher::Tls13AeadAlgorithm for Aead { ) -> Result { Ok(::extract_keys(key, iv)) } - - fn fips(&self) -> bool { - cfg!(feature = "fips") && ::FIPS_APPROVED - } } #[cfg(feature = "tls12")] @@ -462,42 +337,24 @@ impl cipher::Tls12AeadAlgorithm for Aead { let mut full_iv = Vec::with_capacity(iv.len() + extra.len()); full_iv.extend_from_slice(iv); full_iv.extend_from_slice(extra); - match BoringAeadCrypter::::new( - Iv::copy(&full_iv), - key.as_ref(), - ProtocolVersion::TLSv1_2, - ) { - Ok(crypter) => Box::new(crypter), - Err(err) => { - log_and_map( - "Tls12AeadAlgorithm::encrypter BoringAeadCrypter::new", - err, - (), - ); - Box::new(InvalidMessageEncrypter) - } - } + Box::new( + BoringAeadCrypter::::new(Iv::copy(&full_iv), key.as_ref(), ProtocolVersion::TLSv1_2) + .expect("failed to create AEAD crypter"), + ) } fn decrypter(&self, key: cipher::AeadKey, iv: &[u8]) -> Box { let mut pseudo_iv = Vec::with_capacity(iv.len() + ::EXPLICIT_NONCE_LEN); pseudo_iv.extend_from_slice(iv); pseudo_iv.extend_from_slice(&vec![0u8; ::EXPLICIT_NONCE_LEN]); - match BoringAeadCrypter::::new( - Iv::copy(&pseudo_iv), - key.as_ref(), - ProtocolVersion::TLSv1_2, - ) { - Ok(crypter) => Box::new(crypter), - Err(err) => { - log_and_map( - "Tls12AeadAlgorithm::decrypter BoringAeadCrypter::new", - err, - (), - ); - Box::new(InvalidMessageDecrypter) - } - } + Box::new( + BoringAeadCrypter::::new( + Iv::copy(&pseudo_iv), + key.as_ref(), + ProtocolVersion::TLSv1_2, + ) + .expect("failed to create AEAD crypter"), + ) } fn key_block_shape(&self) -> cipher::KeyBlockShape { @@ -517,13 +374,7 @@ impl cipher::Tls12AeadAlgorithm for Aead { let nonce = { let fixed_iv_len = ::FIXED_IV_LEN; let explicit_nonce_len = ::EXPLICIT_NONCE_LEN; - if explicit_nonce_len + fixed_iv_len != 12 { - return Err(cipher::UnsupportedOperationError); - } - - if iv.len() != fixed_iv_len || explicit.len() != explicit_nonce_len { - return Err(cipher::UnsupportedOperationError); - } + assert_eq!(explicit_nonce_len + fixed_iv_len, 12); // grab the IV by constructing a nonce, this is just an xor @@ -534,10 +385,6 @@ impl cipher::Tls12AeadAlgorithm for Aead { }; Ok(::extract_keys(key, Iv::copy(&nonce))) } - - fn fips(&self) -> bool { - cfg!(feature = "fips") && ::FIPS_APPROVED - } } struct QuicHeaderProtector { @@ -553,8 +400,8 @@ impl QuicHeaderProtector { first: &mut u8, packet_number: &mut [u8], remove: bool, - ) -> Result<(), rustls::Error> { - let mask = T::header_protection_mask(self.key.as_ref(), sample)?; + ) { + let mask = T::header_protection_mask(self.key.as_ref(), sample); const LONG_HEADER_FORMAT: u8 = 0x80; let bits_to_mask = if (*first & LONG_HEADER_FORMAT) == LONG_HEADER_FORMAT { @@ -581,8 +428,6 @@ impl QuicHeaderProtector { for (pn_byte, m) in packet_number.iter_mut().zip(&mask[1..]).take(pn_length) { *pn_byte ^= m; } - - Ok(()) } } @@ -598,7 +443,7 @@ impl rustls::quic::HeaderProtectionKey for QuicHeaderProtector return Err(rustls::Error::General("packet number too long".into())); } - self.rfc9001_header_protection(sample, first, packet_number, false)?; + self.rfc9001_header_protection(sample, first, packet_number, false); Ok(()) } @@ -613,7 +458,7 @@ impl rustls::quic::HeaderProtectionKey for QuicHeaderProtector return Err(rustls::Error::General("packet number too long".into())); } - self.rfc9001_header_protection(sample, first, packet_number, true)?; + self.rfc9001_header_protection(sample, first, packet_number, true); Ok(()) } @@ -628,13 +473,10 @@ where T: QuicCipher + BoringAead + 'static, { fn packet_key(&self, key: cipher::AeadKey, iv: Iv) -> Box { - match BoringAeadCrypter::::new(iv, key.as_ref(), ProtocolVersion::TLSv1_3) { - Ok(crypter) => Box::new(crypter), - Err(err) => { - log_and_map("QuicAlgorithm::packet_key BoringAeadCrypter::new", err, ()); - Box::new(InvalidPacketKey::(PhantomData)) - } - } + Box::new( + BoringAeadCrypter::::new(iv, key.as_ref(), ProtocolVersion::TLSv1_3) + .expect("failed to create AEAD crypter"), + ) } fn header_protection_key( @@ -650,78 +492,17 @@ where fn aead_key_len(&self) -> usize { ::KEY_SIZE } - - fn fips(&self) -> bool { - cfg!(feature = "fips") && ::FIPS_APPROVED - } -} - -struct DecryptBufferAdapter<'a, 'p>(&'a mut BorrowedPayload<'p>); - -impl AsRef<[u8]> for DecryptBufferAdapter<'_, '_> { - fn as_ref(&self) -> &[u8] { - self.0 - } -} - -impl AsMut<[u8]> for DecryptBufferAdapter<'_, '_> { - fn as_mut(&mut self) -> &mut [u8] { - self.0 - } -} - -impl Buffer for DecryptBufferAdapter<'_, '_> { - fn extend_from_slice(&mut self, _: &[u8]) -> aead::Result<()> { - Err(aead::Error) - } - - fn truncate(&mut self, len: usize) { - self.0.truncate(len) - } -} - -struct EncryptBufferAdapter<'a>(&'a mut PrefixedPayload); - -impl AsRef<[u8]> for EncryptBufferAdapter<'_> { - fn as_ref(&self) -> &[u8] { - self.0.as_ref() - } -} - -impl AsMut<[u8]> for EncryptBufferAdapter<'_> { - fn as_mut(&mut self) -> &mut [u8] { - self.0.as_mut() - } -} - -impl Buffer for EncryptBufferAdapter<'_> { - fn extend_from_slice(&mut self, other: &[u8]) -> aead::Result<()> { - self.0.extend_from_slice(other); - Ok(()) - } - - fn truncate(&mut self, len: usize) { - self.0.truncate(len) - } } #[cfg(test)] mod tests { use hex_literal::hex; - use rustls::crypto::cipher::Tls13AeadAlgorithm; - use rustls::crypto::cipher::{AeadKey, InboundOpaqueMessage, Iv}; - #[cfg(feature = "tls12")] - use rustls::crypto::cipher::{MessageDecrypter, Tls12AeadAlgorithm}; - use rustls::quic::Algorithm as QuicAlgorithm; - use rustls::quic::HeaderProtectionKey; - use rustls::ContentType; - use rustls::ProtocolVersion; + use rustls::crypto::cipher::{AeadKey, Iv}; use crate::aead::BoringAeadCrypter; use rustls::quic::PacketKey; - use super::aes::Aes128; - use super::{chacha20::ChaCha20Poly1305, BoringCipher, QuicHeaderProtector}; + use super::{chacha20::ChaCha20Poly1305, QuicHeaderProtector}; #[test] fn quic_header_protection_short() { @@ -738,97 +519,14 @@ mod tests { phantom: std::marker::PhantomData::, }; - protector - .rfc9001_header_protection(&sample, &mut first[0], packet_number, false) - .expect("valid sample should protect QUIC header"); + protector.rfc9001_header_protection(&sample, &mut first[0], packet_number, false); assert_eq!(&header[..], &protected_header[..]); let (first, packet_number) = header.split_at_mut(1); - protector - .rfc9001_header_protection(&sample, &mut first[0], packet_number, true) - .expect("valid sample should unprotect QUIC header"); + protector.rfc9001_header_protection(&sample, &mut first[0], packet_number, true); assert_eq!(&header[..], &unprotected_header[..]); } - #[test] - fn quic_header_protection_rejects_invalid_sample_without_mutation() { - let hp_key = hex!("25a282b9e82f06f21f488917a4fc8f1b73573685608597d0efcb076b0ab7a7a4"); - let sample = hex!("5e5cd55c41f69080575d7999c25a5bfb"); - let mut header = hex!("4200bff4"); - let original = header; - let (first, packet_number) = header.split_at_mut(1); - - let protector = QuicHeaderProtector { - key: AeadKey::from(hp_key), - phantom: std::marker::PhantomData::, - }; - - let err = protector - .encrypt_in_place(&sample[..sample.len() - 1], &mut first[0], packet_number) - .expect_err("short sample must be rejected"); - - assert!(matches!(err, rustls::Error::General(_))); - assert_eq!(header, original); - } - - #[test] - fn tls13_decrypter_constructor_failure_returns_erroring_decrypter() { - let mut decrypter = Tls13AeadAlgorithm::decrypter( - &super::Aead::::DEFAULT, - AeadKey::from([0u8; 32]), - Iv::from([0u8; 12]), - ); - let mut payload = vec![0u8; 8]; - let msg = InboundOpaqueMessage::new( - ContentType::ApplicationData, - ProtocolVersion::TLSv1_3, - &mut payload, - ); - - let err = decrypter - .decrypt(msg, 0) - .expect_err("invalid constructor inputs should produce decrypt errors"); - - assert!(matches!(err, rustls::Error::DecryptError)); - } - - #[test] - fn quic_packet_key_constructor_failure_returns_erroring_key() { - let packet_key = super::Aead::::DEFAULT - .packet_key(AeadKey::from([0u8; 32]), Iv::from([0u8; 12])); - let mut payload = [0u8; 0]; - - let enc_err = packet_key.encrypt_in_place(0, &[], &mut payload); - assert!(matches!(enc_err, Err(rustls::Error::EncryptError))); - - let dec_err = packet_key - .decrypt_in_place(0, &[], &mut payload) - .expect_err("invalid constructor inputs should produce decrypt errors"); - assert!(matches!(dec_err, rustls::Error::DecryptError)); - } - - #[cfg(feature = "tls12")] - #[test] - fn tls12_decrypter_constructor_failure_returns_erroring_decrypter() { - let mut decrypter = Tls12AeadAlgorithm::decrypter( - &super::Aead::::DEFAULT, - AeadKey::from([0u8; 32]), - &[0u8; 4], - ); - let mut payload = vec![0u8; 8]; - let msg = InboundOpaqueMessage::new( - ContentType::ApplicationData, - ProtocolVersion::TLSv1_2, - &mut payload, - ); - - let err = decrypter - .decrypt(msg, 0) - .expect_err("invalid constructor inputs should produce decrypt errors"); - - assert!(matches!(err, rustls::Error::DecryptError)); - } - #[test] fn quic_chacha20_crypt() { // test vector from https://www.rfc-editor.org/rfc/rfc9001.html#name-chacha20-poly1305-short-hea @@ -841,9 +539,9 @@ mod tests { let unprotected_header = hex!("4200bff4"); let protector = BoringAeadCrypter::::new( - Iv::from(iv), + Iv::new(iv), &key, - ProtocolVersion::TLSv1_3, + rustls::ProtocolVersion::TLSv1_3, ) .unwrap(); @@ -862,47 +560,4 @@ mod tests { assert_eq!(cleartext, expected_cleartext); } - - #[test] - fn quic_decrypt_rejects_truncated_payload() { - let key = [0u8; ::KEY_SIZE]; - let iv = [0u8; 12]; - let crypter = BoringAeadCrypter::::new( - Iv::from(iv), - &key, - ProtocolVersion::TLSv1_3, - ) - .unwrap(); - - let mut payload = [0u8; ::TAG_LEN - 1]; - let err = crypter - .decrypt_in_place(0, &[], &mut payload) - .expect_err("truncated QUIC payload must fail decryption"); - - assert!(matches!(err, rustls::Error::DecryptError)); - } - - #[cfg(feature = "tls12")] - #[test] - fn tls12_decrypt_rejects_truncated_payload() { - let key = [0u8; ::KEY_SIZE]; - let iv = [0u8; 12]; - let mut decrypter = - BoringAeadCrypter::::new(Iv::from(iv), &key, ProtocolVersion::TLSv1_2).unwrap(); - - let min_payload_len = - ::EXPLICIT_NONCE_LEN + ::TAG_LEN; - let mut payload = vec![0u8; min_payload_len - 1]; - let msg = InboundOpaqueMessage::new( - ContentType::ApplicationData, - ProtocolVersion::TLSv1_2, - &mut payload, - ); - - let err = decrypter - .decrypt(msg, 0) - .expect_err("truncated TLS1.2 payload must fail decryption"); - - assert!(matches!(err, rustls::Error::DecryptError)); - } } diff --git a/boring-rustls-provider/src/aead/aes.rs b/boring-rustls-provider/src/aead/aes.rs index 4fd591c..712d6ce 100644 --- a/boring-rustls-provider/src/aead/aes.rs +++ b/boring-rustls-provider/src/aead/aes.rs @@ -11,17 +11,12 @@ impl BoringAead for Aes128 {} impl BoringCipher for Aes128 { const EXPLICIT_NONCE_LEN: usize = 8; - #[cfg(feature = "tls12")] const FIXED_IV_LEN: usize = 4; const KEY_SIZE: usize = 16; const TAG_LEN: usize = 16; - const INTEGRITY_LIMIT: u64 = 1 << 52; - const CONFIDENTIALITY_LIMIT: u64 = 1 << 23; - const FIPS_APPROVED: bool = true; - fn new_cipher() -> Algorithm { Algorithm::aes_128_gcm() } @@ -41,7 +36,7 @@ impl QuicCipher for Aes128 { const KEY_SIZE: usize = ::KEY_SIZE; const SAMPLE_LEN: usize = 16; - fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> Result<[u8; 5], rustls::Error> { + fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> [u8; 5] { quic_header_protection_mask::< { ::KEY_SIZE }, { ::SAMPLE_LEN }, @@ -57,17 +52,12 @@ impl BoringAead for Aes256 {} impl BoringCipher for Aes256 { const EXPLICIT_NONCE_LEN: usize = 8; - #[cfg(feature = "tls12")] const FIXED_IV_LEN: usize = 4; const KEY_SIZE: usize = 32; const TAG_LEN: usize = 16; - const INTEGRITY_LIMIT: u64 = 1 << 52; - const CONFIDENTIALITY_LIMIT: u64 = 1 << 23; - const FIPS_APPROVED: bool = true; - fn new_cipher() -> Algorithm { Algorithm::aes_256_gcm() } @@ -87,7 +77,7 @@ impl QuicCipher for Aes256 { const KEY_SIZE: usize = ::KEY_SIZE; const SAMPLE_LEN: usize = 16; - fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> Result<[u8; 5], rustls::Error> { + fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> [u8; 5] { quic_header_protection_mask::< { ::KEY_SIZE }, { ::SAMPLE_LEN }, @@ -99,32 +89,19 @@ fn quic_header_protection_mask( cipher: boring::symm::Cipher, hp_key: &[u8], sample: &[u8], -) -> Result<[u8; 5], rustls::Error> { - if hp_key.len() != KEY_SIZE { - return Err(rustls::Error::General( - "header protection key of invalid length".into(), - )); - } - if sample.len() != SAMPLE_LEN { - return Err(rustls::Error::General("sample of invalid length".into())); - } +) -> [u8; 5] { + assert!(hp_key.len() == KEY_SIZE); + assert!(sample.len() >= SAMPLE_LEN); let mut output = [0u8; SAMPLE_LEN]; let mut crypter = boring::symm::Crypter::new(cipher, boring::symm::Mode::Encrypt, hp_key, None) - .map_err(|_| rustls::Error::General("failed generating header protection mask".into()))?; + .expect("failed getting crypter"); - let len = crypter - .update(sample, &mut output) - .map_err(|_| rustls::Error::General("failed generating header protection mask".into()))?; - let _total = len - + crypter.finalize(&mut output[len..]).map_err(|_| { - rustls::Error::General("failed generating header protection mask".into()) - })?; + let len = crypter.update(sample, &mut output).unwrap(); + let _ = len + crypter.finalize(&mut output[len..]).unwrap(); - let mut mask = [0u8; 5]; - mask.copy_from_slice(&output[..5]); - Ok(mask) + output[..5].try_into().unwrap() } #[cfg(test)] diff --git a/boring-rustls-provider/src/aead/chacha20.rs b/boring-rustls-provider/src/aead/chacha20.rs index 142dd59..0681fc7 100644 --- a/boring-rustls-provider/src/aead/chacha20.rs +++ b/boring-rustls-provider/src/aead/chacha20.rs @@ -14,17 +14,12 @@ impl BoringAead for ChaCha20Poly1305 {} impl BoringCipher for ChaCha20Poly1305 { const EXPLICIT_NONCE_LEN: usize = 0; - #[cfg(feature = "tls12")] const FIXED_IV_LEN: usize = 12; const KEY_SIZE: usize = 32; const TAG_LEN: usize = 16; - const INTEGRITY_LIMIT: u64 = 1 << 36; - const CONFIDENTIALITY_LIMIT: u64 = u64::MAX; - const FIPS_APPROVED: bool = false; - fn new_cipher() -> Algorithm { Algorithm::chacha20_poly1305() } @@ -38,21 +33,13 @@ impl QuicCipher for ChaCha20Poly1305 { const KEY_SIZE: usize = 32; const SAMPLE_LEN: usize = 16; - fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> Result<[u8; 5], rustls::Error> { - if hp_key.len() != ::KEY_SIZE { - return Err(rustls::Error::General( - "header protection key of invalid length".into(), - )); - } - if sample.len() != ::SAMPLE_LEN { - return Err(rustls::Error::General("sample of invalid length".into())); - } + fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> [u8; 5] { + assert!(hp_key.len() == ::KEY_SIZE); + assert!(sample.len() >= ::SAMPLE_LEN); let mut mask = [0u8; 5]; // RFC9001 5.4.4: The first 4 bytes of the sampled ciphertext are the block counter. A ChaCha20 implementation could take a 32-bit integer in place of a byte sequence, in which case, the byte sequence is interpreted as a little-endian value. - let mut counter_bytes = [0u8; 4]; - counter_bytes.copy_from_slice(&sample[0..4]); - let counter = u32::from_le_bytes(counter_bytes); + let counter = u32::from_le_bytes(sample[0..4].try_into().unwrap()); // RFC9001 5.4.4: The remaining 12 bytes are used as the nonce. let nonce = &sample[4..16]; unsafe { @@ -65,7 +52,7 @@ impl QuicCipher for ChaCha20Poly1305 { counter, ); }; - Ok(mask) + mask } } @@ -104,8 +91,7 @@ mod tests { let sample = hex!("5e5cd55c41f69080575d7999c25a5bfb"); let hp_key = hex!("25a282b9e82f06f21f488917a4fc8f1b73573685608597d0efcb076b0ab7a7a4"); let expected_mask = hex!("aefefe7d03"); - let mask = ChaCha20Poly1305::header_protection_mask(&hp_key, &sample) - .expect("valid QUIC sample/key should produce a mask"); + let mask = ChaCha20Poly1305::header_protection_mask(&hp_key, &sample); assert_eq!(mask, expected_mask); } } diff --git a/boring-rustls-provider/src/hash.rs b/boring-rustls-provider/src/hash.rs index 95ddc41..44bad44 100644 --- a/boring-rustls-provider/src/hash.rs +++ b/boring-rustls-provider/src/hash.rs @@ -24,14 +24,10 @@ impl hash::Hash for Hash { boring::nid::Nid::SHA256 => hash::HashAlgorithm::SHA256, boring::nid::Nid::SHA384 => hash::HashAlgorithm::SHA384, boring::nid::Nid::SHA512 => hash::HashAlgorithm::SHA512, - _ => unreachable!("hash::Hash is only instantiated with SHA-2 digests"), + _ => unimplemented!(), } } - fn fips(&self) -> bool { - cfg!(feature = "fips") - } - fn output_len(&self) -> usize { MessageDigest::from_nid(self.0) .expect("failed getting digest") diff --git a/boring-rustls-provider/src/helper.rs b/boring-rustls-provider/src/helper.rs index b632357..6d7f173 100644 --- a/boring-rustls-provider/src/helper.rs +++ b/boring-rustls-provider/src/helper.rs @@ -38,6 +38,6 @@ pub(crate) fn log_and_map(func: &'static str, e: E, ma } #[cfg(not(feature = "log"))] -pub(crate) fn log_and_map(_func: &'static str, _e: E, mapped: T) -> T { +pub(crate) fn log_and_map(func: &'static str, e: E, mapped: T) -> T { mapped } diff --git a/boring-rustls-provider/src/hkdf.rs b/boring-rustls-provider/src/hkdf.rs index d4b070d..64c37c4 100644 --- a/boring-rustls-provider/src/hkdf.rs +++ b/boring-rustls-provider/src/hkdf.rs @@ -2,7 +2,6 @@ use std::marker::PhantomData; use boring::hash::MessageDigest; use rustls::crypto::tls13::{self, Hkdf as RustlsHkdf}; -use zeroize::Zeroizing; use crate::helper::{cvt, cvt_p}; @@ -68,7 +67,7 @@ impl RustlsHkdf for Hkdf { let digest = T::new_hash(); let hash_size = digest.size(); - let mut prk = Zeroizing::new([0u8; boring_sys::EVP_MAX_MD_SIZE as usize]); + let mut prk = [0u8; boring_sys::EVP_MAX_MD_SIZE as usize]; let mut prk_len = 0; // if salt isn't set we usen these bytes here as salt @@ -104,7 +103,7 @@ impl RustlsHkdf for Hkdf { okm: &rustls::crypto::tls13::OkmBlock, ) -> Box { let okm = okm.as_ref(); - let mut prk = Zeroizing::new([0u8; boring_sys::EVP_MAX_MD_SIZE as usize]); + let mut prk = [0u8; boring_sys::EVP_MAX_MD_SIZE as usize]; let prk_len = okm.len(); prk[..prk_len].copy_from_slice(okm); @@ -122,7 +121,7 @@ impl RustlsHkdf for Hkdf { message: &[u8], ) -> rustls::crypto::hmac::Tag { let digest = T::new_hash(); - let mut hash = Zeroizing::new([0u8; boring_sys::EVP_MAX_MD_SIZE as usize]); + let mut hash = [0u8; boring_sys::EVP_MAX_MD_SIZE as usize]; let mut hash_len = 0u32; unsafe { cvt_p(boring_sys::HMAC( @@ -138,14 +137,10 @@ impl RustlsHkdf for Hkdf { } rustls::crypto::hmac::Tag::new(&hash[..hash_len as usize]) } - - fn fips(&self) -> bool { - cfg!(feature = "fips") - } } struct HkdfExpander { - prk: Zeroizing<[u8; boring_sys::EVP_MAX_MD_SIZE as usize]>, + prk: [u8; boring_sys::EVP_MAX_MD_SIZE as usize], prk_len: usize, digest: MessageDigest, } @@ -167,17 +162,9 @@ impl tls13::HkdfExpander for HkdfExpander { info: &[&[u8]], output: &mut [u8], ) -> Result<(), tls13::OutputLengthError> { - let max_output_len = self - .hash_len() - .checked_mul(255) - .ok_or(tls13::OutputLengthError)?; - if output.len() > max_output_len { - return Err(tls13::OutputLengthError); - } - let info_concat = info.concat(); unsafe { - cvt(boring_sys::HKDF_expand( + boring_sys::HKDF_expand( output.as_mut_ptr(), output.len(), self.digest.as_ptr(), @@ -185,9 +172,8 @@ impl tls13::HkdfExpander for HkdfExpander { self.prk_len, info_concat.as_ptr(), info_concat.len(), - )) - .map_err(|_| tls13::OutputLengthError)?; - } + ); + }; Ok(()) } @@ -201,7 +187,7 @@ impl tls13::HkdfExpander for HkdfExpander { /// This is infallible, because by definition `OkmBlock` is always exactly /// `HashLen` bytes long. fn expand_block(&self, info: &[&[u8]]) -> tls13::OkmBlock { - let mut output = Zeroizing::new([0u8; boring_sys::EVP_MAX_MD_SIZE as usize]); + let mut output = [0u8; boring_sys::EVP_MAX_MD_SIZE as usize]; let output_len = self.hash_len(); self.expand_slice(info, &mut output[..output_len]) @@ -214,35 +200,3 @@ impl tls13::HkdfExpander for HkdfExpander { self.digest.size() } } - -#[cfg(test)] -mod tests { - use boring::hash::MessageDigest; - use rustls::crypto::tls13::Hkdf as _; - - use super::{Hkdf, Sha256}; - - #[test] - fn expand_slice_rejects_output_larger_than_rfc_limit() { - let hkdf = Hkdf::::DEFAULT; - let expander = hkdf.extract_from_secret(None, b"ikm"); - let hash_len = MessageDigest::sha256().size(); - let mut output = vec![0u8; hash_len * 255 + 1]; - - assert!(expander.expand_slice(&[b"info"], &mut output).is_err()); - } - - #[test] - fn expand_slice_accepts_output_at_rfc_limit() { - let hkdf = Hkdf::::DEFAULT; - let expander = hkdf.extract_from_secret(None, b"ikm"); - let hash_len = MessageDigest::sha256().size(); - let mut output = vec![0u8; hash_len * 255]; - - expander - .expand_slice(&[b"info"], &mut output) - .expect("HKDF expand at RFC limit should succeed"); - - assert!(output.iter().any(|byte| *byte != 0)); - } -} diff --git a/boring-rustls-provider/src/hmac.rs b/boring-rustls-provider/src/hmac.rs index fdcd508..c5d403c 100644 --- a/boring-rustls-provider/src/hmac.rs +++ b/boring-rustls-provider/src/hmac.rs @@ -4,7 +4,6 @@ use boring::hash::MessageDigest; use boring_additions::hmac::HmacCtx; use foreign_types::ForeignType; use rustls::crypto; -use zeroize::Zeroizing; use crate::helper::{cvt, cvt_p}; @@ -20,11 +19,18 @@ struct BoringHmac(pub boring::nid::Nid); impl crypto::hmac::Hmac for BoringHmac { fn with_key(&self, key: &[u8]) -> Box { + let ctx = unsafe { + HmacCtx::from_ptr( + cvt_p(boring_sys::HMAC_CTX_new()).expect("failed getting hmac context"), + ) + }; + let md = MessageDigest::from_nid(self.0).expect("failed getting digest"); Box::new(BoringHmacKey { + ctx, md, - key: Zeroizing::new(key.to_vec()), + key: key.to_vec(), }) } @@ -33,35 +39,34 @@ impl crypto::hmac::Hmac for BoringHmac { .expect("failed getting digest") .size() } - - fn fips(&self) -> bool { - cfg!(feature = "fips") - } } +#[derive(Clone)] struct BoringHmacKey { + ctx: HmacCtx, md: MessageDigest, - key: Zeroizing>, + key: Vec, } impl BoringHmacKey { - fn init(ctx: &HmacCtx, key: &[u8], md: MessageDigest) { + fn init(&self) { unsafe { + // initialize a new hmac cvt(boring_sys::HMAC_Init_ex( - ctx.as_ptr(), - key.as_ptr() as *const c_void, - key.len(), - md.as_ptr(), + self.ctx.as_ptr(), + self.key.as_ptr() as *const c_void, + self.key.len(), + self.md.as_ptr(), ptr::null_mut(), )) } .expect("failed initializing hmac"); } - fn update(ctx: &HmacCtx, bytes: &[u8]) { + fn update(&self, bytes: &[u8]) { unsafe { cvt(boring_sys::HMAC_Update( - ctx.as_ptr(), + self.ctx.as_ptr(), bytes.as_ptr(), bytes.len(), )) @@ -69,11 +74,11 @@ impl BoringHmacKey { .expect("failed updating hmac"); } - fn finish(ctx: &HmacCtx, out: &mut [u8]) -> usize { + fn finish(&self, out: &mut [u8]) -> usize { let mut out_len = 0; unsafe { cvt(boring_sys::HMAC_Final( - ctx.as_ptr(), + self.ctx.as_ptr(), out.as_mut_ptr(), &mut out_len, )) @@ -81,28 +86,21 @@ impl BoringHmacKey { .expect("failed hmac final"); out_len as usize } - - fn new_ctx() -> HmacCtx { - unsafe { - HmacCtx::from_ptr(cvt_p(boring_sys::HMAC_CTX_new()).expect("failed creating HMAC_CTX")) - } - } } impl crypto::hmac::Key for BoringHmacKey { fn sign_concat(&self, first: &[u8], middle: &[&[u8]], last: &[u8]) -> crypto::hmac::Tag { - let ctx = Self::new_ctx(); - Self::init(&ctx, self.key.as_slice(), self.md); + self.init(); - Self::update(&ctx, first); + self.update(first); for m in middle { - Self::update(&ctx, m); + self.update(m); } - Self::update(&ctx, last); + self.update(last); - let mut out = Zeroizing::new([0u8; boring_sys::EVP_MAX_MD_SIZE as usize]); - let out_len = Self::finish(&ctx, &mut out[..]); + let mut out = [0u8; 32]; + let out_len = self.finish(&mut out); crypto::hmac::Tag::new(&out[..out_len]) } @@ -114,7 +112,7 @@ impl crypto::hmac::Key for BoringHmacKey { #[cfg(test)] mod tests { - use super::{SHA256, SHA384}; + use super::SHA256; use hex_literal::hex; #[test] @@ -143,23 +141,4 @@ mod tests { hex!("11fa4a6ee97bebfad9e1087145c556fec9a786cad0659aa10702d21bd2968305") ); } - - #[test] - fn test_sha384_hmac_len() { - let hasher = SHA384.with_key("Very Secret".as_bytes()); - - let tag = hasher.sign_concat( - &[], - &[ - "yay".as_bytes(), - "this".as_bytes(), - "works".as_bytes(), - "well".as_bytes(), - ], - &[], - ); - - assert_eq!(tag.as_ref().len(), hasher.tag_len()); - assert_eq!(tag.as_ref().len(), 48); - } } diff --git a/boring-rustls-provider/src/kx.rs b/boring-rustls-provider/src/kx.rs new file mode 100644 index 0000000..8ec7276 --- /dev/null +++ b/boring-rustls-provider/src/kx.rs @@ -0,0 +1,108 @@ +use rustls::crypto::{self, ActiveKeyExchange}; + +use crate::helper::log_and_map; + +mod dh; +mod ex; + +enum DhKeyType { + EC((boring::ec::EcGroup, i32)), + ED(i32), + FFDHE2048, +} + +/// A X25519-based key exchange +#[derive(Debug)] +pub struct X25519; + +impl crypto::SupportedKxGroup for X25519 { + fn start(&self) -> Result, rustls::Error> { + Ok(Box::new(ex::KeyExchange::with_x25519().map_err(|e| { + log_and_map("X25519.start", e, crypto::GetRandomFailed) + })?)) + } + + fn name(&self) -> rustls::NamedGroup { + rustls::NamedGroup::X25519 + } +} + +/// A X448-based key exchange +#[derive(Debug)] +pub struct X448; + +impl crypto::SupportedKxGroup for X448 { + fn start(&self) -> Result, rustls::Error> { + Ok(Box::new(ex::KeyExchange::with_x448().map_err(|e| { + log_and_map("X448.start", e, crypto::GetRandomFailed) + })?)) + } + + fn name(&self) -> rustls::NamedGroup { + rustls::NamedGroup::X448 + } +} + +/// A secp256r1-based key exchange +#[derive(Debug)] +pub struct Secp256r1; + +impl crypto::SupportedKxGroup for Secp256r1 { + fn start(&self) -> Result, rustls::Error> { + Ok(Box::new(ex::KeyExchange::with_secp256r1().map_err( + |e| log_and_map("Secp256r1.start", e, crypto::GetRandomFailed), + )?)) + } + + fn name(&self) -> rustls::NamedGroup { + rustls::NamedGroup::secp256r1 + } +} + +/// A secp384r1-based key exchange +#[derive(Debug)] +pub struct Secp384r1; + +impl crypto::SupportedKxGroup for Secp384r1 { + fn start(&self) -> Result, rustls::Error> { + Ok(Box::new(ex::KeyExchange::with_secp384r1().map_err( + |e| log_and_map("Secp384r1.start", e, crypto::GetRandomFailed), + )?)) + } + + fn name(&self) -> rustls::NamedGroup { + rustls::NamedGroup::secp384r1 + } +} + +/// A secp521r1-based key exchange +#[derive(Debug)] +pub struct Secp521r1; + +impl crypto::SupportedKxGroup for Secp521r1 { + fn start(&self) -> Result, rustls::Error> { + Ok(Box::new(ex::KeyExchange::with_secp521r1().map_err( + |e| log_and_map("Secp521r1.start", e, crypto::GetRandomFailed), + )?)) + } + + fn name(&self) -> rustls::NamedGroup { + rustls::NamedGroup::secp521r1 + } +} + +/// A ffedhe2048-based key exchange +#[derive(Debug)] +pub struct FfDHe2048; + +impl crypto::SupportedKxGroup for FfDHe2048 { + fn start(&self) -> Result, rustls::Error> { + Ok(Box::new(dh::KeyExchange::generate_ffdhe_2048().map_err( + |e| log_and_map("FfDHe2048.start", e, crypto::GetRandomFailed), + )?)) + } + + fn name(&self) -> rustls::NamedGroup { + rustls::NamedGroup::FFDHE2048 + } +} diff --git a/boring-rustls-provider/src/kx/dh.rs b/boring-rustls-provider/src/kx/dh.rs new file mode 100644 index 0000000..b004e5b --- /dev/null +++ b/boring-rustls-provider/src/kx/dh.rs @@ -0,0 +1,123 @@ +use boring::{dh::Dh, error::ErrorStack, pkey::Private}; +use foreign_types::ForeignType; +use rustls::crypto; + +use crate::helper::{cvt, cvt_p, log_and_map}; + +use super::DhKeyType; + +/// This type can be used to perform a +/// Diffie-Hellman key exchange. +pub struct KeyExchange { + dh: Dh, + pub_bytes: Vec, + key_type: DhKeyType, +} + +impl KeyExchange { + // Generate a new KeyExchange with a random FFDHE_2048 private key + pub fn generate_ffdhe_2048() -> Result { + let mut me = Self { + dh: unsafe { Dh::from_ptr(cvt_p(boring_sys::DH_get_rfc7919_2048())?) }, + pub_bytes: Vec::new(), + key_type: DhKeyType::FFDHE2048, + }; + + me.pub_bytes = unsafe { + // generate a new key pair + cvt(boring_sys::DH_generate_key(me.dh.as_ptr()))?; + + // get a reference to the pub key + let pubkey = boring_sys::DH_get0_pub_key(me.dh.as_ptr()); + + // figure out how many bytes we need, round up to the next full byte + let size = (boring_sys::BN_num_bits(pubkey) as usize + 7) / 8; + + // alloc a vector with enough capacity + let mut v = Vec::with_capacity(size); + + // convert to binary representation + let after_size = boring_sys::BN_bn2bin(pubkey, v.as_mut_ptr()); + // size should be what we calculated before + assert_eq!(size, after_size); + + // ensure those bytes are accessible in the vec + v.set_len(size); + v + }; + + Ok(me) + } + + /// Generate a shared secret with the other's raw public key + fn diffie_hellman(&self, raw_public_key: &[u8]) -> Result, ErrorStack> { + let peer = boring::bn::BigNum::from_slice(raw_public_key)?; + + let secret_len = unsafe { cvt(boring_sys::DH_size(self.dh.as_ptr()))? } as usize; + let mut secret = vec![0u8; secret_len]; + + let secret_len = unsafe { + cvt(boring_sys::DH_compute_key_padded( + secret.as_mut_ptr(), + peer.as_ptr(), + self.dh.as_ptr(), + ))? + } as usize; + + secret.truncate(secret_len); + Ok(secret) + } +} + +impl crypto::ActiveKeyExchange for KeyExchange { + fn complete( + self: Box, + peer_pub_key: &[u8], + ) -> Result { + let expected_len = self.pub_bytes.len(); + + if peer_pub_key.len() != expected_len { + return Err(rustls::Error::from(rustls::PeerMisbehaved::InvalidKeyShare)); + } + + Ok(crypto::SharedSecret::from( + self.diffie_hellman(peer_pub_key) + .map_err(|e| { + log_and_map( + "dh::KeyExchange::diffie_hellman", + e, + rustls::PeerMisbehaved::InvalidKeyShare, + ) + })? + .as_ref(), + )) + } + + fn pub_key(&self) -> &[u8] { + self.pub_bytes.as_ref() + } + + fn group(&self) -> rustls::NamedGroup { + match self.key_type { + DhKeyType::FFDHE2048 => rustls::NamedGroup::FFDHE2048, + _ => unimplemented!(), + } + } +} + +#[cfg(test)] +mod tests { + use crate::kx::dh::KeyExchange; + use rustls::crypto::ActiveKeyExchange; + + #[test] + fn test_derive_dh() { + let alice = KeyExchange::generate_ffdhe_2048().unwrap(); + let bob = KeyExchange::generate_ffdhe_2048().unwrap(); + + let shared_secret1 = alice.diffie_hellman(bob.pub_key()).unwrap(); + let shared_secret2 = bob.diffie_hellman(alice.pub_key()).unwrap(); + + assert_eq!(shared_secret1, shared_secret2) + } +} diff --git a/boring-rustls-provider/src/kx/ex.rs b/boring-rustls-provider/src/kx/ex.rs index 297844b..cd628a5 100644 --- a/boring-rustls-provider/src/kx/ex.rs +++ b/boring-rustls-provider/src/kx/ex.rs @@ -1,22 +1,20 @@ -#[cfg(not(feature = "fips"))] -use std::{mem::MaybeUninit, ptr}; +use std::{ + mem::MaybeUninit, + ptr::{self}, +}; use boring::{ ec::{EcGroup, EcKey}, error::ErrorStack, nid::Nid, - pkey::Id, pkey::{PKey, PKeyRef, Private}, }; -#[cfg(not(feature = "fips"))] use boring_additions::evp::EvpPkeyCtx; -#[cfg(not(feature = "fips"))] use foreign_types::ForeignType; use rustls::crypto; +use spki::der::Decode; -use crate::helper::log_and_map; -#[cfg(not(feature = "fips"))] -use crate::helper::{cvt, cvt_p}; +use crate::helper::{cvt, cvt_p, log_and_map}; use super::DhKeyType; @@ -32,11 +30,16 @@ pub struct KeyExchange { impl KeyExchange { /// Creates a new `KeyExchange` using a random /// private key for the `X25519` Edwards curve - #[cfg(not(feature = "fips"))] pub fn with_x25519() -> Result { Self::ed_from_curve(Nid::from_raw(boring_sys::NID_X25519)) } + /// Creates a new `KeyExchange` using a random + /// private key for the `X448` Edwards curve + pub fn with_x448() -> Result { + Self::ed_from_curve(Nid::from_raw(boring_sys::NID_X448)) + } + /// Creates a new `KeyExchange` using a random /// private key for `sepc256r1` curve /// Also known as `X9_62_PRIME256V1` @@ -50,6 +53,12 @@ impl KeyExchange { Self::ec_from_curve(Nid::SECP384R1) } + /// Creates a new `KeyExchange` using a random + /// private key for `sep521r1` curve + pub fn with_secp521r1() -> Result { + Self::ec_from_curve(Nid::SECP521R1) + } + /// Allows getting a new `KeyExchange` using Eliptic Curves /// on the specified curve fn ec_from_curve(nid: Nid) -> Result { @@ -67,7 +76,6 @@ impl KeyExchange { /// Allows getting a new `KeyExchange` using Edwards Curves /// on the specified curve - #[cfg(not(feature = "fips"))] fn ed_from_curve(nid: Nid) -> Result { let pkey_ctx = unsafe { EvpPkeyCtx::from_ptr(cvt_p(boring_sys::EVP_PKEY_CTX_new_id( @@ -97,24 +105,18 @@ impl KeyExchange { /// Decodes a SPKI public key to it's raw public key component fn raw_public_key(pkey: &PKeyRef) -> Result, ErrorStack> { - if pkey.id() == Id::EC { - let ec_key = pkey.ec_key()?; - let mut bn_ctx = boring::bn::BigNumContext::new()?; + let spki = pkey.public_key_to_der()?; - return ec_key.public_key().to_bytes( - ec_key.group(), - boring::ec::PointConversionForm::UNCOMPRESSED, - &mut bn_ctx, - ); - } + // parse the key + let pkey = spki::SubjectPublicKeyInfoRef::from_der(spki.as_ref()) + .expect("failed parsing spki bytes"); - let mut output = vec![0u8; pkey.raw_public_key_len()?]; - let used_len = { - let used = pkey.raw_public_key(&mut output)?; - used.len() - }; - output.truncate(used_len); - Ok(output) + // return the raw public key as a new vec + Ok(Vec::from( + pkey.subject_public_key + .as_bytes() + .expect("failed getting raw spki bytes"), + )) } /// Derives a shared secret using the peer's raw public key @@ -127,8 +129,8 @@ impl KeyExchange { crate::verify::ec::create_public_key(group, point.as_ref())? } - #[cfg(not(feature = "fips"))] DhKeyType::ED(nid) => crate::verify::ed::public_key(peer_pub_key, Nid::from_raw(*nid))?, + _ => unimplemented!(), }; let mut deriver = boring::derive::Deriver::new(&self.own_key)?; @@ -145,7 +147,7 @@ impl crypto::ActiveKeyExchange for KeyExchange { peer_pub_key: &[u8], ) -> Result { self.diffie_hellman(peer_pub_key) - .map(crypto::SharedSecret::from) + .map(|x| crypto::SharedSecret::from(x.as_slice())) .map_err(|e| { log_and_map( "ex::KeyExchange::diffie_hellman", @@ -161,11 +163,12 @@ impl crypto::ActiveKeyExchange for KeyExchange { fn group(&self) -> rustls::NamedGroup { match self.key_type { - #[cfg(not(feature = "fips"))] DhKeyType::ED(boring_sys::NID_X25519) => rustls::NamedGroup::X25519, + DhKeyType::ED(boring_sys::NID_X448) => rustls::NamedGroup::X448, DhKeyType::EC((_, boring_sys::NID_X9_62_prime256v1)) => rustls::NamedGroup::secp256r1, DhKeyType::EC((_, boring_sys::NID_secp384r1)) => rustls::NamedGroup::secp384r1, - _ => unreachable!("unsupported key type"), + DhKeyType::EC((_, boring_sys::NID_secp521r1)) => rustls::NamedGroup::secp521r1, + _ => unimplemented!(), } } } @@ -187,7 +190,6 @@ mod tests { } #[test] - #[cfg(not(feature = "fips"))] fn test_derive_ed() { let alice = Box::new(KeyExchange::with_x25519().unwrap()); let bob = KeyExchange::with_x25519().unwrap(); diff --git a/boring-rustls-provider/src/kx/mod.rs b/boring-rustls-provider/src/kx/mod.rs deleted file mode 100644 index c8c0e2b..0000000 --- a/boring-rustls-provider/src/kx/mod.rs +++ /dev/null @@ -1,79 +0,0 @@ -use rustls::crypto::{self, ActiveKeyExchange}; - -use crate::helper::log_and_map; - -mod ex; -#[cfg(feature = "mlkem")] -mod pq; -#[cfg(feature = "mlkem")] -pub use pq::X25519MlKem768; - -/// Key type discriminant used by [`ex::KeyExchange`] to select the -/// appropriate peer key parsing and DH derivation logic. -enum DhKeyType { - EC((boring::ec::EcGroup, i32)), - #[cfg(not(feature = "fips"))] - ED(i32), -} - -/// A X25519-based key exchange -#[cfg(not(feature = "fips"))] -#[derive(Debug)] -pub struct X25519; - -#[cfg(not(feature = "fips"))] -impl crypto::SupportedKxGroup for X25519 { - fn start(&self) -> Result, rustls::Error> { - Ok(Box::new(ex::KeyExchange::with_x25519().map_err(|e| { - log_and_map("X25519.start", e, crypto::GetRandomFailed) - })?)) - } - - fn name(&self) -> rustls::NamedGroup { - rustls::NamedGroup::X25519 - } - - fn fips(&self) -> bool { - false - } -} - -/// A secp256r1-based key exchange -#[derive(Debug)] -pub struct Secp256r1; - -impl crypto::SupportedKxGroup for Secp256r1 { - fn start(&self) -> Result, rustls::Error> { - Ok(Box::new(ex::KeyExchange::with_secp256r1().map_err( - |e| log_and_map("Secp256r1.start", e, crypto::GetRandomFailed), - )?)) - } - - fn name(&self) -> rustls::NamedGroup { - rustls::NamedGroup::secp256r1 - } - - fn fips(&self) -> bool { - cfg!(feature = "fips") - } -} - -/// A secp384r1-based key exchange -#[derive(Debug)] -pub struct Secp384r1; - -impl crypto::SupportedKxGroup for Secp384r1 { - fn start(&self) -> Result, rustls::Error> { - Ok(Box::new(ex::KeyExchange::with_secp384r1().map_err( - |e| log_and_map("Secp384r1.start", e, crypto::GetRandomFailed), - )?)) - } - - fn name(&self) -> rustls::NamedGroup { - rustls::NamedGroup::secp384r1 - } - - fn fips(&self) -> bool { - cfg!(feature = "fips") - } -} diff --git a/boring-rustls-provider/src/kx/pq.rs b/boring-rustls-provider/src/kx/pq.rs deleted file mode 100644 index f5c91fe..0000000 --- a/boring-rustls-provider/src/kx/pq.rs +++ /dev/null @@ -1,364 +0,0 @@ -//! X25519MLKEM768 post-quantum hybrid key exchange. -//! -//! Implements the X25519MLKEM768 hybrid key agreement per -//! `draft-ietf-tls-ecdhe-mlkem-00`. Composes ML-KEM-768 (FIPS 203) -//! with X25519, with the ML-KEM component first in all wire encodings. -//! -//! Wire format: -//! - Client key share: `mlkem_pk(1184) || x25519_pk(32)` = 1216 bytes -//! - Server key share: `mlkem_ct(1088) || x25519_pk(32)` = 1120 bytes -//! - Shared secret: `mlkem_ss(32) || x25519_ss(32)` = 64 bytes - -use boring::mlkem::{Algorithm, MlKemPrivateKey, MlKemPublicKey}; -use rustls::crypto::{self, CompletedKeyExchange, SharedSecret}; -use rustls::{Error, NamedGroup, ProtocolVersion}; -use zeroize::Zeroizing; - -const MLKEM768_PUBLIC_KEY_BYTES: usize = 1184; -const MLKEM768_CIPHERTEXT_BYTES: usize = 1088; -const X25519_PUBLIC_KEY_BYTES: usize = 32; -const X25519_PRIVATE_KEY_BYTES: usize = 32; -const X25519_SHARED_SECRET_BYTES: usize = 32; - -const CLIENT_SHARE_LEN: usize = MLKEM768_PUBLIC_KEY_BYTES + X25519_PUBLIC_KEY_BYTES; // 1216 -const SERVER_SHARE_LEN: usize = MLKEM768_CIPHERTEXT_BYTES + X25519_PUBLIC_KEY_BYTES; // 1120 - -/// X25519MLKEM768 post-quantum hybrid key exchange group. -#[derive(Debug)] -pub struct X25519MlKem768; - -impl crypto::SupportedKxGroup for X25519MlKem768 { - /// Client-side: generate ML-KEM-768 + X25519 keypairs. - fn start(&self) -> Result, Error> { - let (mlkem_pub, mlkem_priv) = - MlKemPrivateKey::generate(Algorithm::MlKem768).map_err(|e| { - crate::helper::log_and_map( - "X25519MlKem768::start mlkem generate", - e, - crypto::GetRandomFailed, - ) - })?; - - let mut x25519_pub = [0u8; X25519_PUBLIC_KEY_BYTES]; - let mut x25519_priv = Zeroizing::new([0u8; X25519_PRIVATE_KEY_BYTES]); - // SAFETY: X25519_keypair writes exactly 32 bytes to each output buffer. - unsafe { - boring_sys::X25519_keypair(x25519_pub.as_mut_ptr(), x25519_priv.as_mut_ptr()); - } - - // Wire format: mlkem_pk || x25519_pk - let mut pub_key = Vec::with_capacity(CLIENT_SHARE_LEN); - pub_key.extend_from_slice(mlkem_pub.as_bytes()); - pub_key.extend_from_slice(&x25519_pub); - - Ok(Box::new(ActiveX25519MlKem768 { - mlkem_priv, - x25519_priv, - x25519_pub, - pub_key, - })) - } - - /// Server-side: one-shot encapsulate + DH. - /// - /// Must be overridden for KEMs because the server's output (ciphertext) - /// depends on the client's input (encapsulation key). - fn start_and_complete(&self, client_share: &[u8]) -> Result { - if client_share.len() != CLIENT_SHARE_LEN { - return Err(Error::PeerMisbehaved( - rustls::PeerMisbehaved::InvalidKeyShare, - )); - } - - // Split client share: mlkem_pk(1184) || x25519_pk(32) - let (client_mlkem_pk_bytes, client_x25519_pk) = - client_share.split_at(MLKEM768_PUBLIC_KEY_BYTES); - - // ML-KEM encapsulate - let client_mlkem_pk = - MlKemPublicKey::from_slice(Algorithm::MlKem768, client_mlkem_pk_bytes).map_err( - |e| { - crate::helper::log_and_map( - "X25519MlKem768::start_and_complete mlkem parse", - e, - Error::PeerMisbehaved(rustls::PeerMisbehaved::InvalidKeyShare), - ) - }, - )?; - - let (mlkem_ct, mlkem_ss) = client_mlkem_pk.encapsulate().map_err(|e| { - crate::helper::log_and_map( - "X25519MlKem768::start_and_complete mlkem encap", - e, - Error::PeerMisbehaved(rustls::PeerMisbehaved::InvalidKeyShare), - ) - })?; - let mlkem_ss = Zeroizing::new(mlkem_ss); - - // X25519 key exchange - let mut x25519_server_pub = [0u8; X25519_PUBLIC_KEY_BYTES]; - let mut x25519_server_priv = Zeroizing::new([0u8; X25519_PRIVATE_KEY_BYTES]); - let mut x25519_ss = Zeroizing::new([0u8; X25519_SHARED_SECRET_BYTES]); - - // SAFETY: X25519_keypair writes exactly 32 bytes to each buffer. - // X25519 returns 1 on success, 0 on failure (e.g., low-order point). - unsafe { - boring_sys::X25519_keypair( - x25519_server_pub.as_mut_ptr(), - x25519_server_priv.as_mut_ptr(), - ); - let rc = boring_sys::X25519( - x25519_ss.as_mut_ptr(), - x25519_server_priv.as_ptr(), - client_x25519_pk.as_ptr(), - ); - if rc != 1 { - return Err(Error::PeerMisbehaved( - rustls::PeerMisbehaved::InvalidKeyShare, - )); - } - } - - // Server share: mlkem_ct(1088) || x25519_pk(32) - let mut server_share = Vec::with_capacity(SERVER_SHARE_LEN); - server_share.extend_from_slice(&mlkem_ct); - server_share.extend_from_slice(&x25519_server_pub); - - // Shared secret: mlkem_ss(32) || x25519_ss(32) - let mut secret = Vec::with_capacity(64); - secret.extend_from_slice(&mlkem_ss[..]); - secret.extend_from_slice(&x25519_ss[..]); - - Ok(CompletedKeyExchange { - group: self.name(), - pub_key: server_share, - secret: SharedSecret::from(secret), - }) - } - - fn name(&self) -> NamedGroup { - NamedGroup::X25519MLKEM768 - } - - fn fips(&self) -> bool { - cfg!(feature = "fips") - } - - fn usable_for_version(&self, version: ProtocolVersion) -> bool { - version == ProtocolVersion::TLSv1_3 - } -} - -/// Client-side active hybrid key exchange state. -/// -/// Holds the ML-KEM private key and X25519 private key generated -/// during [`X25519MlKem768::start`], waiting for the server's response. -struct ActiveX25519MlKem768 { - mlkem_priv: MlKemPrivateKey, - x25519_priv: Zeroizing<[u8; X25519_PRIVATE_KEY_BYTES]>, - x25519_pub: [u8; X25519_PUBLIC_KEY_BYTES], - pub_key: Vec, -} - -impl crypto::ActiveKeyExchange for ActiveX25519MlKem768 { - /// Client-side: decapsulate ML-KEM + derive X25519. - fn complete(self: Box, server_share: &[u8]) -> Result { - if server_share.len() != SERVER_SHARE_LEN { - return Err(Error::PeerMisbehaved( - rustls::PeerMisbehaved::InvalidKeyShare, - )); - } - - // Split server share: mlkem_ct(1088) || x25519_pk(32) - let (mlkem_ct, server_x25519_pk) = server_share.split_at(MLKEM768_CIPHERTEXT_BYTES); - - // ML-KEM decapsulate - let mlkem_ss = self.mlkem_priv.decapsulate(mlkem_ct).map_err(|e| { - crate::helper::log_and_map( - "ActiveX25519MlKem768::complete mlkem decap", - e, - Error::PeerMisbehaved(rustls::PeerMisbehaved::InvalidKeyShare), - ) - })?; - let mlkem_ss = Zeroizing::new(mlkem_ss); - - // X25519 derive - let mut x25519_ss = Zeroizing::new([0u8; X25519_SHARED_SECRET_BYTES]); - // SAFETY: X25519 reads 32 bytes from each input and writes 32 to output. - unsafe { - let rc = boring_sys::X25519( - x25519_ss.as_mut_ptr(), - self.x25519_priv.as_ptr(), - server_x25519_pk.as_ptr(), - ); - if rc != 1 { - return Err(Error::PeerMisbehaved( - rustls::PeerMisbehaved::InvalidKeyShare, - )); - } - } - - // Shared secret: mlkem_ss(32) || x25519_ss(32) - let mut secret = Vec::with_capacity(64); - secret.extend_from_slice(&mlkem_ss[..]); - secret.extend_from_slice(&x25519_ss[..]); - - Ok(SharedSecret::from(secret)) - } - - fn hybrid_component(&self) -> Option<(NamedGroup, &[u8])> { - Some((NamedGroup::X25519, &self.x25519_pub)) - } - - fn complete_hybrid_component( - self: Box, - peer_pub_key: &[u8], - ) -> Result { - if peer_pub_key.len() != X25519_PUBLIC_KEY_BYTES { - return Err(Error::PeerMisbehaved( - rustls::PeerMisbehaved::InvalidKeyShare, - )); - } - - let mut x25519_ss = Zeroizing::new([0u8; X25519_SHARED_SECRET_BYTES]); - - // SAFETY: X25519 reads 32 bytes from each input and writes 32 to output. - unsafe { - let rc = boring_sys::X25519( - x25519_ss.as_mut_ptr(), - self.x25519_priv.as_ptr(), - peer_pub_key.as_ptr(), - ); - if rc != 1 { - return Err(Error::PeerMisbehaved( - rustls::PeerMisbehaved::InvalidKeyShare, - )); - } - } - - Ok(SharedSecret::from(Vec::from(&x25519_ss[..]))) - } - - fn pub_key(&self) -> &[u8] { - &self.pub_key - } - - fn group(&self) -> NamedGroup { - NamedGroup::X25519MLKEM768 - } -} - -#[cfg(test)] -mod tests { - use super::*; - use rustls::crypto::SupportedKxGroup; - - #[test] - fn hybrid_round_trip() { - let group = X25519MlKem768; - - // Client generates keypair - let client = group.start().unwrap(); - assert_eq!(client.pub_key().len(), CLIENT_SHARE_LEN); - assert_eq!(client.group(), NamedGroup::X25519MLKEM768); - - // Server encapsulates + derives - let server = group.start_and_complete(client.pub_key()).unwrap(); - assert_eq!(server.pub_key.len(), SERVER_SHARE_LEN); - assert_eq!(server.group, NamedGroup::X25519MLKEM768); - - // Client decapsulates + derives - let client_secret = client.complete(&server.pub_key).unwrap(); - - // Shared secrets must match - assert_eq!( - client_secret.secret_bytes(), - server.secret.secret_bytes(), - "client and server shared secrets differ" - ); - assert_eq!(client_secret.secret_bytes().len(), 64); - } - - #[test] - fn rejects_invalid_client_share() { - let group = X25519MlKem768; - let result = group.start_and_complete(&[0u8; 100]); - assert!(result.is_err()); - } - - #[test] - fn rejects_invalid_server_share() { - let group = X25519MlKem768; - let client = group.start().unwrap(); - let result = client.complete(&[0u8; 100]); - assert!(result.is_err()); - } - - #[test] - fn exposes_x25519_hybrid_component() { - let group = X25519MlKem768; - let client = group.start().unwrap(); - - let (component_group, component_pub_key) = client - .hybrid_component() - .expect("hybrid component should be available"); - - assert_eq!(component_group, NamedGroup::X25519); - assert_eq!(component_pub_key.len(), X25519_PUBLIC_KEY_BYTES); - assert_eq!( - component_pub_key, - &client.pub_key()[MLKEM768_PUBLIC_KEY_BYTES..CLIENT_SHARE_LEN] - ); - } - - #[test] - fn complete_hybrid_component_matches_x25519() { - let group = X25519MlKem768; - let client = group.start().unwrap(); - let (_, client_x25519_pub) = client - .hybrid_component() - .expect("hybrid component should be available"); - - let mut server_x25519_pub = [0u8; X25519_PUBLIC_KEY_BYTES]; - let mut server_x25519_priv = [0u8; X25519_PRIVATE_KEY_BYTES]; - let mut server_x25519_ss = [0u8; X25519_SHARED_SECRET_BYTES]; - - // SAFETY: X25519_keypair writes 32-byte buffers. X25519 returns 1 on success. - unsafe { - boring_sys::X25519_keypair( - server_x25519_pub.as_mut_ptr(), - server_x25519_priv.as_mut_ptr(), - ); - let rc = boring_sys::X25519( - server_x25519_ss.as_mut_ptr(), - server_x25519_priv.as_ptr(), - client_x25519_pub.as_ptr(), - ); - assert_eq!(rc, 1); - } - - let client_secret = client - .complete_hybrid_component(&server_x25519_pub) - .unwrap(); - assert_eq!(client_secret.secret_bytes(), &server_x25519_ss); - } - - #[test] - fn usable_only_for_tls13() { - let group = X25519MlKem768; - assert!(group.usable_for_version(ProtocolVersion::TLSv1_3)); - assert!(!group.usable_for_version(ProtocolVersion::TLSv1_2)); - } - - #[test] - #[cfg(feature = "fips")] - fn reports_fips() { - assert!(X25519MlKem768.fips()); - } - - #[test] - #[cfg(not(feature = "fips"))] - fn reports_non_fips() { - assert!(!X25519MlKem768.fips()); - } -} diff --git a/boring-rustls-provider/src/lib.rs b/boring-rustls-provider/src/lib.rs index 89071cf..0183125 100644 --- a/boring-rustls-provider/src/lib.rs +++ b/boring-rustls-provider/src/lib.rs @@ -1,8 +1,6 @@ use std::sync::Arc; use helper::log_and_map; -#[cfg(all(feature = "fips", feature = "log"))] -use log::warn; use rustls::{ crypto::{CryptoProvider, GetRandomFailed, SupportedKxGroup}, SupportedCipherSuite, @@ -14,7 +12,7 @@ mod hash; mod helper; mod hkdf; mod hmac; -pub mod kx; +mod kx; #[cfg(feature = "tls12")] mod prf; pub mod sign; @@ -23,80 +21,60 @@ pub mod tls12; pub mod tls13; pub mod verify; -pub fn provider() -> CryptoProvider { - #[cfg(feature = "fips")] - { - provider_with_ciphers(ALL_FIPS_CIPHER_SUITES.to_vec()) - } - #[cfg(not(feature = "fips"))] - { - provider_with_ciphers(ALL_CIPHER_SUITES.to_vec()) - } -} - -pub fn provider_with_ciphers(ciphers: Vec) -> CryptoProvider { - #[cfg(feature = "fips")] - let ciphers = { - let original_len = ciphers.len(); - let filtered = ciphers - .into_iter() - .filter(|suite| ALL_FIPS_CIPHER_SUITES.contains(suite)) - .collect::>(); - - if filtered.len() != original_len { - #[cfg(feature = "log")] - warn!( - "filtered {} non-FIPS cipher suite(s) from provider_with_ciphers", - original_len - filtered.len() - ); - } - - filtered - }; - - CryptoProvider { - cipher_suites: ciphers, - #[cfg(feature = "fips")] - kx_groups: ALL_FIPS_KX_GROUPS.to_vec(), - #[cfg(not(feature = "fips"))] - kx_groups: ALL_KX_GROUPS.to_vec(), - #[cfg(feature = "fips")] - signature_verification_algorithms: verify::ALL_FIPS_ALGORITHMS, - #[cfg(not(feature = "fips"))] - signature_verification_algorithms: verify::ALL_ALGORITHMS, - secure_random: &Provider, - key_provider: &Provider, - } -} +/// The boringssl-based Rustls Crypto provider +pub static PROVIDER: &'static dyn CryptoProvider = &Provider; #[derive(Debug)] struct Provider; -impl rustls::crypto::SecureRandom for Provider { - fn fill(&self, bytes: &mut [u8]) -> Result<(), rustls::crypto::GetRandomFailed> { +impl CryptoProvider for Provider { + fn fill_random(&self, bytes: &mut [u8]) -> Result<(), GetRandomFailed> { boring::rand::rand_bytes(bytes).map_err(|e| log_and_map("rand_bytes", e, GetRandomFailed)) } - fn fips(&self) -> bool { - cfg!(feature = "fips") + fn default_cipher_suites(&self) -> &'static [SupportedCipherSuite] { + #[cfg(feature = "fips-only")] + { + ALL_FIPS_CIPHER_SUITES + } + #[cfg(not(feature = "fips-only"))] + { + ALL_CIPHER_SUITES + } + } + + fn default_kx_groups(&self) -> &'static [&'static dyn SupportedKxGroup] { + #[cfg(feature = "fips-only")] + { + ALL_FIPS_KX_GROUPS + } + #[cfg(not(feature = "fips-only"))] + { + ALL_KX_GROUPS + } } -} -impl rustls::crypto::KeyProvider for Provider { fn load_private_key( &self, key_der: PrivateKeyDer<'static>, - ) -> Result, rustls::Error> { + ) -> Result, rustls::Error> { sign::BoringPrivateKey::try_from(key_der).map(|x| Arc::new(x) as _) } - fn fips(&self) -> bool { - cfg!(feature = "fips") + fn signature_verification_algorithms(&self) -> rustls::WebPkiSupportedAlgorithms { + #[cfg(feature = "fips-only")] + { + verify::ALL_FIPS_ALGORITHMS + } + #[cfg(not(feature = "fips-only"))] + { + verify::ALL_ALGORITHMS + } } } -#[cfg(feature = "fips")] -pub static ALL_FIPS_CIPHER_SUITES: &[SupportedCipherSuite] = &[ +#[allow(unused)] +static ALL_FIPS_CIPHER_SUITES: &[SupportedCipherSuite] = &[ SupportedCipherSuite::Tls13(&tls13::AES_256_GCM_SHA384), SupportedCipherSuite::Tls13(&tls13::AES_128_GCM_SHA256), #[cfg(feature = "tls12")] @@ -109,8 +87,8 @@ pub static ALL_FIPS_CIPHER_SUITES: &[SupportedCipherSuite] = &[ SupportedCipherSuite::Tls12(&tls12::ECDHE_RSA_AES128_GCM_SHA256), ]; -#[cfg(not(feature = "fips"))] -pub static ALL_CIPHER_SUITES: &[SupportedCipherSuite] = &[ +#[allow(unused)] +static ALL_CIPHER_SUITES: &[SupportedCipherSuite] = &[ SupportedCipherSuite::Tls13(&tls13::CHACHA20_POLY1305_SHA256), SupportedCipherSuite::Tls13(&tls13::AES_256_GCM_SHA384), SupportedCipherSuite::Tls13(&tls13::AES_128_GCM_SHA256), @@ -128,31 +106,26 @@ pub static ALL_CIPHER_SUITES: &[SupportedCipherSuite] = &[ SupportedCipherSuite::Tls12(&tls12::ECDHE_RSA_AES128_GCM_SHA256), ]; -/// Allowed KX groups for FIPS per [SP 800-52r2](https://doi.org/10.6028/NIST.SP.800-52r2), -/// aligned with boring's `fips202205` compliance policy. +/// Allowed KX curves for FIPS are recommended +/// in [NIST SP 800-186](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-186.pdf) /// -/// The `fips` feature implies `mlkem`, so X25519MLKEM768 is always -/// available and preferred in FIPS mode. -#[cfg(feature = "fips")] -pub static ALL_FIPS_KX_GROUPS: &[&dyn SupportedKxGroup] = &[ - &kx::X25519MlKem768 as _, // PQ hybrid preferred - &kx::Secp256r1 as _, // P-256 - &kx::Secp384r1 as _, // P-384 +/// See Sec. 3.1.2 Table 2 +/// Ordered in decending order of security strength +#[allow(unused)] +pub const ALL_FIPS_KX_GROUPS: &[&dyn SupportedKxGroup] = &[ + &kx::Secp521r1 as _, // P-521 in FIPS lingo + &kx::X448 as _, // Curve448 in FIPS lingo + &kx::Secp384r1 as _, // P-384 in FIPS lingo + &kx::X25519 as _, // Curve25519 in FIPS lingo + &kx::Secp256r1 as _, // P-256 in FIPS lingo ]; -/// All supported KX groups, ordered by preference. -/// -/// Matches boring's default supported group list exactly: -/// X25519MLKEM768 (when mlkem enabled), X25519, P-256, P-384. -#[cfg(all(not(feature = "fips"), feature = "mlkem"))] -pub static ALL_KX_GROUPS: &[&dyn SupportedKxGroup] = &[ - &kx::X25519MlKem768 as _, // PQ hybrid preferred +#[allow(unused)] +pub const ALL_KX_GROUPS: &[&dyn SupportedKxGroup] = &[ &kx::X25519 as _, + &kx::X448 as _, &kx::Secp256r1 as _, &kx::Secp384r1 as _, + &kx::Secp521r1 as _, + &kx::FfDHe2048 as _, ]; - -/// See [`ALL_KX_GROUPS`] (mlkem variant). -#[cfg(not(any(feature = "fips", feature = "mlkem")))] -pub static ALL_KX_GROUPS: &[&dyn SupportedKxGroup] = - &[&kx::X25519 as _, &kx::Secp256r1 as _, &kx::Secp384r1 as _]; diff --git a/boring-rustls-provider/src/prf.rs b/boring-rustls-provider/src/prf.rs index ca5fd35..958992e 100644 --- a/boring-rustls-provider/src/prf.rs +++ b/boring-rustls-provider/src/prf.rs @@ -10,7 +10,7 @@ pub struct PrfTls1WithDigest(pub boring::nid::Nid); impl crypto::tls12::Prf for PrfTls1WithDigest { fn for_key_exchange( &self, - output: &mut [u8; 48], + output: &mut [u8], kx: Box, peer_pub_key: &[u8], label: &[u8], @@ -29,10 +29,6 @@ impl crypto::tls12::Prf for PrfTls1WithDigest { let digest = boring::hash::MessageDigest::from_nid(self.0).expect("failed getting digest"); prf(digest, output, secret, label, seed).expect("failed calculating prf") } - - fn fips(&self) -> bool { - cfg!(feature = "fips") - } } fn prf( diff --git a/boring-rustls-provider/src/sign.rs b/boring-rustls-provider/src/sign.rs index 819b49a..7817b81 100644 --- a/boring-rustls-provider/src/sign.rs +++ b/boring-rustls-provider/src/sign.rs @@ -2,7 +2,6 @@ use std::sync::Arc; use boring::{ hash::MessageDigest, - nid::Nid, pkey::{Id, PKeyRef, Private}, rsa::Padding, sign::{RsaPssSaltlen, Signer}, @@ -21,28 +20,16 @@ const ALL_RSA_SCHEMES: &[SignatureScheme] = &[ SignatureScheme::RSA_PKCS1_SHA256, ]; -const EC_P256_SCHEMES: &[SignatureScheme] = &[SignatureScheme::ECDSA_NISTP256_SHA256]; -const EC_P384_SCHEMES: &[SignatureScheme] = &[SignatureScheme::ECDSA_NISTP384_SHA384]; -const EC_P521_SCHEMES: &[SignatureScheme] = &[SignatureScheme::ECDSA_NISTP521_SHA512]; +const ALL_EC_SCHEMES: &[SignatureScheme] = &[ + SignatureScheme::ECDSA_NISTP256_SHA256, + SignatureScheme::ECDSA_NISTP384_SHA384, + SignatureScheme::ECDSA_NISTP521_SHA512, +]; -#[derive(Debug, Clone, Copy)] -enum EcCurve { - P256, - P384, - P521, -} - -#[derive(Debug, Clone, Copy)] -enum KeyKind { - Rsa, - Ec(EcCurve), - Ed25519, - Ed448, -} - -/// An abstraction over a boringssl private key used for signing. +/// An abstraction over a boringssl private key +/// used for signing #[derive(Debug)] -pub struct BoringPrivateKey(Arc>, KeyKind); +pub struct BoringPrivateKey(Arc>, rustls::SignatureAlgorithm); impl TryFrom> for BoringPrivateKey { type Error = rustls::Error; @@ -50,83 +37,25 @@ impl TryFrom> for BoringPrivateKey { fn try_from(value: PrivateKeyDer<'static>) -> Result { let pkey = match value { PrivateKeyDer::Pkcs8(der) => { - boring::pkey::PKey::private_key_from_pkcs8(der.secret_pkcs8_der()).map_err(|e| { - log_and_map( - "private_key_from_pkcs8", - e, - rustls::Error::General("failed loading private key".into()), - ) - }) + boring::pkey::PKey::private_key_from_pkcs8(der.secret_pkcs8_der()) + .map_err(|e| log_and_map("private_key_from_pkcs8", e, ())) } PrivateKeyDer::Pkcs1(der) => { - boring::pkey::PKey::private_key_from_der(der.secret_pkcs1_der()).map_err(|e| { - log_and_map( - "private_key_from_der_pkcs1", - e, - rustls::Error::General("failed loading private key".into()), - ) - }) + boring::pkey::PKey::private_key_from_der(der.secret_pkcs1_der()) + .map_err(|e| log_and_map("private_key_from_der", e, ())) } - PrivateKeyDer::Sec1(der) => { - boring::pkey::PKey::private_key_from_der(der.secret_sec1_der()).map_err(|e| { - log_and_map( - "private_key_from_der_sec1", - e, - rustls::Error::General("failed loading private key".into()), - ) - }) - } - _ => { - return Err(rustls::Error::General( - "unsupported private key encoding".into(), - )); - } - }?; + _ => Err(()), + } + .map_err(|_| rustls::Error::General("failed loading private key".into()))?; - let kind = match pkey.id() { - Id::RSA => KeyKind::Rsa, - Id::EC => { - let ec_key = pkey.ec_key().map_err(|e| { - log_and_map( - "ec_key", - e, - rustls::Error::General("failed loading EC private key".into()), - ) - })?; - - let curve_nid = ec_key.group().curve_name().ok_or_else(|| { - rustls::Error::General("unsupported EC key without named curve".into()) - })?; - - let curve = match curve_nid { - Nid::X9_62_PRIME256V1 => EcCurve::P256, - Nid::SECP384R1 => EcCurve::P384, - Nid::SECP521R1 => EcCurve::P521, - _ => { - return Err(rustls::Error::General( - "unsupported EC private key curve".into(), - )); - } - }; - - KeyKind::Ec(curve) - } - Id::ED25519 => KeyKind::Ed25519, - Id::ED448 => KeyKind::Ed448, + let sig = match pkey.id() { + Id::RSA => rustls::SignatureAlgorithm::RSA, + Id::EC => rustls::SignatureAlgorithm::ECDSA, + Id::ED25519 => rustls::SignatureAlgorithm::ED25519, + Id::ED448 => rustls::SignatureAlgorithm::ED448, _ => return Err(rustls::Error::General("unsupported key format".into())), }; - - #[cfg(feature = "fips")] - match kind { - KeyKind::Rsa | KeyKind::Ec(EcCurve::P256 | EcCurve::P384) => {} - KeyKind::Ec(EcCurve::P521) | KeyKind::Ed25519 | KeyKind::Ed448 => { - return Err(rustls::Error::General( - "key type is not allowed in FIPS mode".into(), - )); - } - } - - Ok(Self(Arc::new(pkey), kind)) + Ok(Self(Arc::new(pkey), sig)) } } @@ -134,105 +63,75 @@ fn rsa_signer_from_params( key: &PKeyRef, digest: MessageDigest, padding: Padding, -) -> Result, rustls::Error> { - let mut signer = Signer::new(digest, key).map_err(|e| { - log_and_map( - "Signer::new", - e, - rustls::Error::General("failed preparing signer".into()), - ) - })?; - - signer.set_rsa_padding(padding).map_err(|e| { - log_and_map( - "set_rsa_padding", - e, - rustls::Error::General("failed preparing signer".into()), - ) - })?; - +) -> Signer { + let mut signer = Signer::new(digest, key).expect("failed getting signer"); + signer + .set_rsa_padding(padding) + .expect("failed setting padding"); if padding == Padding::PKCS1_PSS { signer .set_rsa_pss_saltlen(RsaPssSaltlen::DIGEST_LENGTH) - .map_err(|e| { - log_and_map( - "set_rsa_pss_saltlen", - e, - rustls::Error::General("failed preparing signer".into()), - ) - })?; - - signer.set_rsa_mgf1_md(digest).map_err(|e| { - log_and_map( - "set_rsa_mgf1_md", - e, - rustls::Error::General("failed preparing signer".into()), - ) - })?; + .expect("failed setting rsa_pss salt lengths"); + signer + .set_rsa_mgf1_md(digest) + .expect("failed setting mgf1 digest"); } - Ok(signer) + signer } -fn ec_signer_from_params( - key: &PKeyRef, - digest: MessageDigest, -) -> Result, rustls::Error> { - Signer::new(digest, key).map_err(|e| { - log_and_map( - "Signer::new", - e, - rustls::Error::General("failed preparing signer".into()), - ) - }) +fn ec_signer_from_params(key: &PKeyRef, digest: MessageDigest) -> Signer { + let signer = Signer::new(digest, key).expect("failed getting signer"); + signer } +impl BoringPrivateKey {} + impl SigningKey for BoringPrivateKey { fn choose_scheme( &self, offered: &[rustls::SignatureScheme], ) -> Option> { - let scheme = match self.1 { - KeyKind::Rsa => ALL_RSA_SCHEMES + match self.1 { + rustls::SignatureAlgorithm::RSA => ALL_RSA_SCHEMES .iter() - .find(|scheme| offered.contains(scheme)), - KeyKind::Ec(EcCurve::P256) => EC_P256_SCHEMES + .find(|scheme| offered.contains(scheme)) + .map(|&scheme| Box::new(BoringSigner(self.0.clone(), scheme)) as _), + rustls::SignatureAlgorithm::ECDSA => ALL_EC_SCHEMES .iter() - .find(|scheme| offered.contains(scheme)), - KeyKind::Ec(EcCurve::P384) => EC_P384_SCHEMES - .iter() - .find(|scheme| offered.contains(scheme)), - KeyKind::Ec(EcCurve::P521) => EC_P521_SCHEMES - .iter() - .find(|scheme| offered.contains(scheme)), - KeyKind::Ed25519 if offered.contains(&rustls::SignatureScheme::ED25519) => { - Some(&rustls::SignatureScheme::ED25519) + .find(|scheme| offered.contains(scheme)) + .map(|&scheme| Box::new(BoringSigner(self.0.clone(), scheme)) as _), + rustls::SignatureAlgorithm::ED25519 + if offered.contains(&rustls::SignatureScheme::ED25519) => + { + Some(Box::new(BoringSigner( + self.0.clone(), + rustls::SignatureScheme::ED25519, + ))) } - KeyKind::Ed448 if offered.contains(&rustls::SignatureScheme::ED448) => { - Some(&rustls::SignatureScheme::ED448) + rustls::SignatureAlgorithm::ED448 + if offered.contains(&rustls::SignatureScheme::ED448) => + { + Some(Box::new(BoringSigner( + self.0.clone(), + rustls::SignatureScheme::ED448, + ))) } _ => None, - }?; - - Some(Box::new(BoringSigner(self.0.clone(), *scheme))) + } } fn algorithm(&self) -> rustls::SignatureAlgorithm { - match self.1 { - KeyKind::Rsa => rustls::SignatureAlgorithm::RSA, - KeyKind::Ec(_) => rustls::SignatureAlgorithm::ECDSA, - KeyKind::Ed25519 => rustls::SignatureAlgorithm::ED25519, - KeyKind::Ed448 => rustls::SignatureAlgorithm::ED448, - } + self.1 } } -/// A boringssl-based Signer. +/// A boringssl-based Signer #[derive(Debug)] pub struct BoringSigner(Arc>, rustls::SignatureScheme); impl BoringSigner { - fn get_signer(&self) -> Result, rustls::Error> { + fn get_signer(&self) -> Signer { match self.1 { SignatureScheme::RSA_PKCS1_SHA256 => { rsa_signer_from_params(self.0.as_ref(), MessageDigest::sha256(), Padding::PKCS1) @@ -243,6 +142,7 @@ impl BoringSigner { SignatureScheme::RSA_PKCS1_SHA512 => { rsa_signer_from_params(self.0.as_ref(), MessageDigest::sha512(), Padding::PKCS1) } + SignatureScheme::RSA_PSS_SHA256 => { rsa_signer_from_params(self.0.as_ref(), MessageDigest::sha256(), Padding::PKCS1_PSS) } @@ -252,6 +152,7 @@ impl BoringSigner { SignatureScheme::RSA_PSS_SHA512 => { rsa_signer_from_params(self.0.as_ref(), MessageDigest::sha512(), Padding::PKCS1_PSS) } + SignatureScheme::ECDSA_NISTP256_SHA256 => { ec_signer_from_params(self.0.as_ref(), MessageDigest::sha256()) } @@ -261,25 +162,19 @@ impl BoringSigner { SignatureScheme::ECDSA_NISTP521_SHA512 => { ec_signer_from_params(self.0.as_ref(), MessageDigest::sha512()) } + SignatureScheme::ED25519 | SignatureScheme::ED448 => { - Signer::new_without_digest(self.0.as_ref()).map_err(|e| { - log_and_map( - "Signer::new_without_digest", - e, - rustls::Error::General("failed preparing signer".into()), - ) - }) + Signer::new_without_digest(self.0.as_ref()).expect("failed getting signer") } - _ => Err(rustls::Error::General( - "unsupported signature scheme for private key".into(), - )), + + _ => unimplemented!(), } } } impl rustls::sign::Signer for BoringSigner { fn sign(&self, message: &[u8]) -> Result, rustls::Error> { - let mut signer = self.get_signer()?; + let mut signer = self.get_signer(); let max_sig_len = signer .len() .map_err(|e| log_and_map("len", e, rustls::Error::General("failed signing".into())))?; @@ -300,75 +195,3 @@ impl rustls::sign::Signer for BoringSigner { self.1 } } - -#[cfg(test)] -mod tests { - use boring::{ - ec::{EcGroup, EcKey}, - nid::Nid, - pkey::{PKey, Private}, - rsa::Rsa, - }; - use rustls::sign::SigningKey; - use rustls::{SignatureAlgorithm, SignatureScheme}; - use rustls_pki_types::{PrivateKeyDer, PrivatePkcs8KeyDer, PrivateSec1KeyDer}; - - use super::BoringPrivateKey; - - fn p256_private_key() -> PKey { - let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); - let ec_key = EcKey::generate(&group).unwrap(); - PKey::from_ec_key(ec_key).unwrap() - } - - #[test] - fn loads_sec1_ec_private_key() { - let pkey = p256_private_key(); - let sec1_der = pkey.private_key_to_der().unwrap(); - let key_der = PrivateKeyDer::Sec1(PrivateSec1KeyDer::from(sec1_der)); - - let key = BoringPrivateKey::try_from(key_der).expect("SEC1 private key should load"); - - assert_eq!(key.algorithm(), SignatureAlgorithm::ECDSA); - } - - #[test] - fn p256_key_chooses_only_p256_scheme() { - let pkey = p256_private_key(); - let pkcs8_der = pkey.private_key_to_der_pkcs8().unwrap(); - let key_der = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(pkcs8_der)); - - let key = BoringPrivateKey::try_from(key_der).expect("P-256 private key should load"); - - let offered = [ - SignatureScheme::ECDSA_NISTP384_SHA384, - SignatureScheme::ECDSA_NISTP256_SHA256, - ]; - let signer = key - .choose_scheme(&offered) - .expect("P-256 key should select P-256 scheme"); - assert_eq!(signer.scheme(), SignatureScheme::ECDSA_NISTP256_SHA256); - - assert!(key - .choose_scheme(&[SignatureScheme::ECDSA_NISTP384_SHA384]) - .is_none()); - } - - #[test] - fn rsa_key_prefers_pss_when_available() { - let rsa = Rsa::generate(2048).unwrap(); - let pkey = PKey::from_rsa(rsa).unwrap(); - let pkcs8_der = pkey.private_key_to_der_pkcs8().unwrap(); - let key_der = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(pkcs8_der)); - - let key = BoringPrivateKey::try_from(key_der).expect("RSA private key should load"); - let signer = key - .choose_scheme(&[ - SignatureScheme::RSA_PKCS1_SHA256, - SignatureScheme::RSA_PSS_SHA256, - ]) - .expect("RSA key should select an offered scheme"); - - assert_eq!(signer.scheme(), SignatureScheme::RSA_PSS_SHA256); - } -} diff --git a/boring-rustls-provider/src/tls12.rs b/boring-rustls-provider/src/tls12.rs index caef421..faf8d12 100644 --- a/boring-rustls-provider/src/tls12.rs +++ b/boring-rustls-provider/src/tls12.rs @@ -23,10 +23,11 @@ const PRF_SHA256: prf::PrfTls1WithDigest = prf::PrfTls1WithDigest(boring::nid::N const PRF_SHA384: prf::PrfTls1WithDigest = prf::PrfTls1WithDigest(boring::nid::Nid::SHA384); pub static ECDHE_ECDSA_AES128_GCM_SHA256: Tls12CipherSuite = Tls12CipherSuite { - common: rustls::crypto::CipherSuiteCommon { + common: rustls::CipherSuiteCommon { suite: rustls::CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, hash_provider: hash::SHA256, confidentiality_limit: 1 << 23, + integrity_limit: 1 << 52, }, aead_alg: &aead::Aead::::DEFAULT, prf_provider: &PRF_SHA256, @@ -35,10 +36,11 @@ pub static ECDHE_ECDSA_AES128_GCM_SHA256: Tls12CipherSuite = Tls12CipherSuite { }; pub static ECDHE_RSA_AES128_GCM_SHA256: Tls12CipherSuite = Tls12CipherSuite { - common: rustls::crypto::CipherSuiteCommon { + common: rustls::CipherSuiteCommon { suite: rustls::CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, hash_provider: hash::SHA256, confidentiality_limit: 1 << 23, + integrity_limit: 1 << 52, }, aead_alg: &aead::Aead::::DEFAULT, prf_provider: &PRF_SHA256, @@ -47,10 +49,11 @@ pub static ECDHE_RSA_AES128_GCM_SHA256: Tls12CipherSuite = Tls12CipherSuite { }; pub static ECDHE_ECDSA_AES256_GCM_SHA384: Tls12CipherSuite = Tls12CipherSuite { - common: rustls::crypto::CipherSuiteCommon { + common: rustls::CipherSuiteCommon { suite: rustls::CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, hash_provider: hash::SHA384, confidentiality_limit: 1 << 23, + integrity_limit: 1 << 52, }, aead_alg: &aead::Aead::::DEFAULT, prf_provider: &PRF_SHA384, @@ -59,10 +62,11 @@ pub static ECDHE_ECDSA_AES256_GCM_SHA384: Tls12CipherSuite = Tls12CipherSuite { }; pub static ECDHE_RSA_AES256_GCM_SHA384: Tls12CipherSuite = Tls12CipherSuite { - common: rustls::crypto::CipherSuiteCommon { + common: rustls::CipherSuiteCommon { suite: rustls::CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, hash_provider: hash::SHA384, confidentiality_limit: 1 << 23, + integrity_limit: 1 << 52, }, aead_alg: &aead::Aead::::DEFAULT, prf_provider: &PRF_SHA384, @@ -71,10 +75,11 @@ pub static ECDHE_RSA_AES256_GCM_SHA384: Tls12CipherSuite = Tls12CipherSuite { }; pub static ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256: Tls12CipherSuite = Tls12CipherSuite { - common: rustls::crypto::CipherSuiteCommon { + common: rustls::CipherSuiteCommon { suite: rustls::CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, hash_provider: hash::SHA256, confidentiality_limit: u64::MAX, + integrity_limit: 1 << 36, }, aead_alg: &aead::Aead::::DEFAULT, prf_provider: &PRF_SHA256, @@ -83,10 +88,11 @@ pub static ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256: Tls12CipherSuite = Tls12Ci }; pub static ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256: Tls12CipherSuite = Tls12CipherSuite { - common: rustls::crypto::CipherSuiteCommon { + common: rustls::CipherSuiteCommon { suite: rustls::CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, hash_provider: hash::SHA256, confidentiality_limit: u64::MAX, + integrity_limit: 1 << 36, }, aead_alg: &aead::Aead::::DEFAULT, prf_provider: &PRF_SHA256, diff --git a/boring-rustls-provider/src/tls13.rs b/boring-rustls-provider/src/tls13.rs index e2a45c8..0769e5e 100644 --- a/boring-rustls-provider/src/tls13.rs +++ b/boring-rustls-provider/src/tls13.rs @@ -3,10 +3,11 @@ use rustls::Tls13CipherSuite; use crate::{aead, hash, hkdf}; pub static AES_128_GCM_SHA256: Tls13CipherSuite = Tls13CipherSuite { - common: rustls::crypto::CipherSuiteCommon { + common: rustls::CipherSuiteCommon { suite: rustls::CipherSuite::TLS13_AES_128_GCM_SHA256, hash_provider: hash::SHA256, confidentiality_limit: 1 << 23, + integrity_limit: 1 << 52, }, hkdf_provider: &hkdf::Hkdf::::DEFAULT, aead_alg: &aead::Aead::::DEFAULT, @@ -14,10 +15,11 @@ pub static AES_128_GCM_SHA256: Tls13CipherSuite = Tls13CipherSuite { }; pub static AES_256_GCM_SHA384: Tls13CipherSuite = Tls13CipherSuite { - common: rustls::crypto::CipherSuiteCommon { + common: rustls::CipherSuiteCommon { suite: rustls::CipherSuite::TLS13_AES_256_GCM_SHA384, hash_provider: hash::SHA384, confidentiality_limit: 1 << 23, + integrity_limit: 1 << 52, }, hkdf_provider: &hkdf::Hkdf::::DEFAULT, aead_alg: &aead::Aead::::DEFAULT, @@ -25,10 +27,11 @@ pub static AES_256_GCM_SHA384: Tls13CipherSuite = Tls13CipherSuite { }; pub static CHACHA20_POLY1305_SHA256: Tls13CipherSuite = Tls13CipherSuite { - common: rustls::crypto::CipherSuiteCommon { + common: rustls::CipherSuiteCommon { suite: rustls::CipherSuite::TLS13_CHACHA20_POLY1305_SHA256, hash_provider: hash::SHA256, confidentiality_limit: u64::MAX, + integrity_limit: 1 << 36, }, hkdf_provider: &hkdf::Hkdf::::DEFAULT, diff --git a/boring-rustls-provider/src/verify.rs b/boring-rustls-provider/src/verify.rs index aa2cda2..5de9140 100644 --- a/boring-rustls-provider/src/verify.rs +++ b/boring-rustls-provider/src/verify.rs @@ -1,4 +1,4 @@ -use rustls::{crypto::WebPkiSupportedAlgorithms, SignatureScheme}; +use rustls::{SignatureScheme, WebPkiSupportedAlgorithms}; pub(crate) mod ec; pub(crate) mod ed; @@ -61,13 +61,6 @@ pub static ALL_ALGORITHMS: WebPkiSupportedAlgorithms = WebPkiSupportedAlgorithms ], }; -/// FIPS-approved signature verification algorithms per SP 800-52r2. -/// -/// Aligned with boring's `fips202205` compliance policy: -/// - RSA: PKCS#1 v1.5 and PSS with SHA-256/384/512 -/// - ECDSA: P-256 with SHA-256 and P-384 with SHA-384 only -/// (SP 800-52r2 Table 4.1: "The curve should be P-256 or P-384") -/// - No P-521, Ed25519, or Ed448 #[allow(unused)] pub static ALL_FIPS_ALGORITHMS: WebPkiSupportedAlgorithms = WebPkiSupportedAlgorithms { all: &[ @@ -79,6 +72,9 @@ pub static ALL_FIPS_ALGORITHMS: WebPkiSupportedAlgorithms = WebPkiSupportedAlgor &rsa::BoringRsaVerifier::RSA_PSS_SHA512, &ec::BoringEcVerifier::ECDSA_NISTP256_SHA256, &ec::BoringEcVerifier::ECDSA_NISTP384_SHA384, + &ec::BoringEcVerifier::ECDSA_NISTP521_SHA512, + //&ed::BoringEdVerifier::ED25519, // FIPS 186-5: requires SHA512 but boring doesn't want us to set a digest, correct? + //&ed::BoringEdVerifier::ED448, // FIPS 186-5: requires SHAKE256 but boring doesn't want us to set a digest, correct? ], mapping: &[ ( @@ -113,5 +109,11 @@ pub static ALL_FIPS_ALGORITHMS: WebPkiSupportedAlgorithms = WebPkiSupportedAlgor SignatureScheme::ECDSA_NISTP384_SHA384, &[&ec::BoringEcVerifier::ECDSA_NISTP384_SHA384], ), + ( + SignatureScheme::ECDSA_NISTP521_SHA512, + &[&ec::BoringEcVerifier::ECDSA_NISTP521_SHA512], + ), + // (SignatureScheme::ED25519, &[&ed::BoringEdVerifier::ED25519]), + // (SignatureScheme::ED448, &[&ed::BoringEdVerifier::ED448]), ], }; diff --git a/boring-rustls-provider/src/verify/ec.rs b/boring-rustls-provider/src/verify/ec.rs index 34c8a3d..3de7165 100644 --- a/boring-rustls-provider/src/verify/ec.rs +++ b/boring-rustls-provider/src/verify/ec.rs @@ -1,5 +1,5 @@ use boring::{error::ErrorStack, hash::MessageDigest}; -use rustls::{pki_types::alg_id, SignatureScheme}; +use rustls::SignatureScheme; use rustls_pki_types::{InvalidSignature, SignatureVerificationAlgorithm}; use crate::helper; @@ -40,7 +40,7 @@ impl SignatureVerificationAlgorithm for BoringEcVerifier { ec_verifier_from_params(public_key.as_ref(), MessageDigest::sha512()) } - _ => return Err(InvalidSignature), + _ => unimplemented!(), } .map_err(|e| helper::log_and_map("ec_verifier_from_params", e, InvalidSignature))?; @@ -52,8 +52,8 @@ impl SignatureVerificationAlgorithm for BoringEcVerifier { fn public_key_alg_id(&self) -> rustls_pki_types::AlgorithmIdentifier { match self.0 { - SignatureScheme::ECDSA_NISTP256_SHA256 => alg_id::ECDSA_P256, - SignatureScheme::ECDSA_NISTP384_SHA384 => alg_id::ECDSA_P384, + SignatureScheme::ECDSA_NISTP256_SHA256 => webpki::alg_id::ECDSA_P256, + SignatureScheme::ECDSA_NISTP384_SHA384 => webpki::alg_id::ECDSA_P384, SignatureScheme::ECDSA_NISTP521_SHA512 => { // See rfc5480 appendix-A (secp521r1): 1.3.132.0.35 rustls_pki_types::AlgorithmIdentifier::from_slice(&[ @@ -61,33 +61,29 @@ impl SignatureVerificationAlgorithm for BoringEcVerifier { 0x04, 0x00, 0x23, ]) } - _ => unreachable!("BoringEcVerifier only supports configured ECDSA schemes"), + _ => unimplemented!(), } } fn signature_alg_id(&self) -> rustls_pki_types::AlgorithmIdentifier { match self.0 { - SignatureScheme::ECDSA_NISTP256_SHA256 => alg_id::ECDSA_SHA256, - SignatureScheme::ECDSA_NISTP384_SHA384 => alg_id::ECDSA_SHA384, + SignatureScheme::ECDSA_NISTP256_SHA256 => webpki::alg_id::ECDSA_SHA256, + SignatureScheme::ECDSA_NISTP384_SHA384 => webpki::alg_id::ECDSA_SHA384, SignatureScheme::ECDSA_NISTP521_SHA512 => { // See rfc5480 appendix-A (ecdsa-with-SHA512): 1.2.840.10045.4.3.4 rustls_pki_types::AlgorithmIdentifier::from_slice(&[ 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x04, ]) } - _ => unreachable!("BoringEcVerifier only supports configured ECDSA schemes"), + _ => unimplemented!(), } } - - fn fips(&self) -> bool { - cfg!(feature = "fips") && !matches!(self.0, SignatureScheme::ECDSA_NISTP521_SHA512) - } } fn ec_verifier_from_params( key: &boring::pkey::PKeyRef, digest: MessageDigest, -) -> Result, ErrorStack> { +) -> Result { boring::sign::Verifier::new(digest, key) } @@ -96,7 +92,7 @@ fn group_for_scheme(scheme: SignatureScheme) -> Result boring::nid::Nid::X9_62_PRIME256V1, SignatureScheme::ECDSA_NISTP384_SHA384 => boring::nid::Nid::SECP384R1, SignatureScheme::ECDSA_NISTP521_SHA512 => boring::nid::Nid::SECP521R1, - _ => unreachable!("BoringEcVerifier only supports configured ECDSA schemes"), + _ => unimplemented!(), }; boring::ec::EcGroup::from_curve_name(nid) } diff --git a/boring-rustls-provider/src/verify/ed.rs b/boring-rustls-provider/src/verify/ed.rs index 3eeeb26..5670c98 100644 --- a/boring-rustls-provider/src/verify/ed.rs +++ b/boring-rustls-provider/src/verify/ed.rs @@ -2,7 +2,7 @@ use std::ptr; use boring::error::ErrorStack; use foreign_types::ForeignType; -use rustls::{pki_types::alg_id, SignatureScheme}; +use rustls::SignatureScheme; use rustls_pki_types::{InvalidSignature, SignatureVerificationAlgorithm}; use crate::helper::{cvt_p, log_and_map}; @@ -41,23 +41,19 @@ impl SignatureVerificationAlgorithm for BoringEdVerifier { fn signature_alg_id(&self) -> rustls_pki_types::AlgorithmIdentifier { match self.0 { - SignatureScheme::ED25519 => alg_id::ED25519, + SignatureScheme::ED25519 => webpki::alg_id::ED25519, SignatureScheme::ED448 => { // rfc8410#section-3: 1.3.101.113: -> DER: 06 03 2B 65 71 rustls_pki_types::AlgorithmIdentifier::from_slice(&[0x06, 0x03, 0x2B, 0x65, 0x71]) } - _ => unreachable!("BoringEdVerifier only supports configured EdDSA schemes"), + _ => unimplemented!(), } } - - fn fips(&self) -> bool { - false - } } fn ed_verifier_from_params( key: &boring::pkey::PKeyRef, -) -> Result, ErrorStack> { +) -> Result { boring::sign::Verifier::new_without_digest(key) } @@ -68,7 +64,7 @@ fn ed_public_key_for_scheme( let nid = boring::nid::Nid::from_raw(match scheme { SignatureScheme::ED25519 => boring_sys::EVP_PKEY_ED25519, SignatureScheme::ED448 => boring_sys::EVP_PKEY_ED448, - _ => return Err(ErrorStack::get()), + _ => unimplemented!(), }); public_key(spki_spk, nid) } diff --git a/boring-rustls-provider/src/verify/rsa.rs b/boring-rustls-provider/src/verify/rsa.rs index e41d494..7dbfedb 100644 --- a/boring-rustls-provider/src/verify/rsa.rs +++ b/boring-rustls-provider/src/verify/rsa.rs @@ -5,7 +5,7 @@ use boring::{ rsa::{Padding, Rsa}, sign::RsaPssSaltlen, }; -use rustls::{pki_types::alg_id, SignatureScheme}; +use rustls::SignatureScheme; use rustls_pki_types::{InvalidSignature, SignatureVerificationAlgorithm}; use spki::der::Reader; @@ -64,8 +64,8 @@ impl SignatureVerificationAlgorithm for BoringRsaVerifier { Padding::PKCS1_PSS, ), - _ => return Err(InvalidSignature), - }?; + _ => unimplemented!(), + }; verifier.verify_oneshot(signature, message).map_or_else( |_| Err(InvalidSignature), |res| if res { Ok(()) } else { Err(InvalidSignature) }, @@ -73,48 +73,43 @@ impl SignatureVerificationAlgorithm for BoringRsaVerifier { } fn public_key_alg_id(&self) -> rustls_pki_types::AlgorithmIdentifier { - alg_id::RSA_ENCRYPTION + webpki::alg_id::RSA_ENCRYPTION } fn signature_alg_id(&self) -> rustls_pki_types::AlgorithmIdentifier { match self.0 { - SignatureScheme::RSA_PKCS1_SHA256 => alg_id::RSA_PKCS1_SHA256, - SignatureScheme::RSA_PKCS1_SHA384 => alg_id::RSA_PKCS1_SHA384, - SignatureScheme::RSA_PKCS1_SHA512 => alg_id::RSA_PKCS1_SHA512, + SignatureScheme::RSA_PKCS1_SHA256 => webpki::alg_id::RSA_PKCS1_SHA256, + SignatureScheme::RSA_PKCS1_SHA384 => webpki::alg_id::RSA_PKCS1_SHA384, + SignatureScheme::RSA_PKCS1_SHA512 => webpki::alg_id::RSA_PKCS1_SHA512, - SignatureScheme::RSA_PSS_SHA256 => alg_id::RSA_PSS_SHA256, - SignatureScheme::RSA_PSS_SHA384 => alg_id::RSA_PSS_SHA384, - SignatureScheme::RSA_PSS_SHA512 => alg_id::RSA_PSS_SHA512, + SignatureScheme::RSA_PSS_SHA256 => webpki::alg_id::RSA_PSS_SHA256, + SignatureScheme::RSA_PSS_SHA384 => webpki::alg_id::RSA_PSS_SHA384, + SignatureScheme::RSA_PSS_SHA512 => webpki::alg_id::RSA_PSS_SHA512, - _ => unreachable!("BoringRsaVerifier only supports configured RSA schemes"), + _ => unimplemented!(), } } - - fn fips(&self) -> bool { - cfg!(feature = "fips") - } } fn rsa_verifier_from_params( key: &boring::pkey::PKeyRef, digest: MessageDigest, padding: Padding, -) -> Result, InvalidSignature> { - let mut verifier = boring::sign::Verifier::new(digest, key) - .map_err(|e| log_and_map("Verifier::new", e, InvalidSignature))?; +) -> boring::sign::Verifier { + let mut verifier = boring::sign::Verifier::new(digest, key).expect("failed getting verifier"); verifier .set_rsa_padding(padding) - .map_err(|e| log_and_map("set_rsa_padding", e, InvalidSignature))?; + .expect("failed setting padding"); if padding == Padding::PKCS1_PSS { verifier .set_rsa_pss_saltlen(RsaPssSaltlen::DIGEST_LENGTH) - .map_err(|e| log_and_map("set_rsa_pss_saltlen", e, InvalidSignature))?; + .expect("failed setting rsa_pss salt lengths"); verifier .set_rsa_mgf1_md(digest) - .map_err(|e| log_and_map("set_rsa_mgf1_md", e, InvalidSignature))?; + .expect("failed setting mgf1 digest"); } - Ok(verifier) + verifier } pub(crate) fn decode_spki_spk( diff --git a/boring-rustls-provider/tests/e2e.rs b/boring-rustls-provider/tests/e2e.rs index c53616e..212f1ca 100644 --- a/boring-rustls-provider/tests/e2e.rs +++ b/boring-rustls-provider/tests/e2e.rs @@ -5,71 +5,15 @@ use tokio::{ net::TcpStream, }; -use boring_rustls_provider::tls13; -#[cfg(feature = "tls12")] -use rustls::version::TLS12; -use rustls::{version::TLS13, ClientConfig, ServerConfig, SupportedCipherSuite}; +use boring_rustls_provider::{tls12, tls13, PROVIDER}; +use rustls::{ + version::{TLS12, TLS13}, + ClientConfig, ServerConfig, SupportedCipherSuite, +}; use rustls_pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer}; use tokio::net::TcpListener; use tokio_rustls::{TlsAcceptor, TlsConnector}; -fn tls13_provider_suites() -> Vec { - boring_rustls_provider::provider() - .cipher_suites - .into_iter() - .filter(|suite| matches!(suite, SupportedCipherSuite::Tls13(_))) - .collect() -} - -#[cfg(feature = "tls12")] -fn tls12_provider_suites_for_ecdsa() -> Vec { - boring_rustls_provider::provider() - .cipher_suites - .into_iter() - .filter(|suite| { - let SupportedCipherSuite::Tls12(suite) = suite else { - return false; - }; - - suite.sign.iter().any(|scheme| { - matches!( - scheme, - rustls::SignatureScheme::ECDSA_NISTP256_SHA256 - | rustls::SignatureScheme::ECDSA_NISTP384_SHA384 - | rustls::SignatureScheme::ECDSA_NISTP521_SHA512 - | rustls::SignatureScheme::ED25519 - | rustls::SignatureScheme::ED448 - ) - }) - }) - .collect() -} - -#[cfg(feature = "tls12")] -fn tls12_provider_suites_for_rsa() -> Vec { - boring_rustls_provider::provider() - .cipher_suites - .into_iter() - .filter(|suite| { - let SupportedCipherSuite::Tls12(suite) = suite else { - return false; - }; - - suite.sign.iter().any(|scheme| { - matches!( - scheme, - rustls::SignatureScheme::RSA_PKCS1_SHA256 - | rustls::SignatureScheme::RSA_PKCS1_SHA384 - | rustls::SignatureScheme::RSA_PKCS1_SHA512 - | rustls::SignatureScheme::RSA_PSS_SHA256 - | rustls::SignatureScheme::RSA_PSS_SHA384 - | rustls::SignatureScheme::RSA_PSS_SHA512 - ) - }) - }) - .collect() -} - #[tokio::test] async fn test_tls13_crypto() { let pki = TestPki::new(&rcgen::PKCS_ECDSA_P256_SHA256); @@ -77,404 +21,25 @@ async fn test_tls13_crypto() { let root_store = pki.client_root_store(); let server_config = pki.server_config(); - let ciphers = tls13_provider_suites(); + let ciphers = [ + SupportedCipherSuite::Tls13(&tls13::AES_128_GCM_SHA256), + SupportedCipherSuite::Tls13(&tls13::AES_256_GCM_SHA384), + SupportedCipherSuite::Tls13(&tls13::CHACHA20_POLY1305_SHA256), + ]; for cipher in ciphers { - let config = ClientConfig::builder_with_provider(Arc::new( - boring_rustls_provider::provider_with_ciphers([cipher].to_vec()), - )) - .with_protocol_versions(&[&TLS13]) - .unwrap() - .with_root_certificates(root_store.clone()) - .with_no_client_auth(); + let config = ClientConfig::builder_with_provider(PROVIDER) + .with_cipher_suites(&[cipher]) + .with_safe_default_kx_groups() + .with_protocol_versions(&[&TLS13]) + .unwrap() + .with_root_certificates(root_store.clone()) + .with_no_client_auth(); do_exchange(config, server_config.clone()).await; } } -#[test] -fn provider_kx_groups_reject_invalid_peer_keys_without_panicking() { - for group in boring_rustls_provider::provider().kx_groups { - let kx = group.start().expect("provider KX group should initialize"); - - let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| kx.complete(&[]))); - assert!(outcome.is_ok(), "KX group {:?} panicked", group.name()); - assert!( - outcome.expect("already checked for panic").is_err(), - "KX group {:?} accepted an invalid key share", - group.name() - ); - } -} - -#[test] -fn provider_verifiers_reject_malformed_inputs_without_panicking() { - let provider = boring_rustls_provider::provider(); - - for (index, verifier) in provider - .signature_verification_algorithms - .all - .iter() - .enumerate() - { - let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - verifier.verify_signature(&[], b"message", &[]) - })); - - assert!(outcome.is_ok(), "verifier #{index} panicked"); - assert!( - outcome.expect("already checked for panic").is_err(), - "verifier #{index} accepted malformed inputs" - ); - } -} - -/// Self-to-self TLS 1.3 handshake using only the X25519MLKEM768 PQ hybrid group. -#[cfg(feature = "mlkem")] -#[tokio::test] -async fn test_tls13_pq_x25519_mlkem768() { - use rustls::NamedGroup; - - let pki = TestPki::new(&rcgen::PKCS_ECDSA_P256_SHA256); - - let root_store = pki.client_root_store(); - - // Build server with only X25519MLKEM768 - let server_provider = - boring_rustls_provider::provider_with_ciphers(vec![SupportedCipherSuite::Tls13( - &tls13::AES_256_GCM_SHA384, - )]); - let server_config = { - let mut cfg = ServerConfig::builder_with_provider(Arc::new(server_provider)) - .with_protocol_versions(&[&TLS13]) - .unwrap() - .with_no_client_auth() - .with_single_cert( - vec![pki.server_cert_der.clone()], - pki.server_key_der.clone_key(), - ) - .unwrap(); - cfg.key_log = Arc::new(rustls::KeyLogFile::new()); - Arc::new(cfg) - }; - - // Build client with only X25519MLKEM768 - let client_provider = - boring_rustls_provider::provider_with_ciphers(vec![SupportedCipherSuite::Tls13( - &tls13::AES_256_GCM_SHA384, - )]); - let config = ClientConfig::builder_with_provider(Arc::new(client_provider)) - .with_protocol_versions(&[&TLS13]) - .unwrap() - .with_root_certificates(root_store) - .with_no_client_auth(); - - let negotiated_group = do_exchange(config, server_config).await; - assert_eq!(negotiated_group, Some(NamedGroup::X25519MLKEM768)); -} - -/// Connect to Cloudflare's PQ test endpoint and verify X25519MLKEM768 -/// was actually negotiated by checking the `/cdn-cgi/trace` response. -/// Marked `#[ignore]` because it depends on an external service. -#[cfg(feature = "mlkem")] -#[ignore] -#[tokio::test] -async fn test_pq_interop_cloudflare() { - let mut root_store = rustls::RootCertStore::empty(); - root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); - - let provider = boring_rustls_provider::provider(); - let config = ClientConfig::builder_with_provider(Arc::new(provider)) - .with_protocol_versions(&[&TLS13]) - .unwrap() - .with_root_certificates(root_store) - .with_no_client_auth(); - - let connector = TlsConnector::from(Arc::new(config)); - let stream = TcpStream::connect("pq.cloudflareresearch.com:443") - .await - .unwrap(); - - let mut stream = connector - .connect( - rustls_pki_types::ServerName::try_from("pq.cloudflareresearch.com").unwrap(), - stream, - ) - .await - .expect("TLS handshake with pq.cloudflareresearch.com failed"); - - // Hit the trace endpoint which reports negotiated TLS parameters - stream - .write_all( - b"GET /cdn-cgi/trace HTTP/1.1\r\nHost: pq.cloudflareresearch.com\r\nConnection: close\r\n\r\n", - ) - .await - .unwrap(); - - let mut buf = Vec::new(); - stream.read_to_end(&mut buf).await.unwrap(); - let response = String::from_utf8_lossy(&buf); - - // Verify TLS 1.3 was used - assert!( - response.contains("tls=TLSv1.3"), - "expected TLSv1.3, got: {response}" - ); - - // Verify X25519MLKEM768 was negotiated as the key exchange - assert!( - response.contains("kex=X25519MLKEM768"), - "expected kex=X25519MLKEM768, got: {response}" - ); -} - -/// Connect to Cloudflare with TLS 1.2 forced and verify that a classical -/// key exchange is used (PQ groups are TLS 1.3 only). -/// Marked `#[ignore]` because it depends on an external service. -#[cfg(all(feature = "mlkem", feature = "tls12"))] -#[ignore] -#[tokio::test] -async fn test_tls12_interop_cloudflare_no_pq() { - let mut root_store = rustls::RootCertStore::empty(); - root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); - - let provider = boring_rustls_provider::provider(); - let config = ClientConfig::builder_with_provider(Arc::new(provider)) - .with_protocol_versions(&[&TLS12]) - .unwrap() - .with_root_certificates(root_store) - .with_no_client_auth(); - - let connector = TlsConnector::from(Arc::new(config)); - let stream = TcpStream::connect("pq.cloudflareresearch.com:443") - .await - .unwrap(); - - let mut stream = connector - .connect( - rustls_pki_types::ServerName::try_from("pq.cloudflareresearch.com").unwrap(), - stream, - ) - .await - .expect("TLS handshake with pq.cloudflareresearch.com (TLS 1.2) failed"); - - stream - .write_all( - b"GET /cdn-cgi/trace HTTP/1.1\r\nHost: pq.cloudflareresearch.com\r\nConnection: close\r\n\r\n", - ) - .await - .unwrap(); - - let mut buf = Vec::new(); - stream.read_to_end(&mut buf).await.unwrap(); - let response = String::from_utf8_lossy(&buf); - - // Verify TLS 1.2 was used - assert!( - response.contains("tls=TLSv1.2"), - "expected TLSv1.2, got: {response}" - ); - - // Verify a classical key exchange was used (not PQ) - assert!( - !response.contains("kex=X25519MLKEM768"), - "TLS 1.2 should not use PQ key exchange, got: {response}" - ); -} - -#[test] -#[cfg(feature = "fips")] -fn is_fips_enabled() { - assert!(boring::fips::enabled()); -} - -#[test] -#[cfg(feature = "fips")] -fn fips_provider_excludes_chacha20_cipher_suites() { - use rustls::CipherSuite; - - let provider = boring_rustls_provider::provider(); - let disallowed = [ - CipherSuite::TLS13_CHACHA20_POLY1305_SHA256, - CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, - CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, - ]; - - for suite in provider.cipher_suites { - let selected = suite.suite(); - assert!( - !disallowed.contains(&selected), - "FIPS provider exposed disallowed cipher suite: {selected:?}" - ); - } -} - -#[test] -#[cfg(feature = "fips")] -fn fips_provider_with_ciphers_filters_non_fips_input() { - use rustls::CipherSuite; - - let provider = boring_rustls_provider::provider_with_ciphers(vec![ - SupportedCipherSuite::Tls13(&tls13::CHACHA20_POLY1305_SHA256), - SupportedCipherSuite::Tls13(&tls13::AES_128_GCM_SHA256), - ]); - - let suites = provider - .cipher_suites - .iter() - .map(|suite| suite.suite()) - .collect::>(); - - assert_eq!(suites, vec![CipherSuite::TLS13_AES_128_GCM_SHA256]); -} - -#[test] -#[cfg(feature = "fips")] -fn fips_provider_restricts_kx_groups() { - use rustls::NamedGroup; - - let provider = boring_rustls_provider::provider(); - let groups = provider - .kx_groups - .iter() - .map(|group| group.name()) - .collect::>(); - - // fips implies mlkem, so X25519MLKEM768 must be present and preferred - assert_eq!( - groups[0], - NamedGroup::X25519MLKEM768, - "X25519MLKEM768 should be the first (preferred) FIPS KX group" - ); - assert!(groups.contains(&NamedGroup::secp256r1)); - assert!(groups.contains(&NamedGroup::secp384r1)); - for group in &groups { - assert!( - matches!( - group, - NamedGroup::X25519MLKEM768 | NamedGroup::secp256r1 | NamedGroup::secp384r1 - ), - "FIPS provider exposed disallowed KX group: {group:?}" - ); - } -} - -#[test] -#[cfg(feature = "fips")] -fn fips_provider_excludes_disallowed_signature_schemes() { - use rustls::SignatureScheme; - - let provider = boring_rustls_provider::provider(); - let schemes = provider - .signature_verification_algorithms - .mapping - .iter() - .map(|(scheme, _)| *scheme) - .collect::>(); - - assert!(schemes.contains(&SignatureScheme::RSA_PSS_SHA256)); - assert!(schemes.contains(&SignatureScheme::ECDSA_NISTP256_SHA256)); - - for disallowed in [ - SignatureScheme::ECDSA_NISTP521_SHA512, - SignatureScheme::ED25519, - SignatureScheme::ED448, - ] { - assert!( - !schemes.contains(&disallowed), - "FIPS provider exposed disallowed signature scheme: {disallowed:?}" - ); - } -} - -#[test] -#[cfg(not(feature = "fips"))] -fn is_fips_disabled() { - assert!(!boring::fips::enabled()); -} - -#[test] -#[cfg(not(feature = "fips"))] -fn non_fips_provider_keeps_non_fips_algorithms() { - use rustls::{CipherSuite, NamedGroup}; - - let provider = boring_rustls_provider::provider(); - - assert!(provider - .cipher_suites - .iter() - .any(|suite| { suite.suite() == CipherSuite::TLS13_CHACHA20_POLY1305_SHA256 })); - - assert!(provider - .kx_groups - .iter() - .any(|group| group.name() == NamedGroup::X25519)); -} - -#[test] -#[cfg(not(feature = "fips"))] -fn non_fips_provider_components_report_non_fips() { - let provider = boring_rustls_provider::provider(); - - assert!(!provider.secure_random.fips()); - assert!(!provider.key_provider.fips()); -} - -#[test] -#[cfg(not(feature = "fips"))] -fn non_fips_provider_with_ciphers_keeps_requested_suites() { - use rustls::CipherSuite; - - let provider = boring_rustls_provider::provider_with_ciphers(vec![ - SupportedCipherSuite::Tls13(&tls13::CHACHA20_POLY1305_SHA256), - SupportedCipherSuite::Tls13(&tls13::AES_128_GCM_SHA256), - ]); - - let suites = provider - .cipher_suites - .iter() - .map(|suite| suite.suite()) - .collect::>(); - - assert_eq!( - suites, - vec![ - CipherSuite::TLS13_CHACHA20_POLY1305_SHA256, - CipherSuite::TLS13_AES_128_GCM_SHA256, - ] - ); -} - -#[test] -#[cfg(all(not(feature = "fips"), feature = "mlkem"))] -fn non_fips_provider_includes_pq_group() { - use rustls::NamedGroup; - - let provider = boring_rustls_provider::provider(); - let groups = provider - .kx_groups - .iter() - .map(|group| group.name()) - .collect::>(); - - assert_eq!( - groups[0], - NamedGroup::X25519MLKEM768, - "X25519MLKEM768 should be the first (preferred) KX group" - ); - assert_eq!( - groups[1], - NamedGroup::X25519, - "X25519 should be the first classical fallback" - ); - assert_eq!( - groups[2], - NamedGroup::secp256r1, - "P-256 should follow X25519, matching boring's default order" - ); -} - -#[cfg(feature = "tls12")] #[tokio::test] async fn test_tls12_ec_crypto() { let pki = TestPki::new(&rcgen::PKCS_ECDSA_P256_SHA256); @@ -482,22 +47,25 @@ async fn test_tls12_ec_crypto() { let root_store = pki.client_root_store(); let server_config = pki.server_config(); - let ciphers = tls12_provider_suites_for_ecdsa(); + let ciphers = [ + SupportedCipherSuite::Tls12(&tls12::ECDHE_ECDSA_AES128_GCM_SHA256), + SupportedCipherSuite::Tls12(&tls12::ECDHE_ECDSA_AES256_GCM_SHA384), + SupportedCipherSuite::Tls12(&tls12::ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256), + ]; for cipher in ciphers { - let config = ClientConfig::builder_with_provider(Arc::new( - boring_rustls_provider::provider_with_ciphers([cipher].to_vec()), - )) - .with_protocol_versions(&[&TLS12]) - .unwrap() - .with_root_certificates(root_store.clone()) - .with_no_client_auth(); + let config = ClientConfig::builder_with_provider(PROVIDER) + .with_cipher_suites(&[cipher]) + .with_safe_default_kx_groups() + .with_protocol_versions(&[&TLS12]) + .unwrap() + .with_root_certificates(root_store.clone()) + .with_no_client_auth(); do_exchange(config, server_config.clone()).await; } } -#[cfg(feature = "tls12")] #[tokio::test] async fn test_tls12_rsa_crypto() { let pki = TestPki::new(&rcgen::PKCS_RSA_SHA256); @@ -505,16 +73,20 @@ async fn test_tls12_rsa_crypto() { let root_store = pki.client_root_store(); let server_config = pki.server_config(); - let ciphers = tls12_provider_suites_for_rsa(); + let ciphers = [ + SupportedCipherSuite::Tls12(&tls12::ECDHE_RSA_AES128_GCM_SHA256), + SupportedCipherSuite::Tls12(&tls12::ECDHE_RSA_AES256_GCM_SHA384), + SupportedCipherSuite::Tls12(&tls12::ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256), + ]; for cipher in ciphers { - let config = ClientConfig::builder_with_provider(Arc::new( - boring_rustls_provider::provider_with_ciphers([cipher].to_vec()), - )) - .with_protocol_versions(&[&TLS12]) - .unwrap() - .with_root_certificates(root_store.clone()) - .with_no_client_auth(); + let config = ClientConfig::builder_with_provider(PROVIDER) + .with_cipher_suites(&[cipher]) + .with_safe_default_kx_groups() + .with_protocol_versions(&[&TLS12]) + .unwrap() + .with_root_certificates(root_store.clone()) + .with_no_client_auth(); do_exchange(config, server_config.clone()).await; } @@ -524,10 +96,7 @@ async fn new_listener() -> TcpListener { TcpListener::bind("localhost:0").await.unwrap() } -async fn do_exchange( - config: ClientConfig, - server_config: Arc, -) -> Option { +async fn do_exchange(config: ClientConfig, server_config: Arc) { let listener = new_listener().await; let addr = listener.local_addr().unwrap(); tokio::spawn(spawn_echo_server(listener, server_config.clone())); @@ -543,18 +112,10 @@ async fn do_exchange( .await .unwrap(); - let negotiated_group = stream - .get_ref() - .1 - .negotiated_key_exchange_group() - .map(|group| group.name()); - stream.write_all(b"HELLO").await.unwrap(); let mut buf = Vec::new(); let bytes = stream.read_to_end(&mut buf).await.unwrap(); assert_eq!(&buf[..bytes], b"HELLO"); - - negotiated_group } async fn spawn_echo_server(listener: TcpListener, config: Arc) { @@ -615,18 +176,11 @@ impl TestPki { } fn server_config(self) -> Arc { - #[cfg(feature = "tls12")] - let versions: &[&'static rustls::SupportedProtocolVersion] = &[&TLS12, &TLS13]; - #[cfg(not(feature = "tls12"))] - let versions: &[&'static rustls::SupportedProtocolVersion] = &[&TLS13]; - - let mut server_config = - ServerConfig::builder_with_provider(Arc::new(boring_rustls_provider::provider())) - .with_protocol_versions(versions) - .unwrap() - .with_no_client_auth() - .with_single_cert(vec![self.server_cert_der], self.server_key_der) - .unwrap(); + let mut server_config = ServerConfig::builder_with_provider(PROVIDER) + .with_safe_defaults() + .with_no_client_auth() + .with_single_cert(vec![self.server_cert_der], self.server_key_der) + .unwrap(); server_config.key_log = Arc::new(rustls::KeyLogFile::new()); diff --git a/boring-rustls-provider/tests/panic_surface.rs b/boring-rustls-provider/tests/panic_surface.rs deleted file mode 100644 index 3a960f4..0000000 --- a/boring-rustls-provider/tests/panic_surface.rs +++ /dev/null @@ -1,244 +0,0 @@ -use std::fs; -use std::path::{Path, PathBuf}; - -const BANNED_TOKENS: &[&str] = &[ - "unwrap(", - "expect(", - "assert!(", - "assert_eq!(", - "assert_ne!(", - "panic!(", - "unreachable!(", - "unimplemented!(", -]; - -struct AllowlistedPanic { - path_suffix: &'static str, - line_fragment: &'static str, - reason: &'static str, -} - -const ALLOWLIST: &[AllowlistedPanic] = &[ - AllowlistedPanic { - path_suffix: "boring-rustls-provider/src/hash.rs", - line_fragment: "failed getting hash digest", - reason: "rustls hash trait is infallible; provider currently cannot surface this as Result", - }, - AllowlistedPanic { - path_suffix: "boring-rustls-provider/src/hash.rs", - line_fragment: "failed getting hasher", - reason: "rustls hash trait is infallible; provider currently cannot surface this as Result", - }, - AllowlistedPanic { - path_suffix: "boring-rustls-provider/src/hash.rs", - line_fragment: "hash::Hash is only instantiated with SHA-2 digests", - reason: "constructor invariant over static hash-provider constants", - }, - AllowlistedPanic { - path_suffix: "boring-rustls-provider/src/hash.rs", - line_fragment: "failed getting digest", - reason: "rustls hash trait is infallible; provider currently cannot surface this as Result", - }, - AllowlistedPanic { - path_suffix: "boring-rustls-provider/src/hash.rs", - line_fragment: "failed finishing hash", - reason: "rustls hash trait is infallible; provider currently cannot surface this as Result", - }, - AllowlistedPanic { - path_suffix: "boring-rustls-provider/src/hash.rs", - line_fragment: "failed adding data to hash", - reason: "rustls hash trait is infallible; provider currently cannot surface this as Result", - }, - AllowlistedPanic { - path_suffix: "boring-rustls-provider/src/hkdf.rs", - line_fragment: "HKDF_extract failed", - reason: "rustls hkdf trait is infallible at this call site", - }, - AllowlistedPanic { - path_suffix: "boring-rustls-provider/src/hkdf.rs", - line_fragment: "HMAC failed", - reason: "rustls hkdf trait is infallible at this call site", - }, - AllowlistedPanic { - path_suffix: "boring-rustls-provider/src/hkdf.rs", - line_fragment: "failed hkdf expand", - reason: "expand_block API is infallible in rustls", - }, - AllowlistedPanic { - path_suffix: "boring-rustls-provider/src/hmac.rs", - line_fragment: "failed getting digest", - reason: "rustls hmac trait is infallible at this call site", - }, - AllowlistedPanic { - path_suffix: "boring-rustls-provider/src/hmac.rs", - line_fragment: "failed initializing hmac", - reason: "rustls hmac trait is infallible at this call site", - }, - AllowlistedPanic { - path_suffix: "boring-rustls-provider/src/hmac.rs", - line_fragment: "failed updating hmac", - reason: "rustls hmac trait is infallible at this call site", - }, - AllowlistedPanic { - path_suffix: "boring-rustls-provider/src/hmac.rs", - line_fragment: "failed hmac final", - reason: "rustls hmac trait is infallible at this call site", - }, - AllowlistedPanic { - path_suffix: "boring-rustls-provider/src/hmac.rs", - line_fragment: "failed creating HMAC_CTX", - reason: "rustls hmac trait is infallible at this call site", - }, - AllowlistedPanic { - path_suffix: "boring-rustls-provider/src/prf.rs", - line_fragment: "failed getting digest", - reason: "rustls tls12::Prf::for_secret is infallible", - }, - AllowlistedPanic { - path_suffix: "boring-rustls-provider/src/prf.rs", - line_fragment: "failed calculating prf", - reason: "rustls tls12::Prf::for_secret is infallible", - }, - AllowlistedPanic { - path_suffix: "boring-rustls-provider/src/verify/rsa.rs", - line_fragment: "BoringRsaVerifier only supports configured RSA schemes", - reason: "static verifier configuration invariant", - }, - AllowlistedPanic { - path_suffix: "boring-rustls-provider/src/verify/ec.rs", - line_fragment: "BoringEcVerifier only supports configured ECDSA schemes", - reason: "static verifier configuration invariant", - }, - AllowlistedPanic { - path_suffix: "boring-rustls-provider/src/verify/ed.rs", - line_fragment: "BoringEdVerifier only supports configured EdDSA schemes", - reason: "static verifier configuration invariant", - }, - AllowlistedPanic { - path_suffix: "boring-rustls-provider/src/kx/ex.rs", - line_fragment: "unsupported key type", - reason: "static KX type invariant", - }, -]; - -#[test] -fn no_unreviewed_runtime_panic_constructs() { - let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let repo_root = manifest_dir - .parent() - .expect("crate must be within repository root"); - - let mut violations = Vec::new(); - for root in [ - repo_root.join("boring-rustls-provider/src"), - repo_root.join("boring-additions/src"), - ] { - collect_rs_files(&root) - .expect("must be able to enumerate source files") - .into_iter() - .for_each(|path| { - let rel = path - .strip_prefix(repo_root) - .expect("path should be under repo root") - .to_string_lossy() - .to_string(); - - let content = fs::read_to_string(&path) - .unwrap_or_else(|e| panic!("failed to read {}: {e}", rel)); - - for (line_no, line) in runtime_lines_only(&content) { - let trimmed = line.trim(); - if trimmed.starts_with("//") { - continue; - } - - for token in BANNED_TOKENS { - if !line.contains(token) { - continue; - } - - let allowed = ALLOWLIST.iter().find(|allow| { - rel.ends_with(allow.path_suffix) && line.contains(allow.line_fragment) - }); - if allowed.is_none() { - violations.push(format!("{rel}:{line_no}: {trimmed}")); - } - } - } - }); - } - - if !violations.is_empty() { - violations.sort(); - panic!( - "found unreviewed panic constructs in runtime code:\n{}\n\nIf intentional, add a targeted allowlist entry with rationale.", - violations.join("\n") - ); - } -} - -#[test] -fn allowlist_entries_have_matching_runtime_lines() { - let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - let repo_root = manifest_dir - .parent() - .expect("crate must be within repository root"); - - let mut missing = Vec::new(); - for entry in ALLOWLIST { - let path = repo_root.join(entry.path_suffix); - let content = fs::read_to_string(&path) - .unwrap_or_else(|e| panic!("failed to read {}: {e}", entry.path_suffix)); - let found = runtime_lines_only(&content) - .into_iter() - .any(|(_, line)| line.contains(entry.line_fragment)); - if !found { - missing.push(format!( - "{} :: '{}' ({})", - entry.path_suffix, entry.line_fragment, entry.reason - )); - } - } - - if !missing.is_empty() { - missing.sort(); - panic!( - "panic allowlist entries no longer match runtime code:\n{}", - missing.join("\n") - ); - } -} - -fn runtime_lines_only(content: &str) -> Vec<(usize, &str)> { - let mut lines = Vec::new(); - for (index, line) in content.lines().enumerate() { - if line.trim_start().starts_with("#[cfg(test)]") { - break; - } - lines.push((index + 1, line)); - } - lines -} - -fn collect_rs_files(root: &Path) -> Result, std::io::Error> { - let mut files = Vec::new(); - collect_rs_files_rec(root, &mut files)?; - Ok(files) -} - -fn collect_rs_files_rec(root: &Path, acc: &mut Vec) -> Result<(), std::io::Error> { - for entry in fs::read_dir(root)? { - let entry = entry?; - let path = entry.path(); - if path.is_dir() { - collect_rs_files_rec(&path, acc)?; - continue; - } - - if path.extension().and_then(|ext| ext.to_str()) == Some("rs") { - acc.push(path); - } - } - - Ok(()) -} diff --git a/boring-sys-additions/src/lib.rs b/boring-sys-additions/src/lib.rs index 5a2523e..3243570 100644 --- a/boring-sys-additions/src/lib.rs +++ b/boring-sys-additions/src/lib.rs @@ -1,7 +1,7 @@ use std::ffi; extern "C" { - /// Calculates `out_len` bytes of the TLS PRF, using `digest`, and + /// Calculates `out_len` bytes of the TLS PDF, using `digest`, and /// writes them to `out`. It returns one on success and zero on error. /// /// This isn't part of the public headers in `BoringSSL` but it is exported diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 11b0573..d2e4700 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -7,7 +7,15 @@ description = "Boring Rustls provider example code and tests." publish = false [dependencies] -env_logger = "0.11" -rustls = { workspace = true, features = [ "logging", "std" ]} +docopt = "~1.1" +env_logger = "0.10" +log = { version = "0.4.4" } +mio = { version = "0.8", features = ["net", "os-poll"] } +pki-types = { package = "rustls-pki-types", version = "0.2" } +rcgen = { version = "0.11.3", features = ["pem"], default-features = false } +rustls = { workspace = true, features = [ "logging" ]} boring-rustls-provider = { path = "../boring-rustls-provider", features = ["logging"] } +rustls-pemfile = { workspace = true } +serde = "1.0" +serde_derive = "1.0" webpki-roots = { workspace = true } diff --git a/examples/src/bin/client.rs b/examples/src/bin/client.rs index bbbb32b..55a4cc0 100644 --- a/examples/src/bin/client.rs +++ b/examples/src/bin/client.rs @@ -2,24 +2,22 @@ use std::io::{stdout, Read, Write}; use std::net::TcpStream; use std::sync::Arc; -fn main() -> Result<(), Box> { +use boring_rustls_provider::PROVIDER; + +fn main() { env_logger::init(); let mut root_store = rustls::RootCertStore::empty(); root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned()); - let config = - rustls::ClientConfig::builder_with_provider(boring_rustls_provider::provider().into()) - .with_safe_default_protocol_versions() - .map_err(|_| std::io::Error::other("failed selecting protocol versions"))? - .with_root_certificates(root_store) - .with_no_client_auth(); + let config = rustls::ClientConfig::builder_with_provider(PROVIDER) + .with_safe_defaults() + .with_root_certificates(root_store) + .with_no_client_auth(); - let server_name = "www.rust-lang.org" - .try_into() - .map_err(|_| std::io::Error::other("invalid server name"))?; - let mut conn = rustls::ClientConnection::new(Arc::new(config), server_name)?; - let mut sock = TcpStream::connect("www.rust-lang.org:443")?; + let server_name = "www.rust-lang.org".try_into().unwrap(); + let mut conn = rustls::ClientConnection::new(Arc::new(config), server_name).unwrap(); + let mut sock = TcpStream::connect("www.rust-lang.org:443").unwrap(); let mut tls = rustls::Stream::new(&mut conn, &mut sock); tls.write_all( concat!( @@ -30,19 +28,16 @@ fn main() -> Result<(), Box> { "\r\n" ) .as_bytes(), - )?; - let ciphersuite = tls - .conn - .negotiated_cipher_suite() - .ok_or_else(|| std::io::Error::other("no negotiated ciphersuite"))?; + ) + .unwrap(); + let ciphersuite = tls.conn.negotiated_cipher_suite().unwrap(); writeln!( &mut std::io::stderr(), "Current ciphersuite: {:?}", ciphersuite.suite() - )?; + ) + .unwrap(); let mut plaintext = Vec::new(); - tls.read_to_end(&mut plaintext)?; - stdout().write_all(&plaintext)?; - - Ok(()) + tls.read_to_end(&mut plaintext).unwrap(); + stdout().write_all(&plaintext).unwrap(); }