Files
PSG-Conduit/scripts/sign-manifest.ps1

123 lines
4.8 KiB
PowerShell

<#
.SYNOPSIS
Sign (or re-sign) manifests/apps.json and produce apps.json.sig.
.DESCRIPTION
Reads the private key from either:
-KeyPath (a PEM file path, default .\keys\manifest-private.pem)
or the env var MANIFEST_SIGNING_KEY (base64-encoded DER private key, used in CI)
Produces manifests/apps.json.sig — a file containing the base64-encoded
ed25519 signature of the raw apps.json bytes.
This detached-signature approach means the JSON is never modified and there
are no canonicalisation issues.
.EXAMPLE
# Local development
.\scripts\sign-manifest.ps1
# CI (key stored as base64 DER in an env var)
$env:MANIFEST_SIGNING_KEY = "<base64-der-private-key>"
.\scripts\sign-manifest.ps1
#>
param(
[string]$KeyPath = ".\keys\manifest-private.pem",
[string]$ManifestPath = ".\manifests\apps.json",
[string]$SigPath = ".\manifests\apps.json.sig"
)
$ErrorActionPreference = "Stop"
# ── Locate OpenSSL (probes common Windows install paths if not on PATH) ───────
. "$PSScriptRoot\_openssl.ps1"
# ── Resolve all paths to absolute using PowerShell's CWD ─────────────────────
# [IO.File] uses .NET's Environment.CurrentDirectory, which differs from
# PowerShell's $PWD when the session was started from another location.
# GetUnresolvedProviderPathFromPSPath works even for paths that don't exist yet.
$ManifestPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($ManifestPath)
$SigPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($SigPath)
$KeyPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($KeyPath)
# ── Resolve private key ──────────────────────────────────────────────────────
$tempKeyFile = $null
if ($env:MANIFEST_SIGNING_KEY) {
Write-Host "Using MANIFEST_SIGNING_KEY from environment" -ForegroundColor Cyan
# CI provides the PEM content as an env var
$tempKeyFile = Join-Path $env:TEMP "psg-sign-key-$([System.Guid]::NewGuid()).pem"
[IO.File]::WriteAllText($tempKeyFile, $env:MANIFEST_SIGNING_KEY)
$resolvedKey = $tempKeyFile
} elseif (Test-Path $KeyPath) {
Write-Host "Using key file: $KeyPath" -ForegroundColor Cyan
$resolvedKey = Resolve-Path $KeyPath
} else {
Write-Error "No signing key found.`nRun scripts/keygen.ps1 first, or set the MANIFEST_SIGNING_KEY env var."
exit 1
}
try {
# ── Sign ─────────────────────────────────────────────────────────────────
$tempSig = Join-Path $env:TEMP "psg-manifest-sig-$([System.Guid]::NewGuid()).bin"
Write-Host "Signing $ManifestPath..." -ForegroundColor Yellow
& openssl pkeyutl `
-sign `
-inkey $resolvedKey `
-rawin `
-in (Resolve-Path $ManifestPath) `
-out $tempSig
if ($LASTEXITCODE -ne 0) {
Write-Error "OpenSSL signing failed (exit $LASTEXITCODE)"
exit 1
}
# Base64-encode the raw signature bytes
$sigBytes = [IO.File]::ReadAllBytes($tempSig)
$sigB64 = [Convert]::ToBase64String($sigBytes)
[IO.File]::WriteAllText($SigPath, $sigB64)
Remove-Item $tempSig -Force
Write-Host "Signature written to $SigPath" -ForegroundColor Green
Write-Host "Sig (base64): $sigB64" -ForegroundColor Gray
} finally {
# Clean up temp key file if we created one
if ($tempKeyFile -and (Test-Path $tempKeyFile)) {
Remove-Item $tempKeyFile -Force
}
}
# ── Verify round-trip ────────────────────────────────────────────────────────
Write-Host "`nVerifying round-trip..." -ForegroundColor Yellow
$tempVerifySig = Join-Path $env:TEMP "psg-verify-sig-$([System.Guid]::NewGuid()).bin"
$sigBytesBack = [Convert]::FromBase64String($sigB64)
[IO.File]::WriteAllBytes($tempVerifySig, $sigBytesBack)
# Use the public key for verification
$pubKeyPath = $KeyPath -replace '-private\.pem$', '-public.pem'
if (-not (Test-Path $pubKeyPath)) {
Write-Warning "Public key not found at $pubKeyPath — skipping verify step"
} else {
& openssl pkeyutl `
-verify `
-inkey $pubKeyPath `
-pubin `
-rawin `
-in (Resolve-Path $ManifestPath) `
-sigfile $tempVerifySig
if ($LASTEXITCODE -eq 0) {
Write-Host "Round-trip verification passed ✓" -ForegroundColor Green
} else {
Write-Error "Round-trip verification FAILED — signature is corrupt!"
}
}
Remove-Item $tempVerifySig -Force -ErrorAction SilentlyContinue