Compare commits

..

2 Commits

Author SHA1 Message Date
autofix-ci[bot]
e222414173 [autofix.ci] apply automated fixes 2026-02-26 19:57:27 +00:00
Claude Bot
8267c13a37 fix(docs): correct bcrypt rounds description from log10 to log2
The bcrypt cost parameter is a power-of-2 exponent (e.g., cost=10 means
2^10 = 1,024 rounds), not log10. This is confirmed by the implementation
in PasswordObject.zig which uses the cost as `rounds_log` with a valid
range of 4-31 (2^4 through 2^31).

Fixes #27474

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-26 19:55:17 +00:00
12 changed files with 603 additions and 567 deletions

View File

@@ -472,9 +472,12 @@ function getBuildCommand(target, options, label) {
const { profile } = target;
const buildProfile = profile || "release";
// Windows code signing is handled by a dedicated 'windows-sign' step after
// all Windows builds complete — see getWindowsSignStep(). smctl is x64-only,
// so signing on the build agent wouldn't work for ARM64 anyway.
if (target.os === "windows" && label === "build-bun") {
// Only sign release builds, not canary builds (DigiCert charges per signature)
// Skip signing on ARM64 for now — smctl (x64-only) silently fails under emulation
const enableSigning = !options.canary && target.arch !== "aarch64" ? " -DENABLE_WINDOWS_CODESIGNING=ON" : "";
return `bun run build:${buildProfile}${enableSigning}`;
}
return `bun run build:${buildProfile}`;
}
@@ -779,74 +782,24 @@ function getBuildImageStep(platform, options) {
};
}
/**
* Batch-signs all Windows artifacts on an x64 agent. DigiCert smctl is x64-only
* and silently fails under ARM64 emulation, so signing must happen here instead
* of inline during each build. Re-uploads signed zips with the same names so
* the release step picks them up transparently.
* @param {Platform[]} windowsPlatforms
* @param {PipelineOptions} options
* @returns {Step}
*/
function getWindowsSignStep(windowsPlatforms, options) {
// Each build-bun step produces two zips: <triplet>-profile.zip and <triplet>.zip
const artifacts = [];
const buildSteps = [];
for (const platform of windowsPlatforms) {
const triplet = getTargetTriplet(platform);
const stepKey = `${getTargetKey(platform)}-build-bun`;
artifacts.push(`${triplet}-profile.zip`, `${triplet}.zip`);
buildSteps.push(stepKey, stepKey);
}
// Run on an x64 build agent — smctl doesn't work on ARM64
const signPlatform = windowsPlatforms.find(p => p.arch === "x64" && !p.baseline) ?? windowsPlatforms[0];
return {
key: "windows-sign",
label: `${getBuildkiteEmoji("windows")} sign`,
depends_on: windowsPlatforms.map(p => `${getTargetKey(p)}-build-bun`),
agents: getEc2Agent(signPlatform, options, {
instanceType: getAzureVmSize("windows", "x64", "test"),
}),
retry: getRetry(),
cancel_on_build_failing: isMergeQueue(),
command: [
`powershell -NoProfile -ExecutionPolicy Bypass -File .buildkite/scripts/sign-windows-artifacts.ps1 ` +
`-Artifacts ${artifacts.join(",")} ` +
`-BuildSteps ${buildSteps.join(",")}`,
],
};
}
/**
* @param {Platform[]} buildPlatforms
* @param {PipelineOptions} options
* @param {{ signed: boolean }} [extra]
* @returns {Step}
*/
function getReleaseStep(buildPlatforms, options, { signed = false } = {}) {
function getReleaseStep(buildPlatforms, options) {
const { canary } = options;
const revision = typeof canary === "number" ? canary : 1;
// When signing ran, depend on windows-sign instead of the raw Windows builds
// so we wait for signed artifacts before releasing.
const depends_on = signed
? [...buildPlatforms.filter(p => p.os !== "windows").map(p => `${getTargetKey(p)}-build-bun`), "windows-sign"]
: buildPlatforms.map(platform => `${getTargetKey(platform)}-build-bun`);
return {
key: "release",
label: getBuildkiteEmoji("rocket"),
agents: {
queue: "test-darwin",
},
depends_on,
depends_on: buildPlatforms.map(platform => `${getTargetKey(platform)}-build-bun`),
env: {
CANARY: revision,
// Tells upload-release.sh to fetch Windows zips from the sign step
// (same filenames, but the signed re-uploads are the ones we want).
WINDOWS_ARTIFACT_STEP: signed ? "windows-sign" : "",
},
command: ".buildkite/scripts/upload-release.sh",
};
@@ -952,7 +905,6 @@ function getBenchmarkStep() {
* @property {string | boolean} [forceBuilds]
* @property {string | boolean} [forceTests]
* @property {string | boolean} [buildImages]
* @property {string | boolean} [signWindows]
* @property {string | boolean} [publishImages]
* @property {number} [canary]
* @property {Platform[]} [buildPlatforms]
@@ -1228,7 +1180,6 @@ async function getPipelineOptions() {
skipBuilds: parseOption(/\[(skip builds?|no builds?|only tests?)\]/i),
forceBuilds: parseOption(/\[(force builds?)\]/i),
skipTests: parseOption(/\[(skip tests?|no tests?|only builds?)\]/i),
signWindows: parseOption(/\[(sign windows)\]/i),
buildImages: parseOption(/\[(build (?:(?:windows|linux) )?images?)\]/i),
dryRun: parseOption(/\[(dry run)\]/i),
publishImages: parseOption(/\[(publish (?:(?:windows|linux) )?images?)\]/i),
@@ -1343,19 +1294,8 @@ async function getPipeline(options = {}) {
}
}
// Sign Windows builds on release (non-canary main) or when [sign windows]
// is in the commit message (for testing the sign step on a branch).
// DigiCert charges per signature, so canary builds are never signed.
const shouldSignWindows = (isMainBranch() && !options.canary) || options.signWindows;
if (shouldSignWindows) {
const windowsPlatforms = buildPlatforms.filter(p => p.os === "windows");
if (windowsPlatforms.length > 0) {
steps.push(getWindowsSignStep(windowsPlatforms, options));
}
}
if (isMainBranch()) {
steps.push(getReleaseStep(buildPlatforms, options, { signed: shouldSignWindows }));
steps.push(getReleaseStep(buildPlatforms, options));
}
steps.push(getBenchmarkStep());

View File

@@ -1,281 +0,0 @@
# Batch Windows code signing for all bun-windows-*.zip Buildkite artifacts.
#
# This runs as a dedicated pipeline step on a Windows x64 agent after all
# Windows build-bun steps complete. Signing is done here instead of inline
# during each build because DigiCert smctl is x64-only and silently fails
# under ARM64 emulation.
#
# Each zip is downloaded, its exe signed in place, and the zip is re-packed
# with the same name so downstream steps (release, tests) see signed binaries.
param(
# Comma-separated list. powershell.exe -File passes everything as
# literal strings, so [string[]] with "a,b,c" becomes a 1-element array.
[Parameter(Mandatory=$true)]
[string]$Artifacts,
# Comma-separated, same length as Artifacts, mapping each zip to its source step.
[Parameter(Mandatory=$true)]
[string]$BuildSteps
)
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
$ArtifactList = $Artifacts -split ","
$BuildStepList = $BuildSteps -split ","
# smctl shells out to signtool.exe which is only in PATH when the VS dev
# environment is loaded. Dot-source the existing helper to set it up.
. $PSScriptRoot\..\..\scripts\vs-shell.ps1
function Log-Info {
param([string]$Message)
Write-Host "[INFO] $Message" -ForegroundColor Cyan
}
function Log-Success {
param([string]$Message)
Write-Host "[SUCCESS] $Message" -ForegroundColor Green
}
function Log-Error {
param([string]$Message)
Write-Host "[ERROR] $Message" -ForegroundColor Red
}
function Log-Debug {
param([string]$Message)
if ($env:DEBUG -eq "true" -or $env:DEBUG -eq "1") {
Write-Host "[DEBUG] $Message" -ForegroundColor Gray
}
}
function Get-BuildkiteSecret {
param([string]$Name)
$value = & buildkite-agent secret get $Name 2>&1
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrEmpty($value)) {
throw "Failed to fetch Buildkite secret: $Name"
}
return $value
}
function Ensure-Secrets {
Log-Info "Fetching signing secrets from Buildkite..."
$env:SM_API_KEY = Get-BuildkiteSecret "SM_API_KEY"
$env:SM_CLIENT_CERT_PASSWORD = Get-BuildkiteSecret "SM_CLIENT_CERT_PASSWORD"
$env:SM_CLIENT_CERT_FILE = Get-BuildkiteSecret "SM_CLIENT_CERT_FILE"
$env:SM_KEYPAIR_ALIAS = Get-BuildkiteSecret "SM_KEYPAIR_ALIAS"
$env:SM_HOST = Get-BuildkiteSecret "SM_HOST"
Log-Success "All signing secrets fetched"
}
function Setup-Certificate {
Log-Info "Decoding client certificate..."
try {
$tempCertPath = Join-Path $env:TEMP "digicert_cert_$(Get-Random).p12"
$certBytes = [System.Convert]::FromBase64String($env:SM_CLIENT_CERT_FILE)
[System.IO.File]::WriteAllBytes($tempCertPath, $certBytes)
$fileSize = (Get-Item $tempCertPath).Length
if ($fileSize -lt 100) {
throw "Decoded certificate too small: $fileSize bytes"
}
$env:SM_CLIENT_CERT_FILE = $tempCertPath
$script:TempCertPath = $tempCertPath
Log-Success "Certificate decoded ($fileSize bytes)"
} catch {
if (Test-Path $env:SM_CLIENT_CERT_FILE) {
Log-Info "Using certificate file path directly: $env:SM_CLIENT_CERT_FILE"
} else {
throw "SM_CLIENT_CERT_FILE is neither valid base64 nor an existing file"
}
}
}
function Install-KeyLocker {
Log-Info "Setting up DigiCert KeyLocker tools..."
$installDir = "C:\BuildTools\DigiCert"
$smctlPath = Join-Path $installDir "smctl.exe"
if (Test-Path $smctlPath) {
Log-Success "smctl already installed at $smctlPath"
$env:PATH = "$installDir;$env:PATH"
return $smctlPath
}
if (!(Test-Path $installDir)) {
New-Item -ItemType Directory -Path $installDir -Force | Out-Null
}
# smctl is x64-only; this script must run on an x64 agent
$msiUrl = "https://bun-ci-assets.bun.sh/Keylockertools-windows-x64.msi"
$msiPath = Join-Path $env:TEMP "Keylockertools-windows-x64.msi"
Log-Info "Downloading KeyLocker MSI from $msiUrl"
if (Test-Path $msiPath) { Remove-Item $msiPath -Force }
(New-Object System.Net.WebClient).DownloadFile($msiUrl, $msiPath)
if (!(Test-Path $msiPath)) { throw "MSI download failed" }
Log-Info "Installing KeyLocker MSI..."
$proc = Start-Process -FilePath "msiexec.exe" -Wait -PassThru -NoNewWindow -ArgumentList @(
"/i", "`"$msiPath`"",
"/quiet", "/norestart",
"TARGETDIR=`"$installDir`"",
"INSTALLDIR=`"$installDir`"",
"ACCEPT_EULA=1",
"ADDLOCAL=ALL"
)
if ($proc.ExitCode -ne 0) {
throw "MSI install failed with exit code $($proc.ExitCode)"
}
if (!(Test-Path $smctlPath)) {
$found = Get-ChildItem -Path $installDir -Filter "smctl.exe" -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
if ($found) {
$smctlPath = $found.FullName
$installDir = $found.DirectoryName
} else {
throw "smctl.exe not found after install"
}
}
$env:PATH = "$installDir;$env:PATH"
Log-Success "smctl installed at $smctlPath"
return $smctlPath
}
function Configure-KeyLocker {
param([string]$Smctl)
Log-Info "Configuring KeyLocker..."
$version = & $Smctl --version 2>&1
Log-Debug "smctl version: $version"
$saveOut = & $Smctl credentials save $env:SM_API_KEY $env:SM_CLIENT_CERT_PASSWORD 2>&1 | Out-String
Log-Debug "credentials save: $saveOut"
$healthOut = & $Smctl healthcheck 2>&1 | Out-String
Log-Debug "healthcheck: $healthOut"
if ($healthOut -notlike "*Healthy*" -and $healthOut -notlike "*SUCCESS*" -and $LASTEXITCODE -ne 0) {
Log-Error "healthcheck output: $healthOut"
# Don't throw — healthcheck is sometimes flaky but signing still works
}
$syncOut = & $Smctl windows certsync 2>&1 | Out-String
Log-Debug "certsync: $syncOut"
Log-Success "KeyLocker configured"
}
function Download-Artifact {
param([string]$Name, [string]$StepKey)
Log-Info "Downloading $Name from step $StepKey"
& buildkite-agent artifact download $Name . --step $StepKey
if ($LASTEXITCODE -ne 0 -or !(Test-Path $Name)) {
throw "Failed to download artifact: $Name"
}
Log-Success "Downloaded $Name ($((Get-Item $Name).Length) bytes)"
}
function Sign-Exe {
param([string]$ExePath, [string]$Smctl)
$fileName = Split-Path $ExePath -Leaf
Log-Info "Signing $fileName ($((Get-Item $ExePath).Length) bytes)..."
$existing = Get-AuthenticodeSignature $ExePath
if ($existing.Status -eq "Valid") {
Log-Info "$fileName already signed by $($existing.SignerCertificate.Subject), skipping"
return
}
$out = & $Smctl sign --keypair-alias $env:SM_KEYPAIR_ALIAS --input $ExePath --verbose 2>&1 | Out-String
Log-Info "smctl output: $out"
# smctl exits 0 even on failure — must also check output text
if ($LASTEXITCODE -ne 0 -or $out -like "*FAILED*" -or $out -like "*error*") {
throw "Signing failed for $fileName (exit $LASTEXITCODE): $out"
}
$sig = Get-AuthenticodeSignature $ExePath
if ($sig.Status -ne "Valid") {
throw "$fileName signature verification failed: $($sig.Status) - $($sig.StatusMessage)"
}
Log-Success "$fileName signed by $($sig.SignerCertificate.Subject)"
}
function Sign-Artifact {
param([string]$ZipName, [string]$Smctl)
Write-Host "================================================" -ForegroundColor Cyan
Write-Host " Signing $ZipName" -ForegroundColor Cyan
Write-Host "================================================" -ForegroundColor Cyan
$extractDir = [System.IO.Path]::GetFileNameWithoutExtension($ZipName)
if (Test-Path $extractDir) { Remove-Item $extractDir -Recurse -Force }
Log-Info "Extracting $ZipName"
Expand-Archive -Path $ZipName -DestinationPath . -Force
if (!(Test-Path $extractDir)) {
throw "Expected directory $extractDir not found after extraction"
}
$exes = Get-ChildItem -Path $extractDir -Filter "*.exe"
if ($exes.Count -eq 0) {
throw "No .exe files found in $extractDir"
}
foreach ($exe in $exes) {
Sign-Exe -ExePath $exe.FullName -Smctl $Smctl
}
Log-Info "Re-packing $ZipName"
Remove-Item $ZipName -Force
Compress-Archive -Path $extractDir -DestinationPath $ZipName -CompressionLevel Optimal
Remove-Item $extractDir -Recurse -Force
Log-Info "Uploading signed $ZipName"
& buildkite-agent artifact upload $ZipName
if ($LASTEXITCODE -ne 0) {
throw "Failed to upload $ZipName"
}
Log-Success "$ZipName signed and uploaded"
}
# Main
try {
Write-Host "================================================" -ForegroundColor Cyan
Write-Host " Windows Artifact Code Signing" -ForegroundColor Cyan
Write-Host "================================================" -ForegroundColor Cyan
if ($ArtifactList.Count -ne $BuildStepList.Count) {
throw "Artifact count ($($ArtifactList.Count)) must match BuildStep count ($($BuildStepList.Count))"
}
Log-Info "Will sign $($ArtifactList.Count) artifacts: $($ArtifactList -join ', ')"
Ensure-Secrets
Setup-Certificate
$smctl = Install-KeyLocker
Configure-KeyLocker -Smctl $smctl
for ($i = 0; $i -lt $ArtifactList.Count; $i++) {
Download-Artifact -Name $ArtifactList[$i] -StepKey $BuildStepList[$i]
Sign-Artifact -ZipName $ArtifactList[$i] -Smctl $smctl
}
Write-Host "================================================" -ForegroundColor Green
Write-Host " All artifacts signed successfully" -ForegroundColor Green
Write-Host "================================================" -ForegroundColor Green
exit 0
} catch {
Log-Error "Signing failed: $_"
exit 1
} finally {
if ($script:TempCertPath -and (Test-Path $script:TempCertPath)) {
Remove-Item $script:TempCertPath -Force -ErrorAction SilentlyContinue
}
}

View File

@@ -0,0 +1,470 @@
# Windows Code Signing Script for Bun
# Uses DigiCert KeyLocker for Authenticode signing
# Native PowerShell implementation - no path translation issues
param(
[Parameter(Mandatory=$true)]
[string]$BunProfileExe,
[Parameter(Mandatory=$true)]
[string]$BunExe
)
$ErrorActionPreference = "Stop"
$ProgressPreference = "SilentlyContinue"
# Logging functions
function Log-Info {
param([string]$Message)
Write-Host "[INFO] $Message" -ForegroundColor Cyan
}
function Log-Success {
param([string]$Message)
Write-Host "[SUCCESS] $Message" -ForegroundColor Green
}
function Log-Error {
param([string]$Message)
Write-Host "[ERROR] $Message" -ForegroundColor Red
}
function Log-Debug {
param([string]$Message)
if ($env:DEBUG -eq "true" -or $env:DEBUG -eq "1") {
Write-Host "[DEBUG] $Message" -ForegroundColor Gray
}
}
# Detect system architecture
$script:IsARM64 = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64
$script:VsArch = if ($script:IsARM64) { "arm64" } else { "amd64" }
# Load Visual Studio environment if not already loaded
function Ensure-VSEnvironment {
if ($null -eq $env:VSINSTALLDIR) {
Log-Info "Loading Visual Studio environment for $script:VsArch..."
$vswhere = "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe"
if (!(Test-Path $vswhere)) {
throw "Command not found: vswhere (did you install Visual Studio?)"
}
$vsDir = & $vswhere -prerelease -latest -property installationPath
if ($null -eq $vsDir) {
$vsDir = Get-ChildItem -Path "C:\Program Files\Microsoft Visual Studio\2022" -Directory -ErrorAction SilentlyContinue
if ($null -eq $vsDir) {
throw "Visual Studio directory not found."
}
$vsDir = $vsDir.FullName
}
Push-Location $vsDir
try {
$vsShell = Join-Path -Path $vsDir -ChildPath "Common7\Tools\Launch-VsDevShell.ps1"
. $vsShell -Arch $script:VsArch -HostArch $script:VsArch
} finally {
Pop-Location
}
Log-Success "Visual Studio environment loaded"
}
if ($env:VSCMD_ARG_TGT_ARCH -eq "x86") {
throw "Visual Studio environment is targeting 32 bit x86, but only 64-bit architectures (x64/arm64) are supported."
}
}
# Check for required environment variables
function Check-Environment {
Log-Info "Checking environment variables..."
$required = @{
"SM_API_KEY" = $env:SM_API_KEY
"SM_CLIENT_CERT_PASSWORD" = $env:SM_CLIENT_CERT_PASSWORD
"SM_KEYPAIR_ALIAS" = $env:SM_KEYPAIR_ALIAS
"SM_HOST" = $env:SM_HOST
"SM_CLIENT_CERT_FILE" = $env:SM_CLIENT_CERT_FILE
}
$missing = @()
foreach ($key in $required.Keys) {
if ([string]::IsNullOrEmpty($required[$key])) {
$missing += $key
} else {
Log-Debug "$key is set (length: $($required[$key].Length))"
}
}
if ($missing.Count -gt 0) {
throw "Missing required environment variables: $($missing -join ', ')"
}
Log-Success "All required environment variables are present"
}
# Setup certificate file
function Setup-Certificate {
Log-Info "Setting up certificate..."
# Always try to decode as base64 first
# If it fails, then treat as file path
try {
Log-Info "Attempting to decode certificate as base64..."
Log-Debug "Input string length: $($env:SM_CLIENT_CERT_FILE.Length) characters"
$tempCertPath = Join-Path $env:TEMP "digicert_cert_$(Get-Random).p12"
# Try to decode as base64
$certBytes = [System.Convert]::FromBase64String($env:SM_CLIENT_CERT_FILE)
[System.IO.File]::WriteAllBytes($tempCertPath, $certBytes)
# Validate the decoded certificate size
$fileSize = (Get-Item $tempCertPath).Length
if ($fileSize -lt 100) {
throw "Decoded certificate too small: $fileSize bytes (expected >100 bytes)"
}
# Update environment to point to file
$env:SM_CLIENT_CERT_FILE = $tempCertPath
Log-Success "Certificate decoded and written to: $tempCertPath"
Log-Debug "Decoded certificate file size: $fileSize bytes"
# Register cleanup
$global:TEMP_CERT_PATH = $tempCertPath
} catch {
# If base64 decode fails, check if it's a file path
Log-Info "Base64 decode failed, checking if it's a file path..."
Log-Debug "Decode error: $_"
if (Test-Path $env:SM_CLIENT_CERT_FILE) {
$fileSize = (Get-Item $env:SM_CLIENT_CERT_FILE).Length
# Validate file size
if ($fileSize -lt 100) {
throw "Certificate file too small: $fileSize bytes at $env:SM_CLIENT_CERT_FILE (possibly corrupted)"
}
Log-Info "Using certificate file: $env:SM_CLIENT_CERT_FILE"
Log-Debug "Certificate file size: $fileSize bytes"
} else {
throw "SM_CLIENT_CERT_FILE is neither valid base64 nor an existing file: $env:SM_CLIENT_CERT_FILE"
}
}
}
# Install DigiCert KeyLocker tools
function Install-KeyLocker {
Log-Info "Setting up DigiCert KeyLocker tools..."
# Define our controlled installation directory
$installDir = "C:\BuildTools\DigiCert"
$smctlPath = Join-Path $installDir "smctl.exe"
# Check if already installed in our controlled location
if (Test-Path $smctlPath) {
Log-Success "KeyLocker tools already installed at: $smctlPath"
# Add to PATH if not already there
if ($env:PATH -notlike "*$installDir*") {
$env:PATH = "$installDir;$env:PATH"
Log-Info "Added to PATH: $installDir"
}
return $smctlPath
}
Log-Info "Installing KeyLocker tools to: $installDir"
# Create the installation directory if it doesn't exist
if (!(Test-Path $installDir)) {
Log-Info "Creating installation directory: $installDir"
try {
New-Item -ItemType Directory -Path $installDir -Force | Out-Null
Log-Success "Created directory: $installDir"
} catch {
throw "Failed to create directory $installDir : $_"
}
}
# Download MSI installer
# Note: KeyLocker tools currently only available for x64, but works on ARM64 via emulation
$msiArch = "x64"
$msiUrl = "https://bun-ci-assets.bun.sh/Keylockertools-windows-${msiArch}.msi"
$msiPath = Join-Path $env:TEMP "Keylockertools-windows-${msiArch}.msi"
Log-Info "Downloading MSI from: $msiUrl"
Log-Info "Downloading to: $msiPath"
try {
# Remove existing MSI if present
if (Test-Path $msiPath) {
Remove-Item $msiPath -Force
Log-Debug "Removed existing MSI file"
}
# Download with progress tracking
$webClient = New-Object System.Net.WebClient
$webClient.DownloadFile($msiUrl, $msiPath)
if (!(Test-Path $msiPath)) {
throw "MSI download failed - file not found"
}
$fileSize = (Get-Item $msiPath).Length
Log-Success "MSI downloaded successfully (size: $fileSize bytes)"
} catch {
throw "Failed to download MSI: $_"
}
# Install MSI
Log-Info "Installing MSI..."
Log-Debug "MSI path: $msiPath"
Log-Debug "File exists: $(Test-Path $msiPath)"
Log-Debug "File size: $((Get-Item $msiPath).Length) bytes"
# Check if running as administrator
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
Log-Info "Running as administrator: $isAdmin"
# Install MSI silently to our controlled directory
$arguments = @(
"/i", "`"$msiPath`"",
"/quiet",
"/norestart",
"TARGETDIR=`"$installDir`"",
"INSTALLDIR=`"$installDir`"",
"ACCEPT_EULA=1",
"ADDLOCAL=ALL"
)
Log-Debug "Running: msiexec.exe $($arguments -join ' ')"
Log-Info "Installing to: $installDir"
$process = Start-Process -FilePath "msiexec.exe" -ArgumentList $arguments -Wait -PassThru -NoNewWindow
if ($process.ExitCode -ne 0) {
Log-Error "MSI installation failed with exit code: $($process.ExitCode)"
# Try to get error details from event log
try {
$events = Get-WinEvent -LogName "Application" -MaxEvents 10 |
Where-Object { $_.ProviderName -eq "MsiInstaller" -and $_.TimeCreated -gt (Get-Date).AddMinutes(-1) }
foreach ($event in $events) {
Log-Debug "MSI Event: $($event.Message)"
}
} catch {
Log-Debug "Could not retrieve MSI installation events"
}
throw "MSI installation failed with exit code: $($process.ExitCode)"
}
Log-Success "MSI installation completed"
# Wait for installation to complete
Start-Sleep -Seconds 2
# Verify smctl.exe exists in our controlled location
if (Test-Path $smctlPath) {
Log-Success "KeyLocker tools installed successfully at: $smctlPath"
# Add to PATH
$env:PATH = "$installDir;$env:PATH"
Log-Info "Added to PATH: $installDir"
return $smctlPath
}
# If not in our expected location, check if it installed somewhere in the directory
$found = Get-ChildItem -Path $installDir -Filter "smctl.exe" -Recurse -ErrorAction SilentlyContinue |
Select-Object -First 1
if ($found) {
Log-Success "Found smctl.exe at: $($found.FullName)"
$smctlDir = $found.DirectoryName
$env:PATH = "$smctlDir;$env:PATH"
return $found.FullName
}
throw "KeyLocker tools installation succeeded but smctl.exe not found in $installDir"
}
# Configure KeyLocker
function Configure-KeyLocker {
param([string]$SmctlPath)
Log-Info "Configuring KeyLocker..."
# Verify smctl is accessible
try {
$version = & $SmctlPath --version 2>&1
Log-Debug "smctl version: $version"
} catch {
throw "Failed to run smctl: $_"
}
# Configure KeyLocker credentials and environment
Log-Info "Configuring KeyLocker credentials..."
try {
# Save credentials (API key and password)
Log-Info "Saving credentials to OS store..."
$saveOutput = & $SmctlPath credentials save $env:SM_API_KEY $env:SM_CLIENT_CERT_PASSWORD 2>&1 | Out-String
Log-Debug "Credentials save output: $saveOutput"
if ($saveOutput -like "*Credentials saved*") {
Log-Success "Credentials saved successfully"
}
# Set environment variables for smctl
Log-Info "Setting KeyLocker environment variables..."
$env:SM_HOST = $env:SM_HOST # Already set, but ensure it's available
$env:SM_API_KEY = $env:SM_API_KEY # Already set
$env:SM_CLIENT_CERT_FILE = $env:SM_CLIENT_CERT_FILE # Path to decoded cert file
Log-Debug "SM_HOST: $env:SM_HOST"
Log-Debug "SM_CLIENT_CERT_FILE: $env:SM_CLIENT_CERT_FILE"
# Run health check
Log-Info "Running KeyLocker health check..."
$healthOutput = & $SmctlPath healthcheck 2>&1 | Out-String
Log-Debug "Health check output: $healthOutput"
if ($healthOutput -like "*Healthy*" -or $healthOutput -like "*SUCCESS*" -or $LASTEXITCODE -eq 0) {
Log-Success "KeyLocker health check passed"
} else {
Log-Error "Health check failed: $healthOutput"
# Don't throw here, sometimes healthcheck is flaky but signing still works
}
# Sync certificates to Windows certificate store
Log-Info "Syncing certificates to Windows store..."
$syncOutput = & $SmctlPath windows certsync 2>&1 | Out-String
Log-Debug "Certificate sync output: $syncOutput"
if ($syncOutput -like "*success*" -or $syncOutput -like "*synced*" -or $LASTEXITCODE -eq 0) {
Log-Success "Certificates synced to Windows store"
} else {
Log-Info "Certificate sync output: $syncOutput"
}
} catch {
throw "Failed to configure KeyLocker: $_"
}
}
# Sign an executable
function Sign-Executable {
param(
[string]$ExePath,
[string]$SmctlPath
)
if (!(Test-Path $ExePath)) {
throw "Executable not found: $ExePath"
}
$fileName = Split-Path $ExePath -Leaf
Log-Info "Signing $fileName..."
Log-Debug "Full path: $ExePath"
Log-Debug "File size: $((Get-Item $ExePath).Length) bytes"
# Check if already signed
$existingSig = Get-AuthenticodeSignature $ExePath
if ($existingSig.Status -eq "Valid") {
Log-Info "$fileName is already signed by: $($existingSig.SignerCertificate.Subject)"
Log-Info "Skipping re-signing"
return
}
# Sign the executable using smctl
try {
# smctl sign command with keypair-alias
$signArgs = @(
"sign",
"--keypair-alias", $env:SM_KEYPAIR_ALIAS,
"--input", $ExePath,
"--verbose"
)
Log-Debug "Running: $SmctlPath $($signArgs -join ' ')"
$signOutput = & $SmctlPath $signArgs 2>&1 | Out-String
if ($LASTEXITCODE -ne 0) {
Log-Error "Signing output: $signOutput"
throw "Signing failed with exit code: $LASTEXITCODE"
}
Log-Debug "Signing output: $signOutput"
Log-Success "Signing command completed"
} catch {
throw "Failed to sign $fileName : $_"
}
# Verify signature
$newSig = Get-AuthenticodeSignature $ExePath
if ($newSig.Status -eq "Valid") {
Log-Success "$fileName signed successfully"
Log-Info "Signed by: $($newSig.SignerCertificate.Subject)"
Log-Info "Thumbprint: $($newSig.SignerCertificate.Thumbprint)"
Log-Info "Valid from: $($newSig.SignerCertificate.NotBefore) to $($newSig.SignerCertificate.NotAfter)"
} else {
throw "$fileName signature verification failed: $($newSig.Status) - $($newSig.StatusMessage)"
}
}
# Cleanup function
function Cleanup {
if ($global:TEMP_CERT_PATH -and (Test-Path $global:TEMP_CERT_PATH)) {
try {
Remove-Item $global:TEMP_CERT_PATH -Force
Log-Info "Cleaned up temporary certificate"
} catch {
Log-Error "Failed to cleanup temporary certificate: $_"
}
}
}
# Main execution
try {
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " Windows Code Signing for Bun" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
# Ensure we're in a VS environment
Ensure-VSEnvironment
# Check environment variables
Check-Environment
# Setup certificate
Setup-Certificate
# Install and configure KeyLocker
$smctlPath = Install-KeyLocker
Configure-KeyLocker -SmctlPath $smctlPath
# Sign both executables
Sign-Executable -ExePath $BunProfileExe -SmctlPath $smctlPath
Sign-Executable -ExePath $BunExe -SmctlPath $smctlPath
Write-Host "========================================" -ForegroundColor Green
Write-Host " Code signing completed successfully!" -ForegroundColor Green
Write-Host "========================================" -ForegroundColor Green
exit 0
} catch {
Log-Error "Code signing failed: $_"
exit 1
} finally {
Cleanup
}

View File

@@ -121,14 +121,7 @@ function download_buildkite_artifact() {
if [ -z "$dir" ]; then
dir="."
fi
# When signing ran, Windows zips exist in two steps with the same name
# (build-bun unsigned, windows-sign signed). Pin to the sign step to
# guarantee we get the signed one.
local step_args=()
if [[ -n "$WINDOWS_ARTIFACT_STEP" && "$name" == bun-windows-* ]]; then
step_args=(--step "$WINDOWS_ARTIFACT_STEP")
fi
run_command buildkite-agent artifact download "$name" "$dir" "${step_args[@]}"
run_command buildkite-agent artifact download "$name" "$dir"
if [ ! -f "$dir/$name" ]; then
echo "error: Cannot find Buildkite artifact: $name"
exit 1

View File

@@ -58,6 +58,23 @@ else()
message(FATAL_ERROR "Unsupported architecture: ${CMAKE_SYSTEM_PROCESSOR}")
endif()
# Windows Code Signing Option
if(WIN32)
optionx(ENABLE_WINDOWS_CODESIGNING BOOL "Enable Windows code signing with DigiCert KeyLocker" DEFAULT OFF)
if(ENABLE_WINDOWS_CODESIGNING)
message(STATUS "Windows code signing: ENABLED")
# Check for required environment variables
if(NOT DEFINED ENV{SM_API_KEY})
message(WARNING "SM_API_KEY not set - code signing may fail")
endif()
if(NOT DEFINED ENV{SM_CLIENT_CERT_FILE})
message(WARNING "SM_CLIENT_CERT_FILE not set - code signing may fail")
endif()
endif()
endif()
if(LINUX)
if(EXISTS "/etc/alpine-release")
set(DEFAULT_ABI "musl")

View File

@@ -1409,8 +1409,47 @@ if(NOT BUN_CPP_ONLY)
${BUILD_PATH}/${bunStripExe}
)
# Windows code signing happens in a dedicated Buildkite step after all
# Windows builds complete. See .buildkite/scripts/sign-windows-artifacts.ps1
# Then sign both executables on Windows
if(WIN32 AND ENABLE_WINDOWS_CODESIGNING)
set(SIGN_SCRIPT "${CMAKE_SOURCE_DIR}/.buildkite/scripts/sign-windows.ps1")
# Verify signing script exists
if(NOT EXISTS "${SIGN_SCRIPT}")
message(FATAL_ERROR "Windows signing script not found: ${SIGN_SCRIPT}")
endif()
# Use PowerShell for Windows code signing (native Windows, no path issues)
find_program(POWERSHELL_EXECUTABLE
NAMES pwsh.exe powershell.exe
PATHS
"C:/Program Files/PowerShell/7"
"C:/Program Files (x86)/PowerShell/7"
"C:/Windows/System32/WindowsPowerShell/v1.0"
DOC "Path to PowerShell executable"
)
if(NOT POWERSHELL_EXECUTABLE)
set(POWERSHELL_EXECUTABLE "powershell.exe")
endif()
message(STATUS "Using PowerShell executable: ${POWERSHELL_EXECUTABLE}")
# Sign both bun-profile.exe and bun.exe after stripping
register_command(
TARGET
${bun}
TARGET_PHASE
POST_BUILD
COMMENT
"Code signing bun-profile.exe and bun.exe with DigiCert KeyLocker"
COMMAND
"${POWERSHELL_EXECUTABLE}" "-NoProfile" "-ExecutionPolicy" "Bypass" "-File" "${SIGN_SCRIPT}" "-BunProfileExe" "${BUILD_PATH}/${bunExe}" "-BunExe" "${BUILD_PATH}/${bunStripExe}"
CWD
${CMAKE_SOURCE_DIR}
SOURCES
${BUILD_PATH}/${bunStripExe}
)
endif()
endif()
# somehow on some Linux systems we need to disable ASLR for ASAN-instrumented binaries to run

View File

@@ -96,7 +96,7 @@ $2b$10$Lyj9kHYZtiyfxh2G60TEfeqs7xkkGiEFFDi3iJGc50ZG/XJ1sxIFi;
The format is composed of:
- `bcrypt`: `$2b`
- `rounds`: `$10` - rounds (log10 of the actual number of rounds)
- `rounds`: `$10` - rounds (log2 of the actual number of rounds)
- `salt`: `$Lyj9kHYZtiyfxh2G60TEfeqs7xkkGiEFFDi3iJGc50ZG/XJ1sxIFi`
- `hash`: `$GzJ8PuBi+K+BVojzPfS5mjnC8OpLGtv8KJqF99eP6a4`

View File

@@ -4,6 +4,7 @@ import { basename, join, relative, resolve } from "node:path";
import {
formatAnnotationToHtml,
getEnv,
getSecret,
isCI,
isWindows,
parseAnnotations,
@@ -164,6 +165,35 @@ async function spawn(command, args, options, label) {
const pipe = process.env.CI === "true";
if (isBuildkite()) {
if (process.env.BUN_LINK_ONLY && isWindows) {
env ||= options?.env || { ...process.env };
// Pass signing secrets directly to the build process
// The PowerShell signing script will handle certificate decoding
env.SM_CLIENT_CERT_PASSWORD = getSecret("SM_CLIENT_CERT_PASSWORD", {
redact: true,
required: true,
});
env.SM_CLIENT_CERT_FILE = getSecret("SM_CLIENT_CERT_FILE", {
redact: true,
required: true,
});
env.SM_API_KEY = getSecret("SM_API_KEY", {
redact: true,
required: true,
});
env.SM_KEYPAIR_ALIAS = getSecret("SM_KEYPAIR_ALIAS", {
redact: true,
required: true,
});
env.SM_HOST = getSecret("SM_HOST", {
redact: true,
required: true,
});
}
}
const subprocess = nodeSpawn(command, effectiveArgs, {
stdio: pipe ? "pipe" : "inherit",
...options,

View File

@@ -767,14 +767,11 @@ pub fn NewWebSocketClient(comptime ssl: bool) type {
) bool {
// For tunnel mode, write through the tunnel instead of direct socket
if (this.proxy_tunnel) |tunnel| {
const wrote = tunnel.write(bytes) catch {
// The tunnel handles TLS encryption and buffering
_ = tunnel.write(bytes) catch {
this.terminate(ErrorCode.failed_to_write);
return false;
};
// Buffer any data the tunnel couldn't accept
if (wrote < bytes.len) {
_ = this.copyToSendBuffer(bytes[wrote..], false);
}
return true;
}
@@ -859,11 +856,9 @@ pub fn NewWebSocketClient(comptime ssl: bool) type {
if (do_write) {
if (comptime Environment.allow_assert) {
if (this.proxy_tunnel == null) {
bun.assert(!this.tcp.isShutdown());
bun.assert(!this.tcp.isClosed());
bun.assert(this.tcp.isEstablished());
}
bun.assert(!this.tcp.isShutdown());
bun.assert(!this.tcp.isClosed());
bun.assert(this.tcp.isEstablished());
}
return this.sendBuffer(this.send_buffer.readableSlice(0));
}
@@ -885,11 +880,9 @@ pub fn NewWebSocketClient(comptime ssl: bool) type {
if (do_write) {
if (comptime Environment.allow_assert) {
if (this.proxy_tunnel == null) {
bun.assert(!this.tcp.isShutdown());
bun.assert(!this.tcp.isClosed());
bun.assert(this.tcp.isEstablished());
}
bun.assert(!this.tcp.isShutdown());
bun.assert(!this.tcp.isClosed());
bun.assert(this.tcp.isEstablished());
}
return this.sendBuffer(this.send_buffer.readableSlice(0));
}
@@ -902,29 +895,21 @@ pub fn NewWebSocketClient(comptime ssl: bool) type {
out_buf: []const u8,
) bool {
bun.assert(out_buf.len > 0);
// Do not use MSG_MORE, see https://github.com/oven-sh/bun/issues/4010
const wrote: usize = if (this.proxy_tunnel) |tunnel|
// In tunnel mode, route through the tunnel's TLS layer
// instead of the detached raw socket.
tunnel.write(out_buf) catch {
this.terminate(ErrorCode.failed_to_write);
return false;
}
else blk: {
if (this.tcp.isClosed()) {
return false;
}
const w = this.tcp.write(out_buf);
if (w < 0) {
this.terminate(ErrorCode.failed_to_write);
return false;
}
break :blk @intCast(w);
};
// Do not set MSG_MORE, see https://github.com/oven-sh/bun/issues/4010
if (this.tcp.isClosed()) {
return false;
}
const wrote = this.tcp.write(out_buf);
if (wrote < 0) {
this.terminate(ErrorCode.failed_to_write);
return false;
}
const expected = @as(usize, @intCast(wrote));
const readable = this.send_buffer.readableSlice(0);
if (readable.ptr == out_buf.ptr) {
this.send_buffer.discard(wrote);
this.send_buffer.discard(expected);
}
return true;
}
@@ -1038,9 +1023,7 @@ pub fn NewWebSocketClient(comptime ssl: bool) type {
}
pub fn hasBackpressure(this: *const WebSocket) bool {
if (this.send_buffer.count > 0) return true;
if (this.proxy_tunnel) |tunnel| return tunnel.hasBackpressure();
return false;
return this.send_buffer.count > 0;
}
pub fn writeBinaryData(
@@ -1372,15 +1355,6 @@ pub fn NewWebSocketClient(comptime ssl: bool) type {
this.handleData(this.tcp, data);
}
/// Called by the WebSocketProxyTunnel when the underlying socket drains.
/// Flushes any buffered plaintext data through the tunnel.
pub fn handleTunnelWritable(this: *WebSocket) void {
if (this.close_received) return;
const send_buf = this.send_buffer.readableSlice(0);
if (send_buf.len == 0) return;
_ = this.sendBuffer(send_buf);
}
pub fn finalize(this: *WebSocket) callconv(.c) void {
log("finalize", .{});
this.clearData();

View File

@@ -297,22 +297,16 @@ pub fn onWritable(this: *WebSocketProxyTunnel) void {
// Send buffered encrypted data
const to_send = this.#write_buffer.slice();
if (to_send.len > 0) {
const written = this.#socket.write(to_send);
if (written < 0) return;
if (to_send.len == 0) return;
const written_usize: usize = @intCast(written);
if (written_usize == to_send.len) {
this.#write_buffer.reset();
} else {
this.#write_buffer.cursor += written_usize;
return; // still have backpressure
}
}
const written = this.#socket.write(to_send);
if (written < 0) return;
// Tunnel drained - let the connected WebSocket flush its send_buffer
if (this.#connected_websocket) |ws| {
ws.handleTunnelWritable();
const written_usize: usize = @intCast(written);
if (written_usize == to_send.len) {
this.#write_buffer.reset();
} else {
this.#write_buffer.cursor += written_usize;
}
}

View File

@@ -698,22 +698,19 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type {
return;
};
// Take the WebSocket upgrade request from proxy state (transfers ownership).
// Store it in input_body_buf so handleWritable can retry on drain.
this.input_body_buf = p.takeWebsocketRequestBuf();
if (this.input_body_buf.len == 0) {
// Take the WebSocket upgrade request from proxy state (transfers ownership)
const upgrade_request = p.takeWebsocketRequestBuf();
if (upgrade_request.len == 0) {
this.terminate(ErrorCode.failed_to_write);
return;
}
// Send through the tunnel (will be encrypted). Buffer any unwritten
// portion in to_send so handleWritable retries when the socket drains.
// Send through the tunnel (will be encrypted)
if (p.getTunnel()) |tunnel| {
const wrote = tunnel.write(this.input_body_buf) catch {
_ = tunnel.write(upgrade_request) catch {
this.terminate(ErrorCode.failed_to_write);
return;
};
this.to_send = this.input_body_buf[wrote..];
} else {
this.terminate(ErrorCode.proxy_tunnel_failed);
}
@@ -1020,17 +1017,6 @@ pub fn NewHTTPUpgradeClient(comptime ssl: bool) type {
tunnel.onWritable();
// In .done state (after WebSocket upgrade), just handle tunnel writes
if (this.state == .done) return;
// Flush any unwritten upgrade request bytes through the tunnel
if (this.to_send.len == 0) return;
this.ref();
defer this.deref();
const wrote = tunnel.write(this.to_send) catch {
this.terminate(ErrorCode.failed_to_write);
return;
};
this.to_send = this.to_send[@min(wrote, this.to_send.len)..];
return;
}
}

View File

@@ -1,126 +0,0 @@
import { expect, test } from "bun:test";
import { tls as tlsCerts } from "harness";
import http from "node:http";
import net from "node:net";
// Regression test: sendBuffer() was writing directly to this.tcp (which is
// detached in proxy tunnel mode) instead of routing through the tunnel's TLS
// layer. Under bidirectional traffic, backpressure pushes writes through the
// sendBuffer slow path, corrupting the TLS stream and killing the connection
// (close code 1006) within seconds.
test("bidirectional ping/pong through TLS proxy", async () => {
const intervals: ReturnType<typeof setInterval>[] = [];
const clearIntervals = () => {
for (const i of intervals) clearInterval(i);
intervals.length = 0;
};
using server = Bun.serve({
port: 0,
tls: { key: tlsCerts.key, cert: tlsCerts.cert },
fetch(req, server) {
if (server.upgrade(req)) return;
return new Response("Expected WebSocket", { status: 400 });
},
websocket: {
message(ws, msg) {
ws.send("echo:" + msg);
},
open(ws) {
// Server pings periodically (like session-ingress's 54s interval, sped up)
intervals.push(
setInterval(() => {
if (ws.readyState === 1) ws.ping();
}, 500),
);
// Server pushes data continuously
intervals.push(
setInterval(() => {
if (ws.readyState === 1) ws.send("push:" + Date.now());
}, 100),
);
},
close() {
clearIntervals();
},
},
});
// HTTP CONNECT proxy
const proxy = http.createServer((req, res) => {
res.writeHead(400);
res.end();
});
proxy.on("connect", (req, clientSocket, head) => {
const [host, port] = req.url!.split(":");
const serverSocket = net.createConnection({ host: host!, port: parseInt(port!) }, () => {
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
serverSocket.pipe(clientSocket);
clientSocket.pipe(serverSocket);
if (head.length > 0) serverSocket.write(head);
});
serverSocket.on("error", () => clientSocket.destroy());
clientSocket.on("error", () => serverSocket.destroy());
});
const { promise: proxyReady, resolve: proxyReadyResolve } = Promise.withResolvers<void>();
proxy.listen(0, "127.0.0.1", () => proxyReadyResolve());
await proxyReady;
const proxyPort = (proxy.address() as net.AddressInfo).port;
const { promise, resolve, reject } = Promise.withResolvers<void>();
const ws = new WebSocket(`wss://localhost:${server.port}`, {
proxy: `http://127.0.0.1:${proxyPort}`,
tls: { rejectUnauthorized: false },
} as any);
const REQUIRED_PONGS = 5;
let pongReceived = true;
let closeCode: number | undefined;
ws.addEventListener("open", () => {
// Client sends pings (like Claude Code's 10s interval, sped up)
intervals.push(
setInterval(() => {
if (!pongReceived) {
reject(new Error("Pong timeout - connection dead"));
return;
}
pongReceived = false;
(ws as any).ping?.();
}, 400),
);
// Client writes data continuously (bidirectional traffic triggers the bug)
intervals.push(
setInterval(() => {
if (ws.readyState === WebSocket.OPEN) ws.send("data:" + Date.now());
}, 50),
);
});
// Resolve as soon as enough pongs arrive (condition-based, not timer-gated)
let pongCount = 0;
ws.addEventListener("pong", () => {
pongCount++;
pongReceived = true;
if (pongCount >= REQUIRED_PONGS) resolve();
});
ws.addEventListener("close", e => {
closeCode = (e as CloseEvent).code;
clearIntervals();
if (pongCount < REQUIRED_PONGS) {
reject(new Error(`Connection closed (${closeCode}) after only ${pongCount}/${REQUIRED_PONGS} pongs`));
}
});
try {
await promise;
expect(pongCount).toBeGreaterThanOrEqual(REQUIRED_PONGS);
ws.close();
} finally {
clearIntervals();
proxy.close();
}
}, 10000);