Skip to content
Merged
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
60 changes: 60 additions & 0 deletions .github/actions/sign-windows-artifacts/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: Sign Windows artifacts
description: Azure OIDC login + Trusted Signing of all .exe under target/distrib, then refresh zips so archives contain the signed binaries.

inputs:
client-id:
required: true
description: Azure AD App Registration client ID (OIDC)
tenant-id:
required: true
description: Azure tenant ID
subscription-id:
required: true
description: Azure subscription ID
endpoint:
required: true
description: Trusted Signing Account endpoint URL
account-name:
required: true
description: Trusted Signing Account name
cert-profile:
required: true
description: Certificate profile name

runs:
using: composite
steps:
- uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0
with:
client-id: ${{ inputs.client-id }}
tenant-id: ${{ inputs.tenant-id }}
subscription-id: ${{ inputs.subscription-id }}

- uses: azure/artifact-signing-action@c7ab2a863ab5f9a846ddb8265964877ef296ee82 # v2.0.0
with:
endpoint: ${{ inputs.endpoint }}
trusted-signing-account-name: ${{ inputs.account-name }}
certificate-profile-name: ${{ inputs.cert-profile }}
files-folder: target/distrib
files-folder-filter: exe
files-folder-recurse: true
file-digest: SHA256
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256

- name: Refresh zip archives with signed executables
shell: pwsh
run: |
Get-ChildItem -Path "target/distrib" -Filter "*.zip" | ForEach-Object {
$zipPath = $_.FullName
$stagingDir = Join-Path $_.DirectoryName $_.BaseName
if (Test-Path $stagingDir) {
Remove-Item $zipPath -Force
Compress-Archive -Path "$stagingDir\*" -DestinationPath $zipPath
$shaFile = "$zipPath.sha256"
if (Test-Path $shaFile) {
$hash = (Get-FileHash $zipPath -Algorithm SHA256).Hash.ToLower()
"$hash $($_.Name)" | Set-Content $shaFile -NoNewline
}
}
}
29 changes: 29 additions & 0 deletions .github/workflows/release-canary.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ concurrency:
permissions:
actions: read
contents: write
id-token: write
issues: write
pull-requests: write

Expand Down Expand Up @@ -110,9 +111,13 @@ jobs:
matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }}
runs-on: ${{ matrix.runner }}
container: ${{ matrix.container && matrix.container.image || null }}
# OIDC token subject must match the AAD federated credential, which is
# scoped to the `release` environment — only claim it on main.
environment: ${{ github.ref == 'refs/heads/main' && 'release' || '' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json
HAS_AZURE_SIGNING: ${{ secrets.AZURE_CLIENT_ID != '' && secrets.AZURE_TENANT_ID != '' && secrets.AZURE_SUBSCRIPTION_ID != '' }}
steps:
- name: Enable windows longpaths
run: git config --global core.longpaths true
Expand Down Expand Up @@ -160,12 +165,36 @@ jobs:
- name: Install dependencies
run: ${{ matrix.packages_install }}

- name: Set up MSVC cross-compilation for Windows ARM64
if: ${{ contains(matrix.targets, 'aarch64-pc-windows-msvc') }}
uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1.13.0
with:
arch: amd64_arm64

- name: Set MSVC ARM64 linker for cargo cross-compilation
if: ${{ contains(matrix.targets, 'aarch64-pc-windows-msvc') }}
shell: pwsh
run: |
$link = (Get-Command link.exe -ErrorAction Stop).Source
"CARGO_TARGET_AARCH64_PC_WINDOWS_MSVC_LINKER=$link" >> $env:GITHUB_ENV

- name: Build artifacts
shell: bash
run: |
dist build --tag="${{ needs.plan.outputs.dist-tag }}" --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
echo "dist ran successfully"

- name: Sign Windows artifacts
if: ${{ runner.os == 'Windows' && env.HAS_AZURE_SIGNING == 'true' }}
uses: ./.github/actions/sign-windows-artifacts
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
endpoint: ${{ vars.AZURE_SIGNING_ENDPOINT }}
account-name: ${{ vars.AZURE_SIGNING_ACCOUNT_NAME }}
cert-profile: ${{ vars.AZURE_SIGNING_CERT_PROFILE }}

- id: dist-files
name: Post-build
shell: bash
Expand Down
51 changes: 27 additions & 24 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ on:

permissions:
contents: write
id-token: write

env:
CARGO_NET_GIT_FETCH_WITH_CLI: true
Expand Down Expand Up @@ -145,10 +146,13 @@ jobs:
matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }}
runs-on: ${{ matrix.runner }}
container: ${{ matrix.container && matrix.container.image || null }}
# OIDC token subject must match the AAD federated credential, which is
# scoped to the `release` environment.
environment: release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json
HAS_SSLDOTCOM_SIGNING: ${{ secrets.SSLDOTCOM_USERNAME != '' && secrets.SSLDOTCOM_PASSWORD != '' && secrets.SSLDOTCOM_CREDENTIAL_ID != '' && secrets.SSLDOTCOM_TOTP_SECRET != '' }}
HAS_AZURE_SIGNING: ${{ secrets.AZURE_CLIENT_ID != '' && secrets.AZURE_TENANT_ID != '' && secrets.AZURE_SUBSCRIPTION_ID != '' }}
steps:
- name: Enable windows longpaths
run: git config --global core.longpaths true
Expand Down Expand Up @@ -196,30 +200,18 @@ jobs:
- name: Install dependencies
run: ${{ matrix.packages_install }}

