mirror of
https://github.com/Omni-guides/Jackify.git
synced 2026-08-14 01:23:46 +02:00
254 lines
10 KiB
Python
254 lines
10 KiB
Python
"""
|
|
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 = """<?xml version="1.0" encoding="utf-8"?>
|
|
<configuration>
|
|
<config>
|
|
<add key="signatureValidationMode" value="accept" />
|
|
</config>
|
|
<packageSources>
|
|
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
|
|
</packageSources>
|
|
<trustedSigners>
|
|
<repository name="nuget.org" serviceIndex="https://api.nuget.org/v3/index.json">
|
|
<certificate fingerprint="0E5F38F57DC1BCC806D8494F4F90FBCEDD988B46760709CBEEC6F4219AA6157D" hashAlgorithm="SHA256" allowUntrustedRoot="true" />
|
|
<certificate fingerprint="5A2901D6ADA3D18260B9C6DFE2133C95D74B9EEF6AE0E5DC334C8454D1477DF4" hashAlgorithm="SHA256" allowUntrustedRoot="true" />
|
|
<certificate fingerprint="1F4B311D9ACC115C8DC8018B5A49E00FCE6DA8E2855F9F014CA6F34570BC482D" hashAlgorithm="SHA256" allowUntrustedRoot="true" />
|
|
</repository>
|
|
</trustedSigners>
|
|
</configuration>
|
|
"""
|
|
|
|
_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("<III", CERT_CERT_PROP_ID, 1, len(der)) + der
|
|
|
|
|
|
def _hex_reg_value(name: str, data: bytes) -> 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
|