using System.Net.Security;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
namespace ZB.MOM.WW.Tools.LdapProbe;
///
/// Wraps the TLS server-certificate validation callback so the probe can *report* the
/// presented chain rather than silently accept or reject it.
///
///
/// This exists because ldap_setup.md records two distinct TLS hazards that both
/// present as a generic connect failure: the DC certs chain to the internal
/// zimmer-rootca (absent from a dev machine's trust store), and they carry
/// RSA 1024-bit keys, which OpenSSL at its default security level rejects outright.
/// Printing the subject, key size and the exact turns
/// "connection refused" into a diagnosis.
///
public sealed class RemoteCertificateValidationCallbackHolder
{
private readonly bool _accept;
private readonly bool _report;
private int _reported;
public RemoteCertificateValidationCallbackHolder(bool acceptAnything, bool report)
{
_accept = acceptAnything;
_report = report;
}
/// The last chain observed, for the summary at the end of a run.
public string? Summary { get; private set; }
public RemoteCertificateValidationCallback Callback => Validate;
private bool Validate(object sender, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors errors)
{
// Report once per process: a failover walk opens several connections and the
// chain is the same each time.
if (_report && Interlocked.Exchange(ref _reported, 1) == 0)
Describe(certificate, chain, errors);
return _accept || errors == SslPolicyErrors.None;
}
private void Describe(X509Certificate? certificate, X509Chain? chain, SslPolicyErrors errors)
{
var lines = new List();
if (certificate is null)
{
lines.Add("server presented NO certificate");
}
else
{
using var cert = new X509Certificate2(certificate);
lines.Add($"subject {cert.Subject}");
lines.Add($"issuer {cert.Issuer}");
lines.Add($"valid {cert.NotBefore:yyyy-MM-dd} -> {cert.NotAfter:yyyy-MM-dd}"
+ (cert.NotAfter < DateTime.Now ? " *** EXPIRED ***" : ""));
lines.Add($"sig alg {cert.SignatureAlgorithm.FriendlyName}");
var (algorithm, size) = KeyInfo(cert);
var weak = size is > 0 and < 2048;
lines.Add($"key {algorithm} {size}-bit" + (weak
? " *** WEAK — OpenSSL SECLEVEL>=2 (Debian .NET containers) rejects this ***"
: ""));
var san = cert.Extensions
.OfType()
.SelectMany(e => e.EnumerateDnsNames())
.ToArray();
if (san.Length > 0)
lines.Add($"SAN {string.Join(", ", san)}");
}
lines.Add($"policy {(errors == SslPolicyErrors.None ? "None (chain validated cleanly)" : errors.ToString())}");
if (chain is not null)
{
foreach (var element in chain.ChainElements)
{
var status = element.ChainElementStatus.Length == 0
? "ok"
: string.Join("; ", element.ChainElementStatus.Select(s => s.Status.ToString()));
lines.Add($" chain {Shorten(element.Certificate.Subject)} [{status}]");
}
}
lines.Add(_accept
? "verdict ACCEPTED because --insecure was passed (diagnostic only — do not ship this)"
: errors == SslPolicyErrors.None
? "verdict ACCEPTED by the OS trust store"
: "verdict REJECTED — rerun with --insecure to see past this, or install zimmer-rootca");
Summary = string.Join(Environment.NewLine, lines.Select(l => " " + l));
Ui.Section("TLS server certificate");
Console.WriteLine(Summary);
Console.WriteLine();
}
private static (string Algorithm, int Size) KeyInfo(X509Certificate2 cert)
{
try
{
using var rsa = cert.GetRSAPublicKey();
if (rsa is not null) return ("RSA", rsa.KeySize);
using var ecdsa = cert.GetECDsaPublicKey();
if (ecdsa is not null) return ("ECDSA", ecdsa.KeySize);
}
catch (CryptographicException)
{
// An unreadable key is itself worth reporting rather than crashing the probe.
}
return (cert.PublicKey.Oid.FriendlyName ?? "unknown", 0);
}
private static string Shorten(string dn)
{
var cn = dn.Split(',').FirstOrDefault(p => p.TrimStart().StartsWith("CN=", StringComparison.OrdinalIgnoreCase));
return cn?.Trim() ?? dn;
}
}