ci: optional MSI + exe code-signing in release.yml
This commit is contained in:
parent
57c2922d1c
commit
4be5b39022
2 changed files with 121 additions and 9 deletions
|
|
@ -35,6 +35,24 @@ jobs:
|
||||||
with:
|
with:
|
||||||
dotnet-version: 8.0.x
|
dotnet-version: 8.0.x
|
||||||
|
|
||||||
|
# Probe for the signing cert up front and expose a step output downstream
|
||||||
|
# steps can use in their `if:` guards. We can't reference `secrets.*`
|
||||||
|
# directly from `if:` (Forgejo/GitHub policy), so we set a dummy env
|
||||||
|
# variable from the secret and check whether it's non-empty here.
|
||||||
|
- name: Detect signing configuration
|
||||||
|
id: signcfg
|
||||||
|
shell: pwsh
|
||||||
|
env:
|
||||||
|
PFX_PROBE: ${{ secrets.SIGN_CERT_PFX_BASE64 }}
|
||||||
|
run: |
|
||||||
|
if ($env:PFX_PROBE) {
|
||||||
|
"enabled=true" >> $env:GITHUB_OUTPUT
|
||||||
|
Write-Host "Code-signing: ENABLED (cert secret detected)."
|
||||||
|
} else {
|
||||||
|
"enabled=false" >> $env:GITHUB_OUTPUT
|
||||||
|
Write-Host "Code-signing: DISABLED (SIGN_CERT_PFX_BASE64 not set). Build will produce unsigned binaries."
|
||||||
|
}
|
||||||
|
|
||||||
- name: Restore (Windows solution filter)
|
- name: Restore (Windows solution filter)
|
||||||
run: dotnet restore TeamsISO.Windows.slnf
|
run: dotnet restore TeamsISO.Windows.slnf
|
||||||
|
|
||||||
|
|
@ -66,6 +84,42 @@ jobs:
|
||||||
--output publish/TeamsISO-Console
|
--output publish/TeamsISO-Console
|
||||||
/p:Version=${{ steps.ver.outputs.version }}
|
/p:Version=${{ steps.ver.outputs.version }}
|
||||||
|
|
||||||
|
# Code-sign the WPF .exe BEFORE the MSI is built, so the MSI's embedded
|
||||||
|
# binaries are signed too. Skipped silently when the signing secrets
|
||||||
|
# aren't configured — that's the default state and keeps unsigned builds
|
||||||
|
# working unchanged.
|
||||||
|
#
|
||||||
|
# To enable signing, set both Forgejo Actions secrets:
|
||||||
|
# SIGN_CERT_PFX_BASE64 — base64 of your code-signing PFX file
|
||||||
|
# ( certutil -encode in.pfx out.b64; strip BEGIN/END lines )
|
||||||
|
# SIGN_CERT_PASSWORD — the PFX password
|
||||||
|
# Optionally:
|
||||||
|
# SIGN_TIMESTAMP_URL — RFC 3161 timestamp server (default: digicert)
|
||||||
|
- name: Sign TeamsISO.exe (optional, skipped if no cert)
|
||||||
|
if: ${{ steps.signcfg.outputs.enabled == 'true' }}
|
||||||
|
env:
|
||||||
|
SIGN_CERT_PFX_BASE64: ${{ secrets.SIGN_CERT_PFX_BASE64 }}
|
||||||
|
SIGN_CERT_PASSWORD: ${{ secrets.SIGN_CERT_PASSWORD }}
|
||||||
|
SIGN_TIMESTAMP_URL: ${{ secrets.SIGN_TIMESTAMP_URL }}
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$tsUrl = if ($env:SIGN_TIMESTAMP_URL) { $env:SIGN_TIMESTAMP_URL } else { 'http://timestamp.digicert.com' }
|
||||||
|
$pfxPath = Join-Path $env:RUNNER_TEMP 'codesign.pfx'
|
||||||
|
[IO.File]::WriteAllBytes($pfxPath, [Convert]::FromBase64String($env:SIGN_CERT_PFX_BASE64))
|
||||||
|
$signtool = Get-ChildItem 'C:\Program Files (x86)\Windows Kits\10\bin' -Recurse -Filter signtool.exe `
|
||||||
|
| Where-Object { $_.FullName -match '\\x64\\' } `
|
||||||
|
| Select-Object -First 1
|
||||||
|
if (-not $signtool) { throw 'signtool.exe not found on runner' }
|
||||||
|
& $signtool.FullName sign `
|
||||||
|
/f $pfxPath `
|
||||||
|
/p $env:SIGN_CERT_PASSWORD `
|
||||||
|
/fd SHA256 `
|
||||||
|
/td SHA256 `
|
||||||
|
/tr $tsUrl `
|
||||||
|
'publish/TeamsISO/TeamsISO.exe'
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "signtool failed on TeamsISO.exe (exit $LASTEXITCODE)" }
|
||||||
|
Remove-Item $pfxPath -Force
|
||||||
|
|
||||||
- name: Build MSI installer
|
- name: Build MSI installer
|
||||||
run: >
|
run: >
|
||||||
dotnet build installer/TeamsISO.Installer.wixproj
|
dotnet build installer/TeamsISO.Installer.wixproj
|
||||||
|
|
@ -82,6 +136,35 @@ jobs:
|
||||||
"name=$($msi.Name)" >> $env:GITHUB_OUTPUT
|
"name=$($msi.Name)" >> $env:GITHUB_OUTPUT
|
||||||
Write-Host "MSI: $($msi.FullName) ($($msi.Length) bytes)"
|
Write-Host "MSI: $($msi.FullName) ($($msi.Length) bytes)"
|
||||||
|
|
||||||
|
# Sign the produced MSI itself. Same gate as exe signing — runs only if
|
||||||
|
# the cert secret is set. Splitting the two stages means the inner exe
|
||||||
|
# is signed before being embedded, AND the wrapping MSI carries its own
|
||||||
|
# signature for SmartScreen.
|
||||||
|
- name: Sign MSI (optional, skipped if no cert)
|
||||||
|
if: ${{ steps.signcfg.outputs.enabled == 'true' }}
|
||||||
|
env:
|
||||||
|
SIGN_CERT_PFX_BASE64: ${{ secrets.SIGN_CERT_PFX_BASE64 }}
|
||||||
|
SIGN_CERT_PASSWORD: ${{ secrets.SIGN_CERT_PASSWORD }}
|
||||||
|
SIGN_TIMESTAMP_URL: ${{ secrets.SIGN_TIMESTAMP_URL }}
|
||||||
|
MSI_PATH: ${{ steps.msi.outputs.path }}
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$tsUrl = if ($env:SIGN_TIMESTAMP_URL) { $env:SIGN_TIMESTAMP_URL } else { 'http://timestamp.digicert.com' }
|
||||||
|
$pfxPath = Join-Path $env:RUNNER_TEMP 'codesign.pfx'
|
||||||
|
[IO.File]::WriteAllBytes($pfxPath, [Convert]::FromBase64String($env:SIGN_CERT_PFX_BASE64))
|
||||||
|
$signtool = Get-ChildItem 'C:\Program Files (x86)\Windows Kits\10\bin' -Recurse -Filter signtool.exe `
|
||||||
|
| Where-Object { $_.FullName -match '\\x64\\' } `
|
||||||
|
| Select-Object -First 1
|
||||||
|
& $signtool.FullName sign `
|
||||||
|
/f $pfxPath `
|
||||||
|
/p $env:SIGN_CERT_PASSWORD `
|
||||||
|
/fd SHA256 `
|
||||||
|
/td SHA256 `
|
||||||
|
/tr $tsUrl `
|
||||||
|
$env:MSI_PATH
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "signtool failed on MSI (exit $LASTEXITCODE)" }
|
||||||
|
Remove-Item $pfxPath -Force
|
||||||
|
|
||||||
- name: Upload MSI as workflow artifact
|
- name: Upload MSI as workflow artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
|
|
|
||||||
|
|
@ -36,15 +36,44 @@ The workflow will:
|
||||||
first if it doesn't exist. Pre-release flag is set automatically when the
|
first if it doesn't exist. Pre-release flag is set automatically when the
|
||||||
tag contains `-alpha`, `-beta`, or `-rc`.
|
tag contains `-alpha`, `-beta`, or `-rc`.
|
||||||
|
|
||||||
## Code signing (TODO)
|
## Code signing
|
||||||
|
|
||||||
The `wixproj` has a `SignOutput` property hook but no actual cert wiring. For a
|
The release workflow has optional signtool integration. It runs only when the
|
||||||
v1.0 release, sign the MSI with an EV cert before publishing:
|
signing-cert secrets are configured on the repository — without them, builds
|
||||||
|
remain unsigned and produce a SmartScreen warning on first launch.
|
||||||
|
|
||||||
1. Add a `SIGNING_CERT_BASE64` and `SIGNING_CERT_PASSWORD` to repo Secrets.
|
### Enabling signing
|
||||||
2. Decode the cert into the runner's cert store at the start of the workflow.
|
|
||||||
3. Set `/p:SignOutput=true` on the `dotnet build` of the wixproj and configure
|
|
||||||
`signtool` invocation (the installer project will need a custom target).
|
|
||||||
|
|
||||||
Until that lands, downstream users will see the standard Windows SmartScreen
|
Set these secrets on `forge.wilddragon.net/zgaetano/teamsiso`
|
||||||
warning on first launch — annoying but not blocking for early adopters.
|
→ Settings → Actions → Secrets:
|
||||||
|
|
||||||
|
| Secret | Required | Notes |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `SIGN_CERT_PFX_BASE64` | yes | Base64 of your code-signing PFX file. Generate with `certutil -encode in.pfx out.b64`, then strip the `-----BEGIN/END CERTIFICATE-----` lines. |
|
||||||
|
| `SIGN_CERT_PASSWORD` | yes | The PFX password. |
|
||||||
|
| `SIGN_TIMESTAMP_URL` | no | RFC 3161 timestamp server. Defaults to `http://timestamp.digicert.com`. |
|
||||||
|
|
||||||
|
When all three are present, the workflow:
|
||||||
|
|
||||||
|
1. Decodes the PFX to a temp file on the runner before building.
|
||||||
|
2. Signs `publish/TeamsISO/TeamsISO.exe` after publish, before MSI build, so the
|
||||||
|
binary embedded in the MSI is signed too.
|
||||||
|
3. Signs the produced MSI itself after WiX builds it.
|
||||||
|
4. Wipes the temp PFX from disk.
|
||||||
|
|
||||||
|
Both signing steps use SHA-256 for both the file hash and the timestamp digest,
|
||||||
|
which is what current Microsoft / SmartScreen guidance requires.
|
||||||
|
|
||||||
|
### Cert types
|
||||||
|
|
||||||
|
- **OV (Organization Validation, ~$200/yr).** SmartScreen reputation is built
|
||||||
|
per-publisher over time; brand-new OV certs still trip the warning until
|
||||||
|
enough downloads accumulate.
|
||||||
|
- **EV (Extended Validation, ~$300/yr, hardware token).** SmartScreen-trusted
|
||||||
|
immediately. Token-based — to use one in CI you'll need to either (a) keep
|
||||||
|
the runner on a host with the token plugged in, or (b) move to a cloud
|
||||||
|
signing service like Azure Trusted Signing or DigiCert KeyLocker.
|
||||||
|
|
||||||
|
For v1.0 we recommend the Azure Trusted Signing route: replace the PFX block
|
||||||
|
in `release.yml` with `azure/trusted-signing-action` once an Azure subscription
|
||||||
|
is set up. The current PFX path is the simplest thing that works for now.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue