Skip to content

Commit 1d3775e

Browse files
authored
Merge pull request #107 from ashishkurmi/main
updating release workflow to sign windows MSIs and binaries
2 parents 8b92fa1 + 6a03fb4 commit 1d3775e

3 files changed

Lines changed: 599 additions & 62 deletions

File tree

.github/workflows/release.yml

Lines changed: 201 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -91,14 +91,12 @@ jobs:
9191
- name: Locate binaries and packages
9292
id: binaries
9393
run: |
94+
# Windows .exes are uploaded to the draft release directly by GoReleaser
95+
# (formats: binary keeps the raw build outputs in dist/<id>_<os>_<arch>/
96+
# subdirs without renaming, only the upload uses the templated name).
97+
# The windows-sign-and-package job downloads them from the draft release,
98+
# so we don't need to locate them locally here.
9499
DARWIN=$(find dist -type f -name '*darwin_unnotarized' | head -1)
95-
# Filter agent vs launcher (-task) explicitly — both ship as
96-
# <name>-<version>-<os>_<arch>.exe so a bare *.exe glob would
97-
# match either.
98-
WIN_AMD64=$(find dist -type f -name 'stepsecurity-dev-machine-guard-*-windows_amd64.exe' ! -name '*-task-*' | head -1)
99-
WIN_ARM64=$(find dist -type f -name 'stepsecurity-dev-machine-guard-*-windows_arm64.exe' ! -name '*-task-*' | head -1)
100-
WIN_TASK_AMD64=$(find dist -type f -name 'stepsecurity-dev-machine-guard-task-*-windows_amd64.exe' | head -1)
101-
WIN_TASK_ARM64=$(find dist -type f -name 'stepsecurity-dev-machine-guard-task-*-windows_arm64.exe' | head -1)
102100
LINUX_AMD64=$(find dist -type f -name 'stepsecurity-dev-machine-guard' -path '*linux_amd64*' | head -1)
103101
LINUX_ARM64=$(find dist -type f -name 'stepsecurity-dev-machine-guard' -path '*linux_arm64*' | head -1)
104102
@@ -107,7 +105,7 @@ jobs:
107105
RPM_AMD64=$(find dist -type f -name '*-amd64.rpm' | head -1)
108106
RPM_ARM64=$(find dist -type f -name '*-arm64.rpm' | head -1)
109107
110-
for label in "darwin:${DARWIN}" "windows_amd64:${WIN_AMD64}" "windows_arm64:${WIN_ARM64}" "windows_task_amd64:${WIN_TASK_AMD64}" "windows_task_arm64:${WIN_TASK_ARM64}" "linux_amd64:${LINUX_AMD64}" "linux_arm64:${LINUX_ARM64}" "deb_amd64:${DEB_AMD64}" "deb_arm64:${DEB_ARM64}" "rpm_amd64:${RPM_AMD64}" "rpm_arm64:${RPM_ARM64}"; do
108+
for label in "darwin:${DARWIN}" "linux_amd64:${LINUX_AMD64}" "linux_arm64:${LINUX_ARM64}" "deb_amd64:${DEB_AMD64}" "deb_arm64:${DEB_ARM64}" "rpm_amd64:${RPM_AMD64}" "rpm_arm64:${RPM_ARM64}"; do
111109
name="${label%%:*}"
112110
path="${label#*:}"
113111
if [ -z "$path" ] || [ ! -f "$path" ]; then
@@ -118,10 +116,6 @@ jobs:
118116
done
119117
120118
echo "darwin=$DARWIN" >> "$GITHUB_OUTPUT"
121-
echo "win_amd64=$WIN_AMD64" >> "$GITHUB_OUTPUT"
122-
echo "win_arm64=$WIN_ARM64" >> "$GITHUB_OUTPUT"
123-
echo "win_task_amd64=$WIN_TASK_AMD64" >> "$GITHUB_OUTPUT"
124-
echo "win_task_arm64=$WIN_TASK_ARM64" >> "$GITHUB_OUTPUT"
125119
echo "linux_amd64=$LINUX_AMD64" >> "$GITHUB_OUTPUT"
126120
echo "linux_arm64=$LINUX_ARM64" >> "$GITHUB_OUTPUT"
127121
echo "deb_amd64=$DEB_AMD64" >> "$GITHUB_OUTPUT"
@@ -148,16 +142,11 @@ jobs:
148142
return 1
149143
}
150144
145+
# Windows .exes are signed in the windows-sign-and-package job after
146+
# Authenticode signing, so the cosign bundles match the bytes users
147+
# download from the published release.
151148
sign_with_retry "${{ steps.binaries.outputs.darwin }}" \
152149
"dist/stepsecurity-dev-machine-guard-darwin_unnotarized.bundle"
153-
sign_with_retry "${{ steps.binaries.outputs.win_amd64 }}" \
154-
"dist/stepsecurity-dev-machine-guard-windows_amd64.exe.bundle"
155-
sign_with_retry "${{ steps.binaries.outputs.win_arm64 }}" \
156-
"dist/stepsecurity-dev-machine-guard-windows_arm64.exe.bundle"
157-
sign_with_retry "${{ steps.binaries.outputs.win_task_amd64 }}" \
158-
"dist/stepsecurity-dev-machine-guard-task-windows_amd64.exe.bundle"
159-
sign_with_retry "${{ steps.binaries.outputs.win_task_arm64 }}" \
160-
"dist/stepsecurity-dev-machine-guard-task-windows_arm64.exe.bundle"
161150
sign_with_retry "${{ steps.binaries.outputs.linux_amd64 }}" \
162151
"dist/stepsecurity-dev-machine-guard-linux_amd64.bundle"
163152
sign_with_retry "${{ steps.binaries.outputs.linux_arm64 }}" \
@@ -175,12 +164,9 @@ jobs:
175164
env:
176165
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
177166
run: |
167+
# Windows .exe bundles are uploaded by the windows-sign-and-package job.
178168
gh release upload "${{ steps.release.outputs.tag }}" \
179169
dist/stepsecurity-dev-machine-guard-darwin_unnotarized.bundle \
180-
dist/stepsecurity-dev-machine-guard-windows_amd64.exe.bundle \
181-
dist/stepsecurity-dev-machine-guard-windows_arm64.exe.bundle \
182-
dist/stepsecurity-dev-machine-guard-task-windows_amd64.exe.bundle \
183-
dist/stepsecurity-dev-machine-guard-task-windows_arm64.exe.bundle \
184170
dist/stepsecurity-dev-machine-guard-linux_amd64.bundle \
185171
dist/stepsecurity-dev-machine-guard-linux_arm64.bundle \
186172
"${{ steps.binaries.outputs.deb_amd64 }}.bundle" \
@@ -190,43 +176,47 @@ jobs:
190176
--clobber
191177
192178
- name: Attest build provenance
179+
# Windows .exe and MSI attestations are emitted by the
180+
# windows-sign-and-package job after Authenticode signing.
193181
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
194182
with:
195183
subject-path: |
196184
${{ steps.binaries.outputs.darwin }}
197-
${{ steps.binaries.outputs.win_amd64 }}
198-
${{ steps.binaries.outputs.win_arm64 }}
199-
${{ steps.binaries.outputs.win_task_amd64 }}
200-
${{ steps.binaries.outputs.win_task_arm64 }}
201185
${{ steps.binaries.outputs.linux_amd64 }}
202186
${{ steps.binaries.outputs.linux_arm64 }}
203187
${{ steps.binaries.outputs.deb_amd64 }}
204188
${{ steps.binaries.outputs.deb_arm64 }}
205189
${{ steps.binaries.outputs.rpm_amd64 }}
206190
${{ steps.binaries.outputs.rpm_arm64 }}
207191
208-
build-msi:
209-
name: Build & Sign MSIs
192+
windows-sign-and-package:
193+
name: Sign Windows binaries, build & sign MSIs
210194
needs: release
211195
runs-on: windows-latest
196+
# The `release` environment (lowercase — must match the GitHub Environment
197+
# name exactly) requires two reviewers and is restricted to main. The Azure
198+
# Federated Identity Credential is bound to this environment's OIDC subject,
199+
# so azure/login below only works from jobs that gate on this environment.
200+
environment: release
212201
permissions:
213202
contents: write
214203
id-token: write
215204
attestations: write
216205

