mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-08-19 01:53:24 +08:00
221 lines
7.5 KiB
Markdown
221 lines
7.5 KiB
Markdown
# 安装器安全模型
|
||
|
||
## 威胁模型
|
||
|
||
### 1. 清单篡改(Manifest Tampering)
|
||
|
||
**威胁**:攻击者篡改远程清单文件,注入恶意插件版本或下载地址。
|
||
|
||
**缓解措施**:
|
||
- `ManifestSignatureVerifier` 使用 Ed25519 离线签名验证清单完整性
|
||
- 签名约定:清单 URL 附加 `.sig` 后缀即为对应签名文件(分离签名)
|
||
- 公钥通过环境变量 `LANMOUNTAIN_PLONDS_MANIFEST_PUBKEY` 或编译时常量配置
|
||
- 密钥未配置时(占位符模式),验证自动跳过并记录警告——流水线可正常运行
|
||
|
||
### 2. 源注入(Source Injection via AddManifestSources)
|
||
|
||
**威胁**:攻击者通过 `AddManifestSources` 机制注入恶意清单源。
|
||
|
||
**缓解措施**:
|
||
- 清单源列表由应用配置控制,不接受运行时外部输入
|
||
- 每个源的清单均经过 `ManifestSignatureVerifier` 验证
|
||
- 恶意源的篡改清单无法通过签名验证
|
||
|
||
### 3. 校验和自循环(Checksum Self-Consistency Loop)
|
||
|
||
**威胁**:攻击者同时修改文件内容和对应校验和,使自校验失效。
|
||
|
||
**缓解措施**:
|
||
- 清单签名覆盖完整清单内容(包括校验和字段)
|
||
- 即使攻击者修改了校验和,签名验证仍会失败
|
||
- 签名使用 Ed25519 公钥密码学,无法伪造
|
||
|
||
### 4. DLL 植入(DLL Planting / DLL Hijacking)
|
||
|
||
**威胁**:攻击者在 `%LOCALAPPDATA%` 路径放置恶意 DLL,利用 `SetDllDirectory` 和 PATH 操纵加载优先级。
|
||
|
||
**缓解措施**:
|
||
- 已移除 `NativeDependencyBootstrapper`(原始 gzip 提取 + PATH 前置 + SetDllDirectory 机制)
|
||
- 原生库现通过 `PublishSingleFile` + `IncludeNativeLibrariesForSelfExtract` 正常打包
|
||
- `EnableCompressionInSingleFile` 确保原生库压缩存储在单文件中
|
||
- 运行时自动解压到安全的临时目录,无 DLL 搜索路径操纵
|
||
|
||
### 5. PE 文件完整性(Authenticode Verification)
|
||
|
||
**威胁**:安装器捆绑或下载的可执行文件被替换或篡改。
|
||
|
||
**缓解措施**:
|
||
- `AuthenticodeVerifier` 使用 WinVerifyTrust API 验证 PE 文件签名
|
||
- 提取签名者主体信息用于审计记录
|
||
- 默认为**报告模式**(不阻止执行)
|
||
- 环境变量 `LANMOUNTAIN_INSTALLER_REQUIRE_SIGNED=1` 启用强制验证模式
|
||
|
||
## 组件架构
|
||
|
||
### ManifestSignatureVerifier
|
||
|
||
```
|
||
ManifestSignatureVerifier
|
||
├── Verify(byte[] manifestBytes, string signatureBase64) → bool
|
||
├── GetSignatureUrl(string manifestUrl) → string
|
||
└── IsConfigured → bool
|
||
```
|
||
|
||
**技术选型**:
|
||
- **Ed25519**(.NET 10 BCL 内置,`System.Security.Cryptography.Ed25519`)
|
||
- 零外部 NuGet 包依赖
|
||
- AOT 安全(无反射,无动态代码生成)
|
||
- 签名大小:64 字节;公钥大小:32 字节
|
||
|
||
### AuthenticodeVerifier
|
||
|
||
```
|
||
AuthenticodeVerifier
|
||
├── VerifyFile(string path) → AuthenticodeResult
|
||
├── EnforcementEnabled → bool
|
||
└── AuthenticodeResult { Status, SignerSubject }
|
||
```
|
||
|
||
**技术选型**:
|
||
- Win32 `WinVerifyTrust` P/Invoke(`WINTRUST_ACTION_GENERIC_VERIFY_V2`)
|
||
- `X509Certificate.CreateFromSignedFile` 提取签名者信息
|
||
- AOT 安全的 DllImport 声明
|
||
|
||
## 密钥配置运行手册
|
||
|
||
### 1. 密钥对生成
|
||
|
||
```powershell
|
||
# 使用 .NET 10 内置 Ed25519 工具生成密钥对
|
||
# 此脚本生成私钥和公钥文件
|
||
|
||
$ErrorActionPreference = 'Stop'
|
||
|
||
# 生成 Ed25519 密钥对
|
||
$privateKeyBytes = [byte[]]::new(64) # Ed25519 种子 + 公钥
|
||
$publicKeyBytes = [byte[]]::new(32)
|
||
|
||
# 使用 .NET 的 Ed25519 类生成密钥
|
||
Add-Type -AssemblyName System.Security.Cryptography
|
||
$key = [System.Security.Cryptography.Ed25519]::GenerateKeyPair()
|
||
$privateKeyBytes = $key.PrivateKey
|
||
$publicKeyBytes = $key.PublicKey
|
||
|
||
# 保存密钥对(私钥需妥善保管,不纳入版本控制)
|
||
$keysDir = ".\keys"
|
||
New-Item -ItemType Directory -Path $keysDir -Force | Out-Null
|
||
|
||
[System.IO.File]::WriteAllBytes("$keysDir\manifest_signing.key", $privateKeyBytes)
|
||
[System.IO.File]::WriteAllBytes("$keysDir\manifest_signing.pub", $publicKeyBytes)
|
||
|
||
Write-Host "密钥对已生成:"
|
||
Write-Host " 私钥: $keysDir\manifest_signing.key"
|
||
Write-Host " 公钥: $keysDir\manifest_signing.pub"
|
||
Write-Host ""
|
||
Write-Host "⚠️ 请务必将私钥文件从版本控制中排除!"
|
||
```
|
||
|
||
### 2. 公钥部署
|
||
|
||
将公钥(Base64 编码)设置为编译时常量或 CI 环境变量:
|
||
|
||
```powershell
|
||
# 读取公钥并转换为 Base64
|
||
$publicKey = [System.IO.File]::ReadAllBytes("$keysDir\manifest_signing.pub")
|
||
$publicKeyBase64 = [System.Convert]::ToBase64String($publicKey)
|
||
|
||
Write-Host "公钥 Base64: $publicKeyBase64"
|
||
```
|
||
|
||
**部署位置**:
|
||
- 开发环境:设置环境变量 `LANMOUNTAIN_PLONDS_MANIFEST_PUBKEY`
|
||
- CI/CD:在构建管道中注入环境变量
|
||
- 生产环境:编译时嵌入或安全存储在配置中心
|
||
|
||
### 3. 清单签名(CI/CD 集成)
|
||
|
||
```powershell
|
||
# 清单签名 PowerShell 片段(用于 CI/CD 管道)
|
||
param(
|
||
[Parameter(Mandatory = $true)]
|
||
[string] $ManifestPath,
|
||
|
||
[Parameter(Mandatory = $true)]
|
||
[string] $PrivateKeyPath
|
||
)
|
||
|
||
$ErrorActionPreference = 'Stop'
|
||
|
||
# 读取清单内容
|
||
$manifestBytes = [System.IO.File]::ReadAllBytes($ManifestPath)
|
||
|
||
# 读取私钥
|
||
$privateKeyBytes = [System.IO.File]::ReadAllBytes($PrivateKeyPath)
|
||
|
||
# 使用 Ed25519 签名
|
||
$signature = [System.Security.Cryptography.Ed25519]::SignData($manifestBytes, $privateKeyBytes)
|
||
$signatureBase64 = [System.Convert]::ToBase64String($signature)
|
||
|
||
# 写入签名文件(manifest.json.sig)
|
||
$sigPath = "$ManifestPath.sig"
|
||
[System.IO.File]::WriteAllText($sigPath, $signatureBase64)
|
||
|
||
Write-Host "清单签名完成:"
|
||
Write-Host " 清单: $ManifestPath"
|
||
Write-Host " 签名: $sigPath"
|
||
Write-Host " 签名者: CI/CD Pipeline"
|
||
```
|
||
|
||
### 4. CI/CD 管道集成示例
|
||
|
||
```yaml
|
||
# GitHub Actions 示例
|
||
- name: Sign manifest
|
||
run: |
|
||
$manifest = Get-Content "releases/manifest.json" -Raw
|
||
$manifestBytes = [System.Text.Encoding]::UTF8.GetBytes($manifest)
|
||
$key = [System.Convert]::FromBase64String("${{ secrets.MANIFEST_SIGNING_KEY }}")
|
||
$sig = [System.Security.Cryptography.Ed25519]::SignData($manifestBytes, $key)
|
||
[System.IO.File]::WriteAllText("releases/manifest.json.sig", [System.Convert]::ToBase64String($sig))
|
||
```
|
||
|
||
## 环境变量参考
|
||
|
||
| 变量名 | 值 | 说明 |
|
||
|--------|-----|------|
|
||
| `LANMOUNTAIN_PLONDS_MANIFEST_PUBKEY` | Base64 编码的 Ed25519 公钥 | 清单签名验证公钥 |
|
||
| `LANMOUNTAIN_INSTALLER_REQUIRE_SIGNED` | `1` | 启用 Authenticode 强制验证(默认关闭) |
|
||
|
||
## 测试
|
||
|
||
运行安装器安全模块测试:
|
||
|
||
```bash
|
||
dotnet test LanMountainDesktop.Tests.csproj --filter "FullyQualifiedName~InstallerSecurity"
|
||
```
|
||
|
||
测试覆盖:
|
||
- Ed25519 签名验证(有效/篡改/空输入)
|
||
- 签名 URL 计算
|
||
- Authenticode 验证(签名文件/未签名文件/不存在文件)
|
||
- 强制验证模式读取
|
||
|
||
## 已移除的安全风险
|
||
|
||
### NativeDependencyBootstrapper(已删除)
|
||
|
||
**原始行为**:
|
||
1. 从嵌入资源中 gzip 解压 `libHarfBuzzSharp.dll` 和 `libSkiaSharp.dll` 到 `%LOCALAPPDATA%\LanDesktopPLONDS\Installer\native\{arch}\{version}\`
|
||
2. 使用 `SetDllDirectory` 将该路径添加到进程 DLL 搜索路径首位
|
||
3. 使用 `NativeLibrary.Load` 显式加载 DLL
|
||
|
||
**安全风险**:
|
||
- DLL 植入:攻击者可在 `%LOCALAPPDATA%` 路径放置恶意 DLL
|
||
- 路径操纵:`SetDllDirectory` 改变进程级 DLL 搜索顺序
|
||
- 版本目录可预测:攻击者可预先创建目标版本目录
|
||
|
||
**替代方案**:
|
||
- `PublishSingleFile` + `IncludeNativeLibrariesForSelfExtract` 自动打包原生库
|
||
- `EnableCompressionInSingleFile` 压缩存储
|
||
- 运行时解压到安全的临时目录(由 .NET 运行时管理)
|