Compare commits

..

10 commits

Author SHA1 Message Date
b667e1450b Make algs public
Some checks failed
Rust check / fmt (push) Has been cancelled
Rust check / test-default (push) Has been cancelled
Rust check / test-tls12 (push) Has been cancelled
Rust check / test-logging-tls12 (push) Has been cancelled
Rust check / test-mlkem (push) Has been cancelled
Rust check / test-mlkem-tls12 (push) Has been cancelled
Rust check / test-fips (push) Has been cancelled
Rust check / check-fips (push) Has been cancelled
2026-04-13 11:44:03 +02:00
Jan Rüth
b88c87235d Harden crypto provider error handling and FIPS reporting
Implement provider-wide FIPS semantics by filtering non-FIPS suites in provider_with_ciphers() and wiring fips() reporting across provider components, KX groups, AEADs, and signature verifiers.

Replace panic-prone hotpath behavior with error returns across TLS/QUIC AEAD setup and header protection, enforce HKDF output bounds, and remove shared HMAC context cloning to tighten runtime safety.

Rework signing and verification paths to support SEC1 EC key loading, curve-aware scheme selection, and consistent malformed-input error handling without panics.

Add comprehensive regression coverage for malformed KX shares, verifier inputs, AEAD truncation and constructor failures, plus a panic-surface test that scans runtime provider/additions code for new panic constructs unless explicitly allowlisted.

Update the example client to return Result and eliminate non-test unwrap-style exits.
2026-04-10 17:17:44 +02:00
Jan Rüth
bbd0ccf0b8 Harden key material zeroization paths
Reduce secret lifetime in HKDF and HMAC internals, avoid extra shared-secret copying in key exchange, and add SHA384 HMAC coverage to guard output sizing.
2026-04-10 15:46:44 +02:00
Jan Rüth
c71d5bbfd8 Drop X448, P-521, FFDHE2048 to match boring's default group list 2026-04-10 15:34:22 +02:00
Jan Rüth
fafbf296b1 Add X25519MLKEM768 post-quantum hybrid key exchange
Implement the X25519MLKEM768 hybrid key exchange group per
draft-ietf-tls-ecdhe-mlkem-00, combining ML-KEM-768 (FIPS 203) with
X25519 for post-quantum/classical hybrid key agreement.

- New mlkem feature gating X25519MLKEM768 via boring's mlkem module
- fips feature now implies mlkem so PQ is always available in FIPS mode
- X25519MLKEM768 is the preferred (first) group in both FIPS and
  non-FIPS configurations when mlkem is enabled
- Uses boring::mlkem for ML-KEM-768 and direct X25519 FFI for the
  classical component (no SPKI overhead)
- Overrides start_and_complete() for server-side KEM encapsulation
- Wire format: ML-KEM component first in all encodings (client share
  1216 bytes, server share 1120 bytes, shared secret 64 bytes)
- TLS 1.3 only (usable_for_version rejects TLS 1.2)
- Unit tests: hybrid round-trip, invalid share rejection, version
  and FIPS flag checks
- E2E tests: self-to-self PQ TLS handshake, FIPS group assertions
- Cloudflare interop tests (ignored): verify kex=X25519MLKEM768 via
  /cdn-cgi/trace for TLS 1.3, verify classical fallback for TLS 1.2
- Update README with PQ section, feature docs, and FIPS KX updates
2026-04-10 15:34:22 +02:00
Jan Rüth
271acbb315 Bump boring to v5, align FIPS to SP 800-52r2, clean up features
- Bump boring/boring-sys from v4 to v5 (zero API breaks)
- Merge fips/fips-only into a single fips feature that both enables
  FIPS-validated BoringSSL and restricts algorithms to SP 800-52r2
