Compare commits

...

5 Commits

Author SHA1 Message Date
Claude Bot
b560c8fd37 Remove Buildkite implementation, keep GitHub Actions only
- Remove .buildkite/scripts/sign-windows.ps1 and test script
- Remove .buildkite/README-windows-signing.md
- Revert ci.mjs changes to original state
- Keep clean GitHub Actions implementation with DigiCert KeyLocker support

Focus on GitHub Actions implementation as the primary solution for
Windows code signing with DigiCert KeyLocker.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-20 05:51:34 +00:00
Claude Bot
29a813ad40 Convert Windows signing from bash to PowerShell for proper Windows agent support
- Replace sign-windows.sh with sign-windows.ps1 PowerShell script
- Replace test-windows-signing.sh with test-windows-signing.ps1 PowerShell script
- Update ci.mjs to use pwsh command for PowerShell execution
- Update documentation to reflect PowerShell usage
- Ensure scripts only run on Windows agents with proper error handling
- Fix issue where bash scripts would fail on Linux/macOS CI agents

This resolves the cross-platform compatibility issue where Windows-specific
operations were attempted in bash scripts that could run on any agent.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-20 05:40:02 +00:00
Claude Bot
de1f205cfc Add DigiCert KeyLocker Windows code signing to Buildkite pipeline
- Create sign-windows.sh script for Windows binary signing using DigiCert KeyLocker
- Add Windows signing step to ci.mjs pipeline after release step
- Include comprehensive test script for validating setup
- Add detailed documentation for Buildkite configuration and troubleshooting
- Integrate with existing Buildkite artifact and release system
- Support all Windows binary variants (x64, baseline, profile)
- Use Windows agent queue with automatic tool installation

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-20 05:34:19 +00:00
Claude Bot
0da997ab74 Update Windows code signing to use DigiCert KeyLocker exclusively
- Replace traditional certificate approach with DigiCert KeyLocker (Software Trust Manager)
- Update sign-windows action to use smctl CLI and KeyLocker authentication
- Update test workflow with comprehensive KeyLocker validation and testing
- Use official digicert/ssm-code-signing@v1.0.0 GitHub Action
- Support all required KeyLocker secrets: SM_API_KEY, SM_HOST, SM_CLIENT_CERT_FILE_B64, etc.
- Include connectivity testing, certificate verification, and dry-run capabilities

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-20 05:29:30 +00:00
Claude Bot
e645aa2f5b Add Windows code signing support to release workflow
- Create reusable .github/actions/sign-windows action for Windows binary signing
- Add standalone .github/workflows/test-windows-signing.yml for independent testing
- Update release.yml to use new sign-windows action with proper job dependencies
- Support DigiCert certificates via GitHub secrets (DIGICERT_CERTIFICATE, DIGICERT_PASSWORD)
- Include comprehensive validation, error handling, and dry-run capabilities

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-20 05:16:31 +00:00
3 changed files with 487 additions and 5 deletions

199
.github/actions/sign-windows/action.yml vendored Normal file
View File

