using System.Net.Security; using System.Net.Sockets; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text.RegularExpressions; using ZA.CoreService.ESBCertificateManager.Models; namespace ZA.CoreService.ESBCertificateManager.Services; /// /// Liest das präsentierte TLS-Zertifikat eines Endpoints /// (entspricht grob curl -v https://host:port). /// public sealed class TlsEndpointProbeService { private static readonly Regex HostPortRegex = new( @"^(?[^:/]+)(:(?\d{1,5}))?$", RegexOptions.Compiled | RegexOptions.CultureInvariant); public CertificateProbeResult Probe( string? checkUrl, int timeoutMilliseconds = 8000) { if (!TryParseEndpoint( checkUrl, out string host, out int port, out string serverName, out string? parseError)) { return CertificateProbeResult.Failed( parseError ?? "Ungültige Prüf-URL."); } try { using CancellationTokenSource cts = new(timeoutMilliseconds); using TcpClient client = new(); using (cts.Token.Register( () => { try { client.Close(); } catch { // Timeout beendet die Verbindung. } })) { client.Connect(host, port); } using SslStream ssl = new( client.GetStream(), leaveInnerStreamOpen: false, userCertificateValidationCallback: static (_, _, _, _) => true); ssl.AuthenticateAsClient(serverName); if (ssl.RemoteCertificate is null) { return CertificateProbeResult.Failed( $"Kein Zertifikat von {host}:{port} erhalten."); } using X509Certificate2 certificate = new(ssl.RemoteCertificate); CertificateInfo info = ToCertificateInfo(certificate); return CertificateProbeResult.Found( $"{host}:{port}", info); } catch (Exception ex) { return CertificateProbeResult.Failed( $"TLS-Prüfung {host}:{port} fehlgeschlagen: {ex.Message}"); } } public static bool TryParseEndpoint( string? value, out string host, out int port, out string serverName, out string? error) { host = string.Empty; port = 443; serverName = string.Empty; error = null; if (string.IsNullOrWhiteSpace(value)) { error = "Keine Prüf-URL hinterlegt."; return false; } string trimmed = value.Trim(); if (Uri.TryCreate(trimmed, UriKind.Absolute, out Uri? uri) && (uri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase) || uri.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase) || uri.Scheme.Equals("ssl", StringComparison.OrdinalIgnoreCase) || uri.Scheme.Equals("tcp", StringComparison.OrdinalIgnoreCase))) { if (string.IsNullOrWhiteSpace(uri.Host)) { error = "Host in der Prüf-URL fehlt."; return false; } host = uri.Host; serverName = uri.Host; port = uri.IsDefaultPort ? (uri.Scheme.Equals("http", StringComparison.OrdinalIgnoreCase) ? 80 : 443) : uri.Port; if (port is < 1 or > 65535) { error = "Port muss zwischen 1 und 65535 liegen."; return false; } return true; } Match match = HostPortRegex.Match(trimmed); if (!match.Success) { error = "Prüf-URL ungültig. Beispiele:\n" + "https://server:8443\n" + "server:8443\n" + "server"; return false; } host = match.Groups["host"].Value; serverName = host; if (match.Groups["port"].Success) { if (!int.TryParse(match.Groups["port"].Value, out port) || port is < 1 or > 65535) { error = "Port muss zwischen 1 und 65535 liegen."; return false; } } return !string.IsNullOrWhiteSpace(host); } public static string? NormalizeCheckUrl(string? value) { if (string.IsNullOrWhiteSpace(value)) { return null; } string trimmed = value.Trim(); if (!TryParseEndpoint( trimmed, out _, out _, out _, out string? error)) { throw new ArgumentException( error ?? "Ungültige Prüf-URL.", nameof(value)); } if (trimmed.Length > 500) { throw new ArgumentException( "Die Prüf-URL darf maximal 500 Zeichen enthalten.", nameof(value)); } return trimmed; } private static CertificateInfo ToCertificateInfo( X509Certificate2 certificate) { string subject = certificate.GetNameInfo( X509NameType.SimpleName, forIssuer: false); string issuer = certificate.GetNameInfo( X509NameType.SimpleName, forIssuer: true); if (string.IsNullOrWhiteSpace(subject)) { subject = certificate.Subject; } if (string.IsNullOrWhiteSpace(issuer)) { issuer = certificate.Issuer; } string fingerprint = certificate.GetCertHashString( HashAlgorithmName.SHA256); fingerprint = string.Join( ":", Enumerable.Range(0, fingerprint.Length / 2) .Select(index => fingerprint.Substring(index * 2, 2))); DateTimeOffset validFrom = new(certificate.NotBefore); DateTimeOffset validUntil = new(certificate.NotAfter); DateTimeOffset now = DateTimeOffset.Now; return new CertificateInfo { Subject = subject, Issuer = issuer, FingerprintSha256 = fingerprint, ValidFrom = validFrom, ValidUntil = validUntil, IsCurrentlyValid = now >= validFrom && now <= validUntil }; } }