Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 144 additions & 4 deletions .github/workflows/windows-msi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -41,7 +47,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"
}
}

Expand Down Expand Up @@ -70,7 +76,63 @@ jobs:
shell: pwsh
run: cargo install cargo-wix --locked

- name: Build MSI
- 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*'
$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
}

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
run: |
if (-not (Test-Path "wix\main.wxs")) {
Expand All @@ -79,7 +141,77 @@ jobs:
if (-not (Test-Path "wix\Product.ico")) {
throw "Missing wix\Product.ico"
}
cargo wix --nocapture
cargo build --release

- name: Sign application executable (PFX)
if: steps.signing.outputs.backend == 'pfx'
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 (PFX)
if: steps.signing.outputs.backend == 'pfx'
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 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
Expand All @@ -98,3 +230,11 @@ jobs:
overwrite_files: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- 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)) {
Remove-Item -LiteralPath $env:CODESIGN_PFX -Force
}
27 changes: 27 additions & 0 deletions .signpath/artifact-configurations/release.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Paste this into the SignPath project as artifact configuration slug: release

GitHub Actions uploads a ZIP (upload-artifact default), so the root is zip-file.
SignPath deep-signs bin/wslc-desktop.exe inside the MSI, then Authenticode-signs the MSI.
-->
<artifact-configuration xmlns="http://signpath.io/artifact-configuration/v1">
<parameters>
<parameter name="version" required="true" />
</parameters>
<zip-file>
<msi-file path="WSLc.Desktop-*.msi" subject="WSLc Desktop" author="WSLc Desktop">
<pe-file
path="bin/wslc-desktop.exe"
product-name="WSLc Desktop"
product-version="${version}"
file-version="${version}"
company-name="WSLc Desktop"
copyright="Copyright WSLc Desktop contributors"
original-filename="wslc-desktop.exe">
<authenticode-sign hash-algorithm="sha256" />
</pe-file>
<authenticode-sign hash-algorithm="sha256" />
</msi-file>
</zip-file>
</artifact-configuration>
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

## [Unreleased]

### 变更

- 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

### 修复
Expand Down
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
55 changes: 53 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,62 @@ 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 内嵌)+ MSI** 做 Authenticode 签名。

#### 推荐:SignPath Foundation(开源免费)

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
.\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 **必须**能走 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) 工作流:构建 MSI 并创建 Release 挂载安装包。
打 `v*` tag 会触发 [Windows MSI](.github/workflows/windows-msi.yml):编译 → 打 MSI → SignPath(或 PFX)签名 → 创建 Release。

```powershell
# 版本与 Cargo.toml 对齐,例如 0.1.2 → v0.1.2
Expand Down
27 changes: 27 additions & 0 deletions build.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading
Loading