Files
OpenFUT/docs/FIFA17_FUT_SQUAD_UPDATE_TLS.md
T
funman300 082246c085 docs: root-cause the FUT Squad Update download failure (client rejects the roster cert)
Recovered the client's own dialog text from memory rather than inferring from the
server, which is what finally identified the subsystem: "An error occurred downloading
the FUT Squad Update" is the ROSTER update, not the player's lineup. Four squad-shaped
fixes before that were aimed at the wrong thing.

Wire capture shows the client aborting the handshake itself: it offers TLS1.2 with RSA
suites, the server selects TLS1.2 and sends its certificate, and the client replies
fatal certificate_unknown. So protocol and ciphers are compatible and the certificate
is the problem. That certificate is DNS-SAN-only while the advertised ROSTERUPDATE_URL
is an IP literal, and it was regenerated Aug 13 -- after the Aug 12 session being used
as the known-good control, which therefore says nothing about the current cert.

Notably autopatch DID patch both ProtoSSL gates in the failing process (log line plus
live bytes reading back patched) and the client still rejected, so those gates do not
govern this path -- contradicting roster_server.py's standing comment that they make
self-signed certs acceptable.

Documents what was ruled out with evidence (hub route shapes, squad shape, squad
round-trip, advertised hosts, TLS version, Blaze health), the probing gotcha that a
default modern TLS context misreports this server as broken, the unresolved question of
why production appears unaffected, and three fix options with a recommendation. No fix
applied.
2026-08-18 05:12:00 +00:00

114 lines
5.9 KiB
Markdown