- name: Configure SSL.com signing env
if: ${{ runner.os == 'Windows' && env.HAS_SSLDOTCOM_SIGNING == 'true' && !fromJson(needs.plan.outputs.val).announcement_is_prerelease }}
shell: bash
env:
SSLDOTCOM_USERNAME: ${{ secrets.SSLDOTCOM_USERNAME }}
SSLDOTCOM_PASSWORD: ${{ secrets.SSLDOTCOM_PASSWORD }}
SSLDOTCOM_CREDENTIAL_ID: ${{ secrets.SSLDOTCOM_CREDENTIAL_ID }}
SSLDOTCOM_TOTP_SECRET: ${{ secrets.SSLDOTCOM_TOTP_SECRET }}
run: |
write_github_env() {
local key="$1"
local value="$2"
local delimiter="EOF_${key}_$$"
{
echo "${key}<<${delimiter}"
echo "${value}"
echo "${delimiter}"
} >> "$GITHUB_ENV"
}
- name: Set up MSVC cross-compilation for Windows ARM64
if: ${{ contains(matrix.targets, 'aarch64-pc-windows-msvc') }}
uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1.13.0
with:
arch: amd64_arm64

write_github_env "SSLDOTCOM_USERNAME" "$SSLDOTCOM_USERNAME"
write_github_env "SSLDOTCOM_PASSWORD" "$SSLDOTCOM_PASSWORD"
write_github_env "SSLDOTCOM_CREDENTIAL_ID" "$SSLDOTCOM_CREDENTIAL_ID"
write_github_env "SSLDOTCOM_TOTP_SECRET" "$SSLDOTCOM_TOTP_SECRET"
- name: Set MSVC ARM64 linker for cargo cross-compilation
if: ${{ contains(matrix.targets, 'aarch64-pc-windows-msvc') }}
shell: pwsh
run: |
$link = (Get-Command link.exe -ErrorAction Stop).Source
"CARGO_TARGET_AARCH64_PC_WINDOWS_MSVC_LINKER=$link" >> $env:GITHUB_ENV

- name: Build artifacts
shell: bash
Expand All @@ -229,6 +221,17 @@ jobs:
dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
echo "dist ran successfully"

- name: Sign Windows artifacts
if: ${{ runner.os == 'Windows' && env.HAS_AZURE_SIGNING == 'true' }}
uses: ./.github/actions/sign-windows-artifacts
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
endpoint: ${{ vars.AZURE_SIGNING_ENDPOINT }}
account-name: ${{ vars.AZURE_SIGNING_ACCOUNT_NAME }}
cert-profile: ${{ vars.AZURE_SIGNING_CERT_PROFILE }}

- id: dist-files
name: Post-build
shell: bash
Expand Down
33 changes: 33 additions & 0 deletions .github/workflows/test-azure-oidc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: test-azure-oidc

