Compare commits
10 commits
107b463b84
...
b667e1450b
| Author | SHA1 | Date | |
|---|---|---|---|
| b667e1450b | |||
|
|
b88c87235d | ||
|
|
bbd0ccf0b8 | ||
|
|
c71d5bbfd8 | ||
|
|
fafbf296b1 | ||
|
|
271acbb315 | ||
|
|
490340afa7 | ||
|
|
aa6e1c36f8 | ||
|
|
03b48134ca | ||
|
|
b1188e2ef1 |
34 changed files with 2481 additions and 759 deletions
101
.github/workflows/ci.yml
vendored
101
.github/workflows/ci.yml
vendored
|
|
@ -9,10 +9,9 @@ on:
|
|||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTFLAGS: -Dwarnings
|
||||
FEATURES: "logging,tls12"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
fmt:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
|
@ -21,9 +20,95 @@ jobs:
|
|||
run: sudo apt-get install -y cmake clang
|
||||
- name: Check fmt
|
||||
run: make fmt
|
||||
- name: Lint
|
||||
run: make lint
|
||||
- name: Tests usual
|
||||
run: make test
|
||||
- name: Build usual
|
||||
run: make build
|
||||
|
||||
test-default:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install dependencies
|
||||
run: sudo apt-get install -y cmake clang
|
||||
- name: Lint (default features)
|
||||
run: make lint FEATURES=""
|
||||
- name: Test (default features)
|
||||
run: make test FEATURES=""
|
||||
- name: Build (default features)
|
||||
run: make build FEATURES=""
|
||||
|
||||
test-tls12:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install dependencies
|
||||
run: sudo apt-get install -y cmake clang
|
||||
- name: Lint (tls12)
|
||||
run: make lint FEATURES="tls12"
|
||||
- name: Test (tls12)
|
||||
run: make test FEATURES="tls12"
|
||||
- name: Build (tls12)
|
||||
run: make build FEATURES="tls12"
|
||||
|
||||
test-logging-tls12:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install dependencies
|
||||
run: sudo apt-get install -y cmake clang
|
||||
- name: Lint (logging,tls12)
|
||||
run: make lint FEATURES="logging,tls12"
|
||||
- name: Test (logging,tls12)
|
||||
run: make test FEATURES="logging,tls12"
|
||||
- name: Build (logging,tls12)
|
||||
run: make build FEATURES="logging,tls12"
|
||||
|
||||
test-mlkem:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install dependencies
|
||||
run: sudo apt-get install -y cmake clang
|
||||
- name: Lint (mlkem)
|
||||
run: make lint FEATURES="mlkem"
|
||||
- name: Test (mlkem)
|
||||
run: make test FEATURES="mlkem"
|
||||
- name: Build (mlkem)
|
||||
run: make build FEATURES="mlkem"
|
||||
|
||||
test-mlkem-tls12:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install dependencies
|
||||
run: sudo apt-get install -y cmake clang
|
||||
- name: Lint (mlkem,tls12)
|
||||
run: make lint FEATURES="mlkem,tls12"
|
||||
- name: Test (mlkem,tls12)
|
||||
run: make test FEATURES="mlkem,tls12"
|
||||
- name: Build (mlkem,tls12)
|
||||
run: make build FEATURES="mlkem,tls12"
|
||||
|
||||
test-fips:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install dependencies
|
||||
run: sudo apt-get install -y cmake clang
|
||||
- name: Test (fips)
|
||||
run: cargo test -p boring-rustls-provider --all-targets --features fips
|
||||
|
||||
check-fips:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install dependencies
|
||||
run: sudo apt-get install -y cmake clang
|
||||
- name: Check (fips)
|
||||
run: cargo check -p boring-rustls-provider --all-targets --features fips
|
||||
- name: Check (fips-precompiled)
|
||||
run: cargo check -p boring-rustls-provider --all-targets --features fips-precompiled
|
||||
|
|
|
|||
20
Cargo.toml
20
Cargo.toml
|
|
@ -2,10 +2,10 @@
|
|||
members = [
|
||||
# things that should probably be in boring crate
|
||||
"boring-additions",
|
||||
# things that should probably be in boring-sys crate
|
||||
"boring-sys-additions",
|
||||
# the main library and tests
|
||||
"boring-rustls-provider",
|
||||
# things that should probably be in boring-sys crate
|
||||
"boring-sys-additions",
|
||||
# tests and example code
|
||||
"examples",
|
||||
]
|
||||
|
|
@ -17,11 +17,11 @@ default-members = [
|
|||
resolver = "2"
|
||||
|
||||
[workspace.dependencies]
|
||||
boring = { version = "4", default-features = false }
|
||||
boring-sys = { version = "4", default-features = false }
|
||||
rustls = { version = "=0.22.0-alpha.6", default-features = false }
|
||||
rustls-pemfile = { version = "=2.0.0-alpha.2" }
|
||||
rustls-pki-types = { version = "0.2.3" }
|
||||
tokio-rustls = { version = "0.25.0-alpha.4" }
|
||||
webpki = { package = "rustls-webpki", version = "0.102.0-alpha.7", default-features = false }
|
||||
webpki-roots = { version = "=0.26.0-alpha.2" }
|
||||
boring = { version = "5", default-features = false }
|
||||
boring-sys = { version = "5", default-features = false }
|
||||
rustls = { version = "0.23", default-features = false }
|
||||
rustls-pemfile = { version = "2" }
|
||||
rustls-pki-types = { version = "1" }
|
||||
tokio-rustls = { version = "0.26", default-features = false }
|
||||
webpki = { package = "rustls-webpki", version = "0.103", default-features = false }
|
||||
webpki-roots = { version = "1.0" }
|
||||
|
|
|
|||
11
Makefile
11
Makefile
|
|
@ -1,4 +1,5 @@
|
|||
FEATURES ?= logging,tls12
|
||||
CARGO_FEATURES := $(if $(strip $(FEATURES)),-F "$(FEATURES)",)
|
||||
|
||||
|
||||
.PHONY: fmt
|
||||
|
|
@ -7,12 +8,16 @@ fmt:
|
|||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
cargo clippy --workspace --all-targets -F "$(FEATURES)"
|
||||
cargo clippy --workspace --all-targets $(CARGO_FEATURES)
|
||||
|
||||
.PHONY: check
|
||||
check:
|
||||
cargo check --workspace --all-targets $(CARGO_FEATURES)
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
cargo test --all-targets -F "$(FEATURES)"
|
||||
cargo test --all-targets $(CARGO_FEATURES)
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
cargo build --all-targets -F "$(FEATURES)"
|
||||
cargo build --all-targets $(CARGO_FEATURES)
|
||||
|
|
|
|||
95
Readme.md
95
Readme.md
|
|
@ -2,55 +2,59 @@
|
|||
|
||||
[](https://github.com/janrueth/boring-rustls-provider/actions/workflows/ci.yml?query=branch%3Amain)
|
||||
|
||||
This is supposed to be the start to a [boringssl](https://github.com/cloudflare/boring)-based [rustls](https://github.com/rustls/rustls) crypto provider.
|
||||
A [BoringSSL](https://github.com/cloudflare/boring)-based [rustls](https://github.com/rustls/rustls) crypto provider.
|
||||
|
||||
## Status
|
||||
This is just a dump of me figuring out how to interface with boring and rustls.
|
||||
It works to establish a connection and exchange data but I haven't written real tests yet, nor did I cleanup the code or made the effort to make it look nice.
|
||||
There is probably some code in here that should rather live in the `boring` crate.
|
||||
Built on `boring` v5 and `rustls` 0.23.
|
||||
|
||||
Further, the rustls crypto provider API is still not stable it seems. This works currently with `rustls = 0.22.0-alpha.5`.
|
||||
## Features
|
||||
|
||||
### Supported ciphers
|
||||
Currently, supports only TLS 1.3:
|
||||
No features are enabled by default. The provider ships with TLS 1.3 support
|
||||
out of the box; additional capabilities are opt-in.
|
||||
|
||||
| Feature | Description |
|
||||
|---|---|
|
||||
| `fips` | Build against FIPS-validated BoringSSL and restrict the provider to FIPS-approved algorithms only (SP 800-52r2). Implies `mlkem`. See [FIPS mode](#fips-mode) below. |
|
||||
| `fips-precompiled` | Deprecated alias for `fips`. Matches the `boring` crate's feature name. |
|
||||
| `mlkem` | Enable the X25519MLKEM768 post-quantum hybrid key exchange group (`draft-ietf-tls-ecdhe-mlkem-00`). Uses ML-KEM-768 (FIPS 203) combined with X25519. See [Post-quantum key exchange](#post-quantum-key-exchange). |
|
||||
| `tls12` | Enable TLS 1.2 cipher suites (`ECDHE-ECDSA` and `ECDHE-RSA` with AES-GCM and ChaCha20-Poly1305). Without this only TLS 1.3 is available. |
|
||||
| `logging` | Enable debug logging of BoringSSL errors and provider internals via the `log` crate. |
|
||||
|
||||
## Supported Algorithms
|
||||
|
||||
### Cipher Suites
|
||||
|
||||
TLS 1.3 (always available):
|
||||
```
|
||||
AES_128_GCM_SHA256
|
||||
AES_256_GCM_SHA384
|
||||
CHACHA20_POLY1305_SHA256
|
||||
```
|
||||
|
||||
QUIC: not yet supported
|
||||
|
||||
TLS 1.2:
|
||||
TLS 1.2 (requires `tls12` feature):
|
||||
```
|
||||
ECDHE_ECDSA_AES128_GCM_SHA256
|
||||
ECDHE_RSA_AES128_GCM_SHA256
|
||||
|
||||
ECDHE_ECDSA_AES256_GCM_SHA384
|
||||
ECDHE_RSA_AES256_GCM_SHA384
|
||||
|
||||
ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
|
||||
ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
|
||||
```
|
||||
|
||||
### Key Exchange Algorithms
|
||||
### Key Exchange Groups
|
||||
|
||||
Matches boring's default supported group list:
|
||||
|
||||
`ECDHE` with curves:
|
||||
```
|
||||
X25519MLKEM768 (0x11ec, requires mlkem feature, TLS 1.3 only)
|
||||
X25519
|
||||
X448
|
||||
secp256r1
|
||||
secp384r1
|
||||
secp521r1
|
||||
secp256r1 (P-256)
|
||||
secp384r1 (P-384)
|
||||
```
|
||||
|
||||
When `mlkem` is enabled, X25519MLKEM768 is the preferred (first) group in both
|
||||
FIPS and non-FIPS configurations.
|
||||
|
||||
`FFDHE` with:
|
||||
```
|
||||
ffdhe2048
|
||||
```
|
||||
|
||||
### Signature Generation / Verification
|
||||
### Signature Algorithms
|
||||
|
||||
```
|
||||
RSA_PKCS1_SHA256
|
||||
|
|
@ -66,6 +70,47 @@ ED25519
|
|||
ED448
|
||||
```
|
||||
|
||||
## Post-Quantum Key Exchange
|
||||
|
||||
The `mlkem` feature enables the **X25519MLKEM768** hybrid key exchange group
|
||||
per `draft-ietf-tls-ecdhe-mlkem-00`. This combines classical X25519
|
||||
Diffie-Hellman with ML-KEM-768 (FIPS 203) post-quantum key encapsulation,
|
||||
ensuring that connections are secure against both classical and quantum
|
||||
adversaries.
|
||||
|
||||
The `fips` feature implies `mlkem`, so X25519MLKEM768 is always available
|
||||
in FIPS mode.
|
||||
|
||||
Wire format (ML-KEM component first in all encodings):
|
||||
- Client key share: `mlkem_pk(1184) || x25519_pk(32)` = 1216 bytes
|
||||
- Server key share: `mlkem_ct(1088) || x25519_pk(32)` = 1120 bytes
|
||||
- Shared secret: `mlkem_ss(32) || x25519_ss(32)` = 64 bytes
|
||||
|
||||
Interoperability has been verified against Cloudflare's PQ endpoints
|
||||
(`pq.cloudflareresearch.com`).
|
||||
|
||||
## FIPS Mode
|
||||
|
||||
When the `fips` feature is enabled the provider builds against a FIPS-validated
|
||||
version of BoringSSL and restricts all algorithm selections to those approved
|
||||
under [SP 800-52r2](https://doi.org/10.6028/NIST.SP.800-52r2), aligned with
|
||||
boring's `fips202205` compliance policy:
|
||||
|
||||
- **Cipher suites**: AES-GCM only (no ChaCha20-Poly1305).
|
||||
- **Key exchange groups**: X25519MLKEM768 (preferred), P-256, and P-384 only
|
||||
(no standalone X25519).
|
||||
- **Signature algorithms**: RSA PKCS#1 / PSS and ECDSA with P-256 or P-384 only
|
||||
(no P-521, Ed25519, or Ed448).
|
||||
|
||||
## Workspace Structure
|
||||
|
||||
| Crate | Purpose |
|
||||
|---|---|
|
||||
| `boring-rustls-provider` | The main rustls crypto provider. |
|
||||
| `boring-additions` | Safe Rust wrappers for BoringSSL APIs not yet exposed by the `boring` crate (AEAD, EVP_PKEY_CTX, HMAC_CTX). Intended for upstreaming. |
|
||||
| `boring-sys-additions` | Raw FFI binding for `CRYPTO_tls1_prf` (internal BoringSSL symbol used for FIPS-compliant TLS 1.2 PRF). Intended for upstreaming. |
|
||||
| `examples` | Example client binary. |
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ description = "Boring additions"
|
|||
publish = false
|
||||
|
||||
[dependencies]
|
||||
aead = { version = "0.5", default_features = false, features = ["alloc"] }
|
||||
boring = { workspace = true }
|
||||
boring-sys = { workspace = true }
|
||||
foreign-types = "0.5"
|
||||
|
|
|
|||
|
|
@ -78,11 +78,7 @@ impl Crypter {
|
|||
///
|
||||
/// # Errors
|
||||
/// Returns the `BoringSSL` error in case of an internal error
|
||||
///
|
||||
/// # Panics
|
||||
/// * If the key length mismatches the `aead_alg` required key length
|
||||
pub fn new(aead_alg: &Algorithm, key: &[u8]) -> Result<Self, ErrorStack> {
|
||||
assert_eq!(aead_alg.key_length(), key.len());
|
||||
boring_sys::init();
|
||||
|
||||
let this = unsafe {
|
||||
|
|
@ -116,10 +112,6 @@ impl Crypter {
|
|||
///
|
||||
/// # Errors
|
||||
/// In case of an error, returns the `BoringSSL` error
|
||||
///
|
||||
/// # Panics
|
||||
/// * If the `nonce` is not the expected lenght
|
||||
/// * If the `tag` has not enough space
|
||||
pub fn seal_in_place(
|
||||
&self,
|
||||
nonce: &[u8],
|
||||
|
|
@ -127,8 +119,9 @@ impl Crypter {
|
|||
buffer: &mut [u8],
|
||||
tag: &mut [u8],
|
||||
) -> Result<usize, ErrorStack> {
|
||||
assert!(tag.len() >= self.max_overhead);
|
||||
assert_eq!(nonce.len(), self.nonce_len);
|
||||
if tag.len() < self.max_overhead || nonce.len() != self.nonce_len {
|
||||
return Err(ErrorStack::get());
|
||||
}
|
||||
|
||||
let mut tag_len = tag.len();
|
||||
unsafe {
|
||||
|
|
@ -156,9 +149,6 @@ impl Crypter {
|
|||
///
|
||||
/// # Errors
|
||||
/// In case of an error, returns the `BoringSSL` error
|
||||
///
|
||||
/// # Panics
|
||||
/// * if the nonce has the wrong lenght
|
||||
pub fn open_in_place(
|
||||
&self,
|
||||
nonce: &[u8],
|
||||
|
|
@ -166,7 +156,9 @@ impl Crypter {
|
|||
buffer: &mut [u8],
|
||||
tag: &[u8],
|
||||
) -> Result<(), ErrorStack> {
|
||||
assert_eq!(nonce.len(), self.nonce_len);
|
||||
if nonce.len() != self.nonce_len {
|
||||
return Err(ErrorStack::get());
|
||||
}
|
||||
|
||||
unsafe {
|
||||
cvt(boring_sys::EVP_AEAD_CTX_open_gather(
|
||||
|
|
@ -188,11 +180,11 @@ impl Crypter {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Crypter;
|
||||
use super::{Algorithm, Crypter};
|
||||
|
||||
#[test]
|
||||
fn in_out() {
|
||||
let key = Crypter::new(&super::Algorithm::aes_128_gcm(), &[0u8; 16]).unwrap();
|
||||
let key = Crypter::new(&Algorithm::aes_128_gcm(), &[0u8; 16]).unwrap();
|
||||
let nonce = [0u8; 12];
|
||||
let associated_data = b"this is authenticated";
|
||||
let mut buffer = Vec::with_capacity(26);
|
||||
|
|
@ -207,4 +199,35 @@ mod tests {
|
|||
|
||||
assert_eq!(b"ABCDE", buffer.as_slice());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_rejects_invalid_key_length() {
|
||||
let result = Crypter::new(&Algorithm::aes_128_gcm(), &[0u8; 15]);
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn seal_rejects_invalid_nonce_and_tag_lengths() {
|
||||
let key = Crypter::new(&Algorithm::aes_128_gcm(), &[0u8; 16]).unwrap();
|
||||
let mut payload = [0u8; 8];
|
||||
|
||||
let mut short_tag = [0u8; 8];
|
||||
let short_tag_result = key.seal_in_place(&[0u8; 12], b"", &mut payload, &mut short_tag);
|
||||
assert!(short_tag_result.is_err());
|
||||
|
||||
let mut tag = [0u8; 16];
|
||||
let wrong_nonce_result = key.seal_in_place(&[0u8; 11], b"", &mut payload, &mut tag);
|
||||
assert!(wrong_nonce_result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_rejects_invalid_nonce_length() {
|
||||
let key = Crypter::new(&Algorithm::aes_128_gcm(), &[0u8; 16]).unwrap();
|
||||
let mut payload = [0u8; 8];
|
||||
let tag = [0u8; 16];
|
||||
|
||||
let result = key.open_in_place(&[0u8; 11], b"", &mut payload, &tag);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,6 @@ use std::{
|
|||
|
||||
use foreign_types::{ForeignType, ForeignTypeRef, Opaque};
|
||||
|
||||
use crate::helper::{cvt, cvt_p};
|
||||
|
||||
pub struct HmacCtxRef(Opaque);
|
||||
|
||||
unsafe impl ForeignTypeRef for HmacCtxRef {
|
||||
|
|
@ -35,20 +33,6 @@ unsafe impl ForeignType for HmacCtx {
|
|||
}
|
||||
}
|
||||
|
||||
impl Clone for HmacCtx {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
cvt_p(boring_sys::HMAC_CTX_new())
|
||||
.map(|ctx| HmacCtx::from_ptr(ctx))
|
||||
.and_then(|ctx| {
|
||||
cvt(boring_sys::HMAC_CTX_copy(ctx.as_ptr(), self.0.as_ptr()))?;
|
||||
Ok(ctx)
|
||||
})
|
||||
}
|
||||
.expect("failed cloning hmac ctx")
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for HmacCtx {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
|
|
|
|||
|
|
@ -8,31 +8,52 @@ description = "Boringssl rustls provider"
|
|||
publish = false
|
||||
|
||||
[features]
|
||||
default = ["tls12"]
|
||||
# Use a FIPS-validated version of boringssl.
|
||||
fips = ["boring/fips", "boring-sys/fips"]
|
||||
logging = ["log"]
|
||||
fips-only = []
|
||||
default = []
|
||||
|
||||
# Build against a FIPS-validated version of BoringSSL and restrict the
|
||||
# provider to FIPS-approved algorithms only. This affects:
|
||||
# - Cipher suites: AES-GCM only (no ChaCha20-Poly1305).
|
||||
# - Key exchange groups: X25519MLKEM768 (preferred), P-256, and P-384
|
||||
# only (no X25519, X448, P-521, or FFDHE).
|
||||
# - Signature algorithms: RSA PKCS#1 / PSS and ECDSA with P-256/P-384
|
||||
# only (no P-521, Ed25519, or Ed448).
|
||||
# Implies `mlkem` to ensure the post-quantum hybrid group is available.
|
||||
# Aligned with boring's `fips202205` compliance policy (SP 800-52r2).
|
||||
fips = ["boring/fips", "mlkem"]
|
||||
|
||||
# Enable X25519MLKEM768 post-quantum hybrid key exchange group
|
||||
# (draft-ietf-tls-ecdhe-mlkem-00). Uses ML-KEM-768 (FIPS 203) combined
|
||||
# with X25519 for hybrid post-quantum/classical key agreement.
|
||||
mlkem = ["boring/mlkem"]
|
||||
|
||||
# Deprecated alias for `fips`. Matches the boring crate's feature name
|
||||
# for backwards compatibility.
|
||||
fips-precompiled = ["fips"]
|
||||
|
||||
# Enable TLS 1.2 cipher suites (ECDHE-ECDSA and ECDHE-RSA with AES-GCM
|
||||
# and ChaCha20-Poly1305). Without this feature only TLS 1.3 is available.
|
||||
tls12 = ["rustls/tls12"]
|
||||
|
||||
# Enable debug logging of BoringSSL errors and provider internals via
|
||||
# the `log` crate. Useful for diagnosing handshake failures.
|
||||
logging = ["log"]
|
||||
|
||||
[dependencies]
|
||||
aead = {version = "0.5", default_features = false, features = ["alloc"] }
|
||||
aead = {version = "0.5", default-features = false, features = ["alloc"] }
|
||||
boring = { workspace = true }
|
||||
boring-additions = { path = "../boring-additions" }
|
||||
boring-sys = { workspace = true }
|
||||
boring-sys-additions = { path = "../boring-sys-additions" }
|
||||
foreign-types = "0.5"
|
||||
lazy_static = "1.4"
|
||||
log = { version = "0.4.4", optional = true }
|
||||
once_cell = "1"
|
||||
rustls = { workspace = true }
|
||||
rustls-pki-types = { workspace = true }
|
||||
spki = "0.7"
|
||||
webpki = { workspace = true, features = ["alloc", "std"] }
|
||||
zeroize = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
hex-literal = "0.4"
|
||||
rcgen = "0.11.3"
|
||||
hex-literal = "1"
|
||||
rcgen = "0.12"
|
||||
tokio = { version = "1.34", features = ["macros", "rt", "net", "io-util", "io-std"] }
|
||||
tokio-rustls = { workspace = true }
|
||||
webpki-roots = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
use std::marker::PhantomData;
|
||||
|
||||
use aead::{AeadCore, AeadInPlace, Nonce, Tag};
|
||||
use aead::{AeadCore, AeadInPlace, Buffer, Nonce, Tag};
|
||||
use boring::error::ErrorStack;
|
||||
use boring_additions::aead::Algorithm;
|
||||
use rustls::crypto::cipher::{self, make_tls12_aad, make_tls13_aad, Iv};
|
||||
#[cfg(feature = "tls12")]
|
||||
use rustls::crypto::cipher::make_tls12_aad;
|
||||
use rustls::crypto::cipher::{self, make_tls13_aad, BorrowedPayload, Iv, PrefixedPayload};
|
||||
use rustls::{ConnectionTrafficSecrets, ContentType, ProtocolVersion};
|
||||
|
||||
use crate::helper::log_and_map;
|
||||
|
|
@ -18,6 +20,8 @@ pub(crate) trait BoringCipher {
|
|||
/// The IV's fixed length (Not the full IV length, only the part that doesn't change).
|
||||
/// Together with [`BoringCipher::explicit_nonce_len`] it determines the total
|
||||
/// lengths of the used nonce.
|
||||
/// Used only by TLS 1.2 code paths.
|
||||
#[cfg(feature = "tls12")]
|
||||
const FIXED_IV_LEN: usize;
|
||||
/// The key size in bytes
|
||||
const KEY_SIZE: usize;
|
||||
|
|
@ -25,6 +29,15 @@ pub(crate) trait BoringCipher {
|
|||
/// The length of the authentication tag
|
||||
const TAG_LEN: usize;
|
||||
|
||||
/// integrity limit
|
||||
const INTEGRITY_LIMIT: u64;
|
||||
|
||||
/// confidentiality limit
|
||||
const CONFIDENTIALITY_LIMIT: u64;
|
||||
|
||||
/// Whether this algorithm is FIPS-approved.
|
||||
const FIPS_APPROVED: bool;
|
||||
|
||||
/// Constructs a new instance of this cipher as an AEAD algorithm
|
||||
fn new_cipher() -> Algorithm;
|
||||
|
||||
|
|
@ -32,14 +45,14 @@ pub(crate) trait BoringCipher {
|
|||
fn extract_keys(key: cipher::AeadKey, iv: cipher::Iv) -> ConnectionTrafficSecrets;
|
||||
}
|
||||
|
||||
pub(crate) trait QuicCipher {
|
||||
pub(crate) trait QuicCipher: Send + Sync {
|
||||
/// The key size in bytes
|
||||
const KEY_SIZE: usize;
|
||||
|
||||
/// the expected length of a sample
|
||||
const SAMPLE_LEN: usize;
|
||||
|
||||
fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> [u8; 5];
|
||||
fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> Result<[u8; 5], rustls::Error>;
|
||||
}
|
||||
|
||||
pub(crate) trait BoringAead: BoringCipher + AeadCore + Send + Sync {}
|
||||
|
|
@ -64,19 +77,21 @@ impl<T: BoringAead> AeadCore for BoringAeadCrypter<T> {
|
|||
impl<T: BoringAead> BoringAeadCrypter<T> {
|
||||
/// Creates a new aead crypter
|
||||
pub fn new(iv: Iv, key: &[u8], tls_version: ProtocolVersion) -> Result<Self, ErrorStack> {
|
||||
assert!(match tls_version {
|
||||
let tls_version_supported = match tls_version {
|
||||
#[cfg(feature = "tls12")]
|
||||
ProtocolVersion::TLSv1_2 => true,
|
||||
ProtocolVersion::TLSv1_3 => true,
|
||||
_ => false,
|
||||
});
|
||||
};
|
||||
if !tls_version_supported {
|
||||
return Err(ErrorStack::get());
|
||||
}
|
||||
|
||||
let cipher = <T as BoringCipher>::new_cipher();
|
||||
|
||||
assert_eq!(
|
||||
cipher.nonce_len(),
|
||||
rustls::crypto::cipher::Nonce::new(&iv, 0).0.len()
|
||||
);
|
||||
if cipher.nonce_len() != rustls::crypto::cipher::Nonce::new(&iv, 0).0.len() {
|
||||
return Err(ErrorStack::get());
|
||||
}
|
||||
|
||||
let crypter = BoringAeadCrypter {
|
||||
crypter: boring_additions::aead::Crypter::new(&cipher, key)?,
|
||||
|
|
@ -123,11 +138,10 @@ where
|
|||
{
|
||||
fn encrypt(
|
||||
&mut self,
|
||||
msg: cipher::BorrowedPlainMessage,
|
||||
msg: cipher::OutboundPlainMessage,
|
||||
seq: u64,
|
||||
) -> Result<cipher::OpaqueMessage, rustls::Error> {
|
||||
) -> Result<cipher::OutboundOpaqueMessage, rustls::Error> {
|
||||
let nonce = cipher::Nonce::new(&self.iv, seq);
|
||||
|
||||
match self.tls_version {
|
||||
#[cfg(feature = "tls12")]
|
||||
ProtocolVersion::TLSv1_2 => {
|
||||
|
|
@ -136,50 +150,57 @@ where
|
|||
|
||||
let total_len = self.encrypted_payload_len(msg.payload.len());
|
||||
|
||||
let mut full_payload = Vec::with_capacity(total_len);
|
||||
let mut full_payload = PrefixedPayload::with_capacity(total_len);
|
||||
full_payload.extend_from_slice(&nonce.0.as_ref()[fixed_iv_len..]);
|
||||
full_payload.extend_from_slice(msg.payload);
|
||||
full_payload.extend_from_chunks(&msg.payload);
|
||||
full_payload.extend_from_slice(&vec![0u8; self.crypter.max_overhead()]);
|
||||
|
||||
let (_, payload) = full_payload.split_at_mut(explicit_nonce_len);
|
||||
let (_, payload) = full_payload.as_mut().split_at_mut(explicit_nonce_len);
|
||||
let (payload, tag) = payload.split_at_mut(msg.payload.len());
|
||||
let aad = cipher::make_tls12_aad(seq, msg.typ, msg.version, msg.payload.len());
|
||||
self.crypter
|
||||
.seal_in_place(&nonce.0, &aad, payload, tag)
|
||||
.map_err(|_| rustls::Error::EncryptError)
|
||||
.map(|_| cipher::OpaqueMessage::new(msg.typ, msg.version, full_payload))
|
||||
.map(|_| cipher::OutboundOpaqueMessage::new(msg.typ, msg.version, full_payload))
|
||||
}
|
||||
|
||||
ProtocolVersion::TLSv1_3 => {
|
||||
let total_len = self.encrypted_payload_len(msg.payload.len());
|
||||
|
||||
let mut payload = Vec::with_capacity(total_len);
|
||||
payload.extend_from_slice(msg.payload);
|
||||
payload.push(msg.typ.get_u8());
|
||||
let mut payload = PrefixedPayload::with_capacity(total_len);
|
||||
payload.extend_from_chunks(&msg.payload);
|
||||
payload.extend_from_slice(&msg.typ.to_array());
|
||||
|
||||
let aad = cipher::make_tls13_aad(total_len);
|
||||
self.encrypt_in_place(Nonce::<T>::from_slice(&nonce.0), &aad, &mut payload)
|
||||
self.encrypt_in_place(
|
||||
Nonce::<T>::from_slice(&nonce.0),
|
||||
&aad,
|
||||
&mut EncryptBufferAdapter(&mut payload),
|
||||
)
|
||||
.map_err(|_| rustls::Error::EncryptError)
|
||||
.map(|_| {
|
||||
cipher::OpaqueMessage::new(
|
||||
cipher::OutboundOpaqueMessage::new(
|
||||
ContentType::ApplicationData,
|
||||
ProtocolVersion::TLSv1_2,
|
||||
payload,
|
||||
)
|
||||
})
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
_ => Err(rustls::Error::EncryptError),
|
||||
}
|
||||
}
|
||||
|
||||
fn encrypted_payload_len(&self, payload_len: usize) -> usize {
|
||||
match self.tls_version {
|
||||
ProtocolVersion::TLSv1_2 => {
|
||||
payload_len + self.crypter.max_overhead() + <T as BoringCipher>::EXPLICIT_NONCE_LEN
|
||||
}
|
||||
ProtocolVersion::TLSv1_3 => payload_len + 1 + self.crypter.max_overhead(),
|
||||
_ => unimplemented!(),
|
||||
}
|
||||
let per_record_overhead = match self.tls_version {
|
||||
ProtocolVersion::TLSv1_2 => self
|
||||
.crypter
|
||||
.max_overhead()
|
||||
.saturating_add(<T as BoringCipher>::EXPLICIT_NONCE_LEN),
|
||||
ProtocolVersion::TLSv1_3 => 1usize.saturating_add(self.crypter.max_overhead()),
|
||||
_ => return payload_len,
|
||||
};
|
||||
|
||||
payload_len.saturating_add(per_record_overhead)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -187,23 +208,30 @@ impl<T> cipher::MessageDecrypter for BoringAeadCrypter<T>
|
|||
where
|
||||
T: BoringAead,
|
||||
{
|
||||
fn decrypt(
|
||||
fn decrypt<'a>(
|
||||
&mut self,
|
||||
mut m: cipher::OpaqueMessage,
|
||||
mut m: cipher::InboundOpaqueMessage<'a>,
|
||||
seq: u64,
|
||||
) -> Result<cipher::PlainMessage, rustls::Error> {
|
||||
) -> Result<cipher::InboundPlainMessage<'a>, rustls::Error> {
|
||||
match self.tls_version {
|
||||
#[cfg(feature = "tls12")]
|
||||
ProtocolVersion::TLSv1_2 => {
|
||||
let explicit_nonce_len = <T as BoringCipher>::EXPLICIT_NONCE_LEN;
|
||||
let tag_len = self.crypter.max_overhead();
|
||||
let min_payload_len = explicit_nonce_len
|
||||
.checked_add(tag_len)
|
||||
.ok_or(rustls::Error::DecryptError)?;
|
||||
|
||||
if m.payload.len() < min_payload_len {
|
||||
return Err(rustls::Error::DecryptError);
|
||||
}
|
||||
|
||||
// payload is: [nonce] | [ciphertext] | [auth tag]
|
||||
let actual_payload_length =
|
||||
m.payload().len() - self.crypter.max_overhead() - explicit_nonce_len;
|
||||
let actual_payload_length = m.payload.len() - tag_len - explicit_nonce_len;
|
||||
|
||||
let aad = make_tls12_aad(seq, m.typ, m.version, actual_payload_length);
|
||||
|
||||
let payload = m.payload_mut();
|
||||
let payload = &mut m.payload;
|
||||
|
||||
// get the nonce
|
||||
let (explicit_nonce, payload) = payload.split_at_mut(explicit_nonce_len);
|
||||
|
|
@ -211,7 +239,9 @@ where
|
|||
let nonce = {
|
||||
let fixed_iv_len = <T as BoringCipher>::FIXED_IV_LEN;
|
||||
|
||||
assert_eq!(explicit_nonce_len + fixed_iv_len, 12);
|
||||
if explicit_nonce_len + fixed_iv_len != 12 {
|
||||
return Err(rustls::Error::DecryptError);
|
||||
}
|
||||
|
||||
// grab the IV by constructing a nonce, this is just an xor
|
||||
let iv = cipher::Nonce::new(&self.iv, seq).0;
|
||||
|
|
@ -222,30 +252,33 @@ where
|
|||
};
|
||||
|
||||
// split off the authentication tag
|
||||
let (payload, tag) =
|
||||
payload.split_at_mut(payload.len() - self.crypter.max_overhead());
|
||||
let (payload, tag) = payload.split_at_mut(payload.len() - tag_len);
|
||||
|
||||
self.crypter
|
||||
.open_in_place(&nonce, &aad, payload, tag)
|
||||
.map_err(|e| log_and_map("open_in_place", e, rustls::Error::DecryptError))
|
||||
.map(|_| {
|
||||
// rotate the nonce to the end
|
||||
m.payload_mut().rotate_left(explicit_nonce_len);
|
||||
m.payload.rotate_left(explicit_nonce_len);
|
||||
|
||||
// truncate buffer to the actual payload
|
||||
m.payload_mut().truncate(actual_payload_length);
|
||||
m.payload.truncate(actual_payload_length);
|
||||
|
||||
m.into_plain_message()
|
||||
})
|
||||
}
|
||||
ProtocolVersion::TLSv1_3 => {
|
||||
let nonce = cipher::Nonce::new(&self.iv, seq);
|
||||
let aad = make_tls13_aad(m.payload().len());
|
||||
self.decrypt_in_place(Nonce::<T>::from_slice(&nonce.0), &aad, m.payload_mut())
|
||||
let aad = make_tls13_aad(m.payload.len());
|
||||
self.decrypt_in_place(
|
||||
Nonce::<T>::from_slice(&nonce.0),
|
||||
&aad,
|
||||
&mut DecryptBufferAdapter(&mut m.payload),
|
||||
)
|
||||
.map_err(|_| rustls::Error::DecryptError)
|
||||
.and_then(|_| m.into_tls13_unpadded_message())
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
_ => Err(rustls::Error::DecryptError),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -278,6 +311,10 @@ where
|
|||
let associated_data = header;
|
||||
let nonce = cipher::Nonce::new(&self.iv, packet_number);
|
||||
|
||||
if payload.len() < self.crypter.max_overhead() {
|
||||
return Err(rustls::Error::DecryptError);
|
||||
}
|
||||
|
||||
let (buffer, tag) = payload.split_at_mut(payload.len() - self.crypter.max_overhead());
|
||||
|
||||
self.crypter
|
||||
|
|
@ -290,6 +327,76 @@ where
|
|||
fn tag_len(&self) -> usize {
|
||||
<T as BoringCipher>::TAG_LEN
|
||||
}
|
||||
|
||||
fn confidentiality_limit(&self) -> u64 {
|
||||
<T as BoringCipher>::CONFIDENTIALITY_LIMIT
|
||||
}
|
||||
|
||||
fn integrity_limit(&self) -> u64 {
|
||||
<T as BoringCipher>::INTEGRITY_LIMIT
|
||||
}
|
||||
}
|
||||
|
||||
struct InvalidMessageEncrypter;
|
||||
|
||||
impl cipher::MessageEncrypter for InvalidMessageEncrypter {
|
||||
fn encrypt(
|
||||
&mut self,
|
||||
_msg: cipher::OutboundPlainMessage<'_>,
|
||||
_seq: u64,
|
||||
) -> Result<cipher::OutboundOpaqueMessage, rustls::Error> {
|
||||
Err(rustls::Error::EncryptError)
|
||||
}
|
||||
|
||||
fn encrypted_payload_len(&self, payload_len: usize) -> usize {
|
||||
payload_len
|
||||
}
|
||||
}
|
||||
|
||||
struct InvalidMessageDecrypter;
|
||||
|
||||
impl cipher::MessageDecrypter for InvalidMessageDecrypter {
|
||||
fn decrypt<'a>(
|
||||
&mut self,
|
||||
_msg: cipher::InboundOpaqueMessage<'a>,
|
||||
_seq: u64,
|
||||
) -> Result<cipher::InboundPlainMessage<'a>, rustls::Error> {
|
||||
Err(rustls::Error::DecryptError)
|
||||
}
|
||||
}
|
||||
|
||||
struct InvalidPacketKey<T>(PhantomData<T>);
|
||||
|
||||
impl<T: BoringCipher + QuicCipher> rustls::quic::PacketKey for InvalidPacketKey<T> {
|
||||
fn encrypt_in_place(
|
||||
&self,
|
||||
_packet_number: u64,
|
||||
_header: &[u8],
|
||||
_payload: &mut [u8],
|
||||
) -> Result<rustls::quic::Tag, rustls::Error> {
|
||||
Err(rustls::Error::EncryptError)
|
||||
}
|
||||
|
||||
fn decrypt_in_place<'a>(
|
||||
&self,
|
||||
_packet_number: u64,
|
||||
_header: &[u8],
|
||||
_payload: &'a mut [u8],
|
||||
) -> Result<&'a [u8], rustls::Error> {
|
||||
Err(rustls::Error::DecryptError)
|
||||
}
|
||||
|
||||
fn tag_len(&self) -> usize {
|
||||
<T as BoringCipher>::TAG_LEN
|
||||
}
|
||||
|
||||
fn confidentiality_limit(&self) -> u64 {
|
||||
0
|
||||
}
|
||||
|
||||
fn integrity_limit(&self) -> u64 {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct Aead<T>(PhantomData<T>);
|
||||
|
|
@ -300,17 +407,31 @@ impl<T> Aead<T> {
|
|||
|
||||
impl<T: BoringAead + 'static> cipher::Tls13AeadAlgorithm for Aead<T> {
|
||||
fn encrypter(&self, key: cipher::AeadKey, iv: cipher::Iv) -> Box<dyn cipher::MessageEncrypter> {
|
||||
Box::new(
|
||||
BoringAeadCrypter::<T>::new(iv, key.as_ref(), ProtocolVersion::TLSv1_3)
|
||||
.expect("failed to create AEAD crypter"),
|
||||
)
|
||||
match BoringAeadCrypter::<T>::new(iv, key.as_ref(), ProtocolVersion::TLSv1_3) {
|
||||
Ok(crypter) => Box::new(crypter),
|
||||
Err(err) => {
|
||||
log_and_map(
|
||||
"Tls13AeadAlgorithm::encrypter BoringAeadCrypter::new",
|
||||
err,
|
||||
(),
|
||||
);
|
||||
Box::new(InvalidMessageEncrypter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn decrypter(&self, key: cipher::AeadKey, iv: cipher::Iv) -> Box<dyn cipher::MessageDecrypter> {
|
||||
Box::new(
|
||||
BoringAeadCrypter::<T>::new(iv, key.as_ref(), ProtocolVersion::TLSv1_3)
|
||||
.expect("failed to create AEAD crypter"),
|
||||
)
|
||||
match BoringAeadCrypter::<T>::new(iv, key.as_ref(), ProtocolVersion::TLSv1_3) {
|
||||
Ok(crypter) => Box::new(crypter),
|
||||
Err(err) => {
|
||||
log_and_map(
|
||||
"Tls13AeadAlgorithm::decrypter BoringAeadCrypter::new",
|
||||
err,
|
||||
(),
|
||||
);
|
||||
Box::new(InvalidMessageDecrypter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn key_len(&self) -> usize {
|
||||
|
|
@ -324,6 +445,10 @@ impl<T: BoringAead + 'static> cipher::Tls13AeadAlgorithm for Aead<T> {
|
|||
) -> Result<ConnectionTrafficSecrets, cipher::UnsupportedOperationError> {
|
||||
Ok(<T as BoringCipher>::extract_keys(key, iv))
|
||||
}
|
||||
|
||||
fn fips(&self) -> bool {
|
||||
cfg!(feature = "fips") && <T as BoringCipher>::FIPS_APPROVED
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tls12")]
|
||||
|
|
@ -337,24 +462,42 @@ impl<T: BoringAead + 'static> cipher::Tls12AeadAlgorithm for Aead<T> {
|
|||
let mut full_iv = Vec::with_capacity(iv.len() + extra.len());
|
||||
full_iv.extend_from_slice(iv);
|
||||
full_iv.extend_from_slice(extra);
|
||||
Box::new(
|
||||
BoringAeadCrypter::<T>::new(Iv::copy(&full_iv), key.as_ref(), ProtocolVersion::TLSv1_2)
|
||||
.expect("failed to create AEAD crypter"),
|
||||
)
|
||||
match BoringAeadCrypter::<T>::new(
|
||||
Iv::copy(&full_iv),
|
||||
key.as_ref(),
|
||||
ProtocolVersion::TLSv1_2,
|
||||
) {
|
||||
Ok(crypter) => Box::new(crypter),
|
||||
Err(err) => {
|
||||
log_and_map(
|
||||
"Tls12AeadAlgorithm::encrypter BoringAeadCrypter::new",
|
||||
err,
|
||||
(),
|
||||
);
|
||||
Box::new(InvalidMessageEncrypter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn decrypter(&self, key: cipher::AeadKey, iv: &[u8]) -> Box<dyn cipher::MessageDecrypter> {
|
||||
let mut pseudo_iv = Vec::with_capacity(iv.len() + <T as BoringCipher>::EXPLICIT_NONCE_LEN);
|
||||
pseudo_iv.extend_from_slice(iv);
|
||||
pseudo_iv.extend_from_slice(&vec![0u8; <T as BoringCipher>::EXPLICIT_NONCE_LEN]);
|
||||
Box::new(
|
||||
BoringAeadCrypter::<T>::new(
|
||||
match BoringAeadCrypter::<T>::new(
|
||||
Iv::copy(&pseudo_iv),
|
||||
key.as_ref(),
|
||||
ProtocolVersion::TLSv1_2,
|
||||
)
|
||||
.expect("failed to create AEAD crypter"),
|
||||
)
|
||||
) {
|
||||
Ok(crypter) => Box::new(crypter),
|
||||
Err(err) => {
|
||||
log_and_map(
|
||||
"Tls12AeadAlgorithm::decrypter BoringAeadCrypter::new",
|
||||
err,
|
||||
(),
|
||||
);
|
||||
Box::new(InvalidMessageDecrypter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn key_block_shape(&self) -> cipher::KeyBlockShape {
|
||||
|
|
@ -374,7 +517,13 @@ impl<T: BoringAead + 'static> cipher::Tls12AeadAlgorithm for Aead<T> {
|
|||
let nonce = {
|
||||
let fixed_iv_len = <T as BoringCipher>::FIXED_IV_LEN;
|
||||
let explicit_nonce_len = <T as BoringCipher>::EXPLICIT_NONCE_LEN;
|
||||
assert_eq!(explicit_nonce_len + fixed_iv_len, 12);
|
||||
if explicit_nonce_len + fixed_iv_len != 12 {
|
||||
return Err(cipher::UnsupportedOperationError);
|
||||
}
|
||||
|
||||
if iv.len() != fixed_iv_len || explicit.len() != explicit_nonce_len {
|
||||
return Err(cipher::UnsupportedOperationError);
|
||||
}
|
||||
|
||||
// grab the IV by constructing a nonce, this is just an xor
|
||||
|
||||
|
|
@ -385,6 +534,10 @@ impl<T: BoringAead + 'static> cipher::Tls12AeadAlgorithm for Aead<T> {
|
|||
};
|
||||
Ok(<T as BoringCipher>::extract_keys(key, Iv::copy(&nonce)))
|
||||
}
|
||||
|
||||
fn fips(&self) -> bool {
|
||||
cfg!(feature = "fips") && <T as BoringCipher>::FIPS_APPROVED
|
||||
}
|
||||
}
|
||||
|
||||
struct QuicHeaderProtector<T: QuicCipher> {
|
||||
|
|
@ -400,8 +553,8 @@ impl<T: QuicCipher> QuicHeaderProtector<T> {
|
|||
first: &mut u8,
|
||||
packet_number: &mut [u8],
|
||||
remove: bool,
|
||||
) {
|
||||
let mask = T::header_protection_mask(self.key.as_ref(), sample);
|
||||
) -> Result<(), rustls::Error> {
|
||||
let mask = T::header_protection_mask(self.key.as_ref(), sample)?;
|
||||
|
||||
const LONG_HEADER_FORMAT: u8 = 0x80;
|
||||
let bits_to_mask = if (*first & LONG_HEADER_FORMAT) == LONG_HEADER_FORMAT {
|
||||
|
|
@ -428,6 +581,8 @@ impl<T: QuicCipher> QuicHeaderProtector<T> {
|
|||
for (pn_byte, m) in packet_number.iter_mut().zip(&mask[1..]).take(pn_length) {
|
||||
*pn_byte ^= m;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -443,7 +598,7 @@ impl<T: QuicCipher> rustls::quic::HeaderProtectionKey for QuicHeaderProtector<T>
|
|||
return Err(rustls::Error::General("packet number too long".into()));
|
||||
}
|
||||
|
||||
self.rfc9001_header_protection(sample, first, packet_number, false);
|
||||
self.rfc9001_header_protection(sample, first, packet_number, false)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -458,7 +613,7 @@ impl<T: QuicCipher> rustls::quic::HeaderProtectionKey for QuicHeaderProtector<T>
|
|||
return Err(rustls::Error::General("packet number too long".into()));
|
||||
}
|
||||
|
||||
self.rfc9001_header_protection(sample, first, packet_number, true);
|
||||
self.rfc9001_header_protection(sample, first, packet_number, true)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -473,10 +628,13 @@ where
|
|||
T: QuicCipher + BoringAead + 'static,
|
||||
{
|
||||
fn packet_key(&self, key: cipher::AeadKey, iv: Iv) -> Box<dyn rustls::quic::PacketKey> {
|
||||
Box::new(
|
||||
BoringAeadCrypter::<T>::new(iv, key.as_ref(), ProtocolVersion::TLSv1_3)
|
||||
.expect("failed to create AEAD crypter"),
|
||||
)
|
||||
match BoringAeadCrypter::<T>::new(iv, key.as_ref(), ProtocolVersion::TLSv1_3) {
|
||||
Ok(crypter) => Box::new(crypter),
|
||||
Err(err) => {
|
||||
log_and_map("QuicAlgorithm::packet_key BoringAeadCrypter::new", err, ());
|
||||
Box::new(InvalidPacketKey::<T>(PhantomData))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn header_protection_key(
|
||||
|
|
@ -492,17 +650,78 @@ where
|
|||
fn aead_key_len(&self) -> usize {
|
||||
<T as QuicCipher>::KEY_SIZE
|
||||
}
|
||||
|
||||
fn fips(&self) -> bool {
|
||||
cfg!(feature = "fips") && <T as BoringCipher>::FIPS_APPROVED
|
||||
}
|
||||
}
|
||||
|
||||
struct DecryptBufferAdapter<'a, 'p>(&'a mut BorrowedPayload<'p>);
|
||||
|
||||
impl AsRef<[u8]> for DecryptBufferAdapter<'_, '_> {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsMut<[u8]> for DecryptBufferAdapter<'_, '_> {
|
||||
fn as_mut(&mut self) -> &mut [u8] {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Buffer for DecryptBufferAdapter<'_, '_> {
|
||||
fn extend_from_slice(&mut self, _: &[u8]) -> aead::Result<()> {
|
||||
Err(aead::Error)
|
||||
}
|
||||
|
||||
fn truncate(&mut self, len: usize) {
|
||||
self.0.truncate(len)
|
||||
}
|
||||
}
|
||||
|
||||
struct EncryptBufferAdapter<'a>(&'a mut PrefixedPayload);
|
||||
|
||||
impl AsRef<[u8]> for EncryptBufferAdapter<'_> {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsMut<[u8]> for EncryptBufferAdapter<'_> {
|
||||
fn as_mut(&mut self) -> &mut [u8] {
|
||||
self.0.as_mut()
|
||||
}
|
||||
}
|
||||
|
||||
impl Buffer for EncryptBufferAdapter<'_> {
|
||||
fn extend_from_slice(&mut self, other: &[u8]) -> aead::Result<()> {
|
||||
self.0.extend_from_slice(other);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn truncate(&mut self, len: usize) {
|
||||
self.0.truncate(len)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use hex_literal::hex;
|
||||
use rustls::crypto::cipher::{AeadKey, Iv};
|
||||
use rustls::crypto::cipher::Tls13AeadAlgorithm;
|
||||
use rustls::crypto::cipher::{AeadKey, InboundOpaqueMessage, Iv};
|
||||
#[cfg(feature = "tls12")]
|
||||
use rustls::crypto::cipher::{MessageDecrypter, Tls12AeadAlgorithm};
|
||||
use rustls::quic::Algorithm as QuicAlgorithm;
|
||||
use rustls::quic::HeaderProtectionKey;
|
||||
use rustls::ContentType;
|
||||
use rustls::ProtocolVersion;
|
||||
|
||||
use crate::aead::BoringAeadCrypter;
|
||||
use rustls::quic::PacketKey;
|
||||
|
||||
use super::{chacha20::ChaCha20Poly1305, QuicHeaderProtector};
|
||||
use super::aes::Aes128;
|
||||
use super::{chacha20::ChaCha20Poly1305, BoringCipher, QuicHeaderProtector};
|
||||
|
||||
#[test]
|
||||
fn quic_header_protection_short() {
|
||||
|
|
@ -519,14 +738,97 @@ mod tests {
|
|||
phantom: std::marker::PhantomData::<ChaCha20Poly1305>,
|
||||
};
|
||||
|
||||
protector.rfc9001_header_protection(&sample, &mut first[0], packet_number, false);
|
||||
protector
|
||||
.rfc9001_header_protection(&sample, &mut first[0], packet_number, false)
|
||||
.expect("valid sample should protect QUIC header");
|
||||
assert_eq!(&header[..], &protected_header[..]);
|
||||
|
||||
let (first, packet_number) = header.split_at_mut(1);
|
||||
protector.rfc9001_header_protection(&sample, &mut first[0], packet_number, true);
|
||||
protector
|
||||
.rfc9001_header_protection(&sample, &mut first[0], packet_number, true)
|
||||
.expect("valid sample should unprotect QUIC header");
|
||||
assert_eq!(&header[..], &unprotected_header[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quic_header_protection_rejects_invalid_sample_without_mutation() {
|
||||
let hp_key = hex!("25a282b9e82f06f21f488917a4fc8f1b73573685608597d0efcb076b0ab7a7a4");
|
||||
let sample = hex!("5e5cd55c41f69080575d7999c25a5bfb");
|
||||
let mut header = hex!("4200bff4");
|
||||
let original = header;
|
||||
let (first, packet_number) = header.split_at_mut(1);
|
||||
|
||||
let protector = QuicHeaderProtector {
|
||||
key: AeadKey::from(hp_key),
|
||||
phantom: std::marker::PhantomData::<ChaCha20Poly1305>,
|
||||
};
|
||||
|
||||
let err = protector
|
||||
.encrypt_in_place(&sample[..sample.len() - 1], &mut first[0], packet_number)
|
||||
.expect_err("short sample must be rejected");
|
||||
|
||||
assert!(matches!(err, rustls::Error::General(_)));
|
||||
assert_eq!(header, original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tls13_decrypter_constructor_failure_returns_erroring_decrypter() {
|
||||
let mut decrypter = Tls13AeadAlgorithm::decrypter(
|
||||
&super::Aead::<Aes128>::DEFAULT,
|
||||
AeadKey::from([0u8; 32]),
|
||||
Iv::from([0u8; 12]),
|
||||
);
|
||||
let mut payload = vec![0u8; 8];
|
||||
let msg = InboundOpaqueMessage::new(
|
||||
ContentType::ApplicationData,
|
||||
ProtocolVersion::TLSv1_3,
|
||||
&mut payload,
|
||||
);
|
||||
|
||||
let err = decrypter
|
||||
.decrypt(msg, 0)
|
||||
.expect_err("invalid constructor inputs should produce decrypt errors");
|
||||
|
||||
assert!(matches!(err, rustls::Error::DecryptError));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quic_packet_key_constructor_failure_returns_erroring_key() {
|
||||
let packet_key = super::Aead::<Aes128>::DEFAULT
|
||||
.packet_key(AeadKey::from([0u8; 32]), Iv::from([0u8; 12]));
|
||||
let mut payload = [0u8; 0];
|
||||
|
||||
let enc_err = packet_key.encrypt_in_place(0, &[], &mut payload);
|
||||
assert!(matches!(enc_err, Err(rustls::Error::EncryptError)));
|
||||
|
||||
let dec_err = packet_key
|
||||
.decrypt_in_place(0, &[], &mut payload)
|
||||
.expect_err("invalid constructor inputs should produce decrypt errors");
|
||||
assert!(matches!(dec_err, rustls::Error::DecryptError));
|
||||
}
|
||||
|
||||
#[cfg(feature = "tls12")]
|
||||
#[test]
|
||||
fn tls12_decrypter_constructor_failure_returns_erroring_decrypter() {
|
||||
let mut decrypter = Tls12AeadAlgorithm::decrypter(
|
||||
&super::Aead::<Aes128>::DEFAULT,
|
||||
AeadKey::from([0u8; 32]),
|
||||
&[0u8; 4],
|
||||
);
|
||||
let mut payload = vec![0u8; 8];
|
||||
let msg = InboundOpaqueMessage::new(
|
||||
ContentType::ApplicationData,
|
||||
ProtocolVersion::TLSv1_2,
|
||||
&mut payload,
|
||||
);
|
||||
|
||||
let err = decrypter
|
||||
.decrypt(msg, 0)
|
||||
.expect_err("invalid constructor inputs should produce decrypt errors");
|
||||
|
||||
assert!(matches!(err, rustls::Error::DecryptError));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quic_chacha20_crypt() {
|
||||
// test vector from https://www.rfc-editor.org/rfc/rfc9001.html#name-chacha20-poly1305-short-hea
|
||||
|
|
@ -539,9 +841,9 @@ mod tests {
|
|||
let unprotected_header = hex!("4200bff4");
|
||||
|
||||
let protector = BoringAeadCrypter::<ChaCha20Poly1305>::new(
|
||||
Iv::new(iv),
|
||||
Iv::from(iv),
|
||||
&key,
|
||||
rustls::ProtocolVersion::TLSv1_3,
|
||||
ProtocolVersion::TLSv1_3,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
|
@ -560,4 +862,47 @@ mod tests {
|
|||
|
||||
assert_eq!(cleartext, expected_cleartext);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quic_decrypt_rejects_truncated_payload() {
|
||||
let key = [0u8; <ChaCha20Poly1305 as BoringCipher>::KEY_SIZE];
|
||||
let iv = [0u8; 12];
|
||||
let crypter = BoringAeadCrypter::<ChaCha20Poly1305>::new(
|
||||
Iv::from(iv),
|
||||
&key,
|
||||
ProtocolVersion::TLSv1_3,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut payload = [0u8; <ChaCha20Poly1305 as BoringCipher>::TAG_LEN - 1];
|
||||
let err = crypter
|
||||
.decrypt_in_place(0, &[], &mut payload)
|
||||
.expect_err("truncated QUIC payload must fail decryption");
|
||||
|
||||
assert!(matches!(err, rustls::Error::DecryptError));
|
||||
}
|
||||
|
||||
#[cfg(feature = "tls12")]
|
||||
#[test]
|
||||
fn tls12_decrypt_rejects_truncated_payload() {
|
||||
let key = [0u8; <Aes128 as BoringCipher>::KEY_SIZE];
|
||||
let iv = [0u8; 12];
|
||||
let mut decrypter =
|
||||
BoringAeadCrypter::<Aes128>::new(Iv::from(iv), &key, ProtocolVersion::TLSv1_2).unwrap();
|
||||
|
||||
let min_payload_len =
|
||||
<Aes128 as BoringCipher>::EXPLICIT_NONCE_LEN + <Aes128 as BoringCipher>::TAG_LEN;
|
||||
let mut payload = vec![0u8; min_payload_len - 1];
|
||||
let msg = InboundOpaqueMessage::new(
|
||||
ContentType::ApplicationData,
|
||||
ProtocolVersion::TLSv1_2,
|
||||
&mut payload,
|
||||
);
|
||||
|
||||
let err = decrypter
|
||||
.decrypt(msg, 0)
|
||||
.expect_err("truncated TLS1.2 payload must fail decryption");
|
||||
|
||||
assert!(matches!(err, rustls::Error::DecryptError));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,12 +11,17 @@ impl BoringAead for Aes128 {}
|
|||
impl BoringCipher for Aes128 {
|
||||
const EXPLICIT_NONCE_LEN: usize = 8;
|
||||
|
||||
#[cfg(feature = "tls12")]
|
||||
const FIXED_IV_LEN: usize = 4;
|
||||
|
||||
const KEY_SIZE: usize = 16;
|
||||
|
||||
const TAG_LEN: usize = 16;
|
||||
|
||||
const INTEGRITY_LIMIT: u64 = 1 << 52;
|
||||
const CONFIDENTIALITY_LIMIT: u64 = 1 << 23;
|
||||
const FIPS_APPROVED: bool = true;
|
||||
|
||||
fn new_cipher() -> Algorithm {
|
||||
Algorithm::aes_128_gcm()
|
||||
}
|
||||
|
|
@ -36,7 +41,7 @@ impl QuicCipher for Aes128 {
|
|||
const KEY_SIZE: usize = <Self as BoringCipher>::KEY_SIZE;
|
||||
const SAMPLE_LEN: usize = 16;
|
||||
|
||||
fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> [u8; 5] {
|
||||
fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> Result<[u8; 5], rustls::Error> {
|
||||
quic_header_protection_mask::<
|
||||
{ <Self as QuicCipher>::KEY_SIZE },
|
||||
{ <Self as QuicCipher>::SAMPLE_LEN },
|
||||
|
|
@ -52,12 +57,17 @@ impl BoringAead for Aes256 {}
|
|||
impl BoringCipher for Aes256 {
|
||||
const EXPLICIT_NONCE_LEN: usize = 8;
|
||||
|
||||
#[cfg(feature = "tls12")]
|
||||
const FIXED_IV_LEN: usize = 4;
|
||||
|
||||
const KEY_SIZE: usize = 32;
|
||||
|
||||
const TAG_LEN: usize = 16;
|
||||
|
||||
const INTEGRITY_LIMIT: u64 = 1 << 52;
|
||||
const CONFIDENTIALITY_LIMIT: u64 = 1 << 23;
|
||||
const FIPS_APPROVED: bool = true;
|
||||
|
||||
fn new_cipher() -> Algorithm {
|
||||
Algorithm::aes_256_gcm()
|
||||
}
|
||||
|
|
@ -77,7 +87,7 @@ impl QuicCipher for Aes256 {
|
|||
const KEY_SIZE: usize = <Self as BoringCipher>::KEY_SIZE;
|
||||
const SAMPLE_LEN: usize = 16;
|
||||
|
||||
fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> [u8; 5] {
|
||||
fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> Result<[u8; 5], rustls::Error> {
|
||||
quic_header_protection_mask::<
|
||||
{ <Self as QuicCipher>::KEY_SIZE },
|
||||
{ <Self as QuicCipher>::SAMPLE_LEN },
|
||||
|
|
@ -89,19 +99,32 @@ fn quic_header_protection_mask<const KEY_SIZE: usize, const SAMPLE_LEN: usize>(
|
|||
cipher: boring::symm::Cipher,
|
||||
hp_key: &[u8],
|
||||
sample: &[u8],
|
||||
) -> [u8; 5] {
|
||||
assert!(hp_key.len() == KEY_SIZE);
|
||||
assert!(sample.len() >= SAMPLE_LEN);
|
||||
) -> Result<[u8; 5], rustls::Error> {
|
||||
if hp_key.len() != KEY_SIZE {
|
||||
return Err(rustls::Error::General(
|
||||
"header protection key of invalid length".into(),
|
||||
));
|
||||
}
|
||||
if sample.len() != SAMPLE_LEN {
|
||||
return Err(rustls::Error::General("sample of invalid length".into()));
|
||||
}
|
||||
|
||||
let mut output = [0u8; SAMPLE_LEN];
|
||||
|
||||
let mut crypter = boring::symm::Crypter::new(cipher, boring::symm::Mode::Encrypt, hp_key, None)
|
||||
.expect("failed getting crypter");
|
||||
.map_err(|_| rustls::Error::General("failed generating header protection mask".into()))?;
|
||||
|
||||
let len = crypter.update(sample, &mut output).unwrap();
|
||||
let _ = len + crypter.finalize(&mut output[len..]).unwrap();
|
||||
let len = crypter
|
||||
.update(sample, &mut output)
|
||||
.map_err(|_| rustls::Error::General("failed generating header protection mask".into()))?;
|
||||
let _total = len
|
||||
+ crypter.finalize(&mut output[len..]).map_err(|_| {
|
||||
rustls::Error::General("failed generating header protection mask".into())
|
||||
})?;
|
||||
|
||||
output[..5].try_into().unwrap()
|
||||
let mut mask = [0u8; 5];
|
||||
mask.copy_from_slice(&output[..5]);
|
||||
Ok(mask)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
|||
|
|
@ -14,12 +14,17 @@ impl BoringAead for ChaCha20Poly1305 {}
|
|||
impl BoringCipher for ChaCha20Poly1305 {
|
||||
const EXPLICIT_NONCE_LEN: usize = 0;
|
||||
|
||||
#[cfg(feature = "tls12")]
|
||||
const FIXED_IV_LEN: usize = 12;
|
||||
|
||||
const KEY_SIZE: usize = 32;
|
||||
|
||||
const TAG_LEN: usize = 16;
|
||||
|
||||
const INTEGRITY_LIMIT: u64 = 1 << 36;
|
||||
const CONFIDENTIALITY_LIMIT: u64 = u64::MAX;
|
||||
const FIPS_APPROVED: bool = false;
|
||||
|
||||
fn new_cipher() -> Algorithm {
|
||||
Algorithm::chacha20_poly1305()
|
||||
}
|
||||
|
|
@ -33,13 +38,21 @@ impl QuicCipher for ChaCha20Poly1305 {
|
|||
const KEY_SIZE: usize = 32;
|
||||
const SAMPLE_LEN: usize = 16;
|
||||
|
||||
fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> [u8; 5] {
|
||||
assert!(hp_key.len() == <Self as QuicCipher>::KEY_SIZE);
|
||||
assert!(sample.len() >= <Self as QuicCipher>::SAMPLE_LEN);
|
||||
fn header_protection_mask(hp_key: &[u8], sample: &[u8]) -> Result<[u8; 5], rustls::Error> {
|
||||
if hp_key.len() != <Self as QuicCipher>::KEY_SIZE {
|
||||
return Err(rustls::Error::General(
|
||||
"header protection key of invalid length".into(),
|
||||
));
|
||||
}
|
||||
if sample.len() != <Self as QuicCipher>::SAMPLE_LEN {
|
||||
return Err(rustls::Error::General("sample of invalid length".into()));
|
||||
}
|
||||
|
||||
let mut mask = [0u8; 5];
|
||||
// RFC9001 5.4.4: The first 4 bytes of the sampled ciphertext are the block counter. A ChaCha20 implementation could take a 32-bit integer in place of a byte sequence, in which case, the byte sequence is interpreted as a little-endian value.
|
||||
let counter = u32::from_le_bytes(sample[0..4].try_into().unwrap());
|
||||
let mut counter_bytes = [0u8; 4];
|
||||
counter_bytes.copy_from_slice(&sample[0..4]);
|
||||
let counter = u32::from_le_bytes(counter_bytes);
|
||||
// RFC9001 5.4.4: The remaining 12 bytes are used as the nonce.
|
||||
let nonce = &sample[4..16];
|
||||
unsafe {
|
||||
|
|
@ -52,7 +65,7 @@ impl QuicCipher for ChaCha20Poly1305 {
|
|||
counter,
|
||||
);
|
||||
};
|
||||
mask
|
||||
Ok(mask)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -91,7 +104,8 @@ mod tests {
|
|||
let sample = hex!("5e5cd55c41f69080575d7999c25a5bfb");
|
||||
let hp_key = hex!("25a282b9e82f06f21f488917a4fc8f1b73573685608597d0efcb076b0ab7a7a4");
|
||||
let expected_mask = hex!("aefefe7d03");
|
||||
let mask = ChaCha20Poly1305::header_protection_mask(&hp_key, &sample);
|
||||
let mask = ChaCha20Poly1305::header_protection_mask(&hp_key, &sample)
|
||||
.expect("valid QUIC sample/key should produce a mask");
|
||||
assert_eq!(mask, expected_mask);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,10 +24,14 @@ impl hash::Hash for Hash {
|
|||
boring::nid::Nid::SHA256 => hash::HashAlgorithm::SHA256,
|
||||
boring::nid::Nid::SHA384 => hash::HashAlgorithm::SHA384,
|
||||
boring::nid::Nid::SHA512 => hash::HashAlgorithm::SHA512,
|
||||
_ => unimplemented!(),
|
||||
_ => unreachable!("hash::Hash is only instantiated with SHA-2 digests"),
|
||||
}
|
||||
}
|
||||
|
||||
fn fips(&self) -> bool {
|
||||
cfg!(feature = "fips")
|
||||
}
|
||||
|
||||
fn output_len(&self) -> usize {
|
||||
MessageDigest::from_nid(self.0)
|
||||
.expect("failed getting digest")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use std::marker::PhantomData;
|
|||
|
||||
use boring::hash::MessageDigest;
|
||||
use rustls::crypto::tls13::{self, Hkdf as RustlsHkdf};
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use crate::helper::{cvt, cvt_p};
|
||||
|
||||
|
|
@ -67,7 +68,7 @@ impl<T: BoringHash> RustlsHkdf for Hkdf<T> {
|
|||
let digest = T::new_hash();
|
||||
let hash_size = digest.size();
|
||||
|
||||
let mut prk = [0u8; boring_sys::EVP_MAX_MD_SIZE as usize];
|
||||
let mut prk = Zeroizing::new([0u8; boring_sys::EVP_MAX_MD_SIZE as usize]);
|
||||
let mut prk_len = 0;
|
||||
|
||||
// if salt isn't set we usen these bytes here as salt
|
||||
|
|
@ -103,7 +104,7 @@ impl<T: BoringHash> RustlsHkdf for Hkdf<T> {
|
|||
okm: &rustls::crypto::tls13::OkmBlock,
|
||||
) -> Box<dyn rustls::crypto::tls13::HkdfExpander> {
|
||||
let okm = okm.as_ref();
|
||||
let mut prk = [0u8; boring_sys::EVP_MAX_MD_SIZE as usize];
|
||||
let mut prk = Zeroizing::new([0u8; boring_sys::EVP_MAX_MD_SIZE as usize]);
|
||||
let prk_len = okm.len();
|
||||
|
||||
prk[..prk_len].copy_from_slice(okm);
|
||||
|
|
@ -121,7 +122,7 @@ impl<T: BoringHash> RustlsHkdf for Hkdf<T> {
|
|||
message: &[u8],
|
||||
) -> rustls::crypto::hmac::Tag {
|
||||
let digest = T::new_hash();
|
||||
let mut hash = [0u8; boring_sys::EVP_MAX_MD_SIZE as usize];
|
||||
let mut hash = Zeroizing::new([0u8; boring_sys::EVP_MAX_MD_SIZE as usize]);
|
||||
let mut hash_len = 0u32;
|
||||
unsafe {
|
||||
cvt_p(boring_sys::HMAC(
|
||||
|
|
@ -137,10 +138,14 @@ impl<T: BoringHash> RustlsHkdf for Hkdf<T> {
|
|||
}
|
||||
rustls::crypto::hmac::Tag::new(&hash[..hash_len as usize])
|
||||
}
|
||||
|
||||
fn fips(&self) -> bool {
|
||||
cfg!(feature = "fips")
|
||||
}
|
||||
}
|
||||
|
||||
struct HkdfExpander {
|
||||
prk: [u8; boring_sys::EVP_MAX_MD_SIZE as usize],
|
||||
prk: Zeroizing<[u8; boring_sys::EVP_MAX_MD_SIZE as usize]>,
|
||||
prk_len: usize,
|
||||
digest: MessageDigest,
|
||||
}
|
||||
|
|
@ -162,9 +167,17 @@ impl tls13::HkdfExpander for HkdfExpander {
|
|||
info: &[&[u8]],
|
||||
output: &mut [u8],
|
||||
) -> Result<(), tls13::OutputLengthError> {
|
||||
let max_output_len = self
|
||||
.hash_len()
|
||||
.checked_mul(255)
|
||||
.ok_or(tls13::OutputLengthError)?;
|
||||
if output.len() > max_output_len {
|
||||
return Err(tls13::OutputLengthError);
|
||||
}
|
||||
|
||||
let info_concat = info.concat();
|
||||
unsafe {
|
||||
boring_sys::HKDF_expand(
|
||||
cvt(boring_sys::HKDF_expand(
|
||||
output.as_mut_ptr(),
|
||||
output.len(),
|
||||
self.digest.as_ptr(),
|
||||
|
|
@ -172,8 +185,9 @@ impl tls13::HkdfExpander for HkdfExpander {
|
|||
self.prk_len,
|
||||
info_concat.as_ptr(),
|
||||
info_concat.len(),
|
||||
);
|
||||
};
|
||||
))
|
||||
.map_err(|_| tls13::OutputLengthError)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -187,7 +201,7 @@ impl tls13::HkdfExpander for HkdfExpander {
|
|||
/// This is infallible, because by definition `OkmBlock` is always exactly
|
||||
/// `HashLen` bytes long.
|
||||
fn expand_block(&self, info: &[&[u8]]) -> tls13::OkmBlock {
|
||||
let mut output = [0u8; boring_sys::EVP_MAX_MD_SIZE as usize];
|
||||
let mut output = Zeroizing::new([0u8; boring_sys::EVP_MAX_MD_SIZE as usize]);
|
||||
let output_len = self.hash_len();
|
||||
|
||||
self.expand_slice(info, &mut output[..output_len])
|
||||
|
|
@ -200,3 +214,35 @@ impl tls13::HkdfExpander for HkdfExpander {
|
|||
self.digest.size()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use boring::hash::MessageDigest;
|
||||
use rustls::crypto::tls13::Hkdf as _;
|
||||
|
||||
use super::{Hkdf, Sha256};
|
||||
|
||||
#[test]
|
||||
fn expand_slice_rejects_output_larger_than_rfc_limit() {
|
||||
let hkdf = Hkdf::<Sha256>::DEFAULT;
|
||||
let expander = hkdf.extract_from_secret(None, b"ikm");
|
||||
let hash_len = MessageDigest::sha256().size();
|
||||
let mut output = vec![0u8; hash_len * 255 + 1];
|
||||
|
||||
assert!(expander.expand_slice(&[b"info"], &mut output).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn expand_slice_accepts_output_at_rfc_limit() {
|
||||
let hkdf = Hkdf::<Sha256>::DEFAULT;
|
||||
let expander = hkdf.extract_from_secret(None, b"ikm");
|
||||
let hash_len = MessageDigest::sha256().size();
|
||||
let mut output = vec![0u8; hash_len * 255];
|
||||
|
||||
expander
|
||||
.expand_slice(&[b"info"], &mut output)
|
||||
.expect("HKDF expand at RFC limit should succeed");
|
||||
|
||||
assert!(output.iter().any(|byte| *byte != 0));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ use boring::hash::MessageDigest;
|
|||
use boring_additions::hmac::HmacCtx;
|
||||
use foreign_types::ForeignType;
|
||||
use rustls::crypto;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use crate::helper::{cvt, cvt_p};
|
||||
|
||||
|
|
@ -19,18 +20,11 @@ struct BoringHmac(pub boring::nid::Nid);
|
|||
|
||||
impl crypto::hmac::Hmac for BoringHmac {
|
||||
fn with_key(&self, key: &[u8]) -> Box<dyn crypto::hmac::Key> {
|
||||
let ctx = unsafe {
|
||||
HmacCtx::from_ptr(
|
||||
cvt_p(boring_sys::HMAC_CTX_new()).expect("failed getting hmac context"),
|
||||
)
|
||||
};
|
||||
|
||||
let md = MessageDigest::from_nid(self.0).expect("failed getting digest");
|
||||
|
||||
Box::new(BoringHmacKey {
|
||||
ctx,
|
||||
md,
|
||||
key: key.to_vec(),
|
||||
key: Zeroizing::new(key.to_vec()),
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -39,34 +33,35 @@ impl crypto::hmac::Hmac for BoringHmac {
|
|||
.expect("failed getting digest")
|
||||
.size()
|
||||
}
|
||||
|
||||
fn fips(&self) -> bool {
|
||||
cfg!(feature = "fips")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct BoringHmacKey {
|
||||
ctx: HmacCtx,
|
||||
md: MessageDigest,
|
||||
key: Vec<u8>,
|
||||
key: Zeroizing<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl BoringHmacKey {
|
||||
fn init(&self) {
|
||||
fn init(ctx: &HmacCtx, key: &[u8], md: MessageDigest) {
|
||||
unsafe {
|
||||
// initialize a new hmac
|
||||
cvt(boring_sys::HMAC_Init_ex(
|
||||
self.ctx.as_ptr(),
|
||||
self.key.as_ptr() as *const c_void,
|
||||
self.key.len(),
|
||||
self.md.as_ptr(),
|
||||
ctx.as_ptr(),
|
||||
key.as_ptr() as *const c_void,
|
||||
key.len(),
|
||||
md.as_ptr(),
|
||||
ptr::null_mut(),
|
||||
))
|
||||
}
|
||||
.expect("failed initializing hmac");
|
||||
}
|
||||
|
||||
fn update(&self, bytes: &[u8]) {
|
||||
fn update(ctx: &HmacCtx, bytes: &[u8]) {
|
||||
unsafe {
|
||||
cvt(boring_sys::HMAC_Update(
|
||||
self.ctx.as_ptr(),
|
||||
ctx.as_ptr(),
|
||||
bytes.as_ptr(),
|
||||
bytes.len(),
|
||||
))
|
||||
|
|
@ -74,11 +69,11 @@ impl BoringHmacKey {
|
|||
.expect("failed updating hmac");
|
||||
}
|
||||
|
||||
fn finish(&self, out: &mut [u8]) -> usize {
|
||||
fn finish(ctx: &HmacCtx, out: &mut [u8]) -> usize {
|
||||
let mut out_len = 0;
|
||||
unsafe {
|
||||
cvt(boring_sys::HMAC_Final(
|
||||
self.ctx.as_ptr(),
|
||||
ctx.as_ptr(),
|
||||
out.as_mut_ptr(),
|
||||
&mut out_len,
|
||||
))
|
||||
|
|
@ -86,21 +81,28 @@ impl BoringHmacKey {
|
|||
.expect("failed hmac final");
|
||||
out_len as usize
|
||||
}
|
||||
|
||||
fn new_ctx() -> HmacCtx {
|
||||
unsafe {
|
||||
HmacCtx::from_ptr(cvt_p(boring_sys::HMAC_CTX_new()).expect("failed creating HMAC_CTX"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crypto::hmac::Key for BoringHmacKey {
|
||||
fn sign_concat(&self, first: &[u8], middle: &[&[u8]], last: &[u8]) -> crypto::hmac::Tag {
|
||||
self.init();
|
||||
let ctx = Self::new_ctx();
|
||||
Self::init(&ctx, self.key.as_slice(), self.md);
|
||||
|
||||
self.update(first);
|
||||
Self::update(&ctx, first);
|
||||
for m in middle {
|
||||
self.update(m);
|
||||
Self::update(&ctx, m);
|
||||
}
|
||||
|
||||
self.update(last);
|
||||
Self::update(&ctx, last);
|
||||
|
||||
let mut out = [0u8; 32];
|
||||
let out_len = self.finish(&mut out);
|
||||
let mut out = Zeroizing::new([0u8; boring_sys::EVP_MAX_MD_SIZE as usize]);
|
||||
let out_len = Self::finish(&ctx, &mut out[..]);
|
||||
|
||||
crypto::hmac::Tag::new(&out[..out_len])
|
||||
}
|
||||
|
|
@ -112,7 +114,7 @@ impl crypto::hmac::Key for BoringHmacKey {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::SHA256;
|
||||
use super::{SHA256, SHA384};
|
||||
use hex_literal::hex;
|
||||
|
||||
#[test]
|
||||
|
|
@ -141,4 +143,23 @@ mod tests {
|
|||
hex!("11fa4a6ee97bebfad9e1087145c556fec9a786cad0659aa10702d21bd2968305")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sha384_hmac_len() {
|
||||
let hasher = SHA384.with_key("Very Secret".as_bytes());
|
||||
|
||||
let tag = hasher.sign_concat(
|
||||
&[],
|
||||
&[
|
||||
"yay".as_bytes(),
|
||||
"this".as_bytes(),
|
||||
"works".as_bytes(),
|
||||
"well".as_bytes(),
|
||||
],
|
||||
&[],
|
||||
);
|
||||
|
||||
assert_eq!(tag.as_ref().len(), hasher.tag_len());
|
||||
assert_eq!(tag.as_ref().len(), 48);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +1,22 @@
|
|||
use std::{
|
||||
mem::MaybeUninit,
|
||||
ptr::{self},
|
||||
};
|
||||
#[cfg(not(feature = "fips"))]
|
||||
use std::{mem::MaybeUninit, ptr};
|
||||
|
||||
use boring::{
|
||||
ec::{EcGroup, EcKey},
|
||||
error::ErrorStack,
|
||||
nid::Nid,
|
||||
pkey::Id,
|
||||
pkey::{PKey, PKeyRef, Private},
|
||||
};
|
||||
#[cfg(not(feature = "fips"))]
|
||||
use boring_additions::evp::EvpPkeyCtx;
|
||||
#[cfg(not(feature = "fips"))]
|
||||
use foreign_types::ForeignType;
|
||||
use rustls::crypto;
|
||||
use spki::der::Decode;
|
||||
|
||||
use crate::helper::{cvt, cvt_p, log_and_map};
|
||||
use crate::helper::log_and_map;
|
||||
#[cfg(not(feature = "fips"))]
|
||||
use crate::helper::{cvt, cvt_p};
|
||||
|
||||
use super::DhKeyType;
|
||||
|
||||
|
|
@ -30,16 +32,11 @@ pub struct KeyExchange {
|
|||
impl KeyExchange {
|
||||
/// Creates a new `KeyExchange` using a random
|
||||
/// private key for the `X25519` Edwards curve
|
||||
#[cfg(not(feature = "fips"))]
|
||||
pub fn with_x25519() -> Result<Self, ErrorStack> {
|
||||
Self::ed_from_curve(Nid::from_raw(boring_sys::NID_X25519))
|
||||
}
|
||||
|
||||
/// Creates a new `KeyExchange` using a random
|
||||
/// private key for the `X448` Edwards curve
|
||||
pub fn with_x448() -> Result<Self, ErrorStack> {
|
||||
Self::ed_from_curve(Nid::from_raw(boring_sys::NID_X448))
|
||||
}
|
||||
|
||||
/// Creates a new `KeyExchange` using a random
|
||||
/// private key for `sepc256r1` curve
|
||||
/// Also known as `X9_62_PRIME256V1`
|
||||
|
|
@ -53,12 +50,6 @@ impl KeyExchange {
|
|||
Self::ec_from_curve(Nid::SECP384R1)
|
||||
}
|
||||
|
||||
/// Creates a new `KeyExchange` using a random
|
||||
/// private key for `sep521r1` curve
|
||||
pub fn with_secp521r1() -> Result<Self, ErrorStack> {
|
||||
Self::ec_from_curve(Nid::SECP521R1)
|
||||
}
|
||||
|
||||
/// Allows getting a new `KeyExchange` using Eliptic Curves
|
||||
/// on the specified curve
|
||||
fn ec_from_curve(nid: Nid) -> Result<Self, ErrorStack> {
|
||||
|
|
@ -76,6 +67,7 @@ impl KeyExchange {
|
|||
|
||||
/// Allows getting a new `KeyExchange` using Edwards Curves
|
||||
/// on the specified curve
|
||||
#[cfg(not(feature = "fips"))]
|
||||
fn ed_from_curve(nid: Nid) -> Result<Self, ErrorStack> {
|
||||
let pkey_ctx = unsafe {
|
||||
EvpPkeyCtx::from_ptr(cvt_p(boring_sys::EVP_PKEY_CTX_new_id(
|
||||
|
|
@ -105,18 +97,24 @@ impl KeyExchange {
|
|||
|
||||
/// Decodes a SPKI public key to it's raw public key component
|
||||
fn raw_public_key(pkey: &PKeyRef<Private>) -> Result<Vec<u8>, ErrorStack> {
|
||||
let spki = pkey.public_key_to_der()?;
|
||||
if pkey.id() == Id::EC {
|
||||
let ec_key = pkey.ec_key()?;
|
||||
let mut bn_ctx = boring::bn::BigNumContext::new()?;
|
||||
|
||||
// parse the key
|
||||
let pkey = spki::SubjectPublicKeyInfoRef::from_der(spki.as_ref())
|
||||
.expect("failed parsing spki bytes");
|
||||
return ec_key.public_key().to_bytes(
|
||||
ec_key.group(),
|
||||
boring::ec::PointConversionForm::UNCOMPRESSED,
|
||||
&mut bn_ctx,
|
||||
);
|
||||
}
|
||||
|
||||
// return the raw public key as a new vec
|
||||
Ok(Vec::from(
|
||||
pkey.subject_public_key
|
||||
.as_bytes()
|
||||
.expect("failed getting raw spki bytes"),
|
||||
))
|
||||
let mut output = vec![0u8; pkey.raw_public_key_len()?];
|
||||
let used_len = {
|
||||
let used = pkey.raw_public_key(&mut output)?;
|
||||
used.len()
|
||||
};
|
||||
output.truncate(used_len);
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
/// Derives a shared secret using the peer's raw public key
|
||||
|
|
@ -129,8 +127,8 @@ impl KeyExchange {
|
|||
|
||||
crate::verify::ec::create_public_key(group, point.as_ref())?
|
||||
}
|
||||
#[cfg(not(feature = "fips"))]
|
||||
DhKeyType::ED(nid) => crate::verify::ed::public_key(peer_pub_key, Nid::from_raw(*nid))?,
|
||||
_ => unimplemented!(),
|
||||
};
|
||||
|
||||
let mut deriver = boring::derive::Deriver::new(&self.own_key)?;
|
||||
|
|
@ -147,7 +145,7 @@ impl crypto::ActiveKeyExchange for KeyExchange {
|
|||
peer_pub_key: &[u8],
|
||||
) -> Result<crypto::SharedSecret, rustls::Error> {
|
||||
self.diffie_hellman(peer_pub_key)
|
||||
.map(|x| crypto::SharedSecret::from(x.as_slice()))
|
||||
.map(crypto::SharedSecret::from)
|
||||
.map_err(|e| {
|
||||
log_and_map(
|
||||
"ex::KeyExchange::diffie_hellman",
|
||||
|
|
@ -163,12 +161,11 @@ impl crypto::ActiveKeyExchange for KeyExchange {
|
|||
|
||||
fn group(&self) -> rustls::NamedGroup {
|
||||
match self.key_type {
|
||||
#[cfg(not(feature = "fips"))]
|
||||
DhKeyType::ED(boring_sys::NID_X25519) => rustls::NamedGroup::X25519,
|
||||
DhKeyType::ED(boring_sys::NID_X448) => rustls::NamedGroup::X448,
|
||||
DhKeyType::EC((_, boring_sys::NID_X9_62_prime256v1)) => rustls::NamedGroup::secp256r1,
|
||||
DhKeyType::EC((_, boring_sys::NID_secp384r1)) => rustls::NamedGroup::secp384r1,
|
||||
DhKeyType::EC((_, boring_sys::NID_secp521r1)) => rustls::NamedGroup::secp521r1,
|
||||
_ => unimplemented!(),
|
||||
_ => unreachable!("unsupported key type"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -190,6 +187,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(feature = "fips"))]
|
||||
fn test_derive_ed() {
|
||||
let alice = Box::new(KeyExchange::with_x25519().unwrap());
|
||||
let bob = KeyExchange::with_x25519().unwrap();
|
||||
|
|
|
|||
79
boring-rustls-provider/src/kx/mod.rs
Normal file
79
boring-rustls-provider/src/kx/mod.rs
Normal 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")
|
||||
}
|
||||
}
|
||||
364
boring-rustls-provider/src/kx/pq.rs
Normal file
364
boring-rustls-provider/src/kx/pq.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use helper::log_and_map;
|
||||
#[cfg(all(feature = "fips", feature = "log"))]
|
||||
use log::warn;
|
||||
use rustls::{
|
||||
crypto::{CryptoProvider, GetRandomFailed, SupportedKxGroup},
|
||||
SupportedCipherSuite,
|
||||
|
|
@ -12,7 +14,7 @@ mod hash;
|
|||
mod helper;
|
||||
mod hkdf;
|
||||
mod hmac;
|
||||
mod kx;
|
||||
pub mod kx;
|
||||
#[cfg(feature = "tls12")]
|
||||
mod prf;
|
||||
pub mod sign;
|
||||
|
|
@ -21,60 +23,80 @@ pub mod tls12;
|
|||
pub mod tls13;
|
||||
pub mod verify;
|
||||
|
||||
/// The boringssl-based Rustls Crypto provider
|
||||
pub static PROVIDER: &'static dyn CryptoProvider = &Provider;
|
||||
pub fn provider() -> CryptoProvider {
|
||||
#[cfg(feature = "fips")]
|
||||
{
|
||||
provider_with_ciphers(ALL_FIPS_CIPHER_SUITES.to_vec())
|
||||
}
|
||||
#[cfg(not(feature = "fips"))]
|
||||
{
|
||||
provider_with_ciphers(ALL_CIPHER_SUITES.to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn provider_with_ciphers(ciphers: Vec<rustls::SupportedCipherSuite>) -> CryptoProvider {
|
||||
#[cfg(feature = "fips")]
|
||||
let ciphers = {
|
||||
let original_len = ciphers.len();
|
||||
let filtered = ciphers
|
||||
.into_iter()
|
||||
.filter(|suite| ALL_FIPS_CIPHER_SUITES.contains(suite))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if filtered.len() != original_len {
|
||||
#[cfg(feature = "log")]
|
||||
warn!(
|
||||
"filtered {} non-FIPS cipher suite(s) from provider_with_ciphers",
|
||||
original_len - filtered.len()
|
||||
);
|
||||
}
|
||||
|
||||
filtered
|
||||
};
|
||||
|
||||
CryptoProvider {
|
||||
cipher_suites: ciphers,
|
||||
#[cfg(feature = "fips")]
|
||||
kx_groups: ALL_FIPS_KX_GROUPS.to_vec(),
|
||||
#[cfg(not(feature = "fips"))]
|
||||
kx_groups: ALL_KX_GROUPS.to_vec(),
|
||||
#[cfg(feature = "fips")]
|
||||
signature_verification_algorithms: verify::ALL_FIPS_ALGORITHMS,
|
||||
#[cfg(not(feature = "fips"))]
|
||||
signature_verification_algorithms: verify::ALL_ALGORITHMS,
|
||||
secure_random: &Provider,
|
||||
key_provider: &Provider,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Provider;
|
||||
|
||||
impl CryptoProvider for Provider {
|
||||
fn fill_random(&self, bytes: &mut [u8]) -> Result<(), GetRandomFailed> {
|
||||
impl rustls::crypto::SecureRandom for Provider {
|
||||
fn fill(&self, bytes: &mut [u8]) -> Result<(), rustls::crypto::GetRandomFailed> {
|
||||
boring::rand::rand_bytes(bytes).map_err(|e| log_and_map("rand_bytes", e, GetRandomFailed))
|
||||
}
|
||||
|
||||
fn default_cipher_suites(&self) -> &'static [SupportedCipherSuite] {
|
||||
#[cfg(feature = "fips-only")]
|
||||
{
|
||||
ALL_FIPS_CIPHER_SUITES
|
||||
}
|
||||
#[cfg(not(feature = "fips-only"))]
|
||||
{
|
||||
ALL_CIPHER_SUITES
|
||||
}
|
||||
}
|
||||
|
||||
fn default_kx_groups(&self) -> &'static [&'static dyn SupportedKxGroup] {
|
||||
#[cfg(feature = "fips-only")]
|
||||
{
|
||||
ALL_FIPS_KX_GROUPS
|
||||
}
|
||||
#[cfg(not(feature = "fips-only"))]
|
||||
{
|
||||
ALL_KX_GROUPS
|
||||
fn fips(&self) -> bool {
|
||||
cfg!(feature = "fips")
|
||||
}
|
||||
}
|
||||
|
||||
impl rustls::crypto::KeyProvider for Provider {
|
||||
fn load_private_key(
|
||||
&self,
|
||||
key_der: PrivateKeyDer<'static>,
|
||||
) -> Result<std::sync::Arc<dyn rustls::sign::SigningKey>, rustls::Error> {
|
||||
) -> Result<Arc<dyn rustls::sign::SigningKey>, rustls::Error> {
|
||||
sign::BoringPrivateKey::try_from(key_der).map(|x| Arc::new(x) as _)
|
||||
}
|
||||
|
||||
fn signature_verification_algorithms(&self) -> rustls::WebPkiSupportedAlgorithms {
|
||||
#[cfg(feature = "fips-only")]
|
||||
{
|
||||
verify::ALL_FIPS_ALGORITHMS
|
||||
}
|
||||
#[cfg(not(feature = "fips-only"))]
|
||||
{
|
||||
verify::ALL_ALGORITHMS
|
||||
}
|
||||
fn fips(&self) -> bool {
|
||||
cfg!(feature = "fips")
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
static ALL_FIPS_CIPHER_SUITES: &[SupportedCipherSuite] = &[
|
||||
#[cfg(feature = "fips")]
|
||||
pub static ALL_FIPS_CIPHER_SUITES: &[SupportedCipherSuite] = &[
|
||||
SupportedCipherSuite::Tls13(&tls13::AES_256_GCM_SHA384),
|
||||
SupportedCipherSuite::Tls13(&tls13::AES_128_GCM_SHA256),
|
||||
#[cfg(feature = "tls12")]
|
||||
|
|
@ -87,8 +109,8 @@ static ALL_FIPS_CIPHER_SUITES: &[SupportedCipherSuite] = &[
|
|||
SupportedCipherSuite::Tls12(&tls12::ECDHE_RSA_AES128_GCM_SHA256),
|
||||
];
|
||||
|
||||
#[allow(unused)]
|
||||
static ALL_CIPHER_SUITES: &[SupportedCipherSuite] = &[
|
||||
#[cfg(not(feature = "fips"))]
|
||||
pub static ALL_CIPHER_SUITES: &[SupportedCipherSuite] = &[
|
||||
SupportedCipherSuite::Tls13(&tls13::CHACHA20_POLY1305_SHA256),
|
||||
SupportedCipherSuite::Tls13(&tls13::AES_256_GCM_SHA384),
|
||||
SupportedCipherSuite::Tls13(&tls13::AES_128_GCM_SHA256),
|
||||
|
|
@ -106,26 +128,31 @@ static ALL_CIPHER_SUITES: &[SupportedCipherSuite] = &[
|
|||
SupportedCipherSuite::Tls12(&tls12::ECDHE_RSA_AES128_GCM_SHA256),
|
||||
];
|
||||
|
||||
/// Allowed KX curves for FIPS are recommended
|
||||
/// in [NIST SP 800-186](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-186.pdf)
|
||||
/// Allowed KX groups for FIPS per [SP 800-52r2](https://doi.org/10.6028/NIST.SP.800-52r2),
|
||||
/// aligned with boring's `fips202205` compliance policy.
|
||||
///
|
||||
/// See Sec. 3.1.2 Table 2
|
||||
/// Ordered in decending order of security strength
|
||||
#[allow(unused)]
|
||||
pub const ALL_FIPS_KX_GROUPS: &[&dyn SupportedKxGroup] = &[
|
||||
&kx::Secp521r1 as _, // P-521 in FIPS lingo
|
||||
&kx::X448 as _, // Curve448 in FIPS lingo
|
||||
&kx::Secp384r1 as _, // P-384 in FIPS lingo
|
||||
&kx::X25519 as _, // Curve25519 in FIPS lingo
|
||||
&kx::Secp256r1 as _, // P-256 in FIPS lingo
|
||||
/// The `fips` feature implies `mlkem`, so X25519MLKEM768 is always
|
||||
/// available and preferred in FIPS mode.
|
||||
#[cfg(feature = "fips")]
|
||||
pub static ALL_FIPS_KX_GROUPS: &[&dyn SupportedKxGroup] = &[
|
||||
&kx::X25519MlKem768 as _, // PQ hybrid preferred
|
||||
&kx::Secp256r1 as _, // P-256
|
||||
&kx::Secp384r1 as _, // P-384
|
||||
];
|
||||
|
||||
#[allow(unused)]
|
||||
pub const ALL_KX_GROUPS: &[&dyn SupportedKxGroup] = &[
|
||||
/// All supported KX groups, ordered by preference.
|
||||
///
|
||||
/// Matches boring's default supported group list exactly:
|
||||
/// X25519MLKEM768 (when mlkem enabled), X25519, P-256, P-384.
|
||||
#[cfg(all(not(feature = "fips"), feature = "mlkem"))]
|
||||
pub static ALL_KX_GROUPS: &[&dyn SupportedKxGroup] = &[
|
||||
&kx::X25519MlKem768 as _, // PQ hybrid preferred
|
||||
&kx::X25519 as _,
|
||||
&kx::X448 as _,
|
||||
&kx::Secp256r1 as _,
|
||||
&kx::Secp384r1 as _,
|
||||
&kx::Secp521r1 as _,
|
||||
&kx::FfDHe2048 as _,
|
||||
];
|
||||
|
||||
/// See [`ALL_KX_GROUPS`] (mlkem variant).
|
||||
#[cfg(not(any(feature = "fips", feature = "mlkem")))]
|
||||
pub static ALL_KX_GROUPS: &[&dyn SupportedKxGroup] =
|
||||
&[&kx::X25519 as _, &kx::Secp256r1 as _, &kx::Secp384r1 as _];
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ pub struct PrfTls1WithDigest(pub boring::nid::Nid);
|
|||
impl crypto::tls12::Prf for PrfTls1WithDigest {
|
||||
fn for_key_exchange(
|
||||
&self,
|
||||
output: &mut [u8],
|
||||
output: &mut [u8; 48],
|
||||
kx: Box<dyn crypto::ActiveKeyExchange>,
|
||||
peer_pub_key: &[u8],
|
||||
label: &[u8],
|
||||
|
|
@ -29,6 +29,10 @@ impl crypto::tls12::Prf for PrfTls1WithDigest {
|
|||
let digest = boring::hash::MessageDigest::from_nid(self.0).expect("failed getting digest");
|
||||
prf(digest, output, secret, label, seed).expect("failed calculating prf")
|
||||
}
|
||||
|
||||
fn fips(&self) -> bool {
|
||||
cfg!(feature = "fips")
|
||||
}
|
||||
}
|
||||
|
||||
fn prf(
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ use std::sync::Arc;
|
|||
|
||||
use boring::{
|
||||
hash::MessageDigest,
|
||||
nid::Nid,
|
||||
pkey::{Id, PKeyRef, Private},
|
||||
rsa::Padding,
|
||||
sign::{RsaPssSaltlen, Signer},
|
||||
|
|
@ -20,16 +21,28 @@ const ALL_RSA_SCHEMES: &[SignatureScheme] = &[
|
|||
SignatureScheme::RSA_PKCS1_SHA256,
|
||||
];
|
||||
|
||||
const ALL_EC_SCHEMES: &[SignatureScheme] = &[
|
||||
SignatureScheme::ECDSA_NISTP256_SHA256,
|
||||
SignatureScheme::ECDSA_NISTP384_SHA384,
|
||||
SignatureScheme::ECDSA_NISTP521_SHA512,
|
||||
];
|
||||
const EC_P256_SCHEMES: &[SignatureScheme] = &[SignatureScheme::ECDSA_NISTP256_SHA256];
|
||||
const EC_P384_SCHEMES: &[SignatureScheme] = &[SignatureScheme::ECDSA_NISTP384_SHA384];
|
||||
const EC_P521_SCHEMES: &[SignatureScheme] = &[SignatureScheme::ECDSA_NISTP521_SHA512];
|
||||
|
||||
/// An abstraction over a boringssl private key
|
||||
/// used for signing
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum EcCurve {
|
||||
P256,
|
||||
P384,
|
||||
P521,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum KeyKind {
|
||||
Rsa,
|
||||
Ec(EcCurve),
|
||||
Ed25519,
|
||||
Ed448,
|
||||
}
|
||||
|
||||
/// An abstraction over a boringssl private key used for signing.
|
||||
#[derive(Debug)]
|
||||
pub struct BoringPrivateKey(Arc<boring::pkey::PKey<Private>>, rustls::SignatureAlgorithm);
|
||||
pub struct BoringPrivateKey(Arc<boring::pkey::PKey<Private>>, KeyKind);
|
||||
|
||||
impl TryFrom<PrivateKeyDer<'static>> for BoringPrivateKey {
|
||||
type Error = rustls::Error;
|
||||
|
|
@ -37,25 +50,83 @@ impl TryFrom<PrivateKeyDer<'static>> for BoringPrivateKey {
|
|||
fn try_from(value: PrivateKeyDer<'static>) -> Result<Self, Self::Error> {
|
||||
let pkey = match value {
|
||||
PrivateKeyDer::Pkcs8(der) => {
|
||||
boring::pkey::PKey::private_key_from_pkcs8(der.secret_pkcs8_der())
|
||||
.map_err(|e| log_and_map("private_key_from_pkcs8", e, ()))
|
||||
boring::pkey::PKey::private_key_from_pkcs8(der.secret_pkcs8_der()).map_err(|e| {
|
||||
log_and_map(
|
||||
"private_key_from_pkcs8",
|
||||
e,
|
||||
rustls::Error::General("failed loading private key".into()),
|
||||
)
|
||||
})
|
||||
}
|
||||
PrivateKeyDer::Pkcs1(der) => {
|
||||
boring::pkey::PKey::private_key_from_der(der.secret_pkcs1_der())
|
||||
.map_err(|e| log_and_map("private_key_from_der", e, ()))
|
||||
boring::pkey::PKey::private_key_from_der(der.secret_pkcs1_der()).map_err(|e| {
|
||||
log_and_map(
|
||||
"private_key_from_der_pkcs1",
|
||||
e,
|
||||
rustls::Error::General("failed loading private key".into()),
|
||||
)
|
||||
})
|
||||
}
|
||||
_ => Err(()),
|
||||
PrivateKeyDer::Sec1(der) => {
|
||||
boring::pkey::PKey::private_key_from_der(der.secret_sec1_der()).map_err(|e| {
|
||||
log_and_map(
|
||||
"private_key_from_der_sec1",
|
||||
e,
|
||||
rustls::Error::General("failed loading private key".into()),
|
||||
)
|
||||
})
|
||||
}
|
||||
.map_err(|_| rustls::Error::General("failed loading private key".into()))?;
|
||||
_ => {
|
||||
return Err(rustls::Error::General(
|
||||
"unsupported private key encoding".into(),
|
||||
));
|
||||
}
|
||||
}?;
|
||||
|
||||
let sig = match pkey.id() {
|
||||
Id::RSA => rustls::SignatureAlgorithm::RSA,
|
||||
Id::EC => rustls::SignatureAlgorithm::ECDSA,
|
||||
Id::ED25519 => rustls::SignatureAlgorithm::ED25519,
|
||||
Id::ED448 => rustls::SignatureAlgorithm::ED448,
|
||||
let kind = match pkey.id() {
|
||||
Id::RSA => KeyKind::Rsa,
|
||||
Id::EC => {
|
||||
let ec_key = pkey.ec_key().map_err(|e| {
|
||||
log_and_map(
|
||||
"ec_key",
|
||||
e,
|
||||
rustls::Error::General("failed loading EC private key".into()),
|
||||
)
|
||||
})?;
|
||||
|
||||
let curve_nid = ec_key.group().curve_name().ok_or_else(|| {
|
||||
rustls::Error::General("unsupported EC key without named curve".into())
|
||||
})?;
|
||||
|
||||
let curve = match curve_nid {
|
||||
Nid::X9_62_PRIME256V1 => EcCurve::P256,
|
||||
Nid::SECP384R1 => EcCurve::P384,
|
||||
Nid::SECP521R1 => EcCurve::P521,
|
||||
_ => {
|
||||
return Err(rustls::Error::General(
|
||||
"unsupported EC private key curve".into(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
KeyKind::Ec(curve)
|
||||
}
|
||||
Id::ED25519 => KeyKind::Ed25519,
|
||||
Id::ED448 => KeyKind::Ed448,
|
||||
_ => return Err(rustls::Error::General("unsupported key format".into())),
|
||||
};
|
||||
Ok(Self(Arc::new(pkey), sig))
|
||||
|
||||
#[cfg(feature = "fips")]
|
||||
match kind {
|
||||
KeyKind::Rsa | KeyKind::Ec(EcCurve::P256 | EcCurve::P384) => {}
|
||||
KeyKind::Ec(EcCurve::P521) | KeyKind::Ed25519 | KeyKind::Ed448 => {
|
||||
return Err(rustls::Error::General(
|
||||
"key type is not allowed in FIPS mode".into(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self(Arc::new(pkey), kind))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -63,75 +134,105 @@ fn rsa_signer_from_params(
|
|||
key: &PKeyRef<Private>,
|
||||
digest: MessageDigest,
|
||||
padding: Padding,
|
||||
) -> Signer {
|
||||
let mut signer = Signer::new(digest, key).expect("failed getting signer");
|
||||
signer
|
||||
.set_rsa_padding(padding)
|
||||
.expect("failed setting padding");
|
||||
) -> Result<Signer<'_>, rustls::Error> {
|
||||
let mut signer = Signer::new(digest, key).map_err(|e| {
|
||||
log_and_map(
|
||||
"Signer::new",
|
||||
e,
|
||||
rustls::Error::General("failed preparing signer".into()),
|
||||
)
|
||||
})?;
|
||||
|
||||
signer.set_rsa_padding(padding).map_err(|e| {
|
||||
log_and_map(
|
||||
"set_rsa_padding",
|
||||
e,
|
||||
rustls::Error::General("failed preparing signer".into()),
|
||||
)
|
||||
})?;
|
||||
|
||||
if padding == Padding::PKCS1_PSS {
|
||||
signer
|
||||
.set_rsa_pss_saltlen(RsaPssSaltlen::DIGEST_LENGTH)
|
||||
.expect("failed setting rsa_pss salt lengths");
|
||||
signer
|
||||
.set_rsa_mgf1_md(digest)
|
||||
.expect("failed setting mgf1 digest");
|
||||
.map_err(|e| {
|
||||
log_and_map(
|
||||
"set_rsa_pss_saltlen",
|
||||
e,
|
||||
rustls::Error::General("failed preparing signer".into()),
|
||||
)
|
||||
})?;
|
||||
|
||||
signer.set_rsa_mgf1_md(digest).map_err(|e| {
|
||||
log_and_map(
|
||||
"set_rsa_mgf1_md",
|
||||
e,
|
||||
rustls::Error::General("failed preparing signer".into()),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
|
||||
signer
|
||||
Ok(signer)
|
||||
}
|
||||
|
||||
fn ec_signer_from_params(key: &PKeyRef<Private>, digest: MessageDigest) -> Signer {
|
||||
let signer = Signer::new(digest, key).expect("failed getting signer");
|
||||
signer
|
||||
fn ec_signer_from_params(
|
||||
key: &PKeyRef<Private>,
|
||||
digest: MessageDigest,
|
||||
) -> Result<Signer<'_>, rustls::Error> {
|
||||
Signer::new(digest, key).map_err(|e| {
|
||||
log_and_map(
|
||||
"Signer::new",
|
||||
e,
|
||||
rustls::Error::General("failed preparing signer".into()),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
impl BoringPrivateKey {}
|
||||
|
||||
impl SigningKey for BoringPrivateKey {
|
||||
fn choose_scheme(
|
||||
&self,
|
||||
offered: &[rustls::SignatureScheme],
|
||||
) -> Option<Box<dyn rustls::sign::Signer>> {
|
||||
match self.1 {
|
||||
rustls::SignatureAlgorithm::RSA => ALL_RSA_SCHEMES
|
||||
let scheme = match self.1 {
|
||||
KeyKind::Rsa => ALL_RSA_SCHEMES
|
||||
.iter()
|
||||
.find(|scheme| offered.contains(scheme))
|
||||
.map(|&scheme| Box::new(BoringSigner(self.0.clone(), scheme)) as _),
|
||||
rustls::SignatureAlgorithm::ECDSA => ALL_EC_SCHEMES
|
||||
.find(|scheme| offered.contains(scheme)),
|
||||
KeyKind::Ec(EcCurve::P256) => EC_P256_SCHEMES
|
||||
.iter()
|
||||
.find(|scheme| offered.contains(scheme))
|
||||
.map(|&scheme| Box::new(BoringSigner(self.0.clone(), scheme)) as _),
|
||||
rustls::SignatureAlgorithm::ED25519
|
||||
if offered.contains(&rustls::SignatureScheme::ED25519) =>
|
||||
{
|
||||
Some(Box::new(BoringSigner(
|
||||
self.0.clone(),
|
||||
rustls::SignatureScheme::ED25519,
|
||||
)))
|
||||
.find(|scheme| offered.contains(scheme)),
|
||||
KeyKind::Ec(EcCurve::P384) => EC_P384_SCHEMES
|
||||
.iter()
|
||||
.find(|scheme| offered.contains(scheme)),
|
||||
KeyKind::Ec(EcCurve::P521) => EC_P521_SCHEMES
|
||||
.iter()
|
||||
.find(|scheme| offered.contains(scheme)),
|
||||
KeyKind::Ed25519 if offered.contains(&rustls::SignatureScheme::ED25519) => {
|
||||
Some(&rustls::SignatureScheme::ED25519)
|
||||
}
|
||||
rustls::SignatureAlgorithm::ED448
|
||||
if offered.contains(&rustls::SignatureScheme::ED448) =>
|
||||
{
|
||||
Some(Box::new(BoringSigner(
|
||||
self.0.clone(),
|
||||
rustls::SignatureScheme::ED448,
|
||||
)))
|
||||
KeyKind::Ed448 if offered.contains(&rustls::SignatureScheme::ED448) => {
|
||||
Some(&rustls::SignatureScheme::ED448)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}?;
|
||||
|
||||
Some(Box::new(BoringSigner(self.0.clone(), *scheme)))
|
||||
}
|
||||
|
||||
fn algorithm(&self) -> rustls::SignatureAlgorithm {
|
||||
self.1
|
||||
match self.1 {
|
||||
KeyKind::Rsa => rustls::SignatureAlgorithm::RSA,
|
||||
KeyKind::Ec(_) => rustls::SignatureAlgorithm::ECDSA,
|
||||
KeyKind::Ed25519 => rustls::SignatureAlgorithm::ED25519,
|
||||
KeyKind::Ed448 => rustls::SignatureAlgorithm::ED448,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A boringssl-based Signer
|
||||
/// A boringssl-based Signer.
|
||||
#[derive(Debug)]
|
||||
pub struct BoringSigner(Arc<boring::pkey::PKey<Private>>, rustls::SignatureScheme);
|
||||
|
||||
impl BoringSigner {
|
||||
fn get_signer(&self) -> Signer {
|
||||
fn get_signer(&self) -> Result<Signer<'_>, rustls::Error> {
|
||||
match self.1 {
|
||||
SignatureScheme::RSA_PKCS1_SHA256 => {
|
||||
rsa_signer_from_params(self.0.as_ref(), MessageDigest::sha256(), Padding::PKCS1)
|
||||
|
|
@ -142,7 +243,6 @@ impl BoringSigner {
|
|||
SignatureScheme::RSA_PKCS1_SHA512 => {
|
||||
rsa_signer_from_params(self.0.as_ref(), MessageDigest::sha512(), Padding::PKCS1)
|
||||
}
|
||||
|
||||
SignatureScheme::RSA_PSS_SHA256 => {
|
||||
rsa_signer_from_params(self.0.as_ref(), MessageDigest::sha256(), Padding::PKCS1_PSS)
|
||||
}
|
||||
|
|
@ -152,7 +252,6 @@ impl BoringSigner {
|
|||
SignatureScheme::RSA_PSS_SHA512 => {
|
||||
rsa_signer_from_params(self.0.as_ref(), MessageDigest::sha512(), Padding::PKCS1_PSS)
|
||||
}
|
||||
|
||||
SignatureScheme::ECDSA_NISTP256_SHA256 => {
|
||||
ec_signer_from_params(self.0.as_ref(), MessageDigest::sha256())
|
||||
}
|
||||
|
|
@ -162,19 +261,25 @@ impl BoringSigner {
|
|||
SignatureScheme::ECDSA_NISTP521_SHA512 => {
|
||||
ec_signer_from_params(self.0.as_ref(), MessageDigest::sha512())
|
||||
}
|
||||
|
||||
SignatureScheme::ED25519 | SignatureScheme::ED448 => {
|
||||
Signer::new_without_digest(self.0.as_ref()).expect("failed getting signer")
|
||||
Signer::new_without_digest(self.0.as_ref()).map_err(|e| {
|
||||
log_and_map(
|
||||
"Signer::new_without_digest",
|
||||
e,
|
||||
rustls::Error::General("failed preparing signer".into()),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
_ => unimplemented!(),
|
||||
_ => Err(rustls::Error::General(
|
||||
"unsupported signature scheme for private key".into(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl rustls::sign::Signer for BoringSigner {
|
||||
fn sign(&self, message: &[u8]) -> Result<Vec<u8>, rustls::Error> {
|
||||
let mut signer = self.get_signer();
|
||||
let mut signer = self.get_signer()?;
|
||||
let max_sig_len = signer
|
||||
.len()
|
||||
.map_err(|e| log_and_map("len", e, rustls::Error::General("failed signing".into())))?;
|
||||
|
|
@ -195,3 +300,75 @@ impl rustls::sign::Signer for BoringSigner {
|
|||
self.1
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use boring::{
|
||||
ec::{EcGroup, EcKey},
|
||||
nid::Nid,
|
||||
pkey::{PKey, Private},
|
||||
rsa::Rsa,
|
||||
};
|
||||
use rustls::sign::SigningKey;
|
||||
use rustls::{SignatureAlgorithm, SignatureScheme};
|
||||
use rustls_pki_types::{PrivateKeyDer, PrivatePkcs8KeyDer, PrivateSec1KeyDer};
|
||||
|
||||
use super::BoringPrivateKey;
|
||||
|
||||
fn p256_private_key() -> PKey<Private> {
|
||||
let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap();
|
||||
let ec_key = EcKey::generate(&group).unwrap();
|
||||
PKey::from_ec_key(ec_key).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn loads_sec1_ec_private_key() {
|
||||
let pkey = p256_private_key();
|
||||
let sec1_der = pkey.private_key_to_der().unwrap();
|
||||
let key_der = PrivateKeyDer::Sec1(PrivateSec1KeyDer::from(sec1_der));
|
||||
|
||||
let key = BoringPrivateKey::try_from(key_der).expect("SEC1 private key should load");
|
||||
|
||||
assert_eq!(key.algorithm(), SignatureAlgorithm::ECDSA);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn p256_key_chooses_only_p256_scheme() {
|
||||
let pkey = p256_private_key();
|
||||
let pkcs8_der = pkey.private_key_to_der_pkcs8().unwrap();
|
||||
let key_der = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(pkcs8_der));
|
||||
|
||||
let key = BoringPrivateKey::try_from(key_der).expect("P-256 private key should load");
|
||||
|
||||
let offered = [
|
||||
SignatureScheme::ECDSA_NISTP384_SHA384,
|
||||
SignatureScheme::ECDSA_NISTP256_SHA256,
|
||||
];
|
||||
let signer = key
|
||||
.choose_scheme(&offered)
|
||||
.expect("P-256 key should select P-256 scheme");
|
||||
assert_eq!(signer.scheme(), SignatureScheme::ECDSA_NISTP256_SHA256);
|
||||
|
||||
assert!(key
|
||||
.choose_scheme(&[SignatureScheme::ECDSA_NISTP384_SHA384])
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rsa_key_prefers_pss_when_available() {
|
||||
let rsa = Rsa::generate(2048).unwrap();
|
||||
let pkey = PKey::from_rsa(rsa).unwrap();
|
||||
let pkcs8_der = pkey.private_key_to_der_pkcs8().unwrap();
|
||||
let key_der = PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(pkcs8_der));
|
||||
|
||||
let key = BoringPrivateKey::try_from(key_der).expect("RSA private key should load");
|
||||
let signer = key
|
||||
.choose_scheme(&[
|
||||
SignatureScheme::RSA_PKCS1_SHA256,
|
||||
SignatureScheme::RSA_PSS_SHA256,
|
||||
])
|
||||
.expect("RSA key should select an offered scheme");
|
||||
|
||||
assert_eq!(signer.scheme(), SignatureScheme::RSA_PSS_SHA256);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,11 +23,10 @@ const PRF_SHA256: prf::PrfTls1WithDigest = prf::PrfTls1WithDigest(boring::nid::N
|
|||
const PRF_SHA384: prf::PrfTls1WithDigest = prf::PrfTls1WithDigest(boring::nid::Nid::SHA384);
|
||||
|
||||
pub static ECDHE_ECDSA_AES128_GCM_SHA256: Tls12CipherSuite = Tls12CipherSuite {
|
||||
common: rustls::CipherSuiteCommon {
|
||||
common: rustls::crypto::CipherSuiteCommon {
|
||||
suite: rustls::CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
hash_provider: hash::SHA256,
|
||||
confidentiality_limit: 1 << 23,
|
||||
integrity_limit: 1 << 52,
|
||||
},
|
||||
aead_alg: &aead::Aead::<aead::aes::Aes128>::DEFAULT,
|
||||
prf_provider: &PRF_SHA256,
|
||||
|
|
@ -36,11 +35,10 @@ pub static ECDHE_ECDSA_AES128_GCM_SHA256: Tls12CipherSuite = Tls12CipherSuite {
|
|||
};
|
||||
|
||||
pub static ECDHE_RSA_AES128_GCM_SHA256: Tls12CipherSuite = Tls12CipherSuite {
|
||||
common: rustls::CipherSuiteCommon {
|
||||
common: rustls::crypto::CipherSuiteCommon {
|
||||
suite: rustls::CipherSuite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
hash_provider: hash::SHA256,
|
||||
confidentiality_limit: 1 << 23,
|
||||
integrity_limit: 1 << 52,
|
||||
},
|
||||
aead_alg: &aead::Aead::<aead::aes::Aes128>::DEFAULT,
|
||||
prf_provider: &PRF_SHA256,
|
||||
|
|
@ -49,11 +47,10 @@ pub static ECDHE_RSA_AES128_GCM_SHA256: Tls12CipherSuite = Tls12CipherSuite {
|
|||
};
|
||||
|
||||
pub static ECDHE_ECDSA_AES256_GCM_SHA384: Tls12CipherSuite = Tls12CipherSuite {
|
||||
common: rustls::CipherSuiteCommon {
|
||||
common: rustls::crypto::CipherSuiteCommon {
|
||||
suite: rustls::CipherSuite::TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
hash_provider: hash::SHA384,
|
||||
confidentiality_limit: 1 << 23,
|
||||
integrity_limit: 1 << 52,
|
||||
},
|
||||
aead_alg: &aead::Aead::<aead::aes::Aes256>::DEFAULT,
|
||||
prf_provider: &PRF_SHA384,
|
||||
|
|
@ -62,11 +59,10 @@ pub static ECDHE_ECDSA_AES256_GCM_SHA384: Tls12CipherSuite = Tls12CipherSuite {
|
|||
};
|
||||
|
||||
pub static ECDHE_RSA_AES256_GCM_SHA384: Tls12CipherSuite = Tls12CipherSuite {
|
||||
common: rustls::CipherSuiteCommon {
|
||||
common: rustls::crypto::CipherSuiteCommon {
|
||||
suite: rustls::CipherSuite::TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
hash_provider: hash::SHA384,
|
||||
confidentiality_limit: 1 << 23,
|
||||
integrity_limit: 1 << 52,
|
||||
},
|
||||
aead_alg: &aead::Aead::<aead::aes::Aes256>::DEFAULT,
|
||||
prf_provider: &PRF_SHA384,
|
||||
|
|
@ -75,11 +71,10 @@ pub static ECDHE_RSA_AES256_GCM_SHA384: Tls12CipherSuite = Tls12CipherSuite {
|
|||
};
|
||||
|
||||
pub static ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256: Tls12CipherSuite = Tls12CipherSuite {
|
||||
common: rustls::CipherSuiteCommon {
|
||||
common: rustls::crypto::CipherSuiteCommon {
|
||||
suite: rustls::CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
hash_provider: hash::SHA256,
|
||||
confidentiality_limit: u64::MAX,
|
||||
integrity_limit: 1 << 36,
|
||||
},
|
||||
aead_alg: &aead::Aead::<aead::chacha20::ChaCha20Poly1305>::DEFAULT,
|
||||
prf_provider: &PRF_SHA256,
|
||||
|
|
@ -88,11 +83,10 @@ pub static ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256: Tls12CipherSuite = Tls12Ci
|
|||
};
|
||||
|
||||
pub static ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256: Tls12CipherSuite = Tls12CipherSuite {
|
||||
common: rustls::CipherSuiteCommon {
|
||||
common: rustls::crypto::CipherSuiteCommon {
|
||||
suite: rustls::CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
hash_provider: hash::SHA256,
|
||||
confidentiality_limit: u64::MAX,
|
||||
integrity_limit: 1 << 36,
|
||||
},
|
||||
aead_alg: &aead::Aead::<aead::chacha20::ChaCha20Poly1305>::DEFAULT,
|
||||
prf_provider: &PRF_SHA256,
|
||||
|
|
|
|||
|
|
@ -3,11 +3,10 @@ use rustls::Tls13CipherSuite;
|
|||
use crate::{aead, hash, hkdf};
|
||||
|
||||
pub static AES_128_GCM_SHA256: Tls13CipherSuite = Tls13CipherSuite {
|
||||
common: rustls::CipherSuiteCommon {
|
||||
common: rustls::crypto::CipherSuiteCommon {
|
||||
suite: rustls::CipherSuite::TLS13_AES_128_GCM_SHA256,
|
||||
hash_provider: hash::SHA256,
|
||||
confidentiality_limit: 1 << 23,
|
||||
integrity_limit: 1 << 52,
|
||||
},
|
||||
hkdf_provider: &hkdf::Hkdf::<hkdf::Sha256>::DEFAULT,
|
||||
aead_alg: &aead::Aead::<aead::aes::Aes128>::DEFAULT,
|
||||
|
|
@ -15,11 +14,10 @@ pub static AES_128_GCM_SHA256: Tls13CipherSuite = Tls13CipherSuite {
|
|||
};
|
||||
|
||||
pub static AES_256_GCM_SHA384: Tls13CipherSuite = Tls13CipherSuite {
|
||||
common: rustls::CipherSuiteCommon {
|
||||
common: rustls::crypto::CipherSuiteCommon {
|
||||
suite: rustls::CipherSuite::TLS13_AES_256_GCM_SHA384,
|
||||
hash_provider: hash::SHA384,
|
||||
confidentiality_limit: 1 << 23,
|
||||
integrity_limit: 1 << 52,
|
||||
},
|
||||
hkdf_provider: &hkdf::Hkdf::<hkdf::Sha384>::DEFAULT,
|
||||
aead_alg: &aead::Aead::<aead::aes::Aes256>::DEFAULT,
|
||||
|
|
@ -27,11 +25,10 @@ pub static AES_256_GCM_SHA384: Tls13CipherSuite = Tls13CipherSuite {
|
|||
};
|
||||
|
||||
pub static CHACHA20_POLY1305_SHA256: Tls13CipherSuite = Tls13CipherSuite {
|
||||
common: rustls::CipherSuiteCommon {
|
||||
common: rustls::crypto::CipherSuiteCommon {
|
||||
suite: rustls::CipherSuite::TLS13_CHACHA20_POLY1305_SHA256,
|
||||
hash_provider: hash::SHA256,
|
||||
confidentiality_limit: u64::MAX,
|
||||
integrity_limit: 1 << 36,
|
||||
},
|
||||
|
||||
hkdf_provider: &hkdf::Hkdf::<hkdf::Sha256>::DEFAULT,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use rustls::{SignatureScheme, WebPkiSupportedAlgorithms};
|
||||
use rustls::{crypto::WebPkiSupportedAlgorithms, SignatureScheme};
|
||||
|
||||
pub(crate) mod ec;
|
||||
pub(crate) mod ed;
|
||||
|
|
@ -61,6 +61,13 @@ pub static ALL_ALGORITHMS: WebPkiSupportedAlgorithms = WebPkiSupportedAlgorithms
|
|||
],
|
||||
};
|
||||
|
||||
/// FIPS-approved signature verification algorithms per SP 800-52r2.
|
||||
///
|
||||
/// Aligned with boring's `fips202205` compliance policy:
|
||||
/// - RSA: PKCS#1 v1.5 and PSS with SHA-256/384/512
|
||||
/// - ECDSA: P-256 with SHA-256 and P-384 with SHA-384 only
|
||||
/// (SP 800-52r2 Table 4.1: "The curve should be P-256 or P-384")
|
||||
/// - No P-521, Ed25519, or Ed448
|
||||
#[allow(unused)]
|
||||
pub static ALL_FIPS_ALGORITHMS: WebPkiSupportedAlgorithms = WebPkiSupportedAlgorithms {
|
||||
all: &[
|
||||
|
|
@ -72,9 +79,6 @@ pub static ALL_FIPS_ALGORITHMS: WebPkiSupportedAlgorithms = WebPkiSupportedAlgor
|
|||
&rsa::BoringRsaVerifier::RSA_PSS_SHA512,
|
||||
&ec::BoringEcVerifier::ECDSA_NISTP256_SHA256,
|
||||
&ec::BoringEcVerifier::ECDSA_NISTP384_SHA384,
|
||||
&ec::BoringEcVerifier::ECDSA_NISTP521_SHA512,
|
||||
//&ed::BoringEdVerifier::ED25519, // FIPS 186-5: requires SHA512 but boring doesn't want us to set a digest, correct?
|
||||
//&ed::BoringEdVerifier::ED448, // FIPS 186-5: requires SHAKE256 but boring doesn't want us to set a digest, correct?
|
||||
],
|
||||
mapping: &[
|
||||
(
|
||||
|
|
@ -109,11 +113,5 @@ pub static ALL_FIPS_ALGORITHMS: WebPkiSupportedAlgorithms = WebPkiSupportedAlgor
|
|||
SignatureScheme::ECDSA_NISTP384_SHA384,
|
||||
&[&ec::BoringEcVerifier::ECDSA_NISTP384_SHA384],
|
||||
),
|
||||
(
|
||||
SignatureScheme::ECDSA_NISTP521_SHA512,
|
||||
&[&ec::BoringEcVerifier::ECDSA_NISTP521_SHA512],
|
||||
),
|
||||
// (SignatureScheme::ED25519, &[&ed::BoringEdVerifier::ED25519]),
|
||||
// (SignatureScheme::ED448, &[&ed::BoringEdVerifier::ED448]),
|
||||
],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
use boring::{error::ErrorStack, hash::MessageDigest};
|
||||
use rustls::SignatureScheme;
|
||||
use rustls::{pki_types::alg_id, SignatureScheme};
|
||||
use rustls_pki_types::{InvalidSignature, SignatureVerificationAlgorithm};
|
||||
|
||||
use crate::helper;
|
||||
|
|
@ -40,7 +40,7 @@ impl SignatureVerificationAlgorithm for BoringEcVerifier {
|
|||
ec_verifier_from_params(public_key.as_ref(), MessageDigest::sha512())
|
||||
}
|
||||
|
||||
_ => unimplemented!(),
|
||||
_ => return Err(InvalidSignature),
|
||||
}
|
||||
.map_err(|e| helper::log_and_map("ec_verifier_from_params", e, InvalidSignature))?;
|
||||
|
||||
|
|
@ -52,8 +52,8 @@ impl SignatureVerificationAlgorithm for BoringEcVerifier {
|
|||
|
||||
fn public_key_alg_id(&self) -> rustls_pki_types::AlgorithmIdentifier {
|
||||
match self.0 {
|
||||
SignatureScheme::ECDSA_NISTP256_SHA256 => webpki::alg_id::ECDSA_P256,
|
||||
SignatureScheme::ECDSA_NISTP384_SHA384 => webpki::alg_id::ECDSA_P384,
|
||||
SignatureScheme::ECDSA_NISTP256_SHA256 => alg_id::ECDSA_P256,
|
||||
SignatureScheme::ECDSA_NISTP384_SHA384 => alg_id::ECDSA_P384,
|
||||
SignatureScheme::ECDSA_NISTP521_SHA512 => {
|
||||
// See rfc5480 appendix-A (secp521r1): 1.3.132.0.35
|
||||
rustls_pki_types::AlgorithmIdentifier::from_slice(&[
|
||||
|
|
@ -61,29 +61,33 @@ impl SignatureVerificationAlgorithm for BoringEcVerifier {
|
|||
0x04, 0x00, 0x23,
|
||||
])
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
_ => unreachable!("BoringEcVerifier only supports configured ECDSA schemes"),
|
||||
}
|
||||
}
|
||||
|
||||
fn signature_alg_id(&self) -> rustls_pki_types::AlgorithmIdentifier {
|
||||
match self.0 {
|
||||
SignatureScheme::ECDSA_NISTP256_SHA256 => webpki::alg_id::ECDSA_SHA256,
|
||||
SignatureScheme::ECDSA_NISTP384_SHA384 => webpki::alg_id::ECDSA_SHA384,
|
||||
SignatureScheme::ECDSA_NISTP256_SHA256 => alg_id::ECDSA_SHA256,
|
||||
SignatureScheme::ECDSA_NISTP384_SHA384 => alg_id::ECDSA_SHA384,
|
||||
SignatureScheme::ECDSA_NISTP521_SHA512 => {
|
||||
// See rfc5480 appendix-A (ecdsa-with-SHA512): 1.2.840.10045.4.3.4
|
||||
rustls_pki_types::AlgorithmIdentifier::from_slice(&[
|
||||
0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x04,
|
||||
])
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
_ => unreachable!("BoringEcVerifier only supports configured ECDSA schemes"),
|
||||
}
|
||||
}
|
||||
|
||||
fn fips(&self) -> bool {
|
||||
cfg!(feature = "fips") && !matches!(self.0, SignatureScheme::ECDSA_NISTP521_SHA512)
|
||||
}
|
||||
}
|
||||
|
||||
fn ec_verifier_from_params(
|
||||
key: &boring::pkey::PKeyRef<boring::pkey::Public>,
|
||||
digest: MessageDigest,
|
||||
) -> Result<boring::sign::Verifier, ErrorStack> {
|
||||
) -> Result<boring::sign::Verifier<'_>, ErrorStack> {
|
||||
boring::sign::Verifier::new(digest, key)
|
||||
}
|
||||
|
||||
|
|
@ -92,7 +96,7 @@ fn group_for_scheme(scheme: SignatureScheme) -> Result<boring::ec::EcGroup, Erro
|
|||
SignatureScheme::ECDSA_NISTP256_SHA256 => boring::nid::Nid::X9_62_PRIME256V1,
|
||||
SignatureScheme::ECDSA_NISTP384_SHA384 => boring::nid::Nid::SECP384R1,
|
||||
SignatureScheme::ECDSA_NISTP521_SHA512 => boring::nid::Nid::SECP521R1,
|
||||
_ => unimplemented!(),
|
||||
_ => unreachable!("BoringEcVerifier only supports configured ECDSA schemes"),
|
||||
};
|
||||
boring::ec::EcGroup::from_curve_name(nid)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use std::ptr;
|
|||
|
||||
use boring::error::ErrorStack;
|
||||
use foreign_types::ForeignType;
|
||||
use rustls::SignatureScheme;
|
||||
use rustls::{pki_types::alg_id, SignatureScheme};
|
||||
use rustls_pki_types::{InvalidSignature, SignatureVerificationAlgorithm};
|
||||
|
||||
use crate::helper::{cvt_p, log_and_map};
|
||||
|
|
@ -41,19 +41,23 @@ impl SignatureVerificationAlgorithm for BoringEdVerifier {
|
|||
|
||||
fn signature_alg_id(&self) -> rustls_pki_types::AlgorithmIdentifier {
|
||||
match self.0 {
|
||||
SignatureScheme::ED25519 => webpki::alg_id::ED25519,
|
||||
SignatureScheme::ED25519 => alg_id::ED25519,
|
||||
SignatureScheme::ED448 => {
|
||||
// rfc8410#section-3: 1.3.101.113: -> DER: 06 03 2B 65 71
|
||||
rustls_pki_types::AlgorithmIdentifier::from_slice(&[0x06, 0x03, 0x2B, 0x65, 0x71])
|
||||
}
|
||||
_ => unimplemented!(),
|
||||
_ => unreachable!("BoringEdVerifier only supports configured EdDSA schemes"),
|
||||
}
|
||||
}
|
||||
|
||||
fn fips(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn ed_verifier_from_params(
|
||||
key: &boring::pkey::PKeyRef<boring::pkey::Public>,
|
||||
) -> Result<boring::sign::Verifier, ErrorStack> {
|
||||
) -> Result<boring::sign::Verifier<'_>, ErrorStack> {
|
||||
boring::sign::Verifier::new_without_digest(key)
|
||||
}
|
||||
|
||||
|
|
@ -64,7 +68,7 @@ fn ed_public_key_for_scheme(
|
|||
let nid = boring::nid::Nid::from_raw(match scheme {
|
||||
SignatureScheme::ED25519 => boring_sys::EVP_PKEY_ED25519,
|
||||
SignatureScheme::ED448 => boring_sys::EVP_PKEY_ED448,
|
||||
_ => unimplemented!(),
|
||||
_ => return Err(ErrorStack::get()),
|
||||
});
|
||||
public_key(spki_spk, nid)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use boring::{
|
|||
rsa::{Padding, Rsa},
|
||||
sign::RsaPssSaltlen,
|
||||
};
|
||||
use rustls::SignatureScheme;
|
||||
use rustls::{pki_types::alg_id, SignatureScheme};
|
||||
use rustls_pki_types::{InvalidSignature, SignatureVerificationAlgorithm};
|
||||
use spki::der::Reader;
|
||||
|
||||
|
|
@ -64,8 +64,8 @@ impl SignatureVerificationAlgorithm for BoringRsaVerifier {
|
|||
Padding::PKCS1_PSS,
|
||||
),
|
||||
|
||||
_ => unimplemented!(),
|
||||
};
|
||||
_ => return Err(InvalidSignature),
|
||||
}?;
|
||||
verifier.verify_oneshot(signature, message).map_or_else(
|
||||
|_| Err(InvalidSignature),
|
||||
|res| if res { Ok(()) } else { Err(InvalidSignature) },
|
||||
|
|
@ -73,43 +73,48 @@ impl SignatureVerificationAlgorithm for BoringRsaVerifier {
|
|||
}
|
||||
|
||||
fn public_key_alg_id(&self) -> rustls_pki_types::AlgorithmIdentifier {
|
||||
webpki::alg_id::RSA_ENCRYPTION
|
||||
alg_id::RSA_ENCRYPTION
|
||||
}
|
||||
|
||||
fn signature_alg_id(&self) -> rustls_pki_types::AlgorithmIdentifier {
|
||||
match self.0 {
|
||||
SignatureScheme::RSA_PKCS1_SHA256 => webpki::alg_id::RSA_PKCS1_SHA256,
|
||||
SignatureScheme::RSA_PKCS1_SHA384 => webpki::alg_id::RSA_PKCS1_SHA384,
|
||||
SignatureScheme::RSA_PKCS1_SHA512 => webpki::alg_id::RSA_PKCS1_SHA512,
|
||||
SignatureScheme::RSA_PKCS1_SHA256 => alg_id::RSA_PKCS1_SHA256,
|
||||
SignatureScheme::RSA_PKCS1_SHA384 => alg_id::RSA_PKCS1_SHA384,
|
||||
SignatureScheme::RSA_PKCS1_SHA512 => alg_id::RSA_PKCS1_SHA512,
|
||||
|
||||
SignatureScheme::RSA_PSS_SHA256 => webpki::alg_id::RSA_PSS_SHA256,
|
||||
SignatureScheme::RSA_PSS_SHA384 => webpki::alg_id::RSA_PSS_SHA384,
|
||||
SignatureScheme::RSA_PSS_SHA512 => webpki::alg_id::RSA_PSS_SHA512,
|
||||
SignatureScheme::RSA_PSS_SHA256 => alg_id::RSA_PSS_SHA256,
|
||||
SignatureScheme::RSA_PSS_SHA384 => alg_id::RSA_PSS_SHA384,
|
||||
SignatureScheme::RSA_PSS_SHA512 => alg_id::RSA_PSS_SHA512,
|
||||
|
||||
_ => unimplemented!(),
|
||||
_ => unreachable!("BoringRsaVerifier only supports configured RSA schemes"),
|
||||
}
|
||||
}
|
||||
|
||||
fn fips(&self) -> bool {
|
||||
cfg!(feature = "fips")
|
||||
}
|
||||
}
|
||||
|
||||
fn rsa_verifier_from_params(
|
||||
key: &boring::pkey::PKeyRef<boring::pkey::Public>,
|
||||
digest: MessageDigest,
|
||||
padding: Padding,
|
||||
) -> boring::sign::Verifier {
|
||||
let mut verifier = boring::sign::Verifier::new(digest, key).expect("failed getting verifier");
|
||||
) -> Result<boring::sign::Verifier<'_>, InvalidSignature> {
|
||||
let mut verifier = boring::sign::Verifier::new(digest, key)
|
||||
.map_err(|e| log_and_map("Verifier::new", e, InvalidSignature))?;
|
||||
verifier
|
||||
.set_rsa_padding(padding)
|
||||
.expect("failed setting padding");
|
||||
.map_err(|e| log_and_map("set_rsa_padding", e, InvalidSignature))?;
|
||||
if padding == Padding::PKCS1_PSS {
|
||||
verifier
|
||||
.set_rsa_pss_saltlen(RsaPssSaltlen::DIGEST_LENGTH)
|
||||
.expect("failed setting rsa_pss salt lengths");
|
||||
.map_err(|e| log_and_map("set_rsa_pss_saltlen", e, InvalidSignature))?;
|
||||
verifier
|
||||
.set_rsa_mgf1_md(digest)
|
||||
.expect("failed setting mgf1 digest");
|
||||
.map_err(|e| log_and_map("set_rsa_mgf1_md", e, InvalidSignature))?;
|
||||
}
|
||||
|
||||
verifier
|
||||
Ok(verifier)
|
||||
}
|
||||
|
||||
pub(crate) fn decode_spki_spk(
|
||||
|
|
|
|||
|
|
@ -5,15 +5,71 @@ use tokio::{
|
|||
net::TcpStream,
|
||||
};
|
||||
|
||||
use boring_rustls_provider::{tls12, tls13, PROVIDER};
|
||||
use rustls::{
|
||||
version::{TLS12, TLS13},
|
||||
ClientConfig, ServerConfig, SupportedCipherSuite,
|
||||
};
|
||||
use boring_rustls_provider::tls13;
|
||||
#[cfg(feature = "tls12")]
|
||||
use rustls::version::TLS12;
|
||||
use rustls::{version::TLS13, ClientConfig, ServerConfig, SupportedCipherSuite};
|
||||
use rustls_pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
|
||||
use tokio::net::TcpListener;
|
||||
use tokio_rustls::{TlsAcceptor, TlsConnector};
|
||||
|
||||
fn tls13_provider_suites() -> Vec<SupportedCipherSuite> {
|
||||
boring_rustls_provider::provider()
|
||||
.cipher_suites
|
||||
.into_iter()
|
||||
.filter(|suite| matches!(suite, SupportedCipherSuite::Tls13(_)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(feature = "tls12")]
|
||||
fn tls12_provider_suites_for_ecdsa() -> Vec<SupportedCipherSuite> {
|
||||
boring_rustls_provider::provider()
|
||||
.cipher_suites
|
||||
.into_iter()
|
||||
.filter(|suite| {
|
||||
let SupportedCipherSuite::Tls12(suite) = suite else {
|
||||
return false;
|
||||
};
|
||||
|
||||
suite.sign.iter().any(|scheme| {
|
||||
matches!(
|
||||
scheme,
|
||||
rustls::SignatureScheme::ECDSA_NISTP256_SHA256
|
||||
| rustls::SignatureScheme::ECDSA_NISTP384_SHA384
|
||||
| rustls::SignatureScheme::ECDSA_NISTP521_SHA512
|
||||
| rustls::SignatureScheme::ED25519
|
||||
| rustls::SignatureScheme::ED448
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(feature = "tls12")]
|
||||
fn tls12_provider_suites_for_rsa() -> Vec<SupportedCipherSuite> {
|
||||
boring_rustls_provider::provider()
|
||||
.cipher_suites
|
||||
.into_iter()
|
||||
.filter(|suite| {
|
||||
let SupportedCipherSuite::Tls12(suite) = suite else {
|
||||
return false;
|
||||
};
|
||||
|
||||
suite.sign.iter().any(|scheme| {
|
||||
matches!(
|
||||
scheme,
|
||||
rustls::SignatureScheme::RSA_PKCS1_SHA256
|
||||
| rustls::SignatureScheme::RSA_PKCS1_SHA384
|
||||
| rustls::SignatureScheme::RSA_PKCS1_SHA512
|
||||
| rustls::SignatureScheme::RSA_PSS_SHA256
|
||||
| rustls::SignatureScheme::RSA_PSS_SHA384
|
||||
| rustls::SignatureScheme::RSA_PSS_SHA512
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_tls13_crypto() {
|
||||
let pki = TestPki::new(&rcgen::PKCS_ECDSA_P256_SHA256);
|
||||
|
|
@ -21,16 +77,12 @@ async fn test_tls13_crypto() {
|
|||
let root_store = pki.client_root_store();
|
||||
let server_config = pki.server_config();
|
||||
|
||||
let ciphers = [
|
||||
SupportedCipherSuite::Tls13(&tls13::AES_128_GCM_SHA256),
|
||||
SupportedCipherSuite::Tls13(&tls13::AES_256_GCM_SHA384),
|
||||
SupportedCipherSuite::Tls13(&tls13::CHACHA20_POLY1305_SHA256),
|
||||
];
|
||||
let ciphers = tls13_provider_suites();
|
||||
|
||||
for cipher in ciphers {
|
||||
let config = ClientConfig::builder_with_provider(PROVIDER)
|
||||
.with_cipher_suites(&[cipher])
|
||||
.with_safe_default_kx_groups()
|
||||
let config = ClientConfig::builder_with_provider(Arc::new(
|
||||
boring_rustls_provider::provider_with_ciphers([cipher].to_vec()),
|
||||
))
|
||||
.with_protocol_versions(&[&TLS13])
|
||||
.unwrap()
|
||||
.with_root_certificates(root_store.clone())
|
||||
|
|
@ -40,6 +92,389 @@ async fn test_tls13_crypto() {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_kx_groups_reject_invalid_peer_keys_without_panicking() {
|
||||
for group in boring_rustls_provider::provider().kx_groups {
|
||||
let kx = group.start().expect("provider KX group should initialize");
|
||||
|
||||
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| kx.complete(&[])));
|
||||
assert!(outcome.is_ok(), "KX group {:?} panicked", group.name());
|
||||
assert!(
|
||||
outcome.expect("already checked for panic").is_err(),
|
||||
"KX group {:?} accepted an invalid key share",
|
||||
group.name()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_verifiers_reject_malformed_inputs_without_panicking() {
|
||||
let provider = boring_rustls_provider::provider();
|
||||
|
||||
for (index, verifier) in provider
|
||||
.signature_verification_algorithms
|
||||
.all
|
||||
.iter()
|
||||
.enumerate()
|
||||
{
|
||||
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
verifier.verify_signature(&[], b"message", &[])
|
||||
}));
|
||||
|
||||
assert!(outcome.is_ok(), "verifier #{index} panicked");
|
||||
assert!(
|
||||
outcome.expect("already checked for panic").is_err(),
|
||||
"verifier #{index} accepted malformed inputs"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Self-to-self TLS 1.3 handshake using only the X25519MLKEM768 PQ hybrid group.
|
||||
#[cfg(feature = "mlkem")]
|
||||
#[tokio::test]
|
||||
async fn test_tls13_pq_x25519_mlkem768() {
|
||||
use rustls::NamedGroup;
|
||||
|
||||
let pki = TestPki::new(&rcgen::PKCS_ECDSA_P256_SHA256);
|
||||
|
||||
let root_store = pki.client_root_store();
|
||||
|
||||
// Build server with only X25519MLKEM768
|
||||
let server_provider =
|
||||
boring_rustls_provider::provider_with_ciphers(vec![SupportedCipherSuite::Tls13(
|
||||
&tls13::AES_256_GCM_SHA384,
|
||||
)]);
|
||||
let server_config = {
|
||||
let mut cfg = ServerConfig::builder_with_provider(Arc::new(server_provider))
|
||||
.with_protocol_versions(&[&TLS13])
|
||||
.unwrap()
|
||||
.with_no_client_auth()
|
||||
.with_single_cert(
|
||||
vec![pki.server_cert_der.clone()],
|
||||
pki.server_key_der.clone_key(),
|
||||
)
|
||||
.unwrap();
|
||||
cfg.key_log = Arc::new(rustls::KeyLogFile::new());
|
||||
Arc::new(cfg)
|
||||
};
|
||||
|
||||
// Build client with only X25519MLKEM768
|
||||
let client_provider =
|
||||
boring_rustls_provider::provider_with_ciphers(vec![SupportedCipherSuite::Tls13(
|
||||
&tls13::AES_256_GCM_SHA384,
|
||||
)]);
|
||||
let config = ClientConfig::builder_with_provider(Arc::new(client_provider))
|
||||
.with_protocol_versions(&[&TLS13])
|
||||
.unwrap()
|
||||
.with_root_certificates(root_store)
|
||||
.with_no_client_auth();
|
||||
|
||||
let negotiated_group = do_exchange(config, server_config).await;
|
||||
assert_eq!(negotiated_group, Some(NamedGroup::X25519MLKEM768));
|
||||
}
|
||||
|
||||
/// Connect to Cloudflare's PQ test endpoint and verify X25519MLKEM768
|
||||
/// was actually negotiated by checking the `/cdn-cgi/trace` response.
|
||||
/// Marked `#[ignore]` because it depends on an external service.
|
||||
#[cfg(feature = "mlkem")]
|
||||
#[ignore]
|
||||
#[tokio::test]
|
||||
async fn test_pq_interop_cloudflare() {
|
||||
let mut root_store = rustls::RootCertStore::empty();
|
||||
root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
|
||||
|
||||
let provider = boring_rustls_provider::provider();
|
||||
let config = ClientConfig::builder_with_provider(Arc::new(provider))
|
||||
.with_protocol_versions(&[&TLS13])
|
||||
.unwrap()
|
||||
.with_root_certificates(root_store)
|
||||
.with_no_client_auth();
|
||||
|
||||
let connector = TlsConnector::from(Arc::new(config));
|
||||
let stream = TcpStream::connect("pq.cloudflareresearch.com:443")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut stream = connector
|
||||
.connect(
|
||||
rustls_pki_types::ServerName::try_from("pq.cloudflareresearch.com").unwrap(),
|
||||
stream,
|
||||
)
|
||||
.await
|
||||
.expect("TLS handshake with pq.cloudflareresearch.com failed");
|
||||
|
||||
// Hit the trace endpoint which reports negotiated TLS parameters
|
||||
stream
|
||||
.write_all(
|
||||
b"GET /cdn-cgi/trace HTTP/1.1\r\nHost: pq.cloudflareresearch.com\r\nConnection: close\r\n\r\n",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut buf = Vec::new();
|
||||
stream.read_to_end(&mut buf).await.unwrap();
|
||||
let response = String::from_utf8_lossy(&buf);
|
||||
|
||||
// Verify TLS 1.3 was used
|
||||
assert!(
|
||||
response.contains("tls=TLSv1.3"),
|
||||
"expected TLSv1.3, got: {response}"
|
||||
);
|
||||
|
||||
// Verify X25519MLKEM768 was negotiated as the key exchange
|
||||
assert!(
|
||||
response.contains("kex=X25519MLKEM768"),
|
||||
"expected kex=X25519MLKEM768, got: {response}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Connect to Cloudflare with TLS 1.2 forced and verify that a classical
|
||||
/// key exchange is used (PQ groups are TLS 1.3 only).
|
||||
/// Marked `#[ignore]` because it depends on an external service.
|
||||
#[cfg(all(feature = "mlkem", feature = "tls12"))]
|
||||
#[ignore]
|
||||
#[tokio::test]
|
||||
async fn test_tls12_interop_cloudflare_no_pq() {
|
||||
let mut root_store = rustls::RootCertStore::empty();
|
||||
root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
|
||||
|
||||
let provider = boring_rustls_provider::provider();
|
||||
let config = ClientConfig::builder_with_provider(Arc::new(provider))
|
||||
.with_protocol_versions(&[&TLS12])
|
||||
.unwrap()
|
||||
.with_root_certificates(root_store)
|
||||
.with_no_client_auth();
|
||||
|
||||
let connector = TlsConnector::from(Arc::new(config));
|
||||
let stream = TcpStream::connect("pq.cloudflareresearch.com:443")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut stream = connector
|
||||
.connect(
|
||||
rustls_pki_types::ServerName::try_from("pq.cloudflareresearch.com").unwrap(),
|
||||
stream,
|
||||
)
|
||||
.await
|
||||
.expect("TLS handshake with pq.cloudflareresearch.com (TLS 1.2) failed");
|
||||
|
||||
stream
|
||||
.write_all(
|
||||
b"GET /cdn-cgi/trace HTTP/1.1\r\nHost: pq.cloudflareresearch.com\r\nConnection: close\r\n\r\n",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut buf = Vec::new();
|
||||
stream.read_to_end(&mut buf).await.unwrap();
|
||||
let response = String::from_utf8_lossy(&buf);
|
||||
|
||||
// Verify TLS 1.2 was used
|
||||
assert!(
|
||||
response.contains("tls=TLSv1.2"),
|
||||
"expected TLSv1.2, got: {response}"
|
||||
);
|
||||
|
||||
// Verify a classical key exchange was used (not PQ)
|
||||
assert!(
|
||||
!response.contains("kex=X25519MLKEM768"),
|
||||
"TLS 1.2 should not use PQ key exchange, got: {response}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "fips")]
|
||||
fn is_fips_enabled() {
|
||||
assert!(boring::fips::enabled());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "fips")]
|
||||
fn fips_provider_excludes_chacha20_cipher_suites() {
|
||||
use rustls::CipherSuite;
|
||||
|
||||
let provider = boring_rustls_provider::provider();
|
||||
let disallowed = [
|
||||
CipherSuite::TLS13_CHACHA20_POLY1305_SHA256,
|
||||
CipherSuite::TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
CipherSuite::TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
|
||||
];
|
||||
|
||||
for suite in provider.cipher_suites {
|
||||
let selected = suite.suite();
|
||||
assert!(
|
||||
!disallowed.contains(&selected),
|
||||
"FIPS provider exposed disallowed cipher suite: {selected:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "fips")]
|
||||
fn fips_provider_with_ciphers_filters_non_fips_input() {
|
||||
use rustls::CipherSuite;
|
||||
|
||||
let provider = boring_rustls_provider::provider_with_ciphers(vec![
|
||||
SupportedCipherSuite::Tls13(&tls13::CHACHA20_POLY1305_SHA256),
|
||||
SupportedCipherSuite::Tls13(&tls13::AES_128_GCM_SHA256),
|
||||
]);
|
||||
|
||||
let suites = provider
|
||||
.cipher_suites
|
||||
.iter()
|
||||
.map(|suite| suite.suite())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(suites, vec![CipherSuite::TLS13_AES_128_GCM_SHA256]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "fips")]
|
||||
fn fips_provider_restricts_kx_groups() {
|
||||
use rustls::NamedGroup;
|
||||
|
||||
let provider = boring_rustls_provider::provider();
|
||||
let groups = provider
|
||||
.kx_groups
|
||||
.iter()
|
||||
.map(|group| group.name())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// fips implies mlkem, so X25519MLKEM768 must be present and preferred
|
||||
assert_eq!(
|
||||
groups[0],
|
||||
NamedGroup::X25519MLKEM768,
|
||||
"X25519MLKEM768 should be the first (preferred) FIPS KX group"
|
||||
);
|
||||
assert!(groups.contains(&NamedGroup::secp256r1));
|
||||
assert!(groups.contains(&NamedGroup::secp384r1));
|
||||
for group in &groups {
|
||||
assert!(
|
||||
matches!(
|
||||
group,
|
||||
NamedGroup::X25519MLKEM768 | NamedGroup::secp256r1 | NamedGroup::secp384r1
|
||||
),
|
||||
"FIPS provider exposed disallowed KX group: {group:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "fips")]
|
||||
fn fips_provider_excludes_disallowed_signature_schemes() {
|
||||
use rustls::SignatureScheme;
|
||||
|
||||
let provider = boring_rustls_provider::provider();
|
||||
let schemes = provider
|
||||
.signature_verification_algorithms
|
||||
.mapping
|
||||
.iter()
|
||||
.map(|(scheme, _)| *scheme)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert!(schemes.contains(&SignatureScheme::RSA_PSS_SHA256));
|
||||
assert!(schemes.contains(&SignatureScheme::ECDSA_NISTP256_SHA256));
|
||||
|
||||
for disallowed in [
|
||||
SignatureScheme::ECDSA_NISTP521_SHA512,
|
||||
SignatureScheme::ED25519,
|
||||
SignatureScheme::ED448,
|
||||
] {
|
||||
assert!(
|
||||
!schemes.contains(&disallowed),
|
||||
"FIPS provider exposed disallowed signature scheme: {disallowed:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(feature = "fips"))]
|
||||
fn is_fips_disabled() {
|
||||
assert!(!boring::fips::enabled());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(feature = "fips"))]
|
||||
fn non_fips_provider_keeps_non_fips_algorithms() {
|
||||
use rustls::{CipherSuite, NamedGroup};
|
||||
|
||||
let provider = boring_rustls_provider::provider();
|
||||
|
||||
assert!(provider
|
||||
.cipher_suites
|
||||
.iter()
|
||||
.any(|suite| { suite.suite() == CipherSuite::TLS13_CHACHA20_POLY1305_SHA256 }));
|
||||
|
||||
assert!(provider
|
||||
.kx_groups
|
||||
.iter()
|
||||
.any(|group| group.name() == NamedGroup::X25519));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(feature = "fips"))]
|
||||
fn non_fips_provider_components_report_non_fips() {
|
||||
let provider = boring_rustls_provider::provider();
|
||||
|
||||
assert!(!provider.secure_random.fips());
|
||||
assert!(!provider.key_provider.fips());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(feature = "fips"))]
|
||||
fn non_fips_provider_with_ciphers_keeps_requested_suites() {
|
||||
use rustls::CipherSuite;
|
||||
|
||||
let provider = boring_rustls_provider::provider_with_ciphers(vec![
|
||||
SupportedCipherSuite::Tls13(&tls13::CHACHA20_POLY1305_SHA256),
|
||||
SupportedCipherSuite::Tls13(&tls13::AES_128_GCM_SHA256),
|
||||
]);
|
||||
|
||||
let suites = provider
|
||||
.cipher_suites
|
||||
.iter()
|
||||
.map(|suite| suite.suite())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
suites,
|
||||
vec![
|
||||
CipherSuite::TLS13_CHACHA20_POLY1305_SHA256,
|
||||
CipherSuite::TLS13_AES_128_GCM_SHA256,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(all(not(feature = "fips"), feature = "mlkem"))]
|
||||
fn non_fips_provider_includes_pq_group() {
|
||||
use rustls::NamedGroup;
|
||||
|
||||
let provider = boring_rustls_provider::provider();
|
||||
let groups = provider
|
||||
.kx_groups
|
||||
.iter()
|
||||
.map(|group| group.name())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(
|
||||
groups[0],
|
||||
NamedGroup::X25519MLKEM768,
|
||||
"X25519MLKEM768 should be the first (preferred) KX group"
|
||||
);
|
||||
assert_eq!(
|
||||
groups[1],
|
||||
NamedGroup::X25519,
|
||||
"X25519 should be the first classical fallback"
|
||||
);
|
||||
assert_eq!(
|
||||
groups[2],
|
||||
NamedGroup::secp256r1,
|
||||
"P-256 should follow X25519, matching boring's default order"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "tls12")]
|
||||
#[tokio::test]
|
||||
async fn test_tls12_ec_crypto() {
|
||||
let pki = TestPki::new(&rcgen::PKCS_ECDSA_P256_SHA256);
|
||||
|
|
@ -47,16 +482,12 @@ async fn test_tls12_ec_crypto() {
|
|||
let root_store = pki.client_root_store();
|
||||
let server_config = pki.server_config();
|
||||
|
||||
let ciphers = [
|
||||
SupportedCipherSuite::Tls12(&tls12::ECDHE_ECDSA_AES128_GCM_SHA256),
|
||||
SupportedCipherSuite::Tls12(&tls12::ECDHE_ECDSA_AES256_GCM_SHA384),
|
||||
SupportedCipherSuite::Tls12(&tls12::ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256),
|
||||
];
|
||||
let ciphers = tls12_provider_suites_for_ecdsa();
|
||||
|
||||
for cipher in ciphers {
|
||||
let config = ClientConfig::builder_with_provider(PROVIDER)
|
||||
.with_cipher_suites(&[cipher])
|
||||
.with_safe_default_kx_groups()
|
||||
let config = ClientConfig::builder_with_provider(Arc::new(
|
||||
boring_rustls_provider::provider_with_ciphers([cipher].to_vec()),
|
||||
))
|
||||
.with_protocol_versions(&[&TLS12])
|
||||
.unwrap()
|
||||
.with_root_certificates(root_store.clone())
|
||||
|
|
@ -66,6 +497,7 @@ async fn test_tls12_ec_crypto() {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "tls12")]
|
||||
#[tokio::test]
|
||||
async fn test_tls12_rsa_crypto() {
|
||||
let pki = TestPki::new(&rcgen::PKCS_RSA_SHA256);
|
||||
|
|
@ -73,16 +505,12 @@ async fn test_tls12_rsa_crypto() {
|
|||
let root_store = pki.client_root_store();
|
||||
let server_config = pki.server_config();
|
||||
|
||||
let ciphers = [
|
||||
SupportedCipherSuite::Tls12(&tls12::ECDHE_RSA_AES128_GCM_SHA256),
|
||||
SupportedCipherSuite::Tls12(&tls12::ECDHE_RSA_AES256_GCM_SHA384),
|
||||
SupportedCipherSuite::Tls12(&tls12::ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256),
|
||||
];
|
||||
let ciphers = tls12_provider_suites_for_rsa();
|
||||
|
||||
for cipher in ciphers {
|
||||
let config = ClientConfig::builder_with_provider(PROVIDER)
|
||||
.with_cipher_suites(&[cipher])
|
||||
.with_safe_default_kx_groups()
|
||||
let config = ClientConfig::builder_with_provider(Arc::new(
|
||||
boring_rustls_provider::provider_with_ciphers([cipher].to_vec()),
|
||||
))
|
||||
.with_protocol_versions(&[&TLS12])
|
||||
.unwrap()
|
||||
.with_root_certificates(root_store.clone())
|
||||
|
|
@ -96,7 +524,10 @@ async fn new_listener() -> TcpListener {
|
|||
TcpListener::bind("localhost:0").await.unwrap()
|
||||
}
|
||||
|
||||
async fn do_exchange(config: ClientConfig, server_config: Arc<ServerConfig>) {
|
||||
async fn do_exchange(
|
||||
config: ClientConfig,
|
||||
server_config: Arc<ServerConfig>,
|
||||
) -> Option<rustls::NamedGroup> {
|
||||
let listener = new_listener().await;
|
||||
let addr = listener.local_addr().unwrap();
|
||||
tokio::spawn(spawn_echo_server(listener, server_config.clone()));
|
||||
|
|
@ -112,10 +543,18 @@ async fn do_exchange(config: ClientConfig, server_config: Arc<ServerConfig>) {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
let negotiated_group = stream
|
||||
.get_ref()
|
||||
.1
|
||||
.negotiated_key_exchange_group()
|
||||
.map(|group| group.name());
|
||||
|
||||
stream.write_all(b"HELLO").await.unwrap();
|
||||
let mut buf = Vec::new();
|
||||
let bytes = stream.read_to_end(&mut buf).await.unwrap();
|
||||
assert_eq!(&buf[..bytes], b"HELLO");
|
||||
|
||||
negotiated_group
|
||||
}
|
||||
|
||||
async fn spawn_echo_server(listener: TcpListener, config: Arc<ServerConfig>) {
|
||||
|
|
@ -176,8 +615,15 @@ impl TestPki {
|
|||
}
|
||||
|
||||
fn server_config(self) -> Arc<ServerConfig> {
|
||||
let mut server_config = ServerConfig::builder_with_provider(PROVIDER)
|
||||
.with_safe_defaults()
|
||||
#[cfg(feature = "tls12")]
|
||||
let versions: &[&'static rustls::SupportedProtocolVersion] = &[&TLS12, &TLS13];
|
||||
#[cfg(not(feature = "tls12"))]
|
||||
let versions: &[&'static rustls::SupportedProtocolVersion] = &[&TLS13];
|
||||
|
||||
let mut server_config =
|
||||
ServerConfig::builder_with_provider(Arc::new(boring_rustls_provider::provider()))
|
||||
.with_protocol_versions(versions)
|
||||
.unwrap()
|
||||
.with_no_client_auth()
|
||||
.with_single_cert(vec![self.server_cert_der], self.server_key_der)
|
||||
.unwrap();
|
||||
|
|
|
|||
244
boring-rustls-provider/tests/panic_surface.rs
Normal file
244
boring-rustls-provider/tests/panic_surface.rs
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
const BANNED_TOKENS: &[&str] = &[
|
||||
"unwrap(",
|
||||
"expect(",
|
||||
"assert!(",
|
||||
"assert_eq!(",
|
||||
"assert_ne!(",
|
||||
"panic!(",
|
||||
"unreachable!(",
|
||||
"unimplemented!(",
|
||||
];
|
||||
|
||||
struct AllowlistedPanic {
|
||||
path_suffix: &'static str,
|
||||
line_fragment: &'static str,
|
||||
reason: &'static str,
|
||||
}
|
||||
|
||||
const ALLOWLIST: &[AllowlistedPanic] = &[
|
||||
AllowlistedPanic {
|
||||
path_suffix: "boring-rustls-provider/src/hash.rs",
|
||||
line_fragment: "failed getting hash digest",
|
||||
reason: "rustls hash trait is infallible; provider currently cannot surface this as Result",
|
||||
},
|
||||
AllowlistedPanic {
|
||||
path_suffix: "boring-rustls-provider/src/hash.rs",
|
||||
line_fragment: "failed getting hasher",
|
||||
reason: "rustls hash trait is infallible; provider currently cannot surface this as Result",
|
||||
},
|
||||
AllowlistedPanic {
|
||||
path_suffix: "boring-rustls-provider/src/hash.rs",
|
||||
line_fragment: "hash::Hash is only instantiated with SHA-2 digests",
|
||||
reason: "constructor invariant over static hash-provider constants",
|
||||
},
|
||||
AllowlistedPanic {
|
||||
path_suffix: "boring-rustls-provider/src/hash.rs",
|
||||
line_fragment: "failed getting digest",
|
||||
reason: "rustls hash trait is infallible; provider currently cannot surface this as Result",
|
||||
},
|
||||
AllowlistedPanic {
|
||||
path_suffix: "boring-rustls-provider/src/hash.rs",
|
||||
line_fragment: "failed finishing hash",
|
||||
reason: "rustls hash trait is infallible; provider currently cannot surface this as Result",
|
||||
},
|
||||
AllowlistedPanic {
|
||||
path_suffix: "boring-rustls-provider/src/hash.rs",
|
||||
line_fragment: "failed adding data to hash",
|
||||
reason: "rustls hash trait is infallible; provider currently cannot surface this as Result",
|
||||
},
|
||||
AllowlistedPanic {
|
||||
path_suffix: "boring-rustls-provider/src/hkdf.rs",
|
||||
line_fragment: "HKDF_extract failed",
|
||||
reason: "rustls hkdf trait is infallible at this call site",
|
||||
},
|
||||
AllowlistedPanic {
|
||||
path_suffix: "boring-rustls-provider/src/hkdf.rs",
|
||||
line_fragment: "HMAC failed",
|
||||
reason: "rustls hkdf trait is infallible at this call site",
|
||||
},
|
||||
AllowlistedPanic {
|
||||
path_suffix: "boring-rustls-provider/src/hkdf.rs",
|
||||
line_fragment: "failed hkdf expand",
|
||||
reason: "expand_block API is infallible in rustls",
|
||||
},
|
||||
AllowlistedPanic {
|
||||
path_suffix: "boring-rustls-provider/src/hmac.rs",
|
||||
line_fragment: "failed getting digest",
|
||||
reason: "rustls hmac trait is infallible at this call site",
|
||||
},
|
||||
AllowlistedPanic {
|
||||
path_suffix: "boring-rustls-provider/src/hmac.rs",
|
||||
line_fragment: "failed initializing hmac",
|
||||
reason: "rustls hmac trait is infallible at this call site",
|
||||
},
|
||||
AllowlistedPanic {
|
||||
path_suffix: "boring-rustls-provider/src/hmac.rs",
|
||||
line_fragment: "failed updating hmac",
|
||||
reason: "rustls hmac trait is infallible at this call site",
|
||||
},
|
||||
AllowlistedPanic {
|
||||
path_suffix: "boring-rustls-provider/src/hmac.rs",
|
||||
line_fragment: "failed hmac final",
|
||||
reason: "rustls hmac trait is infallible at this call site",
|
||||
},
|
||||
AllowlistedPanic {
|
||||
path_suffix: "boring-rustls-provider/src/hmac.rs",
|
||||
line_fragment: "failed creating HMAC_CTX",
|
||||
reason: "rustls hmac trait is infallible at this call site",
|
||||
},
|
||||
AllowlistedPanic {
|
||||
path_suffix: "boring-rustls-provider/src/prf.rs",
|
||||
line_fragment: "failed getting digest",
|
||||
reason: "rustls tls12::Prf::for_secret is infallible",
|
||||
},
|
||||
AllowlistedPanic {
|
||||
path_suffix: "boring-rustls-provider/src/prf.rs",
|
||||
line_fragment: "failed calculating prf",
|
||||
reason: "rustls tls12::Prf::for_secret is infallible",
|
||||
},
|
||||
AllowlistedPanic {
|
||||
path_suffix: "boring-rustls-provider/src/verify/rsa.rs",
|
||||
line_fragment: "BoringRsaVerifier only supports configured RSA schemes",
|
||||
reason: "static verifier configuration invariant",
|
||||
},
|
||||
AllowlistedPanic {
|
||||
path_suffix: "boring-rustls-provider/src/verify/ec.rs",
|
||||
line_fragment: "BoringEcVerifier only supports configured ECDSA schemes",
|
||||
reason: "static verifier configuration invariant",
|
||||
},
|
||||
AllowlistedPanic {
|
||||
path_suffix: "boring-rustls-provider/src/verify/ed.rs",
|
||||
line_fragment: "BoringEdVerifier only supports configured EdDSA schemes",
|
||||
reason: "static verifier configuration invariant",
|
||||
},
|
||||
AllowlistedPanic {
|
||||
path_suffix: "boring-rustls-provider/src/kx/ex.rs",
|
||||
line_fragment: "unsupported key type",
|
||||
reason: "static KX type invariant",
|
||||
},
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn no_unreviewed_runtime_panic_constructs() {
|
||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let repo_root = manifest_dir
|
||||
.parent()
|
||||
.expect("crate must be within repository root");
|
||||
|
||||
let mut violations = Vec::new();
|
||||
for root in [
|
||||
repo_root.join("boring-rustls-provider/src"),
|
||||
repo_root.join("boring-additions/src"),
|
||||
] {
|
||||
collect_rs_files(&root)
|
||||
.expect("must be able to enumerate source files")
|
||||
.into_iter()
|
||||
.for_each(|path| {
|
||||
let rel = path
|
||||
.strip_prefix(repo_root)
|
||||
.expect("path should be under repo root")
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
let content = fs::read_to_string(&path)
|
||||
.unwrap_or_else(|e| panic!("failed to read {}: {e}", rel));
|
||||
|
||||
for (line_no, line) in runtime_lines_only(&content) {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.starts_with("//") {
|
||||
continue;
|
||||
}
|
||||
|
||||
for token in BANNED_TOKENS {
|
||||
if !line.contains(token) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let allowed = ALLOWLIST.iter().find(|allow| {
|
||||
rel.ends_with(allow.path_suffix) && line.contains(allow.line_fragment)
|
||||
});
|
||||
if allowed.is_none() {
|
||||
violations.push(format!("{rel}:{line_no}: {trimmed}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if !violations.is_empty() {
|
||||
violations.sort();
|
||||
panic!(
|
||||
"found unreviewed panic constructs in runtime code:\n{}\n\nIf intentional, add a targeted allowlist entry with rationale.",
|
||||
violations.join("\n")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn allowlist_entries_have_matching_runtime_lines() {
|
||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let repo_root = manifest_dir
|
||||
.parent()
|
||||
.expect("crate must be within repository root");
|
||||
|
||||
let mut missing = Vec::new();
|
||||
for entry in ALLOWLIST {
|
||||
let path = repo_root.join(entry.path_suffix);
|
||||
let content = fs::read_to_string(&path)
|
||||
.unwrap_or_else(|e| panic!("failed to read {}: {e}", entry.path_suffix));
|
||||
let found = runtime_lines_only(&content)
|
||||
.into_iter()
|
||||
.any(|(_, line)| line.contains(entry.line_fragment));
|
||||
if !found {
|
||||
missing.push(format!(
|
||||
"{} :: '{}' ({})",
|
||||
entry.path_suffix, entry.line_fragment, entry.reason
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if !missing.is_empty() {
|
||||
missing.sort();
|
||||
panic!(
|
||||
"panic allowlist entries no longer match runtime code:\n{}",
|
||||
missing.join("\n")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn runtime_lines_only(content: &str) -> Vec<(usize, &str)> {
|
||||
let mut lines = Vec::new();
|
||||
for (index, line) in content.lines().enumerate() {
|
||||
if line.trim_start().starts_with("#[cfg(test)]") {
|
||||
break;
|
||||
}
|
||||
lines.push((index + 1, line));
|
||||
}
|
||||
lines
|
||||
}
|
||||
|
||||
fn collect_rs_files(root: &Path) -> Result<Vec<PathBuf>, std::io::Error> {
|
||||
let mut files = Vec::new();
|
||||
collect_rs_files_rec(root, &mut files)?;
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
fn collect_rs_files_rec(root: &Path, acc: &mut Vec<PathBuf>) -> Result<(), std::io::Error> {
|
||||
for entry in fs::read_dir(root)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
collect_rs_files_rec(&path, acc)?;
|
||||
continue;
|
||||
}
|
||||
|
||||
if path.extension().and_then(|ext| ext.to_str()) == Some("rs") {
|
||||
acc.push(path);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
use std::ffi;
|
||||
|
||||
extern "C" {
|
||||
/// Calculates `out_len` bytes of the TLS PDF, using `digest`, and
|
||||
/// Calculates `out_len` bytes of the TLS PRF, using `digest`, and
|
||||
/// writes them to `out`. It returns one on success and zero on error.
|
||||
///
|
||||
/// This isn't part of the public headers in `BoringSSL` but it is exported
|
||||
|
|
|
|||
|
|
@ -7,15 +7,7 @@ description = "Boring Rustls provider example code and tests."
|
|||
publish = false
|
||||
|
||||
[dependencies]
|
||||
docopt = "~1.1"
|
||||
env_logger = "0.10"
|
||||
log = { version = "0.4.4" }
|
||||
mio = { version = "0.8", features = ["net", "os-poll"] }
|
||||
pki-types = { package = "rustls-pki-types", version = "0.2" }
|
||||
rcgen = { version = "0.11.3", features = ["pem"], default-features = false }
|
||||
rustls = { workspace = true, features = [ "logging" ]}
|
||||
env_logger = "0.11"
|
||||
rustls = { workspace = true, features = [ "logging", "std" ]}
|
||||
boring-rustls-provider = { path = "../boring-rustls-provider", features = ["logging"] }
|
||||
rustls-pemfile = { workspace = true }
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
webpki-roots = { workspace = true }
|
||||
|
|
|
|||
|
|
@ -2,22 +2,24 @@ use std::io::{stdout, Read, Write};
|
|||
use std::net::TcpStream;
|
||||
use std::sync::Arc;
|
||||
|
||||
use boring_rustls_provider::PROVIDER;
|
||||
|
||||
fn main() {
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
env_logger::init();
|
||||
|
||||
let mut root_store = rustls::RootCertStore::empty();
|
||||
root_store.extend(webpki_roots::TLS_SERVER_ROOTS.iter().cloned());
|
||||
|
||||
let config = rustls::ClientConfig::builder_with_provider(PROVIDER)
|
||||
.with_safe_defaults()
|
||||
let config =
|
||||
rustls::ClientConfig::builder_with_provider(boring_rustls_provider::provider().into())
|
||||
.with_safe_default_protocol_versions()
|
||||
.map_err(|_| std::io::Error::other("failed selecting protocol versions"))?
|
||||
.with_root_certificates(root_store)
|
||||
.with_no_client_auth();
|
||||
|
||||
let server_name = "www.rust-lang.org".try_into().unwrap();
|
||||
let mut conn = rustls::ClientConnection::new(Arc::new(config), server_name).unwrap();
|
||||
let mut sock = TcpStream::connect("www.rust-lang.org:443").unwrap();
|
||||
let server_name = "www.rust-lang.org"
|
||||
.try_into()
|
||||
.map_err(|_| std::io::Error::other("invalid server name"))?;
|
||||
let mut conn = rustls::ClientConnection::new(Arc::new(config), server_name)?;
|
||||
let mut sock = TcpStream::connect("www.rust-lang.org:443")?;
|
||||
let mut tls = rustls::Stream::new(&mut conn, &mut sock);
|
||||
tls.write_all(
|
||||
concat!(
|
||||
|
|
@ -28,16 +30,19 @@ fn main() {
|
|||
"\r\n"
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
let ciphersuite = tls.conn.negotiated_cipher_suite().unwrap();
|
||||
)?;
|
||||
let ciphersuite = tls
|
||||
.conn
|
||||
.negotiated_cipher_suite()
|
||||
.ok_or_else(|| std::io::Error::other("no negotiated ciphersuite"))?;
|
||||
writeln!(
|
||||
&mut std::io::stderr(),
|
||||
"Current ciphersuite: {:?}",
|
||||
ciphersuite.suite()
|
||||
)
|
||||
.unwrap();
|
||||
)?;
|
||||
let mut plaintext = Vec::new();
|
||||
tls.read_to_end(&mut plaintext).unwrap();
|
||||
stdout().write_all(&plaintext).unwrap();
|
||||
tls.read_to_end(&mut plaintext)?;
|
||||
stdout().write_all(&plaintext)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue