use std::{path::Path, sync::Arc}; use tokio_rustls::TlsAcceptor; /// Load a previously persisted cert/key pair, or generate and save a new one. /// /// Reusing the same cert across restarts means clients only need to trust it once. pub fn load_or_generate_cert(cert_path: &Path, key_path: &Path) -> anyhow::Result<(Vec, Vec)> { if cert_path.exists() && key_path.exists() { let cert_pem = std::fs::read(cert_path)?; let key_pem = std::fs::read(key_path)?; tracing::info!("Loaded existing TLS cert from {:?}", cert_path); return Ok((cert_pem, key_pem)); } let (cert_pem, key_pem) = generate_self_signed_cert()?; if let Some(parent) = cert_path.parent() { std::fs::create_dir_all(parent)?; } std::fs::write(cert_path, &cert_pem)?; std::fs::write(key_path, &key_pem)?; tracing::info!("Generated new TLS cert, saved to {:?}", cert_path); Ok((cert_pem, key_pem)) } /// Generate a self-signed certificate covering EA FUT hostnames. /// CN is set to `fut.ea.com` so ProtoSSL's CN hostname check passes. /// SANs include wildcard entries for all `*.ea.com` and `*.easports.com` subdomains /// so connections to any EA subdomain are covered. pub fn generate_self_signed_cert() -> anyhow::Result<(Vec, Vec)> { use rcgen::{Certificate, CertificateParams, DistinguishedName, DnType, SanType}; use std::net::{IpAddr, Ipv4Addr}; let mut params = CertificateParams::new(vec![ "fut.ea.com".to_string(), "utas.mob.v4.fut.ea.com".to_string(), "accounts.ea.com".to_string(), "pin.data.ea.com".to_string(), "gateway.ea.com".to_string(), "localhost".to_string(), ]); // Wildcard SANs cover every EA subdomain the game might contact params.subject_alt_names.push(SanType::DnsName("*.ea.com".to_string())); params.subject_alt_names.push(SanType::DnsName("*.easports.com".to_string())); params.subject_alt_names.push(SanType::DnsName("*.ugc.footapi.com".to_string())); params.subject_alt_names.push(SanType::IpAddress(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)))); // CN must match the primary hostname so ProtoSSL's CN check passes let mut dn = DistinguishedName::new(); dn.push(DnType::CommonName, "fut.ea.com"); params.distinguished_name = dn; let cert = Certificate::from_params(params)?; let cert_pem = cert.serialize_pem()?.into_bytes(); let key_pem = cert.serialize_private_key_pem().into_bytes(); Ok((cert_pem, key_pem)) } /// Build a TlsAcceptor from PEM-encoded certificate and private key bytes. pub fn make_tls_acceptor(cert_pem: &[u8], key_pem: &[u8]) -> anyhow::Result { use rustls::{Certificate, PrivateKey, ServerConfig}; let certs: Vec = rustls_pemfile::certs(&mut std::io::Cursor::new(cert_pem))? .into_iter() .map(Certificate) .collect(); let mut keys = rustls_pemfile::pkcs8_private_keys(&mut std::io::Cursor::new(key_pem))?; if keys.is_empty() { anyhow::bail!("no PKCS8 private keys found in key PEM"); } let config = Arc::new( ServerConfig::builder() .with_safe_defaults() .with_no_client_auth() .with_single_cert(certs, PrivateKey(keys.remove(0)))?, ); Ok(TlsAcceptor::from(config)) }