feat: initial PSG Launcher scaffold

This commit is contained in:
2026-05-27 09:01:09 +08:00
commit 2c9b591c52
110 changed files with 11150 additions and 0 deletions

54
scripts/_openssl.ps1 Normal file
View File

@@ -0,0 +1,54 @@
<#
.SYNOPSIS
Shared helper — locate OpenSSL and add its bin dir to the session PATH.
Dot-source this at the top of any script that needs openssl:
. "$PSScriptRoot\_openssl.ps1"
#>
function Find-OpenSSL {
# 1. Already on PATH?
if (Get-Command openssl -ErrorAction SilentlyContinue) {
return (Get-Command openssl).Source
}
# 2. Probe well-known Windows install locations
$candidates = @(
"$env:ProgramFiles\OpenSSL-Win64\bin\openssl.exe"
"$env:ProgramFiles\OpenSSL\bin\openssl.exe"
"${env:ProgramFiles(x86)}\OpenSSL-Win32\bin\openssl.exe"
"C:\Program Files\OpenSSL-Win64\bin\openssl.exe"
"C:\Program Files\OpenSSL\bin\openssl.exe"
"C:\tools\OpenSSL\bin\openssl.exe"
"$env:ChocolateyInstall\bin\openssl.exe"
"$env:ProgramFiles\Git\usr\bin\openssl.exe"
"$env:LocalAppData\Programs\Git\usr\bin\openssl.exe"
)
foreach ($candidate in $candidates) {
if (Test-Path $candidate) {
# Add the bin directory to the session PATH so subsequent
# `& openssl ...` calls work without a full path.
$binDir = Split-Path $candidate
$env:PATH = "$binDir;$env:PATH"
Write-Host "Found OpenSSL at: $candidate" -ForegroundColor Cyan
return $candidate
}
}
# 3. Nothing found — give actionable guidance
Write-Error @"
OpenSSL not found in PATH or any of the standard install locations.
If it is installed somewhere else, add its bin directory to your PATH:
`$env:PATH = 'C:\Path\To\OpenSSL\bin;' + `$env:PATH
Otherwise install it:
winget install ShiningLight.OpenSSL.Light
# then restart this terminal
"@
exit 1
}
# Run on dot-source — sets `$openssl` in the caller's scope
$openssl = Find-OpenSSL
Write-Host "OpenSSL ready: $(& $openssl version)" -ForegroundColor DarkGray

66
scripts/keygen.ps1 Normal file
View File

@@ -0,0 +1,66 @@
<#
.SYNOPSIS
Generate the ed25519 keypair used to sign the apps manifest and app packages.
.DESCRIPTION
Produces two files in .\keys\ :
manifest-private.pem — KEEP SECRET. Never commit, never put on the server.
manifest-public.pem — Safe to commit; goes in src-tauri/src/config.rs.
Also prints the raw 32-byte public key as base64 (the value for config.rs).
.NOTES
Requires OpenSSL 3.x. Install via: winget install ShiningLight.OpenSSL.Light
Verify: openssl version
#>
param(
[string]$OutputDir = ".\keys"
)
$ErrorActionPreference = "Stop"
# ── Locate OpenSSL (probes common Windows install paths if not on PATH) ───────
. "$PSScriptRoot\_openssl.ps1"
New-Item -ItemType Directory -Force -Path $OutputDir | Out-Null
$privPath = Join-Path $OutputDir "manifest-private.pem"
$pubPath = Join-Path $OutputDir "manifest-public.pem"
# ── Generate ed25519 keypair ─────────────────────────────────────────────────
Write-Host "`nGenerating Ed25519 keypair..." -ForegroundColor Yellow
& openssl genpkey -algorithm ed25519 -out $privPath
& openssl pkey -in $privPath -pubout -out $pubPath
# ── Extract raw 32-byte public key as base64 ─────────────────────────────────
# OpenSSL DER-encodes the public key as SubjectPublicKeyInfo (SPKI).
# Ed25519 SPKI = 12-byte header + 32-byte raw key = 44 bytes total.
# Write to a temp file so we can read the raw bytes.
$tempDer = Join-Path $env:TEMP "psg-pubkey-$([System.Guid]::NewGuid()).der"
& openssl pkey -in $privPath -pubout -outform DER -out $tempDer
$derBytes = [IO.File]::ReadAllBytes($tempDer)
Remove-Item $tempDer -Force
# Skip the 12-byte SPKI header to get the raw 32-byte key
$rawKeyBytes = $derBytes[12..43]
$rawKeyB64 = [Convert]::ToBase64String($rawKeyBytes)
# ── Output ───────────────────────────────────────────────────────────────────
Write-Host ""
Write-Host "Keys written to $OutputDir" -ForegroundColor Green
Write-Host " Private : $privPath ← KEEP SECRET, never commit" -ForegroundColor Red
Write-Host " Public : $pubPath"
Write-Host ""
Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Yellow
Write-Host "Paste this value into src-tauri/src/config.rs as MANIFEST_PUBLIC_KEY_B64:" -ForegroundColor Yellow
Write-Host ""
Write-Host " $rawKeyB64" -ForegroundColor Cyan
Write-Host ""
Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Yellow
Write-Host ""
Write-Host "Next steps:"
Write-Host " 1. Copy the base64 value above into config.rs"
Write-Host " 2. Store manifest-private.pem as the Gitea CI secret MANIFEST_SIGNING_KEY"
Write-Host " 3. Run 'npm run tauri signer generate' for the Tauri self-updater key"
Write-Host " and store that in CI as TAURI_SIGNING_PRIVATE_KEY"

