diff --git a/.buildkite/commands/setup_windows_code_signing.ps1 b/.buildkite/commands/setup_windows_code_signing.ps1 index 4fa8af1..9f9282a 100755 --- a/.buildkite/commands/setup_windows_code_signing.ps1 +++ b/.buildkite/commands/setup_windows_code_signing.ps1 @@ -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" 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" diff --git a/.buildkite/commands/verify_windows_signature.ps1 b/.buildkite/commands/verify_windows_signature.ps1 new file mode 100755 index 0000000..c5e8cf2 --- /dev/null +++ b/.buildkite/commands/verify_windows_signature.ps1 @@ -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 + 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) { + 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." +} diff --git a/.buildkite/pipeline.yml b/.buildkite/pipeline.yml index b366475..3b3c789 100644 --- a/.buildkite/pipeline.yml +++ b/.buildkite/pipeline.yml @@ -9,20 +9,31 @@ steps: agents: { queue: windows } plugins: [$CI_TOOLKIT, $NVM_PLUGIN] command: | + $ErrorActionPreference = "Stop" + + 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: