using System.Net.Security; using System.Net.Sockets; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using ZA.CoreService.ESBCertificateManager.Models; namespace ZA.CoreService.ESBCertificateManager.Services; public sealed class TlsCertificateProbe { private readonly int _timeoutSeconds; private readonly int _retryCount; public TlsCertificateProbe(int timeoutSeconds = 8, int retryCount = 2) { _timeoutSeconds = Math.Clamp(timeoutSeconds, 1, 60); _retryCount = Math.Clamp(retryCount, 0, 5); } public async Task<(bool Success, string Status, string? Detail, string? ObservedFingerprint)> ProbeAsync( DeploymentTarget target, string expectedFingerprintSha256, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(target.TlsHost)) { return (true, "TLS übersprungen", "Kein TlsHost konfiguriert.", null); } int port = target.TlsPort is > 0 and <= 65535 ? target.TlsPort.Value : 443; // TlsServerName überschreibt den SNI-Hostnamen wenn gesetzt (wichtig bei IP-Adressen) string serverName = string.IsNullOrWhiteSpace(target.TlsServerName) ? target.TlsHost : target.TlsServerName; string expected = NormalizeFingerprint(expectedFingerprintSha256); Exception? lastError = null; for (int attempt = 0; attempt <= _retryCount; attempt++) { cancellationToken.ThrowIfCancellationRequested(); try { return await ProbeOnceAsync( target.TlsHost, port, serverName, expected, cancellationToken); } catch (OperationCanceledException) { throw; } catch (Exception ex) { lastError = ex; if (attempt < _retryCount) { await Task.Delay(400, cancellationToken); } } } return ( false, "TLS nicht erreichbar", lastError?.Message ?? $"Keine Verbindung zu {target.TlsHost}:{port}", null); } private async Task<(bool Success, string Status, string? Detail, string? ObservedFingerprint)> ProbeOnceAsync( string host, int port, string serverName, string expectedFingerprint, CancellationToken cancellationToken) { using TcpClient client = new(); using CancellationTokenSource timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); timeoutCts.CancelAfter(TimeSpan.FromSeconds(_timeoutSeconds)); await client.ConnectAsync(host, port, timeoutCts.Token); await using SslStream sslStream = new( client.GetStream(), leaveInnerStreamOpen: false, userCertificateValidationCallback: static (_, _, _, _) => true); await sslStream.AuthenticateAsClientAsync( new SslClientAuthenticationOptions { TargetHost = serverName, // SNI: muss zum CN/SAN im Zertifikat passen EnabledSslProtocols = System.Security.Authentication.SslProtocols.Tls12 | System.Security.Authentication.SslProtocols.Tls13 }, timeoutCts.Token); if (sslStream.RemoteCertificate is null) { return (false, "TLS Fail", "Kein Remote-Zertifikat erhalten.", null); } using X509Certificate2 remote = new(sslStream.RemoteCertificate); string actual = Convert.ToHexString(SHA256.HashData(remote.RawData)); if (string.IsNullOrEmpty(expectedFingerprint)) { // Kein erwarteter Fingerprint konfiguriert – nur Konnektivität prüfen return (true, "TLS Pass (kein Fingerprint-Vergleich)", $"{host}:{port} erreichbar. Fingerprint={actual}", actual); } if (string.Equals(actual, expectedFingerprint, StringComparison.OrdinalIgnoreCase)) { return (true, "TLS Pass", $"{host}:{port} Fingerprint stimmt überein.", actual); } return ( false, "TLS Fail", $"{host}:{port} Fingerprint weicht ab. Erwartet={expectedFingerprint}, Ist={actual}", actual); } private static string NormalizeFingerprint(string fingerprint) { return fingerprint .Replace(":", string.Empty, StringComparison.Ordinal) .Replace(" ", string.Empty, StringComparison.Ordinal) .ToUpperInvariant(); } }