Test FFDHE support against OpenSSL

This commit adds a new test crate `openssl-tests` that includes tests of
FFDHE kx and validation of baked-in FFDHE parameters
This commit is contained in:
Arash Sahebolamri 2024-02-06 11:40:00 -08:00 committed by Joe Birr-Pixton
parent c8c56a7aef
commit 8c29d91ed3
10 changed files with 604 additions and 1 deletions

View File

@ -390,3 +390,24 @@ jobs:
- name: run cargo-check-external-types for rustls/
working-directory: rustls/
run: cargo check-external-types
openssl-tests:
name: Run openssl-tests
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Install stable toolchain
uses: dtolnay/rust-toolchain@stable
- name: openssl version
run: openssl version
- name: cargo test (in openssl-tests/)
working-directory: openssl-tests/
run: cargo test --locked -- --include-ignored
env:
RUST_BACKTRACE: 1

99
Cargo.lock generated
View File

@ -115,6 +115,26 @@ version = "1.0.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca"
[[package]]
name = "asn1"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae3ecbce89a22627b5e8e6e11d69715617138290289e385cde773b1fe50befdb"
dependencies = [
"asn1_derive",
]
[[package]]
name = "asn1_derive"
version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "861af988fac460ac69a09f41e6217a8fb9178797b76fcc9478444be6a59be19c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
]
[[package]]
name = "async-attributes"
version = "1.1.2"
@ -883,6 +903,21 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
version = "1.2.1"
@ -1588,6 +1623,44 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "openssl"
version = "0.10.62"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cde4d2d9200ad5909f8dac647e29482e07c3a35de8a13fce7c9c7747ad9f671"
dependencies = [
"bitflags 2.4.2",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.48",
]
[[package]]
name = "openssl-sys"
version = "0.9.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1665caf8ab2dc9aef43d1c0023bd904633a6a05cb30b0ad59bec2ae986e57a7"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "p256"
version = "0.13.2"
@ -1711,6 +1784,12 @@ dependencies = [
"spki",
]
[[package]]
name = "pkg-config"
version = "0.3.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69d3587f8a9e599cc7ec2c00e331f71c4e69a5f9a4b8a6efd5b07466b9736f9a"
[[package]]
name = "platforms"
version = "3.3.0"
@ -2105,6 +2184,20 @@ dependencies = [
"webpki-roots 0.26.0",
]
[[package]]
name = "rustls-openssl-tests"
version = "0.0.1"
dependencies = [
"asn1",
"base64",
"num-bigint",
"once_cell",
"openssl",
"rustls 0.23.0-alpha.0",
"rustls-pemfile 2.0.0",
"rustls-pki-types",
]
[[package]]
name = "rustls-pemfile"
version = "1.0.4"
@ -2589,6 +2682,12 @@ version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cdbaf5e132e593e9fc1de6a15bbec912395b11fb9719e061cf64f804524c503"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.4"

View File

