From dbda9d35f79d19d8fbf3fea36c4175c42257e4ce Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 11 Oct 2026 12:06:39 +0000 Subject: [PATCH 1/2] ci: Authenticode-sign EXE and MSI for releases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gate tag builds on WINDOWS_CODESIGN_* secrets, sign the release binary before WiX packaging, then sign the MSI with RFC3161 timestamping to reduce SmartScreen warnings. Co-authored-by: 绿岸公式 --- .github/workflows/windows-msi.yml | 67 ++++++++++++++++- CHANGELOG.md | 5 ++ README.md | 25 ++++++- scripts/sign-windows.ps1 | 116 ++++++++++++++++++++++++++++++ 4 files changed, 208 insertions(+), 5 deletions(-) create mode 100644 scripts/sign-windows.ps1 diff --git a/.github/workflows/windows-msi.yml b/.github/workflows/windows-msi.yml index 2f41f9c..821c96b 100644 --- a/.github/workflows/windows-msi.yml +++ b/.github/workflows/windows-msi.yml @@ -41,7 +41,7 @@ jobs: foreach ($bin in $wixBins) { Write-Host "Adding WiX bin: $bin" echo "$bin" | Out-File -FilePath $env:GITHUB_PATH -Append -Encoding utf8 - $env:Path = "$bin;$env:Path" + $env:Path = "$bin;$env:PATH" } } @@ -70,7 +70,36 @@ jobs: shell: pwsh run: cargo install cargo-wix --locked - - name: Build MSI + - name: Prepare code signing certificate + id: codesign + shell: pwsh + env: + WINDOWS_CODESIGN_PFX_BASE64: ${{ secrets.WINDOWS_CODESIGN_PFX_BASE64 }} + WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }} + run: | + $isTag = $env:GITHUB_REF -like 'refs/tags/v*' + if ([string]::IsNullOrWhiteSpace($env:WINDOWS_CODESIGN_PFX_BASE64) -or + [string]::IsNullOrWhiteSpace($env:WINDOWS_CODESIGN_PASSWORD)) { + if ($isTag) { + throw @" + Tag releases must be Authenticode-signed to reduce SmartScreen warnings. + Add repository secrets: + WINDOWS_CODESIGN_PFX_BASE64 — base64 of the .pfx + WINDOWS_CODESIGN_PASSWORD — PFX password + "@ + } + Write-Host "No code-signing secrets; producing unsigned build." + "CODESIGN_ENABLED=false" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + return + } + + $pfx = Join-Path $env:RUNNER_TEMP "wslc-codesign.pfx" + [IO.File]::WriteAllBytes($pfx, [Convert]::FromBase64String($env:WINDOWS_CODESIGN_PFX_BASE64)) + "CODESIGN_ENABLED=true" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "CODESIGN_PFX=$pfx" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + Write-Host "Code-signing certificate imported to runner temp." + + - name: Build release binary shell: pwsh run: | if (-not (Test-Path "wix\main.wxs")) { @@ -79,7 +108,31 @@ jobs: if (-not (Test-Path "wix\Product.ico")) { throw "Missing wix\Product.ico" } - cargo wix --nocapture + cargo build --release + + - name: Sign application executable + if: env.CODESIGN_ENABLED == 'true' + shell: pwsh + env: + CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }} + WINDOWS_CODESIGN_TIMESTAMP_URL: ${{ vars.WINDOWS_CODESIGN_TIMESTAMP_URL }} + run: | + ./scripts/sign-windows.ps1 -Path "target\release\wslc-desktop.exe" + + - name: Build MSI (from current release binary) + shell: pwsh + run: cargo wix --nocapture --no-build + + - name: Sign MSI + if: env.CODESIGN_ENABLED == 'true' + shell: pwsh + env: + CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }} + WINDOWS_CODESIGN_TIMESTAMP_URL: ${{ vars.WINDOWS_CODESIGN_TIMESTAMP_URL }} + run: | + $msi = Get-ChildItem "target\wix\*.msi" | Select-Object -First 1 + if (-not $msi) { throw "No MSI under target\wix" } + ./scripts/sign-windows.ps1 -Path $msi.FullName - name: Upload MSI artifact uses: actions/upload-artifact@v4 @@ -98,3 +151,11 @@ jobs: overwrite_files: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Remove certificate from runner + if: always() && env.CODESIGN_ENABLED == 'true' + shell: pwsh + run: | + if ($env:CODESIGN_PFX -and (Test-Path -LiteralPath $env:CODESIGN_PFX)) { + Remove-Item -LiteralPath $env:CODESIGN_PFX -Force + } diff --git a/CHANGELOG.md b/CHANGELOG.md index 45e2f09..6209bf6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ ## [Unreleased] +### 变更 + +- Release 流水线支持 Authenticode 签名(先签 `wslc-desktop.exe`,再打 MSI 并签名),用于降低 SmartScreen 拦截。需配置 `WINDOWS_CODESIGN_PFX_BASE64` / `WINDOWS_CODESIGN_PASSWORD`;`v*` tag 未配置证书时 CI 会失败。 + - CI can Authenticode-sign the EXE and MSI when code-signing secrets are set; tag releases require them. + ## [0.1.2] - 2026-10-11 ### 修复 diff --git a/README.md b/README.md index 0f34998..2cfc8ef 100644 --- a/README.md +++ b/README.md @@ -57,11 +57,32 @@ cargo wix # 当前 0.1.2:WSLc.Desktop-0.1.2-x86_64.msi ``` -可选:`cargo wix --no-build`(已有 release 产物时)、`cargo wix --sign`(需 `signtool`)。 +可选:`cargo wix --no-build`(已有 release 产物时)。 + +### 代码签名(降低 SmartScreen) + +未签名的 MSI/EXE 容易触发 Windows SmartScreen。发布流水线会在有证书时对 **EXE + MSI** 做 Authenticode 签名(SHA-256 + 时间戳)。 + +1. 向受信任 CA 申请 **Windows 代码签名证书**(OV 可签;**EV** 对新发布者抑制 SmartScreen 更有效,声誉建立也更快) +2. 导出为 `.pfx`,在仓库 Settings → Secrets 添加: + - `WINDOWS_CODESIGN_PFX_BASE64`:`[Convert]::ToBase64String([IO.File]::ReadAllBytes('cert.pfx'))` + - `WINDOWS_CODESIGN_PASSWORD`:PFX 密码 +3. 可选 Variables:`WINDOWS_CODESIGN_TIMESTAMP_URL`(默认 `http://timestamp.digicert.com`) + +本地签名(已有 release 二进制与 MSI 时): + +```powershell +cargo build --release +.\scripts\sign-windows.ps1 -Path target\release\wslc-desktop.exe -PfxPath cert.pfx -PfxPassword '***' +cargo wix --no-build +.\scripts\sign-windows.ps1 -Path (Get-ChildItem target\wix\*.msi).FullName -PfxPath cert.pfx -PfxPassword '***' +``` + +`v*` tag 发布**必须**配置上述 Secrets,否则 CI 失败(避免再次放出未签名安装包)。PR / `main` 推送在无证书时仍可打出未签名 artifact。 ### 发布到 GitHub Release -打 `v*` tag 会触发 [Windows MSI](.github/workflows/windows-msi.yml) 工作流:构建 MSI 并创建 Release 挂载安装包。 +打 `v*` tag 会触发 [Windows MSI](.github/workflows/windows-msi.yml):编译 → 签名 EXE → 打 MSI → 签名 MSI → 创建 Release。 ```powershell # 版本与 Cargo.toml 对齐,例如 0.1.2 → v0.1.2 diff --git a/scripts/sign-windows.ps1 b/scripts/sign-windows.ps1 new file mode 100644 index 0000000..ca4069d --- /dev/null +++ b/scripts/sign-windows.ps1 @@ -0,0 +1,116 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Authenticode-sign Windows binaries / MSI with signtool (SHA-256 + RFC3161 timestamp). + +.DESCRIPTION + Expects a PFX on disk (from CI secrets) and Windows SDK signtool.exe. + Retries timestamping a few times — TSA endpoints are occasionally flaky. + +.PARAMETER Path + One or more .exe / .msi files to sign. + +.PARAMETER PfxPath + Path to the code-signing PFX. Defaults to $env:CODESIGN_PFX. + +.PARAMETER PfxPassword + PFX password. Defaults to $env:CODESIGN_PASSWORD / WINDOWS_CODESIGN_PASSWORD. + +.PARAMETER TimestampUrl + RFC3161 timestamp URL. Defaults to DigiCert. +#> +param( + [Parameter(Mandatory = $true)] + [string[]] $Path, + + [string] $PfxPath = $env:CODESIGN_PFX, + + [string] $PfxPassword = $( + if ($env:CODESIGN_PASSWORD) { $env:CODESIGN_PASSWORD } + else { $env:WINDOWS_CODESIGN_PASSWORD } + ), + + [string] $TimestampUrl = $( + if ($env:WINDOWS_CODESIGN_TIMESTAMP_URL) { $env:WINDOWS_CODESIGN_TIMESTAMP_URL } + else { 'http://timestamp.digicert.com' } + ) +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +function Find-SignTool { + $cmd = Get-Command signtool.exe -ErrorAction SilentlyContinue + if ($cmd) { return $cmd.Source } + + $kitRoot = "${env:ProgramFiles(x86)}\Windows Kits\10\bin" + if (-not (Test-Path $kitRoot)) { + throw "signtool.exe not found (Windows SDK missing under '$kitRoot')" + } + $candidate = Get-ChildItem -Path $kitRoot -Recurse -Filter signtool.exe -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -match '\\x64\\signtool\.exe$' } | + Sort-Object FullName -Descending | + Select-Object -First 1 + if (-not $candidate) { + throw 'signtool.exe not found under Windows Kits\10\bin' + } + return $candidate.FullName +} + +if ([string]::IsNullOrWhiteSpace($PfxPath) -or -not (Test-Path -LiteralPath $PfxPath)) { + throw "PFX not found. Set -PfxPath or CODESIGN_PFX (got '$PfxPath')." +} +if ($null -eq $PfxPassword) { + throw 'PFX password missing. Set -PfxPassword or CODESIGN_PASSWORD / WINDOWS_CODESIGN_PASSWORD.' +} + +$files = @() +foreach ($p in $Path) { + if (-not (Test-Path -LiteralPath $p)) { + throw "File to sign not found: $p" + } + $files += (Resolve-Path -LiteralPath $p).Path +} + +$signtool = Find-SignTool +Write-Host "Using signtool: $signtool" +Write-Host "Timestamp URL: $TimestampUrl" + +foreach ($file in $files) { + Write-Host "Signing $file" + $ok = $false + foreach ($attempt in 1..4) { + & $signtool sign ` + /fd SHA256 ` + /td SHA256 ` + /tr $TimestampUrl ` + /f $PfxPath ` + /p $PfxPassword ` + /d 'WSLc Desktop' ` + $file + if ($LASTEXITCODE -eq 0) { + $ok = $true + break + } + Write-Warning "signtool failed (exit $LASTEXITCODE), attempt $attempt/4" + Start-Sleep -Seconds (10 * $attempt) + } + if (-not $ok) { + throw "Failed to sign: $file" + } + + & $signtool verify /pa /v $file + if ($LASTEXITCODE -ne 0) { + throw "signtool verify failed: $file" + } + $sig = Get-AuthenticodeSignature -FilePath $file + if ($sig.Status -ne 'Valid') { + throw "Authenticode status for $file is $($sig.Status)" + } + if ($null -eq $sig.TimeStamperCertificate) { + throw "No timestamp countersignature on $file" + } + Write-Host "Verified: $file (signer=$($sig.SignerCertificate.Subject))" +} + +Write-Host 'All files signed and verified.' From 4ffc4b8de7492e7960753162be603fc52603b3b7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 11 Oct 2026 12:26:22 +0000 Subject: [PATCH 2/2] ci: sign releases via SignPath Foundation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prefer SignPath deep-sign of MSI (nested EXE) for free OSS Authenticode; keep PFX as fallback. Embed VERSIONINFO for SignPath metadata checks and document the code signing policy. Co-authored-by: 绿岸公式 --- .github/workflows/windows-msi.yml | 131 ++++++++++++++---- .signpath/artifact-configurations/release.xml | 27 ++++ CHANGELOG.md | 4 +- Cargo.toml | 3 + README.md | 48 +++++-- build.rs | 27 ++++ 6 files changed, 203 insertions(+), 37 deletions(-) create mode 100644 .signpath/artifact-configurations/release.xml create mode 100644 build.rs diff --git a/.github/workflows/windows-msi.yml b/.github/workflows/windows-msi.yml index 821c96b..cd4522f 100644 --- a/.github/workflows/windows-msi.yml +++ b/.github/workflows/windows-msi.yml @@ -7,15 +7,21 @@ on: pull_request: branches: [main] workflow_dispatch: + inputs: + sign: + description: "Submit unsigned MSI to SignPath (requires SignPath approval)" + type: boolean + default: false permissions: contents: write + actions: read jobs: msi: name: Build MSI (windows-msvc) runs-on: windows-latest - timeout-minutes: 90 + timeout-minutes: 120 steps: - name: Checkout @@ -70,34 +76,61 @@ jobs: shell: pwsh run: cargo install cargo-wix --locked - - name: Prepare code signing certificate - id: codesign + - name: Read package version + id: meta + shell: pwsh + run: | + $m = Select-String -Path Cargo.toml -Pattern '^\s*version\s*=\s*"([^"]+)"' | Select-Object -First 1 + if (-not $m) { throw "Could not read version from Cargo.toml" } + $version = $m.Matches.Groups[1].Value + "version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + Write-Host "Package version: $version" + + - name: Resolve code signing backend + id: signing shell: pwsh env: + SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }} + SIGNPATH_ORGANIZATION_ID: ${{ vars.SIGNPATH_ORGANIZATION_ID }} WINDOWS_CODESIGN_PFX_BASE64: ${{ secrets.WINDOWS_CODESIGN_PFX_BASE64 }} WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }} + DISPATCH_SIGN: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.sign || 'false' }} run: | $isTag = $env:GITHUB_REF -like 'refs/tags/v*' - if ([string]::IsNullOrWhiteSpace($env:WINDOWS_CODESIGN_PFX_BASE64) -or - [string]::IsNullOrWhiteSpace($env:WINDOWS_CODESIGN_PASSWORD)) { - if ($isTag) { - throw @" - Tag releases must be Authenticode-signed to reduce SmartScreen warnings. - Add repository secrets: - WINDOWS_CODESIGN_PFX_BASE64 — base64 of the .pfx - WINDOWS_CODESIGN_PASSWORD — PFX password - "@ - } - Write-Host "No code-signing secrets; producing unsigned build." - "CODESIGN_ENABLED=false" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + $wantSignPath = $isTag -or ($env:DISPATCH_SIGN -eq 'true') + $hasSignPath = -not [string]::IsNullOrWhiteSpace($env:SIGNPATH_API_TOKEN) -and + -not [string]::IsNullOrWhiteSpace($env:SIGNPATH_ORGANIZATION_ID) + $hasPfx = -not [string]::IsNullOrWhiteSpace($env:WINDOWS_CODESIGN_PFX_BASE64) -and + -not [string]::IsNullOrWhiteSpace($env:WINDOWS_CODESIGN_PASSWORD) + + if ($wantSignPath -and $hasSignPath) { + "backend=signpath" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + Write-Host "Signing backend: SignPath (OSS / Foundation certificate)" return } - $pfx = Join-Path $env:RUNNER_TEMP "wslc-codesign.pfx" - [IO.File]::WriteAllBytes($pfx, [Convert]::FromBase64String($env:WINDOWS_CODESIGN_PFX_BASE64)) - "CODESIGN_ENABLED=true" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - "CODESIGN_PFX=$pfx" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - Write-Host "Code-signing certificate imported to runner temp." + if ($hasPfx) { + $pfx = Join-Path $env:RUNNER_TEMP "wslc-codesign.pfx" + [IO.File]::WriteAllBytes($pfx, [Convert]::FromBase64String($env:WINDOWS_CODESIGN_PFX_BASE64)) + "backend=pfx" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + "CODESIGN_PFX=$pfx" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + Write-Host "Signing backend: local PFX (signtool)" + return + } + + if ($isTag) { + throw @" + Tag releases must be Authenticode-signed. + Preferred (free for OSS): SignPath Foundation — set + secrets.SIGNPATH_API_TOKEN + vars.SIGNPATH_ORGANIZATION_ID + and create project/signing policy + artifact config (see README). + Alternative: WINDOWS_CODESIGN_PFX_BASE64 + WINDOWS_CODESIGN_PASSWORD. + "@ + } + + "backend=none" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append + Write-Host "No signing backend configured; producing unsigned build." - name: Build release binary shell: pwsh @@ -110,8 +143,8 @@ jobs: } cargo build --release - - name: Sign application executable - if: env.CODESIGN_ENABLED == 'true' + - name: Sign application executable (PFX) + if: steps.signing.outputs.backend == 'pfx' shell: pwsh env: CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }} @@ -123,8 +156,8 @@ jobs: shell: pwsh run: cargo wix --nocapture --no-build - - name: Sign MSI - if: env.CODESIGN_ENABLED == 'true' + - name: Sign MSI (PFX) + if: steps.signing.outputs.backend == 'pfx' shell: pwsh env: CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }} @@ -134,6 +167,52 @@ jobs: if (-not $msi) { throw "No MSI under target\wix" } ./scripts/sign-windows.ps1 -Path $msi.FullName + - name: Upload unsigned MSI for SignPath + if: steps.signing.outputs.backend == 'signpath' + id: upload-unsigned + uses: actions/upload-artifact@v4 + with: + name: wslc-desktop-msi-unsigned + path: target/wix/*.msi + if-no-files-found: error + retention-days: 5 + + - name: Submit SignPath signing request + if: steps.signing.outputs.backend == 'signpath' + id: signpath + uses: signpath/github-action-submit-signing-request@v3 + with: + api-token: ${{ secrets.SIGNPATH_API_TOKEN }} + organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }} + project-slug: ${{ vars.SIGNPATH_PROJECT_SLUG || 'wslc-desktop' }} + signing-policy-slug: ${{ vars.SIGNPATH_SIGNING_POLICY_SLUG || 'release-signing' }} + artifact-configuration-slug: ${{ vars.SIGNPATH_ARTIFACT_CONFIGURATION_SLUG || 'release' }} + github-artifact-id: ${{ steps.upload-unsigned.outputs.artifact-id }} + wait-for-completion: true + # OSS policies require a human approver in SignPath before signing finishes. + wait-for-completion-timeout-in-seconds: 3600 + output-artifact-directory: ${{ github.workspace }}/signed + parameters: | + version: ${{ toJSON(steps.meta.outputs.version) }} + + - name: Install signed MSI from SignPath + if: steps.signing.outputs.backend == 'signpath' + shell: pwsh + run: | + $signed = Get-ChildItem -Path "signed" -Recurse -Filter "*.msi" -ErrorAction SilentlyContinue | + Select-Object -First 1 + if (-not $signed) { + throw "SignPath completed but no .msi found under signed/" + } + New-Item -ItemType Directory -Force -Path "target\wix" | Out-Null + Get-ChildItem "target\wix\*.msi" -ErrorAction SilentlyContinue | Remove-Item -Force + $dest = Join-Path "target\wix" $signed.Name + Copy-Item -LiteralPath $signed.FullName -Destination $dest -Force + Write-Host "Installed signed MSI: $dest" + if ('${{ steps.signpath.outputs.signing-request-web-url }}') { + Write-Host "SignPath request: ${{ steps.signpath.outputs.signing-request-web-url }}" + } + - name: Upload MSI artifact uses: actions/upload-artifact@v4 with: @@ -152,8 +231,8 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Remove certificate from runner - if: always() && env.CODESIGN_ENABLED == 'true' + - name: Remove PFX from runner + if: always() && steps.signing.outputs.backend == 'pfx' shell: pwsh run: | if ($env:CODESIGN_PFX -and (Test-Path -LiteralPath $env:CODESIGN_PFX)) { diff --git a/.signpath/artifact-configurations/release.xml b/.signpath/artifact-configurations/release.xml new file mode 100644 index 0000000..fb6679c --- /dev/null +++ b/.signpath/artifact-configurations/release.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + diff --git a/CHANGELOG.md b/CHANGELOG.md index 6209bf6..f5d484c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,8 @@ ### 变更 -- Release 流水线支持 Authenticode 签名(先签 `wslc-desktop.exe`,再打 MSI 并签名),用于降低 SmartScreen 拦截。需配置 `WINDOWS_CODESIGN_PFX_BASE64` / `WINDOWS_CODESIGN_PASSWORD`;`v*` tag 未配置证书时 CI 会失败。 - - CI can Authenticode-sign the EXE and MSI when code-signing secrets are set; tag releases require them. +- Release 流水线支持 Authenticode 签名以降低 SmartScreen 拦截。优先走 **SignPath Foundation**(深签 MSI 内 `bin/wslc-desktop.exe` 再签 MSI);也可备选自有 PFX。`v*` tag 未配置任一签名后端时 CI 失败。 + - CI Authenticode-signs via SignPath (preferred, free for OSS) or a local PFX; tag releases require one of them. ## [0.1.2] - 2026-10-11 diff --git a/Cargo.toml b/Cargo.toml index 2d55283..7cdb69f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,9 @@ repository = "https://github.com/timeroute/wslc-desktop" homepage = "https://timeroute.github.io/wslc-desktop-website/" authors = ["WSLc Desktop Contributors"] +[target.'cfg(windows)'.build-dependencies] +winres = "0.1" + [package.metadata.wix] # Used by `cargo wix` on Windows to produce target/wix/*.msi name = "WSLc Desktop" diff --git a/README.md b/README.md index 2cfc8ef..c8d1a00 100644 --- a/README.md +++ b/README.md @@ -61,15 +61,34 @@ cargo wix ### 代码签名(降低 SmartScreen) -未签名的 MSI/EXE 容易触发 Windows SmartScreen。发布流水线会在有证书时对 **EXE + MSI** 做 Authenticode 签名(SHA-256 + 时间戳)。 +未签名的 MSI/EXE 容易触发 Windows SmartScreen。发布流水线对 **EXE(MSI 内嵌)+ MSI** 做 Authenticode 签名。 -1. 向受信任 CA 申请 **Windows 代码签名证书**(OV 可签;**EV** 对新发布者抑制 SmartScreen 更有效,声誉建立也更快) -2. 导出为 `.pfx`,在仓库 Settings → Secrets 添加: - - `WINDOWS_CODESIGN_PFX_BASE64`:`[Convert]::ToBase64String([IO.File]::ReadAllBytes('cert.pfx'))` - - `WINDOWS_CODESIGN_PASSWORD`:PFX 密码 -3. 可选 Variables:`WINDOWS_CODESIGN_TIMESTAMP_URL`(默认 `http://timestamp.digicert.com`) +#### 推荐:SignPath Foundation(开源免费) -本地签名(已有 release 二进制与 MSI 时): +1. 在 [signpath.org/apply](https://signpath.org/apply) 为本仓库申请 SignPath Foundation 免费代码签名(需 OSI 许可、公开仓库、已有 Release、团队 MFA 等,见 [条件](https://signpath.org/terms.html))。 +2. 批准后在 SignPath: + - 启用 Trusted Build System:**GitHub.com**,并安装 SignPath GitHub App + - 创建 Project(建议 slug:`wslc-desktop`) + - 创建 Signing Policy(建议 slug:`release-signing`,发布需人工审批) + - 添加 Artifact Configuration,slug:`release`,XML 使用仓库内 [`.signpath/artifact-configurations/release.xml`](.signpath/artifact-configurations/release.xml) +3. 仓库 Settings → Secrets and variables → Actions: + +| 类型 | 名称 | 说明 | +|------|------|------| +| Secret | `SIGNPATH_API_TOKEN` | SignPath → Profile → API Tokens(Submitter) | +| Variable | `SIGNPATH_ORGANIZATION_ID` | 组织 ID(仪表盘 URL / 设置) | +| Variable(可选) | `SIGNPATH_PROJECT_SLUG` | 默认 `wslc-desktop` | +| Variable(可选) | `SIGNPATH_SIGNING_POLICY_SLUG` | 默认 `release-signing` | +| Variable(可选) | `SIGNPATH_ARTIFACT_CONFIGURATION_SLUG` | 默认 `release` | + +`v*` tag 会上传未签名 MSI → 提交 SignPath → **在 SignPath 控制台批准** → CI 下载已签名 MSI 并创建 Release(等待审批最长约 1 小时)。也可 Actions → **Windows MSI** → **Run workflow** 并勾选 `sign` 做试签。 + +#### 备选:自有 PFX + +若已有 OV/EV 代码签名证书,可改用本地 `signtool`(无 SignPath 配置时生效): + +- Secrets:`WINDOWS_CODESIGN_PFX_BASE64`、`WINDOWS_CODESIGN_PASSWORD` +- 可选 Variable:`WINDOWS_CODESIGN_TIMESTAMP_URL`(默认 DigiCert) ```powershell cargo build --release @@ -78,11 +97,22 @@ cargo wix --no-build .\scripts\sign-windows.ps1 -Path (Get-ChildItem target\wix\*.msi).FullName -PfxPath cert.pfx -PfxPassword '***' ``` -`v*` tag 发布**必须**配置上述 Secrets,否则 CI 失败(避免再次放出未签名安装包)。PR / `main` 推送在无证书时仍可打出未签名 artifact。 +`v*` tag **必须**能走 SignPath 或 PFX 之一,否则 CI 失败。PR / `main` 无证书时仍可打未签名 artifact。 + +### Code signing policy + +Free code signing provided by [SignPath.io](https://signpath.io), certificate by [SignPath Foundation](https://signpath.org). + +| Role | Who | +|------|-----| +| Committers and reviewers | [Repository collaborators](https://github.com/timeroute/wslc-desktop/settings/access) with write access | +| Approvers | [Repository owners / maintainers](https://github.com/timeroute/wslc-desktop/settings/access) who approve SignPath signing requests | + +Privacy policy: This program will not transfer any information to other networked systems unless specifically requested by the user or the person installing or operating it. ### 发布到 GitHub Release -打 `v*` tag 会触发 [Windows MSI](.github/workflows/windows-msi.yml):编译 → 签名 EXE → 打 MSI → 签名 MSI → 创建 Release。 +打 `v*` tag 会触发 [Windows MSI](.github/workflows/windows-msi.yml):编译 → 打 MSI → SignPath(或 PFX)签名 → 创建 Release。 ```powershell # 版本与 Cargo.toml 对齐,例如 0.1.2 → v0.1.2 diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..ef87e8d --- /dev/null +++ b/build.rs @@ -0,0 +1,27 @@ +fn main() { + #[cfg(windows)] + embed_windows_version_info(); +} + +#[cfg(windows)] +fn embed_windows_version_info() { + // VERSIONINFO strings must match SignPath artifact metadata restrictions + // (.signpath/artifact-configurations/release.xml) and the `version` parameter. + let version = std::env::var("CARGO_PKG_VERSION").expect("CARGO_PKG_VERSION"); + let mut res = winres::WindowsResource::new(); + res.set("ProductName", "WSLc Desktop"); + res.set( + "FileDescription", + "Lightweight desktop manager for Windows WSL Containers (WSLc)", + ); + res.set("CompanyName", "WSLc Desktop"); + res.set("LegalCopyright", "Copyright WSLc Desktop contributors"); + res.set("OriginalFilename", "wslc-desktop.exe"); + res.set("InternalName", "wslc-desktop"); + res.set("FileVersion", &version); + res.set("ProductVersion", &version); + if let Err(e) = res.compile() { + eprintln!("winres failed: {e}"); + std::process::exit(1); + } +}