podup 0.21.0

Translate and run docker-compose files on rootless Podman
Documentation
#Requires -Version 5.1
#
# podup installer for Windows — downloads a release binary, verifies it and
# installs it.
#
# Usage:
#   irm https://glyndor.net/install/podup.ps1 | iex
#
# Environment:
#   PODUP_VERSION              Release tag to install (e.g. v0.3.0). Default: latest.
#   PODUP_INSTALL_DIR          Installation directory. Default: %LOCALAPPDATA%\Programs\podup.
#   PODUP_RELEASE_PUBKEY_B64   Override the baked-in Ed25519 release public key (for forks).
#   PODUP_INSECURE_SKIP_VERIFY Set to 1 to accept checksum-only verification.

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
# PowerShell 7.3+ turns a non-zero native exit into a terminating error under
# ErrorActionPreference='Stop'. We branch on $LASTEXITCODE ourselves (a failed
# signature check is expected control flow, not a fatal error), so opt out.
# Harmless no-op on Windows PowerShell 5.1, which lacks this variable.
$PSNativeCommandUseErrorActionPreference = $false

$Repo = 'Glyndor/podup'
$Version = if ($env:PODUP_VERSION) { $env:PODUP_VERSION } else { 'latest' }
$InstallDir = if ($env:PODUP_INSTALL_DIR) { $env:PODUP_INSTALL_DIR } else { Join-Path $env:LOCALAPPDATA 'Programs\podup' }

function Write-LogInfo($msg)  { Write-Host "[info] $msg" -ForegroundColor Blue }
function Write-LogOk($msg)    { Write-Host "[ ok ] $msg" -ForegroundColor Green }
function Write-LogError($msg) { Write-Host "[fail] $msg" -ForegroundColor Red }
function Fail($msg) { Write-LogError $msg; exit 1 }

# --- Platform detection ------------------------------------------------------

$osArch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture
switch ($osArch) {
	'X64'   { $Arch = 'x86_64' }
	'Arm64' { $Arch = 'arm64' }
	default { Fail "Unsupported architecture: $osArch (supported: x86_64, arm64)" }
}

$Artifact = "podup-windows-$Arch.exe"

# --- Resolve download URL ----------------------------------------------------

if ($Version -eq 'latest') {
	$BaseUrl = "https://github.com/$Repo/releases/latest/download"
} elseif ($Version -match '^v[0-9]+\.[0-9]+\.[0-9]+$') {
	$BaseUrl = "https://github.com/$Repo/releases/download/$Version"
} else {
	Fail "PODUP_VERSION must be 'latest' or a semver tag like v1.2.3, got: $Version"
}

# Windows PowerShell 5.1 defaults to TLS 1.0/1.1; force TLS 1.2 for GitHub.
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

$TmpDir = New-Item -ItemType Directory -Path (Join-Path ([System.IO.Path]::GetTempPath()) ([System.IO.Path]::GetRandomFileName()))