@ -2,6 +2,8 @@
members = [
# CI benchmarks
"ci-bench",
# Tests that require OpenSSL
"openssl-tests",
# Network-based tests
"connect-tests",
# tests and example code

17
openssl-tests/Cargo.toml Normal file
View File

@ -0,0 +1,17 @@
[package]
description = "Rustls tests that require OpenSSL"
edition = "2021"
license = "Apache-2.0 OR ISC OR MIT"
name = "rustls-openssl-tests"
publish = false
version = "0.0.1"
[dependencies]
asn1 = "0.15"
base64 = "0.21"
num-bigint = "0.4.4"
once_cell = "1.19"
rustls = {path = "../rustls"}
rustls-pemfile = "2"
rustls-pki-types = "1.0"
openssl = "0.10"

View File

@ -0,0 +1,88 @@
use num_bigint::BigUint;
use rustls::crypto::{
ActiveKeyExchange, CipherSuiteCommon, KeyExchangeAlgorithm, SharedSecret, SupportedKxGroup,
};
use rustls::ffdhe_groups::FfdheGroup;
use rustls::{CipherSuite, NamedGroup, SupportedCipherSuite, Tls12CipherSuite};
/// The (test-only) TLS1.2 ciphersuite TLS_DHE_RSA_WITH_AES_128_GCM_SHA256
pub static TLS_DHE_RSA_WITH_AES_128_GCM_SHA256: SupportedCipherSuite =
SupportedCipherSuite::Tls12(&TLS12_DHE_RSA_WITH_AES_128_GCM_SHA256);
#[derive(Debug)]
pub struct FfdheKxGroup(pub NamedGroup);
impl SupportedKxGroup for FfdheKxGroup {
fn start(&self) -> Result<Box<dyn ActiveKeyExchange>, rustls::Error> {
let mut x = vec![0; 64];
rustls::crypto::ring::default_provider()
.secure_random
.fill(&mut x)?;
let x = BigUint::from_bytes_be(&x);
let group = FfdheGroup::from_named_group(self.0).unwrap();
let p = BigUint::from_bytes_be(group.p);
let g = BigUint::from_bytes_be(group.g);
let x_pub = g.modpow(&x, &p);
let x_pub = to_bytes_be_with_len(x_pub, group.p.len());
Ok(Box::new(ActiveFfdheKx {
x_pub,
x,
p,
group,
named_group: self.0,
}))
}
fn name(&self) -> NamedGroup {
self.0
}
}
static TLS12_DHE_RSA_WITH_AES_128_GCM_SHA256: Tls12CipherSuite =
match &rustls::crypto::ring::cipher_suite::TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 {
SupportedCipherSuite::Tls12(provider) => Tls12CipherSuite {
common: CipherSuiteCommon {
suite: CipherSuite::TLS_DHE_RSA_WITH_AES_128_GCM_SHA256,
..provider.common
},
kx: KeyExchangeAlgorithm::DHE,
..**provider
},
_ => unreachable!(),
};
struct ActiveFfdheKx {
x_pub: Vec<u8>,
x: BigUint,
p: BigUint,
group: FfdheGroup<'static>,
named_group: NamedGroup,
}
impl ActiveKeyExchange for ActiveFfdheKx {
fn complete(self: Box<Self>, peer_pub_key: &[u8]) -> Result<SharedSecret, rustls::Error> {
let peer_pub = BigUint::from_bytes_be(peer_pub_key);
let secret = peer_pub.modpow(&self.x, &self.p);
let secret = to_bytes_be_with_len(secret, self.group.p.len());
Ok(SharedSecret::from(&secret[..]))
}
fn pub_key(&self) -> &[u8] {
&self.x_pub
}
fn group(&self) -> NamedGroup {
self.named_group
}
}
fn to_bytes_be_with_len(n: BigUint, len_bytes: usize) -> Vec<u8> {
let mut bytes = n.to_bytes_le();
bytes.resize(len_bytes, 0);
bytes.reverse();
bytes
}

View File

@ -0,0 +1,233 @@
use std::fs::{self, File};
use std::io::{BufReader, Read, Write};
use std::net::{TcpListener, TcpStream};
use std::sync::Arc;
use std::{str, thread};
use rustls::crypto::ring::default_provider;
use rustls::crypto::CryptoProvider;
use rustls::version::{TLS12, TLS13};
use rustls::{ClientConfig, RootCertStore, ServerConfig, SupportedProtocolVersion};
use rustls_pemfile::Item;
use rustls_pki_types::{CertificateDer, PrivateKeyDer};
use crate::ffdhe::{self, FfdheKxGroup};
use crate::utils::verify_openssl3_available;
use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod};
#[test]
fn rustls_server_with_ffdhe_kx_tls13() {
test_rustls_server_with_ffdhe_kx(&TLS13, 1)
}
#[test]
fn rustls_server_with_ffdhe_kx_tls12() {
test_rustls_server_with_ffdhe_kx(&TLS12, 1)
}
fn test_rustls_server_with_ffdhe_kx(
protocol_version: &'static SupportedProtocolVersion,
iters: usize,
) {
verify_openssl3_available();
let message = "Hello from rustls!\n";
let listener = std::net::TcpListener::bind(("localhost", 0)).unwrap();
let port = listener.local_addr().unwrap().port();
let server_thread = std::thread::spawn(move || {
let config = Arc::new(server_config_with_ffdhe_kx(protocol_version));
for _ in 0..iters {
let mut server = rustls::ServerConnection::new(config.clone()).unwrap();
let (mut tcp_stream, _addr) = listener.accept().unwrap();
server
.writer()
.write_all(message.as_bytes())
.unwrap();
server
.complete_io(&mut tcp_stream)
.unwrap();
tcp_stream.flush().unwrap();
}
});
let mut connector = openssl::ssl::SslConnector::builder(SslMethod::tls()).unwrap();
connector
.set_ca_file(CA_PEM_FILE)
.unwrap();
connector
.set_groups_list("ffdhe2048")
.unwrap();
let connector = connector.build();
for _iter in 0..iters {
let stream = TcpStream::connect(("localhost", port)).unwrap();
let mut stream = connector
.connect("testserver.com", stream)
.unwrap();
let mut buf = String::new();
stream.read_to_string(&mut buf).unwrap();
assert_eq!(buf, message);
}
server_thread.join().unwrap();
}
#[test]
fn rustls_client_with_ffdhe_kx() {
test_rustls_client_with_ffdhe_kx(1);
}
fn test_rustls_client_with_ffdhe_kx(iters: usize) {
verify_openssl3_available();
let message = "Hello from rustls!\n";
println!("crate openssl version: {}", openssl::version::version());
let mut acceptor = SslAcceptor::mozilla_modern_v5(SslMethod::tls()).unwrap();
acceptor
.set_groups_list("ffdhe2048")
.unwrap();
acceptor
.set_private_key_file(PRIV_KEY_FILE, SslFiletype::PEM)
.unwrap();
acceptor
.set_certificate_chain_file(CERT_CHAIN_FILE)
.unwrap();
acceptor.check_private_key().unwrap();
let acceptor = Arc::new(acceptor.build());
let listener = TcpListener::bind(("localhost", 0)).unwrap();
let port = listener.local_addr().unwrap().port();
let server_thread = std::thread::spawn(move || {
for stream in listener.incoming().take(iters) {
match stream {
Ok(stream) => {
let acceptor = acceptor.clone();
thread::spawn(move || {
let mut stream = acceptor.accept(stream).unwrap();
let mut buf = String::new();
stream.read_to_string(&mut buf).unwrap();
assert_eq!(buf, message);
});
}
Err(e) => {
panic!("openssl connection failed: {e}");
}
}
}
});
// client:
for _ in 0..iters {
let mut tcp_stream = std::net::TcpStream::connect(("localhost", port)).unwrap();
let mut client = rustls::client::ClientConnection::new(
client_config_with_ffdhe_kx().into(),
"localhost".try_into().unwrap(),
)
.unwrap();
client
.writer()
.write_all(message.as_bytes())
.unwrap();
client
.complete_io(&mut tcp_stream)
.unwrap();
client.send_close_notify();
client
.write_tls(&mut tcp_stream)
.unwrap();
tcp_stream.flush().unwrap();
}
server_thread.join().unwrap();
}
fn client_config_with_ffdhe_kx() -> ClientConfig {
ClientConfig::builder_with_provider(ffdhe_provider().into())
// OpenSSL 3 does not support RFC 7919 with TLS 1.2: https://github.com/openssl/openssl/issues/10971
.with_protocol_versions(&[&TLS13])
.unwrap()
.with_root_certificates(root_ca())
.with_no_client_auth()
}
// TLS 1.2 requires stripping leading zeros of the shared secret,
// While TLS 1.3 requires the shared secret to be padded with zeros.
// The chance of getting a shared secret with the first byte being zero is 1 in 256,
// so we repeat the tests to have a high chance of getting a kx with this property.
#[test]
#[ignore]
fn rustls_client_with_ffdhe_kx_repeated() {
test_rustls_client_with_ffdhe_kx(512);
}
#[test]
#[ignore]
fn rustls_server_with_ffdhe_tls13_repeated() {
test_rustls_server_with_ffdhe_kx(&TLS13, 512)
}
#[test]
#[ignore]
fn rustls_server_with_ffdhe_tls12_repeated() {
test_rustls_server_with_ffdhe_kx(&TLS12, 512);
}
fn root_ca() -> RootCertStore {
let mut res = RootCertStore::empty();
res.add_parsable_certificates([CertificateDer::from(fs::read(CA_FILE).unwrap())]);
res
}
fn load_certs() -> Vec<CertificateDer<'static>> {
let mut reader = BufReader::new(File::open(CERT_CHAIN_FILE).unwrap());
rustls_pemfile::certs(&mut reader)
.map(|c| c.unwrap())
.collect()
}
fn load_private_key() -> PrivateKeyDer<'static> {
let mut reader = BufReader::new(File::open(PRIV_KEY_FILE).unwrap());
match rustls_pemfile::read_one(&mut reader)
.unwrap()
.unwrap()
{
Item::Pkcs1Key(key) => key.into(),
Item::Pkcs8Key(key) => key.into(),
Item::Sec1Key(key) => key.into(),
_ => panic!("no key in key file {PRIV_KEY_FILE}"),
}
}
fn ffdhe_provider() -> CryptoProvider {
CryptoProvider {
cipher_suites: vec![
ffdhe::TLS_DHE_RSA_WITH_AES_128_GCM_SHA256,
rustls::crypto::ring::cipher_suite::TLS13_AES_128_GCM_SHA256,
],
kx_groups: vec![&FfdheKxGroup(rustls::NamedGroup::FFDHE2048)],
..default_provider()
}
}
fn server_config_with_ffdhe_kx(protocol: &'static SupportedProtocolVersion) -> ServerConfig {
ServerConfig::builder_with_provider(ffdhe_provider().into())
.with_protocol_versions(&[protocol])
.unwrap()
.with_no_client_auth()
.with_single_cert(load_certs(), load_private_key())
.unwrap()
}
const CERT_CHAIN_FILE: &str = "../test-ca/rsa/end.fullchain";
const PRIV_KEY_FILE: &str = "../test-ca/rsa/end.key";
const CA_FILE: &str = "../test-ca/rsa/ca.der";
const CA_PEM_FILE: &str = "../test-ca/rsa/ca.cert";

