Files
LanMountainDesktop/docs/INSTALLER_SECURITY.md
2026-07-31 11:43:50 +09:00

221 lines
7.5 KiB
Markdown
Raw 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.
# 安装器安全模型
## 威胁模型
### 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 运行时管理)