Harden Windows signing verification in CI#28
Conversation
There was a problem hiding this comment.
Pull request overview
This PR hardens the Windows signing verification flow in Buildkite CI by making the Windows build step fail closed (both for produced artifacts and the downloaded signing tools) and by ensuring the Node test suite actually runs in CI.
Changes:
- Wraps native command execution in the Windows Buildkite step to prevent non-zero exit codes from being masked.
- Moves Windows artifact signature verification into a dedicated PowerShell script that enforces presence of
dist\*.exeand verifies expected signer subject (and optional issuer). - Adds a Buildkite “Tests” step that runs
npm teston the default queue.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
.buildkite/pipeline.yml |
Hardens Windows step execution semantics and adds a CI test step. |
.buildkite/commands/verify_windows_signature.ps1 |
Adds fail-closed Authenticode verification for Windows artifacts, including signer identity checks. |
.buildkite/commands/setup_windows_code_signing.ps1 |
Verifies integrity of downloaded signing tools via SHA256 pinning or Microsoft Authenticode signature validation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
6656710 to
6d66d8d
Compare
mokagio
left a comment
There was a problem hiding this comment.
Thanks for this PR as well.
As in #27, I'm cursious whether this is a refinement motivated by a failure that was encountered or by caution.
It seems that Buildkite is not reporting the build status in the commits and PR checks, which is a problem we'll need to address.
In the meantime, I can confirm the new tests step passes:
Unfortunately, the new code in the Windows flow fails to parse:
[2026-07-06T21:40:49Z] At line:5 char:27
[2026-07-06T21:40:49Z] + [Parameter(Mandatory = )]
[2026-07-06T21:40:49Z] + ~
[2026-07-06T21:40:49Z] Missing statement after '=' in named argument.
[2026-07-06T21:40:49Z] At line:5 char:27
[2026-07-06T21:40:49Z] + [Parameter(Mandatory = )]
[2026-07-06T21:40:49Z] + ~
[2026-07-06T21:40:49Z] Missing closing ')' in expression.
[2026-07-06T21:40:49Z] At line:6 char:13
[2026-07-06T21:40:49Z] + [string],
[2026-07-06T21:40:49Z] + ~
[2026-07-06T21:40:49Z] Parameter declarations are a comma-separated list of variable names with optional initializer expressions.
[2026-07-06T21:40:49Z] At line:6 char:13
[2026-07-06T21:40:49Z] + [string],
[2026-07-06T21:40:49Z] + ~
[2026-07-06T21:40:49Z] Missing ')' in function parameter list.
[2026-07-06T21:40:49Z] At line:5 char:27
[2026-07-06T21:40:49Z] + [Parameter(Mandatory = )]
[2026-07-06T21:40:49Z] + ~
[2026-07-06T21:40:49Z] Attribute argument must be a constant or a script block.
[2026-07-06T21:40:49Z] + CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException
[2026-07-06T21:40:49Z] + FullyQualifiedErrorId : MissingExpressionInNamedArgument
|
@mokagio I don't have much experience with signing apps. I asked AI to review PR 21 at WordCamp Europe and its hardening notes seemed like a good addition to be cautious. So, I asked it to split them in smaller PRs. Feel free to make this PR your own as per your discretion :) |
|
@ashfame no worries! And thanks for the clarification. There's always room for improvements, and these are welcome indeed. |
| agents: { queue: windows } | ||
| plugins: [$CI_TOOLKIT, $NVM_PLUGIN] | ||
| command: | | ||
| $ErrorActionPreference = "Stop" |
There was a problem hiding this comment.
I'd suggest to move this whole command to an external script to avoid unexpected issues with the Buildkite variable interpolation (https://buildkite.com/docs/pipelines/configure/environment-variables#runtime-variable-interpolation). It also makes the code easier to read.
For an example of what might go wrong when using scripts directly in the .yml, I think $true here would need escaping so Buildkite doesn't attempt to replace it.
| $ErrorActionPreference = "Stop" | ||
| Set-StrictMode -Version Latest | ||
|
|
||
| & "setup_azure_trusted_signing.ps1" |
There was a problem hiding this comment.
So we run setup_azure_trusted_signing.ps1 from a8c-ci-toolkit-buildkite-plugin, which I think already executes signtool and loads the DLib? Then later in this script, we test the tool integrity after that.
I'd say installation, verification and execution need to be separate phases, so perhaps this should be in a8c-ci-toolkit, immediately after download and before the smoke test.
| } | ||
|
|
||
| $actualSubject = $signature.SignerCertificate.Subject | ||
| if ($actualSubject.IndexOf($env:WINDOWS_EXPECTED_SIGNER_SUBJECT.Trim(), [System.StringComparison]::OrdinalIgnoreCase) -lt 0) { |
There was a problem hiding this comment.
I'm finding this check a bit loose (IndexOf on the subject) 🤔 Not sure if this can also change?
Maybe there's a better way, but I'm not sure how it would work (I guess we need'd to look into https://learn.microsoft.com/en-us/azure/artifact-signing/concept-certificate-management#subscriber-identity-validation-eku -- )
|
|
||
| foreach ($artifact in $artifacts) { | ||
| Write-Host "Verifying Authenticode signature for $($artifact.FullName)" | ||
| & $env:SIGNTOOL_PATH verify /pa /v $artifact.FullName |
There was a problem hiding this comment.
Looking at this, I wonder if we should consider /tw so that "a warning should be generated if the signature is not time stamped"?
Stacked on #27. Follow-up hardening from the PR 21 Windows signing review.
This PR keeps the Windows Azure Trusted Signing migration, but makes CI fail closed around the signed artifact and the downloaded signing tools:
CI setup note: before this lands, the Windows queue needs WINDOWS_EXPECTED_SIGNER_SUBJECT configured to a stable substring of the expected WordPress/Automattic signing certificate subject. The SHA256 env vars are optional but preferable if the downloaded tool versions are meant to be pinned exactly.