6
openssl-tests/src/lib.rs Normal file
View File

@ -0,0 +1,6 @@
#![cfg(test)]
mod ffdhe;
mod ffdhe_kx_with_openssl;
mod utils;
mod validate_ffdhe_params;

View File

@ -0,0 +1,48 @@
use once_cell::sync::Lazy;
pub fn verify_openssl3_available() {
static VERIFIED: Lazy<()> = Lazy::new(verify_openssl3_available_internal);
*VERIFIED
}
/// If OpenSSL 3 is not avaialble, panics with a helpful message
fn verify_openssl3_available_internal() {
let openssl_output = std::process::Command::new("openssl")
.args(["version"])
.output();
match openssl_output {
Ok(output) if !output.status.success() => {
panic!(
"OpenSSL exited with an error status: {}\n{}",
output.status,
std::str::from_utf8(&output.stderr).unwrap_or_default()
);
}
Ok(output) => {
let version_str = std::str::from_utf8(&output.stdout).unwrap();
let parts = version_str
.split(' ')
.collect::<Vec<_>>();
assert_eq!(
parts.first().copied(),
Some("OpenSSL"),
"Unknown version response from OpenSSL: {version_str}"
);
let version = parts.get(1);
let major_version = version
.and_then(|v| v.split('.').next())
.unwrap_or_else(|| {
panic!("Unexpected version response from OpenSSL: {version_str}")
});
assert!(
major_version
.parse::<usize>()
.is_ok_and(|v| v >= 3),
"OpenSSL 3+ is required for the tests here. The installed version is {version:?}"
);
}
Err(e) => {
panic!("OpenSSL 3+ needs to be installed and in PATH.\nThe error encountered: {e}")
}
}
}

