2.10.7: changelog; release.ps1 signs releases

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 18:55:04 +02:00
co-authored by Claude Opus 5.5
parent a499577b7c
commit 77c51b52c5
3 changed files with 71 additions and 4 deletions
+60 -3
View File
@@ -6,11 +6,18 @@
# %AppData%\MSCO\release-token.dpapi (Windows DPAPI, nur dein Windows-Konto kann ihn lesen - anlegen mit
# "release.ps1 -SaveToken"), sonst als Umgebungsvariable MSCO_RELEASE_TOKEN. Ohne Token wird nichts hochgeladen.
# MSCO_NO_RELEASE=1 schaltet das Hochladen ab (Test-Builds).
#
# Signatur: jede Datei wird mit dem privaten Release-Schlüssel signiert (RSA-3072, SHA-256) - der Launcher und das Setup
# installieren nur, was zu ihrem eingebauten öffentlichen Schlüssel passt. Selbst wer den Keymaster oder den Token
# übernimmt, kann so keine fremde Datei als MSCO-Update verteilen. Der Schlüssel liegt DPAPI-verschlüsselt in
# %AppData%\MSCO\release-signing-key.dpapi (einmal anlegen: "release.ps1 -NewSigningKey"; SICHERN: "-ExportSigningKey").
# Gibt es die Version auf dem Keymaster schon, passiert nichts - also einfach die Version erhöhen und bauen.
param(
[ValidateSet("mod", "launcher", "installer")][string]$Product,
[string]$File,
[switch]$SaveToken,
[switch]$NewSigningKey,
[string]$ExportSigningKey = "",
[string]$Version = "",
[string]$Changelog = ""
)
@@ -36,7 +43,39 @@ if ($SaveToken) {
Write-Host "Release token saved encrypted in $tokenDpapi"
exit 0
}
if (-not $Product -or -not $File) { Write-Host "Usage: release.ps1 -Product mod|launcher|installer -File <file> | release.ps1 -SaveToken"; exit 1 }
$keyEntropy = [Text.Encoding]::UTF8.GetBytes("MSCO release signing key v1")
$keyFile = Join-Path $env:APPDATA "MSCO\release-signing-key.dpapi"
# Neues Schlüsselpaar (nur einmal - der öffentliche Schlüssel ist in Launcher und Setup eingebaut)
if ($NewSigningKey) {
if (Test-Path $keyFile) { Write-Host "There already is a signing key ($keyFile) - not replaced"; exit 1 }
$rsa = New-Object System.Security.Cryptography.RSACng 3072
$private = $rsa.ToXmlString($true)
New-Item -ItemType Directory -Force (Split-Path $keyFile) | Out-Null
[IO.File]::WriteAllBytes($keyFile, [Security.Cryptography.ProtectedData]::Protect([Text.Encoding]::UTF8.GetBytes($private), $keyEntropy, $scope))
Write-Host "Signing key saved encrypted in $keyFile"
Write-Host "PUBLIC KEY (put into Launcher/Setup):"
Write-Host $rsa.ToXmlString($false)
exit 0
}
# Sicherung des privaten Schlüssels mit eigenem Passwort (AES-256, PBKDF2) - z.B. für einen USB-Stick/Passwort-Manager
if ($ExportSigningKey) {
if (-not (Test-Path $keyFile)) { Write-Host "No signing key"; exit 1 }
$private = [Security.Cryptography.ProtectedData]::Unprotect([IO.File]::ReadAllBytes($keyFile), $keyEntropy, $scope)
$pw = Read-Host "Password for the backup" -AsSecureString
$plainPw = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($pw))
if ($plainPw.Length -lt 12) { Write-Host "Use at least 12 characters"; exit 1 }
$salt = New-Object byte[] 16; (New-Object Security.Cryptography.RNGCryptoServiceProvider).GetBytes($salt)
$kdf = New-Object Security.Cryptography.Rfc2898DeriveBytes($plainPw, $salt, 300000, [Security.Cryptography.HashAlgorithmName]::SHA256)
$aes = [Security.Cryptography.Aes]::Create(); $aes.Key = $kdf.GetBytes(32); $aes.GenerateIV()
$enc = $aes.CreateEncryptor().TransformFinalBlock($private, 0, $private.Length)
$out = "MSCO-SIGNING-KEY-BACKUP v1`n" + [Convert]::ToBase64String($salt) + "`n" + [Convert]::ToBase64String($aes.IV) + "`n" + [Convert]::ToBase64String($enc)
[IO.File]::WriteAllText($ExportSigningKey, $out)
Write-Host "Backup written to $ExportSigningKey (AES-256, PBKDF2-SHA256 300000) - keep it and the password safe"
exit 0
}
if (-not $Product -or -not $File) { Write-Host "Usage: release.ps1 -Product mod|launcher|installer -File <file> | -SaveToken | -NewSigningKey | -ExportSigningKey <file>"; exit 1 }
if ($env:MSCO_NO_RELEASE -eq "1") { Write-Host "MSCO release: MSCO_NO_RELEASE=1 - not uploading."; exit 0 }
$token = $env:MSCO_RELEASE_TOKEN
@@ -82,6 +121,25 @@ if (-not $Changelog) {
}
}
# Signieren: "MSCO-RELEASE|<produkt>|<version>|<sha256>" - so passt die Signatur nur zu genau dieser Datei, diesem
# Produkt und dieser Version (keine alte Version als neue, keine Mod-DLL als Launcher)
$bytes = [System.IO.File]::ReadAllBytes((Resolve-Path $File))
$sha = [BitConverter]::ToString([Security.Cryptography.SHA256]::Create().ComputeHash($bytes)).Replace("-", "").ToLowerInvariant()
$signature = ""
if (Test-Path $keyFile) {
try {
$private = [Text.Encoding]::UTF8.GetString([Security.Cryptography.ProtectedData]::Unprotect([IO.File]::ReadAllBytes($keyFile), $keyEntropy, $scope))
$rsa = New-Object System.Security.Cryptography.RSACng
$rsa.FromXmlString($private)
$message = [Text.Encoding]::UTF8.GetBytes("MSCO-RELEASE|$Product|$Version|$sha")
$signature = [Convert]::ToBase64String($rsa.SignData($message, [Security.Cryptography.HashAlgorithmName]::SHA256, [Security.Cryptography.RSASignaturePadding]::Pkcs1))
$private = $null
} catch { Write-Host "MSCO release: signing failed ($($_.Exception.Message)) - not uploading"; exit 0 }
} else {
Write-Host "MSCO release: no signing key ($keyFile) - the launcher will refuse this release. Not uploading."
exit 0
}
Add-Type -AssemblyName System.Net.Http
$client = New-Object System.Net.Http.HttpClient
$client.Timeout = [TimeSpan]::FromMinutes(10)
@@ -92,10 +150,9 @@ $boundary = "----MSCO" + [Guid]::NewGuid().ToString("N")
$utf8 = New-Object System.Text.UTF8Encoding($false)
$body = New-Object System.IO.MemoryStream
function Write-Text([string]$text) { $b = $utf8.GetBytes($text); $body.Write($b, 0, $b.Length) }
foreach ($field in @(@("product", $Product), @("version", $Version), @("changelog", $Changelog))) {
foreach ($field in @(@("product", $Product), @("version", $Version), @("changelog", $Changelog), @("signature", $signature))) {
Write-Text "--$boundary`r`nContent-Disposition: form-data; name=`"$($field[0])`"`r`nContent-Type: text/plain; charset=utf-8`r`n`r`n$($field[1])`r`n"
}
$bytes = [System.IO.File]::ReadAllBytes((Resolve-Path $File))
Write-Text "--$boundary`r`nContent-Disposition: form-data; name=`"file`"; filename=`"$([System.IO.Path]::GetFileName($File))`"`r`nContent-Type: application/octet-stream`r`n`r`n"
$body.Write($bytes, 0, $bytes.Length)
Write-Text "`r`n--$boundary--`r`n"