- Tighten FIPS KX groups to P-256 and P-384 only (aligned with
  boring's fips202205 compliance policy)
- Remove ECDSA_P521_SHA512 from FIPS signature verification set
- Simplify fips feature to forward boring/fips only (drop redundant
  boring-sys/fips)
- Add fips-precompiled as deprecated alias matching boring's naming
- Change default features to empty (TLS 1.2 now requires explicit
  tls12 feature opt-in)
- Gate TLS 1.2 code paths properly so the crate compiles and passes
  tests with default (TLS 1.3 only) features
- Update README to reflect current state: boring v5, feature docs,
  FIPS mode documentation, workspace structure
2026-04-10 12:38:26 +02:00
Jan Rüth
490340afa7 Bump and cleanup deps and fix lints 2025-10-07 18:44:13 +02:00
John Howard
aa6e1c36f8 Update to rustls 0.23 2024-03-27 08:34:11 +01:00
John Howard
03b48134ca Fix QUIC compilation and FIPS features
Fix a build issue due to lack of trait constraints.

Pass FIPS feature flag through and test it
2024-02-21 08:44:20 +01:00
Jan Rüth
b1188e2ef1 Update to v22 2023-12-08 13:38:17 +01:00
34 changed files with 2481 additions and 759 deletions

View file

@ -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,95 @@ 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"
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

View file

@ -2,10 +2,10 @@
members = [
# things that should probably be in boring crate
"boring-additions",
# things that should probably be in boring-sys crate
"boring-sys-additions",
# the main library and tests
"boring-rustls-provider",
# things that should probably be in boring-sys crate
"boring-sys-additions",
# tests and example code
"examples",
]
@ -17,11 +17,11 @@ default-members = [
resolver = "2"
[workspace.dependencies]
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" }
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" }

View file

@ -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)"
cargo build --all-targets $(CARGO_FEATURES)

View file

@ -2,55 +2,59 @@
[![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). 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):
```
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
### Key Exchange Groups
Matches boring's default supported group list:
`ECDHE` with curves:
```
X25519MLKEM768 (0x11ec, requires mlkem feature, TLS 1.3 only)
X25519
X448
secp256r1
secp384r1
secp521r1
secp256r1 (P-256)
secp384r1 (P-384)
```
When `mlkem` is enabled, X25519MLKEM768 is the preferred (first) group in both
FIPS and non-FIPS configurations.
`FFDHE` with:
```
ffdhe2048
```
### Signature Generation / Verification
### Signature Algorithms
```
RSA_PKCS1_SHA256
@ -66,6 +70,47 @@ 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

View file

@ -8,7 +8,6 @@ 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"

View file

@ -78,11 +78,7 @@ 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<Self, ErrorStack> {
assert_eq!(aead_alg.key_length(), key.len());
boring_sys::init();
let this = unsafe {
@ -116,10 +112,6 @@ 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],
@ -127,8 +119,9 @@ impl Crypter {
buffer: &mut [u8],
tag: &mut [u8],
) -> Result<usize, ErrorStack> {
assert!(tag.len() >= self.max_overhead);
assert_eq!(nonce.len(), self.nonce_len);
if tag.len() < self.max_overhead || nonce.len() != self.nonce_len {
return Err(ErrorStack::get());
}
let mut tag_len = tag.len();
unsafe {
@ -156,9 +149,6 @@ 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],
@ -166,7 +156,9 @@ impl Crypter {
buffer: &mut [u8],
tag: &[u8],
) -> Result<(), ErrorStack> {
assert_eq!(nonce.len(), self.nonce_len);
if nonce.len() != self.nonce_len {
return Err(ErrorStack::get());
}
unsafe {
cvt(boring_sys::EVP_AEAD_CTX_open_gather(
@ -188,11 +180,11 @@ impl Crypter {
#[cfg(test)]
mod tests {
use super::Crypter;
use super::{Algorithm, Crypter};
#[test]
fn in_out() {
let key = Crypter::new(&super::Algorithm::aes_128_gcm(), &[0u8; 16]).unwrap();
let key = Crypter::new(&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);
@ -207,4 +199,35 @@ 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());
}
}

View file

@ -5,8 +5,6 @@ use std::{
use foreign_types::{ForeignType, ForeignTypeRef, Opaque};
use crate::helper::{cvt, cvt_p};
pub struct HmacCtxRef(Opaque);
unsafe impl ForeignTypeRef for HmacCtxRef {
@ -35,20 +33,6 @@ 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 {

View file

@ -8,31 +8,52 @@ 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 = []
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.
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"
webpki = { workspace = true, features = ["alloc", "std"] }
zeroize = "1"
[dev-dependencies]
hex-literal = "0.4"
rcgen = "0.11.3"
hex-literal = "1"
rcgen = "0.12"
tokio = { version = "1.34", features = ["macros", "rt", "net", "io-util", "io-std"] }
tokio-rustls = { workspace = true }
webpki-roots = { workspace = true }

View file

@ -1,9 +1,11 @@
use std::marker::PhantomData;
use aead::{AeadCore, AeadInPlace, Nonce, Tag};
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, Iv};
#[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;
@ -18,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;
@ -25,6 +29,15 @@ 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;
@ -32,14 +45,14 @@ pub(crate) trait BoringCipher {
fn extract_keys(key: cipher::AeadKey, iv: cipher::Iv) -> ConnectionTrafficSecrets;
}
pub(crate) trait QuicCipher {
pub(crate) trait QuicCipher: Send + Sync {
/// 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]) -> [u8; 5];
fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> Result<[u8; 5], rustls::Error>;
}
pub(crate) trait BoringAead: BoringCipher + AeadCore + Send + Sync {}
@ -64,19 +77,21 @@ impl<T: BoringAead> AeadCore for BoringAeadCrypter<T> {
impl<T: BoringAead> BoringAeadCrypter<T> {
/// Creates a new aead crypter
pub fn new(iv: Iv, key: &[u8], tls_version: ProtocolVersion) -> Result<Self, ErrorStack> {
assert!(match tls_version {
let tls_version_supported = 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 = <T as BoringCipher>::new_cipher();
assert_eq!(
cipher.nonce_len(),
rustls::crypto::cipher::Nonce::new(&iv, 0).0.len()
);
if cipher.nonce_len() != rustls::crypto::cipher::Nonce::new(&iv, 0).0.len() {
return Err(ErrorStack::get());
}
let crypter = BoringAeadCrypter {
crypter: boring_additions::aead::Crypter::new(&cipher, key)?,
@ -123,11 +138,10 @@ where
{
fn encrypt(
&mut self,
msg: cipher::BorrowedPlainMessage,
msg: cipher::OutboundPlainMessage,
seq: u64,
) -> Result<cipher::OpaqueMessage, rustls::Error> {
) -> Result<cipher::OutboundOpaqueMessage, rustls::Error> {
let nonce = cipher::Nonce::new(&self.iv, seq);
match self.tls_version {
#[cfg(feature = "tls12")]
ProtocolVersion::TLSv1_2 => {
@ -136,50 +150,57 @@ where
let total_len = self.encrypted_payload_len(msg.payload.len());
let mut full_payload = Vec::with_capacity(total_len);
let mut full_payload = PrefixedPayload::with_capacity(total_len);
full_payload.extend_from_slice(&nonce.0.as_ref()[fixed_iv_len..]);
full_payload.extend_from_slice(msg.payload);
full_payload.extend_from_chunks(&msg.payload);
full_payload.extend_from_slice(&vec![0u8; self.crypter.max_overhead()]);
let (_, payload) = full_payload.split_at_mut(explicit_nonce_len);
let (_, payload) = full_payload.as_mut().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::OpaqueMessage::new(msg.typ, msg.version, full_payload))
.map(|_| cipher::OutboundOpaqueMessage::new(msg.typ, msg.version, full_payload))
}
ProtocolVersion::TLSv1_3 => {
let total_len = self.encrypted_payload_len(msg.payload.len());
let mut payload = Vec::with_capacity(total_len);
payload.extend_from_slice(msg.payload);
payload.push(msg.typ.get_u8());
let mut payload = PrefixedPayload::with_capacity(total_len);
payload.extend_from_chunks(&msg.payload);
payload.extend_from_slice(&msg.typ.to_array());
let aad = cipher::make_tls13_aad(total_len);
self.encrypt_in_place(Nonce::<T>::from_slice(&nonce.0), &aad, &mut payload)
.map_err(|_| rustls::Error::EncryptError)
.map(|_| {
cipher::OpaqueMessage::new(
ContentType::ApplicationData,
ProtocolVersion::TLSv1_2,
payload,
)
})
self.encrypt_in_place(
Nonce::<T>::from_slice(&nonce.0),
&aad,
&mut EncryptBufferAdapter(&mut payload),
)
.map_err(|_| rustls::Error::EncryptError)
.map(|_| {
cipher::OutboundOpaqueMessage::new(
ContentType::ApplicationData,
ProtocolVersion::TLSv1_2,
payload,
)
})
}
_ => unimplemented!(),
_ => Err(rustls::Error::EncryptError),
}
}
fn encrypted_payload_len(&self, payload_len: usize) -> usize {
match self.tls_version {
ProtocolVersion::TLSv1_2 => {
payload_len + self.crypter.max_overhead() + <T as BoringCipher>::EXPLICIT_NONCE_LEN
}
ProtocolVersion::TLSv1_3 => payload_len + 1 + self.crypter.max_overhead(),
_ => unimplemented!(),
}
let per_record_overhead = match self.tls_version {
ProtocolVersion::TLSv1_2 => self
.crypter
.max_overhead()
.saturating_add(<T as BoringCipher>::EXPLICIT_NONCE_LEN),
ProtocolVersion::TLSv1_3 => 1usize.saturating_add(self.crypter.max_overhead()),
_ => return payload_len,
};
payload_len.saturating_add(per_record_overhead)
}
}
@ -187,23 +208,30 @@ impl<T> cipher::MessageDecrypter for BoringAeadCrypter<T>
where
T: BoringAead,
{
fn decrypt(
fn decrypt<'a>(
&mut self,
mut m: cipher::OpaqueMessage,
mut m: cipher::InboundOpaqueMessage<'a>,
seq: u64,
) -> Result<cipher::PlainMessage, rustls::Error> {
) -> Result<cipher::InboundPlainMessage<'a>, rustls::Error> {
match self.tls_version {
#[cfg(feature = "tls12")]
ProtocolVersion::TLSv1_2 => {
let explicit_nonce_len = <T as BoringCipher>::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() - self.crypter.max_overhead() - explicit_nonce_len;
let actual_payload_length = m.payload.len() - tag_len - explicit_nonce_len;
let aad = make_tls12_aad(seq, m.typ, m.version, actual_payload_length);
let payload = m.payload_mut();
let payload = &mut m.payload;
// get the nonce
let (explicit_nonce, payload) = payload.split_at_mut(explicit_nonce_len);
@ -211,7 +239,9 @@ where
let nonce = {
let fixed_iv_len = <T as BoringCipher>::FIXED_IV_LEN;
assert_eq!(explicit_nonce_len + fixed_iv_len, 12);
if explicit_nonce_len + fixed_iv_len != 12 {
return Err(rustls::Error::DecryptError);
}
// grab the IV by constructing a nonce, this is just an xor
let iv = cipher::Nonce::new(&self.iv, seq).0;
@ -222,30 +252,33 @@ where
};
// split off the authentication tag
let (payload, tag) =
payload.split_at_mut(payload.len() - self.crypter.max_overhead());
let (payload, tag) = payload.split_at_mut(payload.len() - tag_len);
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_mut().rotate_left(explicit_nonce_len);
m.payload.rotate_left(explicit_nonce_len);
// truncate buffer to the actual payload
m.payload_mut().truncate(actual_payload_length);
m.payload.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::<T>::from_slice(&nonce.0), &aad, m.payload_mut())
.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::<T>::from_slice(&nonce.0),
&aad,
&mut DecryptBufferAdapter(&mut m.payload),
)
.map_err(|_| rustls::Error::DecryptError)
.and_then(|_| m.into_tls13_unpadded_message())
}
_ => unimplemented!(),
_ => Err(rustls::Error::DecryptError),
}
}
}
@ -278,6 +311,10 @@ 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
@ -290,6 +327,76 @@ where
fn tag_len(&self) -> usize {
<T as BoringCipher>::TAG_LEN
}
fn confidentiality_limit(&self) -> u64 {
<T as BoringCipher>::CONFIDENTIALITY_LIMIT
}
fn integrity_limit(&self) -> u64 {
<T as BoringCipher>::INTEGRITY_LIMIT
}
}
struct InvalidMessageEncrypter;
impl cipher::MessageEncrypter for InvalidMessageEncrypter {
fn encrypt(
&mut self,
_msg: cipher::OutboundPlainMessage<'_>,
_seq: u64,
) -> Result<cipher::OutboundOpaqueMessage, rustls::Error> {
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<cipher::InboundPlainMessage<'a>, rustls::Error> {
Err(rustls::Error::DecryptError)
}
}
struct InvalidPacketKey<T>(PhantomData<T>);
impl<T: BoringCipher + QuicCipher> rustls::quic::PacketKey for InvalidPacketKey<T> {
fn encrypt_in_place(
&self,
_packet_number: u64,
_header: &[u8],
_payload: &mut [u8],
) -> Result<rustls::quic::Tag, rustls::Error> {
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 {
<T as BoringCipher>::TAG_LEN
}
fn confidentiality_limit(&self) -> u64 {
0
}
fn integrity_limit(&self) -> u64 {
0
}
}
pub(crate) struct Aead<T>(PhantomData<T>);
@ -300,17 +407,31 @@ impl<T> Aead<T> {
impl<T: BoringAead + 'static> cipher::Tls13AeadAlgorithm for Aead<T> {
fn encrypter(&self, key: cipher::AeadKey, iv: cipher::Iv) -> Box<dyn cipher::MessageEncrypter> {
Box::new(
BoringAeadCrypter::<T>::new(iv, key.as_ref(), ProtocolVersion::TLSv1_3)
.expect("failed to create AEAD crypter"),
)
match BoringAeadCrypter::<T>::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)
}
}
}
fn decrypter(&self, key: cipher::AeadKey, iv: cipher::Iv) -> Box<dyn cipher::MessageDecrypter> {
Box::new(
BoringAeadCrypter::<T>::new(iv, key.as_ref(), ProtocolVersion::TLSv1_3)
.expect("failed to create AEAD crypter"),
)
match BoringAeadCrypter::<T>::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)
}
}
}
fn key_len(&self) -> usize {
@ -324,6 +445,10 @@ impl<T: BoringAead + 'static> cipher::Tls13AeadAlgorithm for Aead<T> {
) -> Result<ConnectionTrafficSecrets, cipher::UnsupportedOperationError> {
Ok(<T as BoringCipher>::extract_keys(key, iv))
}
fn fips(&self) -> bool {
cfg!(feature = "fips") && <T as BoringCipher>::FIPS_APPROVED
}
}
#[cfg(feature = "tls12")]
@ -337,24 +462,42 @@ impl<T: BoringAead + 'static> cipher::Tls12AeadAlgorithm for Aead<T> {
let mut full_iv = Vec::with_capacity(iv.len() + extra.len());
full_iv.extend_from_slice(iv);
full_iv.extend_from_slice(extra);
Box::new(
BoringAeadCrypter::<T>::new(Iv::copy(&full_iv), key.as_ref(), ProtocolVersion::TLSv1_2)
.expect("failed to create AEAD crypter"),
)
match BoringAeadCrypter::<T>::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)
}
}
}
fn decrypter(&self, key: cipher::AeadKey, iv: &[u8]) -> Box<dyn cipher::MessageDecrypter> {
let mut pseudo_iv = Vec::with_capacity(iv.len() + <T as BoringCipher>::EXPLICIT_NONCE_LEN);
pseudo_iv.extend_from_slice(iv);
pseudo_iv.extend_from_slice(&vec![0u8; <T as BoringCipher>::EXPLICIT_NONCE_LEN]);
Box::new(
BoringAeadCrypter::<T>::new(
Iv::copy(&pseudo_iv),
key.as_ref(),
ProtocolVersion::TLSv1_2,
)
.expect("failed to create AEAD crypter"),
)
match BoringAeadCrypter::<T>::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)
}
}
}
fn key_block_shape(&self) -> cipher::KeyBlockShape {
@ -374,7 +517,13 @@ impl<T: BoringAead + 'static> cipher::Tls12AeadAlgorithm for Aead<T> {
let nonce = {
let fixed_iv_len = <T as BoringCipher>::FIXED_IV_LEN;
let explicit_nonce_len = <T as BoringCipher>::EXPLICIT_NONCE_LEN;
assert_eq!(explicit_nonce_len + fixed_iv_len, 12);
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);
}
// grab the IV by constructing a nonce, this is just an xor
@ -385,6 +534,10 @@ impl<T: BoringAead + 'static> cipher::Tls12AeadAlgorithm for Aead<T> {
};
Ok(<T as BoringCipher>::extract_keys(key, Iv::copy(&nonce)))
}
fn fips(&self) -> bool {
cfg!(feature = "fips") && <T as BoringCipher>::FIPS_APPROVED
}
}
struct QuicHeaderProtector<T: QuicCipher> {
@ -400,8 +553,8 @@ impl<T: QuicCipher> QuicHeaderProtector<T> {
first: &mut u8,
packet_number: &mut [u8],
remove: bool,
) {
let mask = T::header_protection_mask(self.key.as_ref(), sample);
) -> Result<(), rustls::Error> {
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 {
@ -428,6 +581,8 @@ impl<T: QuicCipher> QuicHeaderProtector<T> {
for (pn_byte, m) in packet_number.iter_mut().zip(&mask[1..]).take(pn_length) {
*pn_byte ^= m;
}
Ok(())
}
}
@ -443,7 +598,7 @@ impl<T: QuicCipher> rustls::quic::HeaderProtectionKey for QuicHeaderProtector<T>
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(())
}
@ -458,7 +613,7 @@ impl<T: QuicCipher> rustls::quic::HeaderProtectionKey for QuicHeaderProtector<T>
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(())
}
@ -473,10 +628,13 @@ where
T: QuicCipher + BoringAead + 'static,
{
fn packet_key(&self, key: cipher::AeadKey, iv: Iv) -> Box<dyn rustls::quic::PacketKey> {
Box::new(
BoringAeadCrypter::<T>::new(iv, key.as_ref(), ProtocolVersion::TLSv1_3)
.expect("failed to create AEAD crypter"),
)
match BoringAeadCrypter::<T>::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::<T>(PhantomData))
}
}
}
fn header_protection_key(
@ -492,17 +650,78 @@ where
fn aead_key_len(&self) -> usize {
<T as QuicCipher>::KEY_SIZE
}
fn fips(&self) -> bool {
cfg!(feature = "fips") && <T as BoringCipher>::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::{AeadKey, Iv};
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 crate::aead::BoringAeadCrypter;
use rustls::quic::PacketKey;
use super::{chacha20::ChaCha20Poly1305, QuicHeaderProtector};
use super::aes::Aes128;
use super::{chacha20::ChaCha20Poly1305, BoringCipher, QuicHeaderProtector};
#[test]
fn quic_header_protection_short() {
@ -519,14 +738,97 @@ mod tests {
phantom: std::marker::PhantomData::<ChaCha20Poly1305>,
};
protector.rfc9001_header_protection(&sample, &mut first[0], packet_number, false);
protector
.rfc9001_header_protection(&sample, &mut first[0], packet_number, false)
.expect("valid sample should protect QUIC header");
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);
protector
.rfc9001_header_protection(&sample, &mut first[0], packet_number, true)
.expect("valid sample should unprotect QUIC header");
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::<ChaCha20Poly1305>,
};
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::<Aes128>::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::<Aes128>::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::<Aes128>::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
@ -539,9 +841,9 @@ mod tests {
let unprotected_header = hex!("4200bff4");
let protector = BoringAeadCrypter::<ChaCha20Poly1305>::new(
Iv::new(iv),
Iv::from(iv),
&key,
rustls::ProtocolVersion::TLSv1_3,
ProtocolVersion::TLSv1_3,
)
.unwrap();
@ -560,4 +862,47 @@ mod tests {
assert_eq!(cleartext, expected_cleartext);
}
#[test]
fn quic_decrypt_rejects_truncated_payload() {
let key = [0u8; <ChaCha20Poly1305 as BoringCipher>::KEY_SIZE];
let iv = [0u8; 12];
let crypter = BoringAeadCrypter::<ChaCha20Poly1305>::new(
Iv::from(iv),
&key,
ProtocolVersion::TLSv1_3,
)
.unwrap();
let mut payload = [0u8; <ChaCha20Poly1305 as BoringCipher>::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; <Aes128 as BoringCipher>::KEY_SIZE];
let iv = [0u8; 12];
let mut decrypter =
BoringAeadCrypter::<Aes128>::new(Iv::from(iv), &key, ProtocolVersion::TLSv1_2).unwrap();
let min_payload_len =
<Aes128 as BoringCipher>::EXPLICIT_NONCE_LEN + <Aes128 as BoringCipher>::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));
}
}

