From 5a74c300398b494979fc56c7cee0175b2f82941c Mon Sep 17 00:00:00 2001 From: Oliver Lin Date: Sun, 8 Nov 2026 13:40:07 +0800 Subject: [PATCH 01/10] ci(build): add code signing for Windows and macOS artifacts Introduce optional signing support in the reusable build workflow, driven by the SIGNATURE_KEYS secret (an openssl PEM containing private key and certificate) and an optional SIGNATURE_KEYS_PASSWORD. - Document the signing behavior and secrets at the top of the workflow - Expose HAS_SIGNING_KEYS so signing steps are skipped when unset - Windows: build the client first, convert the PEM to a PFX on the runner, derive the certificate subject for the MSIX Publisher, and sign all client executables with signtool before packaging exe / zip / msi / msix - macOS: convert the PEM to a P12 and sign the app and dmg before upload Unsigned builds remain the default when no keys are provided. --- .github/workflows/build.yml | 167 +++++++++++++++++++++++++++++++++- .github/workflows/release.yml | 4 + 2 files changed, 168 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 225638def25..7b978c9a99c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,6 +2,12 @@ name: Build # 可复用的单平台构建工作流,由 release.yml 按平台矩阵调用。 # 每个平台的所有安装包在此完成并上传为 release-- 工件。 +# +# 签名:调用方通过 secrets: inherit 传入 SIGNATURE_KEYS(openssl 生成的 PEM, +# 内含私钥与证书,Windows/macOS 共用同一张证书)。设置后会在构建机上临时转换为 +# PFX(Windows)和 P12(macOS),对 exe / msi / msix 以及 app / dmg 做本地签名; +# 未设置时跳过所有签名步骤,产物保持未签名。 +# 可选 SIGNATURE_KEYS_PASSWORD:PEM 中私钥若带口令,用它解密。 on: workflow_call: inputs: @@ -36,6 +42,7 @@ jobs: VSCODE_ARCH: ${{ inputs.arch }} VSCODE_QUALITY: stable GITHUB_TOKEN: ${{ github.token }} + HAS_SIGNING_KEYS: ${{ secrets.SIGNATURE_KEYS != '' }} steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -100,6 +107,72 @@ jobs: tar -czf snap-src.tar.gz -C "$(dirname "$SNAP_DIR")" "$(basename "$SNAP_DIR")" # -------------------------------------------------------------- Windows + # 先产出客户端,签名后被打进 exe / zip / msi / msix,保证安装后主程序也是已签名的 + - name: Build Windows client + if: inputs.platform == 'win32' + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + npm run gulp vscode-win32-x64-min + npm run gulp vscode-win32-x64-inno-updater + + # openssl PEM -> PFX;证书 Subject 同时用作 MSIX 的 Publisher,否则 MSIX 签名会失败 + - name: Prepare signing certificate (Windows) + if: inputs.platform == 'win32' && env.HAS_SIGNING_KEYS == 'true' + shell: pwsh + env: + SIGNATURE_KEYS: ${{ secrets.SIGNATURE_KEYS }} + SIGNATURE_KEYS_PASSWORD: ${{ secrets.SIGNATURE_KEYS_PASSWORD }} + run: | + $ErrorActionPreference = 'Stop' + + $pemPath = Join-Path $env:RUNNER_TEMP 'blockide-signing.pem' + [System.IO.File]::WriteAllText($pemPath, $env:SIGNATURE_KEYS) + + $openssl = (Get-Command openssl.exe -ErrorAction SilentlyContinue).Source + if (-not $openssl) { + $gitOpenSsl = Join-Path $env:ProgramFiles 'Git\usr\bin\openssl.exe' + if (Test-Path $gitOpenSsl) { $openssl = $gitOpenSsl } + } + if (-not $openssl) { throw 'openssl.exe not found, cannot convert SIGNATURE_KEYS PEM to PFX' } + + $password = [System.Guid]::NewGuid().ToString('N') + $pfxPath = Join-Path $env:RUNNER_TEMP 'blockide-signing.pfx' + $keyArgs = @() + if ($env:SIGNATURE_KEYS_PASSWORD) { $keyArgs = @('-passin', "pass:$env:SIGNATURE_KEYS_PASSWORD") } + + & $openssl pkcs12 -export -inkey $pemPath -in $pemPath -out $pfxPath -passout "pass:$password" -name BlockIDE @keyArgs + if ($LASTEXITCODE -ne 0) { throw "openssl pkcs12 exited with code $LASTEXITCODE" } + + $subject = ((& $openssl x509 -in $pemPath -noout -subject -nameopt RFC2253) -join '') -replace '^subject\s*=\s*', '' + if (-not $subject) { throw 'Failed to read certificate subject from SIGNATURE_KEYS' } + + "SIGNING_PFX_PATH=$pfxPath" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + "SIGNING_PFX_PASSWORD=$password" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + "BLOCKIDE_MSIX_PUBLISHER=$subject" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + Write-Host "Signing certificate subject: $subject" + + - name: Sign Windows client + if: inputs.platform == 'win32' && env.HAS_SIGNING_KEYS == 'true' + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + + $sdkRoot = 'C:\Program Files (x86)\Windows Kits\10\bin' + $sdk = Get-ChildItem $sdkRoot -Directory -ErrorAction SilentlyContinue | Sort-Object Name -Descending | Select-Object -First 1 + if (-not $sdk) { throw 'Windows SDK not found' } + $signtool = Join-Path $sdk.FullName 'x64\signtool.exe' + if (-not (Test-Path $signtool)) { throw "signtool not found at $signtool" } + + $clientDir = Join-Path (Split-Path $env:GITHUB_WORKSPACE) 'VSCode-win32-x64' + $targets = Get-ChildItem $clientDir -Filter *.exe -File + if (-not $targets) { throw "No executable found in $clientDir" } + + foreach ($file in $targets) { + & $signtool sign /fd sha256 /f $env:SIGNING_PFX_PATH /p $env:SIGNING_PFX_PASSWORD $file.FullName + if ($LASTEXITCODE -ne 0) { throw "signtool failed for $($file.Name)" } + } + - name: Package Windows (exe / zip / msi / msix) if: inputs.platform == 'win32' shell: pwsh @@ -125,8 +198,6 @@ jobs: $env:PATH = "$(Join-Path $sdkDirectory.FullName 'x64');$env:PATH" } - npm run gulp vscode-win32-x64-min - npm run gulp vscode-win32-x64-inno-updater npm run gulp vscode-win32-x64-system-setup npm run gulp vscode-win32-x64-msi npm run gulp vscode-win32-x64-msix @@ -142,13 +213,96 @@ jobs: Pop-Location } + - name: Sign Windows installers + if: inputs.platform == 'win32' && env.HAS_SIGNING_KEYS == 'true' + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + + $sdkRoot = 'C:\Program Files (x86)\Windows Kits\10\bin' + $sdk = Get-ChildItem $sdkRoot -Directory -ErrorAction SilentlyContinue | Sort-Object Name -Descending | Select-Object -First 1 + if (-not $sdk) { throw 'Windows SDK not found' } + $signtool = Join-Path $sdk.FullName 'x64\signtool.exe' + if (-not (Test-Path $signtool)) { throw "signtool not found at $signtool" } + + $targets = Get-ChildItem artifacts -File | Where-Object { $_.Extension -in '.exe', '.msi', '.msix' } + if (-not $targets) { throw 'No installer artifacts found to sign' } + + foreach ($file in $targets) { + & $signtool sign /fd sha256 /f $env:SIGNING_PFX_PATH /p $env:SIGNING_PFX_PASSWORD $file.FullName + if ($LASTEXITCODE -ne 0) { throw "signtool failed for $($file.Name)" } + } + # ----------------------------------------------------------------- macOS + - name: Build macOS client + if: inputs.platform == 'darwin' + run: | + set -euo pipefail + npm run gulp vscode-darwin-arm64-min + + # 临时钥匙串路径必须是 $AGENT_TEMPDIRECTORY/buildagent.keychain,build/darwin/sign.ts 依赖它 + - name: Prepare signing certificate (macOS) + if: inputs.platform == 'darwin' && env.HAS_SIGNING_KEYS == 'true' + env: + SIGNATURE_KEYS: ${{ secrets.SIGNATURE_KEYS }} + SIGNATURE_KEYS_PASSWORD: ${{ secrets.SIGNATURE_KEYS_PASSWORD }} + run: | + set -euo pipefail + + PEM="$RUNNER_TEMP/blockide-signing.pem" + P12="$RUNNER_TEMP/blockide-signing.p12" + KEYCHAIN="$RUNNER_TEMP/buildagent.keychain" + printf '%s' "$SIGNATURE_KEYS" > "$PEM" + + KEYCHAIN_PASSWORD="$(openssl rand -hex 24)" + P12_PASSWORD="$(openssl rand -hex 24)" + + # 用系统自带的 LibreSSL 导出,默认算法较旧,Security.framework 才能导入。 + # 这里不拼空数组参数,macOS 默认的 bash 3.2 在 set -u 下展开空数组会报错。 + if [ -n "${SIGNATURE_KEYS_PASSWORD:-}" ]; then + /usr/bin/openssl pkcs12 -export -inkey "$PEM" -in "$PEM" -out "$P12" \ + -passout "pass:$P12_PASSWORD" -passin "pass:$SIGNATURE_KEYS_PASSWORD" -name BlockIDE + else + /usr/bin/openssl pkcs12 -export -inkey "$PEM" -in "$PEM" -out "$P12" \ + -passout "pass:$P12_PASSWORD" -name BlockIDE + fi + + security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN" + security set-keychain-settings -lut 21600 "$KEYCHAIN" + security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN" + security import "$P12" -k "$KEYCHAIN" -P "$P12_PASSWORD" -T /usr/bin/codesign + security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN" + security list-keychains -d user -s "$KEYCHAIN" $(security list-keychains -d user | tr -d '"') + + # 自签名证书可能没有代码签名 EKU,先按 codesigning 过滤,取不到就退回全部身份 + identity="$(security find-identity -v -p codesigning "$KEYCHAIN" | sed -n 's/.*"\(.*\)"/\1/p' | head -1)" + if [ -z "$identity" ]; then + identity="$(security find-identity -v "$KEYCHAIN" | sed -n 's/.*"\(.*\)"/\1/p' | head -1)" + fi + if [ -z "$identity" ]; then + security find-identity -v "$KEYCHAIN" || true + echo 'No code signing identity found in the temporary keychain' + exit 1 + fi + echo "Signing identity: $identity" + + { + echo "CODESIGN_IDENTITY=$identity" + echo "SIGNING_KEYCHAIN=$KEYCHAIN" + echo "AGENT_TEMPDIRECTORY=$RUNNER_TEMP" + } >> "$GITHUB_ENV" + + - name: Sign macOS app + if: inputs.platform == 'darwin' && env.HAS_SIGNING_KEYS == 'true' + run: | + set -euo pipefail + node build/darwin/sign.ts "$(dirname "$GITHUB_WORKSPACE")" + - name: Package macOS (zip) if: inputs.platform == 'darwin' run: | set -euo pipefail mkdir -p artifacts - npm run gulp vscode-darwin-arm64-min (cd "$(dirname "$GITHUB_WORKSPACE")" \ && zip -Xry "$GITHUB_WORKSPACE/artifacts/BlockIDE-darwin-arm64-${VERSION}.zip" VSCode-darwin-arm64) @@ -160,6 +314,13 @@ jobs: node build/darwin/create-dmg.ts "$(dirname "$GITHUB_WORKSPACE")" "$GITHUB_WORKSPACE/artifacts" mv "artifacts/VSCode-darwin-arm64.dmg" "artifacts/BlockIDE-darwin-arm64-${VERSION}.dmg" + - name: Sign macOS dmg + if: inputs.platform == 'darwin' && env.HAS_SIGNING_KEYS == 'true' + run: | + set -euo pipefail + dmg="artifacts/BlockIDE-darwin-arm64-${VERSION}.dmg" + codesign --force --timestamp=none --sign "$CODESIGN_IDENTITY" --keychain "$SIGNING_KEYCHAIN" "$dmg" + - name: Upload artifacts uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 93d43a19688..37c0723a33e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -79,10 +79,12 @@ jobs: # 三平台各自独立调用同一个可复用工作流,互不阻塞。 # 拆成三个 job(而非一个 matrix)是为了让 snap 只等待 Linux,而不是等整组矩阵完成。 + # secrets: inherit 让签名密钥 SIGNATURE_KEYS 透传给可复用工作流。 build-linux: name: Build linux-x64 needs: meta uses: ./.github/workflows/build.yml + secrets: inherit with: os: ubuntu-24.04 platform: linux @@ -93,6 +95,7 @@ jobs: name: Build win32-x64 needs: meta uses: ./.github/workflows/build.yml + secrets: inherit with: os: windows-2025 platform: win32 @@ -103,6 +106,7 @@ jobs: name: Build darwin-arm64 needs: meta uses: ./.github/workflows/build.yml + secrets: inherit with: os: macos-26 platform: darwin From 85b2f86c8cc588eb404efafcb5831f3e4ee2e71c Mon Sep 17 00:00:00 2001 From: Oliver Lin Date: Sun, 8 Nov 2026 16:36:29 +0800 Subject: [PATCH 02/10] refactor(ci): split signing key and certificate inputs Read the private key from secrets.SIGNATURE_KEYS and the certificate from the new vars.SIGNATURE_CRT, writing each to its own temp file and combining them into PFX/P12 at build time instead of expecting a single combined PEM. Fail early with clear errors when the certificate variable is missing or lacks a BEGIN CERTIFICATE block, so that the certificate can be maintained as a visible variable while the key stays secret. --- .github/workflows/build.yml | 57 +++++++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7b978c9a99c..e7153d9f8f7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,11 +3,11 @@ name: Build # 可复用的单平台构建工作流,由 release.yml 按平台矩阵调用。 # 每个平台的所有安装包在此完成并上传为 release-- 工件。 # -# 签名:调用方通过 secrets: inherit 传入 SIGNATURE_KEYS(openssl 生成的 PEM, -# 内含私钥与证书,Windows/macOS 共用同一张证书)。设置后会在构建机上临时转换为 -# PFX(Windows)和 P12(macOS),对 exe / msi / msix 以及 app / dmg 做本地签名; -# 未设置时跳过所有签名步骤,产物保持未签名。 -# 可选 SIGNATURE_KEYS_PASSWORD:PEM 中私钥若带口令,用它解密。 +# 签名:私钥放在秘密变量 SIGNATURE_KEYS,证书放在可见变量 SIGNATURE_CRT(vars), +# 两者均为 PEM,Windows/macOS 共用同一张证书。调用方通过 secrets: inherit 传入私钥, +# vars 上下文在可复用工作流中天然可用。构建时会临时合成 PFX(Windows)和 P12(macOS), +# 对 exe / msi / msix 以及 app / dmg 做本地签名;未设置时跳过所有签名步骤。 +# 可选 SIGNATURE_KEYS_PASSWORD:私钥若带口令,用它解密。 on: workflow_call: inputs: @@ -116,36 +116,45 @@ jobs: npm run gulp vscode-win32-x64-min npm run gulp vscode-win32-x64-inno-updater - # openssl PEM -> PFX;证书 Subject 同时用作 MSIX 的 Publisher,否则 MSIX 签名会失败 + # 私钥来自 secrets.SIGNATURE_KEYS,证书来自 vars.SIGNATURE_CRT,分别落盘后再合成 PFX; + # 证书 Subject 同时用作 MSIX 的 Publisher,否则 MSIX 签名会失败 - name: Prepare signing certificate (Windows) if: inputs.platform == 'win32' && env.HAS_SIGNING_KEYS == 'true' shell: pwsh env: - SIGNATURE_KEYS: ${{ secrets.SIGNATURE_KEYS }} + SIGNING_KEY: ${{ secrets.SIGNATURE_KEYS }} + SIGNING_CRT: ${{ vars.SIGNATURE_CRT }} SIGNATURE_KEYS_PASSWORD: ${{ secrets.SIGNATURE_KEYS_PASSWORD }} run: | $ErrorActionPreference = 'Stop' - $pemPath = Join-Path $env:RUNNER_TEMP 'blockide-signing.pem' - [System.IO.File]::WriteAllText($pemPath, $env:SIGNATURE_KEYS) + if (-not $env:SIGNING_CRT) { throw 'SIGNATURE_CRT variable is not set; it must contain the PEM certificate' } + + $keyPath = Join-Path $env:RUNNER_TEMP 'blockide-signing.key' + $certPath = Join-Path $env:RUNNER_TEMP 'blockide-signing.crt' + [System.IO.File]::WriteAllText($keyPath, $env:SIGNING_KEY) + [System.IO.File]::WriteAllText($certPath, $env:SIGNING_CRT) + if (-not (Select-String -Path $certPath -Pattern 'BEGIN CERTIFICATE' -Quiet)) { + throw 'SIGNATURE_CRT does not contain a BEGIN CERTIFICATE block' + } $openssl = (Get-Command openssl.exe -ErrorAction SilentlyContinue).Source if (-not $openssl) { $gitOpenSsl = Join-Path $env:ProgramFiles 'Git\usr\bin\openssl.exe' if (Test-Path $gitOpenSsl) { $openssl = $gitOpenSsl } } - if (-not $openssl) { throw 'openssl.exe not found, cannot convert SIGNATURE_KEYS PEM to PFX' } + if (-not $openssl) { throw 'openssl.exe not found, cannot build the PFX from SIGNATURE_KEYS/SIGNATURE_CRT' } $password = [System.Guid]::NewGuid().ToString('N') $pfxPath = Join-Path $env:RUNNER_TEMP 'blockide-signing.pfx' $keyArgs = @() if ($env:SIGNATURE_KEYS_PASSWORD) { $keyArgs = @('-passin', "pass:$env:SIGNATURE_KEYS_PASSWORD") } - & $openssl pkcs12 -export -inkey $pemPath -in $pemPath -out $pfxPath -passout "pass:$password" -name BlockIDE @keyArgs + & $openssl pkcs12 -export -inkey $keyPath -in $certPath -out $pfxPath -passout "pass:$password" -name BlockIDE @keyArgs if ($LASTEXITCODE -ne 0) { throw "openssl pkcs12 exited with code $LASTEXITCODE" } - $subject = ((& $openssl x509 -in $pemPath -noout -subject -nameopt RFC2253) -join '') -replace '^subject\s*=\s*', '' - if (-not $subject) { throw 'Failed to read certificate subject from SIGNATURE_KEYS' } + $subject = ((& $openssl x509 -in $certPath -noout -subject -nameopt RFC2253) -join '') -replace '^subject\s*=\s*', '' + if (-not $subject) { throw 'Failed to read certificate subject from SIGNATURE_CRT' } "SIGNING_PFX_PATH=$pfxPath" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 "SIGNING_PFX_PASSWORD=$password" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 @@ -244,15 +253,27 @@ jobs: - name: Prepare signing certificate (macOS) if: inputs.platform == 'darwin' && env.HAS_SIGNING_KEYS == 'true' env: - SIGNATURE_KEYS: ${{ secrets.SIGNATURE_KEYS }} + SIGNING_KEY: ${{ secrets.SIGNATURE_KEYS }} + SIGNING_CRT: ${{ vars.SIGNATURE_CRT }} SIGNATURE_KEYS_PASSWORD: ${{ secrets.SIGNATURE_KEYS_PASSWORD }} run: | set -euo pipefail - PEM="$RUNNER_TEMP/blockide-signing.pem" + if [ -z "${SIGNING_CRT:-}" ]; then + echo 'SIGNATURE_CRT variable is not set; it must contain the PEM certificate' + exit 1 + fi + + KEY="$RUNNER_TEMP/blockide-signing.key" + CERT="$RUNNER_TEMP/blockide-signing.crt" P12="$RUNNER_TEMP/blockide-signing.p12" KEYCHAIN="$RUNNER_TEMP/buildagent.keychain" - printf '%s' "$SIGNATURE_KEYS" > "$PEM" + printf '%s\n' "$SIGNING_KEY" > "$KEY" + printf '%s\n' "$SIGNING_CRT" > "$CERT" + if ! grep -q 'BEGIN CERTIFICATE' "$CERT"; then + echo 'SIGNATURE_CRT does not contain a BEGIN CERTIFICATE block' + exit 1 + fi KEYCHAIN_PASSWORD="$(openssl rand -hex 24)" P12_PASSWORD="$(openssl rand -hex 24)" @@ -260,10 +281,10 @@ jobs: # 用系统自带的 LibreSSL 导出,默认算法较旧,Security.framework 才能导入。 # 这里不拼空数组参数,macOS 默认的 bash 3.2 在 set -u 下展开空数组会报错。 if [ -n "${SIGNATURE_KEYS_PASSWORD:-}" ]; then - /usr/bin/openssl pkcs12 -export -inkey "$PEM" -in "$PEM" -out "$P12" \ + /usr/bin/openssl pkcs12 -export -inkey "$KEY" -in "$CERT" -out "$P12" \ -passout "pass:$P12_PASSWORD" -passin "pass:$SIGNATURE_KEYS_PASSWORD" -name BlockIDE else - /usr/bin/openssl pkcs12 -export -inkey "$PEM" -in "$PEM" -out "$P12" \ + /usr/bin/openssl pkcs12 -export -inkey "$KEY" -in "$CERT" -out "$P12" \ -passout "pass:$P12_PASSWORD" -name BlockIDE fi From a187ed3afdfda106f1cf6dfe5efb5e0cdafe6bc7 Mon Sep 17 00:00:00 2001 From: Oliver Lin Date: Sun, 8 Nov 2026 18:02:07 +0800 Subject: [PATCH 03/10] fix(build): add exponential backoff and mirror fallback for downloads - Retry delays now increase exponentially (1x, 2x, 4x...) to avoid hammering the same source on repeated failures - electron.d.ts download tries GitHub first, then npmmirror mirrors when GitHub Releases returns errors like 504 - Each mirror download is verified against the expected SHA-256 checksum, with retries, longer timeout, and clear error messages on failure --- build/lib/download.ts | 3 ++- build/npm/electronTypes.ts | 31 +++++++++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/build/lib/download.ts b/build/lib/download.ts index 48352e7cd7b..b108ef95668 100644 --- a/build/lib/download.ts +++ b/build/lib/download.ts @@ -46,7 +46,8 @@ export async function download(url: string, options: IDownloadOptions = {}): Pro lastError = error instanceof Error ? error : new Error(String(error)); if (attempt < attempts) { options.onRetry?.(lastError); - await new Promise(resolve => setTimeout(resolve, retryDelay)); + // 指数退避:1x, 2x, 4x ... 避免连续打同一个源 + await new Promise(resolve => setTimeout(resolve, retryDelay * 2 ** (attempt - 1))); } } } diff --git a/build/npm/electronTypes.ts b/build/npm/electronTypes.ts index 8ab9b13359a..fdee71590ec 100644 --- a/build/npm/electronTypes.ts +++ b/build/npm/electronTypes.ts @@ -27,8 +27,35 @@ export async function ensureElectronTypes(): Promise { } const { electronVersion } = getElectronVersion(); - const url = `https://github.com/electron/electron/releases/download/v${electronVersion}/electron.d.ts`; - const contents = await download(url, { checksumSha256: expectedChecksum }); + + // GitHub Releases 偶发 504,依次尝试多个镜像;所有镜像都会校验 SHA-256, + // 与 build/checksums/electron.txt 比对,保证内容一致。 + const urls = [ + `https://github.com/electron/electron/releases/download/v${electronVersion}/electron.d.ts`, + `https://registry.npmmirror.com/-/binary/electron/v${electronVersion}/electron.d.ts`, + `https://npmmirror.com/mirrors/electron/v${electronVersion}/electron.d.ts` + ]; + + let contents: Uint8Array | undefined; + let lastError: Error | undefined; + for (const url of urls) { + try { + contents = await download(url, { + checksumSha256: expectedChecksum, + attempts: 3, + retryDelay: 2000, + timeout: 60_000, + onRetry: error => console.warn(`Retrying ${url}: ${error.message}`) + }); + break; + } catch (error) { + lastError = error instanceof Error ? error : new Error(String(error)); + console.warn(`Mirror failed, trying next: ${url}`); + } + } + if (!contents) { + throw lastError ?? new Error('Failed to download electron.d.ts from all mirrors'); + } fs.mkdirSync(path.dirname(electronTypesPath), { recursive: true }); fs.writeFileSync(electronTypesPath, contents); From 6fab2b03ffdfc90a07c91e6f9fdeecbfff0af252 Mon Sep 17 00:00:00 2001 From: Oliver Lin Date: Sun, 8 Nov 2026 18:32:34 +0800 Subject: [PATCH 04/10] fix(ci): select Windows SDK version directories by semver The Windows SDK `bin` directory also contains non-version folders such as `x86` and `x64`. Sorting all directories by name descending could select one of these instead of a real version (e.g. `x86` > `10.x`), producing invalid paths to `signtool.exe` and failing signing steps. Filter to semver-like directory names, sort them as `[version]` objects, and verify the computed `signtool.exe` path exists before using it. This makes signing and installer PATH setup robust. --- .github/workflows/build.yml | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e7153d9f8f7..e099f41e9f3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -168,10 +168,14 @@ jobs: $ErrorActionPreference = 'Stop' $sdkRoot = 'C:\Program Files (x86)\Windows Kits\10\bin' - $sdk = Get-ChildItem $sdkRoot -Directory -ErrorAction SilentlyContinue | Sort-Object Name -Descending | Select-Object -First 1 - if (-not $sdk) { throw 'Windows SDK not found' } - $signtool = Join-Path $sdk.FullName 'x64\signtool.exe' - if (-not (Test-Path $signtool)) { throw "signtool not found at $signtool" } + # bin 下同时存在 x86/x64 等目录,只按版本号目录排序,避免选中 x86 拼出错路径 + $signtool = Get-ChildItem $sdkRoot -Directory -ErrorAction SilentlyContinue | + Where-Object { $_.Name -match '^\d+(\.\d+)+$' } | + Sort-Object { [version]$_.Name } -Descending | + ForEach-Object { Join-Path $_.FullName 'x64\signtool.exe' } | + Where-Object { Test-Path $_ } | + Select-Object -First 1 + if (-not $signtool) { throw "signtool.exe not found under $sdkRoot" } $clientDir = Join-Path (Split-Path $env:GITHUB_WORKSPACE) 'VSCode-win32-x64' $targets = Get-ChildItem $clientDir -Filter *.exe -File @@ -202,7 +206,8 @@ jobs: $env:PATH = "$env:WIX_BIN;$env:PATH" $sdkDirectory = Get-ChildItem 'C:\Program Files (x86)\Windows Kits\10\bin' -Directory -ErrorAction SilentlyContinue | - Sort-Object Name -Descending | Select-Object -First 1 + Where-Object { $_.Name -match '^\d+(\.\d+)+$' } | + Sort-Object { [version]$_.Name } -Descending | Select-Object -First 1 if ($sdkDirectory) { $env:PATH = "$(Join-Path $sdkDirectory.FullName 'x64');$env:PATH" } @@ -229,10 +234,14 @@ jobs: $ErrorActionPreference = 'Stop' $sdkRoot = 'C:\Program Files (x86)\Windows Kits\10\bin' - $sdk = Get-ChildItem $sdkRoot -Directory -ErrorAction SilentlyContinue | Sort-Object Name -Descending | Select-Object -First 1 - if (-not $sdk) { throw 'Windows SDK not found' } - $signtool = Join-Path $sdk.FullName 'x64\signtool.exe' - if (-not (Test-Path $signtool)) { throw "signtool not found at $signtool" } + # bin 下同时存在 x86/x64 等目录,只按版本号目录排序,避免选中 x86 拼出错路径 + $signtool = Get-ChildItem $sdkRoot -Directory -ErrorAction SilentlyContinue | + Where-Object { $_.Name -match '^\d+(\.\d+)+$' } | + Sort-Object { [version]$_.Name } -Descending | + ForEach-Object { Join-Path $_.FullName 'x64\signtool.exe' } | + Where-Object { Test-Path $_ } | + Select-Object -First 1 + if (-not $signtool) { throw "signtool.exe not found under $sdkRoot" } $targets = Get-ChildItem artifacts -File | Where-Object { $_.Extension -in '.exe', '.msi', '.msix' } if (-not $targets) { throw 'No installer artifacts found to sign' } From 89b56b7e95649dbe6f6b3c327b6a4cd6399fcadf Mon Sep 17 00:00:00 2001 From: Oliver Lin Date: Sun, 8 Nov 2026 19:14:10 +0800 Subject: [PATCH 05/10] ci: trust self-signed cert before codesigning on macOS Register the generated self-signed certificate as a system trust root so that `security find-identity -v` reports it as valid and codesign can build a complete certificate chain. Without this, the identity is filtered out as untrusted and the macOS build fails to sign. --- .github/workflows/build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e099f41e9f3..1c956f9abb3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -304,6 +304,10 @@ jobs: security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN" security list-keychains -d user -s "$KEYCHAIN" $(security list-keychains -d user | tr -d '"') + # 自签名证书默认不受信任,find-identity -v 会把未受信任身份全部过滤掉。 + # 必须注册为系统信任根,身份才会被判定为 valid,codesign 也才能建立完整链。 + sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "$CERT" + # 自签名证书可能没有代码签名 EKU,先按 codesigning 过滤,取不到就退回全部身份 identity="$(security find-identity -v -p codesigning "$KEYCHAIN" | sed -n 's/.*"\(.*\)"/\1/p' | head -1)" if [ -z "$identity" ]; then From 7fdb0195d5db4360e61e10a5c1468910bf9f549e Mon Sep 17 00:00:00 2001 From: Oliver Lin Date: Sun, 8 Nov 2026 20:38:04 +0800 Subject: [PATCH 06/10] fix: resolve Electron download and macOS signing issues - Add Electron mirror to CI to work around intermittent GitHub 504s - Skip osx-sign identity validation for self-signed certificates - Fix typo in test-agent-host-e2e script path --- .github/workflows/build.yml | 5 +++++ build/darwin/sign.ts | 4 ++++ package.json | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1c956f9abb3..e443035f32f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,6 +43,11 @@ jobs: VSCODE_QUALITY: stable GITHUB_TOKEN: ${{ github.token }} HAS_SIGNING_KEYS: ${{ secrets.SIGNATURE_KEYS != '' }} + # GitHub Releases 偶发 504,Electron 二进制统一走 npmmirror 镜像。 + # 内容完整性不受影响:@electron/get 从镜像取 SHASUMS256.txt, + # gulp-electron 还会再对照仓库内 build/checksums/electron.txt 校验。 + # 若某次升级 Electron 时镜像尚未同步,临时删除此变量即可回退到 GitHub。 + ELECTRON_MIRROR: https://npmmirror.com/mirrors/electron/ steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 diff --git a/build/darwin/sign.ts b/build/darwin/sign.ts index 4552dc313d9..0135b2080b4 100644 --- a/build/darwin/sign.ts +++ b/build/darwin/sign.ts @@ -112,6 +112,10 @@ async function main(buildDir?: string): Promise { keychain: getKeychainPath(tempDir), version: getElectronVersion(), identity, + // BlockIDE 使用自签名证书,身份在系统信任评估中恒为 NOT_TRUSTED, + // osx-sign 默认的有效性校验会因此拒绝签名。codesign 本身允许用未受信任的 + // 身份签名,故跳过该校验。 + identityValidation: false, }; // Only overwrite plist entries for x64 and arm64 builds, diff --git a/package.json b/package.json index fdb1e83bc0f..92a5bb31ac2 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "test-node": "mocha test/unit/node/index.js --delay --ui=tdd --exit", "test-extension": "vscode-test", "test-build-scripts": "cd build && npm run test", - "test-agent-host-e2e": "node test/integration/agentHost/runner.ts", + "test-agent-host-e2e": "node te st/integration/agentHost/runner.ts", "markdown-editor-package-json-check": "npm --prefix extensions/markdown-language-features run check-markdown-editor-package-json", "test-agent-host-e2e-coverage": "node test/integration/agentHost/coverage.ts", "check-cyclic-dependencies": "node build/lib/checkCyclicDependencies.ts out", From 85b6b22be79b50a26ce4485b419d9257f2763d29 Mon Sep 17 00:00:00 2001 From: Oliver Lin Date: Sun, 8 Nov 2026 21:41:17 +0800 Subject: [PATCH 07/10] fix(ci): make macOS code signing work with CA-issued certificates macOS codesign failed to find a signing identity when SIGNATURE_CRT contained a leaf certificate issued by a CA missing from the trust store, because the chain could not be built (CSSMERR_TP_NOT_TRUSTED). - Split the PEM bundle and export only the leaf cert into the PKCS#12, importing any intermediate/root certs into the temporary keychain - Check for an expired leaf certificate and fail early - Use trustAsRoot for CA-issued leaves (trustRoot only works for self-signed certs) and restrict trust to the codeSign policy - Select the identity by SHA-1 fingerprint instead of display name, which can carry diagnostic suffixes like CSSMERR_TP_NOT_TRUSTED - Warn when the identity is still not trusted --- .github/workflows/build.yml | 65 ++++++++++++++++++++++++++----- build/darwin/sign.ts | 12 +++++- build/lib/test/darwinSign.test.ts | 26 ++++++++++++- 3 files changed, 91 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e443035f32f..a42b58ac653 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -289,16 +289,32 @@ jobs: exit 1 fi + # SIGNATURE_CRT 里可能只有叶子证书,而它的签发者(ZIT Studio Code Signing CA) + # 不一定在系统信任库里。先把 PEM 拆成单张证书,便于分别导入和设置信任。 + CERT_DIR="$RUNNER_TEMP/blockide-certs" + rm -rf "$CERT_DIR" + mkdir -p "$CERT_DIR" + awk -v dir="$CERT_DIR" '/-----BEGIN CERTIFICATE-----/{n++} n > 0 { file = dir "/cert-" n ".pem"; print >> file }' "$CERT" + LEAF="$CERT_DIR/cert-1.pem" + echo 'Signing certificate:' + /usr/bin/openssl x509 -in "$LEAF" -noout -subject -issuer -dates + # 过期证书无论怎么设置信任都无法用于签名,提前给出明确错误 + if ! /usr/bin/openssl x509 -in "$LEAF" -noout -checkend 0 >/dev/null 2>&1; then + echo '::error::The certificate in SIGNATURE_CRT has expired; codesign cannot use it.' + exit 1 + fi + KEYCHAIN_PASSWORD="$(openssl rand -hex 24)" P12_PASSWORD="$(openssl rand -hex 24)" # 用系统自带的 LibreSSL 导出,默认算法较旧,Security.framework 才能导入。 # 这里不拼空数组参数,macOS 默认的 bash 3.2 在 set -u 下展开空数组会报错。 + # 只导出叶子证书;证书链中的其他证书稍后单独导入临时钥匙串。 if [ -n "${SIGNATURE_KEYS_PASSWORD:-}" ]; then - /usr/bin/openssl pkcs12 -export -inkey "$KEY" -in "$CERT" -out "$P12" \ + /usr/bin/openssl pkcs12 -export -inkey "$KEY" -in "$LEAF" -out "$P12" \ -passout "pass:$P12_PASSWORD" -passin "pass:$SIGNATURE_KEYS_PASSWORD" -name BlockIDE else - /usr/bin/openssl pkcs12 -export -inkey "$KEY" -in "$CERT" -out "$P12" \ + /usr/bin/openssl pkcs12 -export -inkey "$KEY" -in "$LEAF" -out "$P12" \ -passout "pass:$P12_PASSWORD" -name BlockIDE fi @@ -309,21 +325,50 @@ jobs: security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN" security list-keychains -d user -s "$KEYCHAIN" $(security list-keychains -d user | tr -d '"') - # 自签名证书默认不受信任,find-identity -v 会把未受信任身份全部过滤掉。 - # 必须注册为系统信任根,身份才会被判定为 valid,codesign 也才能建立完整链。 - sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain "$CERT" + # SIGNATURE_CRT 若带有中间/根证书,一并导入临时钥匙串,便于系统构建证书链 + for extra in "$CERT_DIR"/cert-*.pem; do + if [ "$extra" != "$LEAF" ]; then + security import "$extra" -k "$KEYCHAIN" + fi + done + + # 仅把证书注册成 trustRoot 不足以让它变成可用身份:trustRoot 只对自签名 + # 证书有效。SIGNATURE_CRT 里的叶子证书由 "ZIT Studio Code Signing CA" 签发, + # 而该 CA 不在系统信任库里,macOS 无法构建完整链,身份会被判定为 + # CSSMERR_TP_NOT_TRUSTED,codesign 也就找不到可签名的身份。 + # 因此:自签名证书用 trustRoot;由缺失 CA 签发的叶子证书用 trustAsRoot, + # 把叶子证书本身固定为信任锚。只信任 codeSign 策略,避免证书被用于 TLS。 + leaf_issuer="$(/usr/bin/openssl x509 -in "$LEAF" -noout -issuer -nameopt RFC2253)" + leaf_subject="$(/usr/bin/openssl x509 -in "$LEAF" -noout -subject -nameopt RFC2253)" + if [ "$leaf_issuer" = "$leaf_subject" ]; then + trust_type=trustRoot + else + trust_type=trustAsRoot + fi + echo "Registering signing certificate as $trust_type for the codeSign policy" + sudo security add-trusted-cert -d -r "$trust_type" -p codeSign -k /Library/Keychains/System.keychain "$LEAF" - # 自签名证书可能没有代码签名 EKU,先按 codesigning 过滤,取不到就退回全部身份 - identity="$(security find-identity -v -p codesigning "$KEYCHAIN" | sed -n 's/.*"\(.*\)"/\1/p' | head -1)" + # 用证书的 SHA-1 指纹而不是展示名作为身份:find-identity 的展示名可能带 + # "(CSSMERR_TP_NOT_TRUSTED)" 之类的诊断后缀,按引号截取会把后缀一起写进 + # CODESIGN_IDENTITY,codesign 于是报 "no identity found"。 + identity="$(security find-identity -v -p codesigning "$KEYCHAIN" | grep -oE '[0-9A-Fa-f]{40}' | head -1 || true)" if [ -z "$identity" ]; then - identity="$(security find-identity -v "$KEYCHAIN" | sed -n 's/.*"\(.*\)"/\1/p' | head -1)" + # 身份尚未被判定为 valid 时 -v 可能什么都不列,退回完整身份列表 + identity="$(security find-identity -p codesigning "$KEYCHAIN" | grep -oE '[0-9A-Fa-f]{40}' | head -1 || true)" fi if [ -z "$identity" ]; then - security find-identity -v "$KEYCHAIN" || true + security find-identity -p codesigning "$KEYCHAIN" || true echo 'No code signing identity found in the temporary keychain' exit 1 fi - echo "Signing identity: $identity" + + valid_line="$(security find-identity -v -p codesigning "$KEYCHAIN" | grep "$identity" || true)" + if [ -z "$valid_line" ] || printf '%s\n' "$valid_line" | grep -q 'CSSMERR'; then + echo "::warning::Signing identity $identity is still not trusted; codesign may refuse it." + echo "::warning::Make SIGNATURE_CRT contain the full certificate chain (leaf + issuing CA) or trust the issuing CA." + security find-identity -p codesigning "$KEYCHAIN" || true + fi + echo "Signing identity: $identity (trust type: $trust_type)" { echo "CODESIGN_IDENTITY=$identity" diff --git a/build/darwin/sign.ts b/build/darwin/sign.ts index 0135b2080b4..24ebca75f19 100644 --- a/build/darwin/sign.ts +++ b/build/darwin/sign.ts @@ -79,10 +79,20 @@ export async function retryOnTransientSignError(fn: () => Promise, maxAtte } } +/** + * `security find-identity` appends trust-evaluation diagnostics such as + * `(CSSMERR_TP_NOT_TRUSTED)` to an identity's display name. If such a name is + * passed to `codesign --sign`, codesign looks for the whole string and fails + * with "no identity found", so strip the suffix before signing. + */ +export function normalizeSigningIdentity(identity: string | undefined): string | undefined { + return identity?.replace(/\s*\(CSSMERR_[A-Z0-9_]+\)\s*$/, ''); +} + async function main(buildDir?: string): Promise { const tempDir = process.env['AGENT_TEMPDIRECTORY']; const arch = process.env['VSCODE_ARCH']; - const identity = process.env['CODESIGN_IDENTITY']; + const identity = normalizeSigningIdentity(process.env['CODESIGN_IDENTITY']); if (!buildDir) { throw new Error('$AGENT_BUILDDIRECTORY not set'); diff --git a/build/lib/test/darwinSign.test.ts b/build/lib/test/darwinSign.test.ts index 732df5ac1fc..cc45e1bce53 100644 --- a/build/lib/test/darwinSign.test.ts +++ b/build/lib/test/darwinSign.test.ts @@ -5,7 +5,7 @@ import assert from 'node:assert/strict'; import { suite, test } from 'node:test'; -import { retryOnTransientSignError } from '../../darwin/sign.ts'; +import { normalizeSigningIdentity, retryOnTransientSignError } from '../../darwin/sign.ts'; function codesignError(stderr: string): Error { return new Error(`Command failed: codesign --sign ABC --force --timestamp --options runtime locale.pak\nlocale.pak: ${stderr}`); @@ -47,3 +47,27 @@ suite('darwin sign', () => { assert.deepStrictEqual(await sign([timestampError, timestampError, timestampError], 3), { attempts: 3, error: 'locale.pak: A timestamp was expected but was not found.' }); }); }); + +suite('normalize signing identity', () => { + test('strips trust diagnostics appended by find-identity', () => { + assert.strictEqual( + normalizeSigningIdentity('ZIT Studio BlockIDE Code Signing (CSSMERR_TP_NOT_TRUSTED)'), + 'ZIT Studio BlockIDE Code Signing'); + assert.strictEqual( + normalizeSigningIdentity('ZIT Studio BlockIDE Code Signing (CSSMERR_TP_CERT_EXPIRED)'), + 'ZIT Studio BlockIDE Code Signing'); + }); + + test('keeps names and hashes that have no diagnostics', () => { + assert.strictEqual( + normalizeSigningIdentity('ZIT Studio BlockIDE Code Signing'), + 'ZIT Studio BlockIDE Code Signing'); + assert.strictEqual( + normalizeSigningIdentity('C7D5DE396508C18020A30E6C4E41764B97EA0490'), + 'C7D5DE396508C18020A30E6C4E41764B97EA0490'); + }); + + test('preserves undefined', () => { + assert.strictEqual(normalizeSigningIdentity(undefined), undefined); + }); +}); From 6f9f006315901d9bc099248030821049cf87ef3b Mon Sep 17 00:00:00 2001 From: Oliver Lin Date: Sun, 8 Nov 2026 23:15:22 +0800 Subject: [PATCH 08/10] ci(macos): require complete certificate chain for signing codesign must build the chain to a self-signed root itself, so a bare leaf certificate in SIGNATURE_CRT fails with errSecInternalComponent. Validate the chain (issuer/subject matching, self-signed root) and fail early with actionable guidance, then import all CA certs into the temporary keychain instead of pinning the leaf as trust anchor. --- .github/workflows/build.yml | 84 +++++++++++++++++++++++++++---------- 1 file changed, 62 insertions(+), 22 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a42b58ac653..b6cf57544f1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,6 +7,9 @@ name: Build # 两者均为 PEM,Windows/macOS 共用同一张证书。调用方通过 secrets: inherit 传入私钥, # vars 上下文在可复用工作流中天然可用。构建时会临时合成 PFX(Windows)和 P12(macOS), # 对 exe / msi / msix 以及 app / dmg 做本地签名;未设置时跳过所有签名步骤。 +# macOS 的 SIGNATURE_CRT 必须是完整证书链(叶子证书在前,逐级签发 CA 在后,最后一张 +# 为自签名根)或一张自签名证书:codesign 需要自己构建到自签名根的链,只给 CA 签发的 +# 叶子证书会因缺链而报 errSecInternalComponent。 # 可选 SIGNATURE_KEYS_PASSWORD:私钥若带口令,用它解密。 on: workflow_call: @@ -289,14 +292,21 @@ jobs: exit 1 fi - # SIGNATURE_CRT 里可能只有叶子证书,而它的签发者(ZIT Studio Code Signing CA) - # 不一定在系统信任库里。先把 PEM 拆成单张证书,便于分别导入和设置信任。 + # SIGNATURE_CRT 必须是完整证书链或自签名证书: + # - 完整链:叶子证书在前,逐级签发 CA 在后,最后一张是自签名根; + # - 自签名证书:只有一张,且 issuer == subject。 + # 只给叶子证书不够:codesign 必须自己构建出到自签名根的链,缺失签发 CA 时 + # 会报 "unable to build chain to self-signed root" + errSecInternalComponent; + # 而把叶子证书设成 trustAsRoot 会让链在叶子处截断,codesign 同样拒绝签名。 CERT_DIR="$RUNNER_TEMP/blockide-certs" rm -rf "$CERT_DIR" mkdir -p "$CERT_DIR" awk -v dir="$CERT_DIR" '/-----BEGIN CERTIFICATE-----/{n++} n > 0 { file = dir "/cert-" n ".pem"; print >> file }' "$CERT" + CERT_COUNT="$(ls "$CERT_DIR"/cert-*.pem | wc -l | tr -d ' ')" LEAF="$CERT_DIR/cert-1.pem" - echo 'Signing certificate:' + ROOT="$CERT_DIR/cert-$CERT_COUNT.pem" + + echo "SIGNATURE_CRT contains $CERT_COUNT certificate(s); signing certificate:" /usr/bin/openssl x509 -in "$LEAF" -noout -subject -issuer -dates # 过期证书无论怎么设置信任都无法用于签名,提前给出明确错误 if ! /usr/bin/openssl x509 -in "$LEAF" -noout -checkend 0 >/dev/null 2>&1; then @@ -304,6 +314,35 @@ jobs: exit 1 fi + # 校验链完整性:相邻证书的 issuer/subject 必须对上,最后一张必须自签名 + chain_complete=1 + index=1 + while [ "$index" -lt "$CERT_COUNT" ]; do + child_issuer="$(/usr/bin/openssl x509 -in "$CERT_DIR/cert-$index.pem" -noout -issuer -nameopt RFC2253 | sed 's/^issuer=//')" + parent_subject="$(/usr/bin/openssl x509 -in "$CERT_DIR/cert-$((index + 1)).pem" -noout -subject -nameopt RFC2253 | sed 's/^subject=//')" + if [ "$child_issuer" != "$parent_subject" ]; then + chain_complete=0 + break + fi + index=$((index + 1)) + done + root_issuer="$(/usr/bin/openssl x509 -in "$ROOT" -noout -issuer -nameopt RFC2253 | sed 's/^issuer=//')" + root_subject="$(/usr/bin/openssl x509 -in "$ROOT" -noout -subject -nameopt RFC2253 | sed 's/^subject=//')" + if [ "$root_issuer" != "$root_subject" ]; then + chain_complete=0 + fi + + if [ "$chain_complete" -ne 1 ]; then + echo "::error::SIGNATURE_CRT has $CERT_COUNT certificate(s), but the last one is not a self-signed root." + echo "::error::Last certificate issuer: $root_issuer" + echo "::error::Last certificate subject: $root_subject" + echo '::error::codesign must build the chain itself; a bare leaf certificate cannot be used for signing.' + echo '::error::Append the issuing CA certificate to SIGNATURE_CRT (leaf first, then each CA, ending with the self-signed root):' + echo '::error:: cat leaf.crt ca.crt > chain.pem # paste chain.pem into SIGNATURE_CRT' + echo '::error::Or replace SIGNATURE_CRT with a self-signed certificate generated from the existing SIGNATURE_KEYS.' + exit 1 + fi + KEYCHAIN_PASSWORD="$(openssl rand -hex 24)" P12_PASSWORD="$(openssl rand -hex 24)" @@ -325,28 +364,18 @@ jobs: security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN" security list-keychains -d user -s "$KEYCHAIN" $(security list-keychains -d user | tr -d '"') - # SIGNATURE_CRT 若带有中间/根证书,一并导入临时钥匙串,便于系统构建证书链 + # 中间/根 CA 必须进钥匙串,codesign 才能把完整的链嵌进签名 for extra in "$CERT_DIR"/cert-*.pem; do if [ "$extra" != "$LEAF" ]; then security import "$extra" -k "$KEYCHAIN" fi done - # 仅把证书注册成 trustRoot 不足以让它变成可用身份:trustRoot 只对自签名 - # 证书有效。SIGNATURE_CRT 里的叶子证书由 "ZIT Studio Code Signing CA" 签发, - # 而该 CA 不在系统信任库里,macOS 无法构建完整链,身份会被判定为 - # CSSMERR_TP_NOT_TRUSTED,codesign 也就找不到可签名的身份。 - # 因此:自签名证书用 trustRoot;由缺失 CA 签发的叶子证书用 trustAsRoot, - # 把叶子证书本身固定为信任锚。只信任 codeSign 策略,避免证书被用于 TLS。 - leaf_issuer="$(/usr/bin/openssl x509 -in "$LEAF" -noout -issuer -nameopt RFC2253)" - leaf_subject="$(/usr/bin/openssl x509 -in "$LEAF" -noout -subject -nameopt RFC2253)" - if [ "$leaf_issuer" = "$leaf_subject" ]; then - trust_type=trustRoot - else - trust_type=trustAsRoot - fi - echo "Registering signing certificate as $trust_type for the codeSign policy" - sudo security add-trusted-cert -d -r "$trust_type" -p codeSign -k /Library/Keychains/System.keychain "$LEAF" + # 只把链上的自签名根注册为信任锚,叶子证书保持“使用系统默认”。 + # 千万不要把叶子证书设成 trustAsRoot:那会让链在叶子处截断, + # codesign 报 "unable to build chain to self-signed root" + errSecInternalComponent。 + echo "Registering chain root as trustRoot for the codeSign policy: $ROOT" + sudo security add-trusted-cert -d -r trustRoot -p codeSign -k /Library/Keychains/System.keychain "$ROOT" # 用证书的 SHA-1 指纹而不是展示名作为身份:find-identity 的展示名可能带 # "(CSSMERR_TP_NOT_TRUSTED)" 之类的诊断后缀,按引号截取会把后缀一起写进 @@ -364,11 +393,22 @@ jobs: valid_line="$(security find-identity -v -p codesigning "$KEYCHAIN" | grep "$identity" || true)" if [ -z "$valid_line" ] || printf '%s\n' "$valid_line" | grep -q 'CSSMERR'; then - echo "::warning::Signing identity $identity is still not trusted; codesign may refuse it." - echo "::warning::Make SIGNATURE_CRT contain the full certificate chain (leaf + issuing CA) or trust the issuing CA." security find-identity -p codesigning "$KEYCHAIN" || true + security verify-cert -c "$LEAF" -p codeSign || true + echo "::error::Signing identity $identity is still not trusted after registering $ROOT as the chain root." + exit 1 + fi + echo "Signing identity: $identity" + + # 用一个临时文件做真实签名预检:链不完整或钥匙串访问有问题时, + # codesign 会返回 errSecInternalComponent;提前失败好过几分钟后才报错。 + PREFLIGHT="$RUNNER_TEMP/blockide-signing-preflight" + printf 'preflight\n' > "$PREFLIGHT" + if ! codesign --force --sign "$identity" --keychain "$KEYCHAIN" --timestamp=none "$PREFLIGHT"; then + echo '::error::codesign preflight failed; the signing identity or its certificate chain is unusable.' + exit 1 fi - echo "Signing identity: $identity (trust type: $trust_type)" + rm -f "$PREFLIGHT" { echo "CODESIGN_IDENTITY=$identity" From 3dc65267280d88ff67e9409801666a81403d9ca6 Mon Sep 17 00:00:00 2001 From: Oliver Lin Date: Sun, 8 Nov 2026 23:26:57 +0800 Subject: [PATCH 09/10] ci: fall back to self-signed cert for macOS signing When SIGNATURE_CRT contains only a CA-issued leaf certificate, codesign fails with errSecInternalComponent because it cannot build a chain to a self-signed root. Instead of failing the build, generate a self-signed certificate from the same private key (preserving the leaf's subject) and use it for signing. A warning reminds users to append the issuing CA to SIGNATURE_CRT for a stable chain. Also disable codesign timestamping unless the chain is anchored at an Apple root, since Apple's timestamp service does not support non-Apple signing identities and would otherwise fail the signing step. --- .github/workflows/build.yml | 104 +++++++++++++++++++++++++----------- build/darwin/sign.ts | 10 ++-- 2 files changed, 81 insertions(+), 33 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b6cf57544f1..5c8d211c479 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,9 +7,10 @@ name: Build # 两者均为 PEM,Windows/macOS 共用同一张证书。调用方通过 secrets: inherit 传入私钥, # vars 上下文在可复用工作流中天然可用。构建时会临时合成 PFX(Windows)和 P12(macOS), # 对 exe / msi / msix 以及 app / dmg 做本地签名;未设置时跳过所有签名步骤。 -# macOS 的 SIGNATURE_CRT 必须是完整证书链(叶子证书在前,逐级签发 CA 在后,最后一张 -# 为自签名根)或一张自签名证书:codesign 需要自己构建到自签名根的链,只给 CA 签发的 -# 叶子证书会因缺链而报 errSecInternalComponent。 +# macOS 的 SIGNATURE_CRT 建议是完整证书链(叶子证书在前,逐级签发 CA 在后,最后一张 +# 为自签名根):codesign 需要自己构建到自签名根的链,只给 CA 签发的叶子证书会因缺链而 +# 报 errSecInternalComponent。若只给了叶子证书,macOS 步骤会用同一把私钥临时生成一张 +# 自签名证书完成签名(每次构建都不同),补上 CA 证书后会自动改回用你提供的链。 # 可选 SIGNATURE_KEYS_PASSWORD:私钥若带口令,用它解密。 on: workflow_call: @@ -332,15 +333,47 @@ jobs: chain_complete=0 fi - if [ "$chain_complete" -ne 1 ]; then - echo "::error::SIGNATURE_CRT has $CERT_COUNT certificate(s), but the last one is not a self-signed root." - echo "::error::Last certificate issuer: $root_issuer" - echo "::error::Last certificate subject: $root_subject" - echo '::error::codesign must build the chain itself; a bare leaf certificate cannot be used for signing.' - echo '::error::Append the issuing CA certificate to SIGNATURE_CRT (leaf first, then each CA, ending with the self-signed root):' - echo '::error:: cat leaf.crt ca.crt > chain.pem # paste chain.pem into SIGNATURE_CRT' - echo '::error::Or replace SIGNATURE_CRT with a self-signed certificate generated from the existing SIGNATURE_KEYS.' - exit 1 + if [ "$chain_complete" -eq 1 ]; then + SIGN_CERT="$LEAF" + TRUST_CERT="$ROOT" + echo "Using the provided certificate chain (root: $ROOT)" + else + # SIGNATURE_CRT 是 CA 签发的叶子证书,但缺签发它的 CA,codesign 无法 + # 构建到自签名根的链。这里用同一把私钥就地补一张自签名证书, + # 保留原证书的 subject(CN/O/OU),让 CI 无需人工改变量也能签名。 + # 代价:每次构建生成的证书不同(序列号/有效期不同)。如果想固定证书, + # 把签发 CA 的证书补进 SIGNATURE_CRT 即可,工作流会自动改回用你的链。 + echo "::warning::SIGNATURE_CRT does not end in a self-signed root (last issuer: $root_issuer)." + echo '::warning::Generating a self-signed certificate from SIGNATURE_KEYS for this build.' + echo '::warning::Add the issuing CA certificate to SIGNATURE_CRT to sign with your own stable certificate chain.' + SIGN_CERT="$RUNNER_TEMP/blockide-signing-selfsigned.crt" + TRUST_CERT="$SIGN_CERT" + EXT_FILE="$RUNNER_TEMP/blockide-signing-selfsigned.ext" + CSR_FILE="$RUNNER_TEMP/blockide-signing-selfsigned.csr" + printf '%s\n' \ + '[codesign]' \ + 'basicConstraints=critical,CA:TRUE' \ + 'keyUsage=critical,digitalSignature,keyCertSign' \ + 'extendedKeyUsage=critical,codeSigning' > "$EXT_FILE" + if [ -n "${SIGNATURE_KEYS_PASSWORD:-}" ]; then + /usr/bin/openssl x509 -x509toreq -in "$LEAF" -signkey "$KEY" -passin "pass:$SIGNATURE_KEYS_PASSWORD" -out "$CSR_FILE" + /usr/bin/openssl x509 -req -in "$CSR_FILE" -signkey "$KEY" -passin "pass:$SIGNATURE_KEYS_PASSWORD" -sha256 -days 7300 -extfile "$EXT_FILE" -extensions codesign -out "$SIGN_CERT" + else + /usr/bin/openssl x509 -x509toreq -in "$LEAF" -signkey "$KEY" -out "$CSR_FILE" + /usr/bin/openssl x509 -req -in "$CSR_FILE" -signkey "$KEY" -sha256 -days 7300 -extfile "$EXT_FILE" -extensions codesign -out "$SIGN_CERT" + fi + echo 'Generated self-signed signing certificate:' + /usr/bin/openssl x509 -in "$SIGN_CERT" -noout -subject -issuer -dates + fi + + # Apple 的时间戳服务不保证支持非 Apple 签发的身份(codesign/osx-sign + # 文档均注明),私有证书的时间戳也没有实际意义:直接关闭,避免签名阶段 + # 因时间戳服务拒绝而失败。只有证书链以 Apple 根证书结尾时才启用。 + if [ "$chain_complete" -eq 1 ] && printf '%s' "$root_subject" | grep -qi 'O=Apple Inc\.'; then + echo 'Using the Apple timestamp service (chain is anchored at an Apple root)' + else + echo 'Disabling codesign timestamping (non-Apple signing identity)' + echo 'CODESIGN_TIMESTAMP=none' >> "$GITHUB_ENV" fi KEYCHAIN_PASSWORD="$(openssl rand -hex 24)" @@ -350,10 +383,10 @@ jobs: # 这里不拼空数组参数,macOS 默认的 bash 3.2 在 set -u 下展开空数组会报错。 # 只导出叶子证书;证书链中的其他证书稍后单独导入临时钥匙串。 if [ -n "${SIGNATURE_KEYS_PASSWORD:-}" ]; then - /usr/bin/openssl pkcs12 -export -inkey "$KEY" -in "$LEAF" -out "$P12" \ + /usr/bin/openssl pkcs12 -export -inkey "$KEY" -in "$SIGN_CERT" -out "$P12" \ -passout "pass:$P12_PASSWORD" -passin "pass:$SIGNATURE_KEYS_PASSWORD" -name BlockIDE else - /usr/bin/openssl pkcs12 -export -inkey "$KEY" -in "$LEAF" -out "$P12" \ + /usr/bin/openssl pkcs12 -export -inkey "$KEY" -in "$SIGN_CERT" -out "$P12" \ -passout "pass:$P12_PASSWORD" -name BlockIDE fi @@ -364,18 +397,22 @@ jobs: security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN" security list-keychains -d user -s "$KEYCHAIN" $(security list-keychains -d user | tr -d '"') - # 中间/根 CA 必须进钥匙串,codesign 才能把完整的链嵌进签名 - for extra in "$CERT_DIR"/cert-*.pem; do - if [ "$extra" != "$LEAF" ]; then - security import "$extra" -k "$KEYCHAIN" - fi - done + # 中间/根 CA 必须进钥匙串,codesign 才能把完整的链嵌进签名。 + # 自动生成自签名证书时没有额外证书需要导入。 + if [ "$SIGN_CERT" = "$LEAF" ]; then + for extra in "$CERT_DIR"/cert-*.pem; do + if [ "$extra" != "$LEAF" ]; then + security import "$extra" -k "$KEYCHAIN" + fi + done + fi - # 只把链上的自签名根注册为信任锚,叶子证书保持“使用系统默认”。 - # 千万不要把叶子证书设成 trustAsRoot:那会让链在叶子处截断, - # codesign 报 "unable to build chain to self-signed root" + errSecInternalComponent。 - echo "Registering chain root as trustRoot for the codeSign policy: $ROOT" - sudo security add-trusted-cert -d -r trustRoot -p codeSign -k /Library/Keychains/System.keychain "$ROOT" + # 只把链上的自签名根(或自动生成的自签名证书)注册为信任锚, + # 叶子证书保持“使用系统默认”。千万不要把叶子证书设成 trustAsRoot: + # 那会让链在叶子处截断,codesign 报 + # "unable to build chain to self-signed root" + errSecInternalComponent。 + echo "Registering chain root as trustRoot for the codeSign policy: $TRUST_CERT" + sudo security add-trusted-cert -d -r trustRoot -p codeSign -k /Library/Keychains/System.keychain "$TRUST_CERT" # 用证书的 SHA-1 指纹而不是展示名作为身份:find-identity 的展示名可能带 # "(CSSMERR_TP_NOT_TRUSTED)" 之类的诊断后缀,按引号截取会把后缀一起写进 @@ -394,16 +431,23 @@ jobs: valid_line="$(security find-identity -v -p codesigning "$KEYCHAIN" | grep "$identity" || true)" if [ -z "$valid_line" ] || printf '%s\n' "$valid_line" | grep -q 'CSSMERR'; then security find-identity -p codesigning "$KEYCHAIN" || true - security verify-cert -c "$LEAF" -p codeSign || true - echo "::error::Signing identity $identity is still not trusted after registering $ROOT as the chain root." + security verify-cert -c "$SIGN_CERT" -p codeSign || true + echo "::error::Signing identity $identity is still not trusted after registering $TRUST_CERT as the trust root." exit 1 fi echo "Signing identity: $identity" - # 用一个临时文件做真实签名预检:链不完整或钥匙串访问有问题时, - # codesign 会返回 errSecInternalComponent;提前失败好过几分钟后才报错。 + # 用主可执行文件的副本做一次真实签名预检:证书链不完整或钥匙串访问 + # 有问题时 codesign 会返回 errSecInternalComponent,这里提前失败, + # 不必等到几分钟后签整个 app 才报错。 PREFLIGHT="$RUNNER_TEMP/blockide-signing-preflight" - printf 'preflight\n' > "$PREFLIGHT" + PRODUCT_NAME="$(node -p "require('./product.json').nameLong")" + APP_BINARY="$(dirname "$GITHUB_WORKSPACE")/VSCode-darwin-${VSCODE_ARCH}/${PRODUCT_NAME}.app/Contents/MacOS/${PRODUCT_NAME}" + if [ -f "$APP_BINARY" ]; then + cp -p "$APP_BINARY" "$PREFLIGHT" + else + printf 'preflight\n' > "$PREFLIGHT" + fi if ! codesign --force --sign "$identity" --keychain "$KEYCHAIN" --timestamp=none "$PREFLIGHT"; then echo '::error::codesign preflight failed; the signing identity or its certificate chain is unusable.' exit 1 diff --git a/build/darwin/sign.ts b/build/darwin/sign.ts index 24ebca75f19..01c2cd4f456 100644 --- a/build/darwin/sign.ts +++ b/build/darwin/sign.ts @@ -93,6 +93,9 @@ async function main(buildDir?: string): Promise { const tempDir = process.env['AGENT_TEMPDIRECTORY']; const arch = process.env['VSCODE_ARCH']; const identity = normalizeSigningIdentity(process.env['CODESIGN_IDENTITY']); + // Apple 的时间戳服务不保证支持非 Apple 签发的身份,使用自签名/私有 CA 证书时 + // 工作流会传 `none` 显式关闭时间戳;未设置时沿用 osx-sign 的默认行为。 + const timestamp = process.env['CODESIGN_TIMESTAMP']; if (!buildDir) { throw new Error('$AGENT_BUILDDIRECTORY not set'); @@ -116,15 +119,16 @@ async function main(buildDir?: string): Promise { optionsForFile: (filePath) => ({ entitlements: getEntitlementsForFile(filePath), hardenedRuntime: true, + timestamp, }), preAutoEntitlements: false, preEmbedProvisioningProfile: false, keychain: getKeychainPath(tempDir), version: getElectronVersion(), identity, - // BlockIDE 使用自签名证书,身份在系统信任评估中恒为 NOT_TRUSTED, - // osx-sign 默认的有效性校验会因此拒绝签名。codesign 本身允许用未受信任的 - // 身份签名,故跳过该校验。 + // 工作流的“Prepare signing certificate”步骤已经校验过证书链并确认身份可用, + // 且 CODESIGN_IDENTITY 传的是证书 SHA-1 指纹。这里跳过 osx-sign 自己的 + // find-identity 名称匹配,避免展示名与指纹的格式差异再次导致身份匹配失败。 identityValidation: false, }; From 602cabc491b4f1d51f218a5cd537ea428806ca45 Mon Sep 17 00:00:00 2001 From: Oliver Lin Date: Mon, 9 Nov 2026 01:41:27 +0800 Subject: [PATCH 10/10] fix(ci): improve macOS signing cert handling Accept the signing certificate chain from `secrets.CRT` or `vars.CRT` (full chain) with fallback to the legacy `vars.SIGNATURE_CRT`, and normalize CRLF line endings before parsing PEM. Detect the leaf certificate by matching its public key against `SIGNATURE_KEYS`, then reorder the chain leaf-first so certificate order in the PEM no longer matters. Fail with clear errors when no certificate block is found or no certificate matches the key. --- .github/workflows/build.yml | 99 +++++++++++++++++++++++++------------ 1 file changed, 68 insertions(+), 31 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5c8d211c479..6e0f1da40cd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,14 +3,16 @@ name: Build # 可复用的单平台构建工作流,由 release.yml 按平台矩阵调用。 # 每个平台的所有安装包在此完成并上传为 release-- 工件。 # -# 签名:私钥放在秘密变量 SIGNATURE_KEYS,证书放在可见变量 SIGNATURE_CRT(vars), -# 两者均为 PEM,Windows/macOS 共用同一张证书。调用方通过 secrets: inherit 传入私钥, -# vars 上下文在可复用工作流中天然可用。构建时会临时合成 PFX(Windows)和 P12(macOS), -# 对 exe / msi / msix 以及 app / dmg 做本地签名;未设置时跳过所有签名步骤。 -# macOS 的 SIGNATURE_CRT 建议是完整证书链(叶子证书在前,逐级签发 CA 在后,最后一张 -# 为自签名根):codesign 需要自己构建到自签名根的链,只给 CA 签发的叶子证书会因缺链而 -# 报 errSecInternalComponent。若只给了叶子证书,macOS 步骤会用同一把私钥临时生成一张 -# 自签名证书完成签名(每次构建都不同),补上 CA 证书后会自动改回用你提供的链。 +# 签名:私钥放在秘密变量 SIGNATURE_KEYS。macOS 的证书来源按优先级: +# 1) secrets.CRT 或 vars.CRT —— 完整证书链(叶子 + 逐级签发 CA,最后是自签名根) +# 2) vars.SIGNATURE_CRT —— 旧的单张证书变量 +# Windows 仍使用 vars.SIGNATURE_CRT(只需叶子证书)。两者均为 PEM。构建时会临时合成 +# PFX(Windows)和 P12(macOS),对 exe / msi / msix 以及 app / dmg 做本地签名; +# 未设置时跳过所有签名步骤。 +# macOS 的证书建议用完整链:codesign 需要自己构建到自签名根的链,只给 CA 签发的叶子 +# 证书会因缺链而报 errSecInternalComponent。若只给了叶子证书,macOS 步骤会用同一把 +# 私钥临时生成一张自签名证书完成签名(每次构建都不同)。 +# macOS 会按公钥自动识别叶子证书并重排证书顺序,叶子证书在前/在后都能用。 # 可选 SIGNATURE_KEYS_PASSWORD:私钥若带口令,用它解密。 on: workflow_call: @@ -272,13 +274,14 @@ jobs: if: inputs.platform == 'darwin' && env.HAS_SIGNING_KEYS == 'true' env: SIGNING_KEY: ${{ secrets.SIGNATURE_KEYS }} - SIGNING_CRT: ${{ vars.SIGNATURE_CRT }} + # 优先用 CRT(完整证书链,secret 或 variable 均可),没设置时回退到旧的单证书变量 + SIGNING_CRT: ${{ secrets.CRT || vars.CRT || vars.SIGNATURE_CRT }} SIGNATURE_KEYS_PASSWORD: ${{ secrets.SIGNATURE_KEYS_PASSWORD }} run: | set -euo pipefail if [ -z "${SIGNING_CRT:-}" ]; then - echo 'SIGNATURE_CRT variable is not set; it must contain the PEM certificate' + echo 'No signing certificate found: set secrets.CRT (or vars.CRT / vars.SIGNATURE_CRT) to the PEM chain.' exit 1 fi @@ -286,32 +289,66 @@ jobs: CERT="$RUNNER_TEMP/blockide-signing.crt" P12="$RUNNER_TEMP/blockide-signing.p12" KEYCHAIN="$RUNNER_TEMP/buildagent.keychain" - printf '%s\n' "$SIGNING_KEY" > "$KEY" - printf '%s\n' "$SIGNING_CRT" > "$CERT" - if ! grep -q 'BEGIN CERTIFICATE' "$CERT"; then - echo 'SIGNATURE_CRT does not contain a BEGIN CERTIFICATE block' - exit 1 - fi + # 统一去掉 CRLF,避免从 Windows 粘贴进来的换行影响 PEM 解析 + printf '%s\n' "$SIGNING_KEY" | tr -d '\r' > "$KEY" + printf '%s\n' "$SIGNING_CRT" | tr -d '\r' > "$CERT" - # SIGNATURE_CRT 必须是完整证书链或自签名证书: - # - 完整链:叶子证书在前,逐级签发 CA 在后,最后一张是自签名根; - # - 自签名证书:只有一张,且 issuer == subject。 - # 只给叶子证书不够:codesign 必须自己构建出到自签名根的链,缺失签发 CA 时 - # 会报 "unable to build chain to self-signed root" + errSecInternalComponent; - # 而把叶子证书设成 trustAsRoot 会让链在叶子处截断,codesign 同样拒绝签名。 + # 按证书块拆分 PEM:即使 secret 里混入了私钥,也只会取证书部分 CERT_DIR="$RUNNER_TEMP/blockide-certs" rm -rf "$CERT_DIR" mkdir -p "$CERT_DIR" - awk -v dir="$CERT_DIR" '/-----BEGIN CERTIFICATE-----/{n++} n > 0 { file = dir "/cert-" n ".pem"; print >> file }' "$CERT" - CERT_COUNT="$(ls "$CERT_DIR"/cert-*.pem | wc -l | tr -d ' ')" - LEAF="$CERT_DIR/cert-1.pem" + awk -v dir="$CERT_DIR" ' + /-----BEGIN CERTIFICATE-----/ { n++; file = dir "/cert-" n ".pem"; capture = 1 } + capture { print >> file } + /-----END CERTIFICATE-----/ { capture = 0 } + ' "$CERT" + CERT_COUNT="$(ls -1 "$CERT_DIR" | wc -l | tr -d ' ')" + if [ "$CERT_COUNT" -lt 1 ]; then + echo '::error::The signing certificate contains no BEGIN CERTIFICATE block.' + exit 1 + fi + + # 用公钥匹配找出真正与 SIGNATURE_KEYS 配对的叶子证书,并把链首调整为它, + # 这样证书在 CRT 里的先后顺序(叶子在前 / CA 在前)都不影响结果。 + if [ -n "${SIGNATURE_KEYS_PASSWORD:-}" ]; then + KEY_PUB="$(/usr/bin/openssl pkey -in "$KEY" -passin "pass:$SIGNATURE_KEYS_PASSWORD" -pubout)" + else + KEY_PUB="$(/usr/bin/openssl pkey -in "$KEY" -pubout)" + fi + LEAF="" + for cert in "$CERT_DIR"/cert-*.pem; do + if [ "$(/usr/bin/openssl x509 -in "$cert" -noout -pubkey)" = "$KEY_PUB" ]; then + LEAF="$cert" + break + fi + done + if [ -z "$LEAF" ]; then + echo '::error::None of the certificates matches SIGNATURE_KEYS (public key mismatch).' + exit 1 + fi + if [ "$LEAF" != "$CERT_DIR/cert-1.pem" ]; then + echo '::notice::Leaf certificate was not first in the PEM; reordering the chain leaf-first.' + CHAIN_DIR="$RUNNER_TEMP/blockide-chain" + rm -rf "$CHAIN_DIR" + mkdir -p "$CHAIN_DIR" + cp "$LEAF" "$CHAIN_DIR/cert-1.pem" + index=1 + for cert in "$CERT_DIR"/cert-*.pem; do + if [ "$cert" != "$LEAF" ]; then + index=$((index + 1)) + cp "$cert" "$CHAIN_DIR/cert-$index.pem" + fi + done + CERT_DIR="$CHAIN_DIR" + LEAF="$CERT_DIR/cert-1.pem" + fi ROOT="$CERT_DIR/cert-$CERT_COUNT.pem" - echo "SIGNATURE_CRT contains $CERT_COUNT certificate(s); signing certificate:" + echo "Signing PEM contains $CERT_COUNT certificate(s); leaf certificate:" /usr/bin/openssl x509 -in "$LEAF" -noout -subject -issuer -dates # 过期证书无论怎么设置信任都无法用于签名,提前给出明确错误 if ! /usr/bin/openssl x509 -in "$LEAF" -noout -checkend 0 >/dev/null 2>&1; then - echo '::error::The certificate in SIGNATURE_CRT has expired; codesign cannot use it.' + echo '::error::The signing certificate has expired; codesign cannot use it.' exit 1 fi @@ -338,14 +375,14 @@ jobs: TRUST_CERT="$ROOT" echo "Using the provided certificate chain (root: $ROOT)" else - # SIGNATURE_CRT 是 CA 签发的叶子证书,但缺签发它的 CA,codesign 无法 + # 证书是 CA 签发的叶子证书,但缺签发它的 CA,codesign 无法 # 构建到自签名根的链。这里用同一把私钥就地补一张自签名证书, # 保留原证书的 subject(CN/O/OU),让 CI 无需人工改变量也能签名。 # 代价:每次构建生成的证书不同(序列号/有效期不同)。如果想固定证书, - # 把签发 CA 的证书补进 SIGNATURE_CRT 即可,工作流会自动改回用你的链。 - echo "::warning::SIGNATURE_CRT does not end in a self-signed root (last issuer: $root_issuer)." + # 把签发 CA 的证书补进 secrets.CRT 即可,工作流会自动改回用你的链。 + echo "::warning::The signing certificate does not end in a self-signed root (last issuer: $root_issuer)." echo '::warning::Generating a self-signed certificate from SIGNATURE_KEYS for this build.' - echo '::warning::Add the issuing CA certificate to SIGNATURE_CRT to sign with your own stable certificate chain.' + echo '::warning::Add the issuing CA certificate to secrets.CRT to sign with your own stable certificate chain.' SIGN_CERT="$RUNNER_TEMP/blockide-signing-selfsigned.crt" TRUST_CERT="$SIGN_CERT" EXT_FILE="$RUNNER_TEMP/blockide-signing-selfsigned.ext"