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:
|
||||
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)
|
||||
run: dotnet restore TeamsISO.Windows.slnf
|
||||
|
||||
|
|
@ -66,6 +84,42 @@ jobs:
|
|||
--output publish/TeamsISO-Console
|
||||
/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
|
||||
run: >
|
||||
dotnet build installer/TeamsISO.Installer.wixproj
|
||||
|
|
@ -82,6 +136,35 @@ jobs:
|
|||
"name=$($msi.Name)" >> $env:GITHUB_OUTPUT
|
||||
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
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
|
|
|
|||
|
|
@ -36,15 +36,44 @@ The workflow will:
|
|||
first if it doesn't exist. Pre-release flag is set automatically when the
|
||||
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
|
||||
v1.0 release, sign the MSI with an EV cert before publishing:
|
||||
The release workflow has optional signtool integration. It runs only when the
|
||||
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.
|
||||
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).
|
||||
### Enabling signing
|
||||
|
||||
Until that lands, downstream users will see the standard Windows SmartScreen
|
||||
warning on first launch — annoying but not blocking for early adopters.
|
||||
Set these secrets on `forge.wilddragon.net/zgaetano/teamsiso`
|
||||
→ 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