ci: provision SDK code-signing roots in the disposable Wine prefix
PR and Push Build/Test / build-and-test (pull_request) Blocked by required conditions
PR and Push Build/Test / portable-build-and-test (pull_request) Blocked by required conditions

This commit is contained in:
dh
2026-10-03 15:00:48 +02:00
parent 83726a2233
commit 05e23857dd
3 changed files with 191 additions and 0 deletions
+150
View File
@@ -0,0 +1,150 @@
#:property PublishAot=false
#:property UseAppHost=false
#:property AssemblyName=WineSdkTrust
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
if (args.Length != 3 || (args[0] != "--validate" && args[0] != "--import"))
{
Console.Error.WriteLine("Usage: WineSdkTrust <--validate|--import> <codesignctl.pem> <timestampctl.pem>");
return 2;
}
var import = args[0] == "--import";
if (import && (!OperatingSystem.IsWindows() || !IsWine()))
{
Console.Error.WriteLine("REFUSED: --import requires Windows under Wine (ntdll.dll!wine_get_version). No certificate store was opened.");
return 2;
}
Console.WriteLine($"OS: {RuntimeInformation.OSDescription}");
Console.WriteLine($"Runtime: {RuntimeInformation.FrameworkDescription}");
Console.WriteLine($"Mode: {args[0]}");
var certificates = new X509Certificate2Collection();
try
{
foreach (var path in args.Skip(1))
{
if (!File.Exists(path) || new FileInfo(path).Length == 0)
{
throw new InvalidDataException($"The SDK certificate bundle is missing or empty: {path}");
}
var bundle = new X509Certificate2Collection();
try
{
bundle.ImportFromPemFile(path);
if (bundle.Count == 0)
{
throw new InvalidDataException($"The SDK bundle contains no PEM certificates: {path}");
}
var legacyRoots = 0;
foreach (var certificate in bundle)
{
if (certificate.HasPrivateKey)
{
throw new InvalidDataException($"The SDK bundle must contain public certificates only: {certificate.Thumbprint}");
}
var constraints = certificate.Extensions.OfType<X509BasicConstraintsExtension>().SingleOrDefault();
if (constraints is { CertificateAuthority: false })
{
throw new InvalidDataException($"The SDK bundle contains a non-CA certificate: {certificate.Thumbprint}");
}
if (constraints is null)
{
// Microsoft also ships historical roots without the BasicConstraints extension.
if (!certificate.SubjectName.RawData.AsSpan().SequenceEqual(certificate.IssuerName.RawData))
{
throw new InvalidDataException($"A certificate without CA constraints is not self-issued: {certificate.Thumbprint}");
}
legacyRoots++;
}
}
Console.WriteLine($"Bundle: {Path.GetFullPath(path)}");
Console.WriteLine($" SHA256: {Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(path)))}");
Console.WriteLine($" Certificates: {bundle.Count}; historical self-issued roots without BasicConstraints: {legacyRoots}");
certificates.AddRange(bundle);
bundle.Clear();
}
finally
{
foreach (var certificate in bundle)
{
certificate.Dispose();
}
}
}
var thumbprints = certificates.Select(certificate => certificate.Thumbprint).ToHashSet(StringComparer.OrdinalIgnoreCase);
Console.WriteLine($"Unique SDK certificate thumbprints: {thumbprints.Count}");
if (!import)
{
Console.WriteLine("PASS: both SDK bundles validated; no certificate store was opened.");
return 0;
}
using (var store = new X509Store(StoreName.Root, StoreLocation.CurrentUser))
{
store.Open(OpenFlags.ReadWrite);
store.AddRange(certificates);
}
using (var store = new X509Store(StoreName.Root, StoreLocation.CurrentUser))
{
store.Open(OpenFlags.ReadOnly);
var installed = store.Certificates;
try
{
var installedThumbprints = installed.Select(certificate => certificate.Thumbprint).ToHashSet(StringComparer.OrdinalIgnoreCase);
var missing = thumbprints.Except(installedThumbprints).ToArray();
if (missing.Length != 0)
{
throw new CryptographicException($"SDK certificates missing after import: {string.Join(", ", missing)}");
}
}
finally
{
foreach (var certificate in installed)
{
certificate.Dispose();
}
}
}
Console.WriteLine($"PASS: all {thumbprints.Count} SDK certificate thumbprints verified in CurrentUser Root.");
return 0;
}
catch (Exception exception)
{
Console.Error.WriteLine($"FAIL: {exception.GetType().Name}: {exception.Message}");
return 1;
}
finally
{
foreach (var certificate in certificates)
{
certificate.Dispose();
}
}
static bool IsWine()
{
if (!NativeLibrary.TryLoad("ntdll.dll", out var library))
{
return false;
}
try
{
return NativeLibrary.TryGetExport(library, "wine_get_version", out _);
}
finally
{
NativeLibrary.Free(library);
}
}