//! FIFA 17's TLS profile — what the retail client was *observed* to offer. //! //! # Evidence, not assumption //! //! 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 the transport hosts link OpenSSL directly. //! //! # Deliberately not enabled //! //! * **RC4 and MD5.** The client offers them; it does not need them. It already //! negotiates `AES256-GCM-SHA384` against the Python oracle, so resurrecting //! RC4 for completeness would weaken the service for nothing. //! * **SSLv3.** Never. //! * **A lowered security level.** Not applied pre-emptively. The default //! policy is tried first; if a retail handshake fails because OpenSSL rejects //! something genuinely required, the narrowest possible change is made and //! documented — not a blanket `SECLEVEL=0`. //! //! # Why this is data and not code //! //! These are facts about FIFA 17, so they live in the FIFA 17 adapter rather //! than in `openfut-tls`, which must stay game-independent. They are plain //! strings so this crate keeps its lean dependency list: an adapter should not //! drag OpenSSL into the build of everything that reads a card table. //! //! Confirmed live on 2026-08-11: the retail client reached the FUT hub through //! a Rust host configured from exactly these values, negotiating //! `TLSv1.2 / AES256-GCM-SHA384` with `sni=winter15.gosredirector.ea.com`. /// The suites enabled for FIFA 17: the six RSA+AES options the client offers, /// strongest first, in OpenSSL's pre-TLS-1.3 naming. /// /// Order expresses preference; the client's own order put AES-256-GCM first /// anyway, which is what the live handshake selected. 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 are deliberately refused. pub const REFUSED_SUITES: [&str; 2] = ["RC4-SHA", "RC4-MD5"]; /// All eight suites the captured ClientHello offered, for handshake rehearsals. pub const OBSERVED_CLIENT_SUITES: &str = "AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:\ AES256-SHA:AES128-SHA:RC4-SHA:RC4-MD5"; /// The SNI the retail client sends to the redirector. /// /// Nothing routes on it — recorded so a rehearsal handshake matches the real /// one, and so a log line can show whether a connection came from the game or /// from a probe. pub const CLIENT_SNI: &str = "winter15.gosredirector.ea.com"; /// Protocol floor, as OpenSSL spells it. /// /// The floor is NOT dropped to TLS 1.0 pre-emptively. The Python oracle permits /// it, but no evidence shows this client needs it, and "the oracle permits it" /// is not "the client requires it". pub const MIN_VERSION: &str = "TLSv1.2"; /// Protocol ceiling. The client offers no TLS 1.3, so the window is exact. pub const MAX_VERSION: &str = "TLSv1.2"; /// The suite the live retail handshake selected, and which a rehearsal is /// expected to reproduce. pub const EXPECTED_SUITE: &str = "AES256-GCM-SHA384"; #[cfg(test)] mod tests { use super::*; /// The enabled list must be exactly the client's offer minus the refusals. /// Stated as a derivation so adding a suite to one constant without the /// other is a test failure rather than a silent divergence. #[test] fn the_enabled_list_is_the_offer_minus_the_refusals() { let offered: Vec<&str> = OBSERVED_CLIENT_SUITES.split(':').collect(); let enabled: Vec<&str> = CIPHER_LIST.split(':').collect(); let expected: Vec<&str> = offered .iter() .copied() .filter(|s| !REFUSED_SUITES.contains(s)) .collect(); assert_eq!(enabled, expected); assert_eq!(offered.len(), 8, "the capture showed eight suites"); assert_eq!(enabled.len(), 6); } /// Every enabled suite must be static RSA. An ECDHE suite slipping in would /// be silently useless to this client, which offers none. #[test] fn nothing_forward_secret_is_enabled() { for s in CIPHER_LIST.split(':') { assert!( !s.contains("ECDHE") && !s.contains("DHE"), "{s} is forward-secret; FIFA 17 offers no such suite" ); } } #[test] fn rc4_and_md5_are_refused_not_merely_absent() { for s in REFUSED_SUITES { assert!( OBSERVED_CLIENT_SUITES.contains(s), "{s} must be one the client actually offers" ); assert!(!CIPHER_LIST.contains(s), "{s} must not be enabled"); } } #[test] fn the_expected_suite_is_one_we_enable_and_the_client_offers() { assert!(CIPHER_LIST.split(':').any(|s| s == EXPECTED_SUITE)); assert!(OBSERVED_CLIENT_SUITES .split(':') .any(|s| s == EXPECTED_SUITE)); assert_eq!( CIPHER_LIST.split(':').next(), Some(EXPECTED_SUITE), "preference order should put the observed selection first" ); } #[test] fn the_protocol_window_is_exactly_tls12() { assert_eq!(MIN_VERSION, "TLSv1.2"); assert_eq!(MAX_VERSION, "TLSv1.2"); } }