View File

@ -0,0 +1,89 @@
use base64::prelude::*;
use rustls::ffdhe_groups::FfdheGroup;
use rustls::NamedGroup;
use crate::utils::verify_openssl3_available;
#[test]
fn ffdhe_params_correct() {
use NamedGroup::*;
verify_openssl3_available();
let groups = [FFDHE2048, FFDHE3072, FFDHE4096, FFDHE6144, FFDHE8192];
for group in groups {
println!("testing {group:?}");
test_ffdhe_params_correct(group);
}
}
fn test_ffdhe_params_correct(group: NamedGroup) {
let (p, g) = get_ffdhe_params_from_openssl(group);
let openssl_params = FfdheGroup::from_params_trimming_leading_zeros(&p, &g);
let rustls_params = FfdheGroup::from_named_group(group).unwrap();
assert_eq!(rustls_params.named_group(), Some(group));
assert_eq!(rustls_params, openssl_params);
}
/// Get FFDHE parameters `(p, g)` for the given `ffdhe_group` from OpenSSL
fn get_ffdhe_params_from_openssl(ffdhe_group: NamedGroup) -> (Vec<u8>, Vec<u8>) {
let group = match ffdhe_group {
NamedGroup::FFDHE2048 => "group:ffdhe2048",
NamedGroup::FFDHE3072 => "group:ffdhe3072",
NamedGroup::FFDHE4096 => "group:ffdhe4096",
NamedGroup::FFDHE6144 => "group:ffdhe6144",
NamedGroup::FFDHE8192 => "group:ffdhe8192",
_ => panic!("not an ffdhe group: {ffdhe_group:?}"),
};
let openssl_output = std::process::Command::new("openssl")
.args([
"genpkey",
"-genparam",
"-algorithm",
"DH",
"-text",
"-pkeyopt",
group,
])
.output()
.unwrap();
parse_dh_params_pem(&openssl_output.stdout)
}
/// Parse PEM-encoded DH parameters, returning `(p, g)`
fn parse_dh_params_pem(data: &[u8]) -> (Vec<u8>, Vec<u8>) {
let output_str = std::str::from_utf8(data).unwrap();
let output_str_lines = output_str.lines().collect::<Vec<_>>();
assert_eq!(output_str_lines[0], "-----BEGIN DH PARAMETERS-----");
let last_line = output_str_lines
.iter()
.enumerate()
.find(|(_i, l)| **l == "-----END DH PARAMETERS-----")
.unwrap()
.0;
let stripped = &output_str_lines[1..last_line];
let base64_encoded = stripped
.iter()
.fold(String::new(), |acc, l| acc + l);
let base64_decoded = BASE64_STANDARD
.decode(base64_encoded)
.unwrap();
let res: asn1::ParseResult<_> = asn1::parse(&base64_decoded, |d| {
d.read_element::<asn1::Sequence>()?
.parse(|d| {
let p = d.read_element::<asn1::BigUint>()?;
let g = d.read_element::<asn1::BigUint>()?;
Ok((p, g))
})
});
let res = res.unwrap();
(res.0.as_bytes().to_vec(), res.1.as_bytes().to_vec())
}

View File

@ -3,7 +3,7 @@
use crate::NamedGroup;
#[derive(Clone, Copy, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
/// Parameters of an FFDHE group, with Big-endian byte order
pub struct FfdheGroup<'a> {
pub p: &'a [u8],