fix(desktop): clarify signing prerequisites and portable fixtures

This commit is contained in:
winewill committed 2026-10-22 19:37:19 +08:00
1 parent 6c5d7c918a
commit 5a45e0a448
12 files changed
+27 -15

No files matched your search

+2 -2
View File
@@ -45,8 +45,8 @@
en: b42f103358d0dfd7
zh: a534164ad9eb00da
/desktop-packaging/runtime-file-selection-and-installation-checks:
en: 10c919aa787c1884
zh: e46c4838793224dd
en: abf7aa7bc2d37ac3
zh: d7011883eec14d89
/desktop-packaging/desktop-runtime-preparation:
en: 59783a64552d4464
zh: d7876d51b4015578
+1 -1
View File
@@ -265,7 +265,7 @@ When extraction fails, the installer writes a report with the 7-Zip result and i
Windows packaging fixes the 7-Zip filter to `BCJ` for compatibility with the bundled NSIS decoder. This preserves ARM64 binaries carried by dependencies in x64 installers; automatic ARM64 filtering produces entries that this decoder cannot extract.
`DSH_DESKTOP_WINDOWS_COMPRESSION_LEVEL` in the Windows dotenv or machine preset selects installer archive compression from `0` to `9`. If omitted, the existing `ELECTRON_BUILDER_COMPRESSION_LEVEL` environment override remains effective, otherwise the default is `3` for both test and production. The manual Windows workflow's `compression_level` dropdown defaults to `3` and also offers `7`, matching the Mac ZIP choices; it overrides the file setting for that run. Direct local builds retain explicit machine settings. Nonempty workflow compression overrides reject the other target platform. Lower levels trade installer size for packaging time; qualify the complete signed workflow and artifact size before changing a release preset.
`DSH_DESKTOP_WINDOWS_COMPRESSION_LEVEL` in the Windows dotenv or machine preset selects installer archive compression from `0` to `9`. If omitted, the existing `ELECTRON_BUILDER_COMPRESSION_LEVEL` environment override remains effective, otherwise the default is `3` for both test and production. The manual Windows workflow's `compression_level` dropdown defaults to `3` and also offers `7`, matching the Mac ZIP choices; it overrides the file setting for that run. Direct local builds retain explicit machine settings. Any defined Mac ZIP workflow override, including an empty value, rejects Windows; nonempty Windows workflow overrides reject macOS. Lower levels trade installer size for packaging time; qualify the complete signed workflow and artifact size before changing a release preset.
NSIS removes its temporary extraction tree during installation, before the completion page or an automatic launch. The installed production packages remain ordinary files; startup does not extract them again. Installation still writes the complete application tree.
+1 -1
View File
@@ -265,7 +265,7 @@ Windows 安装器在启动时和选定目标目录后检查应用是否正在运
Windows 打包将 7-Zip 过滤器固定为 `BCJ`,以兼容内置的 NSIS 解码器。这样可以保留 x64 安装包中由依赖携带的 ARM64 二进制文件;自动 ARM64 过滤会生成该解码器无法解压的条目。
Windows dotenv 或机器 preset 中的 `DSH_DESKTOP_WINDOWS_COMPRESSION_LEVEL` 选择 `0` 至 `9` 的安装包归档压缩等级。省略时,现有 `ELECTRON_BUILDER_COMPRESSION_LEVEL` 环境覆盖仍然生效,否则 test 和 production 均默认为 `3`。手动 Windows workflow 的 `compression_level` 下拉默认选 `3`,也提供 `7`,与 Mac ZIP 选项一致;它对本轮覆盖文件中的设置。直接本地打包保留显式机器配置。非空的 workflow 压缩覆盖值会拒绝用于另一目标平台。较低等级以安装包体积换取打包时间;修改发布 preset 前应验证完整签名 workflow 和产物体积。
Windows dotenv 或机器 preset 中的 `DSH_DESKTOP_WINDOWS_COMPRESSION_LEVEL` 选择 `0` 至 `9` 的安装包归档压缩等级。省略时,现有 `ELECTRON_BUILDER_COMPRESSION_LEVEL` 环境覆盖仍然生效,否则 test 和 production 均默认为 `3`。手动 Windows workflow 的 `compression_level` 下拉默认选 `3`,也提供 `7`,与 Mac ZIP 选项一致;它对本轮覆盖文件中的设置。直接本地打包保留显式机器配置。任何已定义的 Mac ZIP workflow 覆盖值(包括空值)均拒绝用于 Windows;非空的 Windows workflow 覆盖值拒绝用于 macOS。较低等级以安装包体积换取打包时间;修改发布 preset 前应验证完整签名 workflow 和产物体积。
NSIS 在安装阶段清理临时解压目录,完成后才显示完成页或自动启动应用。已安装的生产依赖保持为普通文件;启动时不会再次解压。安装仍会写入完整的应用目录树。
@@ -11,7 +11,7 @@ export function localMacOSPackageEnvironment(environment?: NodeJS.ProcessEnv): N
* Read a machine preset when selected, otherwise the target's required dotenv; no ambient release fallback.
* PACKAGE_TEST_RELEASE_ID overrides the test preset only; blank retains its configured ID.
* PACKAGE_MACOS_ZIP_COMPRESSION_LEVEL selects 3 or 7 for Mac workflow builds, overriding the file setting.
* Nonempty workflow compression overrides reject the other platform.
* Any defined Mac ZIP override rejects Windows, including an empty value; nonempty Windows overrides reject macOS.
* PACKAGE_WINDOWS_COMPRESSION_LEVEL selects 3 or 7 for Windows workflow builds; blank retains the file setting.
* @param platform Target platform.
* @param environment Parent environment, retained only for unrelated build tools.
@@ -37,7 +37,7 @@ export function localMacOSPackageEnvironment(environment = process.env) {
/**
* Read a machine preset when selected, otherwise the target's required dotenv; no ambient release fallback.
* PACKAGE_MACOS_ZIP_COMPRESSION_LEVEL selects 3 or 7 for Mac workflow builds, overriding the file setting.
* Nonempty workflow compression overrides reject the other platform.
* Any defined Mac ZIP override rejects Windows, including an empty value; nonempty Windows overrides reject macOS.
* PACKAGE_TEST_RELEASE_ID overrides the test preset only; blank retains its configured ID.
* PACKAGE_WINDOWS_COMPRESSION_LEVEL selects 3 or 7 for Windows workflow builds; blank retains the file setting.
* @param {'win32' | 'darwin'} platform Target platform.
@@ -81,7 +81,7 @@ export function windowsTokenIdentityFailure(result: Exclude<WindowsTokenIdentity
'provider-unavailable': 'The configured eToken provider is unavailable; check the SafeNet driver.',
'container-unavailable': 'The configured container could not be opened; after replacing the UKey, check its container name and visibility.',
'certificate-mismatch': 'The returned container public keys do not match the configured certificate; check the certificate and container after replacing the UKey.',
unknown: 'The container public identity could not be confirmed; check device visibility and SafeNet public-information support.',
unknown: 'The container public identity could not be confirmed; check that PowerShell 7 (pwsh) can start, device visibility and SafeNet public-information support.',
}
return `${reasons[result]} No PIN was supplied and no signing was attempted by this check. Signing stopped; device lock and PIN retry counts are unknown.`
}
@@ -376,7 +376,7 @@ it.each(['3', '7'])('applies the workflow ZIP choice %s over the Mac file settin
})
})
it.each(['3', '7'])('rejects Mac workflow ZIP choice %s on Windows before reading settings', (level) => {
it.each(['', '3', '7'])('rejects Mac workflow ZIP choice %j on Windows before reading settings', (level) => {
expect(() => loadDesktopPackageEnvironment('win32', { PACKAGE_MACOS_ZIP_COMPRESSION_LEVEL: level }, 'missing-settings'))
.toThrow('PACKAGE_MACOS_ZIP_COMPRESSION_LEVEL requires macOS')
})
@@ -5,7 +5,7 @@ import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { rootCertificates } from 'node:tls'
import { expect, it, vi } from 'vitest'
import { inspectWindowsTokenIdentity } from '../scripts/windows-token-identity.ts'
import { inspectWindowsTokenIdentity, windowsTokenIdentityFailure } from '../scripts/windows-token-identity.ts'
const launch = vi.hoisted(() => vi.fn(async (_file: string, _args: string[], _options: { env: NodeJS.ProcessEnv; timeout: number }) => ({ stdout: '', stderr: '' })))
vi.mock('node:child_process', async (importOriginal) => {
@@ -78,6 +78,18 @@ it('reports a helper timeout or stderr as unknown without retry', async () => {
expect(await inspectWindowsTokenIdentity(input)).toBe('unknown')
})
it('reports a missing query launcher without exposing process diagnostics or retrying', async () => {
launch.mockClear()
launch.mockRejectedValueOnce(Object.assign(new Error('private launcher diagnostic'), { code: 'ENOENT' }))
const result = await inspectWindowsTokenIdentity(input)
expect(result).toBe('unknown')
expect(launch).toHaveBeenCalledOnce()
const message = windowsTokenIdentityFailure('unknown')
expect(message).toContain('PowerShell 7 (pwsh) can start')
expect(message).not.toContain('private launcher diagnostic')
expect(message).toContain('no signing was attempted')
})
it.skipIf(process.platform !== 'win32')('exercises native control flow with a fake CSP and never constructs the native adapter', () => {
const root = mkdtempSync(join(tmpdir(), 'token-public-test-'))
try {
@@ -27,8 +27,8 @@
en: 4b5548818375eb9a
zh: 8c975adbb1dca9c8
/windows-signing-and-runner-operations/signing-and-timestamp-completion:
en: 74003162e4c21446
zh: a8392e981eaec99c
en: a8d23c931cafc236
zh: 28fd4860549cdc7b
/windows-signing-and-runner-operations/failure-records-and-operator-recovery:
en: 5401b605a2f3a44e
zh: fc1f6ee667bcd20d
+1 -1
View File
@@ -66,7 +66,7 @@ Follow the [version and build instructions](../packaging/README.md#release-versi
Signed Windows builds run a supervised signing preflight before compilation or dependency preparation. Static configuration, certificate validity, audit storage, compiler availability and any retained signing interlock are checked without token access. The local .NET Framework C# compiler creates a small private probe; the production signer signs it once, and verification requires the configured certificate and a timestamp before building continues. The probe is never executed. The overall 60-second preflight deadline includes timestamp attempts; deadline expiry, hardware signing errors and verification failures stop the run without another hardware attempt. Success proves the current signing path works, not that the PIN was independently authenticated: SafeNet may reuse login state. Do not log out or repeat authentication to test the PIN. `--check`, preparation-only and `--unsigned` modes do not run this hardware preflight; unsigned artifacts remain ineligible for release upload. Automated regression tests use a fake signer; release operators qualify real hardware separately.
Before compiling the probe, a separate noninteractive process reads public keys from the fixed eToken CSP using `CRYPT_VERIFYCONTEXT | CRYPT_SILENT`. It receives the container name but no PIN or signing credentials, and has a 15-second deadline within the existing preflight deadline. Both RSA key slots must be read completely (an absent slot is allowed); at least one public key must match the configured certificate. Provider/container access failure, unsupported public reads, malformed output, timeout or mismatch stops before hardware signing. A container-access error does not establish that the device or container is absent. Public information can be cached by middleware: a match does not authenticate the device, validate the PIN or establish remaining retries. This path is covered by a simulated CSP; the installed SafeNet version still requires separate operator qualification. No failure automatically retries authentication or clears the existing interlock.
Public identity queries require PowerShell 7 (`pwsh`) on the build account’s `PATH`; an unavailable or failed query process stops signing with an unknown identity result. Before compiling the probe, a separate noninteractive process reads public keys from the fixed eToken CSP using `CRYPT_VERIFYCONTEXT | CRYPT_SILENT`. It receives the container name but no PIN or signing credentials, and has a 15-second deadline within the existing preflight deadline. Both RSA key slots must be read completely (an absent slot is allowed); at least one public key must match the configured certificate. Provider/container access failure, unsupported public reads, malformed output, timeout or mismatch stops before hardware signing. A container-access error does not establish that the device or container is absent. Public information can be cached by middleware: a match does not authenticate the device, validate the PIN or establish remaining retries. This path is covered by a simulated CSP; the installed SafeNet version still requires separate operator qualification. No failure automatically retries authentication or clears the existing interlock.
Insert and unlock the token before packaging. The electron-builder hook passes each artifact to the CRLF `scripts/windows-sign.cmd`, which invokes the configured SignTool once with `/f`, SafeNet `/kc "[{{PIN}}]=container"`, `/csp "eToken Base Cryptographic Provider"`, a SHA-256 file digest, without a timestamp. The hook then completes a DigiCert SHA-256 RFC 3161 timestamp on isolated copies without signing credentials. The hook never substitutes electron-builder's bundled SignTool and never retries a failed signing request. Windows release packaging fails instead of emitting unsigned artifacts when the SignTool, certificate, container, PIN, token, or signature is unavailable.
+1 -1
View File
@@ -66,7 +66,7 @@ PIN 不能包含 `]`、引号或换行,因为这些字符用于分隔 SafeNet
Windows 签名构建在编译或准备依赖前执行受监督的签名预检。静态配置、证书有效期、审计存储、编译器可用性及遗留签名锁的检查不访问 Token。本地 .NET Framework C# 编译器生成一个专用小探针,由正式签名器仅签名一次,随后必须验出配置的证书和时间戳才能继续构建。探针绝不执行。预检的整体 60 秒期限包含时间戳尝试;超时、硬件签名报错或验签失败都会停止本轮流程,不再次调用硬件。成功只证明当前签名路径可用,不证明 PIN 已独立认证:SafeNet 可能复用登录状态。不要为了验证 PIN 而注销或重复认证。`--check`、仅准备和 `--unsigned` 模式不执行此硬件预检;未签名产物仍不能发布上传。自动回归测试使用假签名器,真实硬件由发布操作人员单独验收。
编译探针前,独立非交互进程使用 `CRYPT_VERIFYCONTEXT | CRYPT_SILENT` 从固定的 eToken CSP 读取公钥。该进程只接收容器名,不接收 PIN 或签名凭据;15 秒期限计入已有预检总期限。两个 RSA 密钥槽必须完整读取(允许槽不存在),且至少一个公钥与配置证书匹配。提供程序或容器访问失败、不支持公开读取、输出损坏、超时或不匹配时,在硬件签名前停止。容器访问错误不能证明设备或容器不存在。中间件可能缓存公开信息,因此匹配不代表设备已认证、PIN 正确或已确认剩余尝试次数。此路径已由模拟 CSP 覆盖,当前 SafeNet 版本仍需操作人员单独验收。任何失败都不会自动重试认证或清除已有保护锁。
公开身份查询要求构建账户的 `PATH` 中有 PowerShell 7(`pwsh`);查询进程不可用或失败时,以身份未知结果停止签名。编译探针前,独立非交互进程使用 `CRYPT_VERIFYCONTEXT | CRYPT_SILENT` 从固定的 eToken CSP 读取公钥。该进程只接收容器名,不接收 PIN 或签名凭据;15 秒期限计入已有预检总期限。两个 RSA 密钥槽必须完整读取(允许槽不存在),且至少一个公钥与配置证书匹配。提供程序或容器访问失败、不支持公开读取、输出损坏、超时或不匹配时,在硬件签名前停止。容器访问错误不能证明设备或容器不存在。中间件可能缓存公开信息,因此匹配不代表设备已认证、PIN 正确或已确认剩余尝试次数。此路径已由模拟 CSP 覆盖,当前 SafeNet 版本仍需操作人员单独验收。任何失败都不会自动重试认证或清除已有保护锁。
打包前插入并解锁 Token。electron-builder 钩子把每个产物交给采用 CRLF 的 `scripts/windows-sign.cmd`;该 CMD 只调用一次已配置的 SignTool,并指定 `/f`、SafeNet `/kc "[{{PIN}}]=容器"`、`/csp "eToken Base Cryptographic Provider"`和 SHA-256 文件摘要,不请求时间戳。随后钩子在隔离副本上完成 DigiCert SHA-256 RFC 3161 时间戳,不传递签名凭据。钩子不会改用 electron-builder 内置的 SignTool,也不会重试失败的签名请求。SignTool、证书、容器、PIN、Token 或签名不可用时,Windows 发布打包会失败,不会生成未签名产物。
+2 -2
View File
@@ -1,12 +1,12 @@
import { execFileSync } from 'node:child_process'
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
import { existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { expect, it } from 'vitest'
import { prepareWindowsBuildWorkspace, removeWindowsBuildWorkspace } from '../apps/desktop/scripts/windows-build-workspace.mjs'
it('clones the exact clean commit and removes only its run-owned checkout', () => {
const root = mkdtempSync(join(tmpdir(), 'package-workspace-'))
const root = realpathSync(mkdtempSync(join(tmpdir(), 'package-workspace-')))
try {
const sourceDirectory = join(root, 'original')
mkdirSync(sourceDirectory)