View file

@ -11,12 +11,17 @@ 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()
}
@ -36,7 +41,7 @@ impl QuicCipher for Aes128 {
const KEY_SIZE: usize = <Self as BoringCipher>::KEY_SIZE;
const SAMPLE_LEN: usize = 16;
fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> [u8; 5] {
fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> Result<[u8; 5], rustls::Error> {
quic_header_protection_mask::<
{ <Self as QuicCipher>::KEY_SIZE },
{ <Self as QuicCipher>::SAMPLE_LEN },
@ -52,12 +57,17 @@ 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()
}
@ -77,7 +87,7 @@ impl QuicCipher for Aes256 {
const KEY_SIZE: usize = <Self as BoringCipher>::KEY_SIZE;
const SAMPLE_LEN: usize = 16;
fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> [u8; 5] {
fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> Result<[u8; 5], rustls::Error> {
quic_header_protection_mask::<
{ <Self as QuicCipher>::KEY_SIZE },
{ <Self as QuicCipher>::SAMPLE_LEN },
@ -89,19 +99,32 @@ fn quic_header_protection_mask<const KEY_SIZE: usize, const SAMPLE_LEN: usize>(
cipher: boring::symm::Cipher,
hp_key: &[u8],
sample: &[u8],
) -> [u8; 5] {
assert!(hp_key.len() == KEY_SIZE);
assert!(sample.len() >= SAMPLE_LEN);
) -> 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()));
}
let mut output = [0u8; SAMPLE_LEN];
let mut crypter = boring::symm::Crypter::new(cipher, boring::symm::Mode::Encrypt, hp_key, None)
.expect("failed getting crypter");
.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 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())
})?;
output[..5].try_into().unwrap()
let mut mask = [0u8; 5];
mask.copy_from_slice(&output[..5]);
Ok(mask)
}
#[cfg(test)]

