Compare commits

..
34 changed files with 759 additions and 2481 deletions

View file

@ -9,9 +9,10 @@ on:
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: -Dwarnings
FEATURES: "logging,tls12"
jobs:
fmt:
build:
runs-on: ubuntu-latest
steps:
@ -20,95 +21,9 @@ jobs:
run: sudo apt-get install -y cmake clang
- name: Check fmt
run: make fmt
test-default:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install dependencies
run: sudo apt-get install -y cmake clang
- name: Lint (default features)
run: make lint FEATURES=""
- name: Test (default features)
run: make test FEATURES=""
- name: Build (default features)
run: make build FEATURES=""
test-tls12:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install dependencies
run: sudo apt-get install -y cmake clang
- name: Lint (tls12)
run: make lint FEATURES="tls12"
- name: Test (tls12)
run: make test FEATURES="tls12"
- name: Build (tls12)
run: make build FEATURES="tls12"
test-logging-tls12:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install dependencies
run: sudo apt-get install -y cmake clang
- name: Lint (logging,tls12)
run: make lint FEATURES="logging,tls12"
- name: Test (logging,tls12)
run: make test FEATURES="logging,tls12"
- name: Build (logging,tls12)
run: make build FEATURES="logging,tls12"
test-mlkem:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install dependencies
run: sudo apt-get install -y cmake clang
- name: Lint (mlkem)
run: make lint FEATURES="mlkem"
- name: Test (mlkem)
run: make test FEATURES="mlkem"
- name: Build (mlkem)
run: make build FEATURES="mlkem"
test-mlkem-tls12:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install dependencies
run: sudo apt-get install -y cmake clang
- name: Lint (mlkem,tls12)
run: make lint FEATURES="mlkem,tls12"
- name: Test (mlkem,tls12)
run: make test FEATURES="mlkem,tls12"
- name: Build (mlkem,tls12)
run: make build FEATURES="mlkem,tls12"
test-fips:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install dependencies
run: sudo apt-get install -y cmake clang
- name: Test (fips)
run: cargo test -p boring-rustls-provider --all-targets --features fips
check-fips:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install dependencies
run: sudo apt-get install -y cmake clang
- name: Check (fips)
run: cargo check -p boring-rustls-provider --all-targets --features fips
- name: Check (fips-precompiled)
run: cargo check -p boring-rustls-provider --all-targets --features fips-precompiled
- name: Lint
run: make lint
- name: Tests usual
run: make test
- name: Build usual
run: make build

View file

