using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
namespace LanDesktopPLONDS.Installer.Services;
///
/// Authenticode 签名验证结果。
///
internal enum AuthenticodeStatus
{
/// 文件具有有效的 Authenticode 签名。
Signed,
/// 文件未签名。
Unsigned,
/// 签名无效或验证过程出错。
Invalid
}
///
/// Authenticode 验证结果数据。
///
internal sealed class AuthenticodeResult
{
/// 验证状态。
public AuthenticodeStatus Status { get; init; }
/// 签名者主体名称(仅在 Status == Signed 时有效)。
public string? SignerSubject { get; init; }
/// 是否要求强制签名验证。
public bool EnforcementEnabled { get; init; }
public override string ToString() => Status switch
{
AuthenticodeStatus.Signed => $"Signed ({SignerSubject ?? "unknown"})",
AuthenticodeStatus.Unsigned => "Unsigned",
AuthenticodeStatus.Invalid => "Invalid",
_ => "Unknown"
};
}
///
/// Windows Authenticode(WinVerifyTrust)签名验证器。
/// 使用 Win32 P/Invoke 验证 PE 文件的 Authenticode 签名,
/// 并通过 X509Certificate 提取签名者信息。
/// 默认为仅报告模式;设置 LANMOUNTAIN_INSTALLER_REQUIRE_SIGNED=1 启用强制验证。
///
internal static class AuthenticodeVerifier
{
private const string RequireSignedEnvVar = "LANMOUNTAIN_INSTALLER_REQUIRE_SIGNED";
///
/// 当前是否启用了强制签名验证。
///
public static bool EnforcementEnabled =>
string.Equals(
Environment.GetEnvironmentVariable(RequireSignedEnvVar),
"1",
StringComparison.Ordinal);
///
/// 对指定 PE 文件执行 Authenticode 签名验证。
///
/// 要验证的文件路径。
/// 验证结果,包含状态和签名者信息。
public static AuthenticodeResult VerifyFile(string path)
{
ArgumentException.ThrowIfNullOrWhiteSpace(path);
if (!File.Exists(path))
{
InstallerStartupDiagnostics.Log($"[Authenticode] 文件不存在:{path}");
return new AuthenticodeResult { Status = AuthenticodeStatus.Invalid };
}
if (!OperatingSystem.IsWindows())
{
InstallerStartupDiagnostics.Log("[Authenticode] 非 Windows 平台,跳过 Authenticode 验证。");
return new AuthenticodeResult { Status = AuthenticodeStatus.Unsigned };
}
// 第一步:通过 Win32 WinVerifyTrust 验证签名有效性
var trustStatus = WinVerifyTrustNative(path);
if (trustStatus != 0)
{
// TRUST_E_NOSIGNATURE (0x800B0100) = 文件未签名
if (trustStatus == unchecked((int)0x800B0100))
{
InstallerStartupDiagnostics.Log($"[Authenticode] 文件未签名:{path}");
return new AuthenticodeResult { Status = AuthenticodeStatus.Unsigned };
}
InstallerStartupDiagnostics.Log(
$"[Authenticode] WinVerifyTrust 返回错误 0x{trustStatus:X8}:{path}");
return new AuthenticodeResult { Status = AuthenticodeStatus.Invalid };
}
// 第二步:提取签名者信息
string? signerSubject = null;
try
{
#pragma warning disable SYSLIB0057 // X509Certificate.CreateFromSignedFile 已过时
using var cert = X509Certificate.CreateFromSignedFile(path);
#pragma warning restore SYSLIB0057
signerSubject = cert.Subject;
}
catch (CryptographicException ex)
{
// WinVerifyTrust 通过但无法读取证书——记录但不视为失败
InstallerStartupDiagnostics.Log(
$"[Authenticode] 签名有效但无法读取证书信息:{ex.Message}");
}
InstallerStartupDiagnostics.Log(
$"[Authenticode] 签名验证通过:{path},签名者={signerSubject ?? "unknown"}");
return new AuthenticodeResult
{
Status = AuthenticodeStatus.Signed,
SignerSubject = signerSubject
};
}
// WinVerifyTrust 相关常量
private const int WTD_UI_NONE = 2;
private const int WTD_REVOKE_NONE = 0;
private const int WTD_CHOICE_FILE = 1;
private const int WTD_STATEACTION_VERIFY = 1;
private const int WTD_STATEACTION_CLOSE = 2;
private const int WTD_SAFER_FLAG = 0x100;
private const string WinTrustDll = "wintrust.dll";
// WINTRUST_ACTION_GENERIC_VERIFY_V2 = {00AAC56B-CD44-11d0-8CC2-00C04FC295EE}
private static readonly Guid s_winTrustActionGenericVerifyV2 =
new(0x00AAC56B, 0xCD44, 0x11d0, 0x8C, 0xC2, 0x00, 0xC0, 0x4F, 0xC2, 0x95, 0xEE);
///
/// 调用 WinVerifyTrust 的简化封装。
///
private static int WinVerifyTrustNative(string filePath)
{
var fileInfo = new WINTRUST_FILE_INFO
{
cbStruct = Marshal.SizeOf(),
pcwszFilePath = filePath,
hFile = IntPtr.Zero,
pgKnownSubject = IntPtr.Zero
};
var pFileInfo = Marshal.AllocHGlobal(Marshal.SizeOf());
try
{
Marshal.StructureToPtr(fileInfo, pFileInfo, false);
var data = new WINTRUST_DATA
{
cbStruct = Marshal.SizeOf(),
pPolicyCallbackData = IntPtr.Zero,
pSIPClientData = IntPtr.Zero,
dwUIChoice = WTD_UI_NONE,
fdwRevocationChecks = WTD_REVOKE_NONE,
dwUnionChoice = WTD_CHOICE_FILE,
pFile = pFileInfo,
dwStateAction = WTD_STATEACTION_VERIFY,
hWVTStateData = IntPtr.Zero,
pwszURLReference = IntPtr.Zero,
dwProvFlags = WTD_SAFER_FLAG,
dwUIContext = 0
};
// 使用局部副本传递 ref 参数(static readonly 字段不能用于 ref)
var actionId = s_winTrustActionGenericVerifyV2;
var result = WinVerifyTrustCore(IntPtr.Zero, ref actionId, ref data);
// 清理状态句柄
data.dwStateAction = WTD_STATEACTION_CLOSE;
actionId = s_winTrustActionGenericVerifyV2;
WinVerifyTrustCore(IntPtr.Zero, ref actionId, ref data);
return result;
}
finally
{
Marshal.FreeHGlobal(pFileInfo);
}
}
///
/// Win32 WinVerifyTrust P/Invoke 核心调用。
/// AOT 安全的 DllImport 声明。
///
[DllImport(WinTrustDll, EntryPoint = "WinVerifyTrust", SetLastError = false)]
private static extern int WinVerifyTrustCore(
IntPtr hwnd,
ref Guid pgActionID,
ref WINTRUST_DATA pWVTData);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct WINTRUST_FILE_INFO
{
public int cbStruct;
[MarshalAs(UnmanagedType.LPWStr)]
public string? pcwszFilePath;
public IntPtr hFile;
public IntPtr pgKnownSubject;
}
[StructLayout(LayoutKind.Sequential)]
private struct WINTRUST_DATA
{
public int cbStruct;
public IntPtr pPolicyCallbackData;
public IntPtr pSIPClientData;
public int dwUIChoice;
public int fdwRevocationChecks;
public int dwUnionChoice;
public IntPtr pFile;
public int dwStateAction;
public IntPtr hWVTStateData;
public IntPtr pwszURLReference; // LPWSTR 作为 IntPtr 传递更安全
public int dwProvFlags;
public int dwUIContext;
}
}