launcher: refuse to launch on invalid manifest signature
The previous flow threw InvalidOperationException on signature
failure, which was caught by the outer catch(Exception) and silently
fell through to the offline launch branch — the exact bypass the
design doc forbids ('server is lying' is more dangerous than 'server
is down'). Introduce ManifestSignatureException and handle it in a
dedicated catch above the generic fallback, exiting with code 2
without launching the game.
This commit is contained in:
15
src/Metin2Launcher/Manifest/ManifestSignatureException.cs
Normal file
15
src/Metin2Launcher/Manifest/ManifestSignatureException.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace Metin2Launcher.Manifest;
|
||||
|
||||
/// <summary>
|
||||
/// Thrown when the update manifest's Ed25519 signature does not verify.
|
||||
///
|
||||
/// This is the one failure mode where the launcher must NOT fall back to an offline
|
||||
/// launch: a signature failure means something between us and the trusted signing key
|
||||
/// is lying, and silently launching the old client would mask an active attack. The
|
||||
/// entry point catches this distinctly from generic update failures and refuses to
|
||||
/// start the game.
|
||||
/// </summary>
|
||||
public sealed class ManifestSignatureException : Exception
|
||||
{
|
||||
public ManifestSignatureException(string message) : base(message) { }
|
||||
}
|
||||
@@ -33,6 +33,13 @@ public static class Program
|
||||
{
|
||||
updatedOk = await RunUpdateAsync(clientRoot, stagingDir, ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (ManifestSignatureException ex)
|
||||
{
|
||||
// Signature failure is the one case where we do NOT fall back silently.
|
||||
// Something between us and the trusted key is lying; refuse to launch.
|
||||
Log.Error("manifest signature verification FAILED — refusing to launch", ex);
|
||||
return 2;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Log.Warn("update cancelled / timed out");
|
||||
@@ -82,9 +89,8 @@ public static class Program
|
||||
|
||||
if (!SignatureVerifier.Verify(loaded.RawBytes, loaded.Signature, LauncherConfig.PublicKeyHex))
|
||||
{
|
||||
// Signature failure is the one case where we do NOT fall back silently.
|
||||
Log.Error("manifest signature verification FAILED — refusing to launch");
|
||||
throw new InvalidOperationException("manifest signature invalid");
|
||||
throw new ManifestSignatureException(
|
||||
"Ed25519 signature over manifest.json did not verify against the launcher's public key");
|
||||
}
|
||||
|
||||
var manifest = loaded.Manifest;
|
||||
@@ -99,8 +105,24 @@ public static class Program
|
||||
foreach (var file in applicable)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
var finalPath = Path.Combine(clientRoot, file.Path.Replace('/', Path.DirectorySeparatorChar));
|
||||
var stagingPath = Path.Combine(stagingDir, file.Path.Replace('/', Path.DirectorySeparatorChar));
|
||||
|
||||
string finalPath;
|
||||
string stagingPath;
|
||||
try
|
||||
{
|
||||
finalPath = PathSafety.ResolveInside(clientRoot, file.Path);
|
||||
stagingPath = PathSafety.ResolveInside(stagingDir, file.Path);
|
||||
}
|
||||
catch (UnsafePathException ex)
|
||||
{
|
||||
if (file.IsRequired)
|
||||
{
|
||||
Log.Error($"required file rejected by path safety: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
Log.Warn($"optional file rejected by path safety: {ex.Message}");
|
||||
continue;
|
||||
}
|
||||
|
||||
var localHash = FileHasher.HashFileOrNull(finalPath);
|
||||
if (localHash == file.Sha256)
|
||||
|
||||
Reference in New Issue
Block a user