use std::io::{BufRead, BufReader, Write};
use std::net::{TcpListener, TcpStream};
use std::process::{Child, Command, Stdio};
use std::sync::Arc;
use std::time::Duration;
use rustls::pki_types::ServerName;
use rustls::{ClientConfig, ClientConnection, RootCertStore, StreamOwned, SupportedCipherSuite};
fn find_openssl() -> Option<String> {
let candidates = std::env::var("OPENSSL").ok().into_iter().chain(
[
"openssl",
"/opt/homebrew/opt/openssl@3/bin/openssl",
"/usr/local/opt/openssl@3/bin/openssl",
]
.iter()
.map(|s| s.to_string()),
);
for candidate in candidates {
if let Ok(out) = Command::new(&candidate).arg("version").output() {
if out.status.success() && out.stdout.starts_with(b"OpenSSL 3") {
return Some(candidate);
}
}
}
None
}
struct ServerGuard(Child);
impl Drop for ServerGuard {
fn drop(&mut self) {
let _ = self.0.kill();
let _ = self.0.wait();
}
}
fn interop(suite: SupportedCipherSuite, server_args: &[&str]) {
let Some(openssl) = find_openssl() else {
eprintln!("skipping interop test: no OpenSSL 3.x binary found");
return;
};
let kp = rcgen::KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256).unwrap();
let params = rcgen::CertificateParams::new(vec!["localhost".into()]).unwrap();
let cert = params.self_signed(&kp).unwrap();
let port = TcpListener::bind("127.0.0.1:0")
.unwrap()
.local_addr()
.unwrap()
.port();
let dir =
std::env::temp_dir().join(format!("rustls-ccm-interop-{}-{port}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let cert_path = dir.join("cert.pem");
let key_path = dir.join("key.pem");
std::fs::write(&cert_path, cert.pem()).unwrap();
std::fs::write(&key_path, kp.serialize_pem()).unwrap();
let child = Command::new(&openssl)
.arg("s_server")
.args(["-accept", &port.to_string()])
.arg("-cert")
.arg(&cert_path)
.arg("-key")
.arg(&key_path)
.args(["-rev", "-naccept", "1", "-quiet"])
.args(server_args)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.unwrap();
let _guard = ServerGuard(child);
let mut sock = None;
for _ in 0..100 {
match TcpStream::connect(("127.0.0.1", port)) {
Ok(s) => {
sock = Some(s);
break;
}
Err(_) => std::thread::sleep(Duration::from_millis(50)),
}
}
let sock = sock.expect("openssl s_server did not start listening");
sock.set_read_timeout(Some(Duration::from_secs(10)))
.unwrap();
let mut roots = RootCertStore::empty();
roots.add(cert.der().clone()).unwrap();
let mut provider = rustls::crypto::aws_lc_rs::default_provider();
provider.cipher_suites = vec![suite];
let versions: &[&rustls::SupportedProtocolVersion] = match suite {
SupportedCipherSuite::Tls12(_) => &[&rustls::version::TLS12],
SupportedCipherSuite::Tls13(_) => &[&rustls::version::TLS13],
};
let config = ClientConfig::builder_with_provider(Arc::new(provider))
.with_protocol_versions(versions)
.unwrap()
.with_root_certificates(roots)
.with_no_client_auth();
let server_name = ServerName::try_from("localhost").unwrap();
let conn = ClientConnection::new(Arc::new(config), server_name).unwrap();
let mut tls = StreamOwned::new(conn, sock);
tls.write_all(b"ping\n").unwrap();
tls.flush().unwrap();
let mut reader = BufReader::new(&mut tls);
let mut line = String::new();
reader.read_line(&mut line).unwrap();
assert_eq!(line.trim_end(), "gnip", "bad echo from s_server: {line:?}");
assert_eq!(
tls.conn.negotiated_cipher_suite().unwrap().suite(),
suite.suite()
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn tls12_aes128_ccm_vs_openssl() {
interop(
*rustls_ccm::TLS_ECDHE_ECDSA_WITH_AES_128_CCM,
&["-tls1_2", "-cipher", "ECDHE-ECDSA-AES128-CCM"],
);
}
#[test]
fn tls12_aes256_ccm_vs_openssl() {
interop(
*rustls_ccm::TLS_ECDHE_ECDSA_WITH_AES_256_CCM,
&["-tls1_2", "-cipher", "ECDHE-ECDSA-AES256-CCM"],
);
}
#[test]
fn tls12_aes128_ccm8_vs_openssl() {
interop(
*rustls_ccm::TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8,
&["-tls1_2", "-cipher", "ECDHE-ECDSA-AES128-CCM8@SECLEVEL=0"],
);
}
#[test]
fn tls12_aes256_ccm8_vs_openssl() {
interop(
*rustls_ccm::TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8,
&["-tls1_2", "-cipher", "ECDHE-ECDSA-AES256-CCM8@SECLEVEL=0"],
);
}
#[test]
fn tls13_aes128_ccm_vs_openssl() {
interop(
*rustls_ccm::TLS13_AES_128_CCM_SHA256,
&["-tls1_3", "-ciphersuites", "TLS_AES_128_CCM_SHA256"],
);
}
#[test]
fn tls13_aes128_ccm8_vs_openssl() {
interop(
*rustls_ccm::TLS13_AES_128_CCM_8_SHA256,
&[
"-tls1_3",
"-ciphersuites",
"TLS_AES_128_CCM_8_SHA256",
"-cipher",
"DEFAULT@SECLEVEL=0",
],
);
}