# Validates that the federated identity credentials and GitHub repo secrets
# are wired up correctly. Does NOT invoke Trusted Signing — only confirms the
# OIDC handshake and that az can authenticate as the configured principal.
# Safe to run before the (paid) Trusted Signing Account is created.

on:
workflow_dispatch:

permissions:
id-token: write
contents: read

jobs:
oidc:
runs-on: ubuntu-latest
# Sandbox AAD app's federated credential trusts this environment only.
environment: sandbox-release
steps:
- name: Azure login (OIDC)
uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

- name: Confirm authenticated identity
run: |
az account show
echo
echo "Signed-in service principal:"
az ad sp show --id "${{ secrets.AZURE_CLIENT_ID }}" --query "{displayName: displayName, appId: appId}" -o table
130 changes: 130 additions & 0 deletions .github/workflows/test-signing-self-signed.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
name: test-signing-self-signed

# Zero-cost validation of the Windows signing pipeline.
# Mirrors the release.yml signing flow but uses a self-signed certificate
# via signtool directly instead of azure/artifact-signing-action. This proves
# the signing mechanics and the zip-refresh logic work end to end without
# any Azure resources or costs.

on:
workflow_dispatch:
pull_request:
paths:
- ".github/workflows/test-signing-self-signed.yml"

permissions:
contents: read

jobs:
signing-mechanics:
runs-on: windows-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

