feat(batch9): implement f3 nats server ocsp wiring

This commit is contained in:
Joseph Doherty
2026-02-28 12:28:16 -05:00
parent 8f3e4a5a23
commit 87b4363eeb
7 changed files with 659 additions and 2 deletions

View File

@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0
using System.Globalization;
using System.Formats.Asn1;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text.Json;
@@ -15,6 +16,8 @@ namespace ZB.MOM.NatsNet.Server;
internal static class OcspHandler
{
private const string CertPemLabel = "CERTIFICATE";
private const string TlsFeaturesOid = "1.3.6.1.5.5.7.1.24";
private const int StatusRequestExtension = 5;
internal static (List<X509Certificate2>? certificates, Exception? error) ParseCertPEM(string name)
{
@@ -50,6 +53,43 @@ internal static class OcspHandler
}
}
internal static bool HasOCSPStatusRequest(X509Certificate2 cert)
{
foreach (var extension in cert.Extensions)
{
if (!string.Equals(extension.Oid?.Value, TlsFeaturesOid, StringComparison.Ordinal))
{
continue;
}
try
{
var reader = new AsnReader(extension.RawData, AsnEncodingRules.DER);
var seq = reader.ReadSequence();
while (seq.HasData)
{
if (seq.ReadInteger() == StatusRequestExtension)
{
return true;
}
}
if (reader.HasData)
{
return false;
}
}
catch
{
return false;
}
break;
}
return false;
}
internal static (X509Certificate2? issuer, Exception? error) GetOCSPIssuerLocally(
IReadOnlyList<X509Certificate2> trustedCAs,
IReadOnlyList<X509Certificate2> certBundle)
@@ -85,6 +125,11 @@ internal static class OcspHandler
if (!chain.Build(leaf) || chain.ChainElements.Count < 2)
{
if (string.Equals(leaf.Subject, leaf.Issuer, StringComparison.Ordinal))
{
return (leaf, null);
}
return (null, null);
}

View File

@@ -86,6 +86,9 @@ internal sealed class OcspMonitor
/// <summary>Path to the TLS certificate file being monitored.</summary>
public string? CertFile { get; set; }
/// <summary>Connection kind this monitor applies to (client/router/gateway/leaf).</summary>
public string Kind { get; set; } = string.Empty;
/// <summary>Path to the CA certificate file used to verify OCSP responses.</summary>
public string? CaFile { get; set; }

View File

@@ -411,8 +411,12 @@ public sealed partial class NatsServer
// Assign leaf options.
s._leafNodeEnabled = opts.LeafNode.Port != 0 || opts.LeafNode.Remotes.Count > 0;
// OCSP (stub — session 23).
// s.EnableOcsp() — deferred
var ocspError = s.EnableOCSP();
if (ocspError != null)
{
s._mu.ExitWriteLock();
return (null, ocspError);
}
// Gateway (stub — session 16).
// s.NewGateway(opts) — deferred

View File

@@ -139,6 +139,22 @@ public sealed partial class NatsServer
Noticef("Server Exiting..");
var monitors = GetOcspMonitors();
foreach (var monitor in monitors)
{
monitor.Stop();
}
_mu.EnterWriteLock();
try
{
_ocsps = null;
}
finally
{
_mu.ExitWriteLock();
}
if (_ocsprc != null) { /* stub — stop OCSP cache in session 23 */ }
DisposeSignalHandlers();

View File

@@ -0,0 +1,400 @@
// Copyright 2012-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using ZB.MOM.NatsNet.Server.Auth.CertificateIdentityProvider;
using ZB.MOM.NatsNet.Server.Auth.Ocsp;
namespace ZB.MOM.NatsNet.Server;
/// <summary>
/// TLS configuration slot used by OCSP wiring to apply wrapped TLS settings.
/// Mirrors Go <c>tlsConfigKind</c> shape used by <c>configureOCSP</c>.
/// </summary>
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<SslServerAuthenticationOptions> 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<OcspTlsConfig> ConfigureOCSP()
{
var opts = GetOpts();
var configs = new List<OcspTlsConfig>();
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<byte[]> { 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<OcspMonitor>(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<OcspMonitor>(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;
}
}