// Copyright 2012-2026 The NATS Authors // Licensed under the Apache License, Version 2.0 using System.Net.Security; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using ZB.MOM.NatsNet.Server.Auth.CertificateIdentityProvider; using ZB.MOM.NatsNet.Server.Auth.Ocsp; namespace ZB.MOM.NatsNet.Server; /// /// TLS configuration slot used by OCSP wiring to apply wrapped TLS settings. /// Mirrors Go tlsConfigKind shape used by configureOCSP. /// internal sealed class OcspTlsConfig { public required string Kind { get; init; } public required SslServerAuthenticationOptions TlsConfig { get; init; } public required TlsConfigOpts? TlsOptions { get; init; } public required Action Apply { get; init; } public bool IsLeafSpoke { get; init; } } public sealed partial class NatsServer { private const string ClientKindName = "client"; private const string RouterKindName = "router"; private const string GatewayKindName = "gateway"; private const string LeafKindName = "leaf"; private const string DefaultOcspStoreDir = "ocsp"; internal OcspMonitor[] GetOcspMonitors() { _mu.EnterReadLock(); try { return _ocsps is null ? [] : [.. _ocsps]; } finally { _mu.ExitReadLock(); } } internal Exception? SetupOCSPStapleStoreDir() { var storeDir = GetOpts().StoreDir; if (string.IsNullOrEmpty(storeDir)) { return null; } var ocspDir = Path.Combine(storeDir, DefaultOcspStoreDir); try { if (!Directory.Exists(ocspDir)) { Directory.CreateDirectory(ocspDir); } else { var attributes = File.GetAttributes(ocspDir); if ((attributes & FileAttributes.Directory) != FileAttributes.Directory) { return new InvalidOperationException("OCSP storage directory is not a directory"); } } } catch (Exception ex) { return new InvalidOperationException($"could not create OCSP storage directory - {ex.Message}", ex); } return null; } internal List ConfigureOCSP() { var opts = GetOpts(); var configs = new List(); if (opts.TlsConfig != null) { configs.Add(new OcspTlsConfig { Kind = ClientKindName, TlsConfig = opts.TlsConfig, TlsOptions = opts.TlsConfigOpts, Apply = tls => opts.TlsConfig = tls, }); } if (opts.Websocket.TlsConfig != null) { configs.Add(new OcspTlsConfig { Kind = ClientKindName, TlsConfig = opts.Websocket.TlsConfig, TlsOptions = opts.Websocket.TlsConfigOpts, Apply = tls => opts.Websocket.TlsConfig = tls, }); } if (opts.Mqtt.TlsConfig != null) { configs.Add(new OcspTlsConfig { Kind = ClientKindName, TlsConfig = opts.Mqtt.TlsConfig, TlsOptions = opts.Mqtt.TlsConfigOpts, Apply = tls => opts.Mqtt.TlsConfig = tls, }); } if (opts.Cluster.TlsConfig != null) { configs.Add(new OcspTlsConfig { Kind = RouterKindName, TlsConfig = opts.Cluster.TlsConfig, TlsOptions = opts.Cluster.TlsConfigOpts, Apply = tls => opts.Cluster.TlsConfig = tls, }); } if (opts.LeafNode.TlsConfig != null) { configs.Add(new OcspTlsConfig { Kind = LeafKindName, TlsConfig = opts.LeafNode.TlsConfig, TlsOptions = opts.LeafNode.TlsConfigOpts, Apply = tls => opts.LeafNode.TlsConfig = tls, }); } foreach (var remote in opts.LeafNode.Remotes) { if (remote.TlsConfig == null) { continue; } var capturedRemote = remote; configs.Add(new OcspTlsConfig { Kind = LeafKindName, TlsConfig = remote.TlsConfig, TlsOptions = remote.TlsConfigOpts, IsLeafSpoke = true, Apply = tls => capturedRemote.TlsConfig = tls, }); } if (opts.Gateway.TlsConfig != null) { configs.Add(new OcspTlsConfig { Kind = GatewayKindName, TlsConfig = opts.Gateway.TlsConfig, TlsOptions = opts.Gateway.TlsConfigOpts, Apply = tls => opts.Gateway.TlsConfig = tls, }); } foreach (var gateway in opts.Gateway.Gateways) { if (gateway.TlsConfig == null) { continue; } var capturedGateway = gateway; configs.Add(new OcspTlsConfig { Kind = GatewayKindName, TlsConfig = gateway.TlsConfig, TlsOptions = gateway.TlsConfigOpts, Apply = tls => capturedGateway.TlsConfig = tls, }); } return configs; } internal (SslServerAuthenticationOptions? tlsConfig, OcspMonitor? monitor, Exception? error) NewOCSPMonitor( OcspTlsConfig config) { var opts = GetOpts(); var ocspConfig = opts.OcspConfig; var certFile = config.TlsOptions?.CertFile ?? opts.TlsCert; var caFile = config.TlsOptions?.CaFile ?? opts.TlsCaCert; if (config.TlsConfig.ServerCertificate is not X509Certificate2 leaf) { return (null, null, new InvalidOperationException("no certificate found")); } var shutdownOnRevoke = false; var mustStaple = OcspHandler.HasOCSPStatusRequest(leaf); if (ocspConfig != null) { switch (ocspConfig.Mode) { case OcspMode.Never: if (mustStaple) { Warnf("Certificate at '{0}' has MustStaple but OCSP is disabled", certFile); } return (config.TlsConfig, null, null); case OcspMode.Always: mustStaple = true; shutdownOnRevoke = true; break; case OcspMode.Must when mustStaple: shutdownOnRevoke = true; break; case OcspMode.Auto when !mustStaple: return (config.TlsConfig, null, null); } } if (!mustStaple) { return (config.TlsConfig, null, null); } var setupError = SetupOCSPStapleStoreDir(); if (setupError != null) { return (null, null, setupError); } var chain = new List { leaf.RawData }; if (config.TlsConfig.ServerCertificateContext != null) { foreach (var intermediate in config.TlsConfig.ServerCertificateContext.IntermediateCertificates) { chain.Add(intermediate.RawData); } } var (issuer, issuerError) = OcspHandler.GetOCSPIssuer(caFile, chain); if (issuerError != null || issuer == null) { return (null, null, issuerError ?? new InvalidOperationException("no issuers found")); } var monitor = new OcspMonitor { Kind = config.Kind, Server = this, CertFile = certFile, CaFile = caFile, Leaf = leaf, Issuer = issuer, ShutdownOnRevoke = shutdownOnRevoke, }; var (_, response, statusError) = monitor.GetStatus(); if (statusError != null) { return (null, null, new InvalidOperationException($"bad OCSP status update for certificate at '{certFile}': {statusError.Message}", statusError)); } if (response != null && response.Status != OcspStatusAssertion.Good && shutdownOnRevoke) { return (null, null, new InvalidOperationException( $"found existing OCSP status for certificate at '{certFile}': {OcspHandler.OcspStatusString((int)response.Status)}")); } return (config.TlsConfig, monitor, null); } internal Exception? EnableOCSP() { var configs = ConfigureOCSP(); var monitors = new List(configs.Count); foreach (var config in configs) { if (config.Kind != LeafKindName) { var (tlsConfig, monitor, error) = NewOCSPMonitor(config); if (error != null) { return error; } if (monitor != null && tlsConfig != null) { monitors.Add(monitor); config.Apply(tlsConfig); } } // OCSP peer verification hook is implemented in batch 9 F4. } _mu.EnterWriteLock(); try { _ocsps = monitors.Count == 0 ? null : [.. monitors]; } finally { _mu.ExitWriteLock(); } return null; } internal void StartOCSPMonitoring() { OcspMonitor[]? monitors; _mu.EnterReadLock(); try { monitors = _ocsps; } finally { _mu.ExitReadLock(); } if (monitors == null || monitors.Length == 0) { return; } foreach (var monitor in monitors) { Noticef("OCSP Stapling enabled for {0} connections", monitor.Kind); monitor.Start(); StartGoRoutine(() => monitor.Run(_quitCts.Token).GetAwaiter().GetResult()); } } internal Exception? ReloadOCSP() { var setupError = SetupOCSPStapleStoreDir(); if (setupError != null) { return setupError; } var existingMonitors = GetOcspMonitors(); foreach (var monitor in existingMonitors) { monitor.Stop(); } var configs = ConfigureOCSP(); var replacement = new List(configs.Count); _mu.EnterWriteLock(); try { _ocspPeerVerify = false; } finally { _mu.ExitWriteLock(); } foreach (var config in configs) { if (config.Kind != LeafKindName) { var (tlsConfig, monitor, error) = NewOCSPMonitor(config); if (error != null) { return error; } if (monitor != null && tlsConfig != null) { replacement.Add(monitor); config.Apply(tlsConfig); } } } _mu.EnterWriteLock(); try { _ocsps = replacement.Count == 0 ? null : [.. replacement]; } finally { _mu.ExitWriteLock(); } StartOCSPMonitoring(); return null; } internal (SslServerAuthenticationOptions? tlsConfig, bool plugged, Exception? error) PlugTLSOCSPPeer(OcspTlsConfig? config) { if (config == null || config.TlsConfig == null) { return (null, false, new InvalidOperationException(OcspMessages.ErrUnableToPlugTLSEmptyConfig)); } var kind = config.Kind; var isSpoke = config.IsLeafSpoke; var tlsOptions = config.TlsOptions; if (tlsOptions?.OcspPeerConfig == null || !tlsOptions.OcspPeerConfig.Verify) { return (config.TlsConfig, false, null); } Debugf(OcspMessages.DbgPlugTLSForKind, config.Kind); if (kind == ClientKindName || (kind == LeafKindName && !isSpoke)) { if (!tlsOptions.Verify) { return (null, false, new InvalidOperationException(OcspMessages.ErrMTLSRequired)); } return PlugClientTLSOCSPPeer(config); } if (kind == LeafKindName && isSpoke) { return PlugServerTLSOCSPPeer(config); } return (config.TlsConfig, false, null); } internal (SslServerAuthenticationOptions? tlsConfig, bool plugged, Exception? error) PlugClientTLSOCSPPeer(OcspTlsConfig? config) { if (config?.TlsConfig == null || config.TlsOptions == null) { return (null, false, new InvalidOperationException(OcspMessages.ErrUnableToPlugTLSClient)); } var tlsConfig = config.TlsConfig; var tlsOptions = config.TlsOptions; if (tlsOptions.OcspPeerConfig == null || !tlsOptions.OcspPeerConfig.Verify) { return (tlsConfig, false, null); } tlsConfig.RemoteCertificateValidationCallback = (_, _, chain, _) => { if (chain?.ChainElements == null || chain.ChainElements.Count == 0) { return false; } var converted = chain.ChainElements .Select(e => e.Certificate) .OfType() .ToArray(); return TlsClientOCSPValid([converted], tlsOptions.OcspPeerConfig); }; return (tlsConfig, true, null); } internal (SslServerAuthenticationOptions? tlsConfig, bool plugged, Exception? error) PlugServerTLSOCSPPeer(OcspTlsConfig? config) { if (config?.TlsConfig == null || config.TlsOptions == null) { return (null, false, new InvalidOperationException(OcspMessages.ErrUnableToPlugTLSServer)); } var tlsConfig = config.TlsConfig; var tlsOptions = config.TlsOptions; if (tlsOptions.OcspPeerConfig == null || !tlsOptions.OcspPeerConfig.Verify) { return (tlsConfig, false, null); } tlsConfig.RemoteCertificateValidationCallback = (_, _, chain, _) => { if (chain?.ChainElements == null || chain.ChainElements.Count == 0) { return false; } var converted = chain.ChainElements .Select(e => e.Certificate) .OfType() .ToArray(); return TlsServerOCSPValid([converted], tlsOptions.OcspPeerConfig); }; return (tlsConfig, true, null); } internal bool TlsServerOCSPValid(X509Certificate2[][] chains, OcspPeerConfig options) { Debugf(OcspMessages.DbgNumServerChains, chains.Length); return PeerOCSPValid(chains, options); } internal bool TlsClientOCSPValid(X509Certificate2[][] chains, OcspPeerConfig options) { Debugf(OcspMessages.DbgNumClientChains, chains.Length); return PeerOCSPValid(chains, options); } internal bool PeerOCSPValid(X509Certificate2[][] chains, OcspPeerConfig options) { var peer = OcspHandler.PeerFromVerifiedChains(chains); if (peer == null) { Errorf(OcspMessages.ErrPeerEmptyAutoReject); return false; } for (var chainIndex = 0; chainIndex < chains.Length; chainIndex++) { var chain = chains[chainIndex]; Debugf(OcspMessages.DbgLinksInChain, chainIndex, chain.Length); if (chain.Length == 1) { Debugf(OcspMessages.DbgSelfSignedValid, chainIndex); return true; } var chainEligible = false; var eligibleLinks = new List(); for (var linkPos = 0; linkPos < chain.Length - 1; linkPos++) { var cert = chain[linkPos]; var link = new ChainLink { Leaf = cert }; if (OcspUtilities.CertOCSPEligible(link)) { chainEligible = true; link.Issuer = chain[linkPos + 1]; eligibleLinks.Add(link); } } if (!chainEligible) { Debugf(OcspMessages.DbgValidNonOCSPChain, chainIndex); return true; } Debugf(OcspMessages.DbgChainIsOCSPEligible, chainIndex, eligibleLinks.Count); var chainValid = true; foreach (var link in eligibleLinks) { var (reason, good) = CertOCSPGood(link, options); if (good) { continue; } Debugf(reason); chainValid = false; break; } if (chainValid) { Debugf(OcspMessages.DbgChainIsOCSPValid, chainIndex); return true; } } Debugf(OcspMessages.DbgNoOCSPValidChains); return false; } internal (string reason, bool good) CertOCSPGood(ChainLink? link, OcspPeerConfig options) { if (link?.Leaf == null || link.Issuer == null || link.OcspWebEndpoints == null || link.OcspWebEndpoints.Count == 0) { return ("Empty chainlink found", false); } var log = new OcspLog { Debugf = (fmt, args) => Debugf(fmt, args), Noticef = (fmt, args) => Noticef(fmt, args), Warnf = (fmt, args) => Warnf(fmt, args), Errorf = (fmt, args) => Errorf(fmt, args), }; var fingerprint = Convert.ToHexString(SHA256.HashData(link.Leaf.RawData)).ToLowerInvariant(); var cached = _ocsprc?.Get(fingerprint); if (cached is { Length: > 0 }) { var (parsed, parseError) = OcspHandler.ParseOcspResponse(cached); if (parseError == null && parsed != null && OcspUtilities.OCSPResponseCurrent(parsed, options, log)) { if (parsed.Status == OcspStatusAssertion.Revoked || (parsed.Status == OcspStatusAssertion.Unknown && !options.UnknownIsGood)) { if (options.WarnOnly) { Warnf("allowing OCSP peer due warn_only for [{0}]", link.Leaf.Subject); return (string.Empty, true); } return ($"OCSP response invalid status: {OcspStatusAssertionExtensions.GetStatusAssertionStr((int)parsed.Status)}", false); } Debugf(OcspMessages.DbgOCSPValidPeerLink, link.Leaf.Subject); return (string.Empty, true); } } if (options.AllowWhenCAUnreachable || options.WarnOnly) { if (options.WarnOnly) { Warnf("allowing OCSP peer due warn_only for [{0}]", link.Leaf.Subject); } if (options.AllowWhenCAUnreachable) { Warnf("allowing OCSP peer due unreachable CA for [{0}]", link.Leaf.Subject); } return (string.Empty, true); } return ("failed to fetch OCSP response", false); } }