122
scripts/sign-manifest.ps1 Normal file
View File

@@ -0,0 +1,122 @@
<#
.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

75
scripts/sign-package.ps1 Normal file
View File

@@ -0,0 +1,75 @@
<#
.SYNOPSIS
Sign a single app package file and output its hash + signature for the manifest.
.DESCRIPTION
Run this once per app package you want to add to apps.json.
Copy the output values into the appropriate platforms entry in manifests/apps.json,
then re-run sign-manifest.ps1 to re-sign the updated manifest.
.EXAMPLE
.\scripts\sign-package.ps1 -PackagePath .\dist\my-app.exe
#>
param(
[Parameter(Mandatory)]
[string]$PackagePath,
[string]$KeyPath = ".\keys\manifest-private.pem"
)
$ErrorActionPreference = "Stop"
# ── Locate OpenSSL (probes common Windows install paths if not on PATH) ───────
. "$PSScriptRoot\_openssl.ps1"
# Resolve to absolute paths so .NET IO methods use the correct CWD
$PackagePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($PackagePath)
$KeyPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($KeyPath)
$resolvedPkg = Resolve-Path $PackagePath
$resolvedKey = $null
$tempKeyFile = $null
if ($env:MANIFEST_SIGNING_KEY) {
$tempKeyFile = Join-Path $env:TEMP "psg-sign-key-$([System.Guid]::NewGuid()).pem"
[IO.File]::WriteAllText($tempKeyFile, $env:MANIFEST_SIGNING_KEY)
$resolvedKey = $tempKeyFile
} else {
$resolvedKey = Resolve-Path $KeyPath
}
try {
# SHA-256 hash
$hashObj = Get-FileHash -Path $resolvedPkg -Algorithm SHA256
$hash = $hashObj.Hash.ToLower()
# File size
$sizeBytes = (Get-Item $resolvedPkg).Length
# ed25519 signature
$tempSig = Join-Path $env:TEMP "psg-pkg-sig-$([System.Guid]::NewGuid()).bin"
& openssl pkeyutl -sign -inkey $resolvedKey -rawin -in $resolvedPkg -out $tempSig
if ($LASTEXITCODE -ne 0) { throw "openssl signing failed" }
$sigB64 = [Convert]::ToBase64String([IO.File]::ReadAllBytes($tempSig))
Remove-Item $tempSig -Force
Write-Host ""
Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Yellow
Write-Host "Add this to the relevant platforms entry in manifests/apps.json:" -ForegroundColor Yellow
Write-Host ""
Write-Host @"
"hash_sha256": "$hash",
"size_bytes": $sizeBytes,
"signature": "$sigB64"
"@ -ForegroundColor Cyan
Write-Host ""
Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Yellow
Write-Host "Then re-run: .\scripts\sign-manifest.ps1"
} finally {
if ($tempKeyFile -and (Test-Path $tempKeyFile)) {
Remove-Item $tempKeyFile -Force
}
}