View file

@ -14,12 +14,17 @@ 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()
}
@ -33,13 +38,21 @@ impl QuicCipher for ChaCha20Poly1305 {
const KEY_SIZE: usize = 32;
const SAMPLE_LEN: usize = 16;
fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> [u8; 5] {
assert!(hp_key.len() == <Self as QuicCipher>::KEY_SIZE);
assert!(sample.len() >= <Self as QuicCipher>::SAMPLE_LEN);
fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> Result<[u8; 5], rustls::Error> {
if hp_key.len() != <Self as QuicCipher>::KEY_SIZE {
return Err(rustls::Error::General(
"header protection key of invalid length".into(),
));
}
if sample.len() != <Self as QuicCipher>::SAMPLE_LEN {
return Err(rustls::Error::General("sample of invalid length".into()));
}
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 counter = u32::from_le_bytes(sample[0..4].try_into().unwrap());
let mut counter_bytes = [0u8; 4];
counter_bytes.copy_from_slice(&sample[0..4]);
let counter = u32::from_le_bytes(counter_bytes);
// RFC9001 5.4.4: The remaining 12 bytes are used as the nonce.
let nonce = &sample[4..16];
unsafe {
@ -52,7 +65,7 @@ impl QuicCipher for ChaCha20Poly1305 {
counter,
);
};
mask
Ok(mask)
}
}
@ -91,7 +104,8 @@ 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);
let mask = ChaCha20Poly1305::header_protection_mask(&hp_key, &sample)
.expect("valid QUIC sample/key should produce a mask");
assert_eq!(mask, expected_mask);
}
}

View file

