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 } }