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
7 changes: 7 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,15 @@ jobs:
shellcheck --shell=bash --severity=error scripts/check-tool-pins.sh
dash -n scripts/install.sh
bash -n scripts/tests/install-verify.sh
shellcheck --shell=bash --severity=error scripts/tests/install-ps1-verify.sh
bash -n scripts/tests/install-ps1-verify.sh
- name: Verification harness (mandatory cosign / fail-closed)
run: bash scripts/tests/install-verify.sh
# Same property on Windows (backend#2078). pwsh is preinstalled on the
# ubuntu runner image; the harness FAILS rather than skips if it isn't,
# since "cannot tell" is not evidence that verification is mandatory.
- name: Verification harness — Windows (mandatory cosign / fail-closed)
run: bash scripts/tests/install-ps1-verify.sh

test:
timeout-minutes: 15
Expand Down
203 changes: 184 additions & 19 deletions scripts/install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
# 2. Resolves the latest release tag (or honors $env:RELEASE_VERSION)
# 3. Downloads tracebloc-<tag>-windows-amd64.exe + SHA256SUMS
# 4. Verifies SHA256
# 5. (Optional) Verifies cosign signature if cosign.exe is on PATH
# 5. Verifies the cosign signature — MANDATORY (RFC-0001 R8). If cosign
# isn't on PATH it bootstraps a pinned, checksum-verified copy; if it
# can't, the install FAILS CLOSED rather than trusting the same-channel
# SHA256 alone. TRACEBLOC_ALLOW_UNVERIFIED=1 is the one (loud) escape.
# 6. Installs to $env:USERPROFILE\AppData\Local\Programs\tracebloc\tracebloc.exe
# and PATH-adds it via user-scope env var
#
Expand Down Expand Up @@ -45,6 +48,111 @@ $InstallPrefix = if ($env:INSTALL_PREFIX) { $env:INSTALL_PREFIX } `
$GitHubRepo = 'tracebloc/cli'
$BinaryName = 'tracebloc.exe'

# Pinned verifier. Keep in lockstep with tracebloc/client's install.sh /
# install.ps1 COSIGN_VERSION and release.yml's cosign-installer pin.
$CosignVersion = 'v2.4.1'

# The ONE escape from mandatory verification, for a genuinely constrained
# environment. Loud, and never the default (RFC-0001 R8).
#
# Compare against '1' explicitly. NOT [bool]$env:... — PowerShell casts any
# non-empty string to $true, so TRACEBLOC_ALLOW_UNVERIFIED=0 would have
# switched the bypass ON. Matches install.sh's `[ "$ALLOW_UNVERIFIED" = "1" ]`.
$AllowUnverified = ($env:TRACEBLOC_ALLOW_UNVERIFIED -eq '1')

# TLS 1.2 floor. PowerShell 5.1 defaults to SSL3/TLS1.0 on older Windows, and
# every fetch below carries either the binary we are about to run or the
# verifier that authenticates it — neither may negotiate down. PS7+ already
# defaults higher; setting it is harmless there.
try {
[Net.ServicePointManager]::SecurityProtocol =
[Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
} catch { }

# ---------------------------------------------------------------------
# cosign bootstrap (RFC-0001 R8).
# ---------------------------------------------------------------------

function Get-Sha256([string]$Path) {
return (Get-FileHash -Algorithm SHA256 -Path $Path).Hash.ToLower()
}

# Resolve a cosign we can vouch for: one already on PATH, else a pinned build
# fetched and checked against sigstore's own published checksums. Returns the
# path, or $null when it cannot be obtained — the caller decides what that means.
#
# A cosign we cannot vouch for is no better than no cosign, so a checksum
# mismatch returns $null rather than a usable path.
function Resolve-Cosign([string]$TmpDir) {
$onPath = Get-Command cosign -ErrorAction SilentlyContinue
if ($onPath) { return $onPath.Source }

# BOTH architectures fetch the amd64 build, deliberately.
#
# Sigstore has never published a Windows arm64 cosign — not at $CosignVersion,
# not at any release. `cosign-windows-amd64.exe` is the only Windows asset
# there is, so asking for a per-arch name 404s and blocks Windows-on-ARM
# permanently (tracebloc/client#734, fixed there the same way).
#
# Running it under Windows-on-ARM's x64 emulation costs nothing that matters:
# cosign verifies a signature over BYTES, so the instruction set it was
# compiled for cannot change the verdict, and the artifact we hand it is
# still the native arm64 binary. It is checksum-verified below exactly as on
# amd64, so the trust chain is identical.
#
# Do not "fix" this to $arch. There is nothing on the other end — and
# since the asset is arch-independent there is nothing to branch on
# either; whether it RUNS here is Test-CosignRuns' question, not ours.
$base = "https://github.com/sigstore/cosign/releases/download/$CosignVersion"
$asset = 'cosign-windows-amd64.exe'
$bin = Join-Path $TmpDir 'cosign.exe'
$sums = Join-Path $TmpDir 'cosign_checksums.txt'

Write-Host " cosign not found — downloading pinned cosign $CosignVersion (~17 MB) to verify the signature..."
try {
Invoke-WebRequest -Uri "$base/$asset" -OutFile $bin -UseBasicParsing
Invoke-WebRequest -Uri "$base/cosign_checksums.txt" -OutFile $sums -UseBasicParsing
} catch {
Write-Host " ⚠ couldn't download cosign: $($_.Exception.Message)"
return $null
}

# cosign_checksums.txt lines: "<sha256> <asset>".
$want = $null
foreach ($line in Get-Content -LiteralPath $sums) {
$parts = @($line -split '\s+' | Where-Object { $_ -ne '' })
if ($parts.Count -ge 2 -and $parts[-1] -eq $asset) { $want = $parts[0].ToLower(); break }
}
if (-not $want) { return $null }
if ((Get-Sha256 $bin) -ne $want) {
Write-Host " Bootstrapped cosign failed its own checksum — not using it." -ForegroundColor Red
return $null
}
Write-Host " ✓ cosign $CosignVersion downloaded and checksum-verified"
return $bin
}

# Can this cosign actually EXECUTE here? A trivial `cosign version`.
#
# A binary that will not start reports through the same channel as a signature
# that did not verify, and those warrant opposite reactions — only one of them
# means the artifact may be tampered with. Windows-on-ARM makes it real: the
# amd64 build needs x64 emulation, and where that is absent cosign never runs.
# The 255 preset means a binary that never starts cannot leave a stale 0 behind.
function Test-CosignRuns([string]$Cosign) {
$global:LASTEXITCODE = 255
$prev = $ErrorActionPreference
try {
$ErrorActionPreference = 'Continue'
& $Cosign version 2>&1 | Out-Null
} catch {
return $false
} finally {
$ErrorActionPreference = $prev
}
return ($LASTEXITCODE -eq 0)
}

# ---------------------------------------------------------------------
# Detect arch.
# ---------------------------------------------------------------------
Expand Down Expand Up @@ -137,45 +245,102 @@ try {
Write-Host " ✓ checksum matches"

# -------------------------------------------------------------
# Cosign signature verification (optional).
# Cosign signature verification — MANDATORY (RFC-0001 R8).
#
# The SHA256 above is same-channel: it comes from the same GitHub
# release as the binary, so whoever could swap the binary could swap
# SHA256SUMS with it. It proves the download completed, not who built
# it. The cosign signature is the independent, Sigstore-rooted proof
# that tracebloc's release workflow produced these bytes.
#
# So this no longer skips when cosign is absent — it bootstraps a
# pinned, checksum-verified cosign, and FAILS CLOSED when it cannot.
# This mirrors install.sh exactly; Windows was the one platform still
# installing on the checksum alone (backend#2078).
#
# TRACEBLOC_ALLOW_UNVERIFIED=1 covers "cannot verify" — no cosign, no
# .sig/.cert. It deliberately does NOT cover a verification that ran
# and FAILED: that is evidence of tampering, and no env var overrides
# it.
# -------------------------------------------------------------
if (Get-Command cosign -ErrorAction SilentlyContinue) {
$cosign = Resolve-Cosign $tmpDir

if ($cosign -and -not (Test-CosignRuns $cosign)) {
# Distinct from "no cosign": we have one, it just won't start here.
# On Windows-on-ARM that means x64 emulation is missing or blocked;
# it can also be SmartScreen/AV quarantine or a policy block. Saying
# "install cosign" here would be useless advice — one is installed.
if ($AllowUnverified) {
Write-Host " WARNING: cosign is present but won't run here — signature NOT" -ForegroundColor Yellow
Write-Host " verified (TRACEBLOC_ALLOW_UNVERIFIED=1)." -ForegroundColor Yellow
$cosign = $null
} else {
Write-Host "Error: cosign was found but won't execute on this machine, so the" -ForegroundColor Red
Write-Host " signature can't be verified (RFC-0001 R8)." -ForegroundColor Red
Write-Host " On Windows-on-ARM this usually means x64 emulation is" -ForegroundColor Red
Write-Host " unavailable; it can also be a quarantine or policy block." -ForegroundColor Red
Write-Host " Fix that, or for a constrained environment re-run with" -ForegroundColor Red
Write-Host " TRACEBLOC_ALLOW_UNVERIFIED=1." -ForegroundColor Red
exit 1
}
}

if (-not $cosign) {
if (-not $AllowUnverified) {
Write-Host "Error: cosign is required to verify the binary's signature and" -ForegroundColor Red
Write-Host " could not be found or bootstrapped — refusing to install on" -ForegroundColor Red
Write-Host " an unauthenticated, same-channel checksum alone (RFC-0001 R8)." -ForegroundColor Red
Write-Host " Fix: install cosign and re-run —" -ForegroundColor Red
Write-Host " https://docs.sigstore.dev/cosign/system_config/installation/" -ForegroundColor Red
Write-Host " or for a constrained environment re-run with" -ForegroundColor Red
Write-Host " TRACEBLOC_ALLOW_UNVERIFIED=1." -ForegroundColor Red
exit 1
}
Write-Host " WARNING: cosign unavailable and couldn't be bootstrapped —" -ForegroundColor Yellow
Write-Host " signature NOT verified (TRACEBLOC_ALLOW_UNVERIFIED=1). The SHA256" -ForegroundColor Yellow
Write-Host " above is same-channel only; do not use this path in production." -ForegroundColor Yellow
} else {
Write-Host "Verifying cosign signature..."
# Separate "download .sig/.cert" (recoverable if absent — old
# releases predate signing) from "verify the downloaded sig"
# (NOT recoverable — a failed verification means the binary
# is potentially tampered, refuse to install). Bugbot PR #11
# caught the prior structure: with $ErrorActionPreference =
# 'Stop', Write-Error inside the try-block was thrown and
# caught by the same catch that handled missing-sig, so a
# failed verify silently downgraded to "skip + continue."

# "download .sig/.cert" and "verify the downloaded sig" must stay
# separate. With $ErrorActionPreference = 'Stop', a Write-Error
# inside the try-block is thrown and caught by the same catch that
# handles a missing sig — so a FAILED verify silently downgrades to
# "skip + continue" (Bugbot, PR #11). The verify below therefore
# runs OUTSIDE any try/catch: & invokes cosign as an external
# process, whose non-zero $LASTEXITCODE cannot be caught anyway.
$sigDownloaded = $false
try {
Invoke-WebRequest -Uri "$baseUrl/$binaryFile.sig" -OutFile (Join-Path $tmpDir "$binaryFile.sig") -UseBasicParsing
Invoke-WebRequest -Uri "$baseUrl/$binaryFile.cert" -OutFile (Join-Path $tmpDir "$binaryFile.cert") -UseBasicParsing
$sigDownloaded = $true
} catch {
Write-Host " ⚠ couldn't download .sig/.cert — release may pre-date signing."
if (-not $AllowUnverified) {
Write-Host "Error: couldn't download $binaryFile.sig / .cert for $tag — the" -ForegroundColor Red
Write-Host " release is unsigned or incomplete. Every supported release" -ForegroundColor Red
Write-Host " is cosign-signed; refusing to install unverified (RFC-0001 R8)." -ForegroundColor Red
Write-Host ' Pin a signed $env:RELEASE_VERSION, or re-run with TRACEBLOC_ALLOW_UNVERIFIED=1.' -ForegroundColor Red
exit 1
}
Write-Host " WARNING: .sig/.cert not published for $tag — signature NOT" -ForegroundColor Yellow
Write-Host " verified (TRACEBLOC_ALLOW_UNVERIFIED=1)." -ForegroundColor Yellow
}

if ($sigDownloaded) {
# Verify OUTSIDE the try/catch: a non-zero $LASTEXITCODE
# from cosign is a hard refusal, not a swallowed
# exception. & invokes cosign as an external process,
# which doesn't interact with $ErrorActionPreference.
& cosign verify-blob `
& $cosign verify-blob `
--certificate-identity-regexp "https://github.com/$GitHubRepo/.github/workflows/release.yml@refs/tags/v.*" `
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' `
--certificate (Join-Path $tmpDir "$binaryFile.cert") `
--signature (Join-Path $tmpDir "$binaryFile.sig") `
(Join-Path $tmpDir $binaryFile) 2>$null
if ($LASTEXITCODE -ne 0) {
# No TRACEBLOC_ALLOW_UNVERIFIED branch here, deliberately.
# Verification RAN and said no.
Write-Host "Error: cosign signature verification FAILED — refusing to install." -ForegroundColor Red
exit 1
}
Write-Host " ✓ cosign signature valid"
}
} else {
Write-Host " (cosign not installed; SHA256 verified, signature skipped)"
}

# -------------------------------------------------------------
Expand Down
Loading
Loading