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
+10
View File
@@ -1,5 +1,15 @@
# Changelog # Changelog
## 2.10.7
**Sicherheit**
- Daten von Servern und Mitspielern werden auf gültige Längen geprüft – eine gefälschte Längenangabe ließ das Spiel
vorher mit „Out of Memory“ abstürzen
- Mitspieler können nicht mehr beliebige Spiel-Zustände bei anderen auslösen (z.B. am eigenen Spieler oder an
Systemen wie Tod/Speichern) – nur noch Klicks auf klickbare Dinge und echte Schraub-/Montage-Schritte
- Uhrzeit/Wetter und Meldungen im Stil von Server-/Admin-Nachrichten („* …“) nur noch vom Admin bzw. Host
- Release ist mit dem MSCO-Release-Schlüssel signiert
## 2.10.6 ## 2.10.6
- Neuer Konsolenbefehl **oilcheck**: zeigt, woher Öl ausläuft – die Leck-Werte des Motors (Ölwanne, Ölfilter, - Neuer Konsolenbefehl **oilcheck**: zeigt, woher Öl ausläuft – die Leck-Werte des Motors (Ölwanne, Ölfilter,
+1 -1
View File
@@ -11,7 +11,7 @@ namespace MSCO
/// Version der Mod. Kleine Updates: nur die dritte Stelle erhöhen (2.9.1, 2.9.2 ...) - sie bleiben mit /// Version der Mod. Kleine Updates: nur die dritte Stelle erhöhen (2.9.1, 2.9.2 ...) - sie bleiben mit
/// Servern und Mitspielern derselben ersten zwei Stellen kompatibel. /// Servern und Mitspielern derselben ersten zwei Stellen kompatibel.
/// </summary> /// </summary>
public const string ModVersion = "2.10.6"; public const string ModVersion = "2.10.7";
/// <summary> /// <summary>
/// Netzwerk-Version (die ersten beiden Stellen). Nur erhöhen, wenn sich das Protokoll inkompatibel ändert - dann /// Netzwerk-Version (die ersten beiden Stellen). Nur erhöhen, wenn sich das Protokoll inkompatibel ändert - dann
/// auch GameServer.ModVersion im Server. /// auch GameServer.ModVersion im Server.
+60 -3
View File
@@ -6,11 +6,18 @@
# %AppData%\MSCO\release-token.dpapi (Windows DPAPI, nur dein Windows-Konto kann ihn lesen - anlegen mit # %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. # "release.ps1 -SaveToken"), sonst als Umgebungsvariable MSCO_RELEASE_TOKEN. Ohne Token wird nichts hochgeladen.
# MSCO_NO_RELEASE=1 schaltet das Hochladen ab (Test-Builds). # 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. # Gibt es die Version auf dem Keymaster schon, passiert nichts - also einfach die Version erhöhen und bauen.
param( param(
[ValidateSet("mod", "launcher", "installer")][string]$Product, [ValidateSet("mod", "launcher", "installer")][string]$Product,
[string]$File, [string]$File,
[switch]$SaveToken, [switch]$SaveToken,
[switch]$NewSigningKey,
[string]$ExportSigningKey = "",
[string]$Version = "", [string]$Version = "",
[string]$Changelog = "" [string]$Changelog = ""
) )
@@ -36,7 +43,39 @@ if ($SaveToken) {
Write-Host "Release token saved encrypted in $tokenDpapi" Write-Host "Release token saved encrypted in $tokenDpapi"
exit 0 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 } if ($env:MSCO_NO_RELEASE -eq "1") { Write-Host "MSCO release: MSCO_NO_RELEASE=1 - not uploading."; exit 0 }
$token = $env:MSCO_RELEASE_TOKEN $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 Add-Type -AssemblyName System.Net.Http
$client = New-Object System.Net.Http.HttpClient $client = New-Object System.Net.Http.HttpClient
$client.Timeout = [TimeSpan]::FromMinutes(10) $client.Timeout = [TimeSpan]::FromMinutes(10)
@@ -92,10 +150,9 @@ $boundary = "----MSCO" + [Guid]::NewGuid().ToString("N")
$utf8 = New-Object System.Text.UTF8Encoding($false) $utf8 = New-Object System.Text.UTF8Encoding($false)
$body = New-Object System.IO.MemoryStream $body = New-Object System.IO.MemoryStream
function Write-Text([string]$text) { $b = $utf8.GetBytes($text); $body.Write($b, 0, $b.Length) } 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" 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" 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) $body.Write($bytes, 0, $bytes.Length)
Write-Text "`r`n--$boundary--`r`n" Write-Text "`r`n--$boundary--`r`n"