Files
2026-08-11 19:41:54 +08:00

228 lines
7.9 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
namespace LanDesktopPLONDS.Installer.Services;
/// <summary>
/// Authenticode 签名验证结果。
/// </summary>
internal enum AuthenticodeStatus
{
/// <summary>文件具有有效的 Authenticode 签名。</summary>
Signed,
/// <summary>文件未签名。</summary>
Unsigned,
/// <summary>签名无效或验证过程出错。</summary>
Invalid
}
/// <summary>
/// Authenticode 验证结果数据。
/// </summary>
internal sealed class AuthenticodeResult
{
/// <summary>验证状态。</summary>
public AuthenticodeStatus Status { get; init; }
/// <summary>签名者主体名称(仅在 Status == Signed 时有效)。</summary>
public string? SignerSubject { get; init; }
/// <summary>是否要求强制签名验证。</summary>
public bool EnforcementEnabled { get; init; }
public override string ToString() => Status switch
{
AuthenticodeStatus.Signed => $"Signed ({SignerSubject ?? "unknown"})",
AuthenticodeStatus.Unsigned => "Unsigned",
AuthenticodeStatus.Invalid => "Invalid",
_ => "Unknown"
};
}
/// <summary>
/// Windows AuthenticodeWinVerifyTrust签名验证器。
/// 使用 Win32 P/Invoke 验证 PE 文件的 Authenticode 签名,
/// 并通过 X509Certificate 提取签名者信息。
/// 默认为仅报告模式;设置 LANMOUNTAIN_INSTALLER_REQUIRE_SIGNED=1 启用强制验证。
/// </summary>
internal static class AuthenticodeVerifier
{
private const string RequireSignedEnvVar = "LANMOUNTAIN_INSTALLER_REQUIRE_SIGNED";
/// <summary>
/// 当前是否启用了强制签名验证。
/// </summary>
public static bool EnforcementEnabled =>
string.Equals(
Environment.GetEnvironmentVariable(RequireSignedEnvVar),
"1",
StringComparison.Ordinal);
/// <summary>
/// 对指定 PE 文件执行 Authenticode 签名验证。
/// </summary>
/// <param name="path">要验证的文件路径。</param>
/// <returns>验证结果,包含状态和签名者信息。</returns>
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);
/// <summary>
/// 调用 WinVerifyTrust 的简化封装。
/// </summary>
private static int WinVerifyTrustNative(string filePath)
{
var fileInfo = new WINTRUST_FILE_INFO
{
cbStruct = Marshal.SizeOf<WINTRUST_FILE_INFO>(),
pcwszFilePath = filePath,
hFile = IntPtr.Zero,
pgKnownSubject = IntPtr.Zero
};
var pFileInfo = Marshal.AllocHGlobal(Marshal.SizeOf<WINTRUST_FILE_INFO>());
try
{
Marshal.StructureToPtr(fileInfo, pFileInfo, false);
var data = new WINTRUST_DATA
{
cbStruct = Marshal.SizeOf<WINTRUST_DATA>(),
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);
}
}
/// <summary>
/// Win32 WinVerifyTrust P/Invoke 核心调用。
/// AOT 安全的 DllImport 声明。
/// </summary>
[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;
}
}