Compare 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
`ECDHE` with curves:
### Key Exchange Groups
Matches boring's default supported group list:
```
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());
}
}

Some files were not shown because too many files have changed in this diff Show more