@ -24,10 +24,14 @@ 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,
_ => unimplemented!(),
_ => unreachable!("hash::Hash is only instantiated with SHA-2 digests"),
}
}
fn fips(&self) -> bool {
cfg!(feature = "fips")
}
fn output_len(&self) -> usize {
MessageDigest::from_nid(self.0)
.expect("failed getting digest")

View file

@ -38,6 +38,6 @@ pub(crate) fn log_and_map<E: core::fmt::Display, T>(func: &'static str, e: E, ma
}
#[cfg(not(feature = "log"))]
pub(crate) fn log_and_map<E: core::fmt::Display, T>(func: &'static str, e: E, mapped: T) -> T {
pub(crate) fn log_and_map<E: core::fmt::Display, T>(_func: &'static str, _e: E, mapped: T) -> T {
mapped
}

View file

@ -2,6 +2,7 @@ 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};
@ -67,7 +68,7 @@ impl<T: BoringHash> RustlsHkdf for Hkdf<T> {
let digest = T::new_hash();
let hash_size = digest.size();
let mut prk = [0u8; boring_sys::EVP_MAX_MD_SIZE as usize];
let mut prk = Zeroizing::new([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
@ -103,7 +104,7 @@ impl<T: BoringHash> RustlsHkdf for Hkdf<T> {
okm: &rustls::crypto::tls13::OkmBlock,
) -> Box<dyn rustls::crypto::tls13::HkdfExpander> {
let okm = okm.as_ref();
let mut prk = [0u8; boring_sys::EVP_MAX_MD_SIZE as usize];
let mut prk = Zeroizing::new([0u8; boring_sys::EVP_MAX_MD_SIZE as usize]);
let prk_len = okm.len();
prk[..prk_len].copy_from_slice(okm);
@ -121,7 +122,7 @@ impl<T: BoringHash> RustlsHkdf for Hkdf<T> {
message: &[u8],
) -> rustls::crypto::hmac::Tag {
let digest = T::new_hash();
let mut hash = [0u8; boring_sys::EVP_MAX_MD_SIZE as usize];
let mut hash = Zeroizing::new([0u8; boring_sys::EVP_MAX_MD_SIZE as usize]);
let mut hash_len = 0u32;
unsafe {
cvt_p(boring_sys::HMAC(
@ -137,10 +138,14 @@ impl<T: BoringHash> RustlsHkdf for Hkdf<T> {
}
rustls::crypto::hmac::Tag::new(&hash[..hash_len as usize])
}
fn fips(&self) -> bool {
cfg!(feature = "fips")
}
}
struct HkdfExpander {
prk: [u8; boring_sys::EVP_MAX_MD_SIZE as usize],
prk: Zeroizing<[u8; boring_sys::EVP_MAX_MD_SIZE as usize]>,
prk_len: usize,
digest: MessageDigest,
}
@ -162,9 +167,17 @@ 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 {
boring_sys::HKDF_expand(
cvt(boring_sys::HKDF_expand(
output.as_mut_ptr(),
output.len(),
self.digest.as_ptr(),
@ -172,8 +185,9 @@ impl tls13::HkdfExpander for HkdfExpander {
self.prk_len,
info_concat.as_ptr(),
info_concat.len(),
);
};
))
.map_err(|_| tls13::OutputLengthError)?;
}
Ok(())
}
@ -187,7 +201,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 = [0u8; boring_sys::EVP_MAX_MD_SIZE as usize];
let mut output = Zeroizing::new([0u8; boring_sys::EVP_MAX_MD_SIZE as usize]);
let output_len = self.hash_len();
self.expand_slice(info, &mut output[..output_len])
@ -200,3 +214,35 @@ 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::<Sha256>::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::<Sha256>::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));
}
}

View file

@ -4,6 +4,7 @@ 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};
@ -19,18 +20,11 @@ struct BoringHmac(pub boring::nid::Nid);
impl crypto::hmac::Hmac for BoringHmac {
fn with_key(&self, key: &[u8]) -> Box<dyn crypto::hmac::Key> {
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: key.to_vec(),
key: Zeroizing::new(key.to_vec()),
})
}
@ -39,34 +33,35 @@ 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: Vec<u8>,
key: Zeroizing<Vec<u8>>,
}
impl BoringHmacKey {
fn init(&self) {
fn init(ctx: &HmacCtx, key: &[u8], md: MessageDigest) {
unsafe {
// initialize a new hmac
cvt(boring_sys::HMAC_Init_ex(
self.ctx.as_ptr(),
self.key.as_ptr() as *const c_void,
self.key.len(),
self.md.as_ptr(),
ctx.as_ptr(),
key.as_ptr() as *const c_void,
key.len(),
md.as_ptr(),
ptr::null_mut(),
))
}
.expect("failed initializing hmac");
}
fn update(&self, bytes: &[u8]) {
fn update(ctx: &HmacCtx, bytes: &[u8]) {
unsafe {
cvt(boring_sys::HMAC_Update(
self.ctx.as_ptr(),
ctx.as_ptr(),
bytes.as_ptr(),
bytes.len(),
))
@ -74,11 +69,11 @@ impl BoringHmacKey {
.expect("failed updating hmac");
}
fn finish(&self, out: &mut [u8]) -> usize {
fn finish(ctx: &HmacCtx, out: &mut [u8]) -> usize {
let mut out_len = 0;
unsafe {
cvt(boring_sys::HMAC_Final(
self.ctx.as_ptr(),
ctx.as_ptr(),
out.as_mut_ptr(),
&mut out_len,
))
@ -86,21 +81,28 @@ 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 {
self.init();
let ctx = Self::new_ctx();
Self::init(&ctx, self.key.as_slice(), self.md);
self.update(first);
Self::update(&ctx, first);
for m in middle {
self.update(m);
Self::update(&ctx, m);
}
self.update(last);
Self::update(&ctx, last);
let mut out = [0u8; 32];
let out_len = self.finish(&mut out);
let mut out = Zeroizing::new([0u8; boring_sys::EVP_MAX_MD_SIZE as usize]);
let out_len = Self::finish(&ctx, &mut out[..]);
crypto::hmac::Tag::new(&out[..out_len])
}
@ -112,7 +114,7 @@ impl crypto::hmac::Key for BoringHmacKey {
#[cfg(test)]
mod tests {
use super::SHA256;
use super::{SHA256, SHA384};
use hex_literal::hex;
#[test]
@ -141,4 +143,23 @@ 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);
}
}

View file

@ -1,108 +0,0 @@
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<Box<(dyn ActiveKeyExchange + 'static)>, 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<Box<(dyn ActiveKeyExchange + 'static)>, 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<Box<(dyn ActiveKeyExchange + 'static)>, 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<Box<(dyn ActiveKeyExchange + 'static)>, 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<Box<(dyn ActiveKeyExchange + 'static)>, 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<Box<(dyn ActiveKeyExchange + 'static)>, 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
}
}

View file

@ -1,123 +0,0 @@
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<Private>,
pub_bytes: Vec<u8>,
key_type: DhKeyType,
}
impl KeyExchange {
// Generate a new KeyExchange with a random FFDHE_2048 private key
pub fn generate_ffdhe_2048() -> Result<Self, ErrorStack> {
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<Vec<u8>, 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<Self>,
peer_pub_key: &[u8],
) -> Result<crypto::SharedSecret, rustls::Error> {
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)
}
}

View file

@ -1,20 +1,22 @@
use std::{
mem::MaybeUninit,
ptr::{self},
};
#[cfg(not(feature = "fips"))]
use std::{mem::MaybeUninit, ptr};
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::{cvt, cvt_p, log_and_map};
use crate::helper::log_and_map;
#[cfg(not(feature = "fips"))]
use crate::helper::{cvt, cvt_p};
use super::DhKeyType;
@ -30,16 +32,11 @@ 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, ErrorStack> {
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, ErrorStack> {
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`
@ -53,12 +50,6 @@ 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, ErrorStack> {
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<Self, ErrorStack> {
@ -76,6 +67,7 @@ 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<Self, ErrorStack> {
let pkey_ctx = unsafe {
EvpPkeyCtx::from_ptr(cvt_p(boring_sys::EVP_PKEY_CTX_new_id(
@ -105,18 +97,24 @@ impl KeyExchange {
/// Decodes a SPKI public key to it's raw public key component
fn raw_public_key(pkey: &PKeyRef<Private>) -> Result<Vec<u8>, ErrorStack> {
let spki = pkey.public_key_to_der()?;
if pkey.id() == Id::EC {
let ec_key = pkey.ec_key()?;
let mut bn_ctx = boring::bn::BigNumContext::new()?;
// parse the key
let pkey = spki::SubjectPublicKeyInfoRef::from_der(spki.as_ref())
.expect("failed parsing spki bytes");
return ec_key.public_key().to_bytes(
ec_key.group(),
boring::ec::PointConversionForm::UNCOMPRESSED,
&mut bn_ctx,
);
}
// return the raw public key as a new vec
Ok(Vec::from(
pkey.subject_public_key
.as_bytes()
.expect("failed getting raw 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)
}
/// Derives a shared secret using the peer's raw public key
@ -129,8 +127,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)?;
@ -147,7 +145,7 @@ impl crypto::ActiveKeyExchange for KeyExchange {
peer_pub_key: &[u8],
) -> Result<crypto::SharedSecret, rustls::Error> {
self.diffie_hellman(peer_pub_key)
.map(|x| crypto::SharedSecret::from(x.as_slice()))
.map(crypto::SharedSecret::from)
.map_err(|e| {
log_and_map(
"ex::KeyExchange::diffie_hellman",
@ -163,12 +161,11 @@ 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,
DhKeyType::EC((_, boring_sys::NID_secp521r1)) => rustls::NamedGroup::secp521r1,
_ => unimplemented!(),
_ => unreachable!("unsupported key type"),
}
}
}
@ -190,6 +187,7 @@ 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();

View file

@ -0,0 +1,79 @@
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<Box<dyn ActiveKeyExchange + 'static>, 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<Box<dyn ActiveKeyExchange + 'static>, 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<Box<dyn ActiveKeyExchange + 'static>, 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")
}
}

View file

@ -0,0 +1,364 @@
//! 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<Box<dyn crypto::ActiveKeyExchange>, 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<CompletedKeyExchange, Error> {
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<u8>,
}
impl crypto::ActiveKeyExchange for ActiveX25519MlKem768 {
/// Client-side: decapsulate ML-KEM + derive X25519.
fn complete(self: Box<Self>, server_share: &[u8]) -> Result<SharedSecret, Error> {
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<Self>,
peer_pub_key: &[u8],
) -> Result<SharedSecret, Error> {
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());
}
}

View file

@ -1,6 +1,8 @@
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,
@ -12,7 +14,7 @@ mod hash;
mod helper;
mod hkdf;
mod hmac;
mod kx;
pub mod kx;
#[cfg(feature = "tls12")]
mod prf;
pub mod sign;
@ -21,60 +23,80 @@ pub mod tls12;
pub mod tls13;
pub mod verify;
/// The boringssl-based Rustls Crypto provider
pub static PROVIDER: &'static dyn CryptoProvider = &Provider;
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<rustls::SupportedCipherSuite>) -> 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::<Vec<_>>();
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,
}
}
#[derive(Debug)]
struct Provider;
impl CryptoProvider for Provider {
fn fill_random(&self, bytes: &mut [u8]) -> Result<(), GetRandomFailed> {
impl rustls::crypto::SecureRandom for Provider {
fn fill(&self, bytes: &mut [u8]) -> Result<(), rustls::crypto::GetRandomFailed> {
boring::rand::rand_bytes(bytes).map_err(|e| log_and_map("rand_bytes", e, GetRandomFailed))
}
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
}
}
fn load_private_key(
&self,
key_der: PrivateKeyDer<'static>,
) -> Result<std::sync::Arc<dyn rustls::sign::SigningKey>, rustls::Error> {
sign::BoringPrivateKey::try_from(key_der).map(|x| Arc::new(x) as _)
}
fn signature_verification_algorithms(&self) -> rustls::WebPkiSupportedAlgorithms {
#[cfg(feature = "fips-only")]
{
verify::ALL_FIPS_ALGORITHMS
}
#[cfg(not(feature = "fips-only"))]
{
verify::ALL_ALGORITHMS
}
fn fips(&self) -> bool {
cfg!(feature = "fips")
}
}
#[allow(unused)]
static ALL_FIPS_CIPHER_SUITES: &[SupportedCipherSuite] = &[
impl rustls::crypto::KeyProvider for Provider {
fn load_private_key(
&self,
key_der: PrivateKeyDer<'static>,
) -> Result<Arc<dyn rustls::sign::SigningKey>, rustls::Error> {
sign::BoringPrivateKey::try_from(key_der).map(|x| Arc::new(x) as _)
}
fn fips(&self) -> bool {
cfg!(feature = "fips")
}
}
#[cfg(feature = "fips")]
pub static ALL_FIPS_CIPHER_SUITES: &[SupportedCipherSuite] = &[
SupportedCipherSuite::Tls13(&tls13::AES_256_GCM_SHA384),
SupportedCipherSuite::Tls13(&tls13::AES_128_GCM_SHA256),
#[cfg(feature = "tls12")]
@ -87,8 +109,8 @@ static ALL_FIPS_CIPHER_SUITES: &[SupportedCipherSuite] = &[
SupportedCipherSuite::Tls12(&tls12::ECDHE_RSA_AES128_GCM_SHA256),
];
#[allow(unused)]
static ALL_CIPHER_SUITES: &[SupportedCipherSuite] = &[
#[cfg(not(feature = "fips"))]
pub 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),
@ -106,26 +128,31 @@ 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
#[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
/// 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
];
#[allow(unused)]
pub const ALL_KX_GROUPS: &[&dyn SupportedKxGroup] = &[
/// 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
&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 _];

View file

@ -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],
output: &mut [u8; 48],
kx: Box<dyn crypto::ActiveKeyExchange>,
peer_pub_key: &[u8],
label: &[u8],
@ -29,6 +29,10 @@ 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(

View file

@ -2,6 +2,7 @@ use std::sync::Arc;
use boring::{
hash::MessageDigest,
nid::Nid,
pkey::{Id, PKeyRef, Private},
rsa::Padding,
sign::{RsaPssSaltlen, Signer},
@ -20,16 +21,28 @@ const ALL_RSA_SCHEMES: &[SignatureScheme] = &[
SignatureScheme::RSA_PKCS1_SHA256,
];
const ALL_EC_SCHEMES: &[SignatureScheme] = &[
SignatureScheme::ECDSA_NISTP256_SHA256,
SignatureScheme::ECDSA_NISTP384_SHA384,
SignatureScheme::ECDSA_NISTP521_SHA512,
];
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];
/// An abstraction over a boringssl private key
/// used for signing
#[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.
#[derive(Debug)]
pub struct BoringPrivateKey(Arc<boring::pkey::PKey<Private>>, rustls::SignatureAlgorithm);
pub struct BoringPrivateKey(Arc<boring::pkey::PKey<Private>>, KeyKind);
impl TryFrom<PrivateKeyDer<'static>> for BoringPrivateKey {
type Error = rustls::Error;
@ -37,25 +50,83 @@ impl TryFrom<PrivateKeyDer<'static>> for BoringPrivateKey {
fn try_from(value: PrivateKeyDer<'static>) -> Result<Self, Self::Error> {
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, ()))
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()),
)
})
}
PrivateKeyDer::Pkcs1(der) => {
boring::pkey::PKey::private_key_from_der(der.secret_pkcs1_der())
.map_err(|e| log_and_map("private_key_from_der", e, ()))
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()),
)
})
}
_ => Err(()),
}
.map_err(|_| rustls::Error::General("failed loading private key".into()))?;
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(),
));
}
}?;
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,
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,
_ => return Err(rustls::Error::General("unsupported key format".into())),
};
Ok(Self(Arc::new(pkey), sig))
#[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))
}
}
@ -63,75 +134,105 @@ fn rsa_signer_from_params(
key: &PKeyRef<Private>,
digest: MessageDigest,
padding: Padding,
) -> Signer {
let mut signer = Signer::new(digest, key).expect("failed getting signer");
signer
.set_rsa_padding(padding)
.expect("failed setting padding");
) -> Result<Signer<'_>, 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()),
)
})?;
if padding == Padding::PKCS1_PSS {
signer
.set_rsa_pss_saltlen(RsaPssSaltlen::DIGEST_LENGTH)
.expect("failed setting rsa_pss salt lengths");
signer
.set_rsa_mgf1_md(digest)
.expect("failed setting mgf1 digest");
.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()),
)
})?;
}
signer
Ok(signer)
}
fn ec_signer_from_params(key: &PKeyRef<Private>, digest: MessageDigest) -> Signer {
let signer = Signer::new(digest, key).expect("failed getting signer");
signer
fn ec_signer_from_params(
key: &PKeyRef<Private>,
digest: MessageDigest,
) -> Result<Signer<'_>, rustls::Error> {
Signer::new(digest, key).map_err(|e| {
log_and_map(
"Signer::new",
e,
rustls::Error::General("failed preparing signer".into()),
)
})
}
impl BoringPrivateKey {}
impl SigningKey for BoringPrivateKey {
fn choose_scheme(
&self,
offered: &[rustls::SignatureScheme],
) -> Option<Box<dyn rustls::sign::Signer>> {
match self.1 {
rustls::SignatureAlgorithm::RSA => ALL_RSA_SCHEMES
let scheme = match self.1 {
KeyKind::Rsa => ALL_RSA_SCHEMES
.iter()
.find(|scheme| offered.contains(scheme))
.map(|&scheme| Box::new(BoringSigner(self.0.clone(), scheme)) as _),
rustls::SignatureAlgorithm::ECDSA => ALL_EC_SCHEMES
.find(|scheme| offered.contains(scheme)),
KeyKind::Ec(EcCurve::P256) => EC_P256_SCHEMES
.iter()
.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,
)))
.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)
}
rustls::SignatureAlgorithm::ED448
if offered.contains(&rustls::SignatureScheme::ED448) =>
{
Some(Box::new(BoringSigner(
self.0.clone(),
rustls::SignatureScheme::ED448,
)))
KeyKind::Ed448 if offered.contains(&rustls::SignatureScheme::ED448) => {
Some(&rustls::SignatureScheme::ED448)
}
_ => None,
}
}?;
Some(Box::new(BoringSigner(self.0.clone(), *scheme)))
}
fn algorithm(&self) -> rustls::SignatureAlgorithm {
self.1
match self.1 {
KeyKind::Rsa => rustls::SignatureAlgorithm::RSA,
KeyKind::Ec(_) => rustls::SignatureAlgorithm::ECDSA,
KeyKind::Ed25519 => rustls::SignatureAlgorithm::ED25519,
KeyKind::Ed448 => rustls::SignatureAlgorithm::ED448,
}
}
}
/// A boringssl-based Signer
/// A boringssl-based Signer.
#[derive(Debug)]
pub struct BoringSigner(Arc<boring::pkey::PKey<Private>>, rustls::SignatureScheme);
impl BoringSigner {
fn get_signer(&self) -> Signer {
fn get_signer(&self) -> Result<Signer<'_>, rustls::Error> {
match self.1 {
SignatureScheme::RSA_PKCS1_SHA256 => {
rsa_signer_from_params(self.0.as_ref(), MessageDigest::sha256(), Padding::PKCS1)
@ -142,7 +243,6 @@ 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)
}
@ -152,7 +252,6 @@ 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())
}
@ -162,19 +261,25 @@ 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()).expect("failed getting signer")
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()),
)
})
}
_ => unimplemented!(),
_ => Err(rustls::Error::General(
"unsupported signature scheme for private key".into(),
)),
}
}
}
impl rustls::sign::Signer for BoringSigner {
fn sign(&self, message: &[u8]) -> Result<Vec<u8>, 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())))?;
@ -195,3 +300,75 @@ 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<Private> {
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);
}
}

View file

@ -23,11 +23,10 @@ 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::CipherSuiteCommon {
common: rustls::crypto::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::<aead::aes::Aes128>::DEFAULT,
prf_provider: &PRF_SHA256,
@ -36,11 +35,10 @@ pub static ECDHE_ECDSA_AES128_GCM_SHA256: Tls12CipherSuite = Tls12CipherSuite {
};
pub static ECDHE_RSA_AES128_GCM_SHA256: Tls12CipherSuite = Tls12CipherSuite {
common: rustls::CipherSuiteCommon {
common: rustls::crypto::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::<aead::aes::Aes128>::DEFAULT,
prf_provider: &PRF_SHA256,
@ -49,11 +47,10 @@ pub static ECDHE_RSA_AES128_GCM_SHA256: Tls12CipherSuite = Tls12CipherSuite {
};
pub static ECDHE_ECDSA_AES256_GCM_SHA384: Tls12CipherSuite = Tls12CipherSuite {
common: rustls::CipherSuiteCommon {
common: rustls::crypto::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::<aead::aes::Aes256>::DEFAULT,
prf_provider: &PRF_SHA384,
@ -62,11 +59,10 @@ pub static ECDHE_ECDSA_AES256_GCM_SHA384: Tls12CipherSuite = Tls12CipherSuite {
};
pub static ECDHE_RSA_AES256_GCM_SHA384: Tls12CipherSuite = Tls12CipherSuite {
common: rustls::CipherSuiteCommon {
common: rustls::crypto::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::<aead::aes::Aes256>::DEFAULT,
prf_provider: &PRF_SHA384,
@ -75,11 +71,10 @@ pub static ECDHE_RSA_AES256_GCM_SHA384: Tls12CipherSuite = Tls12CipherSuite {
};
pub static ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256: Tls12CipherSuite = Tls12CipherSuite {
common: rustls::CipherSuiteCommon {
common: rustls::crypto::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::<aead::chacha20::ChaCha20Poly1305>::DEFAULT,
prf_provider: &PRF_SHA256,
@ -88,11 +83,10 @@ pub static ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256: Tls12CipherSuite = Tls12Ci
};
pub static ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256: Tls12CipherSuite = Tls12CipherSuite {
common: rustls::CipherSuiteCommon {
common: rustls::crypto::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::<aead::chacha20::ChaCha20Poly1305>::DEFAULT,
prf_provider: &PRF_SHA256,

View file

@ -3,11 +3,10 @@ use rustls::Tls13CipherSuite;
use crate::{aead, hash, hkdf};
pub static AES_128_GCM_SHA256: Tls13CipherSuite = Tls13CipherSuite {
common: rustls::CipherSuiteCommon {
common: rustls::crypto::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::<hkdf::Sha256>::DEFAULT,
aead_alg: &aead::Aead::<aead::aes::Aes128>::DEFAULT,
@ -15,11 +14,10 @@ pub static AES_128_GCM_SHA256: Tls13CipherSuite = Tls13CipherSuite {
};
pub static AES_256_GCM_SHA384: Tls13CipherSuite = Tls13CipherSuite {
common: rustls::CipherSuiteCommon {
common: rustls::crypto::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::<hkdf::Sha384>::DEFAULT,
aead_alg: &aead::Aead::<aead::aes::Aes256>::DEFAULT,
@ -27,11 +25,10 @@ pub static AES_256_GCM_SHA384: Tls13CipherSuite = Tls13CipherSuite {
};
pub static CHACHA20_POLY1305_SHA256: Tls13CipherSuite = Tls13CipherSuite {
common: rustls::CipherSuiteCommon {
common: rustls::crypto::CipherSuiteCommon {
suite: rustls::CipherSuite::TLS13_CHACHA20_POLY1305_SHA256,
hash_provider: hash::SHA256,
confidentiality_limit: u64::MAX,
integrity_limit: 1 << 36,
},
hkdf_provider: &hkdf::Hkdf::<hkdf::Sha256>::DEFAULT,

View file

@ -1,4 +1,4 @@
use rustls::{SignatureScheme, WebPkiSupportedAlgorithms};
use rustls::{crypto::WebPkiSupportedAlgorithms, SignatureScheme};
pub(crate) mod ec;
pub(crate) mod ed;
@ -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]),
],
};

View file

@ -1,5 +1,5 @@
use boring::{error::ErrorStack, hash::MessageDigest};
use rustls::SignatureScheme;
use rustls::{pki_types::alg_id, 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())
}
_ => unimplemented!(),
_ => return Err(InvalidSignature),
}
.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 => webpki::alg_id::ECDSA_P256,
SignatureScheme::ECDSA_NISTP384_SHA384 => webpki::alg_id::ECDSA_P384,
SignatureScheme::ECDSA_NISTP256_SHA256 => alg_id::ECDSA_P256,
SignatureScheme::ECDSA_NISTP384_SHA384 => 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,29 +61,33 @@ impl SignatureVerificationAlgorithm for BoringEcVerifier {
0x04, 0x00, 0x23,
])
}
_ => unimplemented!(),
_ => unreachable!("BoringEcVerifier only supports configured ECDSA schemes"),
}
}
fn signature_alg_id(&self) -> rustls_pki_types::AlgorithmIdentifier {
match self.0 {
SignatureScheme::ECDSA_NISTP256_SHA256 => webpki::alg_id::ECDSA_SHA256,
SignatureScheme::ECDSA_NISTP384_SHA384 => webpki::alg_id::ECDSA_SHA384,
SignatureScheme::ECDSA_NISTP256_SHA256 => alg_id::ECDSA_SHA256,
SignatureScheme::ECDSA_NISTP384_SHA384 => 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,
])
}
_ => unimplemented!(),
_ => unreachable!("BoringEcVerifier only supports configured ECDSA schemes"),
}
}
fn fips(&self) -> bool {
cfg!(feature = "fips") && !matches!(self.0, SignatureScheme::ECDSA_NISTP521_SHA512)
}
}
fn ec_verifier_from_params(
key: &boring::pkey::PKeyRef<boring::pkey::Public>,
digest: MessageDigest,
) -> Result<boring::sign::Verifier, ErrorStack> {
) -> Result<boring::sign::Verifier<'_>, ErrorStack> {
boring::sign::Verifier::new(digest, key)
}
@ -92,7 +96,7 @@ fn group_for_scheme(scheme: SignatureScheme) -> Result<boring::ec::EcGroup, Erro
SignatureScheme::ECDSA_NISTP256_SHA256 => boring::nid::Nid::X9_62_PRIME256V1,
SignatureScheme::ECDSA_NISTP384_SHA384 => boring::nid::Nid::SECP384R1,
SignatureScheme::ECDSA_NISTP521_SHA512 => boring::nid::Nid::SECP521R1,
_ => unimplemented!(),
_ => unreachable!("BoringEcVerifier only supports configured ECDSA schemes"),
};
boring::ec::EcGroup::from_curve_name(nid)
}

View file

@ -2,7 +2,7 @@ use std::ptr;
use boring::error::ErrorStack;
use foreign_types::ForeignType;
use rustls::SignatureScheme;
use rustls::{pki_types::alg_id, SignatureScheme};
use rustls_pki_types::{InvalidSignature, SignatureVerificationAlgorithm};
use crate::helper::{cvt_p, log_and_map};
@ -41,19 +41,23 @@ impl SignatureVerificationAlgorithm for BoringEdVerifier {
fn signature_alg_id(&self) -> rustls_pki_types::AlgorithmIdentifier {
match self.0 {
SignatureScheme::ED25519 => webpki::alg_id::ED25519,
SignatureScheme::ED25519 => 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])
}
_ => unimplemented!(),
_ => unreachable!("BoringEdVerifier only supports configured EdDSA schemes"),
}
}
fn fips(&self) -> bool {
false
}
}
fn ed_verifier_from_params(
key: &boring::pkey::PKeyRef<boring::pkey::Public>,
) -> Result<boring::sign::Verifier, ErrorStack> {
) -> Result<boring::sign::Verifier<'_>, ErrorStack> {
boring::sign::Verifier::new_without_digest(key)
}
@ -64,7 +68,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,
_ => unimplemented!(),
_ => return Err(ErrorStack::get()),
});
public_key(spki_spk, nid)
}

View file

@ -5,7 +5,7 @@ use boring::{
rsa::{Padding, Rsa},
sign::RsaPssSaltlen,
};
use rustls::SignatureScheme;
use rustls::{pki_types::alg_id, SignatureScheme};
use rustls_pki_types::{InvalidSignature, SignatureVerificationAlgorithm};
use spki::der::Reader;
@ -64,8 +64,8 @@ impl SignatureVerificationAlgorithm for BoringRsaVerifier {
Padding::PKCS1_PSS,
),
_ => unimplemented!(),
};
_ => return Err(InvalidSignature),
}?;
verifier.verify_oneshot(signature, message).map_or_else(
|_| Err(InvalidSignature),
|res| if res { Ok(()) } else { Err(InvalidSignature) },
@ -73,43 +73,48 @@ impl SignatureVerificationAlgorithm for BoringRsaVerifier {
}
fn public_key_alg_id(&self) -> rustls_pki_types::AlgorithmIdentifier {
webpki::alg_id::RSA_ENCRYPTION
alg_id::RSA_ENCRYPTION
}
fn signature_alg_id(&self) -> rustls_pki_types::AlgorithmIdentifier {
match self.0 {
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_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_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,
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,
_ => unimplemented!(),
_ => unreachable!("BoringRsaVerifier only supports configured RSA schemes"),
}
}
fn fips(&self) -> bool {
cfg!(feature = "fips")
}
}
fn rsa_verifier_from_params(
key: &boring::pkey::PKeyRef<boring::pkey::Public>,
digest: MessageDigest,
padding: Padding,
) -> boring::sign::Verifier {
let mut verifier = boring::sign::Verifier::new(digest, key).expect("failed getting verifier");
) -> Result<boring::sign::Verifier<'_>, InvalidSignature> {
let mut verifier = boring::sign::Verifier::new(digest, key)
.map_err(|e| log_and_map("Verifier::new", e, InvalidSignature))?;
verifier
.set_rsa_padding(padding)
.expect("failed setting padding");
.map_err(|e| log_and_map("set_rsa_padding", e, InvalidSignature))?;
if padding == Padding::PKCS1_PSS {
verifier
.set_rsa_pss_saltlen(RsaPssSaltlen::DIGEST_LENGTH)
.expect("failed setting rsa_pss salt lengths");
.map_err(|e| log_and_map("set_rsa_pss_saltlen", e, InvalidSignature))?;
verifier
.set_rsa_mgf1_md(digest)
.expect("failed setting mgf1 digest");
.map_err(|e| log_and_map("set_rsa_mgf1_md", e, InvalidSignature))?;
}
verifier
Ok(verifier)
}
pub(crate) fn decode_spki_spk(

View file

@ -5,15 +5,71 @@ use tokio::{
net::TcpStream,
};
use boring_rustls_provider::{tls12, tls13, PROVIDER};
use rustls::{
version::{TLS12, TLS13},
ClientConfig, ServerConfig, SupportedCipherSuite,
};
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};
fn tls13_provider_suites() -> Vec<SupportedCipherSuite> {
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<SupportedCipherSuite> {
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<SupportedCipherSuite> {
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);
@ -21,25 +77,404 @@ async fn test_tls13_crypto() {
let root_store = pki.client_root_store();
let server_config = pki.server_config();
let ciphers = [
SupportedCipherSuite::Tls13(&tls13::AES_128_GCM_SHA256),
SupportedCipherSuite::Tls13(&tls13::AES_256_GCM_SHA384),
SupportedCipherSuite::Tls13(&tls13::CHACHA20_POLY1305_SHA256),
];
let ciphers = tls13_provider_suites();
for cipher in ciphers {
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();
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();
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::<Vec<_>>();
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::<Vec<_>>();
// 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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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);
@ -47,25 +482,22 @@ async fn test_tls12_ec_crypto() {
let root_store = pki.client_root_store();
let server_config = pki.server_config();
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),
];
let ciphers = tls12_provider_suites_for_ecdsa();
for cipher in ciphers {
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();
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();
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);
@ -73,20 +505,16 @@ async fn test_tls12_rsa_crypto() {
let root_store = pki.client_root_store();
let server_config = pki.server_config();
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),
];
let ciphers = tls12_provider_suites_for_rsa();
for cipher in ciphers {
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();
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();
do_exchange(config, server_config.clone()).await;
}
@ -96,7 +524,10 @@ async fn new_listener() -> TcpListener {
TcpListener::bind("localhost:0").await.unwrap()
}
async fn do_exchange(config: ClientConfig, server_config: Arc<ServerConfig>) {
async fn do_exchange(
config: ClientConfig,
server_config: Arc<ServerConfig>,
) -> Option<rustls::NamedGroup> {
let listener = new_listener().await;
let addr = listener.local_addr().unwrap();
tokio::spawn(spawn_echo_server(listener, server_config.clone()));
@ -112,10 +543,18 @@ async fn do_exchange(config: ClientConfig, server_config: Arc<ServerConfig>) {
.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<ServerConfig>) {
@ -176,11 +615,18 @@ impl TestPki {
}
fn server_config(self) -> Arc<ServerConfig> {
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();
#[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();
server_config.key_log = Arc::new(rustls::KeyLogFile::new());

View file

@ -0,0 +1,244 @@
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<Vec<PathBuf>, 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<PathBuf>) -> 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(())
}

View file

@ -1,7 +1,7 @@
use std::ffi;
extern "C" {
/// Calculates `out_len` bytes of the TLS PDF, using `digest`, and
/// Calculates `out_len` bytes of the TLS PRF, 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

View file

@ -7,15 +7,7 @@ description = "Boring Rustls provider example code and tests."
publish = false
[dependencies]
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" ]}
env_logger = "0.11"
rustls = { workspace = true, features = [ "logging", "std" ]}
boring-rustls-provider = { path = "../boring-rustls-provider", features = ["logging"] }
rustls-pemfile = { workspace = true }
serde = "1.0"
serde_derive = "1.0"
webpki-roots = { workspace = true }

View file

@ -2,22 +2,24 @@ use std::io::{stdout, Read, Write};
use std::net::TcpStream;
use std::sync::Arc;
use boring_rustls_provider::PROVIDER;
fn main() {
fn main() -> Result<(), Box<dyn std::error::Error>> {
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(PROVIDER)
.with_safe_defaults()
.with_root_certificates(root_store)
.with_no_client_auth();
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 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 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 mut tls = rustls::Stream::new(&mut conn, &mut sock);
tls.write_all(
concat!(
@ -28,16 +30,19 @@ fn main() {
"\r\n"
)
.as_bytes(),
)
.unwrap();
let ciphersuite = tls.conn.negotiated_cipher_suite().unwrap();
)?;
let ciphersuite = tls
.conn
.negotiated_cipher_suite()
.ok_or_else(|| std::io::Error::other("no negotiated ciphersuite"))?;
writeln!(
&mut std::io::stderr(),
"Current ciphersuite: {:?}",
ciphersuite.suite()
)
.unwrap();
)?;
let mut plaintext = Vec::new();
tls.read_to_end(&mut plaintext).unwrap();
stdout().write_all(&plaintext).unwrap();
tls.read_to_end(&mut plaintext)?;
stdout().write_all(&plaintext)?;
Ok(())
}