- name: Set up fake distrib layout (mirrors release.yml output)
shell: pwsh
run: |
New-Item -ItemType Directory -Path target/distrib/bt-x86_64-pc-windows-msvc -Force | Out-Null
# Use a real signable PE — copy a system exe so signtool has something legitimate to sign
Copy-Item C:\Windows\System32\where.exe target/distrib/bt-x86_64-pc-windows-msvc/bt.exe
# Build the matching zip that release.yml's "Refresh zip" step expects to find
Compress-Archive -Path target/distrib/bt-x86_64-pc-windows-msvc/* -DestinationPath target/distrib/bt-x86_64-pc-windows-msvc.zip
$hash = (Get-FileHash target/distrib/bt-x86_64-pc-windows-msvc.zip -Algorithm SHA256).Hash.ToLower()
"$hash bt-x86_64-pc-windows-msvc.zip" | Set-Content target/distrib/bt-x86_64-pc-windows-msvc.zip.sha256 -NoNewline

- name: Create self-signed code-signing certificate
shell: pwsh
run: |
$cert = New-SelfSignedCertificate `
-Subject "CN=BT Self-Signed Test" `
-Type CodeSigningCert `
-CertStoreLocation Cert:\CurrentUser\My `
-KeyUsage DigitalSignature `
-HashAlgorithm SHA256 `
-NotAfter (Get-Date).AddDays(7)
$pfxPath = Join-Path $env:RUNNER_TEMP "test-signing.pfx"
$pwd = ConvertTo-SecureString -String "midwife-ding-car-haskell-matador-jeff-dungeon" -Force -AsPlainText
Export-PfxCertificate -Cert "Cert:\CurrentUser\My\$($cert.Thumbprint)" -FilePath $pfxPath -Password $pwd | Out-Null
"PFX_PATH=$pfxPath" >> $env:GITHUB_ENV

- name: Locate signtool
shell: pwsh
run: |
$signtool = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin" -Recurse -Filter signtool.exe -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -match '\\x64\\signtool\.exe$' } |
Sort-Object FullName -Descending |
Select-Object -First 1
if (-not $signtool) { throw "signtool.exe not found on runner" }
"SIGNTOOL=$($signtool.FullName)" >> $env:GITHUB_ENV

- name: Sign exe (mirrors azure/artifact-signing-action behavior)
shell: pwsh
run: |
Get-ChildItem -Path target/distrib -Filter *.exe -Recurse | ForEach-Object {
Write-Host "Signing: $($_.FullName)"
& $env:SIGNTOOL sign `
/f $env:PFX_PATH `
/p "midwife-ding-car-haskell-matador-jeff-dungeon" `
/fd SHA256 `
/tr http://timestamp.digicert.com `
/td SHA256 `
$_.FullName
if ($LASTEXITCODE -ne 0) { throw "signtool sign failed" }
}

- name: Verify signature on raw exe
shell: pwsh
run: |
# Get-AuthenticodeSignature returns a structured status. We accept
# NotTrusted (untrusted self-signed root, but signature itself is valid
# and timestamped) — that's the expected state for this test. signtool
# verify can't be used because every supported way to install a self-
# signed cert into the runner's Root store hangs on a CryptUI prompt.
# Production signing uses Trusted Signing, which chains to a trusted
# Microsoft root and verifies cleanly without any of this.
$sig = Get-AuthenticodeSignature target/distrib/bt-x86_64-pc-windows-msvc/bt.exe
if ($sig.Status -notin @('Valid', 'NotTrusted')) {
throw "Signature invalid (status=$($sig.Status)): $($sig.StatusMessage)"
}
if (-not $sig.SignerCertificate) { throw "Signature missing signer certificate" }
if (-not $sig.TimeStamperCertificate) { throw "Signature missing RFC3161 timestamp" }
Write-Host "Raw exe signature OK (status=$($sig.Status), signer=$($sig.SignerCertificate.Subject))"

- name: Refresh zip archives with signed executables (verbatim from release.yml)
shell: pwsh
run: |
Get-ChildItem -Path "target/distrib" -Filter "*.zip" | ForEach-Object {
$zipPath = $_.FullName
$stagingDir = Join-Path $_.DirectoryName $_.BaseName
if (Test-Path $stagingDir) {
Remove-Item $zipPath -Force
Compress-Archive -Path "$stagingDir\*" -DestinationPath $zipPath
$shaFile = "$zipPath.sha256"
if (Test-Path $shaFile) {
$hash = (Get-FileHash $zipPath -Algorithm SHA256).Hash.ToLower()
"$hash $($_.Name)" | Set-Content $shaFile -NoNewline
}
}
}

- name: Verify signature survives the zip refresh
shell: pwsh
run: |
$verifyDir = Join-Path $env:RUNNER_TEMP "verify"
New-Item -ItemType Directory -Path $verifyDir -Force | Out-Null
Expand-Archive -Path target/distrib/bt-x86_64-pc-windows-msvc.zip -DestinationPath $verifyDir -Force
$sig = Get-AuthenticodeSignature (Join-Path $verifyDir "bt.exe")
if ($sig.Status -notin @('Valid', 'NotTrusted')) {
throw "Signature lost after zip refresh (status=$($sig.Status)): $($sig.StatusMessage)"
}
if (-not $sig.SignerCertificate) { throw "Signature missing signer certificate after zip refresh" }
if (-not $sig.TimeStamperCertificate) { throw "Signature missing RFC3161 timestamp after zip refresh" }
Write-Host "Signature survived the zip refresh (status=$($sig.Status))"

- name: Verify sha256 file matches the refreshed zip
shell: pwsh
run: |
$expected = (Get-Content target/distrib/bt-x86_64-pc-windows-msvc.zip.sha256 -Raw).Split(' ')[0].Trim()
$actual = (Get-FileHash target/distrib/bt-x86_64-pc-windows-msvc.zip -Algorithm SHA256).Hash.ToLower()
if ($expected -ne $actual) { throw "sha256 mismatch: expected=$expected actual=$actual" }
Write-Host "sha256 file matches the refreshed zip"
3 changes: 1 addition & 2 deletions dist-workspace.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ ci = "github"
create-release = true
# Which actions to run on pull requests
pr-run-mode = "plan"
ssldotcom-windows-sign = "test"
# The installers to generate for each app
installers = ["shell", "powershell"]
homepage = "https://github.com/braintrustdata/bt"
Expand All @@ -29,4 +28,4 @@ windows-archive = ".zip"
install-success-msg = ""

[dist.github-custom-runners]
aarch64-pc-windows-msvc = "windows-11-arm"
aarch64-pc-windows-msvc = "windows-2022" # azure/artifact-signing-action doesn't run on ARM, so we need to cross-compile bt.exe for ARM64 and sign it on windows amd64
Loading