Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 34 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -645,49 +645,73 @@ jobs:
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

- name: Stage x64 OpenClaw Executables for Signing
- name: Stage x64 OpenClaw Binaries for Signing
shell: pwsh
run: |
New-Item -ItemType Directory -Path signing-input-x64 -Force | Out-Null
New-Item -ItemType HardLink -Path signing-input-x64\OpenClaw.Tray.WinUI.exe -Target artifacts\tray-win-x64\OpenClaw.Tray.WinUI.exe | Out-Null
$openClawBinaries = @(
"OpenClaw.Tray.WinUI.exe",
"OpenClaw.Tray.WinUI.dll",
"OpenClaw.Chat.dll",
"OpenClaw.Connection.dll",
"OpenClaw.SetupEngine.UI.dll",
"OpenClaw.SetupEngine.dll",
"OpenClaw.Shared.dll",
"OpenClawTray.FunctionalUI.dll"
)
foreach ($binary in $openClawBinaries) {
New-Item -ItemType HardLink -Path "signing-input-x64\$binary" -Target "artifacts\tray-win-x64\$binary" | Out-Null
}

- name: Stage ARM64 OpenClaw Executables for Signing
- name: Stage ARM64 OpenClaw Binaries for Signing
shell: pwsh
run: |
New-Item -ItemType Directory -Path signing-input-arm64 -Force | Out-Null
New-Item -ItemType HardLink -Path signing-input-arm64\OpenClaw.Tray.WinUI.exe -Target artifacts\tray-win-arm64\OpenClaw.Tray.WinUI.exe | Out-Null
$openClawBinaries = @(
"OpenClaw.Tray.WinUI.exe",
"OpenClaw.Tray.WinUI.dll",
"OpenClaw.Chat.dll",
"OpenClaw.Connection.dll",
"OpenClaw.SetupEngine.UI.dll",
"OpenClaw.SetupEngine.dll",
"OpenClaw.Shared.dll",
"OpenClawTray.FunctionalUI.dll"
)
foreach ($binary in $openClawBinaries) {
New-Item -ItemType HardLink -Path "signing-input-arm64\$binary" -Target "artifacts\tray-win-arm64\$binary" | Out-Null
}

- name: Sign x64 OpenClaw Executables
- name: Sign x64 OpenClaw Binaries
uses: azure/artifact-signing-action@v2
with:
endpoint: https://eus.codesigning.azure.net/
signing-account-name: openclaw
certificate-profile-name: openclaw
files-folder: signing-input-x64
files-folder-filter: exe
files-folder-filter: exe,dll
files-folder-depth: 1
file-digest: SHA256
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256

- name: Sign ARM64 OpenClaw Executables
- name: Sign ARM64 OpenClaw Binaries
uses: azure/artifact-signing-action@v2
with:
endpoint: https://eus.codesigning.azure.net/
signing-account-name: openclaw
certificate-profile-name: openclaw
files-folder: signing-input-arm64
files-folder-filter: exe
files-folder-filter: exe,dll
files-folder-depth: 1
file-digest: SHA256
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256

- name: Verify x64 Release Executable Signing Policy
- name: Verify x64 Release Binary Signing Policy
shell: pwsh
run: .\scripts\Test-ReleaseExecutableSignatures.ps1 -PayloadPath artifacts/tray-win-x64 -RequireSignedOpenClaw

- name: Verify ARM64 Release Executable Signing Policy
- name: Verify ARM64 Release Binary Signing Policy
shell: pwsh
run: .\scripts\Test-ReleaseExecutableSignatures.ps1 -PayloadPath artifacts/tray-win-arm64 -RequireSignedOpenClaw

Expand Down
35 changes: 23 additions & 12 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ build/sign/publish release artifacts.

```powershell
Select-String .\.github\workflows\ci.yml -Pattern `
"Verify Release Executable Signing Policy", `
"Verify Release Binary Signing Policy", `
"OpenClaw.Tray.WinUI.exe", `
"build-msix:", `
"MSIX distribution is paused"
Expand Down Expand Up @@ -89,14 +89,21 @@ installers and signed portable update payloads. This pause is independent of
whether a tag is stable or alpha. Re-enable MSIX only with packaged
camera/microphone consent validation and release coverage.

