<# .SYNOPSIS Verify a downloaded MoonBite release artifact (Windows PowerShell 5.1+ / pwsh). .DESCRIPTION Checks, in order: 1. sha256 - the artifact's hash appears in SHA256SUMS.txt under its own name (REQUIRED) 2. minisign - SHA256SUMS.txt.minisig verifies against the pinned public key (when minisign.exe is on PATH and the key is not the placeholder) 3. sigstore - `gh attestation verify --owner ` passes (when gh is on PATH) Exit code 0 only when every check that could run passed. Checks that could not run are reported as SKIP, and the verdict says what was not proven. .EXAMPLE .\scripts\release\Verify-Release.ps1 .\moonbite-miner-windows-x86_64.zip .\scripts\release\Verify-Release.ps1 .\moonbite-wallet-windows-x86_64.zip -Sums .\SHA256SUMS.txt -PubKey .\moonbite-release.pub #> [CmdletBinding()] param( [Parameter(Mandatory = $true, Position = 0)] [string] $Artifact, [Parameter(Position = 1)] [string] $Sums = "", [string] $PubKey = "", [string] $Owner = "moonbitecoin" ) $ErrorActionPreference = "Stop" $script:pass = 0; $script:fail = 0; $script:skip = 0 function Pass($m) { Write-Host "PASS $m"; $script:pass++ } function Fail($m) { Write-Host "FAIL $m"; $script:fail++ } function Skip($m) { Write-Host "SKIP $m"; $script:skip++ } function Have($cmd) { return [bool](Get-Command $cmd -ErrorAction SilentlyContinue) } if (-not (Test-Path -LiteralPath $Artifact -PathType Leaf)) { Write-Host "FAIL artifact not found: $Artifact"; exit 1 } $Artifact = (Resolve-Path -LiteralPath $Artifact).Path $dir = Split-Path -Parent $Artifact $name = Split-Path -Leaf $Artifact if ($Sums -eq "") { $Sums = Join-Path $dir "SHA256SUMS.txt" } if ($PubKey -eq "") { foreach ($c in @((Join-Path $dir "moonbite-release.pub"), (Join-Path $PSScriptRoot "..\..\deploy\moonbite-release.pub"))) { if (Test-Path -LiteralPath $c -PathType Leaf) { $PubKey = (Resolve-Path -LiteralPath $c).Path; break } } } Write-Host "artifact : $Artifact" Write-Host "manifest : $Sums" if ($PubKey -ne "") { Write-Host "pubkey : $PubKey" } else { Write-Host "pubkey : " } Write-Host "" # 1. sha256 --------------------------------------------------------------------- if (-not (Test-Path -LiteralPath $Sums -PathType Leaf)) { Fail "sha256: manifest not found ($Sums). Download SHA256SUMS.txt from the same release." } else { $actual = (Get-FileHash -LiteralPath $Artifact -Algorithm SHA256).Hash.ToLowerInvariant() $expected = $null $pattern = '^([0-9a-fA-F]{64})\s+\*?(?:.*/)?' + [regex]::Escape($name) + '\s*$' foreach ($line in Get-Content -LiteralPath $Sums) { $m = [regex]::Match($line, $pattern) if ($m.Success) { $expected = $m.Groups[1].Value.ToLowerInvariant(); break } } if ($null -eq $expected) { Fail "sha256: '$name' is not listed in $Sums" } elseif ($expected -eq $actual) { Pass "sha256: $actual matches manifest" } else { Fail "sha256: MISMATCH manifest=$expected actual=$actual -- do not run this file" } } # 2. minisign ------------------------------------------------------------------- $keyLine = $null if ($PubKey -ne "" -and (Test-Path -LiteralPath $PubKey -PathType Leaf)) { foreach ($line in Get-Content -LiteralPath $PubKey) { $t = $line.Trim() if ($t -eq "" -or $t.StartsWith("#") -or $t.StartsWith("untrusted comment")) { continue } $keyLine = $t; break } } $sig = "$Sums.minisig" if (-not (Have "minisign")) { Skip "minisign: tool not installed (https://jedisct1.github.io/minisign/)" } elseif ($null -eq $keyLine -or $keyLine.StartsWith("REPLACE_ME")) { Skip "minisign: release public key is still a placeholder (key ceremony pending, deploy/RELEASE-SIGNING.md)" } elseif (-not (Test-Path -LiteralPath $sig -PathType Leaf)) { Skip "minisign: no $(Split-Path -Leaf $Sums).minisig next to the manifest" } else { # Native stderr must not become a terminating error (PS 5.1 NativeCommandError). $ErrorActionPreference = "Continue" & minisign -Vm $Sums -P $keyLine *> $null $ErrorActionPreference = "Stop" if ($LASTEXITCODE -eq 0) { Pass "minisign: $(Split-Path -Leaf $Sums).minisig verifies against pinned key" } else { Fail "minisign: signature on $(Split-Path -Leaf $Sums) does NOT verify -- manifest may be forged" } } # 3. Sigstore attestation ------------------------------------------------------- if (-not (Have "gh")) { Skip "sigstore: GitHub CLI (gh) not installed; cannot check build provenance" } else { $ErrorActionPreference = "Continue" $out = (& gh attestation verify $Artifact --owner $Owner 2>&1 | ForEach-Object { "$_" } | Out-String) $ErrorActionPreference = "Stop" if ($LASTEXITCODE -eq 0) { Pass "sigstore: attestation verified for owner '$Owner' (built by GitHub Actions from the public repo)" } elseif ($out -match "not logged|auth login|authentication|Bad credentials|HTTP 401") { Skip "sigstore: gh is not authenticated or its token is invalid (run 'gh auth login'); attestation not checked" } else { Fail "sigstore: no valid attestation for this file under owner '$Owner'" ($out -split "`n" | Select-Object -Last 5) | ForEach-Object { Write-Host " $_" } } } # Summary ------------------------------------------------------------------------ Write-Host "" Write-Host ("result: {0} passed, {1} failed, {2} skipped" -f $script:pass, $script:fail, $script:skip) if ($script:fail -gt 0) { Write-Host "VERDICT: FAIL - do not use this artifact."; exit 1 } if ($script:pass -eq 0) { Write-Host "VERDICT: NOTHING PROVEN - no check could run."; exit 1 } if ($script:skip -gt 0) { Write-Host "VERDICT: PASS (with $($script:skip) check(s) skipped - integrity shown; see SKIP lines for what was not proven)" } else { Write-Host "VERDICT: PASS - integrity, authenticity and build provenance all verified." } exit 0