using System.Net; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; namespace MSCO.Server; /// /// Zertifikat für HTTPS im Web-Panel. Ohne eigenes Zertifikat (PanelCertificatePath) erzeugt der Server einmal ein /// selbst signiertes und speichert es in data/panel_cert.pfx (nur für den Server-Benutzer lesbar). Der Browser warnt /// beim ersten Besuch ("nicht vertrauenswürdig") - die Verbindung ist trotzdem verschlüsselt; den Fingerabdruck zeigt /// die Server-Konsole zum Vergleichen. /// public static class PanelTls { public const string FileName = "panel_cert.pfx"; public static X509Certificate2 Load(ServerConfig config, string dataDir, Action log) { if (!string.IsNullOrWhiteSpace(config.PanelCertificatePath)) { string path = Path.IsPathRooted(config.PanelCertificatePath) ? config.PanelCertificatePath : Path.Combine(dataDir, config.PanelCertificatePath); var own = X509CertificateLoader.LoadPkcs12FromFile(path, config.PanelCertificatePassword ?? ""); log($"Web panel HTTPS certificate: {own.Subject} (from {path})"); return own; } string file = Path.Combine(dataDir, FileName); X509Certificate2 cert = null; if (File.Exists(file)) { try { cert = X509CertificateLoader.LoadPkcs12FromFile(file, null); if (cert.NotAfter < DateTime.Now.AddDays(30)) cert = null; // bald abgelaufen: neu erzeugen } catch { cert = null; } } if (cert == null) { cert = CreateSelfSigned(config); Directory.CreateDirectory(dataDir); File.WriteAllBytes(file, cert.Export(X509ContentType.Pfx)); Restrict(file); cert = X509CertificateLoader.LoadPkcs12FromFile(file, null); // aus der Datei laden (Windows braucht einen gespeicherten Schlüssel) log("Web panel: created a self-signed HTTPS certificate (" + FileName + ")"); } log("Web panel HTTPS fingerprint (SHA-256): " + Fingerprint(cert)); return cert; } public static string Fingerprint(X509Certificate2 cert) => string.Join(":", Convert.ToHexString(cert.GetCertHash(HashAlgorithmName.SHA256)).Chunk(2).Select(c => new string(c))); static X509Certificate2 CreateSelfSigned(ServerConfig config) { using var rsa = RSA.Create(3072); var req = new CertificateRequest("CN=MSCO Server Panel", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); req.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, true)); req.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment, true)); req.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension(new OidCollection { new Oid("1.3.6.1.5.5.7.3.1") }, false)); var san = new SubjectAlternativeNameBuilder(); san.AddDnsName("localhost"); san.AddIpAddress(IPAddress.Loopback); string publicAddress = (config.PublicAddress ?? "").Trim(); if (IPAddress.TryParse(publicAddress, out var ip)) san.AddIpAddress(ip); else if (publicAddress.Length > 0 && Uri.CheckHostName(publicAddress) == UriHostNameType.Dns) san.AddDnsName(publicAddress); req.CertificateExtensions.Add(san.Build()); return req.CreateSelfSigned(DateTimeOffset.Now.AddDays(-1), DateTimeOffset.Now.AddYears(5)); } /// Datei nur für den Benutzer des Servers lesbar (Linux). public static void Restrict(string file) { if (OperatingSystem.IsWindows()) return; try { File.SetUnixFileMode(file, UnixFileMode.UserRead | UnixFileMode.UserWrite); } catch { } } /// Beginnen die Bytes wie ein TLS-Handshake (ClientHello: 16 03 0x)? public static bool LooksLikeTls(byte[] b, int n) => n >= 3 && b[0] == 0x16 && b[1] == 0x03 && b[2] <= 0x04; }