Skip to content
Open
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
62 changes: 62 additions & 0 deletions .buildkite/commands/setup_windows_code_signing.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,68 @@
# Windows AMI as of a8c-ci-toolkit 6.0.0, so there is no separate host-preparation step.

$ErrorActionPreference = "Stop"
Set-StrictMode -Version Latest

& "setup_azure_trusted_signing.ps1"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

function Assert-SigningToolIntegrity {
param (
[Parameter(Mandatory = $true)]
[string]$Path,
[Parameter(Mandatory = $true)]
[string]$Description,
[Parameter(Mandatory = $true)]
[string]$ExpectedSha256EnvVar
)

if ([string]::IsNullOrWhiteSpace($Path)) {
Write-Host "[!] $Description path is not set."
exit 1
}

if (-not (Test-Path -LiteralPath $Path)) {
Write-Host "[!] $Description was not found at $Path."
exit 1
}

$expectedSha256 = [Environment]::GetEnvironmentVariable($ExpectedSha256EnvVar)
if (-not [string]::IsNullOrWhiteSpace($expectedSha256)) {
$actualSha256 = (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash
if ($actualSha256 -ne $expectedSha256.Trim().ToUpperInvariant()) {
Write-Host "[!] $Description SHA256 mismatch."
Write-Host "Expected: $($expectedSha256.Trim().ToUpperInvariant())"
Write-Host "Actual: $actualSha256"
exit 1
}

Write-Host "$Description SHA256 matched $ExpectedSha256EnvVar."
return
}

$signature = Get-AuthenticodeSignature -FilePath $Path
if ($signature.Status -ne "Valid" -or $null -eq $signature.SignerCertificate) {
Write-Host "[!] $Description does not have a valid Authenticode signature."
Write-Host "Status: $($signature.Status)"
exit 1
}

$subject = $signature.SignerCertificate.Subject
if ($subject.IndexOf("Microsoft", [System.StringComparison]::OrdinalIgnoreCase) -lt 0) {
Write-Host "[!] $Description is not signed by an expected Microsoft certificate."
Write-Host "Actual signer subject: $subject"
exit 1
}

Write-Host "$Description Authenticode signature is valid: $subject"
}

Assert-SigningToolIntegrity `
-Path $env:SIGNTOOL_PATH `
-Description "signtool.exe" `
-ExpectedSha256EnvVar "WINDOWS_SIGNTOOL_SHA256"

Assert-SigningToolIntegrity `
-Path $env:AZURE_CODE_SIGNING_DLIB `
-Description "Azure Trusted Signing DLib" `
-ExpectedSha256EnvVar "AZURE_CODE_SIGNING_DLIB_SHA256"
51 changes: 51 additions & 0 deletions .buildkite/commands/verify_windows_signature.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
$ErrorActionPreference = "Stop"
Set-StrictMode -Version Latest

$artifacts = @(Get-ChildItem -Path "dist\*.exe" -File -ErrorAction SilentlyContinue)
if ($artifacts.Count -eq 0) {
Write-Host "[!] No Windows .exe artifacts found in dist."
exit 1
}

if ([string]::IsNullOrWhiteSpace($env:SIGNTOOL_PATH)) {
Write-Host "[!] SIGNTOOL_PATH is not set."
exit 1
}

if ([string]::IsNullOrWhiteSpace($env:WINDOWS_EXPECTED_SIGNER_SUBJECT)) {
Write-Host "[!] WINDOWS_EXPECTED_SIGNER_SUBJECT must be set to verify the artifact signer identity."
exit 1
}

foreach ($artifact in $artifacts) {
Write-Host "Verifying Authenticode signature for $($artifact.FullName)"
& $env:SIGNTOOL_PATH verify /pa /v $artifact.FullName

@iangmaia iangmaia Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at this, I wonder if we should consider /tw so that "a warning should be generated if the signature is not time stamped"?

if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

$signature = Get-AuthenticodeSignature -FilePath $artifact.FullName
if ($signature.Status -ne "Valid" -or $null -eq $signature.SignerCertificate) {
Write-Host "[!] $($artifact.Name) does not have a valid Authenticode signature."
Write-Host "Status: $($signature.Status)"
exit 1
}

$actualSubject = $signature.SignerCertificate.Subject
if ($actualSubject.IndexOf($env:WINDOWS_EXPECTED_SIGNER_SUBJECT.Trim(), [System.StringComparison]::OrdinalIgnoreCase) -lt 0) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Write-Host "[!] $($artifact.Name) was not signed by the expected subject."
Write-Host "Expected subject to contain: $($env:WINDOWS_EXPECTED_SIGNER_SUBJECT.Trim())"
Write-Host "Actual signer subject: $actualSubject"
exit 1
}

if (-not [string]::IsNullOrWhiteSpace($env:WINDOWS_EXPECTED_SIGNER_ISSUER)) {
$actualIssuer = $signature.SignerCertificate.Issuer
if ($actualIssuer.IndexOf($env:WINDOWS_EXPECTED_SIGNER_ISSUER.Trim(), [System.StringComparison]::OrdinalIgnoreCase) -lt 0) {
Write-Host "[!] $($artifact.Name) was not signed by the expected issuer."
Write-Host "Expected issuer to contain: $($env:WINDOWS_EXPECTED_SIGNER_ISSUER.Trim())"
Write-Host "Actual signer issuer: $actualIssuer"
exit 1
}
}

Write-Host "$($artifact.Name) signer identity verified."
}
23 changes: 17 additions & 6 deletions .buildkite/pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,31 @@ steps:
agents: { queue: windows }
plugins: [$CI_TOOLKIT, $NVM_PLUGIN]
command: |
$ErrorActionPreference = "Stop"

@iangmaia iangmaia Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.


function Invoke-NativeCommand {
param (
[Parameter(Mandatory = $true)]
[string]$FilePath,
[Parameter(ValueFromRemainingArguments = $true)]
[string[]]$ArgumentList
)

& $FilePath @ArgumentList
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
}

echo "~~~ Setup code signing"
.buildkite/commands/setup_windows_code_signing.ps1

echo "~~~ Install dependencies"
bash .buildkite/commands/install_node_dependencies.sh
Invoke-NativeCommand bash .buildkite/commands/install_node_dependencies.sh

echo "~~~ Build Windows artifact"
npm run dist:win
Invoke-NativeCommand npm run dist:win

echo "~~~ Verify Azure Trusted Signing signature"
Get-ChildItem dist\*.exe | ForEach-Object {
& $env:SIGNTOOL_PATH verify /pa /v $_.FullName
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
}
.buildkite/commands/verify_windows_signature.ps1
artifact_paths:
- dist\*.exe
notify:
Expand Down
Loading