217206
steps:
218207
- name: Harden the runner (Audit all outbound calls)
219-
uses: step-security/harden-runner@58077d3c7e43986b6b15fba718e8ea69e387dfcc # v2.15.1
208+
# trusted-signing-action talks to wus2.codesigning.azure.net,
209+
# login.microsoftonline.com, timestamp.acs.microsoft.com, and Azure
210+
# storage CDNs. Audit mode logs warnings without blocking — capture
211+
# the full endpoint list before switching to block mode.
212+
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
220213
with:
221214
egress-policy: audit
222215

223216
- name: Checkout repository
224217
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
225218

226219
- name: Install WiX 4 + Util extension
227-
# WiX 4 ships as a .NET global tool. The Util extension (WixQuietExec
228-
# and friends) is a separate NuGet package that must be added to the
229-
# global wix tool before referencing util: namespace types.
230220
shell: pwsh
231221
run: |
232222
dotnet tool install --global wix --version 4.0.5
@@ -241,54 +231,212 @@ jobs:
241231
run: |
242232
$tag = "${{ needs.release.outputs.release_tag }}"
243233
New-Item -ItemType Directory -Path dist -Force | Out-Null
244-
# Goreleaser produces archive names like:
245-
# stepsecurity-dev-machine-guard-<version>-windows_amd64.exe
246-
# We download them by exact pattern to dist/.
247234
gh release download "$tag" `
248235
-R "${{ github.repository }}" `
249236
-p "*-windows_amd64.exe" `
250237
-p "*-windows_arm64.exe" `
251238
-D dist
252239
Get-ChildItem dist | Format-Table Name, Length
253240
254-
- name: Build MSIs (x64 + arm64)
241+
- name: Resolve Windows binary paths
242+
id: paths
255243
shell: pwsh
256244
run: |
257-
$version = "${{ needs.release.outputs.version }}"
258-
# Both agent and launcher .exes share the *-windows_<arch>.exe
259-
# suffix; filter on the -task- segment to tell them apart.
260-
$amd64 = Get-ChildItem dist -Filter "stepsecurity-dev-machine-guard-*-windows_amd64.exe" | Where-Object Name -NotLike '*-task-*' | Select-Object -First 1
261-
$arm64 = Get-ChildItem dist -Filter "stepsecurity-dev-machine-guard-*-windows_arm64.exe" | Where-Object Name -NotLike '*-task-*' | Select-Object -First 1
262-
$taskAmd64 = Get-ChildItem dist -Filter "stepsecurity-dev-machine-guard-task-*-windows_amd64.exe" | Select-Object -First 1
263-
$taskArm64 = Get-ChildItem dist -Filter "stepsecurity-dev-machine-guard-task-*-windows_arm64.exe" | Select-Object -First 1
245+
$ErrorActionPreference = 'Stop'
246+
$amd64 = Get-ChildItem dist -Filter "stepsecurity-dev-machine-guard-*-windows_amd64.exe" | Where-Object Name -NotLike '*-task-*' | Select-Object -First 1
247+
$arm64 = Get-ChildItem dist -Filter "stepsecurity-dev-machine-guard-*-windows_arm64.exe" | Where-Object Name -NotLike '*-task-*' | Select-Object -First 1
248+
$taskAmd64 = Get-ChildItem dist -Filter "stepsecurity-dev-machine-guard-task-*-windows_amd64.exe" | Select-Object -First 1
249+
$taskArm64 = Get-ChildItem dist -Filter "stepsecurity-dev-machine-guard-task-*-windows_arm64.exe" | Select-Object -First 1
264250
if (-not $amd64 -or -not $arm64 -or -not $taskAmd64 -or -not $taskArm64) {
265251
Write-Error "Windows .exe assets (agent + launcher, both arches) missing under dist/"
266252
Get-ChildItem dist | Format-Table Name
267253
exit 1
268254
}
255+
# trusted-signing-action v2 requires absolute (rooted) paths in `files:`.
256+
"amd64=$($amd64.FullName)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
257+
"arm64=$($arm64.FullName)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
258+
"task_amd64=$($taskAmd64.FullName)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
259+
"task_arm64=$($taskArm64.FullName)" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
260+
261+
- name: Azure login (OIDC)
262+
uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0
263+
with:
264+
client-id: ${{ secrets.AZURE_CLIENT_ID }}
265+
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
266+
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
267+
268+
- name: Authenticode-sign Windows .exes
269+
uses: Azure/trusted-signing-action@c7ab2a863ab5f9a846ddb8265964877ef296ee82 # v2.0.0
270+
with:
271+
endpoint: ${{ secrets.AZURE_TS_ENDPOINT }}
272+
signing-account-name: ${{ secrets.AZURE_TS_ACCOUNT_NAME }}
273+
certificate-profile-name: ${{ secrets.AZURE_TS_CERT_PROFILE_NAME }}
274+
files: |
275+
${{ steps.paths.outputs.amd64 }}
276+
${{ steps.paths.outputs.arm64 }}
277+
${{ steps.paths.outputs.task_amd64 }}
278+
${{ steps.paths.outputs.task_arm64 }}
279+
file-digest: SHA256
280+
timestamp-rfc3161: http://timestamp.acs.microsoft.com
281+
timestamp-digest: SHA256
282+
# Disable the action's same-job module cache to avoid a silent-exit
283+
# bug when this action runs twice in the same job. See "Authenticode-sign MSIs"
284+
# below — when both invocations get same-key cache hits, the second
285+
# invocation dies without logging an error.
286+
cache-dependencies: false
287+
288+
- name: Verify .exe signatures
289+
shell: pwsh
290+
run: |
291+
$ErrorActionPreference = 'Stop'
292+
$failed = $false
293+
foreach ($f in @(
294+
'${{ steps.paths.outputs.amd64 }}',
295+
'${{ steps.paths.outputs.arm64 }}',
296+
'${{ steps.paths.outputs.task_amd64 }}',
297+
'${{ steps.paths.outputs.task_arm64 }}'
298+
)) {
299+
$sig = Get-AuthenticodeSignature $f
300+
$subject = if ($sig.SignerCertificate) { $sig.SignerCertificate.Subject } else { '<none>' }
301+
Write-Host "$(Split-Path $f -Leaf): Status=$($sig.Status), Signer=$subject"
302+
if ($sig.Status -ne 'Valid') {
303+
Write-Host "::error::Signature status is $($sig.Status) (expected Valid) for $f"
304+
$failed = $true
305+
}
306+
if (-not $sig.TimeStamperCertificate) {
307+
Write-Host "::error::No RFC3161 timestamp on $f"
308+
$failed = $true
309+
}
310+
}
311+
if ($failed) { exit 1 }
312+
313+
- name: Install cosign
314+
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
315+
316+
- name: Sign signed .exes with Sigstore
317+
shell: bash
318+
run: |
319+
set -euo pipefail
320+
version="${{ needs.release.outputs.version }}"
321+
322+
sign_with_retry() {
323+
local blob="$1"
324+
local bundle="$2"
325+
for attempt in 1 2 3; do
326+
if cosign sign-blob "$blob" --bundle "$bundle" --yes; then
327+
return 0
328+
fi
329+
echo "::warning::Signing attempt $attempt failed for $(basename "$blob"), retrying in 10s..."
330+
sleep 10
331+
done
332+
echo "::error::Signing failed for $(basename "$blob") after 3 attempts"
333+
return 1
334+
}
335+
336+
sign_with_retry "dist/stepsecurity-dev-machine-guard-${version}-windows_amd64.exe" \
337+
"dist/stepsecurity-dev-machine-guard-windows_amd64.exe.bundle"
338+
sign_with_retry "dist/stepsecurity-dev-machine-guard-${version}-windows_arm64.exe" \
339+
"dist/stepsecurity-dev-machine-guard-windows_arm64.exe.bundle"
340+
sign_with_retry "dist/stepsecurity-dev-machine-guard-task-${version}-windows_amd64.exe" \
341+
"dist/stepsecurity-dev-machine-guard-task-windows_amd64.exe.bundle"
342+
sign_with_retry "dist/stepsecurity-dev-machine-guard-task-${version}-windows_arm64.exe" \
343+
"dist/stepsecurity-dev-machine-guard-task-windows_arm64.exe.bundle"
344+
345+
- name: Upload signed .exes and bundles to draft release
346+
env:
347+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
348+
shell: bash
349+
run: |
350+
set -euo pipefail
351+
tag="${{ needs.release.outputs.release_tag }}"
352+
version="${{ needs.release.outputs.version }}"
353+
gh release upload "$tag" \
354+
"dist/stepsecurity-dev-machine-guard-${version}-windows_amd64.exe" \
355+
"dist/stepsecurity-dev-machine-guard-${version}-windows_arm64.exe" \
356+
"dist/stepsecurity-dev-machine-guard-task-${version}-windows_amd64.exe" \
357+
"dist/stepsecurity-dev-machine-guard-task-${version}-windows_arm64.exe" \
358+
dist/stepsecurity-dev-machine-guard-windows_amd64.exe.bundle \
359+
dist/stepsecurity-dev-machine-guard-windows_arm64.exe.bundle \
360+
dist/stepsecurity-dev-machine-guard-task-windows_amd64.exe.bundle \
361+
dist/stepsecurity-dev-machine-guard-task-windows_arm64.exe.bundle \
362+
--clobber
363+
364+
- name: Attest .exe build provenance
365+
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
366+
with:
367+
subject-path: |
368+
${{ steps.paths.outputs.amd64 }}
369+
${{ steps.paths.outputs.arm64 }}
370+
${{ steps.paths.outputs.task_amd64 }}
371+
${{ steps.paths.outputs.task_arm64 }}
372+
373+
- name: Build MSIs (x64 + arm64)
374+
id: msi
375+
shell: pwsh
376+
run: |
377+
$ErrorActionPreference = 'Stop'
378+
$version = "${{ needs.release.outputs.version }}"
269379
270380
wix build packaging/windows/Product.wxs `
271381
-arch x64 `
272382
-ext WixToolset.Util.wixext `
273383
-d Arch=x64 `
274384
-d "Version=$version" `
275-
-d "BinaryPath=$($amd64.FullName)" `
276-
-d "LauncherPath=$($taskAmd64.FullName)" `
385+
-d "BinaryPath=${{ steps.paths.outputs.amd64 }}" `
386+
-d "LauncherPath=${{ steps.paths.outputs.task_amd64 }}" `
277387
-out "dist/stepsecurity-dev-machine-guard-$version-x64.msi"
278388
279389
wix build packaging/windows/Product.wxs `
280390
-arch arm64 `
281391
-ext WixToolset.Util.wixext `
282392
-d Arch=arm64 `
283393
-d "Version=$version" `
284-
-d "BinaryPath=$($arm64.FullName)" `
285-
-d "LauncherPath=$($taskArm64.FullName)" `
394+
-d "BinaryPath=${{ steps.paths.outputs.arm64 }}" `
395+
-d "LauncherPath=${{ steps.paths.outputs.task_arm64 }}" `
286396
-out "dist/stepsecurity-dev-machine-guard-$version-arm64.msi"
287397
398+
$x64 = (Get-Item "dist/stepsecurity-dev-machine-guard-$version-x64.msi").FullName
399+
$arm64 = (Get-Item "dist/stepsecurity-dev-machine-guard-$version-arm64.msi").FullName
400+
"x64=$x64" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
401+
"arm64=$arm64" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
402+
288403
Get-ChildItem dist -Filter "*.msi" | Format-Table Name, Length
289404
290-
- name: Install cosign
291-
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
405+
- name: Authenticode-sign MSIs
406+
uses: Azure/trusted-signing-action@c7ab2a863ab5f9a846ddb8265964877ef296ee82 # v2.0.0
407+
with:
408+
endpoint: ${{ secrets.AZURE_TS_ENDPOINT }}
409+
signing-account-name: ${{ secrets.AZURE_TS_ACCOUNT_NAME }}
410+
certificate-profile-name: ${{ secrets.AZURE_TS_CERT_PROFILE_NAME }}
411+
files: |
412+
${{ steps.msi.outputs.x64 }}
413+
${{ steps.msi.outputs.arm64 }}
414+
file-digest: SHA256
415+
timestamp-rfc3161: http://timestamp.acs.microsoft.com
416+
timestamp-digest: SHA256
417+
# Same workaround as the .exe sign step above — disable in-job cache
418+
# to avoid the silent-exit bug when both invocations get cache hits.
419+
cache-dependencies: false
420+
421+
- name: Verify MSI signatures
422+
shell: pwsh
423+
run: |
424+
$ErrorActionPreference = 'Stop'
425+
$failed = $false
426+
foreach ($msi in @('${{ steps.msi.outputs.x64 }}', '${{ steps.msi.outputs.arm64 }}')) {
427+
$sig = Get-AuthenticodeSignature $msi
428+
$subject = if ($sig.SignerCertificate) { $sig.SignerCertificate.Subject } else { '<none>' }
429+
Write-Host "$(Split-Path $msi -Leaf): Status=$($sig.Status), Signer=$subject"
430+
if ($sig.Status -ne 'Valid') {
431+
Write-Host "::error::MSI signature status is $($sig.Status) (expected Valid) for $msi"
432+
$failed = $true
433+
}
434+
if (-not $sig.TimeStamperCertificate) {
435+
Write-Host "::error::No RFC3161 timestamp on $msi"
436+
$failed = $true
437+
}
438+
}
439+
if ($failed) { exit 1 }
292440
293441
- name: Sign MSIs with Sigstore
294442
shell: bash
@@ -328,5 +476,5 @@ jobs:
328476
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
329477
with:
330478
subject-path: |
331-
dist/stepsecurity-dev-machine-guard-${{ needs.release.outputs.version }}-x64.msi
332-
dist/stepsecurity-dev-machine-guard-${{ needs.release.outputs.version }}-arm64.msi
479+
${{ steps.msi.outputs.x64 }}
480+
${{ steps.msi.outputs.arm64 }}

0 commit comments

Comments
 (0)