diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 541e743..4e4eef6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,10 +9,9 @@ on: env: CARGO_TERM_COLOR: always RUSTFLAGS: -Dwarnings - FEATURES: "logging,tls12" jobs: - build: + fmt: runs-on: ubuntu-latest steps: @@ -21,9 +20,57 @@ jobs: run: sudo apt-get install -y cmake clang - name: Check fmt run: make fmt - - name: Lint - run: make lint - - name: Tests usual - run: make test - - name: Build usual - run: make build + + 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" + + 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 diff --git a/Cargo.toml b/Cargo.toml index 72a4b52..62f9a8e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,8 +17,8 @@ default-members = [ resolver = "2" [workspace.dependencies] -boring = { version = "4", default-features = false } -boring-sys = { version = "4", default-features = false } +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" } diff --git a/Makefile b/Makefile index 5189d1e..cfa515a 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,5 @@ FEATURES ?= logging,tls12 +CARGO_FEATURES := $(if $(strip $(FEATURES)),-F "$(FEATURES)",) .PHONY: fmt @@ -7,12 +8,16 @@ fmt: .PHONY: lint lint: - cargo clippy --workspace --all-targets -F "$(FEATURES)" + cargo clippy --workspace --all-targets $(CARGO_FEATURES) + +.PHONY: check +check: + cargo check --workspace --all-targets $(CARGO_FEATURES) .PHONY: test test: - cargo test --all-targets -F "$(FEATURES)" + cargo test --all-targets $(CARGO_FEATURES) .PHONY: build build: - cargo build --all-targets -F "$(FEATURES)" \ No newline at end of file + cargo build --all-targets $(CARGO_FEATURES) diff --git a/Readme.md b/Readme.md index f44e007..4a0ab65 100644 --- a/Readme.md +++ b/Readme.md @@ -2,55 +2,60 @@ [![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) -This is supposed to be the start to a [boringssl](https://github.com/cloudflare/boring)-based [rustls](https://github.com/rustls/rustls) crypto provider. +A [BoringSSL](https://github.com/cloudflare/boring)-based [rustls](https://github.com/rustls/rustls) crypto provider. -## 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. +Built on `boring` v5 and `rustls` 0.23. -Further, the rustls crypto provider API is still not stable it seems. This works currently with `rustls = 0.22.0-alpha.5`. +## Features -### Supported ciphers -Currently, supports only TLS 1.3: +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). See [FIPS mode](#fips-mode) below. | +| `fips-precompiled` | Deprecated alias for `fips`. Matches the `boring` crate's feature name. | +| `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): ``` AES_128_GCM_SHA256 AES_256_GCM_SHA384 CHACHA20_POLY1305_SHA256 ``` -QUIC: not yet supported - -TLS 1.2: +TLS 1.2 (requires `tls12` feature): ``` 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 Algorithms - -`ECDHE` with curves: +### Key Exchange Groups + +ECDHE: ``` X25519 X448 -secp256r1 -secp384r1 -secp521r1 +secp256r1 (P-256) +secp384r1 (P-384) +secp521r1 (P-521) ``` - -`FFDHE` with: +FFDHE: ``` ffdhe2048 ``` -### Signature Generation / Verification +### Signature Algorithms ``` RSA_PKCS1_SHA256 @@ -66,6 +71,30 @@ ED25519 ED448 ``` +## 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**: P-256 and P-384 only (no X25519, X448, P-521, or FFDHE). +- **Signature algorithms**: RSA PKCS#1 / PSS and ECDSA with P-256 or P-384 only + (no P-521, Ed25519, or Ed448). + +Post-quantum hybrid key exchange (`P256Kyber768Draft00`) is planned for the +FIPS group set but not yet implemented. + +## 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-rustls-provider/Cargo.toml b/boring-rustls-provider/Cargo.toml index 8a02f51..ffb9248 100644 --- a/boring-rustls-provider/Cargo.toml +++ b/boring-rustls-provider/Cargo.toml @@ -8,13 +8,30 @@ description = "Boringssl rustls provider" publish = false [features] -default = ["tls12"] -# Use a FIPS-validated version of boringssl. -fips = ["boring/fips", "boring-sys/fips"] -logging = ["log"] -fips-only = ["boring/fips", "boring-sys/fips"] +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: P-256 and P-384 only (no X25519, X448, P-521, +# or FFDHE). P256Kyber768Draft00 will be added once implemented. +# - Signature algorithms: RSA PKCS#1 / PSS and ECDSA with P-256/P-384 +# only (no P-521, Ed25519, or Ed448). +# Aligned with boring's `fips202205` compliance policy (SP 800-52r2). +fips = ["boring/fips"] + +# 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. 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"] } boring = { workspace = true } diff --git a/boring-rustls-provider/src/aead.rs b/boring-rustls-provider/src/aead.rs index b3cc1ba..cade8a3 100644 --- a/boring-rustls-provider/src/aead.rs +++ b/boring-rustls-provider/src/aead.rs @@ -3,9 +3,9 @@ use std::marker::PhantomData; use aead::{AeadCore, AeadInPlace, Buffer, Nonce, Tag}; use boring::error::ErrorStack; use boring_additions::aead::Algorithm; -use rustls::crypto::cipher::{ - self, make_tls12_aad, make_tls13_aad, BorrowedPayload, Iv, PrefixedPayload, -}; +#[cfg(feature = "tls12")] +use rustls::crypto::cipher::make_tls12_aad; +use rustls::crypto::cipher::{self, make_tls13_aad, BorrowedPayload, Iv, PrefixedPayload}; use rustls::{ConnectionTrafficSecrets, ContentType, ProtocolVersion}; use crate::helper::log_and_map; @@ -20,6 +20,8 @@ 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; @@ -569,7 +571,8 @@ impl Buffer for EncryptBufferAdapter<'_> { #[cfg(test)] mod tests { use hex_literal::hex; - use rustls::crypto::cipher::{AeadKey, Iv}; + use rustls::crypto::cipher::AeadKey; + use rustls::crypto::cipher::Iv; use crate::aead::BoringAeadCrypter; use rustls::quic::PacketKey; @@ -611,7 +614,7 @@ mod tests { let unprotected_header = hex!("4200bff4"); let protector = BoringAeadCrypter::::new( - Iv::new(iv), + Iv::from(iv), &key, rustls::ProtocolVersion::TLSv1_3, ) diff --git a/boring-rustls-provider/src/aead/aes.rs b/boring-rustls-provider/src/aead/aes.rs index 6a5e2af..03e0f3c 100644 --- a/boring-rustls-provider/src/aead/aes.rs +++ b/boring-rustls-provider/src/aead/aes.rs @@ -11,6 +11,7 @@ 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; @@ -55,6 +56,7 @@ 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; diff --git a/boring-rustls-provider/src/aead/chacha20.rs b/boring-rustls-provider/src/aead/chacha20.rs index 2590aab..c0af24c 100644 --- a/boring-rustls-provider/src/aead/chacha20.rs +++ b/boring-rustls-provider/src/aead/chacha20.rs @@ -14,6 +14,7 @@ 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; diff --git a/boring-rustls-provider/src/lib.rs b/boring-rustls-provider/src/lib.rs index 9f39914..b4579ec 100644 --- a/boring-rustls-provider/src/lib.rs +++ b/boring-rustls-provider/src/lib.rs @@ -22,11 +22,11 @@ pub mod tls13; pub mod verify; pub fn provider() -> CryptoProvider { - #[cfg(feature = "fips-only")] + #[cfg(feature = "fips")] { provider_with_ciphers(ALL_FIPS_CIPHER_SUITES.to_vec()) } - #[cfg(not(feature = "fips-only"))] + #[cfg(not(feature = "fips"))] { provider_with_ciphers(ALL_CIPHER_SUITES.to_vec()) } @@ -35,13 +35,13 @@ pub fn provider() -> CryptoProvider { pub fn provider_with_ciphers(ciphers: Vec) -> CryptoProvider { CryptoProvider { cipher_suites: ciphers, - #[cfg(feature = "fips-only")] + #[cfg(feature = "fips")] kx_groups: ALL_FIPS_KX_GROUPS.to_vec(), - #[cfg(not(feature = "fips-only"))] + #[cfg(not(feature = "fips"))] kx_groups: ALL_KX_GROUPS.to_vec(), - #[cfg(feature = "fips-only")] + #[cfg(feature = "fips")] signature_verification_algorithms: verify::ALL_FIPS_ALGORITHMS, - #[cfg(not(feature = "fips-only"))] + #[cfg(not(feature = "fips"))] signature_verification_algorithms: verify::ALL_ALGORITHMS, secure_random: &Provider, key_provider: &Provider, @@ -99,18 +99,15 @@ static ALL_CIPHER_SUITES: &[SupportedCipherSuite] = &[ SupportedCipherSuite::Tls12(&tls12::ECDHE_RSA_AES128_GCM_SHA256), ]; -/// Allowed KX curves for FIPS are recommended -/// in [NIST SP 800-186](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-186.pdf) +/// 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. /// -/// See Sec. 3.1.2 Table 2 -/// Ordered in decending order of security strength +/// See Section 3.3.1 and 3.4.2.2. +// TODO: Add P256Kyber768Draft00 once the PQ hybrid KEM is implemented (Step 3). #[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 + &kx::Secp256r1 as _, // P-256 + &kx::Secp384r1 as _, // P-384 ]; #[allow(unused)] diff --git a/boring-rustls-provider/src/verify.rs b/boring-rustls-provider/src/verify.rs index 15eaea0..aa2cda2 100644 --- a/boring-rustls-provider/src/verify.rs +++ b/boring-rustls-provider/src/verify.rs @@ -61,6 +61,13 @@ 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: &[ @@ -72,9 +79,6 @@ 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: &[ ( @@ -109,11 +113,5 @@ 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/tests/e2e.rs b/boring-rustls-provider/tests/e2e.rs index 21b017a..9411fd2 100644 --- a/boring-rustls-provider/tests/e2e.rs +++ b/boring-rustls-provider/tests/e2e.rs @@ -5,11 +5,12 @@ use tokio::{ net::TcpStream, }; -use boring_rustls_provider::{tls12, tls13}; -use rustls::{ - version::{TLS12, TLS13}, - ClientConfig, ServerConfig, SupportedCipherSuite, -}; +#[cfg(feature = "tls12")] +use boring_rustls_provider::tls12; +use boring_rustls_provider::tls13; +#[cfg(feature = "tls12")] +use rustls::version::TLS12; +use rustls::{version::TLS13, ClientConfig, ServerConfig, SupportedCipherSuite}; use rustls_pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer}; use tokio::net::TcpListener; use tokio_rustls::{TlsAcceptor, TlsConnector}; @@ -41,17 +42,107 @@ async fn test_tls13_crypto() { } #[test] -#[cfg(any(feature = "fips", feature = "fips-only"))] +#[cfg(feature = "fips")] fn is_fips_enabled() { assert!(boring::fips::enabled()); } #[test] -#[cfg(not(any(feature = "fips", feature = "fips-only")))] +#[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_restricts_kx_groups() { + use rustls::NamedGroup; + + let provider = boring_rustls_provider::provider(); + let groups = provider + .kx_groups + .iter() + .map(|group| group.name()) + .collect::>(); + + assert!(groups.contains(&NamedGroup::secp256r1)); + assert!(groups.contains(&NamedGroup::secp384r1)); + for group in groups { + assert!( + matches!(group, 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)); +} + +#[cfg(feature = "tls12")] #[tokio::test] async fn test_tls12_ec_crypto() { let pki = TestPki::new(&rcgen::PKCS_ECDSA_P256_SHA256); @@ -78,6 +169,7 @@ async fn test_tls12_ec_crypto() { } } +#[cfg(feature = "tls12")] #[tokio::test] async fn test_tls12_rsa_crypto() { let pki = TestPki::new(&rcgen::PKCS_RSA_SHA256); @@ -188,9 +280,14 @@ 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(&[&TLS12, &TLS13]) + .with_protocol_versions(versions) .unwrap() .with_no_client_auth() .with_single_cert(vec![self.server_cert_der], self.server_key_der)