try {
	# --- Download ------------------------------------------------------------

	function Get-ReleaseFile($name) {
		$dest = Join-Path $TmpDir $name
		$url = "$BaseUrl/$name"
		try {
			Invoke-WebRequest -Uri $url -OutFile $dest -UseBasicParsing
		} catch {
			Fail "Download failed: $url"
		}
		return $dest
	}

	Write-LogInfo "Downloading $Artifact ($Version) ..."
	$artifactPath = Get-ReleaseFile $Artifact
	$sumsPath = Get-ReleaseFile 'SHA256SUMS'
	$sigPath  = Get-ReleaseFile 'SHA256SUMS.sig'

	# --- Verify --------------------------------------------------------------

	# Checksum alone is not a trust anchor: a tampered release can ship a matching
	# SHA256SUMS. The binary is trusted only after at least one cryptographic proof
	# tied to the release key or the repository's build identity succeeds — the
	# Ed25519 signature over SHA256SUMS, or the GitHub build-provenance attestation.
	# If neither verifier can run, the install fails closed. Set
	# PODUP_INSECURE_SKIP_VERIFY=1 to explicitly opt out (checksum only).

	# Baked-in base64 (unpadded) raw Ed25519 public keys (32 bytes each) matching
	# the release signing key (RELEASE_SIGN_KEY). Up to two are accepted: the
	# second is empty except during a key rotation, when it holds the new key so a
	# release signed by either key verifies. The signature passes if any key
	# validates. Override for a fork via PODUP_RELEASE_PUBKEY_B64 / _PUBKEY2_B64.
	$PubKeyB64  = if ($env:PODUP_RELEASE_PUBKEY_B64) { $env:PODUP_RELEASE_PUBKEY_B64 } else { 'APh+kh61dJeT0HzG+KQXELzDjK4ccvqY9K+FptOZ3+Y' }
	$PubKey2B64 = if ($env:PODUP_RELEASE_PUBKEY2_B64) { $env:PODUP_RELEASE_PUBKEY2_B64 } else { '' }
	$PubKeys = @($PubKeyB64, $PubKey2B64 | Where-Object { $_ })

	$verified = $false

	# Locate a python interpreter that has the 'cryptography' package. Each
	# candidate carries any leading args (the 'py' launcher needs '-3').
	function Find-Python {
		$candidates = @(
			@{ Exe = 'python3'; Pre = @() },
			@{ Exe = 'python';  Pre = @() },
			@{ Exe = 'py';      Pre = @('-3') }
		)
		foreach ($c in $candidates) {
			if (-not (Get-Command $c.Exe -ErrorAction SilentlyContinue)) { continue }
			$probeArgs = $c.Pre + @('-c', 'from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey')
			& $c.Exe @probeArgs 2>$null
			if ($LASTEXITCODE -eq 0) { return $c }
		}
		return $null
	}

	Write-LogInfo 'Verifying SHA256SUMS signature ...'
	if ($PubKeys.Count -gt 0) {
		$python = Find-Python
		if ($python) {
			$pyScript = Join-Path $TmpDir 'verify_ed25519.py'
			# Python source — indentation is significant, keep as-is.
			$pySource = @'
import base64, sys
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from cryptography.exceptions import InvalidSignature
sig_file, data_file = sys.argv[1], sys.argv[2]
sig = open(sig_file, "rb").read()
data = open(data_file, "rb").read()
for pubkey_b64 in sys.argv[3:]:
    try:
        Ed25519PublicKey.from_public_bytes(base64.b64decode(pubkey_b64 + "==")).verify(sig, data)
        sys.exit(0)
    except (InvalidSignature, ValueError):
        continue
sys.exit(1)
'@
			Set-Content -Path $pyScript -Value $pySource -Encoding ASCII
			$pyArgs = $python.Pre + @($pyScript, $sigPath, $sumsPath) + $PubKeys
			& $python.Exe @pyArgs
			if ($LASTEXITCODE -eq 0) {
				Write-LogOk 'SHA256SUMS signature verified'
				$verified = $true
			} else {
				Fail 'SHA256SUMS signature verification failed — release may be tampered'
			}
		} else {
			Write-LogInfo 'python3+cryptography not available — cannot check Ed25519 signature'
		}
	} else {
		Write-LogInfo 'no release public key configured — skipping Ed25519 signature check'
	}

	# Build-provenance attestation: proves the binary was produced by this repo's
	# release workflow (GitHub OIDC). Strong even without the release public key.
	$ghAttestation = $false
	if (Get-Command gh -ErrorAction SilentlyContinue) {
		& gh attestation --help *> $null
		if ($LASTEXITCODE -eq 0) { $ghAttestation = $true }
	}
	if ($ghAttestation) {
		Write-LogInfo 'Verifying artifact attestation ...'
		& gh attestation verify $artifactPath --repo $Repo | Out-Null
		if ($LASTEXITCODE -ne 0) { Fail "Attestation verification failed for $Artifact" }
		Write-LogOk 'Attestation verified'
		$verified = $true
	} else {
		Write-LogInfo 'GitHub CLI with attestation support not found — cannot check attestation'
	}

	# Fail closed unless a strong proof succeeded or the user explicitly opts out.
	if (-not $verified) {
		if ($env:PODUP_INSECURE_SKIP_VERIFY -eq '1') {
			Write-LogInfo 'PODUP_INSECURE_SKIP_VERIFY=1 — proceeding with checksum verification only'
		} else {
			Fail "No signature or attestation verifier available. Install 'gh' (>= 2.49) or python3 with the 'cryptography' package, set PODUP_RELEASE_PUBKEY_B64, or re-run with PODUP_INSECURE_SKIP_VERIFY=1 to accept checksum-only verification."
		}
	}

	Write-LogInfo 'Verifying SHA-256 checksum ...'
	$expectedLine = Select-String -Path $sumsPath -Pattern ("\s" + [regex]::Escape($Artifact) + "$") | Select-Object -First 1
	if (-not $expectedLine) { Fail "No checksum entry for $Artifact in SHA256SUMS" }
	$expected = ($expectedLine.Line -split '\s+')[0].ToLower()
	$actual = (Get-FileHash -Path $artifactPath -Algorithm SHA256).Hash.ToLower()
	if ($expected -ne $actual) { Fail "Checksum verification failed for $Artifact" }
	Write-LogOk 'Checksum verified'

	# --- Install -------------------------------------------------------------

	if (-not (Test-Path $InstallDir)) {
		New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
	}
	$target = Join-Path $InstallDir 'podup.exe'
	Copy-Item -Path $artifactPath -Destination $target -Force

	# Add the install dir to the user PATH if it is not already there.
	$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
	$onPath = ($userPath -split ';') -contains $InstallDir
	if (-not $onPath) {
		$newPath = if ([string]::IsNullOrEmpty($userPath)) { $InstallDir } else { "$userPath;$InstallDir" }
		[Environment]::SetEnvironmentVariable('Path', $newPath, 'User')
		$env:Path = "$env:Path;$InstallDir"
		Write-LogInfo "Added $InstallDir to your user PATH (restart your shell to pick it up)"
	}

	$installed = & $target --version
	Write-LogOk "podup installed: $installed"
} finally {
	Remove-Item -Path $TmpDir -Recurse -Force -ErrorAction SilentlyContinue
}