## Executable signing policy
## Binary signing policy

Only OpenClaw-owned executables should be signed by the OpenClaw release signing
Only OpenClaw-owned binaries should be signed by the OpenClaw release signing
identity.

OpenClaw-owned executables:
OpenClaw-owned binaries:

- `OpenClaw.Tray.WinUI.exe`
- `OpenClaw.Tray.WinUI.dll`
- `OpenClaw.Chat.dll`
- `OpenClaw.Connection.dll`
- `OpenClaw.SetupEngine.UI.dll`
- `OpenClaw.SetupEngine.dll`
- `OpenClaw.Shared.dll`
- `OpenClawTray.FunctionalUI.dll`

Third-party/runtime executables that must not be OpenClaw-signed:

Expand All @@ -106,8 +113,11 @@ Third-party/runtime executables that must not be OpenClaw-signed:
- `SetupEngine\RestartAgent.exe`

CI enforces this with `scripts\Test-ReleaseExecutableSignatures.ps1`. The
verifier fails closed on unknown `.exe` files so future payload changes are
reviewed deliberately.
verifier inspects every shipped `.exe` and `.dll`, fails closed on unknown
executables and unknown OpenClaw-named binaries, and rejects an OpenClaw
signature on third-party/runtime binaries. When release signing is required,
every allowlisted OpenClaw binary must have a valid signature from the expected
OpenClaw release signer; a valid signature from another publisher is rejected.

CI also checks native runtime dependencies before release packaging. Both the
x64 and ARM64 portable payloads must ship `vcruntime140.dll` in the payload
Expand Down Expand Up @@ -146,13 +156,13 @@ Do not add `AZURE_CLIENT_SECRET` back to the release workflow. The Entra app
registration should have a federated credential for:
`repo:openclaw/openclaw-windows-node:environment:release-signing`.

## How CI signs payload executables
## How CI signs payload binaries

The release workflow does not recursively sign every `.exe`. Instead it creates
temporary signing input directories with hardlinks to only the OpenClaw-owned
executables from the x64 and ARM64 payloads, then runs Azure Artifact Signing on
those allowlists. Because these are NTFS hardlinks, signing the staged file
signs the real payload file.
executables and DLLs from the x64 and ARM64 payloads, then runs Azure Artifact
Signing on those allowlists. Because these are NTFS hardlinks, signing the
staged file signs the real payload file.

After signing, CI verifies the actual payload directory, not the staging folder.
If hardlink signing does not affect the payload, the verifier fails before
Expand Down Expand Up @@ -180,8 +190,8 @@ The release job should:

1. Download x64/ARM64 tray payload artifacts.
2. Authenticate to Azure with OIDC in the `release-signing` environment.
3. Sign only the OpenClaw-owned EXEs in both payloads.
4. Verify executable signing policy.
3. Sign only the OpenClaw-owned EXEs and DLLs in both payloads.
4. Verify binary signing policy.
5. Create the portable x64 and ARM64 ZIPs.
6. Build Inno installers.
7. Sign installers.
Expand All @@ -206,6 +216,7 @@ Expected:
- Installer EXEs are signed.
- In ZIP payload:
- `OpenClaw.Tray.WinUI.exe` is OpenClaw-signed.
- All listed OpenClaw-owned DLLs are OpenClaw-signed.
- `wxc-exec.exe`, `createdump.exe`, and `RestartAgent.exe` are not
OpenClaw-signed.

Expand Down
94 changes: 65 additions & 29 deletions scripts/Test-ReleaseExecutableSignatures.ps1
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
<#
.SYNOPSIS
Verifies release payload executable signing policy.
Verifies release payload binary signing policy.

