using System;
using System.Security.Cryptography;
using System.Text;
#if NETSTANDARD2_0
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Security;
#endif
namespace Deploymentcenter.Client
{
///
/// Prueft die Signatur eines Releases.
///
/// Der SHA256 eines Pakets stammt aus derselben Quelle wie das Paket
/// selbst. Wer den Webroot oder die FTP-Zugangsdaten kontrolliert, tauscht
/// beide gemeinsam aus - der Hash schuetzt dann gegen Uebertragungsfehler,
/// nicht gegen Manipulation. Ausgerechnet auf dem Pfad, der fremden Code
/// ausfuehrt.
///
/// Bewusst asymmetrisch: bei einem HMAC braeuchte der pruefende Agent
/// denselben geheimen Schluessel wie der signierende Server. Der Agent
/// laeuft auf Kundensystemen; ein dort ausgelesener Schluessel liesse sich
/// zum Signieren beliebiger Pakete verwenden. Der Server signiert daher
/// mit einem privaten RSA-Schluessel, der Agent prueft mit dem
/// oeffentlichen aus /api/updateservice/v1/pubkey.
///
public static class ReleaseVerifier
{
///
/// Kanonische Darstellung eines Releases - muss zeichengenau der
/// serverseitigen Fassung in ReleaseSigner::canonical() entsprechen.
/// Signiert wird bewusst diese Zeile und nicht das Manifest-JSON:
/// JSON-Ausgabe ist nicht bytestabil (Schluesselreihenfolge, Escaping,
/// Zahlenformat), eine Signatur darueber waere unzuverlaessig pruefbar.
///
public static string BuildCanonical(
string productSlug,
string version,
string channel,
string platform,
string? sha256Hash,
string downloadUrl,
long sizeBytes)
{
return string.Join("\n", new[]
{
"dc-release-v1",
productSlug ?? string.Empty,
version ?? string.Empty,
channel ?? string.Empty,
platform ?? string.Empty,
(sha256Hash ?? string.Empty).ToLowerInvariant(),
downloadUrl ?? string.Empty,
sizeBytes.ToString(System.Globalization.CultureInfo.InvariantCulture)
});
}
///
/// Prueft eine base64-kodierte RSA-SHA256-Signatur gegen einen
/// oeffentlichen Schluessel im PEM-Format.
///
public static bool Verify(string canonical, string? signatureBase64, string? publicKeyPem)
{
if (string.IsNullOrWhiteSpace(signatureBase64) || string.IsNullOrWhiteSpace(publicKeyPem))
return false;
byte[] signature;
try
{
signature = Convert.FromBase64String(signatureBase64!.Trim());
}
catch (FormatException)
{
return false;
}
byte[] data = Encoding.UTF8.GetBytes(canonical);
try
{
#if NET8_0_OR_GREATER
using var rsa = RSA.Create();
rsa.ImportFromPem(publicKeyPem!.ToCharArray());
return rsa.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
#else
AsymmetricKeyParameter key = PublicKeyFactory.CreateKey(DecodePem(publicKeyPem!));
ISigner signer = SignerUtilities.GetSigner("SHA256withRSA");
signer.Init(false, key);
signer.BlockUpdate(data, 0, data.Length);
return signer.VerifySignature(signature);
#endif
}
catch
{
// Ein nicht lesbarer Schluessel oder eine unpassende Signatur
// sind kein Sonderfall, sondern schlicht "nicht geprueft".
return false;
}
}
///
/// Fingerabdruck eines PEM-Schluessels. Damit erkennt der Agent den
/// einmal geholten Schluessel wieder, statt ihm bei jedem Aufruf neu
/// zu vertrauen - ein spaeter ausgetauschter Schluessel faellt so auf.
///
public static string Fingerprint(string publicKeyPem)
{
using var sha256 = SHA256.Create();
byte[] hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(publicKeyPem ?? string.Empty));
return BitConverter.ToString(hash).Replace("-", string.Empty).ToLowerInvariant();
}
#if NETSTANDARD2_0
/// Entfernt Kopf- und Fusszeile eines PEM und dekodiert base64.
private static byte[] DecodePem(string pem)
{
var builder = new StringBuilder();
foreach (string line in pem.Split('\n'))
{
string trimmed = line.Trim();
if (trimmed.Length == 0 || trimmed.StartsWith("-----", StringComparison.Ordinal))
continue;
builder.Append(trimmed);
}
return Convert.FromBase64String(builder.ToString());
}
#endif
}
}