Initial commit: ESB Certificate Manager.
EOF Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user