feat: initial PSG Launcher scaffold
This commit is contained in:
54
scripts/_openssl.ps1
Normal file
54
scripts/_openssl.ps1
Normal 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
66
scripts/keygen.ps1
Normal 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
122
scripts/sign-manifest.ps1
Normal 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
75
scripts/sign-package.ps1
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user