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; } }