forked from Manuel/meeting-assistant
ci: provision SDK code-signing roots in the disposable Wine prefix
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user