Files
OpenFUT/scripts/check-tls-parity.sh
T
funman300 fc411bb6f1 scripts: require one certificate across the whole FIFA-facing TLS stack
Two live gates were lost to a second variable I had been asked to eliminate.
The Rust redirector was pointed at the repo's fifa17-recon/tools/redir_cert.pem
(fingerprint F9:16:1A...), while the running container serves a different cert
baked into its image (E7:F9:46...) which the Python redirector, roster and the
rest of the stack all share. So the A/B compared TLS implementation AND
certificate identity at once.

FIFA 17's ProtoSSL caches the server certificate for a backend. The redirector
is the first TLS connection of a session, so its cert becomes the one the client
expects; the next service presenting a different cert fails its handshake. That
is why the redirect itself always succeeded and the failure surfaced later, on
the roster fetch -- "An error occurred downloading the FUT Squad Update".

It stayed invisible because Python's socketserver swallows it: a handshake
failure at accept() raises ssl.SSLError, which subclasses OSError and is
discarded by _handle_request_noblock. No request log, no stderr. Every server
looked healthy while the client could not talk to any of them.

Confirmed on the wire: tls-observe in front of the roster server captured four
ClientHellos from the client, correct SNI and the same 8 static-RSA suites it
offers the redirector, none of which produced a request.

The check is mutation-tested against the real bug: with a redirector started on
the stale repo cert it exits 1 and names the mismatch.
2026-08-11 04:38:23 +00:00

58 lines
2.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# Every FIFA-facing TLS service must present the SAME certificate.
#
# check-tls-parity.sh <host:port> <host:port> [<host:port> ...]
#
# WHY
#
# FIFA 17's ProtoSSL caches the server certificate it saw for a backend. The
# redirector is the first TLS connection of a session, so whatever cert it
# presents becomes the one the client expects from that backend. If a later
# service — roster, UTAS — presents a different cert, that handshake fails.
#
# Python's socketserver makes this invisible: a handshake failure at accept()
# raises ssl.SSLError, which subclasses OSError and is silently swallowed by
# _handle_request_noblock. No request log, no stderr, nothing. The observable
# symptom is a client that logs in fine and then cannot download the FUT Squad
# Update, with every server looking healthy.
#
# This cost two live gates. The Rust redirector was pointed at the repo's
# fifa17-recon/tools/redir_cert.pem while the running container serves a
# different cert baked into its image — so the A/B had two variables, TLS
# implementation AND certificate identity, when it was meant to have one.
#
# Run this before arming any gate that puts a new TLS implementation in front
# of the client.
set -uo pipefail
[[ $# -ge 2 ]] || { echo "usage: check-tls-parity.sh <host:port> <host:port> [...]" >&2; exit 2; }
fingerprint() {
timeout 8 openssl s_client -connect "$1" </dev/null 2>/dev/null \
| openssl x509 -noout -fingerprint -sha256 2>/dev/null | cut -d= -f2
}
ref=""; ref_target=""; rc=0
for target in "$@"; do
fp="$(fingerprint "$target")"
if [[ -z "$fp" ]]; then
printf " %-24s %s\n" "$target" "NO TLS / unreachable"
rc=1
continue
fi
printf " %-24s %s\n" "$target" "$fp"
if [[ -z "$ref" ]]; then
ref="$fp"; ref_target="$target"
elif [[ "$fp" != "$ref" ]]; then
rc=1
fi
done
if [[ $rc -eq 0 ]]; then
echo "OK: all $# services present the same certificate"
else
echo "REFUSING: certificate mismatch across the FIFA-facing stack (reference: $ref_target)" >&2
echo " A client that caches the first cert it sees will fail on the others." >&2
fi
exit $rc