From 9bc1c925b2fe3f49018f320d6d2ff84cd32e443e Mon Sep 17 00:00:00 2001 From: fanlusky <1730976608@qq.com> Date: Thu, 17 Sep 2026 23:10:54 +0800 Subject: [PATCH] =?UTF-8?q?fix(installer):=20=E6=AD=A3=E5=BC=8F=E7=AD=BE?= =?UTF-8?q?=E5=90=8D=E8=A6=86=E7=9B=96=E5=8C=85=E5=86=85=E5=85=A8=E9=83=A8?= =?UTF-8?q?=20EXE/DLL?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 此前只有 MetasequoiaImeServer.exe 拿到真实签名——它是唯一一个技术上必须签的, uiAccess=true 没有可信 Authenticode 签名就会被 Windows 忽略。其余随包分发的 二进制全部未签名:设置页、表情面板、手写面板、键盘面板、看门狗、词库回放, 以及 libcurl/sqlite3/zlib/WebView2Loader 四个 DLL 和两个架构的 TSF DLL。 Microsoft Defender 会按信誉拦下这些未签名的可执行文件,用户看到的现象是输入法 装完了、某个面板却打不开。 package-simplysign.ps1 和 release workflow 现在都签 server_exe\ 与 tsf_dll\ 下的 全部 EXE/DLL,共 13 个文件,加上外层安装包。 所有文件在一次 signtool sign 调用里签完,而不是逐个调用:SimplySign 可能对每次 调用弹一次 PIN,逐个签会把本地打包变成十几次手机确认。签名配额按文件计,合并 调用不会多花配额;证书每月 3000 次,每次发布十几个仍然远在额度之内。 --- .github/workflows/release.yml | 10 ++-- installer/README.md | 13 +++-- installer/package-simplysign.ps1 | 89 +++++++++++++++++++++++--------- 3 files changed, 81 insertions(+), 31 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f053f8de..3a3edf54 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,7 +4,7 @@ name: Release # # The whole cycle runs inside the one run that the original push started. land-release-pr.sh merges with GITHUB_TOKEN, whose pushes do not start workflow runs, so the merge cannot produce a second Release run queued behind this one on the concurrency group. # -# This spends a code signing signature on every releasable merge to main. The certificate allows 3000 signatures a month, so the quota is not what constrains this; what it spends is release hygiene -- see docs/product-release.md. +# This spends code signing signatures on every releasable merge to main -- one per payload EXE/DLL plus one for the installer, so roughly fifteen per release rather than one. The certificate allows 3000 signatures a month, so the quota is still not what constrains this; what it spends is release hygiene -- see docs/product-release.md. # # workflow_dispatch stays, and is now a repair path rather than the normal one. It builds an existing draft, which is what a run that failed after creating the draft leaves behind. on: @@ -499,7 +499,11 @@ jobs: --windows-commit "$env:WINDOWS_COMMIT" --output installer/app_data/product-manifest.json - - name: Sign the uiAccess server + # Every payload EXE/DLL, not just the uiAccess server. The server is the only one that + # technically requires a signature (uiAccess=true is ignored without one), but Microsoft + # Defender blocks the unsigned helper processes -- settings page, emoji panel, watchdog -- + # and the user sees an input method that installs and then cannot open its own panels. + - name: Sign the payload binaries if: ${{ steps.signing.outputs.signing_enabled == 'true' }} shell: pwsh working-directory: installer @@ -508,7 +512,7 @@ jobs: CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PASSWORD }} CERTIFICATE_THUMBPRINT: ${{ vars.WINDOWS_SIGNING_CERTIFICATE_THUMBPRINT }} TIMESTAMP_URL: ${{ vars.WINDOWS_SIGNING_TIMESTAMP_URL || 'http://timestamp.digicert.com' }} - run: ../scripts/ci/sign-binaries.ps1 -Path 'server_exe/MetasequoiaImeServer.exe' + run: ../scripts/ci/sign-binaries.ps1 -Recurse -Path 'server_exe', 'tsf_dll' - name: Install the Chinese language file for Inno Setup shell: pwsh diff --git a/installer/README.md b/installer/README.md index 98d5c6f7..180a5f54 100644 --- a/installer/README.md +++ b/installer/README.md @@ -2,7 +2,7 @@ 本目录的脚本从 Windows 合仓目录收集产物、签名、用 Inno Setup 打成安装包。它服务两条流程: -- **正式发布**:由 `MSIME-Windows` 的 release workflow 驱动,用真实证书签名 `MetasequoiaImeServer.exe`(保证 `uiAccess` 生效)和最终安装包;产物是挂在 Release 上的 `MetasequoiaIME_Setup_v<版本>.exe`,并携带与 Release 二进制匹配的 PDB 符号文件。见下面「CI 契约」 +- **正式发布**:由 `MSIME-Windows` 的 release workflow 驱动,用真实证书签名包内全部 EXE/DLL 和最终安装包;产物是挂在 Release 上的 `MetasequoiaIME_Setup_v<版本>.exe`,并携带与 Release 二进制匹配的 PDB 符号文件。见下面「CI 契约」 - **本地测试**:手工跑,用本机自签名证书,用来在自己机器上验证安装流程。本文其余部分讲的是这条 版本号不是固定的,由 `Prepare-PackageFiles.ps1` 的 `-TargetVersion` 决定,默认 `0.0.1`;正式发布时 CI 传入真实版本。 @@ -86,10 +86,17 @@ pwsh -File .\package-simplysign.ps1 1.2.3 命令行版本会同时写入本轮 TSF DLL 的版本资源和安装包元数据(脚本结束后恢复源码模板)。脚本会 增量编译 TSF、Server 和设置页,制作完整包,用 `http://time.certum.pl` 的 RFC 3161 时间戳签名 -`MetasequoiaImeServer.exe`(使 `uiAccess=true` 生效),编译 Inno Setup 安装包,再签名并校验 -最终安装包。产物位于 `Output\MetasequoiaIME_Setup_v1.2.3.exe`;脚本只产出文件,不会自动启动 +`server_exe\` 和 `tsf_dll\` 下的**全部 EXE/DLL**,编译 Inno Setup 安装包,再签名并校验最终 +安装包。产物位于 `Output\MetasequoiaIME_Setup_v1.2.3.exe`;脚本只产出文件,不会自动启动 安装程序。 +**为什么整包都要签。** `MetasequoiaImeServer.exe` 是唯一一个技术上必须签的——`uiAccess=true` +没有可信 Authenticode 签名就会被 Windows 忽略。但其余辅助进程(设置页、表情面板、手写面板、 +键盘面板、看门狗、词库回放)和随包分发的 DLL 不签名会被 Microsoft Defender 按信誉拦下, +用户看到的现象是输入法装完了、某个面板却打不开。所有文件在一次 `signtool sign` 调用里签完: +SimplySign 可能对每次调用弹一次 PIN,逐个文件调用会变成十几次手机确认;签名配额按文件计, +合并调用不会多花配额。 + 默认不把 PDB 放入安装包;需要与正式 CI 一致的符号包时加 `-IncludeSymbols`。如果 SimplySign 同时暴露了多张有效的 Certum Code Signing 证书,脚本会列出它们并要求用 `-CertificateThumbprint <指纹>` 明确选择。可用 `-Reconfigure` 强制重新配置 CMake,也可用 diff --git a/installer/package-simplysign.ps1 b/installer/package-simplysign.ps1 index 722f7faf..96e4bd66 100644 --- a/installer/package-simplysign.ps1 +++ b/installer/package-simplysign.ps1 @@ -125,36 +125,77 @@ function Get-SimplySignCertificate { function Invoke-SimplySign { param( - [Parameter(Mandatory)][string]$LiteralPath, + [Parameter(Mandatory)][string[]]$LiteralPath, [Parameter(Mandatory)]$Certificate ) - if (-not (Test-Path -LiteralPath $LiteralPath -PathType Leaf)) { - throw "待签名文件不存在:$LiteralPath" - } + # 最外层的 @() 不能省:只有一个待签文件时管道结果是标量,.Count 在严格模式下会报错。 + $resolvedPaths = @( + @( + foreach ($item in $LiteralPath) { + if (-not (Test-Path -LiteralPath $item -PathType Leaf)) { + throw "待签名文件不存在:$item" + } + (Resolve-Path -LiteralPath $item).Path + } + ) | Sort-Object -Unique + ) - $resolvedPath = (Resolve-Path -LiteralPath $LiteralPath).Path $thumbprint = $Certificate.Thumbprint -replace '\s', '' - Write-Host "正在使用 SimplySign 签名:$resolvedPath" - & $script:signTool sign /sha1 $thumbprint /s My /fd sha256 /tr $TimestampUrl /td sha256 /v $resolvedPath + Write-Host "正在使用 SimplySign 签名 $($resolvedPaths.Count) 个文件:" + $resolvedPaths | ForEach-Object { Write-Host " $_" } + + # 一次 signtool 调用签完所有文件。SimplySign 可能对每次调用弹一次 PIN,逐个文件调用会让 + # 整包签名变成十几次手机确认。签名配额按文件计,合并调用不会多花配额。 + & $script:signTool sign /sha1 $thumbprint /s My /fd sha256 /tr $TimestampUrl /td sha256 /v @($resolvedPaths) if ($LASTEXITCODE -ne 0) { - throw "signtool.exe 签名失败,退出码:$LASTEXITCODE;文件:$resolvedPath" + throw "signtool.exe 签名失败,退出码:$LASTEXITCODE" } - & $script:signTool verify /pa /all /v $resolvedPath - if ($LASTEXITCODE -ne 0) { - throw "signtool.exe 签名校验失败,退出码:$LASTEXITCODE;文件:$resolvedPath" + foreach ($resolvedPath in $resolvedPaths) { + & $script:signTool verify /pa /all /v $resolvedPath + if ($LASTEXITCODE -ne 0) { + throw "signtool.exe 签名校验失败,退出码:$LASTEXITCODE;文件:$resolvedPath" + } + + $signature = Get-AuthenticodeSignature -LiteralPath $resolvedPath + if (-not $signature.SignerCertificate -or + (($signature.SignerCertificate.Thumbprint -replace '\s', '') -ine $thumbprint)) { + throw "签名证书指纹与选择的 SimplySign 证书不一致:$resolvedPath" + } + if (-not $signature.TimeStamperCertificate) { + throw "签名中没有可信时间戳:$resolvedPath" + } + Write-Host "签名有效:$resolvedPath" + } +} + +# 包内所有随安装包落到用户磁盘上的 EXE/DLL。只签 MetasequoiaImeServer.exe 是不够的: +# 未签名的辅助进程(设置页、表情面板、看门狗等)和随包分发的 DLL 会被 Microsoft Defender +# 的 SmartScreen/信誉判定拦下,而它们是用户实际会启动的程序。 +function Get-PayloadBinaryPaths { + $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" + } } - $signature = Get-AuthenticodeSignature -LiteralPath $resolvedPath - if (-not $signature.SignerCertificate -or - (($signature.SignerCertificate.Thumbprint -replace '\s', '') -ine $thumbprint)) { - throw "签名证书指纹与选择的 SimplySign 证书不一致:$resolvedPath" + $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,请先运行 Prepare-PackageFiles.ps1。' } - if (-not $signature.TimeStamperCertificate) { - throw "签名中没有可信时间戳:$resolvedPath" - } - Write-Host "签名有效:$($signature.SignerCertificate.Subject)" + return @($targets.FullName) } # Fail before compiling anything if SimplySign is not connected. Merely having its desktop process @@ -218,12 +259,10 @@ try { -NoticesDirectory . ` -HelpCodeDirectory engine/helpcode - # This is the only payload binary requiring a real signature: uiAccess=true is ignored by - # Windows unless the executable carries a trusted Authenticode signature. The outer installer is - # signed after compilation. This deliberately matches the formal release workflow. - Invoke-SimplySign ` - -LiteralPath (Join-Path $PSScriptRoot 'server_exe\MetasequoiaImeServer.exe') ` - -Certificate $certificate + # 整包签名,而不只是 MetasequoiaImeServer.exe。它是唯一一个「必须」签的——uiAccess=true + # 没有可信签名就会被 Windows 忽略——但其余辅助进程和 DLL 不签名会被 Defender 拦截, + # 用户看到的就是输入法装完之后某个面板打不开。外层安装包在编译之后单独签。 + Invoke-SimplySign -LiteralPath (Get-PayloadBinaryPaths) -Certificate $certificate & (Join-Path $PSScriptRoot 'Compile-Installer.ps1') -IsccPath $IsccPath if (-not (Test-Path -LiteralPath $compiledInstallerPath -PathType Leaf)) {