.DESCRIPTION
Classifies every .exe in a release payload. OpenClaw-owned executables must
be signed when -RequireSignedOpenClaw is passed. Third-party executables,
Classifies every .exe and .dll in a release payload. OpenClaw-owned binaries
must be signed when -RequireSignedOpenClaw is passed. Third-party binaries,
including wxc-exec.exe, must not be signed by the OpenClaw release signer.
Unknown executables fail closed.
Unknown executables and unknown OpenClaw-named binaries fail closed.

.PARAMETER PayloadPath
Root directory of the release payload to inspect.

.PARAMETER RequireSignedOpenClaw
Require OpenClaw-owned executables to have valid Authenticode signatures.
Require OpenClaw-owned binaries to have valid Authenticode signatures.

.PARAMETER OpenClawSignerPattern
Regex used to identify the OpenClaw release signer in signer subjects.
.PARAMETER OpenClawSignerSubject
Exact certificate subject required for OpenClaw-owned binaries.
#>
[CmdletBinding()]
param(
Expand All @@ -24,7 +24,7 @@ param(

[switch]$RequireSignedOpenClaw,

[string]$OpenClawSignerPattern = "OpenClaw Foundation"
[string]$OpenClawSignerSubject = "CN=OpenClaw Foundation, O=OpenClaw Foundation, L=Mill Valley, S=California, C=US"
)

Set-StrictMode -Version Latest
Expand All @@ -41,70 +41,106 @@ function Get-RelativePath {
[System.IO.Path]::GetRelativePath($Root, $Path).Replace('/', '\')
}

function Get-ExecutableClassification {
function Get-BinaryClassification {
param([Parameter(Mandatory = $true)][string]$RelativePath)

switch -Regex ($RelativePath) {
'^OpenClaw\.Tray\.WinUI\.exe$' { return "OpenClawOwned" }
'^OpenClaw\.Tray\.WinUI\.dll$' { return "OpenClawOwned" }
'^OpenClaw\.Chat\.dll$' { return "OpenClawOwned" }
'^OpenClaw\.Connection\.dll$' { return "OpenClawOwned" }
'^OpenClaw\.SetupEngine\.UI\.dll$' { return "OpenClawOwned" }
'^OpenClaw\.SetupEngine\.dll$' { return "OpenClawOwned" }
'^OpenClaw\.Shared\.dll$' { return "OpenClawOwned" }
'^OpenClawTray\.FunctionalUI\.dll$' { return "OpenClawOwned" }
'(^|\\)createdump\.exe$' { return "ThirdPartyExcluded" }
'(^|\\)RestartAgent\.exe$' { return "ThirdPartyExcluded" }
'^tools\\mxc\\[^\\]+\\wxc-exec\.exe$' { return "ThirdPartyExcluded" }
default { return "Unknown" }
'(^|\\)OpenClaw[^\\]*\.(exe|dll)$' { return "UnknownOpenClaw" }
'\.dll$' { return "ThirdPartyExcluded" }
default { return "UnknownExecutable" }
}
}

$executables = @(
Get-ChildItem -LiteralPath $payloadRoot -Recurse -File -Filter *.exe |
$binaries = @(
Get-ChildItem -LiteralPath $payloadRoot -Recurse -File |
Where-Object { $_.Extension -in ".exe", ".dll" } |
Sort-Object FullName |
ForEach-Object {
$relativePath = Get-RelativePath -Root $payloadRoot -Path $_.FullName
$signature = Get-AuthenticodeSignature -LiteralPath $_.FullName
$signerSubject = if ($signature.SignerCertificate) { $signature.SignerCertificate.Subject } else { "" }
[pscustomobject]@{
RelativePath = $relativePath
Classification = Get-ExecutableClassification -RelativePath $relativePath
Classification = Get-BinaryClassification -RelativePath $relativePath
SignatureStatus = $signature.Status.ToString()
SignerSubject = $signerSubject
}
}
)

if ($executables.Count -eq 0) {
throw "No executables found under $payloadRoot."
if ($binaries.Count -eq 0) {
throw "No executable or DLL binaries found under $payloadRoot."
}

$executables | Format-Table -AutoSize
$binaries | Format-Table -AutoSize

$errors = New-Object System.Collections.Generic.List[string]

foreach ($exe in $executables) {
switch ($exe.Classification) {
foreach ($binary in $binaries) {
switch ($binary.Classification) {
"OpenClawOwned" {
if ($RequireSignedOpenClaw -and $exe.SignatureStatus -ne "Valid") {
$errors.Add("OpenClaw executable is not validly signed: $($exe.RelativePath) [$($exe.SignatureStatus)]")
if ($RequireSignedOpenClaw -and $binary.SignatureStatus -ne "Valid") {
$errors.Add("OpenClaw binary is not validly signed: $($binary.RelativePath) [$($binary.SignatureStatus)]")
}
elseif ($RequireSignedOpenClaw -and
-not [string]::Equals(
$binary.SignerSubject,
$OpenClawSignerSubject,
[StringComparison]::OrdinalIgnoreCase)) {
$errors.Add("OpenClaw binary is not signed by the expected OpenClaw signer: $($binary.RelativePath) [$($binary.SignerSubject)]")
}
}
"ThirdPartyExcluded" {
if ($exe.SignatureStatus -eq "Valid" -and $exe.SignerSubject -match $OpenClawSignerPattern) {
$errors.Add("Third-party executable appears to be signed by OpenClaw release signer: $($exe.RelativePath) [$($exe.SignerSubject)]")
if ($binary.SignatureStatus -eq "Valid" -and
[string]::Equals(
$binary.SignerSubject,
$OpenClawSignerSubject,
[StringComparison]::OrdinalIgnoreCase)) {
$errors.Add("Third-party binary appears to be signed by OpenClaw release signer: $($binary.RelativePath) [$($binary.SignerSubject)]")
}
}
"UnknownOpenClaw" {
$errors.Add("Unknown OpenClaw binary in release payload: $($binary.RelativePath)")
}
default {
$errors.Add("Unknown executable in release payload: $($exe.RelativePath)")
$errors.Add("Unknown executable in release payload: $($binary.RelativePath)")
}
}
}

if (-not ($executables | Where-Object RelativePath -eq "OpenClaw.Tray.WinUI.exe")) {
$errors.Add("Missing OpenClaw.Tray.WinUI.exe.")
@(
"OpenClaw.Tray.WinUI.exe",
"OpenClaw.Tray.WinUI.dll",
"OpenClaw.Chat.dll",
"OpenClaw.Connection.dll",
"OpenClaw.SetupEngine.UI.dll",
"OpenClaw.SetupEngine.dll",
"OpenClaw.Shared.dll",
"OpenClawTray.FunctionalUI.dll"
) | ForEach-Object {
$requiredBinary = $_
if (-not ($binaries | Where-Object RelativePath -eq $requiredBinary)) {
$errors.Add("Missing OpenClaw binary: $_.")
}
}
if ($executables | Where-Object RelativePath -eq "SetupEngine\OpenClaw.SetupEngine.UI.exe") {
if ($binaries | Where-Object RelativePath -eq "SetupEngine\OpenClaw.SetupEngine.UI.exe") {
$errors.Add("SetupEngine\OpenClaw.SetupEngine.UI.exe should not be present in the release payload.")
}
if ($executables | Where-Object RelativePath -eq "SetupEngine\OpenClaw.SetupEngine.exe") {
if ($binaries | Where-Object RelativePath -eq "SetupEngine\OpenClaw.SetupEngine.exe") {
$errors.Add("SetupEngine\OpenClaw.SetupEngine.exe should not be present in the release payload.")
}
if (-not ($executables | Where-Object RelativePath -match '^tools\\mxc\\[^\\]+\\wxc-exec\.exe$')) {
if (-not ($binaries | Where-Object RelativePath -match '^tools\\mxc\\[^\\]+\\wxc-exec\.exe$')) {
$errors.Add("Missing tools\mxc\<arch>\wxc-exec.exe third-party executable.")
}

Expand All @@ -113,4 +149,4 @@ if ($errors.Count -gt 0) {
exit 1
}

Write-Host "Release executable signing policy passed." -ForegroundColor Green
Write-Host "Release binary signing policy passed." -ForegroundColor Green
Loading
Loading