195 lines
7.5 KiB
PowerShell
195 lines
7.5 KiB
PowerShell
# 用本机自签名测试证书给安装文件(server_exe\ 与 tsf_dll\)里的 EXE/DLL 签名。
|
||
# 每台机器各自生成证书,本仓库不包含任何预置证书或指纹。
|
||
#
|
||
# 直接运行,不需要参数:
|
||
# pwsh -File .\Sign-PackageBinaries-Local.ps1
|
||
#
|
||
# 证书在第一次运行时自动创建,并装进本机受信任存储(这一步需要管理员 PowerShell;
|
||
# 没有管理员权限只会警告,签名照做,只是 signtool verify 的链校验过不了)。
|
||
# 之后每次运行都会复用同一张证书,已经被它签过的文件也会自动跳过。
|
||
#
|
||
# 每台机器各自生成自己的测试证书,不需要在机器之间搬私钥。想让纯测试机信任本机签名,
|
||
# 把证书的公钥导出成 .cer 拷过去,在那台机器上用 certutil 装进受信任根即可:
|
||
# certutil -addstore Root MetasequoiaImeLocalTest.cer
|
||
# certutil -addstore TrustedPublisher MetasequoiaImeLocalTest.cer
|
||
|
||
$ErrorActionPreference = 'Stop'
|
||
Set-StrictMode -Version Latest
|
||
|
||
$CertificateSubject = 'CN=Metasequoia IME Local Test Code Signing'
|
||
$CertificateValidityYears = 5
|
||
$CodeSigningEku = '1.3.6.1.5.5.7.3.3'
|
||
$TrustStores = @('Cert:\LocalMachine\Root', 'Cert:\LocalMachine\TrustedPublisher')
|
||
|
||
function Test-Administrator {
|
||
$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
|
||
$principal = [System.Security.Principal.WindowsPrincipal]::new($identity)
|
||
return $principal.IsInRole(
|
||
[System.Security.Principal.WindowsBuiltInRole]::Administrator)
|
||
}
|
||
|
||
function Find-SignTool {
|
||
$kitsBin = Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\bin'
|
||
$candidate = Get-ChildItem -LiteralPath $kitsBin -Directory -ErrorAction SilentlyContinue |
|
||
Sort-Object { try { [version]$_.Name } catch { [version]'0.0' } } -Descending |
|
||
ForEach-Object { Join-Path $_.FullName 'x64\signtool.exe' } |
|
||
Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } |
|
||
Select-Object -First 1
|
||
if (-not $candidate) {
|
||
throw '找不到 signtool.exe;请安装 Windows SDK。'
|
||
}
|
||
return $candidate
|
||
}
|
||
|
||
function Get-LocalTestCertificate {
|
||
$now = Get-Date
|
||
$candidates = @(
|
||
Get-ChildItem -Path 'Cert:\CurrentUser\My' |
|
||
Where-Object {
|
||
$_.Subject -eq $CertificateSubject -and
|
||
$_.HasPrivateKey -and
|
||
$_.NotBefore -le $now -and
|
||
$_.NotAfter -gt $now -and
|
||
($_.EnhancedKeyUsageList.ObjectId -contains $CodeSigningEku)
|
||
} |
|
||
Sort-Object NotAfter -Descending
|
||
)
|
||
if ($candidates.Count -eq 0) {
|
||
return $null
|
||
}
|
||
return $candidates[0]
|
||
}
|
||
|
||
function Add-CertificateTrust {
|
||
param([Parameter(Mandatory)]$Certificate)
|
||
|
||
$missing = @(
|
||
$TrustStores | Where-Object {
|
||
-not (Test-Path -LiteralPath (Join-Path $_ $Certificate.Thumbprint))
|
||
}
|
||
)
|
||
if ($missing.Count -eq 0) {
|
||
Write-Host '证书已在本机受信任存储中,跳过信任步骤。'
|
||
return
|
||
}
|
||
if (-not (Test-Administrator)) {
|
||
Write-Warning @"
|
||
证书还没进本机受信任存储,而写入它需要管理员权限:$($missing -join '、')
|
||
请用管理员 PowerShell 重新运行一次本脚本。在那之前签名依然有效,只是链校验会失败。
|
||
"@
|
||
return
|
||
}
|
||
|
||
$temporaryFile = Join-Path ([System.IO.Path]::GetTempPath()) `
|
||
('msime-local-test-{0}.cer' -f $Certificate.Thumbprint)
|
||
try {
|
||
Export-Certificate -Cert $Certificate -FilePath $temporaryFile -Type CERT -Force | Out-Null
|
||
foreach ($store in $missing) {
|
||
Import-Certificate -FilePath $temporaryFile -CertStoreLocation $store | Out-Null
|
||
Write-Host "已信任:$store"
|
||
}
|
||
}
|
||
finally {
|
||
if (Test-Path -LiteralPath $temporaryFile) {
|
||
Remove-Item -LiteralPath $temporaryFile -Force
|
||
}
|
||
}
|
||
}
|
||
|
||
function Initialize-LocalTestCertificate {
|
||
$certificate = Get-LocalTestCertificate
|
||
if ($certificate) {
|
||
Write-Host '已有本机测试证书,跳过创建。'
|
||
}
|
||
else {
|
||
# -Type CodeSigningCert 负责代码签名 EKU;Exportable 是为了以后能导出公钥给测试机。
|
||
$certificate = New-SelfSignedCertificate `
|
||
-Type CodeSigningCert `
|
||
-Subject $CertificateSubject `
|
||
-FriendlyName 'Metasequoia IME Local Test Code Signing' `
|
||
-KeyAlgorithm RSA `
|
||
-KeyLength 3072 `
|
||
-HashAlgorithm SHA256 `
|
||
-KeyExportPolicy Exportable `
|
||
-KeyUsage DigitalSignature `
|
||
-CertStoreLocation 'Cert:\CurrentUser\My' `
|
||
-NotAfter (Get-Date).AddYears($CertificateValidityYears)
|
||
Write-Host '已创建本机测试证书。'
|
||
}
|
||
Write-Host "指纹:$($certificate.Thumbprint)"
|
||
Write-Host "有效期至:$($certificate.NotAfter.ToString('yyyy-MM-dd HH:mm:ss'))"
|
||
Add-CertificateTrust -Certificate $certificate
|
||
return $certificate
|
||
}
|
||
|
||
function Test-SignedByLocalTestCertificate {
|
||
param([Parameter(Mandatory)][string]$LiteralPath)
|
||
|
||
$signature = Get-AuthenticodeSignature -LiteralPath $LiteralPath
|
||
if (-not $signature.SignerCertificate) {
|
||
return $false
|
||
}
|
||
# 不看 Status:测试证书没装进受信任根时链校验必然失败,但文件确实已由它签过名。
|
||
return $signature.SignerCertificate.Thumbprint.Replace(' ', '') -ieq $script:thumbprint
|
||
}
|
||
|
||
function Invoke-Sign {
|
||
param([Parameter(Mandatory)][string]$LiteralPath)
|
||
|
||
# 自签名证书的时间戳没有意义,测试机还可能离线,所以不加 /tr。
|
||
& $script:signTool sign /sha1 $script:thumbprint /s My /fd sha256 /v $LiteralPath
|
||
if ($LASTEXITCODE -ne 0) {
|
||
throw "signtool.exe 签名失败,退出码:$LASTEXITCODE"
|
||
}
|
||
|
||
& $script:signTool verify /pa /v $LiteralPath
|
||
if ($LASTEXITCODE -ne 0) {
|
||
Write-Warning "已签名但链校验失败(测试证书尚未被本机信任):$LiteralPath"
|
||
}
|
||
}
|
||
|
||
$packageDirectories = @(
|
||
(Join-Path $PSScriptRoot 'server_exe'),
|
||
(Join-Path $PSScriptRoot 'tsf_dll')
|
||
)
|
||
foreach ($directory in $packageDirectories) {
|
||
if (-not (Test-Path -LiteralPath $directory -PathType Container)) {
|
||
throw "待签名目录不存在,请先运行 Prepare-PackageFiles.ps1:$directory"
|
||
}
|
||
}
|
||
|
||
# 最外层的 @() 不能省:只有一个待签文件时,管道结果会是标量,.Count 在严格模式下会报错。
|
||
$targets = @(
|
||
@(
|
||
foreach ($directory in $packageDirectories) {
|
||
Get-ChildItem -LiteralPath $directory -Recurse -File |
|
||
Where-Object { $_.Extension -in @('.exe', '.dll') }
|
||
}
|
||
) | Sort-Object FullName -Unique
|
||
)
|
||
if ($targets.Count -eq 0) {
|
||
throw '没有找到待签名的 EXE 或 DLL。'
|
||
}
|
||
|
||
Write-Warning '使用本机测试证书签名,产物只能内部测试,不要对外分发。'
|
||
$certificate = Initialize-LocalTestCertificate
|
||
$script:thumbprint = $certificate.Thumbprint
|
||
$script:signTool = Find-SignTool
|
||
|
||
$signedCount = 0
|
||
$skippedCount = 0
|
||
foreach ($target in $targets) {
|
||
if (Test-SignedByLocalTestCertificate -LiteralPath $target.FullName) {
|
||
Write-Host "已由本机测试证书签名,跳过:$($target.FullName)"
|
||
$skippedCount++
|
||
continue
|
||
}
|
||
Write-Host "正在签名:$($target.FullName)"
|
||
Invoke-Sign -LiteralPath $target.FullName
|
||
$signedCount++
|
||
}
|
||
|
||
Write-Host "包内 EXE/DLL 本地签名完成:新签名 $signedCount 个,已签名跳过 $skippedCount 个,总计 $($targets.Count) 个文件。"
|
||
# 链校验警告会让最后一个 signtool 留下非零退出码;对本脚本来说整体是成功的。
|
||
$global:LASTEXITCODE = 0
|