mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-08-18 15:33:33 +08:00
228 lines
7.9 KiB
C#
228 lines
7.9 KiB
C#
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 Authenticode(WinVerifyTrust)签名验证器。
|
||
/// 使用 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;
|
||
}
|
||
}
|