""" NuGet package signature trust configuration for Wine prefixes. Synthesis compiles C# patchers under Wine using an installed .NET SDK, which requires NuGet package signature validation to succeed. Split out of tool_config_service.py to keep that file under the project's size guardrail. Old (2018-2021 era) Microsoft BCL packages carry timestamp signatures chaining to legacy VeriSign/Symantec roots that modern distros have removed from their trust stores. Wine seeds its Root store from the host bundle, so those roots are missing in the prefix and NuGet fails restore with NU3028 ("timestamping certificate is not trusted"). NuGet treats an untrusted root on the TIMESTAMP chain as a hard error unconditionally (see Timestamp.Verify in NuGet.Client - UntrustedRoot is logged as Error regardless of signatureValidationMode, allowUntrustedRoot, or any env var), so the only fix is making the roots actually trusted in the prefix's cert store. """ import base64 import hashlib import logging import os import re import struct import subprocess import tempfile from pathlib import Path from typing import Callable, Iterator, Optional, Tuple logger = logging.getLogger(__name__) # Every NuGet.Config we write pins nuget.org's repository signing certs by # fingerprint with allowUntrustedRoot, so the SIGNING chain does not depend on # the CA root chain being trusted. The TIMESTAMP chain is not covered by this # (see module docstring) - that requires the root cert import below. # Fingerprints are nuget.org's own signing certs (public, stable across signing # key rotations) - see SulfurNitride/Fluorine-Manager commit d453ed91b6dc332fd45987c642e9d230efe870c8. _NUGET_CONFIG_TEMPLATE = """ """ _PEM_CERT_RE = re.compile( r"-----BEGIN CERTIFICATE-----(.*?)-----END CERTIFICATE-----", re.DOTALL ) # Registry path Wine's crypt32 reads trusted roots from - the same location it # seeds with the host CA bundle at prefix init. _ROOT_STORE_KEY = "HKEY_LOCAL_MACHINE\\Software\\Microsoft\\SystemCertificates\\Root\\Certificates" CERT_CERT_PROP_ID = 32 def _find_sdk_trusted_roots(prefix_path: Path) -> Optional[Tuple[Path, Path]]: """ Locate the newest installed .NET SDK's bundled trustedroots PEM files. Returns (codesignctl.pem, timestampctl.pem) or None if not found. """ sdk_root = prefix_path / "drive_c" / "Program Files" / "dotnet" / "sdk" if not sdk_root.is_dir(): return None for version_dir in sorted(sdk_root.iterdir(), reverse=True): trustedroots = version_dir / "trustedroots" codesign = trustedroots / "codesignctl.pem" timestamp = trustedroots / "timestampctl.pem" if codesign.exists() and timestamp.exists(): return codesign, timestamp return None def _read_pem_bundle(bundle_path: Path) -> Iterator[bytes]: """Yield DER-encoded certificates from a concatenated PEM bundle.""" text = bundle_path.read_text(encoding="utf-8", errors="replace") for match in _PEM_CERT_RE.finditer(text): yield base64.b64decode("".join(match.group(1).split())) def _cert_registry_blob(der: bytes) -> bytes: """ Serialize a certificate the way crypt32 stores it in the registry: a property record of DWORD propid, DWORD reserved (1), DWORD length, then the raw DER. A blob containing only the CERT_CERT_PROP_ID record is valid. """ return struct.pack(" str: """Format a REG_BINARY value as regedit .reg hex syntax with line wrapping.""" hex_bytes = [f"{b:02x}" for b in data] lines = [] line = f'"{name}"=hex:' for hb in hex_bytes: if len(line) + len(hb) + 2 > 76: lines.append(line + "\\") line = " " line += hb + "," lines.append(line.rstrip(",")) return "\n".join(lines) def _build_certs_reg_content(bundles: Tuple[Path, ...]) -> Tuple[str, int]: """ Build a .reg file adding every cert in the given PEM bundles to the Wine prefix's Root store. Returns (reg_content, unique_cert_count). Re-importing is idempotent - regedit overwrites identical keys in place. """ seen = set() parts = ["Windows Registry Editor Version 5.00"] for bundle in bundles: for der in _read_pem_bundle(bundle): thumbprint = hashlib.sha1(der).hexdigest().upper() if thumbprint in seen: continue seen.add(thumbprint) parts.append("") parts.append(f"[{_ROOT_STORE_KEY}\\{thumbprint}]") parts.append(_hex_reg_value("Blob", _cert_registry_blob(der))) parts.append("") return "\n".join(parts), len(seen) def install_nuget_cert( prefix_path: Path, wine_bin: str, log: Callable[[str], None], ) -> bool: """ Import the .NET SDK's bundled code-signing and timestamp trusted-root certificates into the Wine prefix's Root cert store, by writing serialized cert blobs directly into the registry via regedit. Regedit is the only mechanism that actually persists here: certutil.exe and rundll32 cryptext.dll are unimplemented stubs in Wine, and crypt32 cert store writes (X509Store.Add / CertAddCertificateContextToStore) under GE-Proton report success but never reach wineserver's registry - the added certs are visible only inside the importing process and are gone once it exits (confirmed on CachyOS + GE-Proton10-14: store reported the add, cross-process reg query and the on-disk .reg files never saw it). Requires the .NET SDK already installed in the prefix - the PEM bundles ship inside the SDK and are the exact trust anchors NuGet itself uses for package and timestamp signature validation on Linux. """ trusted_roots = _find_sdk_trusted_roots(prefix_path) if trusted_roots is None: log(".NET SDK trusted root bundles not found - skipping NuGet certificate import") return False try: reg_content, cert_count = _build_certs_reg_content(trusted_roots) with tempfile.NamedTemporaryFile( mode="w", suffix=".reg", delete=False, encoding="utf-8" ) as tf: tf.write(reg_content) reg_file = tf.name log(f"Importing {cert_count} code-signing and timestamp roots into Wine cert store...") env = os.environ.copy() env["WINEPREFIX"] = str(prefix_path) env["WINEDEBUG"] = "-all" env["WINEDLLOVERRIDES"] = "winemenubuilder.exe=d" env["DISPLAY"] = env.get("DISPLAY", ":0") try: result = subprocess.run( [wine_bin, "regedit", reg_file], env=env, capture_output=True, text=True, timeout=120, ) finally: try: os.unlink(reg_file) except Exception: pass if result.returncode != 0: log(f"Certificate registry import exited with code {result.returncode}") log(f"stderr: {result.stderr[:500]}") return False # wineserver batches registry writes in memory and flushes to the # on-disk .reg files lazily. "wineserver -w" blocks until wineserver # exits, forcing the flush - same pattern used elsewhere for registry # writes (see modlist_wine_ops.py). wineserver_bin = os.path.join(os.path.dirname(wine_bin), "wineserver") if os.path.exists(wineserver_bin): try: subprocess.run( [wineserver_bin, "-w"], env=env, timeout=60, capture_output=True, ) except Exception as e: log(f"wineserver flush failed (non-fatal): {e}") else: log(f"wineserver not found at {wineserver_bin}; registry flush may not persist") log(f"Imported {cert_count} certificates into Wine cert store") return True except Exception as e: log(f"Failed to install NuGet certificates: {e}") return False def configure_nuget_signature_policy( prefix_path: Path, log: Callable[[str], None], ) -> bool: """ Write a NuGet.Config that pins nuget.org's signing certificates by fingerprint with allowUntrustedRoot, so signing-chain validation does not depend on the CA root chain. Timestamp-chain trust is handled separately by install_nuget_cert (see module docstring). Steam/Proton prefixes always use "steamuser" as the Windows user. Must run before the first dotnet invocation in the prefix: the SDK auto-generates a bare-bones NuGet.Config (no trust policy) as a side effect of any restore if none exists yet, and this function only writes when the file is absent. """ config_dir = prefix_path / "drive_c" / "users" / "steamuser" / "AppData" / "Roaming" / "NuGet" config_path = config_dir / "NuGet.Config" if config_path.exists(): log("NuGet.Config already exists - leaving it unchanged") return True try: config_dir.mkdir(parents=True, exist_ok=True) config_path.write_text(_NUGET_CONFIG_TEMPLATE, encoding="utf-8") log("NuGet signature policy configured (nuget.org signers trusted by fingerprint)") return True except Exception as e: log(f"Failed to write NuGet.Config: {e}") return False