From 249656bff309d93e5f96a109afa4ee32c34c05af Mon Sep 17 00:00:00 2001 From: Scott Hanselman Date: Wed, 19 Aug 2026 16:04:59 -0700 Subject: [PATCH 1/2] fix(release): enforce first-party binary signing Expand the release policy and verifier to all eight OpenClaw-owned PE files, reject unknown OpenClaw binaries, preserve third-party exclusions, and require the exact OpenClaw Foundation signer subject. Co-authored-by: clawsweeper <280513762+clawsweeper@users.noreply.github.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: ea6ca262-e24c-4a46-99c1-1a7b59b24943 --- docs/RELEASING.md | 35 ++++--- scripts/Test-ReleaseExecutableSignatures.ps1 | 94 +++++++++++++------ .../ReleaseSigningWorkflowTests.cs | 35 +++++-- 3 files changed, 115 insertions(+), 49 deletions(-) diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 807e43396..588514861 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -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" @@ -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: @@ -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 @@ -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 @@ -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. @@ -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. diff --git a/scripts/Test-ReleaseExecutableSignatures.ps1 b/scripts/Test-ReleaseExecutableSignatures.ps1 index 550cc167b..357e2c3b1 100644 --- a/scripts/Test-ReleaseExecutableSignatures.ps1 +++ b/scripts/Test-ReleaseExecutableSignatures.ps1 @@ -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( @@ -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 @@ -41,20 +41,30 @@ 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 @@ -62,49 +72,75 @@ $executables = @( $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\\wxc-exec.exe third-party executable.") } @@ -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 diff --git a/tests/OpenClaw.Tray.Tests/ReleaseSigningWorkflowTests.cs b/tests/OpenClaw.Tray.Tests/ReleaseSigningWorkflowTests.cs index b4063785f..37fa375d8 100644 --- a/tests/OpenClaw.Tray.Tests/ReleaseSigningWorkflowTests.cs +++ b/tests/OpenClaw.Tray.Tests/ReleaseSigningWorkflowTests.cs @@ -3,7 +3,7 @@ namespace OpenClaw.Tray.Tests; public sealed class ReleaseSigningWorkflowTests { [Fact] - public void ReleaseWorkflow_SignsOnlyOpenClawOwnedPayloadExecutables() + public void ReleaseWorkflow_SignsOnlyOpenClawOwnedPayloadBinaries() { var workflow = File.ReadAllText(Path.Combine(TestRepositoryPaths.GetRepositoryRoot(), ".github", "workflows", "ci.yml")); @@ -15,19 +15,27 @@ public void ReleaseWorkflow_SignsOnlyOpenClawOwnedPayloadExecutables() Assert.Contains("endpoint: https://eus.codesigning.azure.net/", workflow); Assert.Contains("signing-account-name: openclaw", workflow); Assert.Contains("certificate-profile-name: openclaw", workflow); - Assert.Contains("Stage x64 OpenClaw Executables for Signing", workflow); - Assert.Contains(@"New-Item -ItemType HardLink -Path signing-input-x64\OpenClaw.Tray.WinUI.exe -Target artifacts\tray-win-x64\OpenClaw.Tray.WinUI.exe", workflow); + Assert.Contains("Stage x64 OpenClaw Binaries for Signing", workflow); + Assert.Contains("OpenClaw.Tray.WinUI.exe", workflow); + Assert.Contains("OpenClaw.Tray.WinUI.dll", workflow); + Assert.Contains("OpenClaw.Chat.dll", workflow); + Assert.Contains("OpenClaw.Connection.dll", workflow); + Assert.Contains("OpenClaw.SetupEngine.UI.dll", workflow); + Assert.Contains("OpenClaw.SetupEngine.dll", workflow); + Assert.Contains("OpenClaw.Shared.dll", workflow); + Assert.Contains("OpenClawTray.FunctionalUI.dll", workflow); + Assert.Contains(@"New-Item -ItemType HardLink -Path ""signing-input-x64\$binary"" -Target ""artifacts\tray-win-x64\$binary""", workflow); Assert.DoesNotContain("signing-input-x64\\OpenClaw.SetupEngine.exe", workflow); Assert.DoesNotContain("signing-input-x64\\OpenClaw.SetupEngine.UI.exe", workflow); - Assert.Contains("Sign x64 OpenClaw Executables", workflow); + Assert.Contains("Sign x64 OpenClaw Binaries", workflow); Assert.Contains("files-folder: signing-input-x64", workflow); - Assert.Contains("Stage ARM64 OpenClaw Executables for Signing", workflow); - Assert.Contains(@"New-Item -ItemType HardLink -Path signing-input-arm64\OpenClaw.Tray.WinUI.exe -Target artifacts\tray-win-arm64\OpenClaw.Tray.WinUI.exe", workflow); + Assert.Contains("Stage ARM64 OpenClaw Binaries for Signing", workflow); + Assert.Contains(@"New-Item -ItemType HardLink -Path ""signing-input-arm64\$binary"" -Target ""artifacts\tray-win-arm64\$binary""", workflow); Assert.DoesNotContain("signing-input-arm64\\OpenClaw.SetupEngine.exe", workflow); Assert.DoesNotContain("signing-input-arm64\\OpenClaw.SetupEngine.UI.exe", workflow); - Assert.Contains("Sign ARM64 OpenClaw Executables", workflow); + Assert.Contains("Sign ARM64 OpenClaw Binaries", workflow); Assert.Contains("files-folder: signing-input-arm64", workflow); - Assert.Contains("files-folder-filter: exe", workflow); + Assert.Contains("files-folder-filter: exe,dll", workflow); Assert.DoesNotContain("files-folder-recurse: true", workflow); } @@ -40,6 +48,13 @@ public void ReleaseWorkflow_VerifiesExecutableSigningPolicy() Assert.Contains("Test-ReleaseExecutableSignatures.ps1 -PayloadPath artifacts/tray-win-x64 -RequireSignedOpenClaw", workflow); Assert.Contains("Test-ReleaseExecutableSignatures.ps1 -PayloadPath artifacts/tray-win-arm64 -RequireSignedOpenClaw", workflow); Assert.Contains(@"^OpenClaw\.Tray\.WinUI\.exe$", verifier); + Assert.Contains(@"^OpenClaw\.Tray\.WinUI\.dll$", verifier); + Assert.Contains(@"^OpenClaw\.Chat\.dll$", verifier); + Assert.Contains(@"^OpenClaw\.Connection\.dll$", verifier); + Assert.Contains(@"^OpenClaw\.SetupEngine\.UI\.dll$", verifier); + Assert.Contains(@"^OpenClaw\.SetupEngine\.dll$", verifier); + Assert.Contains(@"^OpenClaw\.Shared\.dll$", verifier); + Assert.Contains(@"^OpenClawTray\.FunctionalUI\.dll$", verifier); Assert.DoesNotContain(@"^SetupEngine\\OpenClaw\.SetupEngine\.exe$", verifier); Assert.DoesNotContain(@"^SetupEngine\\OpenClaw\.SetupEngine\.UI\.exe$", verifier); Assert.Contains("SetupEngine\\OpenClaw.SetupEngine.exe should not be present", verifier); @@ -48,6 +63,10 @@ public void ReleaseWorkflow_VerifiesExecutableSigningPolicy() Assert.Contains(@"(^|\\)RestartAgent\.exe$", verifier); Assert.Contains(@"^tools\\mxc\\[^\\]+\\wxc-exec\.exe$", verifier); Assert.Contains("Unknown executable in release payload", verifier); + Assert.Contains("Unknown OpenClaw binary in release payload", verifier); + Assert.Contains("$OpenClawSignerSubject", verifier); + Assert.Contains("[StringComparison]::OrdinalIgnoreCase", verifier); + Assert.Contains("OpenClaw binary is not signed by the expected OpenClaw signer", verifier); } [Fact] From c04577d0eb8879fb22ced15a20a10d791eef3742 Mon Sep 17 00:00:00 2001 From: Scott Hanselman Date: Wed, 19 Aug 2026 18:00:30 -0700 Subject: [PATCH 2/2] Rename OpenClaw Executables to Binaries in CI fix(release): sign all first-party payload binaries --- .github/workflows/ci.yml | 44 +++++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 793885388..74ea7c97a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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