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: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
RUSTFLAGS: -Dwarnings RUSTFLAGS: -Dwarnings
FEATURES: "logging,tls12"
jobs: jobs:
build: fmt:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
@ -21,9 +20,95 @@ jobs:
run: sudo apt-get install -y cmake clang run: sudo apt-get install -y cmake clang
- name: Check fmt - name: Check fmt
run: make fmt run: make fmt
- name: Lint
run: make lint test-default:
- name: Tests usual runs-on: ubuntu-latest
run: make test
- name: Build usual steps:
run: make build - 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 = [ members = [
# things that should probably be in boring crate # things that should probably be in boring crate
"boring-additions", "boring-additions",
# things that should probably be in boring-sys crate
"boring-sys-additions",
# the main library and tests # the main library and tests
"boring-rustls-provider", "boring-rustls-provider",
# things that should probably be in boring-sys crate
"boring-sys-additions",
# tests and example code # tests and example code
"examples", "examples",
] ]
@ -17,11 +17,11 @@ default-members = [
resolver = "2" resolver = "2"
[workspace.dependencies] [workspace.dependencies]
boring = { version = "4", default-features = false } boring = { version = "5", default-features = false }
boring-sys = { version = "4", default-features = false } boring-sys = { version = "5", default-features = false }
rustls = { version = "=0.22.0-alpha.6", default-features = false } rustls = { version = "0.23", default-features = false }
rustls-pemfile = { version = "=2.0.0-alpha.2" } rustls-pemfile = { version = "2" }
rustls-pki-types = { version = "0.2.3" } rustls-pki-types = { version = "1" }
tokio-rustls = { version = "0.25.0-alpha.4" } tokio-rustls = { version = "0.26", default-features = false }
webpki = { package = "rustls-webpki", version = "0.102.0-alpha.7", default-features = false } webpki = { package = "rustls-webpki", version = "0.103", default-features = false }
webpki-roots = { version = "=0.26.0-alpha.2" } webpki-roots = { version = "1.0" }

View file

@ -1,4 +1,5 @@
FEATURES ?= logging,tls12 FEATURES ?= logging,tls12
CARGO_FEATURES := $(if $(strip $(FEATURES)),-F "$(FEATURES)",)
.PHONY: fmt .PHONY: fmt
@ -7,12 +8,16 @@ fmt:
.PHONY: lint .PHONY: lint
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 .PHONY: test
test: test:
cargo test --all-targets -F "$(FEATURES)" cargo test --all-targets $(CARGO_FEATURES)
.PHONY: build .PHONY: build
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) [![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 Built on `boring` v5 and `rustls` 0.23.
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.
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 No features are enabled by default. The provider ships with TLS 1.3 support
Currently, supports only TLS 1.3: 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_128_GCM_SHA256
AES_256_GCM_SHA384 AES_256_GCM_SHA384
CHACHA20_POLY1305_SHA256 CHACHA20_POLY1305_SHA256
``` ```
QUIC: not yet supported TLS 1.2 (requires `tls12` feature):
TLS 1.2:
``` ```
ECDHE_ECDSA_AES128_GCM_SHA256 ECDHE_ECDSA_AES128_GCM_SHA256
ECDHE_RSA_AES128_GCM_SHA256 ECDHE_RSA_AES128_GCM_SHA256
ECDHE_ECDSA_AES256_GCM_SHA384 ECDHE_ECDSA_AES256_GCM_SHA384
ECDHE_RSA_AES256_GCM_SHA384 ECDHE_RSA_AES256_GCM_SHA384
ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
``` ```
### Key Exchange Algorithms ### Key Exchange Groups
`ECDHE` with curves: Matches boring's default supported group list:
``` ```
X25519MLKEM768 (0x11ec, requires mlkem feature, TLS 1.3 only)
X25519 X25519
X448 secp256r1 (P-256)
secp256r1 secp384r1 (P-384)
secp384r1
secp521r1
``` ```
When `mlkem` is enabled, X25519MLKEM768 is the preferred (first) group in both
FIPS and non-FIPS configurations.
`FFDHE` with: ### Signature Algorithms
```
ffdhe2048
```
### Signature Generation / Verification
``` ```
RSA_PKCS1_SHA256 RSA_PKCS1_SHA256
@ -66,6 +70,47 @@ ED25519
ED448 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 ## License
MIT MIT

View file

@ -8,7 +8,6 @@ description = "Boring additions"
publish = false publish = false
[dependencies] [dependencies]
aead = { version = "0.5", default_features = false, features = ["alloc"] }
boring = { workspace = true } boring = { workspace = true }
boring-sys = { workspace = true } boring-sys = { workspace = true }
foreign-types = "0.5" foreign-types = "0.5"

View file

@ -78,11 +78,7 @@ impl Crypter {
/// ///
/// # Errors /// # Errors
/// Returns the `BoringSSL` error in case of an internal error /// 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> { pub fn new(aead_alg: &Algorithm, key: &[u8]) -> Result<Self, ErrorStack> {
assert_eq!(aead_alg.key_length(), key.len());
boring_sys::init(); boring_sys::init();
let this = unsafe { let this = unsafe {
@ -116,10 +112,6 @@ impl Crypter {
/// ///
/// # Errors /// # Errors
/// In case of an error, returns the `BoringSSL` error /// 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( pub fn seal_in_place(
&self, &self,
nonce: &[u8], nonce: &[u8],
@ -127,8 +119,9 @@ impl Crypter {
buffer: &mut [u8], buffer: &mut [u8],
tag: &mut [u8], tag: &mut [u8],
) -> Result<usize, ErrorStack> { ) -> Result<usize, ErrorStack> {
assert!(tag.len() >= self.max_overhead); if tag.len() < self.max_overhead || nonce.len() != self.nonce_len {
assert_eq!(nonce.len(), self.nonce_len); return Err(ErrorStack::get());
}
let mut tag_len = tag.len(); let mut tag_len = tag.len();
unsafe { unsafe {
@ -156,9 +149,6 @@ impl Crypter {
/// ///
/// # Errors /// # Errors
/// In case of an error, returns the `BoringSSL` error /// In case of an error, returns the `BoringSSL` error
///
/// # Panics
/// * if the nonce has the wrong lenght
pub fn open_in_place( pub fn open_in_place(
&self, &self,
nonce: &[u8], nonce: &[u8],
@ -166,7 +156,9 @@ impl Crypter {
buffer: &mut [u8], buffer: &mut [u8],
tag: &[u8], tag: &[u8],
) -> Result<(), ErrorStack> { ) -> Result<(), ErrorStack> {
assert_eq!(nonce.len(), self.nonce_len); if nonce.len() != self.nonce_len {
return Err(ErrorStack::get());
}
unsafe { unsafe {
cvt(boring_sys::EVP_AEAD_CTX_open_gather( cvt(boring_sys::EVP_AEAD_CTX_open_gather(
@ -188,11 +180,11 @@ impl Crypter {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::Crypter; use super::{Algorithm, Crypter};
#[test] #[test]
fn in_out() { 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 nonce = [0u8; 12];
let associated_data = b"this is authenticated"; let associated_data = b"this is authenticated";
let mut buffer = Vec::with_capacity(26); let mut buffer = Vec::with_capacity(26);
@ -207,4 +199,35 @@ mod tests {
assert_eq!(b"ABCDE", buffer.as_slice()); 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 foreign_types::{ForeignType, ForeignTypeRef, Opaque};
use crate::helper::{cvt, cvt_p};
pub struct HmacCtxRef(Opaque); pub struct HmacCtxRef(Opaque);
unsafe impl ForeignTypeRef for HmacCtxRef { 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 { impl Drop for HmacCtx {
fn drop(&mut self) { fn drop(&mut self) {
unsafe { unsafe {

View file

@ -8,31 +8,52 @@ description = "Boringssl rustls provider"
publish = false publish = false
[features] [features]
default = ["tls12"] default = []
# Use a FIPS-validated version of boringssl.
fips = ["boring/fips", "boring-sys/fips"] # Build against a FIPS-validated version of BoringSSL and restrict the
logging = ["log"] # provider to FIPS-approved algorithms only. This affects:
fips-only = [] # - 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"] tls12 = ["rustls/tls12"]
# Enable debug logging of BoringSSL errors and provider internals via
# the `log` crate. Useful for diagnosing handshake failures.
logging = ["log"]
[dependencies] [dependencies]
aead = {version = "0.5", default_features = false, features = ["alloc"] } aead = {version = "0.5", default-features = false, features = ["alloc"] }
boring = { workspace = true } boring = { workspace = true }
boring-additions = { path = "../boring-additions" } boring-additions = { path = "../boring-additions" }
boring-sys = { workspace = true } boring-sys = { workspace = true }
boring-sys-additions = { path = "../boring-sys-additions" } boring-sys-additions = { path = "../boring-sys-additions" }
foreign-types = "0.5" foreign-types = "0.5"
lazy_static = "1.4"
log = { version = "0.4.4", optional = true } log = { version = "0.4.4", optional = true }
once_cell = "1"
rustls = { workspace = true } rustls = { workspace = true }
rustls-pki-types = { workspace = true } rustls-pki-types = { workspace = true }
spki = "0.7" spki = "0.7"
webpki = { workspace = true, features = ["alloc", "std"] } zeroize = "1"
[dev-dependencies] [dev-dependencies]
hex-literal = "0.4" hex-literal = "1"
rcgen = "0.11.3" rcgen = "0.12"
tokio = { version = "1.34", features = ["macros", "rt", "net", "io-util", "io-std"] } tokio = { version = "1.34", features = ["macros", "rt", "net", "io-util", "io-std"] }
tokio-rustls = { workspace = true } tokio-rustls = { workspace = true }
webpki-roots = { workspace = true }

View file

@ -1,9 +1,11 @@
use std::marker::PhantomData; use std::marker::PhantomData;
use aead::{AeadCore, AeadInPlace, Nonce, Tag}; use aead::{AeadCore, AeadInPlace, Buffer, Nonce, Tag};
use boring::error::ErrorStack; use boring::error::ErrorStack;
use boring_additions::aead::Algorithm; 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 rustls::{ConnectionTrafficSecrets, ContentType, ProtocolVersion};
use crate::helper::log_and_map; 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). /// 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 /// Together with [`BoringCipher::explicit_nonce_len`] it determines the total
/// lengths of the used nonce. /// lengths of the used nonce.
/// Used only by TLS 1.2 code paths.
#[cfg(feature = "tls12")]
const FIXED_IV_LEN: usize; const FIXED_IV_LEN: usize;
/// The key size in bytes /// The key size in bytes
const KEY_SIZE: usize; const KEY_SIZE: usize;
@ -25,6 +29,15 @@ pub(crate) trait BoringCipher {
/// The length of the authentication tag /// The length of the authentication tag
const TAG_LEN: usize; 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 /// Constructs a new instance of this cipher as an AEAD algorithm
fn new_cipher() -> Algorithm; fn new_cipher() -> Algorithm;
@ -32,14 +45,14 @@ pub(crate) trait BoringCipher {
fn extract_keys(key: cipher::AeadKey, iv: cipher::Iv) -> ConnectionTrafficSecrets; 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 /// The key size in bytes
const KEY_SIZE: usize; const KEY_SIZE: usize;
/// the expected length of a sample /// the expected length of a sample
const SAMPLE_LEN: usize; 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 {} pub(crate) trait BoringAead: BoringCipher + AeadCore + Send + Sync {}
@ -64,19 +77,21 @@ impl<T: BoringAead> AeadCore for BoringAeadCrypter<T> {
impl<T: BoringAead> BoringAeadCrypter<T> { impl<T: BoringAead> BoringAeadCrypter<T> {
/// Creates a new aead crypter /// Creates a new aead crypter
pub fn new(iv: Iv, key: &[u8], tls_version: ProtocolVersion) -> Result<Self, ErrorStack> { 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")] #[cfg(feature = "tls12")]
ProtocolVersion::TLSv1_2 => true, ProtocolVersion::TLSv1_2 => true,
ProtocolVersion::TLSv1_3 => true, ProtocolVersion::TLSv1_3 => true,
_ => false, _ => false,
}); };
if !tls_version_supported {
return Err(ErrorStack::get());
}
let cipher = <T as BoringCipher>::new_cipher(); let cipher = <T as BoringCipher>::new_cipher();
assert_eq!( if cipher.nonce_len() != rustls::crypto::cipher::Nonce::new(&iv, 0).0.len() {
cipher.nonce_len(), return Err(ErrorStack::get());
rustls::crypto::cipher::Nonce::new(&iv, 0).0.len() }
);
let crypter = BoringAeadCrypter { let crypter = BoringAeadCrypter {
crypter: boring_additions::aead::Crypter::new(&cipher, key)?, crypter: boring_additions::aead::Crypter::new(&cipher, key)?,
@ -123,11 +138,10 @@ where
{ {
fn encrypt( fn encrypt(
&mut self, &mut self,
msg: cipher::BorrowedPlainMessage, msg: cipher::OutboundPlainMessage,
seq: u64, seq: u64,
) -> Result<cipher::OpaqueMessage, rustls::Error> { ) -> Result<cipher::OutboundOpaqueMessage, rustls::Error> {
let nonce = cipher::Nonce::new(&self.iv, seq); let nonce = cipher::Nonce::new(&self.iv, seq);
match self.tls_version { match self.tls_version {
#[cfg(feature = "tls12")] #[cfg(feature = "tls12")]
ProtocolVersion::TLSv1_2 => { ProtocolVersion::TLSv1_2 => {
@ -136,50 +150,57 @@ where
let total_len = self.encrypted_payload_len(msg.payload.len()); 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(&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()]); 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 (payload, tag) = payload.split_at_mut(msg.payload.len());
let aad = cipher::make_tls12_aad(seq, msg.typ, msg.version, msg.payload.len()); let aad = cipher::make_tls12_aad(seq, msg.typ, msg.version, msg.payload.len());
self.crypter self.crypter
.seal_in_place(&nonce.0, &aad, payload, tag) .seal_in_place(&nonce.0, &aad, payload, tag)
.map_err(|_| rustls::Error::EncryptError) .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 => { ProtocolVersion::TLSv1_3 => {
let total_len = self.encrypted_payload_len(msg.payload.len()); let total_len = self.encrypted_payload_len(msg.payload.len());
let mut payload = Vec::with_capacity(total_len); let mut payload = PrefixedPayload::with_capacity(total_len);
payload.extend_from_slice(msg.payload); payload.extend_from_chunks(&msg.payload);
payload.push(msg.typ.get_u8()); payload.extend_from_slice(&msg.typ.to_array());
let aad = cipher::make_tls13_aad(total_len); let aad = cipher::make_tls13_aad(total_len);
self.encrypt_in_place(Nonce::<T>::from_slice(&nonce.0), &aad, &mut payload) self.encrypt_in_place(
.map_err(|_| rustls::Error::EncryptError) Nonce::<T>::from_slice(&nonce.0),
.map(|_| { &aad,
cipher::OpaqueMessage::new( &mut EncryptBufferAdapter(&mut payload),
ContentType::ApplicationData, )
ProtocolVersion::TLSv1_2, .map_err(|_| rustls::Error::EncryptError)
payload, .map(|_| {
) cipher::OutboundOpaqueMessage::new(
}) ContentType::ApplicationData,
ProtocolVersion::TLSv1_2,
payload,
)
})
} }
_ => unimplemented!(), _ => Err(rustls::Error::EncryptError),
} }
} }
fn encrypted_payload_len(&self, payload_len: usize) -> usize { fn encrypted_payload_len(&self, payload_len: usize) -> usize {
match self.tls_version { let per_record_overhead = match self.tls_version {
ProtocolVersion::TLSv1_2 => { ProtocolVersion::TLSv1_2 => self
payload_len + self.crypter.max_overhead() + <T as BoringCipher>::EXPLICIT_NONCE_LEN .crypter
} .max_overhead()
ProtocolVersion::TLSv1_3 => payload_len + 1 + self.crypter.max_overhead(), .saturating_add(<T as BoringCipher>::EXPLICIT_NONCE_LEN),
_ => unimplemented!(), 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 where
T: BoringAead, T: BoringAead,
{ {
fn decrypt( fn decrypt<'a>(
&mut self, &mut self,
mut m: cipher::OpaqueMessage, mut m: cipher::InboundOpaqueMessage<'a>,
seq: u64, seq: u64,
) -> Result<cipher::PlainMessage, rustls::Error> { ) -> Result<cipher::InboundPlainMessage<'a>, rustls::Error> {
match self.tls_version { match self.tls_version {
#[cfg(feature = "tls12")] #[cfg(feature = "tls12")]
ProtocolVersion::TLSv1_2 => { ProtocolVersion::TLSv1_2 => {
let explicit_nonce_len = <T as BoringCipher>::EXPLICIT_NONCE_LEN; 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] // payload is: [nonce] | [ciphertext] | [auth tag]
let actual_payload_length = let actual_payload_length = m.payload.len() - tag_len - explicit_nonce_len;
m.payload().len() - self.crypter.max_overhead() - explicit_nonce_len;
let aad = make_tls12_aad(seq, m.typ, m.version, actual_payload_length); 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 // get the nonce
let (explicit_nonce, payload) = payload.split_at_mut(explicit_nonce_len); let (explicit_nonce, payload) = payload.split_at_mut(explicit_nonce_len);
@ -211,7 +239,9 @@ where
let nonce = { let nonce = {
let fixed_iv_len = <T as BoringCipher>::FIXED_IV_LEN; 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 // grab the IV by constructing a nonce, this is just an xor
let iv = cipher::Nonce::new(&self.iv, seq).0; let iv = cipher::Nonce::new(&self.iv, seq).0;
@ -222,30 +252,33 @@ where
}; };
// split off the authentication tag // split off the authentication tag
let (payload, tag) = let (payload, tag) = payload.split_at_mut(payload.len() - tag_len);
payload.split_at_mut(payload.len() - self.crypter.max_overhead());
self.crypter self.crypter
.open_in_place(&nonce, &aad, payload, tag) .open_in_place(&nonce, &aad, payload, tag)
.map_err(|e| log_and_map("open_in_place", e, rustls::Error::DecryptError)) .map_err(|e| log_and_map("open_in_place", e, rustls::Error::DecryptError))
.map(|_| { .map(|_| {
// rotate the nonce to the end // 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 // truncate buffer to the actual payload
m.payload_mut().truncate(actual_payload_length); m.payload.truncate(actual_payload_length);
m.into_plain_message() m.into_plain_message()
}) })
} }
ProtocolVersion::TLSv1_3 => { ProtocolVersion::TLSv1_3 => {
let nonce = cipher::Nonce::new(&self.iv, seq); let nonce = cipher::Nonce::new(&self.iv, seq);
let aad = make_tls13_aad(m.payload().len()); let aad = make_tls13_aad(m.payload.len());
self.decrypt_in_place(Nonce::<T>::from_slice(&nonce.0), &aad, m.payload_mut()) self.decrypt_in_place(
.map_err(|_| rustls::Error::DecryptError) Nonce::<T>::from_slice(&nonce.0),
.and_then(|_| m.into_tls13_unpadded_message()) &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 associated_data = header;
let nonce = cipher::Nonce::new(&self.iv, packet_number); 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()); let (buffer, tag) = payload.split_at_mut(payload.len() - self.crypter.max_overhead());
self.crypter self.crypter
@ -290,6 +327,76 @@ where
fn tag_len(&self) -> usize { fn tag_len(&self) -> usize {
<T as BoringCipher>::TAG_LEN <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>); pub(crate) struct Aead<T>(PhantomData<T>);
@ -300,17 +407,31 @@ impl<T> Aead<T> {
impl<T: BoringAead + 'static> cipher::Tls13AeadAlgorithm for Aead<T> { impl<T: BoringAead + 'static> cipher::Tls13AeadAlgorithm for Aead<T> {
fn encrypter(&self, key: cipher::AeadKey, iv: cipher::Iv) -> Box<dyn cipher::MessageEncrypter> { fn encrypter(&self, key: cipher::AeadKey, iv: cipher::Iv) -> Box<dyn cipher::MessageEncrypter> {
Box::new( match BoringAeadCrypter::<T>::new(iv, key.as_ref(), ProtocolVersion::TLSv1_3) {
BoringAeadCrypter::<T>::new(iv, key.as_ref(), ProtocolVersion::TLSv1_3) Ok(crypter) => Box::new(crypter),
.expect("failed to create AEAD 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> { fn decrypter(&self, key: cipher::AeadKey, iv: cipher::Iv) -> Box<dyn cipher::MessageDecrypter> {
Box::new( match BoringAeadCrypter::<T>::new(iv, key.as_ref(), ProtocolVersion::TLSv1_3) {
BoringAeadCrypter::<T>::new(iv, key.as_ref(), ProtocolVersion::TLSv1_3) Ok(crypter) => Box::new(crypter),
.expect("failed to create AEAD crypter"), Err(err) => {
) log_and_map(
"Tls13AeadAlgorithm::decrypter BoringAeadCrypter::new",
err,
(),
);
Box::new(InvalidMessageDecrypter)
}
}
} }
fn key_len(&self) -> usize { fn key_len(&self) -> usize {
@ -324,6 +445,10 @@ impl<T: BoringAead + 'static> cipher::Tls13AeadAlgorithm for Aead<T> {
) -> Result<ConnectionTrafficSecrets, cipher::UnsupportedOperationError> { ) -> Result<ConnectionTrafficSecrets, cipher::UnsupportedOperationError> {
Ok(<T as BoringCipher>::extract_keys(key, iv)) Ok(<T as BoringCipher>::extract_keys(key, iv))
} }
fn fips(&self) -> bool {
cfg!(feature = "fips") && <T as BoringCipher>::FIPS_APPROVED
}
} }
#[cfg(feature = "tls12")] #[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()); let mut full_iv = Vec::with_capacity(iv.len() + extra.len());
full_iv.extend_from_slice(iv); full_iv.extend_from_slice(iv);
full_iv.extend_from_slice(extra); full_iv.extend_from_slice(extra);
Box::new( match BoringAeadCrypter::<T>::new(
BoringAeadCrypter::<T>::new(Iv::copy(&full_iv), key.as_ref(), ProtocolVersion::TLSv1_2) Iv::copy(&full_iv),
.expect("failed to create AEAD crypter"), 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> { 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); 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(iv);
pseudo_iv.extend_from_slice(&vec![0u8; <T as BoringCipher>::EXPLICIT_NONCE_LEN]); pseudo_iv.extend_from_slice(&vec![0u8; <T as BoringCipher>::EXPLICIT_NONCE_LEN]);
Box::new( match BoringAeadCrypter::<T>::new(
BoringAeadCrypter::<T>::new( Iv::copy(&pseudo_iv),
Iv::copy(&pseudo_iv), key.as_ref(),
key.as_ref(), ProtocolVersion::TLSv1_2,
ProtocolVersion::TLSv1_2, ) {
) Ok(crypter) => Box::new(crypter),
.expect("failed to create AEAD crypter"), Err(err) => {
) log_and_map(
"Tls12AeadAlgorithm::decrypter BoringAeadCrypter::new",
err,
(),
);
Box::new(InvalidMessageDecrypter)
}
}
} }
fn key_block_shape(&self) -> cipher::KeyBlockShape { fn key_block_shape(&self) -> cipher::KeyBlockShape {
@ -374,7 +517,13 @@ impl<T: BoringAead + 'static> cipher::Tls12AeadAlgorithm for Aead<T> {
let nonce = { let nonce = {
let fixed_iv_len = <T as BoringCipher>::FIXED_IV_LEN; let fixed_iv_len = <T as BoringCipher>::FIXED_IV_LEN;
let explicit_nonce_len = <T as BoringCipher>::EXPLICIT_NONCE_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 // 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))) 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> { struct QuicHeaderProtector<T: QuicCipher> {
@ -400,8 +553,8 @@ impl<T: QuicCipher> QuicHeaderProtector<T> {
first: &mut u8, first: &mut u8,
packet_number: &mut [u8], packet_number: &mut [u8],
remove: bool, remove: bool,
) { ) -> Result<(), rustls::Error> {
let mask = T::header_protection_mask(self.key.as_ref(), sample); let mask = T::header_protection_mask(self.key.as_ref(), sample)?;
const LONG_HEADER_FORMAT: u8 = 0x80; const LONG_HEADER_FORMAT: u8 = 0x80;
let bits_to_mask = if (*first & LONG_HEADER_FORMAT) == LONG_HEADER_FORMAT { 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) { for (pn_byte, m) in packet_number.iter_mut().zip(&mask[1..]).take(pn_length) {
*pn_byte ^= m; *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())); 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(()) Ok(())
} }
@ -458,7 +613,7 @@ impl<T: QuicCipher> rustls::quic::HeaderProtectionKey for QuicHeaderProtector<T>
return Err(rustls::Error::General("packet number too long".into())); 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(()) Ok(())
} }
@ -473,10 +628,13 @@ where
T: QuicCipher + BoringAead + 'static, T: QuicCipher + BoringAead + 'static,
{ {
fn packet_key(&self, key: cipher::AeadKey, iv: Iv) -> Box<dyn rustls::quic::PacketKey> { fn packet_key(&self, key: cipher::AeadKey, iv: Iv) -> Box<dyn rustls::quic::PacketKey> {
Box::new( match BoringAeadCrypter::<T>::new(iv, key.as_ref(), ProtocolVersion::TLSv1_3) {
BoringAeadCrypter::<T>::new(iv, key.as_ref(), ProtocolVersion::TLSv1_3) Ok(crypter) => Box::new(crypter),
.expect("failed to create AEAD crypter"), Err(err) => {
) log_and_map("QuicAlgorithm::packet_key BoringAeadCrypter::new", err, ());
Box::new(InvalidPacketKey::<T>(PhantomData))
}
}
} }
fn header_protection_key( fn header_protection_key(
@ -492,17 +650,78 @@ where
fn aead_key_len(&self) -> usize { fn aead_key_len(&self) -> usize {
<T as QuicCipher>::KEY_SIZE <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)] #[cfg(test)]
mod tests { mod tests {
use hex_literal::hex; 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 crate::aead::BoringAeadCrypter;
use rustls::quic::PacketKey; use rustls::quic::PacketKey;
use super::{chacha20::ChaCha20Poly1305, QuicHeaderProtector}; use super::aes::Aes128;
use super::{chacha20::ChaCha20Poly1305, BoringCipher, QuicHeaderProtector};
#[test] #[test]
fn quic_header_protection_short() { fn quic_header_protection_short() {
@ -519,14 +738,97 @@ mod tests {
phantom: std::marker::PhantomData::<ChaCha20Poly1305>, 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[..]); assert_eq!(&header[..], &protected_header[..]);
let (first, packet_number) = header.split_at_mut(1); 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[..]); 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] #[test]
fn quic_chacha20_crypt() { fn quic_chacha20_crypt() {
// test vector from https://www.rfc-editor.org/rfc/rfc9001.html#name-chacha20-poly1305-short-hea // 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 unprotected_header = hex!("4200bff4");
let protector = BoringAeadCrypter::<ChaCha20Poly1305>::new( let protector = BoringAeadCrypter::<ChaCha20Poly1305>::new(
Iv::new(iv), Iv::from(iv),
&key, &key,
rustls::ProtocolVersion::TLSv1_3, ProtocolVersion::TLSv1_3,
) )
.unwrap(); .unwrap();
@ -560,4 +862,47 @@ mod tests {
assert_eq!(cleartext, expected_cleartext); 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 { impl BoringCipher for Aes128 {
const EXPLICIT_NONCE_LEN: usize = 8; const EXPLICIT_NONCE_LEN: usize = 8;
#[cfg(feature = "tls12")]
const FIXED_IV_LEN: usize = 4; const FIXED_IV_LEN: usize = 4;
const KEY_SIZE: usize = 16; const KEY_SIZE: usize = 16;
const TAG_LEN: 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 { fn new_cipher() -> Algorithm {
Algorithm::aes_128_gcm() Algorithm::aes_128_gcm()
} }
@ -36,7 +41,7 @@ impl QuicCipher for Aes128 {
const KEY_SIZE: usize = <Self as BoringCipher>::KEY_SIZE; const KEY_SIZE: usize = <Self as BoringCipher>::KEY_SIZE;
const SAMPLE_LEN: usize = 16; 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::< quic_header_protection_mask::<
{ <Self as QuicCipher>::KEY_SIZE }, { <Self as QuicCipher>::KEY_SIZE },
{ <Self as QuicCipher>::SAMPLE_LEN }, { <Self as QuicCipher>::SAMPLE_LEN },
@ -52,12 +57,17 @@ impl BoringAead for Aes256 {}
impl BoringCipher for Aes256 { impl BoringCipher for Aes256 {
const EXPLICIT_NONCE_LEN: usize = 8; const EXPLICIT_NONCE_LEN: usize = 8;
#[cfg(feature = "tls12")]
const FIXED_IV_LEN: usize = 4; const FIXED_IV_LEN: usize = 4;
const KEY_SIZE: usize = 32; const KEY_SIZE: usize = 32;
const TAG_LEN: 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 { fn new_cipher() -> Algorithm {
Algorithm::aes_256_gcm() Algorithm::aes_256_gcm()
} }
@ -77,7 +87,7 @@ impl QuicCipher for Aes256 {
const KEY_SIZE: usize = <Self as BoringCipher>::KEY_SIZE; const KEY_SIZE: usize = <Self as BoringCipher>::KEY_SIZE;
const SAMPLE_LEN: usize = 16; 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::< quic_header_protection_mask::<
{ <Self as QuicCipher>::KEY_SIZE }, { <Self as QuicCipher>::KEY_SIZE },
{ <Self as QuicCipher>::SAMPLE_LEN }, { <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, cipher: boring::symm::Cipher,
hp_key: &[u8], hp_key: &[u8],
sample: &[u8], sample: &[u8],
) -> [u8; 5] { ) -> Result<[u8; 5], rustls::Error> {
assert!(hp_key.len() == KEY_SIZE); if hp_key.len() != KEY_SIZE {
assert!(sample.len() >= SAMPLE_LEN); 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 output = [0u8; SAMPLE_LEN];
let mut crypter = boring::symm::Crypter::new(cipher, boring::symm::Mode::Encrypt, hp_key, None) 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
let _ = len + crypter.finalize(&mut output[len..]).unwrap(); .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)] #[cfg(test)]

View file

@ -14,12 +14,17 @@ impl BoringAead for ChaCha20Poly1305 {}
impl BoringCipher for ChaCha20Poly1305 { impl BoringCipher for ChaCha20Poly1305 {
const EXPLICIT_NONCE_LEN: usize = 0; const EXPLICIT_NONCE_LEN: usize = 0;
#[cfg(feature = "tls12")]
const FIXED_IV_LEN: usize = 12; const FIXED_IV_LEN: usize = 12;
const KEY_SIZE: usize = 32; const KEY_SIZE: usize = 32;
const TAG_LEN: usize = 16; 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 { fn new_cipher() -> Algorithm {
Algorithm::chacha20_poly1305() Algorithm::chacha20_poly1305()
} }
@ -33,13 +38,21 @@ impl QuicCipher for ChaCha20Poly1305 {
const KEY_SIZE: usize = 32; const KEY_SIZE: usize = 32;
const SAMPLE_LEN: usize = 16; 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> {
assert!(hp_key.len() == <Self as QuicCipher>::KEY_SIZE); if hp_key.len() != <Self as QuicCipher>::KEY_SIZE {
assert!(sample.len() >= <Self as QuicCipher>::SAMPLE_LEN); 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]; 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. // 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. // RFC9001 5.4.4: The remaining 12 bytes are used as the nonce.
let nonce = &sample[4..16]; let nonce = &sample[4..16];
unsafe { unsafe {
@ -52,7 +65,7 @@ impl QuicCipher for ChaCha20Poly1305 {
counter, counter,
); );
}; };
mask Ok(mask)
} }
} }
@ -91,7 +104,8 @@ mod tests {
let sample = hex!("5e5cd55c41f69080575d7999c25a5bfb"); let sample = hex!("5e5cd55c41f69080575d7999c25a5bfb");
let hp_key = hex!("25a282b9e82f06f21f488917a4fc8f1b73573685608597d0efcb076b0ab7a7a4"); let hp_key = hex!("25a282b9e82f06f21f488917a4fc8f1b73573685608597d0efcb076b0ab7a7a4");
let expected_mask = hex!("aefefe7d03"); 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); 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::SHA256 => hash::HashAlgorithm::SHA256,
boring::nid::Nid::SHA384 => hash::HashAlgorithm::SHA384, boring::nid::Nid::SHA384 => hash::HashAlgorithm::SHA384,
boring::nid::Nid::SHA512 => hash::HashAlgorithm::SHA512, 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 { fn output_len(&self) -> usize {
MessageDigest::from_nid(self.0) MessageDigest::from_nid(self.0)
.expect("failed getting digest") .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"))] #[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 mapped
} }

View file

@ -2,6 +2,7 @@ use std::marker::PhantomData;
use boring::hash::MessageDigest; use boring::hash::MessageDigest;
use rustls::crypto::tls13::{self, Hkdf as RustlsHkdf}; use rustls::crypto::tls13::{self, Hkdf as RustlsHkdf};
use zeroize::Zeroizing;
use crate::helper::{cvt, cvt_p}; use crate::helper::{cvt, cvt_p};
@ -67,7 +68,7 @@ impl<T: BoringHash> RustlsHkdf for Hkdf<T> {
let digest = T::new_hash(); let digest = T::new_hash();
let hash_size = digest.size(); 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; let mut prk_len = 0;
// if salt isn't set we usen these bytes here as salt // 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, okm: &rustls::crypto::tls13::OkmBlock,
) -> Box<dyn rustls::crypto::tls13::HkdfExpander> { ) -> Box<dyn rustls::crypto::tls13::HkdfExpander> {
let okm = okm.as_ref(); 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(); let prk_len = okm.len();
prk[..prk_len].copy_from_slice(okm); prk[..prk_len].copy_from_slice(okm);
@ -121,7 +122,7 @@ impl<T: BoringHash> RustlsHkdf for Hkdf<T> {
message: &[u8], message: &[u8],
) -> rustls::crypto::hmac::Tag { ) -> rustls::crypto::hmac::Tag {
let digest = T::new_hash(); 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; let mut hash_len = 0u32;
unsafe { unsafe {
cvt_p(boring_sys::HMAC( 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]) rustls::crypto::hmac::Tag::new(&hash[..hash_len as usize])
} }
fn fips(&self) -> bool {
cfg!(feature = "fips")
}
} }
struct HkdfExpander { 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, prk_len: usize,
digest: MessageDigest, digest: MessageDigest,
} }
@ -162,9 +167,17 @@ impl tls13::HkdfExpander for HkdfExpander {
info: &[&[u8]], info: &[&[u8]],
output: &mut [u8], output: &mut [u8],
) -> Result<(), tls13::OutputLengthError> { ) -> 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(); let info_concat = info.concat();
unsafe { unsafe {
boring_sys::HKDF_expand( cvt(boring_sys::HKDF_expand(
output.as_mut_ptr(), output.as_mut_ptr(),
output.len(), output.len(),
self.digest.as_ptr(), self.digest.as_ptr(),
@ -172,8 +185,9 @@ impl tls13::HkdfExpander for HkdfExpander {
self.prk_len, self.prk_len,
info_concat.as_ptr(), info_concat.as_ptr(),
info_concat.len(), info_concat.len(),
); ))
}; .map_err(|_| tls13::OutputLengthError)?;
}
Ok(()) Ok(())
} }
@ -187,7 +201,7 @@ impl tls13::HkdfExpander for HkdfExpander {
/// This is infallible, because by definition `OkmBlock` is always exactly /// This is infallible, because by definition `OkmBlock` is always exactly
/// `HashLen` bytes long. /// `HashLen` bytes long.
fn expand_block(&self, info: &[&[u8]]) -> tls13::OkmBlock { 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(); let output_len = self.hash_len();
self.expand_slice(info, &mut output[..output_len]) self.expand_slice(info, &mut output[..output_len])
@ -200,3 +214,35 @@ impl tls13::HkdfExpander for HkdfExpander {
self.digest.size() 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 boring_additions::hmac::HmacCtx;
use foreign_types::ForeignType; use foreign_types::ForeignType;
use rustls::crypto; use rustls::crypto;
use zeroize::Zeroizing;
use crate::helper::{cvt, cvt_p}; use crate::helper::{cvt, cvt_p};
@ -19,18 +20,11 @@ struct BoringHmac(pub boring::nid::Nid);
impl crypto::hmac::Hmac for BoringHmac { impl crypto::hmac::Hmac for BoringHmac {
fn with_key(&self, key: &[u8]) -> Box<dyn crypto::hmac::Key> { 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"); let md = MessageDigest::from_nid(self.0).expect("failed getting digest");
Box::new(BoringHmacKey { Box::new(BoringHmacKey {
ctx,
md, 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") .expect("failed getting digest")
.size() .size()
} }
fn fips(&self) -> bool {
cfg!(feature = "fips")
}
} }
#[derive(Clone)]
struct BoringHmacKey { struct BoringHmacKey {
ctx: HmacCtx,
md: MessageDigest, md: MessageDigest,
key: Vec<u8>, key: Zeroizing<Vec<u8>>,
} }
impl BoringHmacKey { impl BoringHmacKey {
fn init(&self) { fn init(ctx: &HmacCtx, key: &[u8], md: MessageDigest) {
unsafe { unsafe {
// initialize a new hmac
cvt(boring_sys::HMAC_Init_ex( cvt(boring_sys::HMAC_Init_ex(
self.ctx.as_ptr(), ctx.as_ptr(),
self.key.as_ptr() as *const c_void, key.as_ptr() as *const c_void,
self.key.len(), key.len(),
self.md.as_ptr(), md.as_ptr(),
ptr::null_mut(), ptr::null_mut(),
)) ))
} }
.expect("failed initializing hmac"); .expect("failed initializing hmac");
} }
fn update(&self, bytes: &[u8]) { fn update(ctx: &HmacCtx, bytes: &[u8]) {
unsafe { unsafe {
cvt(boring_sys::HMAC_Update( cvt(boring_sys::HMAC_Update(
self.ctx.as_ptr(), ctx.as_ptr(),
bytes.as_ptr(), bytes.as_ptr(),
bytes.len(), bytes.len(),
)) ))
@ -74,11 +69,11 @@ impl BoringHmacKey {
.expect("failed updating hmac"); .expect("failed updating hmac");
} }
fn finish(&self, out: &mut [u8]) -> usize { fn finish(ctx: &HmacCtx, out: &mut [u8]) -> usize {
let mut out_len = 0; let mut out_len = 0;
unsafe { unsafe {
cvt(boring_sys::HMAC_Final( cvt(boring_sys::HMAC_Final(
self.ctx.as_ptr(), ctx.as_ptr(),
out.as_mut_ptr(), out.as_mut_ptr(),
&mut out_len, &mut out_len,
)) ))
@ -86,21 +81,28 @@ impl BoringHmacKey {
.expect("failed hmac final"); .expect("failed hmac final");
out_len as usize 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 { impl crypto::hmac::Key for BoringHmacKey {
fn sign_concat(&self, first: &[u8], middle: &[&[u8]], last: &[u8]) -> crypto::hmac::Tag { 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 { for m in middle {
self.update(m); Self::update(&ctx, m);
} }
self.update(last); Self::update(&ctx, last);
let mut out = [0u8; 32]; let mut out = Zeroizing::new([0u8; boring_sys::EVP_MAX_MD_SIZE as usize]);
let out_len = self.finish(&mut out); let out_len = Self::finish(&ctx, &mut out[..]);
crypto::hmac::Tag::new(&out[..out_len]) crypto::hmac::Tag::new(&out[..out_len])
} }
@ -112,7 +114,7 @@ impl crypto::hmac::Key for BoringHmacKey {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::SHA256; use super::{SHA256, SHA384};
use hex_literal::hex; use hex_literal::hex;
#[test] #[test]
@ -141,4 +143,23 @@ mod tests {
hex!("11fa4a6ee97bebfad9e1087145c556fec9a786cad0659aa10702d21bd2968305") 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::{ #[cfg(not(feature = "fips"))]
mem::MaybeUninit, use std::{mem::MaybeUninit, ptr};
ptr::{self},
};
use boring::{ use boring::{
ec::{EcGroup, EcKey}, ec::{EcGroup, EcKey},
error::ErrorStack, error::ErrorStack,
nid::Nid, nid::Nid,
pkey::Id,
pkey::{PKey, PKeyRef, Private}, pkey::{PKey, PKeyRef, Private},
}; };
#[cfg(not(feature = "fips"))]
use boring_additions::evp::EvpPkeyCtx; use boring_additions::evp::EvpPkeyCtx;
#[cfg(not(feature = "fips"))]
use foreign_types::ForeignType; use foreign_types::ForeignType;
use rustls::crypto; 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; use super::DhKeyType;
@ -30,16 +32,11 @@ pub struct KeyExchange {
impl KeyExchange { impl KeyExchange {
/// Creates a new `KeyExchange` using a random /// Creates a new `KeyExchange` using a random
/// private key for the `X25519` Edwards curve /// private key for the `X25519` Edwards curve
#[cfg(not(feature = "fips"))]
pub fn with_x25519() -> Result<Self, ErrorStack> { pub fn with_x25519() -> Result<Self, ErrorStack> {
Self::ed_from_curve(Nid::from_raw(boring_sys::NID_X25519)) 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 /// Creates a new `KeyExchange` using a random
/// private key for `sepc256r1` curve /// private key for `sepc256r1` curve
/// Also known as `X9_62_PRIME256V1` /// Also known as `X9_62_PRIME256V1`
@ -53,12 +50,6 @@ impl KeyExchange {
Self::ec_from_curve(Nid::SECP384R1) 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 /// Allows getting a new `KeyExchange` using Eliptic Curves
/// on the specified curve /// on the specified curve
fn ec_from_curve(nid: Nid) -> Result<Self, ErrorStack> { fn ec_from_curve(nid: Nid) -> Result<Self, ErrorStack> {
@ -76,6 +67,7 @@ impl KeyExchange {
/// Allows getting a new `KeyExchange` using Edwards Curves /// Allows getting a new `KeyExchange` using Edwards Curves
/// on the specified curve /// on the specified curve
#[cfg(not(feature = "fips"))]
fn ed_from_curve(nid: Nid) -> Result<Self, ErrorStack> { fn ed_from_curve(nid: Nid) -> Result<Self, ErrorStack> {
let pkey_ctx = unsafe { let pkey_ctx = unsafe {
EvpPkeyCtx::from_ptr(cvt_p(boring_sys::EVP_PKEY_CTX_new_id( 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 /// Decodes a SPKI public key to it's raw public key component
fn raw_public_key(pkey: &PKeyRef<Private>) -> Result<Vec<u8>, ErrorStack> { 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 return ec_key.public_key().to_bytes(
let pkey = spki::SubjectPublicKeyInfoRef::from_der(spki.as_ref()) ec_key.group(),
.expect("failed parsing spki bytes"); boring::ec::PointConversionForm::UNCOMPRESSED,
&mut bn_ctx,
);
}
// return the raw public key as a new vec let mut output = vec![0u8; pkey.raw_public_key_len()?];
Ok(Vec::from( let used_len = {
pkey.subject_public_key let used = pkey.raw_public_key(&mut output)?;
.as_bytes() used.len()
.expect("failed getting raw spki bytes"), };
)) output.truncate(used_len);
Ok(output)
} }
/// Derives a shared secret using the peer's raw public key /// 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())? 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))?, 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)?; let mut deriver = boring::derive::Deriver::new(&self.own_key)?;
@ -147,7 +145,7 @@ impl crypto::ActiveKeyExchange for KeyExchange {
peer_pub_key: &[u8], peer_pub_key: &[u8],
) -> Result<crypto::SharedSecret, rustls::Error> { ) -> Result<crypto::SharedSecret, rustls::Error> {
self.diffie_hellman(peer_pub_key) self.diffie_hellman(peer_pub_key)
.map(|x| crypto::SharedSecret::from(x.as_slice())) .map(crypto::SharedSecret::from)
.map_err(|e| { .map_err(|e| {
log_and_map( log_and_map(
"ex::KeyExchange::diffie_hellman", "ex::KeyExchange::diffie_hellman",
@ -163,12 +161,11 @@ impl crypto::ActiveKeyExchange for KeyExchange {
fn group(&self) -> rustls::NamedGroup { fn group(&self) -> rustls::NamedGroup {
match self.key_type { match self.key_type {
#[cfg(not(feature = "fips"))]
DhKeyType::ED(boring_sys::NID_X25519) => rustls::NamedGroup::X25519, 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_X9_62_prime256v1)) => rustls::NamedGroup::secp256r1,
DhKeyType::EC((_, boring_sys::NID_secp384r1)) => rustls::NamedGroup::secp384r1, DhKeyType::EC((_, boring_sys::NID_secp384r1)) => rustls::NamedGroup::secp384r1,
DhKeyType::EC((_, boring_sys::NID_secp521r1)) => rustls::NamedGroup::secp521r1, _ => unreachable!("unsupported key type"),
_ => unimplemented!(),
} }
} }
} }
@ -190,6 +187,7 @@ mod tests {
} }
#[test] #[test]
#[cfg(not(feature = "fips"))]
fn test_derive_ed() { fn test_derive_ed() {
let alice = Box::new(KeyExchange::with_x25519().unwrap()); let alice = Box::new(KeyExchange::with_x25519().unwrap());
let bob = 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