Files

171 lines
7.6 KiB
PowerShell
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.
# 在 runner 上现场生成自签名代码签名证书,签 dev-build 的包内二进制与安装包。
#
# dev-build workflow 每次运行生成一张新证书,私钥只存在于 runner 内存中,运行结束即消失;
# 公钥导出成 .cer 进 artifact,由用户用 import-dev-trust.ps1 导入本机受信任存储。
# uiAccess=true 的 Server 没有可信签名会被 Windows 忽略,所以签名是 dev 包可用的前提。
#
# 环境变量:DEV_CERT_PATH(.cer 输出路径)。在 installer/ 目录下运行,与
# Sign-PackageBinaries-Local.ps1 相同的待签集合(server_exe/ 与 tsf_dll/ 下全部 EXE/DLL)。
#
# pwsh -File .\sign-dev-build.ps1 # 签包内二进制
# pwsh -File .\sign-dev-build.ps1 -Installer # 签编译完成的安装包
# pwsh -File .\sign-dev-build.ps1 -FilePath a.exe,b.exe # 签显式列出的文件(server 单独构建用)
[CmdletBinding()]
param(
[switch]$Installer,
[string[]]$FilePath
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
$CertificateSubject = 'CN=Metasequoia IME Dev Build Code Signing'
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 not found in the Windows SDK.'
}
return $candidate
}
# 同一次运行里重复调用本脚本时复用签名步骤生成的证书,保持包内二进制与安装包同一签名。
#
# 复用的判断必须来自「本次运行写入的标记」,不能只靠「证书看起来像 dev 证书」:改了 subject
# 之后,任何一台没被重置的机器上都会留下上次构建的证书,有效期 30 天,subject 和 EKU 全都
# 对得上。只按属性筛就会把上一个任务生成的私钥拿来签名,产出的包属于别的流水线。标记文件
# 放在 RUNNER_TEMP 下(托管 runner 每次都是新的临时目录),内容就是本次要复用的指纹。
function Get-RunMarkerPath {
$root = if ($env:RUNNER_TEMP) { $env:RUNNER_TEMP } else { [IO.Path]::GetTempPath() }
return Join-Path $root 'msime-dev-build-certificate.thumbprint'
}
function Get-RunCertificate {
$marker = Get-RunMarkerPath
if (-not (Test-Path -LiteralPath $marker -PathType Leaf)) {
return $null
}
$thumbprint = (Get-Content -LiteralPath $marker -Raw -ErrorAction SilentlyContinue)
if ($thumbprint) { $thumbprint = $thumbprint.Trim() }
if (-not $thumbprint) {
return $null
}
$certificate = Get-ChildItem -Path 'Cert:\CurrentUser\My' -ErrorAction SilentlyContinue |
Where-Object { $_.Thumbprint -eq $thumbprint -and $_.HasPrivateKey } |
Select-Object -First 1
if (-not $certificate) {
# 标记在但证书没了(比如被清理):当作本次没有证书,重新生成并覆盖标记。
Write-Host "The certificate recorded for this run is gone from the store: $thumbprint"
}
return $certificate
}
function New-RunCertificate {
# -Type CodeSigningCert 负责代码签名 EKU。有效期权衡的是两件事:artifact 只留 7 天,
# 但用户可能下载到本地后放很久才装,而安装器检查的是签名时刻证书是否有效;拉得太长又会
# 让一张随构建轮换的证书变成长期可信身份。90 天覆盖到拖延的安装,又不至于成为长期凭据。
# import-dev-trust.ps1 会打印实际的到期时间,安装器发现包内证书过期时也会明确报出来。
$certificate = New-SelfSignedCertificate `
-Type CodeSigningCert `
-Subject $CertificateSubject `
-FriendlyName 'Metasequoia IME Dev Build Code Signing' `
-KeyAlgorithm RSA `
-KeyLength 3072 `
-HashAlgorithm SHA256 `
-KeyUsage DigitalSignature `
-CertStoreLocation 'Cert:\CurrentUser\My' `
-NotBefore (Get-Date).AddHours(-1) `
-NotAfter (Get-Date).AddDays(90)
return $certificate
}
$certificate = Get-RunCertificate
if ($certificate) {
Write-Host "Reusing the certificate created earlier in this run: $($certificate.Thumbprint)"
}
else {
$certificate = New-RunCertificate
# 先落标记再往下走:后面任何一步失败重跑,都应该复用这一张而不是再生成一张。
Set-Content -LiteralPath (Get-RunMarkerPath) -Value $certificate.Thumbprint -NoNewline -Encoding utf8NoBOM
Write-Host "Created the dev-build signing certificate: $($certificate.Thumbprint)"
}
$certPath = $env:DEV_CERT_PATH
if (-not $certPath) {
throw 'DEV_CERT_PATH is not set; the workflow must say where to export the public key.'
}
$certDirectory = Split-Path -Parent $certPath
if (-not (Test-Path -LiteralPath $certDirectory -PathType Container)) {
New-Item -ItemType Directory -Path $certDirectory -Force | Out-Null
}
Export-Certificate -Cert $certificate -FilePath $certPath -Type CERT -Force | Out-Null
Write-Host "Public key exported to $certPath"
$signTool = Find-SignTool
if ($Installer) {
$candidates = @(Get-ChildItem -LiteralPath (Join-Path $PSScriptRoot 'Output') -Filter '*.exe' -File -ErrorAction SilentlyContinue)
if ($candidates.Count -eq 0) {
throw 'No installer found under Output\. Compile it first.'
}
if ($candidates.Count -gt 1) {
throw "Multiple installers under Output\, refusing to guess: $($candidates.Name -join ', ')"
}
$targets = @($candidates[0].FullName)
}
elseif ($FilePath) {
$targets = @(
foreach ($item in $FilePath) {
if (-not (Test-Path -LiteralPath $item -PathType Leaf)) {
throw "File to sign does not exist: $item"
}
(Resolve-Path -LiteralPath $item).Path
}
)
}
else {
$packageDirectories = @(
(Join-Path $PSScriptRoot 'server_exe'),
(Join-Path $PSScriptRoot 'tsf_dll')
)
foreach ($directory in $packageDirectories) {
if (-not (Test-Path -LiteralPath $directory -PathType Container)) {
throw "Missing staging directory, run Prepare-PackageFiles.ps1 first: $directory"
}
}
# 最外层的 @() 不能省:只有一个待签文件时管道结果是标量,.Count 在严格模式下会报错。
$targets = @(
@(
foreach ($directory in $packageDirectories) {
Get-ChildItem -LiteralPath $directory -Recurse -File |
Where-Object { $_.Extension -in @('.exe', '.dll') }
}
) | Sort-Object FullName -Unique
) | ForEach-Object { $_.FullName }
if ($targets.Count -eq 0) {
throw 'No EXE or DLL found to sign.'
}
}
$thumbprint = $certificate.Thumbprint
Write-Host "Signing $($targets.Count) file(s) with $thumbprint"
# 自签名证书的时间戳没有意义(测试机可能离线,且证书随 build 轮换),所以不加 /tr。
& $signTool sign /sha1 $thumbprint /s My /fd sha256 /v @($targets)
if ($LASTEXITCODE -ne 0) {
throw "signtool.exe signing failed with exit code $LASTEXITCODE"
}
foreach ($target in $targets) {
$signature = Get-AuthenticodeSignature -LiteralPath $target
if (-not $signature.SignerCertificate -or
$signature.SignerCertificate.Thumbprint -ne $thumbprint) {
throw "Signature missing or signed by the wrong certificate: $target"
}
}
Write-Host "Signed $($targets.Count) file(s); every signature verified against the dev certificate."