@ -2,10 +2,10 @@
members = [
# things that should probably be in boring crate
"boring-additions",
# the main library and tests
"boring-rustls-provider",
# things that should probably be in boring-sys crate
"boring-sys-additions",
# the main library and tests
"boring-rustls-provider",
# tests and example code
"examples",
]
@ -17,11 +17,11 @@ default-members = [
resolver = "2"
[workspace.dependencies]
boring = { version = "5", default-features = false }
boring-sys = { version = "5", default-features = false }
rustls = { version = "0.23", default-features = false }
rustls-pemfile = { version = "2" }
rustls-pki-types = { version = "1" }
tokio-rustls = { version = "0.26", default-features = false }
webpki = { package = "rustls-webpki", version = "0.103", default-features = false }
webpki-roots = { version = "1.0" }
boring = { version = "4", default-features = false }
boring-sys = { version = "4", default-features = false }
rustls = { version = "=0.22.0-alpha.6", default-features = false }
rustls-pemfile = { version = "=2.0.0-alpha.2" }
rustls-pki-types = { version = "0.2.3" }
tokio-rustls = { version = "0.25.0-alpha.4" }
webpki = { package = "rustls-webpki", version = "0.102.0-alpha.7", default-features = false }
webpki-roots = { version = "=0.26.0-alpha.2" }

View file

@ -1,5 +1,4 @@
FEATURES ?= logging,tls12
CARGO_FEATURES := $(if $(strip $(FEATURES)),-F "$(FEATURES)",)
.PHONY: fmt
@ -8,16 +7,12 @@ fmt:
.PHONY: lint
lint:
cargo clippy --workspace --all-targets $(CARGO_FEATURES)
.PHONY: check
check:
cargo check --workspace --all-targets $(CARGO_FEATURES)
cargo clippy --workspace --all-targets -F "$(FEATURES)"
.PHONY: test
test:
cargo test --all-targets $(CARGO_FEATURES)
cargo test --all-targets -F "$(FEATURES)"
.PHONY: build
build:
cargo build --all-targets $(CARGO_FEATURES)
cargo build --all-targets -F "$(FEATURES)"

View file

@ -2,59 +2,55 @@
[![Build Status](https://github.com/janrueth/boring-rustls-provider/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/janrueth/boring-rustls-provider/actions/workflows/ci.yml?query=branch%3Amain)
A [BoringSSL](https://github.com/cloudflare/boring)-based [rustls](https://github.com/rustls/rustls) crypto provider.
This is supposed to be the start to a [boringssl](https://github.com/cloudflare/boring)-based [rustls](https://github.com/rustls/rustls) crypto provider.
Built on `boring` v5 and `rustls` 0.23.
## Status
This is just a dump of me figuring out how to interface with boring and rustls.
It works to establish a connection and exchange data but I haven't written real tests yet, nor did I cleanup the code or made the effort to make it look nice.
There is probably some code in here that should rather live in the `boring` crate.
## Features
Further, the rustls crypto provider API is still not stable it seems. This works currently with `rustls = 0.22.0-alpha.5`.
No features are enabled by default. The provider ships with TLS 1.3 support
out of the box; additional capabilities are opt-in.
| Feature | Description |
|---|---|
| `fips` | Build against FIPS-validated BoringSSL and restrict the provider to FIPS-approved algorithms only (SP 800-52r2). Implies `mlkem`. See [FIPS mode](#fips-mode) below. |
| `fips-precompiled` | Deprecated alias for `fips`. Matches the `boring` crate's feature name. |
| `mlkem` | Enable the X25519MLKEM768 post-quantum hybrid key exchange group (`draft-ietf-tls-ecdhe-mlkem-00`). Uses ML-KEM-768 (FIPS 203) combined with X25519. See [Post-quantum key exchange](#post-quantum-key-exchange). |
| `tls12` | Enable TLS 1.2 cipher suites (`ECDHE-ECDSA` and `ECDHE-RSA` with AES-GCM and ChaCha20-Poly1305). Without this only TLS 1.3 is available. |
| `logging` | Enable debug logging of BoringSSL errors and provider internals via the `log` crate. |
## Supported Algorithms
### Cipher Suites
TLS 1.3 (always available):
### Supported ciphers
Currently, supports only TLS 1.3:
```
AES_128_GCM_SHA256
AES_256_GCM_SHA384
CHACHA20_POLY1305_SHA256
```
TLS 1.2 (requires `tls12` feature):
QUIC: not yet supported
TLS 1.2:
```
ECDHE_ECDSA_AES128_GCM_SHA256
ECDHE_RSA_AES128_GCM_SHA256
ECDHE_ECDSA_AES256_GCM_SHA384
ECDHE_RSA_AES256_GCM_SHA384
ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
```
### Key Exchange Groups
Matches boring's default supported group list:
### Key Exchange Algorithms
`ECDHE` with curves:
```
X25519MLKEM768 (0x11ec, requires mlkem feature, TLS 1.3 only)
X25519
secp256r1 (P-256)
secp384r1 (P-384)
X448
secp256r1
secp384r1
secp521r1
```
When `mlkem` is enabled, X25519MLKEM768 is the preferred (first) group in both
FIPS and non-FIPS configurations.
### Signature Algorithms
`FFDHE` with:
```
ffdhe2048
```
### Signature Generation / Verification
```
RSA_PKCS1_SHA256
@ -70,47 +66,6 @@ ED25519
ED448
```
## Post-Quantum Key Exchange
The `mlkem` feature enables the **X25519MLKEM768** hybrid key exchange group
per `draft-ietf-tls-ecdhe-mlkem-00`. This combines classical X25519
Diffie-Hellman with ML-KEM-768 (FIPS 203) post-quantum key encapsulation,
ensuring that connections are secure against both classical and quantum
adversaries.
The `fips` feature implies `mlkem`, so X25519MLKEM768 is always available
in FIPS mode.
Wire format (ML-KEM component first in all encodings):
- Client key share: `mlkem_pk(1184) || x25519_pk(32)` = 1216 bytes
- Server key share: `mlkem_ct(1088) || x25519_pk(32)` = 1120 bytes
- Shared secret: `mlkem_ss(32) || x25519_ss(32)` = 64 bytes
Interoperability has been verified against Cloudflare's PQ endpoints
(`pq.cloudflareresearch.com`).
## FIPS Mode
When the `fips` feature is enabled the provider builds against a FIPS-validated
version of BoringSSL and restricts all algorithm selections to those approved
under [SP 800-52r2](https://doi.org/10.6028/NIST.SP.800-52r2), aligned with
boring's `fips202205` compliance policy:
- **Cipher suites**: AES-GCM only (no ChaCha20-Poly1305).
- **Key exchange groups**: X25519MLKEM768 (preferred), P-256, and P-384 only
(no standalone X25519).
- **Signature algorithms**: RSA PKCS#1 / PSS and ECDSA with P-256 or P-384 only
(no P-521, Ed25519, or Ed448).
## Workspace Structure
| Crate | Purpose |
|---|---|
| `boring-rustls-provider` | The main rustls crypto provider. |
| `boring-additions` | Safe Rust wrappers for BoringSSL APIs not yet exposed by the `boring` crate (AEAD, EVP_PKEY_CTX, HMAC_CTX). Intended for upstreaming. |
| `boring-sys-additions` | Raw FFI binding for `CRYPTO_tls1_prf` (internal BoringSSL symbol used for FIPS-compliant TLS 1.2 PRF). Intended for upstreaming. |
| `examples` | Example client binary. |
## License
MIT

View file

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

View file

@ -78,7 +78,11 @@ impl Crypter {
///
/// # Errors
/// Returns the `BoringSSL` error in case of an internal error
///
/// # Panics
/// * If the key length mismatches the `aead_alg` required key length
pub fn new(aead_alg: &Algorithm, key: &[u8]) -> Result<Self, ErrorStack> {
assert_eq!(aead_alg.key_length(), key.len());
boring_sys::init();
let this = unsafe {
@ -112,6 +116,10 @@ impl Crypter {
///
/// # Errors
/// In case of an error, returns the `BoringSSL` error
///
/// # Panics
/// * If the `nonce` is not the expected lenght
/// * If the `tag` has not enough space
pub fn seal_in_place(
&self,
nonce: &[u8],
@ -119,9 +127,8 @@ impl Crypter {
buffer: &mut [u8],
tag: &mut [u8],
) -> Result<usize, ErrorStack> {
if tag.len() < self.max_overhead || nonce.len() != self.nonce_len {
return Err(ErrorStack::get());
}
assert!(tag.len() >= self.max_overhead);
assert_eq!(nonce.len(), self.nonce_len);
let mut tag_len = tag.len();
unsafe {
@ -149,6 +156,9 @@ impl Crypter {
///
/// # Errors
/// In case of an error, returns the `BoringSSL` error
///
/// # Panics
/// * if the nonce has the wrong lenght
pub fn open_in_place(
&self,
nonce: &[u8],
@ -156,9 +166,7 @@ impl Crypter {
buffer: &mut [u8],
tag: &[u8],
) -> Result<(), ErrorStack> {
if nonce.len() != self.nonce_len {
return Err(ErrorStack::get());
}
assert_eq!(nonce.len(), self.nonce_len);
unsafe {
cvt(boring_sys::EVP_AEAD_CTX_open_gather(
@ -180,11 +188,11 @@ impl Crypter {
#[cfg(test)]
mod tests {
use super::{Algorithm, Crypter};
use super::Crypter;
#[test]
fn in_out() {
let key = Crypter::new(&Algorithm::aes_128_gcm(), &[0u8; 16]).unwrap();
let key = Crypter::new(&super::Algorithm::aes_128_gcm(), &[0u8; 16]).unwrap();
let nonce = [0u8; 12];
let associated_data = b"this is authenticated";
let mut buffer = Vec::with_capacity(26);
@ -199,35 +207,4 @@ mod tests {
assert_eq!(b"ABCDE", buffer.as_slice());
}
#[test]
fn new_rejects_invalid_key_length() {
let result = Crypter::new(&Algorithm::aes_128_gcm(), &[0u8; 15]);
assert!(result.is_err());
}
#[test]
fn seal_rejects_invalid_nonce_and_tag_lengths() {
let key = Crypter::new(&Algorithm::aes_128_gcm(), &[0u8; 16]).unwrap();
let mut payload = [0u8; 8];
let mut short_tag = [0u8; 8];
let short_tag_result = key.seal_in_place(&[0u8; 12], b"", &mut payload, &mut short_tag);
assert!(short_tag_result.is_err());
let mut tag = [0u8; 16];
let wrong_nonce_result = key.seal_in_place(&[0u8; 11], b"", &mut payload, &mut tag);
assert!(wrong_nonce_result.is_err());
}
#[test]
fn open_rejects_invalid_nonce_length() {
let key = Crypter::new(&Algorithm::aes_128_gcm(), &[0u8; 16]).unwrap();
let mut payload = [0u8; 8];
let tag = [0u8; 16];
let result = key.open_in_place(&[0u8; 11], b"", &mut payload, &tag);
assert!(result.is_err());
}
}

View file

@ -5,6 +5,8 @@ use std::{
use foreign_types::{ForeignType, ForeignTypeRef, Opaque};
use crate::helper::{cvt, cvt_p};
pub struct HmacCtxRef(Opaque);
unsafe impl ForeignTypeRef for HmacCtxRef {
@ -33,6 +35,20 @@ unsafe impl ForeignType for HmacCtx {
}
}
impl Clone for HmacCtx {
fn clone(&self) -> Self {
unsafe {
cvt_p(boring_sys::HMAC_CTX_new())
.map(|ctx| HmacCtx::from_ptr(ctx))
.and_then(|ctx| {
cvt(boring_sys::HMAC_CTX_copy(ctx.as_ptr(), self.0.as_ptr()))?;
Ok(ctx)
})
}
.expect("failed cloning hmac ctx")
}
}
impl Drop for HmacCtx {
fn drop(&mut self) {
unsafe {

View file

@ -8,52 +8,31 @@ description = "Boringssl rustls provider"
publish = false
[features]
default = []
# Build against a FIPS-validated version of BoringSSL and restrict the
# provider to FIPS-approved algorithms only. This affects:
# - Cipher suites: AES-GCM only (no ChaCha20-Poly1305).
# - Key exchange groups: X25519MLKEM768 (preferred), P-256, and P-384
# only (no X25519, X448, P-521, or FFDHE).
# - Signature algorithms: RSA PKCS#1 / PSS and ECDSA with P-256/P-384
# only (no P-521, Ed25519, or Ed448).
# Implies `mlkem` to ensure the post-quantum hybrid group is available.
# Aligned with boring's `fips202205` compliance policy (SP 800-52r2).
fips = ["boring/fips", "mlkem"]
# Enable X25519MLKEM768 post-quantum hybrid key exchange group
# (draft-ietf-tls-ecdhe-mlkem-00). Uses ML-KEM-768 (FIPS 203) combined
# with X25519 for hybrid post-quantum/classical key agreement.
mlkem = ["boring/mlkem"]
# Deprecated alias for `fips`. Matches the boring crate's feature name
# for backwards compatibility.
fips-precompiled = ["fips"]
# Enable TLS 1.2 cipher suites (ECDHE-ECDSA and ECDHE-RSA with AES-GCM
# and ChaCha20-Poly1305). Without this feature only TLS 1.3 is available.
default = ["tls12"]
# Use a FIPS-validated version of boringssl.
fips = ["boring/fips", "boring-sys/fips"]
logging = ["log"]
fips-only = []
tls12 = ["rustls/tls12"]
# Enable debug logging of BoringSSL errors and provider internals via
# the `log` crate. Useful for diagnosing handshake failures.
logging = ["log"]
[dependencies]
aead = {version = "0.5", default-features = false, features = ["alloc"] }
aead = {version = "0.5", default_features = false, features = ["alloc"] }
boring = { workspace = true }
boring-additions = { path = "../boring-additions" }
boring-sys = { workspace = true }
boring-sys-additions = { path = "../boring-sys-additions" }
foreign-types = "0.5"
lazy_static = "1.4"
log = { version = "0.4.4", optional = true }
once_cell = "1"
rustls = { workspace = true }
rustls-pki-types = { workspace = true }
spki = "0.7"
zeroize = "1"
webpki = { workspace = true, features = ["alloc", "std"] }
[dev-dependencies]
hex-literal = "1"
rcgen = "0.12"
hex-literal = "0.4"
rcgen = "0.11.3"
tokio = { version = "1.34", features = ["macros", "rt", "net", "io-util", "io-std"] }
tokio-rustls = { workspace = true }
webpki-roots = { workspace = true }

View file

@ -1,11 +1,9 @@
use std::marker::PhantomData;
use aead::{AeadCore, AeadInPlace, Buffer, Nonce, Tag};
use aead::{AeadCore, AeadInPlace, Nonce, Tag};
use boring::error::ErrorStack;
use boring_additions::aead::Algorithm;
#[cfg(feature = "tls12")]
use rustls::crypto::cipher::make_tls12_aad;
use rustls::crypto::cipher::{self, make_tls13_aad, BorrowedPayload, Iv, PrefixedPayload};
use rustls::crypto::cipher::{self, make_tls12_aad, make_tls13_aad, Iv};
use rustls::{ConnectionTrafficSecrets, ContentType, ProtocolVersion};
use crate::helper::log_and_map;
@ -20,8 +18,6 @@ pub(crate) trait BoringCipher {
/// The IV's fixed length (Not the full IV length, only the part that doesn't change).
/// Together with [`BoringCipher::explicit_nonce_len`] it determines the total
/// lengths of the used nonce.
/// Used only by TLS 1.2 code paths.
#[cfg(feature = "tls12")]
const FIXED_IV_LEN: usize;
/// The key size in bytes
const KEY_SIZE: usize;
@ -29,15 +25,6 @@ pub(crate) trait BoringCipher {
/// The length of the authentication tag
const TAG_LEN: usize;
/// integrity limit
const INTEGRITY_LIMIT: u64;
/// confidentiality limit
const CONFIDENTIALITY_LIMIT: u64;
/// Whether this algorithm is FIPS-approved.
const FIPS_APPROVED: bool;
/// Constructs a new instance of this cipher as an AEAD algorithm
fn new_cipher() -> Algorithm;
@ -45,14 +32,14 @@ pub(crate) trait BoringCipher {
fn extract_keys(key: cipher::AeadKey, iv: cipher::Iv) -> ConnectionTrafficSecrets;
}
pub(crate) trait QuicCipher: Send + Sync {
pub(crate) trait QuicCipher {
/// The key size in bytes
const KEY_SIZE: usize;
/// the expected length of a sample
const SAMPLE_LEN: usize;
fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> Result<[u8; 5], rustls::Error>;
fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> [u8; 5];
}
pub(crate) trait BoringAead: BoringCipher + AeadCore + Send + Sync {}
@ -77,21 +64,19 @@ impl<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> {
let tls_version_supported = match tls_version {
assert!(match tls_version {
#[cfg(feature = "tls12")]
ProtocolVersion::TLSv1_2 => true,
ProtocolVersion::TLSv1_3 => true,
_ => false,
};
if !tls_version_supported {
return Err(ErrorStack::get());
}
});
let cipher = <T as BoringCipher>::new_cipher();
if cipher.nonce_len() != rustls::crypto::cipher::Nonce::new(&iv, 0).0.len() {
return Err(ErrorStack::get());
}
assert_eq!(
cipher.nonce_len(),
rustls::crypto::cipher::Nonce::new(&iv, 0).0.len()
);
let crypter = BoringAeadCrypter {
crypter: boring_additions::aead::Crypter::new(&cipher, key)?,
@ -138,10 +123,11 @@ where
{
fn encrypt(
&mut self,
msg: cipher::OutboundPlainMessage,
msg: cipher::BorrowedPlainMessage,
seq: u64,
) -> Result<cipher::OutboundOpaqueMessage, rustls::Error> {
) -> Result<cipher::OpaqueMessage, rustls::Error> {
let nonce = cipher::Nonce::new(&self.iv, seq);
match self.tls_version {
#[cfg(feature = "tls12")]
ProtocolVersion::TLSv1_2 => {
@ -150,57 +136,50 @@ where
let total_len = self.encrypted_payload_len(msg.payload.len());
let mut full_payload = PrefixedPayload::with_capacity(total_len);
let mut full_payload = Vec::with_capacity(total_len);
full_payload.extend_from_slice(&nonce.0.as_ref()[fixed_iv_len..]);
full_payload.extend_from_chunks(&msg.payload);
full_payload.extend_from_slice(msg.payload);
full_payload.extend_from_slice(&vec![0u8; self.crypter.max_overhead()]);
let (_, payload) = full_payload.as_mut().split_at_mut(explicit_nonce_len);
let (_, payload) = full_payload.split_at_mut(explicit_nonce_len);
let (payload, tag) = payload.split_at_mut(msg.payload.len());
let aad = cipher::make_tls12_aad(seq, msg.typ, msg.version, msg.payload.len());
self.crypter
.seal_in_place(&nonce.0, &aad, payload, tag)
.map_err(|_| rustls::Error::EncryptError)
.map(|_| cipher::OutboundOpaqueMessage::new(msg.typ, msg.version, full_payload))
.map(|_| cipher::OpaqueMessage::new(msg.typ, msg.version, full_payload))
}
ProtocolVersion::TLSv1_3 => {
let total_len = self.encrypted_payload_len(msg.payload.len());
let mut payload = PrefixedPayload::with_capacity(total_len);
payload.extend_from_chunks(&msg.payload);
payload.extend_from_slice(&msg.typ.to_array());
let mut payload = Vec::with_capacity(total_len);
payload.extend_from_slice(msg.payload);
payload.push(msg.typ.get_u8());
let aad = cipher::make_tls13_aad(total_len);
self.encrypt_in_place(
Nonce::<T>::from_slice(&nonce.0),
&aad,
&mut EncryptBufferAdapter(&mut payload),
)
self.encrypt_in_place(Nonce::<T>::from_slice(&nonce.0), &aad, &mut payload)
.map_err(|_| rustls::Error::EncryptError)
.map(|_| {
cipher::OutboundOpaqueMessage::new(
cipher::OpaqueMessage::new(
ContentType::ApplicationData,
ProtocolVersion::TLSv1_2,
payload,
)
})
}
_ => Err(rustls::Error::EncryptError),
_ => unimplemented!(),
}
}
fn encrypted_payload_len(&self, payload_len: usize) -> usize {
let per_record_overhead = match self.tls_version {
ProtocolVersion::TLSv1_2 => self
.crypter
.max_overhead()
.saturating_add(<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)
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!(),
}
}
}
@ -208,30 +187,23 @@ impl<T> cipher::MessageDecrypter for BoringAeadCrypter<T>
where
T: BoringAead,
{
fn decrypt<'a>(
fn decrypt(
&mut self,
mut m: cipher::InboundOpaqueMessage<'a>,
mut m: cipher::OpaqueMessage,
seq: u64,
) -> Result<cipher::InboundPlainMessage<'a>, rustls::Error> {
) -> Result<cipher::PlainMessage, 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() - tag_len - explicit_nonce_len;
let actual_payload_length =
m.payload().len() - self.crypter.max_overhead() - explicit_nonce_len;
let aad = make_tls12_aad(seq, m.typ, m.version, actual_payload_length);
let payload = &mut m.payload;
let payload = m.payload_mut();
// get the nonce
let (explicit_nonce, payload) = payload.split_at_mut(explicit_nonce_len);
@ -239,9 +211,7 @@ where
let nonce = {
let fixed_iv_len = <T as BoringCipher>::FIXED_IV_LEN;
if explicit_nonce_len + fixed_iv_len != 12 {
return Err(rustls::Error::DecryptError);
}
assert_eq!(explicit_nonce_len + fixed_iv_len, 12);
// grab the IV by constructing a nonce, this is just an xor
let iv = cipher::Nonce::new(&self.iv, seq).0;
@ -252,33 +222,30 @@ where
};
// split off the authentication tag
let (payload, tag) = payload.split_at_mut(payload.len() - tag_len);
let (payload, tag) =
payload.split_at_mut(payload.len() - self.crypter.max_overhead());
self.crypter
.open_in_place(&nonce, &aad, payload, tag)
.map_err(|e| log_and_map("open_in_place", e, rustls::Error::DecryptError))
.map(|_| {
// rotate the nonce to the end
m.payload.rotate_left(explicit_nonce_len);
m.payload_mut().rotate_left(explicit_nonce_len);
// truncate buffer to the actual payload
m.payload.truncate(actual_payload_length);
m.payload_mut().truncate(actual_payload_length);
m.into_plain_message()
})
}
ProtocolVersion::TLSv1_3 => {
let nonce = cipher::Nonce::new(&self.iv, seq);
let aad = make_tls13_aad(m.payload.len());
self.decrypt_in_place(
Nonce::<T>::from_slice(&nonce.0),
&aad,
&mut DecryptBufferAdapter(&mut m.payload),
)
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())
}
_ => Err(rustls::Error::DecryptError),
_ => unimplemented!(),
}
}
}
@ -311,10 +278,6 @@ where
let associated_data = header;
let nonce = cipher::Nonce::new(&self.iv, packet_number);
if payload.len() < self.crypter.max_overhead() {
return Err(rustls::Error::DecryptError);
}
let (buffer, tag) = payload.split_at_mut(payload.len() - self.crypter.max_overhead());
self.crypter
@ -327,76 +290,6 @@ where
fn tag_len(&self) -> usize {
<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>);
@ -407,31 +300,17 @@ 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> {
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)
}
}
Box::new(
BoringAeadCrypter::<T>::new(iv, key.as_ref(), ProtocolVersion::TLSv1_3)
.expect("failed to create AEAD crypter"),
)
}
fn decrypter(&self, key: cipher::AeadKey, iv: cipher::Iv) -> Box<dyn cipher::MessageDecrypter> {
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)
}
}
Box::new(
BoringAeadCrypter::<T>::new(iv, key.as_ref(), ProtocolVersion::TLSv1_3)
.expect("failed to create AEAD crypter"),
)
}
fn key_len(&self) -> usize {
@ -445,10 +324,6 @@ 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")]
@ -462,42 +337,24 @@ 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);
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)
}
}
Box::new(
BoringAeadCrypter::<T>::new(Iv::copy(&full_iv), key.as_ref(), ProtocolVersion::TLSv1_2)
.expect("failed to create AEAD crypter"),
)
}
fn decrypter(&self, key: cipher::AeadKey, iv: &[u8]) -> Box<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]);
match BoringAeadCrypter::<T>::new(
Box::new(
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)
}
}
)
.expect("failed to create AEAD crypter"),
)
}
fn key_block_shape(&self) -> cipher::KeyBlockShape {
@ -517,13 +374,7 @@ 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;
if explicit_nonce_len + fixed_iv_len != 12 {
return Err(cipher::UnsupportedOperationError);
}
if iv.len() != fixed_iv_len || explicit.len() != explicit_nonce_len {
return Err(cipher::UnsupportedOperationError);
}
assert_eq!(explicit_nonce_len + fixed_iv_len, 12);
// grab the IV by constructing a nonce, this is just an xor
@ -534,10 +385,6 @@ impl<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> {
@ -553,8 +400,8 @@ impl<T: QuicCipher> QuicHeaderProtector<T> {
first: &mut u8,
packet_number: &mut [u8],
remove: bool,
) -> Result<(), rustls::Error> {
let mask = T::header_protection_mask(self.key.as_ref(), sample)?;
) {
let mask = T::header_protection_mask(self.key.as_ref(), sample);
const LONG_HEADER_FORMAT: u8 = 0x80;
let bits_to_mask = if (*first & LONG_HEADER_FORMAT) == LONG_HEADER_FORMAT {
@ -581,8 +428,6 @@ impl<T: QuicCipher> QuicHeaderProtector<T> {
for (pn_byte, m) in packet_number.iter_mut().zip(&mask[1..]).take(pn_length) {
*pn_byte ^= m;
}
Ok(())
}
}
@ -598,7 +443,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(())
}
@ -613,7 +458,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(())
}
@ -628,13 +473,10 @@ where
T: QuicCipher + BoringAead + 'static,
{
fn packet_key(&self, key: cipher::AeadKey, iv: Iv) -> Box<dyn rustls::quic::PacketKey> {
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))
}
}
Box::new(
BoringAeadCrypter::<T>::new(iv, key.as_ref(), ProtocolVersion::TLSv1_3)
.expect("failed to create AEAD crypter"),
)
}
fn header_protection_key(
@ -650,78 +492,17 @@ where
fn aead_key_len(&self) -> usize {
<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::Tls13AeadAlgorithm;
use rustls::crypto::cipher::{AeadKey, InboundOpaqueMessage, Iv};
#[cfg(feature = "tls12")]
use rustls::crypto::cipher::{MessageDecrypter, Tls12AeadAlgorithm};
use rustls::quic::Algorithm as QuicAlgorithm;
use rustls::quic::HeaderProtectionKey;
use rustls::ContentType;
use rustls::ProtocolVersion;
use rustls::crypto::cipher::{AeadKey, Iv};
use crate::aead::BoringAeadCrypter;
use rustls::quic::PacketKey;
use super::aes::Aes128;
use super::{chacha20::ChaCha20Poly1305, BoringCipher, QuicHeaderProtector};
use super::{chacha20::ChaCha20Poly1305, QuicHeaderProtector};
#[test]
fn quic_header_protection_short() {
@ -738,97 +519,14 @@ mod tests {
phantom: std::marker::PhantomData::<ChaCha20Poly1305>,
};
protector
.rfc9001_header_protection(&sample, &mut first[0], packet_number, false)
.expect("valid sample should protect QUIC header");
protector.rfc9001_header_protection(&sample, &mut first[0], packet_number, false);
assert_eq!(&header[..], &protected_header[..]);
let (first, packet_number) = header.split_at_mut(1);
protector
.rfc9001_header_protection(&sample, &mut first[0], packet_number, true)
.expect("valid sample should unprotect QUIC header");
protector.rfc9001_header_protection(&sample, &mut first[0], packet_number, true);
assert_eq!(&header[..], &unprotected_header[..]);
}
#[test]
fn quic_header_protection_rejects_invalid_sample_without_mutation() {
let hp_key = hex!("25a282b9e82f06f21f488917a4fc8f1b73573685608597d0efcb076b0ab7a7a4");
let sample = hex!("5e5cd55c41f69080575d7999c25a5bfb");
let mut header = hex!("4200bff4");
let original = header;
let (first, packet_number) = header.split_at_mut(1);
let protector = QuicHeaderProtector {
key: AeadKey::from(hp_key),
phantom: std::marker::PhantomData::<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
@ -841,9 +539,9 @@ mod tests {
let unprotected_header = hex!("4200bff4");
let protector = BoringAeadCrypter::<ChaCha20Poly1305>::new(
Iv::from(iv),
Iv::new(iv),
&key,
ProtocolVersion::TLSv1_3,
rustls::ProtocolVersion::TLSv1_3,
)
.unwrap();
@ -862,47 +560,4 @@ mod tests {
assert_eq!(cleartext, expected_cleartext);
}
#[test]
fn quic_decrypt_rejects_truncated_payload() {
let key = [0u8; <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,17 +11,12 @@ impl BoringAead for Aes128 {}
impl BoringCipher for Aes128 {
const EXPLICIT_NONCE_LEN: usize = 8;
#[cfg(feature = "tls12")]
const FIXED_IV_LEN: usize = 4;
const KEY_SIZE: usize = 16;
const TAG_LEN: usize = 16;
const INTEGRITY_LIMIT: u64 = 1 << 52;
const CONFIDENTIALITY_LIMIT: u64 = 1 << 23;
const FIPS_APPROVED: bool = true;
fn new_cipher() -> Algorithm {
Algorithm::aes_128_gcm()
}
@ -41,7 +36,7 @@ impl QuicCipher for Aes128 {
const KEY_SIZE: usize = <Self as BoringCipher>::KEY_SIZE;
const SAMPLE_LEN: usize = 16;
fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> Result<[u8; 5], rustls::Error> {
fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> [u8; 5] {
quic_header_protection_mask::<
{ <Self as QuicCipher>::KEY_SIZE },
{ <Self as QuicCipher>::SAMPLE_LEN },
@ -57,17 +52,12 @@ impl BoringAead for Aes256 {}
impl BoringCipher for Aes256 {
const EXPLICIT_NONCE_LEN: usize = 8;
#[cfg(feature = "tls12")]
const FIXED_IV_LEN: usize = 4;
const KEY_SIZE: usize = 32;
const TAG_LEN: usize = 16;
const INTEGRITY_LIMIT: u64 = 1 << 52;
const CONFIDENTIALITY_LIMIT: u64 = 1 << 23;
const FIPS_APPROVED: bool = true;
fn new_cipher() -> Algorithm {
Algorithm::aes_256_gcm()
}
@ -87,7 +77,7 @@ impl QuicCipher for Aes256 {
const KEY_SIZE: usize = <Self as BoringCipher>::KEY_SIZE;
const SAMPLE_LEN: usize = 16;
fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> Result<[u8; 5], rustls::Error> {
fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> [u8; 5] {
quic_header_protection_mask::<
{ <Self as QuicCipher>::KEY_SIZE },
{ <Self as QuicCipher>::SAMPLE_LEN },
@ -99,32 +89,19 @@ fn quic_header_protection_mask<const KEY_SIZE: usize, const SAMPLE_LEN: usize>(
cipher: boring::symm::Cipher,
hp_key: &[u8],
sample: &[u8],
) -> Result<[u8; 5], rustls::Error> {
if hp_key.len() != KEY_SIZE {
return Err(rustls::Error::General(
"header protection key of invalid length".into(),
));
}
if sample.len() != SAMPLE_LEN {
return Err(rustls::Error::General("sample of invalid length".into()));
}
) -> [u8; 5] {
assert!(hp_key.len() == KEY_SIZE);
assert!(sample.len() >= SAMPLE_LEN);
let mut output = [0u8; SAMPLE_LEN];
let mut crypter = boring::symm::Crypter::new(cipher, boring::symm::Mode::Encrypt, hp_key, None)
.map_err(|_| rustls::Error::General("failed generating header protection mask".into()))?;
.expect("failed getting crypter");
let len = crypter
.update(sample, &mut output)
.map_err(|_| rustls::Error::General("failed generating header protection mask".into()))?;
let _total = len
+ crypter.finalize(&mut output[len..]).map_err(|_| {
rustls::Error::General("failed generating header protection mask".into())
})?;
let len = crypter.update(sample, &mut output).unwrap();
let _ = len + crypter.finalize(&mut output[len..]).unwrap();
let mut mask = [0u8; 5];
mask.copy_from_slice(&output[..5]);
Ok(mask)
output[..5].try_into().unwrap()
}
#[cfg(test)]

View file

@ -14,17 +14,12 @@ impl BoringAead for ChaCha20Poly1305 {}
impl BoringCipher for ChaCha20Poly1305 {
const EXPLICIT_NONCE_LEN: usize = 0;
#[cfg(feature = "tls12")]
const FIXED_IV_LEN: usize = 12;
const KEY_SIZE: usize = 32;
const TAG_LEN: usize = 16;
const INTEGRITY_LIMIT: u64 = 1 << 36;
const CONFIDENTIALITY_LIMIT: u64 = u64::MAX;
const FIPS_APPROVED: bool = false;
fn new_cipher() -> Algorithm {
Algorithm::chacha20_poly1305()
}
@ -38,21 +33,13 @@ impl QuicCipher for ChaCha20Poly1305 {
const KEY_SIZE: usize = 32;
const SAMPLE_LEN: usize = 16;
fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> Result<[u8; 5], rustls::Error> {
if hp_key.len() != <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()));
}
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);
let mut mask = [0u8; 5];
// RFC9001 5.4.4: The first 4 bytes of the sampled ciphertext are the block counter. A ChaCha20 implementation could take a 32-bit integer in place of a byte sequence, in which case, the byte sequence is interpreted as a little-endian value.
let mut counter_bytes = [0u8; 4];
counter_bytes.copy_from_slice(&sample[0..4]);
let counter = u32::from_le_bytes(counter_bytes);
let counter = u32::from_le_bytes(sample[0..4].try_into().unwrap());
// RFC9001 5.4.4: The remaining 12 bytes are used as the nonce.
let nonce = &sample[4..16];
unsafe {
@ -65,7 +52,7 @@ impl QuicCipher for ChaCha20Poly1305 {
counter,
);
};
Ok(mask)
mask
}
}
@ -104,8 +91,7 @@ mod tests {
let sample = hex!("5e5cd55c41f69080575d7999c25a5bfb");
let hp_key = hex!("25a282b9e82f06f21f488917a4fc8f1b73573685608597d0efcb076b0ab7a7a4");
let expected_mask = hex!("aefefe7d03");
let mask = ChaCha20Poly1305::header_protection_mask(&hp_key, &sample)
.expect("valid QUIC sample/key should produce a mask");
let mask = ChaCha20Poly1305::header_protection_mask(&hp_key, &sample);
assert_eq!(mask, expected_mask);
}
}

View file

@ -24,14 +24,10 @@ impl hash::Hash for Hash {
boring::nid::Nid::SHA256 => hash::HashAlgorithm::SHA256,
boring::nid::Nid::SHA384 => hash::HashAlgorithm::SHA384,
boring::nid::Nid::SHA512 => hash::HashAlgorithm::SHA512,
_ => unreachable!("hash::Hash is only instantiated with SHA-2 digests"),
_ => unimplemented!(),
}
}
fn fips(&self) -> bool {
cfg!(feature = "fips")
}
fn output_len(&self) -> usize {
MessageDigest::from_nid(self.0)
.expect("failed getting digest")

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,7 +2,6 @@ use std::marker::PhantomData;
use boring::hash::MessageDigest;
use rustls::crypto::tls13::{self, Hkdf as RustlsHkdf};
use zeroize::Zeroizing;
use crate::helper::{cvt, cvt_p};
@ -68,7 +67,7 @@ impl<T: BoringHash> RustlsHkdf for Hkdf<T> {
let digest = T::new_hash();
let hash_size = digest.size();
let mut prk = Zeroizing::new([0u8; boring_sys::EVP_MAX_MD_SIZE as usize]);
let mut prk = [0u8; boring_sys::EVP_MAX_MD_SIZE as usize];
let mut prk_len = 0;
// if salt isn't set we usen these bytes here as salt
@ -104,7 +103,7 @@ impl<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 = Zeroizing::new([0u8; boring_sys::EVP_MAX_MD_SIZE as usize]);
let mut prk = [0u8; boring_sys::EVP_MAX_MD_SIZE as usize];
let prk_len = okm.len();
prk[..prk_len].copy_from_slice(okm);
@ -122,7 +121,7 @@ impl<T: BoringHash> RustlsHkdf for Hkdf<T> {
message: &[u8],
) -> rustls::crypto::hmac::Tag {
let digest = T::new_hash();
let mut hash = Zeroizing::new([0u8; boring_sys::EVP_MAX_MD_SIZE as usize]);
let mut hash = [0u8; boring_sys::EVP_MAX_MD_SIZE as usize];
let mut hash_len = 0u32;
unsafe {
cvt_p(boring_sys::HMAC(
@ -138,14 +137,10 @@ impl<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: Zeroizing<[u8; boring_sys::EVP_MAX_MD_SIZE as usize]>,
prk: [u8; boring_sys::EVP_MAX_MD_SIZE as usize],
prk_len: usize,
digest: MessageDigest,
}
@ -167,17 +162,9 @@ impl tls13::HkdfExpander for HkdfExpander {
info: &[&[u8]],
output: &mut [u8],
) -> Result<(), tls13::OutputLengthError> {
let max_output_len = self
.hash_len()
.checked_mul(255)
.ok_or(tls13::OutputLengthError)?;
if output.len() > max_output_len {
return Err(tls13::OutputLengthError);
}
let info_concat = info.concat();
unsafe {
cvt(boring_sys::HKDF_expand(
boring_sys::HKDF_expand(
output.as_mut_ptr(),
output.len(),
self.digest.as_ptr(),
@ -185,9 +172,8 @@ impl tls13::HkdfExpander for HkdfExpander {
self.prk_len,
info_concat.as_ptr(),
info_concat.len(),
))
.map_err(|_| tls13::OutputLengthError)?;
}
);
};
Ok(())
}
@ -201,7 +187,7 @@ impl tls13::HkdfExpander for HkdfExpander {
/// This is infallible, because by definition `OkmBlock` is always exactly
/// `HashLen` bytes long.
fn expand_block(&self, info: &[&[u8]]) -> tls13::OkmBlock {
let mut output = Zeroizing::new([0u8; boring_sys::EVP_MAX_MD_SIZE as usize]);
let mut output = [0u8; boring_sys::EVP_MAX_MD_SIZE as usize];
let output_len = self.hash_len();
self.expand_slice(info, &mut output[..output_len])
@ -214,35 +200,3 @@ impl tls13::HkdfExpander for HkdfExpander {
self.digest.size()
}
}
#[cfg(test)]
mod tests {
use boring::hash::MessageDigest;
use rustls::crypto::tls13::Hkdf as _;
use super::{Hkdf, Sha256};
#[test]
fn expand_slice_rejects_output_larger_than_rfc_limit() {
let hkdf = Hkdf::<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,7 +4,6 @@ use boring::hash::MessageDigest;
use boring_additions::hmac::HmacCtx;
use foreign_types::ForeignType;
use rustls::crypto;
use zeroize::Zeroizing;
use crate::helper::{cvt, cvt_p};
@ -20,11 +19,18 @@ struct BoringHmac(pub boring::nid::Nid);
impl crypto::hmac::Hmac for BoringHmac {
fn with_key(&self, key: &[u8]) -> Box<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: Zeroizing::new(key.to_vec()),
key: key.to_vec(),
})
}
@ -33,35 +39,34 @@ impl crypto::hmac::Hmac for BoringHmac {
.expect("failed getting digest")
.size()
}
fn fips(&self) -> bool {
cfg!(feature = "fips")
}
}
#[derive(Clone)]
struct BoringHmacKey {
ctx: HmacCtx,
md: MessageDigest,
key: Zeroizing<Vec<u8>>,
key: Vec<u8>,
}
impl BoringHmacKey {
fn init(ctx: &HmacCtx, key: &[u8], md: MessageDigest) {
fn init(&self) {
unsafe {
// initialize a new hmac
cvt(boring_sys::HMAC_Init_ex(
ctx.as_ptr(),
key.as_ptr() as *const c_void,
key.len(),
md.as_ptr(),
self.ctx.as_ptr(),
self.key.as_ptr() as *const c_void,
self.key.len(),
self.md.as_ptr(),
ptr::null_mut(),
))
}
.expect("failed initializing hmac");
}
fn update(ctx: &HmacCtx, bytes: &[u8]) {
fn update(&self, bytes: &[u8]) {
unsafe {
cvt(boring_sys::HMAC_Update(
ctx.as_ptr(),
self.ctx.as_ptr(),
bytes.as_ptr(),
bytes.len(),
))
@ -69,11 +74,11 @@ impl BoringHmacKey {
.expect("failed updating hmac");
}
fn finish(ctx: &HmacCtx, out: &mut [u8]) -> usize {
fn finish(&self, out: &mut [u8]) -> usize {
let mut out_len = 0;
unsafe {
cvt(boring_sys::HMAC_Final(
ctx.as_ptr(),
self.ctx.as_ptr(),
out.as_mut_ptr(),
&mut out_len,
))
@ -81,28 +86,21 @@ impl BoringHmacKey {
.expect("failed hmac final");
out_len as usize
}
fn new_ctx() -> HmacCtx {
unsafe {
HmacCtx::from_ptr(cvt_p(boring_sys::HMAC_CTX_new()).expect("failed creating HMAC_CTX"))
}
}
}
impl crypto::hmac::Key for BoringHmacKey {
fn sign_concat(&self, first: &[u8], middle: &[&[u8]], last: &[u8]) -> crypto::hmac::Tag {
let ctx = Self::new_ctx();
Self::init(&ctx, self.key.as_slice(), self.md);
self.init();
Self::update(&ctx, first);
self.update(first);
for m in middle {
Self::update(&ctx, m);
self.update(m);
}
Self::update(&ctx, last);
self.update(last);
let mut out = Zeroizing::new([0u8; boring_sys::EVP_MAX_MD_SIZE as usize]);
let out_len = Self::finish(&ctx, &mut out[..]);
let mut out = [0u8; 32];
let out_len = self.finish(&mut out);
crypto::hmac::Tag::new(&out[..out_len])
}
@ -114,7 +112,7 @@ impl crypto::hmac::Key for BoringHmacKey {
#[cfg(test)]
mod tests {
use super::{SHA256, SHA384};
use super::SHA256;
use hex_literal::hex;
#[test]
@ -143,23 +141,4 @@ mod tests {
hex!("11fa4a6ee97bebfad9e1087145c556fec9a786cad0659aa10702d21bd2968305")
);
}
#[test]
fn test_sha384_hmac_len() {
let hasher = SHA384.with_key("Very Secret".as_bytes());
let tag = hasher.sign_concat(
&[],
&[
"yay".as_bytes(),
"this".as_bytes(),
"works".as_bytes(),
"well".as_bytes(),
],
&[],
);
assert_eq!(tag.as_ref().len(), hasher.tag_len());
assert_eq!(tag.as_ref().len(), 48);
}
}

View file

@ -0,0 +1,108 @@
use rustls::crypto::{self, ActiveKeyExchange};
use crate::helper::log_and_map;
mod dh;
mod ex;
enum DhKeyType {
EC((boring::ec::EcGroup, i32)),
ED(i32),
FFDHE2048,
}
/// A X25519-based key exchange
#[derive(Debug)]
pub struct X25519;
impl crypto::SupportedKxGroup for X25519 {
fn start(&self) -> Result<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

@ -0,0 +1,123 @@
use boring::{dh::Dh, error::ErrorStack, pkey::Private};
use foreign_types::ForeignType;
use rustls::crypto;
use crate::helper::{cvt, cvt_p, log_and_map};
use super::DhKeyType;
/// This type can be used to perform a
/// Diffie-Hellman key exchange.
pub struct KeyExchange {
dh: Dh<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,22 +1,20 @@
#[cfg(not(feature = "fips"))]
use std::{mem::MaybeUninit, ptr};
use std::{
mem::MaybeUninit,
ptr::{self},
};
use boring::{
ec::{EcGroup, EcKey},
error::ErrorStack,
nid::Nid,
pkey::Id,
pkey::{PKey, PKeyRef, Private},
};
#[cfg(not(feature = "fips"))]
use boring_additions::evp::EvpPkeyCtx;
#[cfg(not(feature = "fips"))]
use foreign_types::ForeignType;
use rustls::crypto;
use spki::der::Decode;
use crate::helper::log_and_map;
#[cfg(not(feature = "fips"))]
use crate::helper::{cvt, cvt_p};
use crate::helper::{cvt, cvt_p, log_and_map};
use super::DhKeyType;
@ -32,11 +30,16 @@ pub struct KeyExchange {
impl KeyExchange {
/// Creates a new `KeyExchange` using a random
/// private key for the `X25519` Edwards curve
#[cfg(not(feature = "fips"))]
pub fn with_x25519() -> Result<Self, 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`
@ -50,6 +53,12 @@ impl KeyExchange {
Self::ec_from_curve(Nid::SECP384R1)
}
/// Creates a new `KeyExchange` using a random
/// private key for `sep521r1` curve
pub fn with_secp521r1() -> Result<Self, 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> {
@ -67,7 +76,6 @@ impl KeyExchange {
/// Allows getting a new `KeyExchange` using Edwards Curves
/// on the specified curve
#[cfg(not(feature = "fips"))]
fn ed_from_curve(nid: Nid) -> Result<Self, ErrorStack> {
let pkey_ctx = unsafe {
EvpPkeyCtx::from_ptr(cvt_p(boring_sys::EVP_PKEY_CTX_new_id(
@ -97,24 +105,18 @@ impl KeyExchange {
/// Decodes a SPKI public key to it's raw public key component
fn raw_public_key(pkey: &PKeyRef<Private>) -> Result<Vec<u8>, ErrorStack> {
if pkey.id() == Id::EC {
let ec_key = pkey.ec_key()?;
let mut bn_ctx = boring::bn::BigNumContext::new()?;
let spki = pkey.public_key_to_der()?;
return ec_key.public_key().to_bytes(
ec_key.group(),
boring::ec::PointConversionForm::UNCOMPRESSED,
&mut bn_ctx,
);
}
// parse the key
let pkey = spki::SubjectPublicKeyInfoRef::from_der(spki.as_ref())
.expect("failed parsing spki bytes");
let mut output = vec![0u8; pkey.raw_public_key_len()?];
let used_len = {
let used = pkey.raw_public_key(&mut output)?;
used.len()
};
output.truncate(used_len);
Ok(output)
// return the raw public key as a new vec
Ok(Vec::from(
pkey.subject_public_key
.as_bytes()
.expect("failed getting raw spki bytes"),
))
}
/// Derives a shared secret using the peer's raw public key
@ -127,8 +129,8 @@ impl KeyExchange {
crate::verify::ec::create_public_key(group, point.as_ref())?
}
#[cfg(not(feature = "fips"))]
DhKeyType::ED(nid) => crate::verify::ed::public_key(peer_pub_key, Nid::from_raw(*nid))?,
_ => unimplemented!(),
};
let mut deriver = boring::derive::Deriver::new(&self.own_key)?;
@ -145,7 +147,7 @@ impl crypto::ActiveKeyExchange for KeyExchange {
peer_pub_key: &[u8],
) -> Result<crypto::SharedSecret, rustls::Error> {
self.diffie_hellman(peer_pub_key)
.map(crypto::SharedSecret::from)
.map(|x| crypto::SharedSecret::from(x.as_slice()))
.map_err(|e| {
log_and_map(
"ex::KeyExchange::diffie_hellman",
@ -161,11 +163,12 @@ impl crypto::ActiveKeyExchange for KeyExchange {
fn group(&self) -> rustls::NamedGroup {
match self.key_type {
#[cfg(not(feature = "fips"))]
DhKeyType::ED(boring_sys::NID_X25519) => rustls::NamedGroup::X25519,
DhKeyType::ED(boring_sys::NID_X448) => rustls::NamedGroup::X448,
DhKeyType::EC((_, boring_sys::NID_X9_62_prime256v1)) => rustls::NamedGroup::secp256r1,
DhKeyType::EC((_, boring_sys::NID_secp384r1)) => rustls::NamedGroup::secp384r1,
_ => unreachable!("unsupported key type"),
DhKeyType::EC((_, boring_sys::NID_secp521r1)) => rustls::NamedGroup::secp521r1,
_ => unimplemented!(),
}
}
}
@ -187,7 +190,6 @@ mod tests {
}
#[test]
#[cfg(not(feature = "fips"))]
fn test_derive_ed() {
let alice = Box::new(KeyExchange::with_x25519().unwrap());
let bob = KeyExchange::with_x25519().unwrap();

View file

@ -1,79 +0,0 @@
use rustls::crypto::{self, ActiveKeyExchange};
use crate::helper::log_and_map;
mod ex;
#[cfg(feature = "mlkem")]
mod pq;
#[cfg(feature = "mlkem")]
pub use pq::X25519MlKem768;
/// Key type discriminant used by [`ex::KeyExchange`] to select the
/// appropriate peer key parsing and DH derivation logic.
enum DhKeyType {
EC((boring::ec::EcGroup, i32)),
#[cfg(not(feature = "fips"))]
ED(i32),
}
/// A X25519-based key exchange
#[cfg(not(feature = "fips"))]
#[derive(Debug)]
pub struct X25519;
#[cfg(not(feature = "fips"))]
impl crypto::SupportedKxGroup for X25519 {
fn start(&self) -> Result<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

@ -1,364 +0,0 @@
//! X25519MLKEM768 post-quantum hybrid key exchange.
//!
//! Implements the X25519MLKEM768 hybrid key agreement per
//! `draft-ietf-tls-ecdhe-mlkem-00`. Composes ML-KEM-768 (FIPS 203)
//! with X25519, with the ML-KEM component first in all wire encodings.
//!
//! Wire format:
//! - Client key share: `mlkem_pk(1184) || x25519_pk(32)` = 1216 bytes
//! - Server key share: `mlkem_ct(1088) || x25519_pk(32)` = 1120 bytes
//! - Shared secret: `mlkem_ss(32) || x25519_ss(32)` = 64 bytes
use boring::mlkem::{Algorithm, MlKemPrivateKey, MlKemPublicKey};
use rustls::crypto::{self, CompletedKeyExchange, SharedSecret};
use rustls::{Error, NamedGroup, ProtocolVersion};
use zeroize::Zeroizing;
const MLKEM768_PUBLIC_KEY_BYTES: usize = 1184;
const MLKEM768_CIPHERTEXT_BYTES: usize = 1088;
const X25519_PUBLIC_KEY_BYTES: usize = 32;
const X25519_PRIVATE_KEY_BYTES: usize = 32;
const X25519_SHARED_SECRET_BYTES: usize = 32;
const CLIENT_SHARE_LEN: usize = MLKEM768_PUBLIC_KEY_BYTES + X25519_PUBLIC_KEY_BYTES; // 1216
const SERVER_SHARE_LEN: usize = MLKEM768_CIPHERTEXT_BYTES + X25519_PUBLIC_KEY_BYTES; // 1120
/// X25519MLKEM768 post-quantum hybrid key exchange group.
#[derive(Debug)]
pub struct X25519MlKem768;
impl crypto::SupportedKxGroup for X25519MlKem768 {
/// Client-side: generate ML-KEM-768 + X25519 keypairs.
fn start(&self) -> Result<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