@@ -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