171 lines
7.6 KiB
PowerShell
171 lines
7.6 KiB
PowerShell
# 在 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."
|