Skip to content

Commit 2c4df9b

Browse files
danielhanchendanielhanchen
andauthored
Authenticode-sign the Windows stable-diffusion.cpp prebuilts (#13)
Co-authored-by: danielhanchen <moonshotaisubstack@gmail.com>
1 parent ea65600 commit 2c4df9b

4 files changed

Lines changed: 443 additions & 0 deletions

File tree

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# SPDX-License-Identifier: MIT
2+
# Copyright 2026-present the Unsloth AI Inc. team.
3+
name: Sign Windows binaries
4+
description: >-
5+
Authenticode-sign every PE in a built Windows tree with Azure Trusted Signing,
6+
so the bundles the Unsloth Studio installer downloads are not blocked by Smart
7+
App Control when they load on a user machine.
8+
9+
inputs:
10+
path:
11+
description: Directory to sign, searched recursively.
12+
required: true
13+
azure-client-id:
14+
description: AZURE_CLIENT_ID from the release-signing environment.
15+
required: false
16+
default: ''
17+
azure-client-secret:
18+
description: AZURE_CLIENT_SECRET from the release-signing environment.
19+
required: false
20+
default: ''
21+
azure-tenant-id:
22+
description: AZURE_TENANT_ID from the release-signing environment.
23+
required: false
24+
default: ''
25+
azure-account:
26+
description: AZURE_TRUSTED_SIGNING_ACCOUNT_NAME from the release-signing environment.
27+
required: false
28+
default: ''
29+
azure-certificate-profile:
30+
description: AZURE_CERTIFICATE_PROFILE_NAME from the release-signing environment.
31+
required: false
32+
default: ''
33+
endpoint:
34+
description: Azure Trusted Signing endpoint for the account's region.
35+
required: false
36+
default: https://eus.codesigning.azure.net
37+
38+
outputs:
39+
signed:
40+
description: '"true" when the tree was signed, "false" when signing was skipped on a fork.'
41+
value: ${{ steps.gate.outputs.signed }}
42+
43+
runs:
44+
using: composite
45+
steps:
46+
# Forks cannot read the signing secrets, so their builds are allowed through
47+
# unsigned rather than failing on something a contributor cannot fix. On the
48+
# canonical repo the same missing secret is a hard error: silently shipping
49+
# an unsigned release is the exact failure this action exists to prevent,
50+
# and a skip that looks like a pass is how the Defender scan in
51+
# unslothai/unsloth#8358 went missing for five releases.
52+
- name: Decide whether signing runs
53+
id: gate
54+
shell: pwsh
55+
env:
56+
AZURE_CLIENT_ID: ${{ inputs.azure-client-id }}
57+
run: |
58+
$ErrorActionPreference = 'Continue'
59+
$haveSecrets = -not [string]::IsNullOrWhiteSpace($env:AZURE_CLIENT_ID)
60+
# By owner rather than by full name: every unslothai repo that
61+
# publishes Windows bundles must fail closed here, and a fork under
62+
# any other owner cannot read the secrets to begin with.
63+
$canonical = ($env:GITHUB_REPOSITORY_OWNER -eq 'unslothai')
64+
if ($haveSecrets) {
65+
Add-Content -Path $env:GITHUB_OUTPUT -Value 'signed=true'
66+
Write-Host 'Azure Trusted Signing credentials present; signing.'
67+
} elseif ($canonical) {
68+
Write-Host "::error::Azure Trusted Signing secrets are missing on $env:GITHUB_REPOSITORY."
69+
Write-Host 'Add AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID,'
70+
Write-Host 'AZURE_TRUSTED_SIGNING_ACCOUNT_NAME and AZURE_CERTIFICATE_PROFILE_NAME'
71+
Write-Host 'to the release-signing environment. Refusing to publish unsigned binaries.'
72+
exit 1
73+
} else {
74+
Add-Content -Path $env:GITHUB_OUTPUT -Value 'signed=false'
75+
Write-Host "::warning::No signing secrets on $env:GITHUB_REPOSITORY; leaving binaries unsigned."
76+
}
77+
78+
# Pinned by digest. This binary runs in the step that holds the signing
79+
# credentials, so where it comes from is itself a key-handling question.
80+
- name: Install trusted-signing-cli
81+
if: ${{ steps.gate.outputs.signed == 'true' }}
82+
shell: pwsh
83+
env:
84+
TRUSTED_SIGNING_CLI_URL: https://github.com/Levminer/artifact-signing-cli/releases/download/0.10.0/trusted-signing-cli.exe
85+
TRUSTED_SIGNING_CLI_SHA256: 8c9d750ca582891a7433925dcf302d5d3761fbed757b46c4bf5b417562dccb0e
86+
run: |
87+
$ErrorActionPreference = 'Stop'
88+
$dir = Join-Path $env:RUNNER_TEMP 'trusted-signing-cli'
89+
New-Item -ItemType Directory -Force -Path $dir | Out-Null
90+
$dest = Join-Path $dir 'trusted-signing-cli.exe'
91+
Invoke-WebRequest -Uri $env:TRUSTED_SIGNING_CLI_URL -OutFile $dest -MaximumRetryCount 3 -RetryIntervalSec 5
92+
$actual = (Get-FileHash $dest -Algorithm SHA256).Hash.ToLower()
93+
$expected = $env:TRUSTED_SIGNING_CLI_SHA256.ToLower()
94+
if ($actual -ne $expected) {
95+
Write-Host "::error::trusted-signing-cli digest mismatch, refusing to sign with an unverified binary. Expected $expected, got $actual."
96+
exit 1
97+
}
98+
Write-Host "verified trusted-signing-cli sha256=$actual"
99+
Add-Content -Path $env:GITHUB_PATH -Value $dir
100+
101+
# Fails closed, matching the desktop release workflow. Without this a
102+
# missing or broken binary only surfaces later as a signing error with no
103+
# obvious cause.
104+
- name: Verify trusted-signing-cli
105+
if: ${{ steps.gate.outputs.signed == 'true' }}
106+
shell: pwsh
107+
run: |
108+
$ErrorActionPreference = 'Stop'
109+
$cli = Get-Command trusted-signing-cli -ErrorAction SilentlyContinue
110+
if (-not $cli) {
111+
Write-Output "::error::trusted-signing-cli is not on PATH. Check the install step above."
112+
exit 1
113+
}
114+
# sign-windows-tree.ps1 invokes this by bare name, so whatever PATH
115+
# resolves is what signs. Only the digest-verified copy may do that;
116+
# anything else under the same name is unverified by definition.
117+
$verified = [IO.Path]::GetFullPath((Join-Path (Join-Path $env:RUNNER_TEMP 'trusted-signing-cli') 'trusted-signing-cli.exe'))
118+
if ([IO.Path]::GetFullPath($cli.Source) -ne $verified) {
119+
Write-Output "::error::trusted-signing-cli resolved to $($cli.Source), not the verified $verified. Refusing to sign with a binary that was never digest checked."
120+
exit 1
121+
}
122+
# Two distinct failure modes, neither covering the other: a truncated
123+
# download cannot start at all, which raises under Stop and must be
124+
# caught; a binary that starts and exits non-zero does not raise, so
125+
# $LASTEXITCODE has to be read.
126+
try {
127+
& $cli.Source --version
128+
} catch {
129+
$detail = ($_.Exception.Message -replace '\s+', ' ').Trim()
130+
Write-Output "::error::trusted-signing-cli could not be started. Re-run to fetch it again; if it persists the pinned asset is bad. Underlying error: $detail"
131+
exit 1
132+
}
133+
if ($LASTEXITCODE -ne 0) {
134+
Write-Output "::error::trusted-signing-cli is on PATH but exited $LASTEXITCODE."
135+
exit 1
136+
}
137+
138+
- name: Sign every PE in the build tree
139+
if: ${{ steps.gate.outputs.signed == 'true' }}
140+
shell: pwsh
141+
env:
142+
AZURE_CLIENT_ID: ${{ inputs.azure-client-id }}
143+
AZURE_CLIENT_SECRET: ${{ inputs.azure-client-secret }}
144+
AZURE_TENANT_ID: ${{ inputs.azure-tenant-id }}
145+
AZURE_TRUSTED_SIGNING_ACCOUNT_NAME: ${{ inputs.azure-account }}
146+
AZURE_CERTIFICATE_PROFILE_NAME: ${{ inputs.azure-certificate-profile }}
147+
run: |
148+
$ErrorActionPreference = 'Continue'
149+
& "$env:GITHUB_ACTION_PATH/../../scripts/sign-windows-tree.ps1" -Path "${{ inputs.path }}" -Endpoint "${{ inputs.endpoint }}"
150+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# SPDX-License-Identifier: MIT
2+
# Copyright 2026-present the Unsloth AI Inc. team.
3+
# Fail if any PE inside a packaged Windows bundle is unsigned.
4+
#
5+
# This is the release gate. sign-windows-tree.ps1 signs the build output, but the
6+
# packaging steps copy extra files in afterwards (the OpenMP runtime out of the
7+
# Visual Studio redist tree, and the ROCm runtime DLLs out of the TheRock dist),
8+
# so the only place that can prove what actually ships is the finished zip.
9+
#
10+
# Runs against zips rather than directories on purpose: an unsigned file that
11+
# gets added between signing and packaging is invisible to any earlier check.
12+
13+
param(
14+
# Bundle zips to verify; every PE inside each is checked.
15+
[Parameter(Mandatory = $true)][string[]] $Path,
16+
# 7-Zip, preinstalled on the GitHub Windows images.
17+
[string] $SevenZip = '7z',
18+
# Known-unsigned leaf names to accept. Keep this empty. Anything listed here
19+
# is a file we ship that Smart App Control can still refuse to load.
20+
[string[]] $Allow = @()
21+
)
22+
23+
$ErrorActionPreference = 'Continue'
24+
$peExtensions = @('.exe', '.dll', '.pyd', '.sys', '.ocx', '.cpl', '.scr')
25+
26+
$unsigned = @()
27+
$checked = 0
28+
29+
foreach ($bundle in $Path) {
30+
if (-not (Test-Path -LiteralPath $bundle -PathType Leaf)) {
31+
Write-Host "::error::bundle not found: $bundle"
32+
exit 1
33+
}
34+
$name = Split-Path $bundle -Leaf
35+
Write-Host ''
36+
Write-Host "=== $name ==="
37+
38+
$root = if ($env:RUNNER_TEMP) { $env:RUNNER_TEMP } else { [System.IO.Path]::GetTempPath() }
39+
$dest = Join-Path $root ('sigcheck-' + [System.IO.Path]::GetFileNameWithoutExtension($name))
40+
Remove-Item -LiteralPath $dest -Recurse -Force -ErrorAction SilentlyContinue
41+
& $SevenZip x -y "-o$dest" $bundle | Out-Null
42+
# 7-Zip leaves a partial tree behind on error, so a created directory proves
43+
# nothing about whether the archive was fully extracted.
44+
if ($LASTEXITCODE -ne 0) {
45+
Write-Host "::error::7-Zip exited $LASTEXITCODE unpacking $name; contents not verified"
46+
exit 1
47+
}
48+
if (-not (Test-Path -LiteralPath $dest)) {
49+
Write-Host "::error::could not unpack $name; cannot verify its contents"
50+
exit 1
51+
}
52+
53+
$inner = @(
54+
Get-ChildItem -LiteralPath $dest -Recurse -File |
55+
Where-Object { $peExtensions -contains $_.Extension.ToLowerInvariant() }
56+
)
57+
# No hits means the archive did not contain what we think it does, not that
58+
# the payload is clean.
59+
if ($inner.Count -eq 0) {
60+
Write-Host "::error::no executable payload found inside $name; contents not verified"
61+
exit 1
62+
}
63+
64+
foreach ($f in ($inner | Sort-Object Name)) {
65+
$checked++
66+
$s = Get-AuthenticodeSignature -LiteralPath $f.FullName
67+
if ($s.Status -eq 'Valid') {
68+
Write-Host (' signed {0}' -f $f.Name)
69+
} elseif ($Allow -contains $f.Name) {
70+
Write-Host (' ALLOWED {0} ({1}) - explicitly accepted as unsigned' -f $f.Name, $s.Status)
71+
} else {
72+
# UnknownError covers both "no signature at all" and "chain did not
73+
# build"; StatusMessage is what distinguishes them.
74+
Write-Host (' UNSIGNED {0} ({1}) {2}' -f $f.Name, $s.Status, $s.StatusMessage)
75+
$unsigned += [pscustomobject]@{ Bundle = $name; File = $f.Name; Status = [string]$s.Status }
76+
}
77+
}
78+
Remove-Item -LiteralPath $dest -Recurse -Force -ErrorAction SilentlyContinue
79+
}
80+
81+
Write-Host ''
82+
Write-Host "checked $checked file(s) across $($Path.Count) bundle(s)"
83+
84+
if ($unsigned.Count -eq 0) {
85+
Write-Host 'Every PE in every bundle is validly signed.'
86+
exit 0
87+
}
88+
89+
Write-Host ''
90+
Write-Host '================ UNSIGNED FILES ================'
91+
$unsigned | Format-Table Bundle, File, Status -AutoSize | Out-String | Write-Host
92+
foreach ($u in $unsigned) {
93+
Write-Host "::error file=$($u.File)::$($u.File) in $($u.Bundle) is $($u.Status) and needs signing"
94+
}
95+
Write-Host ''
96+
Write-Host 'These ship to user machines and are loaded by llama-server.'
97+
Write-Host 'Smart App Control blocks unknown unsigned binaries as they load.'
98+
exit 1

0 commit comments

Comments
 (0)