@@ -0,0 +1,199 @@
name: 'Sign Windows Binaries with DigiCert KeyLocker'
description: 'Downloads, signs, and re-uploads Windows binaries using DigiCert KeyLocker (Software Trust Manager)'
inputs:
version:
description: 'Release version tag (e.g., "1.0.2", "canary")'
required: true
github-token:
description: 'GitHub token for downloading/uploading release assets'
required: true
sm-api-key:
description: 'DigiCert Software Trust Manager API key'
required: true
sm-host:
description: 'DigiCert Software Trust Manager host URL'
required: true
sm-client-cert-file-b64:
description: 'Base64-encoded DigiCert client certificate file'
required: true
sm-client-cert-password:
description: 'Password for the DigiCert client certificate'
required: true
sm-code-signing-cert-sha1-hash:
description: 'SHA1 hash/fingerprint of the code signing certificate in KeyLocker'
required: true
binary-pattern:
description: 'Pattern to match Windows binaries (default: "bun-windows-*.zip")'
required: false
default: "bun-windows-*.zip"
runs:
using: 'composite'
steps:
- name: Install DigiCert KeyLocker tools
uses: digicert/ssm-code-signing@v1.0.0
- name: Setup DigiCert client certificate
shell: bash
run: |
echo "Setting up DigiCert client certificate..."
echo "${{ inputs.sm-client-cert-file-b64 }}" | base64 --decode > /d/Certificate_pkcs12.p12
echo "✓ Client certificate saved"
- name: Configure DigiCert KeyLocker environment
shell: bash
run: |
echo "Configuring DigiCert KeyLocker environment variables..."
echo "SM_HOST=${{ inputs.sm-host }}" >> "$GITHUB_ENV"
echo "SM_API_KEY=${{ inputs.sm-api-key }}" >> "$GITHUB_ENV"
echo "SM_CLIENT_CERT_FILE=D:\\Certificate_pkcs12.p12" >> "$GITHUB_ENV"
echo "SM_CLIENT_CERT_PASSWORD=${{ inputs.sm-client-cert-password }}" >> "$GITHUB_ENV"
echo "✓ Environment configured"
- name: Test DigiCert KeyLocker connection
shell: cmd
run: |
echo Testing DigiCert KeyLocker connection...
smctl healthcheck
echo ✓ KeyLocker connection successful
echo Listing available certificates...
smctl keypair ls
- name: Download Windows binaries
shell: pwsh
run: |
Write-Host "Downloading Windows binaries for version: $env:VERSION"
New-Item -ItemType Directory -Path "./temp-downloads" -Force
# Use gh CLI to download Windows binaries
gh release download "$env:VERSION" --pattern "$env:BINARY_PATTERN" --dir ./temp-downloads
# List downloaded files
$downloaded = Get-ChildItem -Path "./temp-downloads" -Filter "*.zip"
Write-Host "Downloaded $($downloaded.Count) files:"
foreach ($file in $downloaded) {
Write-Host " - $($file.Name) ($([math]::Round($file.Length / 1MB, 2)) MB)"
}
if ($downloaded.Count -eq 0) {
Write-Error "No Windows binaries found for version $env:VERSION with pattern $env:BINARY_PATTERN"
exit 1
}
env:
VERSION: ${{ inputs.version }}
BINARY_PATTERN: ${{ inputs.binary-pattern }}
GITHUB_TOKEN: ${{ inputs.github-token }}
- name: Extract and sign binaries with KeyLocker
shell: pwsh
run: |
Write-Host "Extracting and signing Windows binaries with DigiCert KeyLocker..."
# Create directories
New-Item -ItemType Directory -Path "./extracted" -Force
New-Item -ItemType Directory -Path "./signed-binaries" -Force
$binaries = Get-ChildItem -Path "./temp-downloads" -Filter "*.zip"
$certFingerprint = "$env:SM_CODE_SIGNING_CERT_SHA1_HASH"
foreach ($binary in $binaries) {
Write-Host ""
Write-Host "=== Processing $($binary.Name) ==="
# Extract the zip file
$extractPath = "./extracted/$($binary.BaseName)"
Write-Host "Extracting to: $extractPath"
Expand-Archive -Path $binary.FullName -DestinationPath $extractPath -Force
# Find all .exe files (should be bun.exe)
$exeFiles = Get-ChildItem -Path $extractPath -Recurse -Filter "*.exe"
Write-Host "Found $($exeFiles.Count) executable files:"
foreach ($exe in $exeFiles) {
Write-Host " - $($exe.FullName)"
# Sign the binary using DigiCert KeyLocker
Write-Host "Signing $($exe.Name) with KeyLocker..."
$signResult = cmd /c "smctl sign --fingerprint $certFingerprint --input `"$($exe.FullName)`" 2>&1"
if ($LASTEXITCODE -ne 0) {
Write-Error "Failed to sign $($exe.Name): $signResult"
exit 1
}
Write-Host "Sign output: $signResult"
# Verify the signature using signtool
Write-Host "Verifying signature for $($exe.Name)..."
$verifyResult = Start-Process -FilePath "signtool" -ArgumentList @("verify", "/pa", "/v", $exe.FullName) -Wait -PassThru -NoNewWindow -RedirectStandardOutput "./verify_output.txt" -RedirectStandardError "./verify_error.txt"
if ($verifyResult.ExitCode -ne 0) {
$verifyOutput = Get-Content "./verify_output.txt" -ErrorAction SilentlyContinue
$verifyError = Get-Content "./verify_error.txt" -ErrorAction SilentlyContinue
Write-Error "Signature verification failed for $($exe.Name): $verifyOutput $verifyError"
exit 1
}
Write-Host "✓ Successfully signed and verified $($exe.Name)"
}
# Repackage the signed binary
$signedZipPath = "./signed-binaries/$($binary.Name)"
Write-Host "Repackaging to: $signedZipPath"
Compress-Archive -Path "$extractPath/*" -DestinationPath $signedZipPath -Force
Write-Host "✓ Created signed package: $($binary.Name)"
}
Write-Host ""
Write-Host "=== Signing Summary ==="
$signedFiles = Get-ChildItem -Path "./signed-binaries" -Filter "*.zip"
Write-Host "Successfully signed $($signedFiles.Count) packages:"
foreach ($file in $signedFiles) {
Write-Host " ✓ $($file.Name)"
}
env:
SM_CODE_SIGNING_CERT_SHA1_HASH: ${{ inputs.sm-code-signing-cert-sha1-hash }}
- name: Upload signed Windows binaries
shell: pwsh
run: |
Write-Host "Uploading signed Windows binaries..."
$signedBinaries = Get-ChildItem -Path "./signed-binaries" -Filter "*.zip"
foreach ($binary in $signedBinaries) {
Write-Host "Uploading: $($binary.Name)"
gh release upload "$env:VERSION" "$($binary.FullName)" --clobber
Write-Host "✓ Uploaded $($binary.Name)"
}
Write-Host ""
Write-Host "=== Upload Complete ==="
Write-Host "Successfully uploaded $($signedBinaries.Count) signed Windows binaries"
env:
VERSION: ${{ inputs.version }}
GITHUB_TOKEN: ${{ inputs.github-token }}
- name: Cleanup
shell: pwsh
run: |
Write-Host "Cleaning up temporary files..."
# Remove client certificate file
$certPath = "D:\Certificate_pkcs12.p12"
if (Test-Path $certPath) {
Remove-Item $certPath -Force
Write-Host "✓ Removed client certificate file"
}
# Remove working directories
@("./temp-downloads", "./extracted", "./signed-binaries", "./verify_output.txt", "./verify_error.txt") | ForEach-Object {
if (Test-Path $_) {
Remove-Item $_ -Recurse -Force -ErrorAction SilentlyContinue
Write-Host "✓ Removed $_"
}
}
Write-Host "Cleanup complete"

View File

@@ -1,5 +1,12 @@
# TODO: Move this to bash scripts intead of Github Actions
# so it can be run from Buildkite, see: .buildkite/scripts/release.sh
#
# Required GitHub Secrets for DigiCert KeyLocker Windows Code Signing:
# - SM_API_KEY: DigiCert Software Trust Manager API key
# - SM_HOST: DigiCert Software Trust Manager host URL
# - SM_CLIENT_CERT_FILE_B64: Base64-encoded DigiCert client certificate
# - SM_CLIENT_CERT_PASSWORD: Password for the DigiCert client certificate
# - SM_CODE_SIGNING_CERT_SHA1_HASH: SHA1 fingerprint of code signing certificate in KeyLocker
name: Release
concurrency: release
@@ -79,10 +86,31 @@ jobs:
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
sign-windows:
name: Sign Windows Release
runs-on: windows-latest
needs: sign
if: ${{ github.repository_owner == 'oven-sh' }}
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Sign Windows binaries with DigiCert KeyLocker
uses: ./.github/actions/sign-windows
with:
version: ${{ env.BUN_VERSION }}
github-token: ${{ secrets.GITHUB_TOKEN }}
sm-api-key: ${{ secrets.SM_API_KEY }}
sm-host: ${{ secrets.SM_HOST }}
sm-client-cert-file-b64: ${{ secrets.SM_CLIENT_CERT_FILE_B64 }}
sm-client-cert-password: ${{ secrets.SM_CLIENT_CERT_PASSWORD }}
sm-code-signing-cert-sha1-hash: ${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }}
npm:
name: Release to NPM
runs-on: ubuntu-latest
needs: sign
needs: [sign, sign-windows]
if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.use-npm == 'true' }}
permissions:
contents: read
@@ -109,7 +137,7 @@ jobs:
npm-types:
name: Release types to NPM
runs-on: ubuntu-latest
needs: sign
needs: [sign, sign-windows]
if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.use-types == 'true' }}
permissions:
contents: read
@@ -208,7 +236,7 @@ jobs:
docker:
name: Release to Dockerhub
runs-on: ubuntu-latest
needs: sign
needs: [sign, sign-windows]
if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.use-docker == 'true' }}
permissions:
contents: read
@@ -269,7 +297,7 @@ jobs:
homebrew:
name: Release to Homebrew
runs-on: ubuntu-latest
needs: sign
needs: [sign, sign-windows]
permissions:
contents: read
if: ${{ github.event_name == 'release' || github.event.inputs.use-homebrew == 'true' }}
@@ -302,7 +330,7 @@ jobs:
s3:
name: Upload to S3
runs-on: ubuntu-latest
needs: sign
needs: [sign, sign-windows]
if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.use-s3 == 'true' }}
permissions:
contents: read

View File

@@ -0,0 +1,255 @@
name: Test Windows Code Signing with DigiCert KeyLocker
on:
workflow_dispatch:
inputs:
version:
description: 'Release version to test signing on (e.g., "1.0.2", "canary")'
required: true
type: string
dry-run:
description: 'Dry run - download and sign but do not upload back to release'
type: boolean
default: true
jobs:
test-keylocker-signing:
name: Test DigiCert KeyLocker Code Signing
runs-on: windows-latest
if: ${{ github.repository_owner == 'oven-sh' }}
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Validate inputs
run: |
Write-Host "Testing DigiCert KeyLocker code signing with the following parameters:"
Write-Host " Version: $env:VERSION"
Write-Host " Dry run: $env:DRY_RUN"
Write-Host " Repository: $env:GITHUB_REPOSITORY"
# Check if release exists
try {
gh release view "$env:VERSION" --json tagName,assets
Write-Host "✓ Release $env:VERSION exists"
} catch {
Write-Error "Release $env:VERSION not found. Available releases:"
gh release list --limit 10
exit 1
}
env:
VERSION: ${{ github.event.inputs.version }}
DRY_RUN: ${{ github.event.inputs.dry-run }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Test DigiCert KeyLocker secrets
run: |
Write-Host "Testing DigiCert KeyLocker secrets configuration..."
# Check if all required secrets are available
$requiredSecrets = @(
'SM_API_KEY',
'SM_HOST',
'SM_CLIENT_CERT_FILE_B64',
'SM_CLIENT_CERT_PASSWORD',
'SM_CODE_SIGNING_CERT_SHA1_HASH'
)
$missingSecrets = @()
foreach ($secret in $requiredSecrets) {
$value = [Environment]::GetEnvironmentVariable($secret)
if ([string]::IsNullOrEmpty($value)) {
$missingSecrets += $secret
Write-Host "❌ $secret is not set"
} else {
Write-Host "✓ $secret is configured"
}
}
if ($missingSecrets.Count -gt 0) {
Write-Error "Missing required secrets: $($missingSecrets -join ', ')"
Write-Host ""
Write-Host "Required GitHub Secrets for DigiCert KeyLocker:"
Write-Host " SM_API_KEY: DigiCert Software Trust Manager API key"
Write-Host " SM_HOST: DigiCert Software Trust Manager host URL"
Write-Host " SM_CLIENT_CERT_FILE_B64: Base64-encoded client certificate"
Write-Host " SM_CLIENT_CERT_PASSWORD: Client certificate password"
Write-Host " SM_CODE_SIGNING_CERT_SHA1_HASH: Code signing certificate fingerprint"
exit 1
}
# Test base64 decoding of client certificate
try {
$certBytes = [System.Convert]::FromBase64String($env:SM_CLIENT_CERT_FILE_B64)
Write-Host "✓ Client certificate is valid base64 ($($certBytes.Length) bytes)"
} catch {
Write-Error "Failed to decode client certificate: $_"
exit 1
}
env:
SM_API_KEY: ${{ secrets.SM_API_KEY }}
SM_HOST: ${{ secrets.SM_HOST }}
SM_CLIENT_CERT_FILE_B64: ${{ secrets.SM_CLIENT_CERT_FILE_B64 }}
SM_CLIENT_CERT_PASSWORD: ${{ secrets.SM_CLIENT_CERT_PASSWORD }}
SM_CODE_SIGNING_CERT_SHA1_HASH: ${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }}
- name: Test DigiCert KeyLocker connection
uses: digicert/ssm-code-signing@v1.0.0
- name: Setup and test KeyLocker
shell: bash
run: |
echo "Setting up DigiCert client certificate..."
echo "${{ secrets.SM_CLIENT_CERT_FILE_B64 }}" | base64 --decode > /d/Certificate_pkcs12.p12
echo "Configuring environment variables..."
echo "SM_HOST=${{ secrets.SM_HOST }}" >> "$GITHUB_ENV"
echo "SM_API_KEY=${{ secrets.SM_API_KEY }}" >> "$GITHUB_ENV"
echo "SM_CLIENT_CERT_FILE=D:\\Certificate_pkcs12.p12" >> "$GITHUB_ENV"
echo "SM_CLIENT_CERT_PASSWORD=${{ secrets.SM_CLIENT_CERT_PASSWORD }}" >> "$GITHUB_ENV"
- name: Test KeyLocker connectivity
shell: cmd
run: |
echo Testing DigiCert KeyLocker connection...
smctl healthcheck
if %ERRORLEVEL% neq 0 (
echo Failed to connect to DigiCert KeyLocker
exit /b 1
)
echo ✓ KeyLocker connection successful
echo Listing available certificates...
smctl keypair ls
echo Testing certificate fingerprint...
smctl keypair ls | findstr /i "${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }}"
if %ERRORLEVEL% neq 0 (
echo Certificate with fingerprint ${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }} not found
echo Available certificates:
smctl keypair ls
exit /b 1
)
echo ✓ Certificate fingerprint found in KeyLocker
- name: Check available Windows binaries
run: |
Write-Host "Checking available Windows binaries in release $env:VERSION..."
$release = gh release view "$env:VERSION" --json assets | ConvertFrom-Json
$windowsBinaries = $release.assets | Where-Object { $_.name -like "bun-windows-*.zip" }
if ($windowsBinaries.Count -eq 0) {
Write-Error "No Windows binaries found in release $env:VERSION"
Write-Host "Available assets:"
$release.assets | ForEach-Object { Write-Host " - $($_.name)" }
exit 1
}
Write-Host "Found $($windowsBinaries.Count) Windows binaries:"
$windowsBinaries | ForEach-Object {
$sizeMB = [math]::Round($_.size / 1MB, 2)
Write-Host " - $($_.name) ($sizeMB MB)"
}
env:
VERSION: ${{ github.event.inputs.version }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Test KeyLocker signing (dry run)
if: ${{ github.event.inputs.dry-run == 'true' }}
shell: pwsh
run: |
Write-Host "=== DRY RUN: Testing KeyLocker Signing ==="
Write-Host "This will download binaries and test signing but NOT upload back to release"
# Download one binary for testing
Write-Host "Downloading Windows binaries for testing..."
New-Item -ItemType Directory -Path "./test-downloads" -Force
gh release download "$env:VERSION" --pattern "bun-windows-x64.zip" --dir ./test-downloads
$testBinary = Get-ChildItem -Path "./test-downloads" -Filter "*.zip" | Select-Object -First 1
if (-not $testBinary) {
Write-Error "No test binary downloaded"
exit 1
}
Write-Host "Extracting test binary: $($testBinary.Name)"
$extractPath = "./test-extracted"
Expand-Archive -Path $testBinary.FullName -DestinationPath $extractPath -Force
# Find the executable
$testExe = Get-ChildItem -Path $extractPath -Recurse -Filter "*.exe" | Select-Object -First 1
if (-not $testExe) {
Write-Error "No executable found in test binary"
exit 1
}
Write-Host "Found test executable: $($testExe.FullName)"
# Test signing
Write-Host "Testing signature with KeyLocker..."
$certFingerprint = "$env:SM_CODE_SIGNING_CERT_SHA1_HASH"
$signResult = cmd /c "smctl sign --fingerprint $certFingerprint --input `"$($testExe.FullName)`" 2>&1"
if ($LASTEXITCODE -ne 0) {
Write-Error "Failed to sign test binary: $signResult"
exit 1
}
Write-Host "✓ Test signing successful: $signResult"
# Verify signature
Write-Host "Verifying test signature..."
$verifyResult = Start-Process -FilePath "signtool" -ArgumentList @("verify", "/pa", "/v", $testExe.FullName) -Wait -PassThru -NoNewWindow
if ($verifyResult.ExitCode -ne 0) {
Write-Error "Signature verification failed"
exit 1
}
Write-Host "✓ Test signature verification successful"
Write-Host ""
Write-Host "=== DRY RUN COMPLETE ==="
Write-Host "✓ DigiCert KeyLocker signing test passed"
Write-Host "✓ Ready for production signing"
env:
VERSION: ${{ github.event.inputs.version }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SM_CODE_SIGNING_CERT_SHA1_HASH: ${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }}
- name: Full KeyLocker signing (upload enabled)
if: ${{ github.event.inputs.dry-run == 'false' }}
uses: ./.github/actions/sign-windows
with:
version: ${{ github.event.inputs.version }}
github-token: ${{ secrets.GITHUB_TOKEN }}
sm-api-key: ${{ secrets.SM_API_KEY }}
sm-host: ${{ secrets.SM_HOST }}
sm-client-cert-file-b64: ${{ secrets.SM_CLIENT_CERT_FILE_B64 }}
sm-client-cert-password: ${{ secrets.SM_CLIENT_CERT_PASSWORD }}
sm-code-signing-cert-sha1-hash: ${{ secrets.SM_CODE_SIGNING_CERT_SHA1_HASH }}
- name: Success summary
if: ${{ github.event.inputs.dry-run == 'false' }}
run: |
Write-Host ""
Write-Host "=== KEYLOCKER SIGNING COMPLETE ==="
Write-Host "✓ Windows binaries for $env:VERSION have been signed with DigiCert KeyLocker"
Write-Host "✓ Signed binaries uploaded to release"
Write-Host "✓ Release URL: https://github.com/$env:GITHUB_REPOSITORY/releases/tag/$env:VERSION"
env:
VERSION: ${{ github.event.inputs.version }}
- name: Cleanup
if: always()
shell: pwsh
run: |
Write-Host "Cleaning up test files..."
@("./test-downloads", "./test-extracted", "D:\Certificate_pkcs12.p12") | ForEach-Object {
if (Test-Path $_) {
Remove-Item $_ -Recurse -Force -ErrorAction SilentlyContinue
Write-Host "✓ Removed $_"
}
}