136 lines
4.7 KiB
C#
136 lines
4.7 KiB
C#
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();
|
||
}
|
||
}
|