feat(tool): import cert into the Proton prefix on Linux
Build & Deploy / build (push) Successful in 28s
Build & Deploy / build (push) Successful in 28s
On Linux the tool now actually trusts the server certificate inside the game's Proton/Wine prefix (the wine certutil tool is broken under Proton): it fetches the live cert from the server (backend.txt), builds the wine serialized cert blob (SHA1 + encoded cert) and writes it to the prefix Root store via `wine reg import`. Auto-detects the Proton wine and lists compatdata prefixes (most-recent first) so the non-Steam-shortcut prefix is picked correctly; the chosen prefix is remembered. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -158,28 +158,56 @@ internal static class Program
|
|||||||
// ---- Certificate -------------------------------------------------------
|
// ---- Certificate -------------------------------------------------------
|
||||||
private static void ImportCert()
|
private static void ImportCert()
|
||||||
{
|
{
|
||||||
Console.Write("Chemin du certificat (.crt) : ");
|
// Prefer fetching the cert live from the server the client points at (backend.txt), so
|
||||||
var path = Console.ReadLine()?.Trim().Trim('"');
|
// it's always the current one; fall back to a .crt file.
|
||||||
if (string.IsNullOrEmpty(path) || !File.Exists(path)) { Console.WriteLine("⚠ Fichier introuvable."); return; }
|
X509Certificate2? cert = null;
|
||||||
|
var target = ReadBackendUrl();
|
||||||
if (OperatingSystem.IsWindows())
|
if (target != null && TryFetchServerCert(target, out cert))
|
||||||
|
Console.WriteLine($"Certificat récupéré depuis {target}");
|
||||||
|
else
|
||||||
{
|
{
|
||||||
ImportCertWindows(path);
|
Console.Write("Chemin d'un certificat (.crt) : ");
|
||||||
return;
|
var p = Console.ReadLine()?.Trim().Trim('"');
|
||||||
|
if (string.IsNullOrEmpty(p) || !File.Exists(p)) { Console.WriteLine("⚠ Annulé."); return; }
|
||||||
|
try { cert = new X509Certificate2(p); } catch (Exception ex) { Console.WriteLine("✖ " + ex.Message); return; }
|
||||||
|
}
|
||||||
|
if (cert == null) { Console.WriteLine("⚠ Aucun certificat."); return; }
|
||||||
|
Console.WriteLine($" empreinte : {cert.Thumbprint}");
|
||||||
|
|
||||||
|
if (OperatingSystem.IsWindows()) ImportCertWindows(cert);
|
||||||
|
else ImportCertProton(cert);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Linux/Proton : le certificat doit être approuvé DANS le préfixe Wine du jeu.
|
private static string? ReadBackendUrl()
|
||||||
var prefix = FindProtonPrefix() ?? $"~/.steam/steam/steamapps/compatdata/{AppId}/pfx";
|
{
|
||||||
Console.WriteLine("Sous Proton/Wine, ajoute le certificat au préfixe du jeu (wine requis) :");
|
if (string.IsNullOrEmpty(_settings.GameFolder)) return null;
|
||||||
Console.WriteLine($" WINEPREFIX=\"{prefix}\" wine certutil -addstore -f Root \"{path}\"");
|
var f = Path.Combine(_settings.GameFolder, "backend.txt");
|
||||||
|
try { return File.Exists(f) ? File.ReadAllText(f).Trim() : null; } catch { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryFetchServerCert(string url, out X509Certificate2? cert)
|
||||||
|
{
|
||||||
|
cert = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var uri = new Uri(url);
|
||||||
|
using var tcp = new System.Net.Sockets.TcpClient();
|
||||||
|
tcp.Connect(uri.Host, uri.Port <= 0 ? 443 : uri.Port);
|
||||||
|
X509Certificate2? captured = null;
|
||||||
|
using var ssl = new System.Net.Security.SslStream(tcp.GetStream(), false,
|
||||||
|
(_, c, _, _) => { if (c != null) captured = new X509Certificate2(c.Export(X509ContentType.Cert)); return true; });
|
||||||
|
ssl.AuthenticateAsClient(uri.Host);
|
||||||
|
cert = captured;
|
||||||
|
return cert != null;
|
||||||
|
}
|
||||||
|
catch { return false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
|
[System.Runtime.Versioning.SupportedOSPlatform("windows")]
|
||||||
private static void ImportCertWindows(string path)
|
private static void ImportCertWindows(X509Certificate2 cert)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var cert = new X509Certificate2(path);
|
|
||||||
using var store = new X509Store(StoreName.Root, StoreLocation.CurrentUser);
|
using var store = new X509Store(StoreName.Root, StoreLocation.CurrentUser);
|
||||||
store.Open(OpenFlags.ReadWrite);
|
store.Open(OpenFlags.ReadWrite);
|
||||||
store.Add(cert);
|
store.Add(cert);
|
||||||
@@ -188,16 +216,132 @@ internal static class Program
|
|||||||
catch (Exception ex) { Console.WriteLine("✖ Import : " + ex.Message); }
|
catch (Exception ex) { Console.WriteLine("✖ Import : " + ex.Message); }
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? FindProtonPrefix()
|
// Trust the cert inside the game's Proton/Wine prefix by writing the serialized cert blob
|
||||||
|
// into the prefix Root store via `wine reg import` (the wine `certutil` tool is unreliable
|
||||||
|
// under Proton). The prefix is chosen explicitly, so it works even though the client runs
|
||||||
|
// as a non-Steam shortcut (whose appid isn't the game's 868270).
|
||||||
|
private static void ImportCertProton(X509Certificate2 cert)
|
||||||
|
{
|
||||||
|
var wine = FindProtonWine();
|
||||||
|
if (wine == null) { Console.WriteLine("⚠ Aucun wine Proton trouvé (Steam/Proton installé ?)."); return; }
|
||||||
|
var prefix = SelectPrefix();
|
||||||
|
if (prefix == null) return;
|
||||||
|
|
||||||
|
// Wine serialized cert blob: SHA1-hash property (id 3) + encoded cert property (id 0x20).
|
||||||
|
var der = cert.RawData;
|
||||||
|
var sha1 = cert.GetCertHash();
|
||||||
|
byte[] blob;
|
||||||
|
using (var ms = new MemoryStream())
|
||||||
|
{
|
||||||
|
void Prop(uint id, byte[] data)
|
||||||
|
{
|
||||||
|
ms.Write(BitConverter.GetBytes(id));
|
||||||
|
ms.Write(BitConverter.GetBytes(1u));
|
||||||
|
ms.Write(BitConverter.GetBytes((uint)data.Length));
|
||||||
|
ms.Write(data);
|
||||||
|
}
|
||||||
|
Prop(3, sha1);
|
||||||
|
Prop(0x20, der);
|
||||||
|
blob = ms.ToArray();
|
||||||
|
}
|
||||||
|
var thumb = Convert.ToHexString(sha1);
|
||||||
|
var hex = string.Join(",", blob.Select(b => b.ToString("x2")));
|
||||||
|
var regFile = Path.Combine(Path.GetTempPath(), "prospect-cert.reg");
|
||||||
|
File.WriteAllText(regFile,
|
||||||
|
"REGEDIT4\r\n\r\n" +
|
||||||
|
$"[HKEY_LOCAL_MACHINE\\Software\\Microsoft\\SystemCertificates\\Root\\Certificates\\{thumb}]\r\n" +
|
||||||
|
$"\"Blob\"=hex:{hex}\r\n");
|
||||||
|
|
||||||
|
Console.WriteLine("⚠ Ferme complètement le jeu avant de continuer (sinon le registre ne sera pas pris en compte).");
|
||||||
|
Console.Write("Appuie sur Entrée pour importer… ");
|
||||||
|
Console.ReadLine();
|
||||||
|
|
||||||
|
var wineserver = Path.Combine(Path.GetDirectoryName(wine)!, "wineserver");
|
||||||
|
var regWinePath = "Z:" + regFile.Replace('/', '\\'); // Z: maps to / inside the prefix
|
||||||
|
RunWine(wineserver, "-k", prefix); // stop any running wineserver (flush)
|
||||||
|
RunWine(wine, $"reg import \"{regWinePath}\"", prefix);
|
||||||
|
RunWine(wineserver, "-k", prefix); // flush the change to disk
|
||||||
|
|
||||||
|
var sysreg = Path.Combine(prefix, "system.reg");
|
||||||
|
var ok = File.Exists(sysreg) && File.ReadAllText(sysreg).Contains(thumb, StringComparison.OrdinalIgnoreCase);
|
||||||
|
Console.WriteLine(ok
|
||||||
|
? $"✔ Certificat importé dans le préfixe : {prefix}\n Relance le jeu."
|
||||||
|
: "✖ Import non confirmé — assure-toi que le jeu est fermé et réessaie.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RunWine(string exe, string args, string prefix)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var psi = new ProcessStartInfo(exe, args)
|
||||||
|
{
|
||||||
|
UseShellExecute = false,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
};
|
||||||
|
psi.Environment["WINEPREFIX"] = prefix;
|
||||||
|
psi.Environment["WINEDEBUG"] = "-all";
|
||||||
|
using var p = Process.Start(psi);
|
||||||
|
p?.WaitForExit(20000);
|
||||||
|
}
|
||||||
|
catch (Exception ex) { Console.WriteLine(" (wine) " + ex.Message); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? FindProtonWine()
|
||||||
|
{
|
||||||
|
foreach (var root in SteamRoots())
|
||||||
|
{
|
||||||
|
var common = Path.Combine(root, "steamapps", "common");
|
||||||
|
if (!Directory.Exists(common)) continue;
|
||||||
|
var candidates = Directory.GetDirectories(common, "Proton*")
|
||||||
|
.Select(d => Path.Combine(d, "files", "bin", "wine"))
|
||||||
|
.Where(File.Exists)
|
||||||
|
.OrderByDescending(w => w.Contains("Experimental"))
|
||||||
|
.ThenByDescending(File.GetLastWriteTimeUtc)
|
||||||
|
.ToList();
|
||||||
|
if (candidates.Count > 0) return candidates[0];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? SelectPrefix()
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(_settings.WinePrefix) && Directory.Exists(_settings.WinePrefix))
|
||||||
|
return _settings.WinePrefix;
|
||||||
|
|
||||||
|
var prefixes = new List<string>();
|
||||||
|
foreach (var root in SteamRoots())
|
||||||
|
{
|
||||||
|
var cd = Path.Combine(root, "steamapps", "compatdata");
|
||||||
|
if (!Directory.Exists(cd)) continue;
|
||||||
|
foreach (var d in Directory.GetDirectories(cd))
|
||||||
|
{
|
||||||
|
var pfx = Path.Combine(d, "pfx");
|
||||||
|
if (File.Exists(Path.Combine(pfx, "system.reg"))) prefixes.Add(Path.GetFullPath(pfx));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prefixes = prefixes.Distinct()
|
||||||
|
.OrderByDescending(p => File.GetLastWriteTimeUtc(Path.Combine(p, "system.reg")))
|
||||||
|
.ToList();
|
||||||
|
if (prefixes.Count == 0) { Console.WriteLine("⚠ Aucun préfixe Proton trouvé."); return null; }
|
||||||
|
|
||||||
|
Console.WriteLine("Préfixes Proton détectés (le plus récemment utilisé en premier = sûrement ton jeu) :");
|
||||||
|
for (var i = 0; i < prefixes.Count; i++)
|
||||||
|
Console.WriteLine($" {i + 1}) {prefixes[i]}");
|
||||||
|
Console.Write("Numéro du préfixe (Entrée = 1) : ");
|
||||||
|
var raw = Console.ReadLine()?.Trim();
|
||||||
|
var idx = string.IsNullOrEmpty(raw) ? 0 : (int.TryParse(raw, out var n) ? n - 1 : -1);
|
||||||
|
if (idx < 0 || idx >= prefixes.Count) { Console.WriteLine("⚠ Choix invalide."); return null; }
|
||||||
|
_settings.WinePrefix = prefixes[idx];
|
||||||
|
SaveSettings();
|
||||||
|
return _settings.WinePrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<string> SteamRoots()
|
||||||
{
|
{
|
||||||
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||||
foreach (var dir in new[]
|
yield return Path.Combine(home, ".steam", "steam");
|
||||||
{
|
yield return Path.Combine(home, ".local", "share", "Steam");
|
||||||
Path.Combine(home, ".steam", "steam", "steamapps", "compatdata", AppId, "pfx"),
|
|
||||||
Path.Combine(home, ".local", "share", "Steam", "steamapps", "compatdata", AppId, "pfx"),
|
|
||||||
})
|
|
||||||
if (Directory.Exists(dir)) return dir;
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Launch ------------------------------------------------------------
|
// ---- Launch ------------------------------------------------------------
|
||||||
@@ -255,5 +399,6 @@ internal static class Program
|
|||||||
private sealed class Settings
|
private sealed class Settings
|
||||||
{
|
{
|
||||||
public string GameFolder { get; set; } = "";
|
public string GameFolder { get; set; } = "";
|
||||||
|
public string WinePrefix { get; set; } = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user