# FIFA 17 "FUT Squad Update" download failure — root cause
**Status:** root-caused, not yet fixed. Blocks any FIFA session pointed at a stack whose
roster server presents the current self-signed certificate.
**Symptom (client):** entering the FUT hub shows
> An error occurred downloading the FUT Squad Update. Please try again.
Recovered from the live client's memory with `scripts/client-error-string.py`, not from a
log. Nothing server-side reports an error: every UTAS route answers 200 and the Blaze
session stays healthy, answering PINGs while the client sits on the dialog.
## What it is
"Squad Update" here is the **roster update**, not the player's lineup. The Blaze responder
advertises it as the FUT loading gate (`blaze_responder_v3b.py`):
```python
ROSTER_HOST = "%s:8081" % _ADVERTISE
("ROSTERUPDATE_URL", "https://%s/fifa17/fut/rosterupdate.xml" % ROSTER_HOST)
("ROSTER_URL", "https://%s/fifa17/roster/" % ROSTER_HOST)
```
The client fetches that URL over TLS, **by IP**, from `roster_server.py`.
## Root cause
The client completes TCP, sends a ClientHello, receives the certificate, and then
**aborts the handshake itself**. Captured and decoded from the wire:
```
CLIENT: ClientHello client_version = TLS1.2
offers 8 suites: RSA_AES_256_GCM_SHA384, RSA_AES_128_GCM_SHA256,
RSA_AES_256_CBC_SHA256, RSA_AES_128_CBC_SHA256, RSA_AES_256_CBC_SHA,
RSA_AES_128_CBC_SHA, RSA_RC4_128_SHA, RSA_RC4_128_MD5
SERVER: ServerHello TLS1.2 -> Certificate (909 B) -> ServerHelloDone
CLIENT: Alert level=fatal desc=certificate_unknown
```
Protocol and cipher selection are **compatible** — TLS1.2 with an RSA suite is offered and
chosen. The client rejects the **certificate**:
```
subject/issuer = CN = winter15.gosredirector.ea.com (self-signed, CA:TRUE)
SAN = DNS:winter15.gosredirector.ea.com, DNS:*.gosredirector.ea.com, DNS:*.ea.com
notBefore = Aug 13 01:12:30 2026
sha256 = FF:66:69:48:E2:02:AB:F4:A4:40:78:B1:8C:E1:F9:65:96:32:BB:FF:1F:3D:CC:BC:07:54:02:11:8F:4D:12:3F
```
Two facts matter:
1. **The SAN carries DNS names only, no `iPAddress` entry**, while the advertised URL is an
IP literal (`https://10.10.0.120:8081/...`). A DNS-only SAN cannot match an IP dial.
2. **The certificate was regenerated on Aug 13.** `roster_server.py` loads the same
`redir_cert.pem`/`redir_key.pem` as the Blaze redirector, so the redirector rejects
identically — visible as `REDIR REJECTED ... TLS/SSL connection has been closed (EOF)`
in the Blaze log. That is tolerated only because the client then reaches Blaze main over
plain TCP; the roster fetch has no such fallback, so it is where the failure surfaces.
`roster_server.py` still carries the comment *"ProtoSSL cert-verify is patched (autopatch),
so our self-signed cert is accepted."* That assumption no longer holds for this path.
autopatch demonstrably patched both gates in the failing process — the log shows
`pid 56298: PATCHED cert gates` and the live bytes read back as the patched patterns
(`909090909090` at `0x146132548`, `31c0c3` at `0x1461361b0`) — and the client still sent
`certificate_unknown`. So **those two gates do not govern this validation**; either another
check exists on the roster path, or it validates before reaching them.
## Ruled out, with evidence
| Hypothesis | Evidence against |
| --- | --- |
| Missing/mistyped UTAS field | 0 structural differences across 14 hub routes vs production (`scripts/hub-diff-prod-staging.py`) |
| Player squad shape | matches production field-for-field, including `squadType`, `custom`, `chemistry`, `manager` |
| Squad save | PUT/GET round-trips exactly, every field and slot |
| Cert gates unpatched | autopatch log + live byte read both confirm patched |
| Roster server broken | serves `200`, `application/xml` over TLS1.2 `AES256-GCM-SHA384` |
| Advertised host wrong | production and staging Blaze envs are identical for `OPENFUT_ADVERTISE`, `POW_*`; the scripts are byte-identical |
| TLS version mismatch | client offers TLS1.2 and the server selects it |
| Blaze session | healthy, PING/PING_REPLY throughout |
## Probing gotcha
A default modern Python TLS context gets `SSLV3_ALERT_HANDSHAKE_FAILURE` against this
server and looks like a server fault. It is not — the server requires legacy-compatible
cipher selection. Probe with `ALL:@SECLEVEL=0`, and note it rejects TLS1.0/1.1 outright
(`TLSV1_ALERT_PROTOCOL_VERSION`), accepting only TLS1.2.
## Why production appears unaffected
Unresolved, and worth stating plainly rather than guessing. Production advertises the same
URL, backed by the same server and certificate, so the same rejection should occur. The
operator reports production is clean. The likely explanation is that a production client
does not re-fetch the roster (already satisfied), while a session against a different Blaze
forces the fetch — but that has not been measured. The known-good hook log used as a
control (Aug 12 19:13) **predates the Aug 13 certificate regeneration**, so it is not
evidence about the current certificate.
## Fix options (none applied)
1. **Reissue the certificate with an `iPAddress` SAN** for the advertised address. Smallest
change, addresses the concrete mismatch, and benefits the redirector too. Note
`openfut-tls` exists precisely so the redirector and roster hosts cannot configure TLS
separately — the 2026-08-11 mismatch it was written for is the same class of bug.
2. **Serve the roster over plain HTTP** for an isolated stack, by pointing
`ROSTERUPDATE_URL`/`ROSTER_URL` at a staging-owned HTTP server. Sidesteps TLS entirely;
only acceptable off production.
3. **Find the real validation site** on the roster path and extend the patch set. Most
faithful, most work, and the only option that restores the original assumption.
Option 1 is the recommended starting point because it is testable without the client:
a probe that validates hostname/IP against the presented certificate will fail before the
fix and pass after.