tls: extract a shared listener; move FIFA 17's profile into its adapter
The redirector was the only host that spoke TLS, so its TLS lived inside
it. The roster host needs the same listener, and that made the choice
explicit: share this code or copy it.
Copying it is what already went wrong. On 2026-08-11 the Rust redirector
served one certificate while the container served another. ProtoSSL
caches the server certificate per backend, so the redirector -- the first
TLS connection of a session -- decided what the client expected, and
every later service failed its handshake. Silently: Python's socketserver
swallows ssl.SSLError as OSError. Three gates went to it. One place to
configure TLS is the structural fix, so it exists before the second host
does rather than after.
Split along the line the architecture already draws:
openfut-tls how to build an acceptor. Game-independent.
Knows nothing about which suites any client
offers.
adapter-fifa17::tls what FIFA 17 was OBSERVED to offer: the six
enabled suites, the two refused, the TLS 1.2
window, the EA SNI. Plain strings, so the
adapter keeps its lean dependencies -- reading
a card table should not build OpenSSL.
redirector-host joins the two. Chooses no cipher of its own.
Behaviour is unchanged, and shown to be:
* tests/fifa17_tls_profile.rs carries over every case from the deleted
module -- FIFA's eight suites negotiate AES256-GCM-SHA384, each enabled
suite works alone, RC4-only is refused, ECDHE-only is refused. Deleting
a module must not quietly delete its evidence.
* one test pins the composed values literally against the host as it was
when gates 1-14 passed. A "pure refactor" that cannot fail is not a
claim, it is an assumption.
* the rebuilt binary self-tests to the same TLSv1.2 / AES256-GCM-SHA384
the retail client negotiated at 17:09 today.
Two improvements fall out of having one place to look:
* the startup banner now prints cert_sha256. The mismatch above raised no
error at startup and broke the client much later with nothing logged;
it is now the first line of the log.
* tls_min/tls_max print as TLSv1.2 rather than SslVersion(771). This line
is gate evidence and gets read by people.
Nothing deployed and nothing restarted: FIFA is mid-session on the
running redirector, which is untouched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,7 @@ use std::time::Duration;
|
||||
use openfut_adapter_fifa17::blaze::AdapterConfig;
|
||||
use openfut_host_config::{self as hostcfg, ConfigError};
|
||||
|
||||
use crate::tls::TlsConfig;
|
||||
use openfut_tls::{ProtocolVersion, TlsConfig};
|
||||
|
||||
/// How long the oracle holds a redirector connection open after responding
|
||||
/// (`time.sleep(0.3)` in `blaze_responder_v3b.redir_handle`).
|
||||
@@ -22,6 +22,28 @@ use crate::tls::TlsConfig;
|
||||
/// reimplementation OF.
|
||||
pub const ORACLE_CLOSE_DWELL: Duration = Duration::from_millis(300);
|
||||
|
||||
/// Compose the shared TLS listener from the FIFA 17 adapter's observed profile.
|
||||
///
|
||||
/// The split is deliberate: `openfut-tls` knows how to build an acceptor,
|
||||
/// `openfut-adapter-fifa17` knows what FIFA 17 offers, and this host knows only
|
||||
/// how to join them. A second host serving the same client uses this same pair
|
||||
/// rather than repeating either half — one configuration path is the structural
|
||||
/// guard against the 2026-08-11 certificate mismatch, where two services
|
||||
/// configured TLS separately and disagreed.
|
||||
fn fifa17_tls(cert: impl Into<String>, key: impl Into<String>) -> Result<TlsConfig, ConfigError> {
|
||||
use openfut_adapter_fifa17::tls as profile;
|
||||
let ver = |s: &str| {
|
||||
ProtocolVersion::parse(s).map_err(|e| ConfigError(format!("FIFA 17 TLS profile: {e}")))
|
||||
};
|
||||
Ok(TlsConfig::new(
|
||||
cert,
|
||||
key,
|
||||
profile::CIPHER_LIST,
|
||||
ver(profile::MIN_VERSION)?,
|
||||
ver(profile::MAX_VERSION)?,
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RedirectorConfig {
|
||||
/// BIND: where this listener binds. Never client-visible.
|
||||
@@ -57,7 +79,10 @@ impl RedirectorConfig {
|
||||
)?;
|
||||
let key = hostcfg::required("OPENFUT_REDIRECTOR_KEY", "path to the matching private key")?;
|
||||
|
||||
let mut tls = TlsConfig::new(cert, key);
|
||||
// FIFA 17's observed profile, supplied by the adapter. The transport
|
||||
// host chooses no cipher and no protocol window of its own: those are
|
||||
// facts about the client, and belong with the game they describe.
|
||||
let mut tls = fifa17_tls(cert, key)?;
|
||||
if let Some(list) = hostcfg::optional_opt("OPENFUT_REDIRECTOR_CIPHERS") {
|
||||
tls.cipher_list = list;
|
||||
}
|
||||
@@ -97,10 +122,11 @@ impl RedirectorConfig {
|
||||
RedirectorConfig {
|
||||
listen_addr: "127.0.0.1".into(),
|
||||
listen_port: 0,
|
||||
tls: TlsConfig::new(
|
||||
tls: fifa17_tls(
|
||||
format!("{base}/redir_cert.pem"),
|
||||
format!("{base}/redir_key.pem"),
|
||||
),
|
||||
)
|
||||
.expect("the adapter's TLS profile must be valid"),
|
||||
adapter: AdapterConfig::advertising(advertise),
|
||||
close_dwell: ORACLE_CLOSE_DWELL,
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
//! the Blaze sidecar's.
|
||||
|
||||
pub mod config;
|
||||
pub mod tls;
|
||||
|
||||
use std::io::{Read, Write};
|
||||
use std::net::{TcpListener, TcpStream};
|
||||
@@ -72,7 +71,7 @@ pub fn identity() -> String {
|
||||
} else {
|
||||
"release"
|
||||
},
|
||||
tls::openssl_version(),
|
||||
openfut_tls::openssl_version(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -83,7 +82,7 @@ pub fn identity() -> String {
|
||||
pub fn banner(cfg: &RedirectorConfig) -> String {
|
||||
format!(
|
||||
"openfut-redirector-host v{} commit={} profile={} openssl={} listen={} \
|
||||
advertise={}:{} tls_min={:?} tls_max={:?} security_level={} ciphers={}",
|
||||
advertise={}:{} tls_min={} tls_max={} security_level={} cert_sha256={} ciphers={}",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
BUILD_COMMIT,
|
||||
if cfg!(debug_assertions) {
|
||||
@@ -91,16 +90,24 @@ pub fn banner(cfg: &RedirectorConfig) -> String {
|
||||
} else {
|
||||
"release"
|
||||
},
|
||||
tls::openssl_version(),
|
||||
openfut_tls::openssl_version(),
|
||||
cfg.listen_on(),
|
||||
cfg.adapter.endpoints.advertise,
|
||||
cfg.adapter.endpoints.blaze_port,
|
||||
cfg.tls.min_version,
|
||||
cfg.tls.max_version,
|
||||
// Readable names, not `SslVersion(771)`: this line is gate evidence and
|
||||
// is read by humans comparing one run against another.
|
||||
cfg.tls.min_version.as_str(),
|
||||
cfg.tls.max_version.as_str(),
|
||||
cfg.tls
|
||||
.security_level
|
||||
.map(|l| l.to_string())
|
||||
.unwrap_or_else(|| "default".into()),
|
||||
// WHICH certificate, not just that one loaded. Serving a different
|
||||
// certificate from the rest of the stack raises no error at startup and
|
||||
// breaks the client much later, with nothing logged anywhere. Printing
|
||||
// it unprompted is what turns that into a one-glance comparison.
|
||||
openfut_tls::certificate_fingerprint(&cfg.tls.cert_path)
|
||||
.unwrap_or_else(|e| format!("unreadable ({e})")),
|
||||
cfg.tls.cipher_list,
|
||||
)
|
||||
}
|
||||
@@ -166,13 +173,18 @@ fn record(outcomes: &Outcomes, o: ConnOutcome) {
|
||||
|
||||
/// Build TLS, rehearse the retail handshake, and bind.
|
||||
pub fn bind(cfg: RedirectorConfig) -> std::io::Result<Server> {
|
||||
let acceptor = tls::build_acceptor(&cfg.tls)
|
||||
let acceptor = openfut_tls::build_acceptor(&cfg.tls)
|
||||
.map_err(|e| std::io::Error::other(format!("TLS setup failed: {e}")))?;
|
||||
// Rehearse the retail handshake BEFORE accepting anything: a client
|
||||
// restricted to exactly the suites FIFA offers must connect. Catching a
|
||||
// cipher/version misconfiguration here means it never shows up as an
|
||||
// unexplained failure during a live gate.
|
||||
let neg = tls::self_test(&cfg.tls, tls::OBSERVED_CLIENT_SUITES).map_err(|e| {
|
||||
let neg = openfut_tls::self_test(
|
||||
&cfg.tls,
|
||||
openfut_adapter_fifa17::tls::OBSERVED_CLIENT_SUITES,
|
||||
openfut_adapter_fifa17::tls::CLIENT_SNI,
|
||||
)
|
||||
.map_err(|e| {
|
||||
std::io::Error::other(format!(
|
||||
"self-test failed — a FIFA-like client cannot connect: {e}"
|
||||
))
|
||||
@@ -322,7 +334,10 @@ pub enum BodyRead {
|
||||
/// No `Content-Length` header — nothing to drain.
|
||||
None,
|
||||
Complete(usize),
|
||||
Short { got: usize, want: usize },
|
||||
Short {
|
||||
got: usize,
|
||||
want: usize,
|
||||
},
|
||||
}
|
||||
|
||||
/// Parse `Content-Length` and read the remainder of the body into `buf`.
|
||||
|
||||
@@ -1,358 +0,0 @@
|
||||
//! TLS for the FIFA 17 redirector — a deliberately narrow legacy compatibility
|
||||
//! island.
|
||||
//!
|
||||
//! # Driven by an observed ClientHello, not by generic legacy assumptions
|
||||
//!
|
||||
//! A retail FIFA 17 client was captured through a passive proxy while reaching
|
||||
//! the FUT hub. It offers **exactly eight suites, every one static-RSA**:
|
||||
//!
|
||||
//! ```text
|
||||
//! 0x009D TLS_RSA_WITH_AES_256_GCM_SHA384 0x0035 TLS_RSA_WITH_AES_256_CBC_SHA
|
||||
//! 0x009C TLS_RSA_WITH_AES_128_GCM_SHA256 0x002F TLS_RSA_WITH_AES_128_CBC_SHA
|
||||
//! 0x003D TLS_RSA_WITH_AES_256_CBC_SHA256 0x0005 TLS_RSA_WITH_RC4_128_SHA
|
||||
//! 0x003C TLS_RSA_WITH_AES_128_CBC_SHA256 0x0004 TLS_RSA_WITH_RC4_128_MD5
|
||||
//!
|
||||
//! client_version TLS 1.2 extensions: server_name, signature_algorithms only
|
||||
//! ```
|
||||
//!
|
||||
//! Zero forward-secret suites, which is why `rustls` cannot serve this client
|
||||
//! and why this crate uses `openssl` directly.
|
||||
//!
|
||||
//! # What is deliberately NOT enabled
|
||||
//!
|
||||
//! * **RC4 and MD5.** The client offers them; we do not need them. It already
|
||||
//! negotiates `AES256-GCM-SHA384` against the Python oracle, so resurrecting
|
||||
//! RC4 for historical completeness would weaken the service for nothing.
|
||||
//! * **SSLv3.** Never.
|
||||
//! * **A lowered security level.** Not applied pre-emptively. The default
|
||||
//! policy is tried first; if the retail handshake fails because OpenSSL
|
||||
//! rejects something actually required, the narrowest possible change is made
|
||||
//! and documented — not a blanket `SECLEVEL=0`.
|
||||
//!
|
||||
//! # Scope
|
||||
//!
|
||||
//! Every setting here applies to THIS listener's `SslContext` only. Nothing
|
||||
//! global is weakened, and no other OpenFUT component links OpenSSL.
|
||||
|
||||
use std::fmt;
|
||||
use std::path::Path;
|
||||
|
||||
use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod, SslOptions, SslVersion};
|
||||
|
||||
/// The suites we enable: the six RSA+AES options the client offers, strongest
|
||||
/// first, in OpenSSL's pre-TLS-1.3 naming.
|
||||
///
|
||||
/// RC4/MD5 are excluded on purpose (see module docs). Order expresses our
|
||||
/// preference; the client's own order put AES-256-GCM first anyway.
|
||||
pub const CIPHER_LIST: &str =
|
||||
"AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA";
|
||||
|
||||
/// Suites the observed client offers that we deliberately refuse.
|
||||
pub const REFUSED_SUITES: [&str; 2] = ["RC4-SHA", "RC4-MD5"];
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TlsError(pub String);
|
||||
|
||||
impl fmt::Display for TlsError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for TlsError {}
|
||||
|
||||
/// TLS knobs, all overridable so a failed retail handshake is a configuration
|
||||
/// change rather than a code change.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TlsConfig {
|
||||
pub cert_path: String,
|
||||
pub key_path: String,
|
||||
pub cipher_list: String,
|
||||
pub min_version: SslVersion,
|
||||
pub max_version: SslVersion,
|
||||
/// `None` = leave OpenSSL's default policy alone. Only set this if a real
|
||||
/// handshake proves it necessary, and record why.
|
||||
pub security_level: Option<u32>,
|
||||
}
|
||||
|
||||
impl TlsConfig {
|
||||
/// Defaults chosen from the observed client: it speaks TLS 1.2 and offers
|
||||
/// no TLS 1.3, so both floor and ceiling are 1.2.
|
||||
///
|
||||
/// The floor is NOT dropped to TLS 1.0 pre-emptively. The oracle allows it,
|
||||
/// but no evidence shows this client needs it, and "the oracle permits it"
|
||||
/// is not the same as "the client requires it".
|
||||
pub fn new(cert_path: impl Into<String>, key_path: impl Into<String>) -> TlsConfig {
|
||||
TlsConfig {
|
||||
cert_path: cert_path.into(),
|
||||
key_path: key_path.into(),
|
||||
cipher_list: CIPHER_LIST.to_string(),
|
||||
min_version: SslVersion::TLS1_2,
|
||||
max_version: SslVersion::TLS1_2,
|
||||
security_level: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The OpenSSL version this binary is linked against.
|
||||
///
|
||||
/// Vendored, so it is fixed at build time rather than inherited from the host.
|
||||
/// Printed at startup and recorded in gate evidence: a TLS implementation
|
||||
/// change is not a routine dependency bump, it invalidates the compatibility
|
||||
/// testing.
|
||||
pub fn openssl_version() -> String {
|
||||
openssl::version::version().to_string()
|
||||
}
|
||||
|
||||
/// Build the acceptor for the redirector listener.
|
||||
pub fn build_acceptor(cfg: &TlsConfig) -> Result<SslAcceptor, TlsError> {
|
||||
// `mozilla_intermediate` would preload a modern, forward-secret-only
|
||||
// policy — precisely wrong here. Start from the bare method and state every
|
||||
// choice explicitly, so nothing is inherited silently.
|
||||
let mut b = SslAcceptor::mozilla_intermediate(SslMethod::tls())
|
||||
.map_err(|e| TlsError(format!("acceptor: {e}")))?;
|
||||
|
||||
b.set_min_proto_version(Some(cfg.min_version))
|
||||
.map_err(|e| TlsError(format!("min proto: {e}")))?;
|
||||
b.set_max_proto_version(Some(cfg.max_version))
|
||||
.map_err(|e| TlsError(format!("max proto: {e}")))?;
|
||||
|
||||
// Explicit list, replacing whatever policy the profile brought with it.
|
||||
b.set_cipher_list(&cfg.cipher_list)
|
||||
.map_err(|e| TlsError(format!("cipher list {:?}: {e}", cfg.cipher_list)))?;
|
||||
|
||||
if let Some(level) = cfg.security_level {
|
||||
b.set_security_level(level);
|
||||
}
|
||||
|
||||
// SSLv3 is never acceptable, whatever the version floor says.
|
||||
b.set_options(SslOptions::NO_SSLV3);
|
||||
|
||||
let cert = Path::new(&cfg.cert_path);
|
||||
let key = Path::new(&cfg.key_path);
|
||||
if !cert.exists() {
|
||||
return Err(TlsError(format!(
|
||||
"certificate not found: {}",
|
||||
cfg.cert_path
|
||||
)));
|
||||
}
|
||||
if !key.exists() {
|
||||
return Err(TlsError(format!("private key not found: {}", cfg.key_path)));
|
||||
}
|
||||
b.set_certificate_chain_file(cert)
|
||||
.map_err(|e| TlsError(format!("certificate {}: {e}", cfg.cert_path)))?;
|
||||
b.set_private_key_file(key, SslFiletype::PEM)
|
||||
.map_err(|e| TlsError(format!("private key {}: {e}", cfg.key_path)))?;
|
||||
// Catches a mismatched pair at startup rather than mid-handshake, where it
|
||||
// would look like a client problem.
|
||||
b.check_private_key()
|
||||
.map_err(|e| TlsError(format!("certificate and key do not match: {e}")))?;
|
||||
|
||||
Ok(b.build())
|
||||
}
|
||||
|
||||
/// The eight suites the captured retail ClientHello offers, in OpenSSL naming.
|
||||
///
|
||||
/// Used to build a client that behaves like FIFA 17 for the self-test below.
|
||||
pub const OBSERVED_CLIENT_SUITES: &str =
|
||||
"AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:\
|
||||
AES256-SHA:AES128-SHA:RC4-SHA:RC4-MD5";
|
||||
|
||||
/// What a handshake actually negotiated.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Negotiated {
|
||||
pub version: String,
|
||||
pub cipher: String,
|
||||
}
|
||||
|
||||
/// Handshake against ourselves with a client restricted to `client_ciphers`.
|
||||
///
|
||||
/// Enumerating the server's configured list would be the obvious check, but the
|
||||
/// crate exposes no such accessor at this version — and a real handshake is
|
||||
/// better evidence anyway: it proves MUTUAL support and reports what was
|
||||
/// actually chosen, which is the thing a gate is judged on. Defaulting
|
||||
/// `client_ciphers` to the observed FIFA suites makes this a direct rehearsal
|
||||
/// of the retail handshake.
|
||||
pub fn self_test(cfg: &TlsConfig, client_ciphers: &str) -> Result<Negotiated, TlsError> {
|
||||
use openssl::ssl::{SslConnector, SslVerifyMode};
|
||||
use std::io::{Read, Write};
|
||||
use std::net::{TcpListener, TcpStream};
|
||||
|
||||
let acceptor = build_acceptor(cfg)?;
|
||||
let listener = TcpListener::bind("127.0.0.1:0").map_err(|e| TlsError(format!("bind: {e}")))?;
|
||||
let addr = listener
|
||||
.local_addr()
|
||||
.map_err(|e| TlsError(format!("addr: {e}")))?;
|
||||
|
||||
let server = std::thread::spawn(move || -> Result<Negotiated, String> {
|
||||
let (sock, _) = listener.accept().map_err(|e| e.to_string())?;
|
||||
let mut tls = acceptor.accept(sock).map_err(|e| e.to_string())?;
|
||||
let neg = {
|
||||
let s = tls.ssl();
|
||||
Negotiated {
|
||||
version: s.version_str().to_string(),
|
||||
cipher: s
|
||||
.current_cipher()
|
||||
.map(|c| c.name().to_string())
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
};
|
||||
let mut buf = [0u8; 64];
|
||||
let _ = tls.read(&mut buf);
|
||||
let _ = tls.write_all(b"ok");
|
||||
let _ = tls.flush();
|
||||
Ok(neg)
|
||||
});
|
||||
|
||||
let mut cb =
|
||||
SslConnector::builder(SslMethod::tls()).map_err(|e| TlsError(format!("connector: {e}")))?;
|
||||
cb.set_cipher_list(client_ciphers)
|
||||
.map_err(|e| TlsError(format!("client cipher list: {e}")))?;
|
||||
// The point is the cipher/version negotiation, not PKI: the oracle's
|
||||
// certificate is self-signed and the real client's checks are patched out.
|
||||
cb.set_verify(SslVerifyMode::NONE);
|
||||
let connector = cb.build();
|
||||
|
||||
let sock = TcpStream::connect(addr).map_err(|e| TlsError(format!("connect: {e}")))?;
|
||||
let mut client = connector
|
||||
.configure()
|
||||
.and_then(|c| {
|
||||
c.verify_hostname(false)
|
||||
.into_ssl("winter15.gosredirector.ea.com")
|
||||
})
|
||||
.and_then(|ssl| openssl::ssl::SslStream::new(ssl, sock))
|
||||
.map_err(|e| TlsError(format!("client setup: {e}")))?;
|
||||
client
|
||||
.connect()
|
||||
.map_err(|e| TlsError(format!("client handshake failed: {e}")))?;
|
||||
let _ = client.write_all(b"ping");
|
||||
let _ = client.flush();
|
||||
let mut buf = [0u8; 8];
|
||||
let _ = client.read(&mut buf);
|
||||
|
||||
server
|
||||
.join()
|
||||
.map_err(|_| TlsError("server thread panicked".into()))?
|
||||
.map_err(|e| TlsError(format!("server handshake failed: {e}")))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Reuse the proven Python redirector's material, so TLS implementation is
|
||||
/// the only variable in the A/B.
|
||||
fn oracle_cert() -> (String, String) {
|
||||
let base = concat!(env!("CARGO_MANIFEST_DIR"), "/../fifa17-recon/tools");
|
||||
(
|
||||
format!("{base}/redir_cert.pem"),
|
||||
format!("{base}/redir_key.pem"),
|
||||
)
|
||||
}
|
||||
|
||||
fn cfg() -> TlsConfig {
|
||||
let (c, k) = oracle_cert();
|
||||
TlsConfig::new(c, k)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn acceptor_builds_with_the_oracle_certificate() {
|
||||
assert!(build_acceptor(&cfg()).is_ok(), "acceptor must build");
|
||||
}
|
||||
|
||||
/// The decisive test: the configured list must actually contain a suite the
|
||||
/// observed client offers, or the retail handshake cannot succeed.
|
||||
/// The decisive test: a client restricted to exactly the suites the retail
|
||||
/// ClientHello offered must complete a handshake, and must land on the
|
||||
/// suite the real client already negotiated against Python.
|
||||
#[test]
|
||||
fn a_client_offering_only_fifas_suites_completes_a_handshake() {
|
||||
let neg = self_test(&cfg(), OBSERVED_CLIENT_SUITES).expect("handshake");
|
||||
assert_eq!(neg.cipher, "AES256-GCM-SHA384", "negotiated {neg:?}");
|
||||
assert_eq!(neg.version, "TLSv1.2", "negotiated {neg:?}");
|
||||
}
|
||||
|
||||
/// Each RSA+AES suite individually, so a client that offered only one of
|
||||
/// them would still connect.
|
||||
#[test]
|
||||
fn every_enabled_rsa_aes_suite_can_be_negotiated_alone() {
|
||||
for suite in [
|
||||
"AES256-GCM-SHA384",
|
||||
"AES128-GCM-SHA256",
|
||||
"AES256-SHA256",
|
||||
"AES128-SHA256",
|
||||
"AES256-SHA",
|
||||
"AES128-SHA",
|
||||
] {
|
||||
let neg = self_test(&cfg(), suite)
|
||||
.unwrap_or_else(|e| panic!("{suite} could not be negotiated: {e}"));
|
||||
assert_eq!(neg.cipher, suite);
|
||||
}
|
||||
}
|
||||
|
||||
/// RC4/MD5 are offered by the client and deliberately refused by us, so a
|
||||
/// client offering ONLY those must fail to connect. This is the test that
|
||||
/// proves the refusal is real rather than aspirational.
|
||||
#[test]
|
||||
fn a_client_offering_only_rc4_is_refused() {
|
||||
let err = self_test(&cfg(), "RC4-SHA:RC4-MD5");
|
||||
assert!(err.is_err(), "RC4-only client should not connect: {err:?}");
|
||||
}
|
||||
|
||||
/// A forward-secret-only client must also fail — proving we did not
|
||||
/// silently inherit a modern profile that would exclude FIFA.
|
||||
#[test]
|
||||
fn a_forward_secret_only_client_is_not_what_we_serve() {
|
||||
let r = self_test(&cfg(), "ECDHE-RSA-AES256-GCM-SHA384");
|
||||
assert!(r.is_err(), "we should not be offering ECDHE; got {r:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn security_level_is_not_lowered_by_default() {
|
||||
assert_eq!(
|
||||
cfg().security_level,
|
||||
None,
|
||||
"the default policy is tried first; lowering requires evidence"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn protocol_window_matches_the_observed_client() {
|
||||
let c = cfg();
|
||||
assert_eq!(c.min_version, SslVersion::TLS1_2);
|
||||
assert_eq!(c.max_version, SslVersion::TLS1_2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_missing_certificate_fails_clearly() {
|
||||
let mut c = cfg();
|
||||
c.cert_path = "/nonexistent/cert.pem".into();
|
||||
let e = match build_acceptor(&c) {
|
||||
Err(e) => e.to_string(),
|
||||
Ok(_) => panic!("a missing certificate must not build"),
|
||||
};
|
||||
assert!(e.contains("certificate not found"), "{e}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_mismatched_key_is_caught_at_startup() {
|
||||
let (cert, _) = oracle_cert();
|
||||
let mut c = cfg();
|
||||
// Point the key at the certificate: a valid PEM that is not the key.
|
||||
c.key_path = cert;
|
||||
assert!(build_acceptor(&c).is_err(), "mismatch must not build");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unusable_cipher_list_is_an_error_not_a_silent_empty_set() {
|
||||
let mut c = cfg();
|
||||
c.cipher_list = "THIS-IS-NOT-A-CIPHER".into();
|
||||
assert!(build_acceptor(&c).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openssl_version_is_reportable() {
|
||||
let v = openssl_version();
|
||||
assert!(v.contains("OpenSSL"), "{v}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user