feat(batch9): implement f3 nats server ocsp wiring
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
400
dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Ocsp.cs
Normal file
400
dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Ocsp.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user