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
|
// Licensed under the Apache License, Version 2.0
|
||||||
|
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Formats.Asn1;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Security.Cryptography.X509Certificates;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
@@ -15,6 +16,8 @@ namespace ZB.MOM.NatsNet.Server;
|
|||||||
internal static class OcspHandler
|
internal static class OcspHandler
|
||||||
{
|
{
|
||||||
private const string CertPemLabel = "CERTIFICATE";
|
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)
|
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(
|
internal static (X509Certificate2? issuer, Exception? error) GetOCSPIssuerLocally(
|
||||||
IReadOnlyList<X509Certificate2> trustedCAs,
|
IReadOnlyList<X509Certificate2> trustedCAs,
|
||||||
IReadOnlyList<X509Certificate2> certBundle)
|
IReadOnlyList<X509Certificate2> certBundle)
|
||||||
@@ -85,6 +125,11 @@ internal static class OcspHandler
|
|||||||
|
|
||||||
if (!chain.Build(leaf) || chain.ChainElements.Count < 2)
|
if (!chain.Build(leaf) || chain.ChainElements.Count < 2)
|
||||||
{
|
{
|
||||||
|
if (string.Equals(leaf.Subject, leaf.Issuer, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return (leaf, null);
|
||||||
|
}
|
||||||
|
|
||||||
return (null, null);
|
return (null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -86,6 +86,9 @@ internal sealed class OcspMonitor
|
|||||||
/// <summary>Path to the TLS certificate file being monitored.</summary>
|
/// <summary>Path to the TLS certificate file being monitored.</summary>
|
||||||
public string? CertFile { get; set; }
|
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>
|
/// <summary>Path to the CA certificate file used to verify OCSP responses.</summary>
|
||||||
public string? CaFile { get; set; }
|
public string? CaFile { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -411,8 +411,12 @@ public sealed partial class NatsServer
|
|||||||
// Assign leaf options.
|
// Assign leaf options.
|
||||||
s._leafNodeEnabled = opts.LeafNode.Port != 0 || opts.LeafNode.Remotes.Count > 0;
|
s._leafNodeEnabled = opts.LeafNode.Port != 0 || opts.LeafNode.Remotes.Count > 0;
|
||||||
|
|
||||||
// OCSP (stub — session 23).
|
var ocspError = s.EnableOCSP();
|
||||||
// s.EnableOcsp() — deferred
|
if (ocspError != null)
|
||||||
|
{
|
||||||
|
s._mu.ExitWriteLock();
|
||||||
|
return (null, ocspError);
|
||||||
|
}
|
||||||
|
|
||||||
// Gateway (stub — session 16).
|
// Gateway (stub — session 16).
|
||||||
// s.NewGateway(opts) — deferred
|
// s.NewGateway(opts) — deferred
|
||||||
|
|||||||
@@ -139,6 +139,22 @@ public sealed partial class NatsServer
|
|||||||
|
|
||||||
Noticef("Server Exiting..");
|
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 */ }
|
if (_ocsprc != null) { /* stub — stop OCSP cache in session 23 */ }
|
||||||
|
|
||||||
DisposeSignalHandlers();
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
189
dotnet/tests/ZB.MOM.NatsNet.Server.Tests/NatsServerOcspTests.cs
Normal file
189
dotnet/tests/ZB.MOM.NatsNet.Server.Tests/NatsServerOcspTests.cs
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
using System.Net.Security;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Shouldly;
|
||||||
|
using ZB.MOM.NatsNet.Server;
|
||||||
|
|
||||||
|
namespace ZB.MOM.NatsNet.Server.Tests;
|
||||||
|
|
||||||
|
public sealed class NatsServerOcspTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly List<string> _tempDirs = [];
|
||||||
|
private readonly List<X509Certificate2> _certs = [];
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetupOCSPStapleStoreDir_WithStoreDir_CreatesDirectory()
|
||||||
|
{
|
||||||
|
var dir = MakeTempDir();
|
||||||
|
var server = NewServer(new ServerOptions
|
||||||
|
{
|
||||||
|
StoreDir = dir,
|
||||||
|
});
|
||||||
|
|
||||||
|
var err = server.SetupOCSPStapleStoreDir();
|
||||||
|
|
||||||
|
err.ShouldBeNull();
|
||||||
|
Directory.Exists(Path.Combine(dir, "ocsp")).ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ConfigureOCSP_WithTlsConfig_ReturnsClientEntry()
|
||||||
|
{
|
||||||
|
var cert = CreateSelfSignedCertificate("CN=configure-ocsp");
|
||||||
|
var server = NewServer(new ServerOptions
|
||||||
|
{
|
||||||
|
TlsConfig = new SslServerAuthenticationOptions { ServerCertificate = cert },
|
||||||
|
});
|
||||||
|
|
||||||
|
var configs = server.ConfigureOCSP();
|
||||||
|
|
||||||
|
configs.Count.ShouldBe(1);
|
||||||
|
configs[0].Kind.ShouldBe("client");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NewOCSPMonitor_OcspNever_ReturnsNoMonitor()
|
||||||
|
{
|
||||||
|
var cert = CreateSelfSignedCertificate("CN=monitor-never");
|
||||||
|
var server = NewServer(new ServerOptions
|
||||||
|
{
|
||||||
|
StoreDir = MakeTempDir(),
|
||||||
|
OcspConfig = new OcspConfig { Mode = ZB.MOM.NatsNet.Server.OcspMode.Never },
|
||||||
|
});
|
||||||
|
|
||||||
|
var config = new OcspTlsConfig
|
||||||
|
{
|
||||||
|
Kind = "client",
|
||||||
|
TlsConfig = new SslServerAuthenticationOptions { ServerCertificate = cert },
|
||||||
|
TlsOptions = null,
|
||||||
|
Apply = _ => { },
|
||||||
|
};
|
||||||
|
|
||||||
|
var (_, monitor, err) = server.NewOCSPMonitor(config);
|
||||||
|
|
||||||
|
err.ShouldBeNull();
|
||||||
|
monitor.ShouldBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void EnableOCSP_WithAlwaysMode_AddsMonitor()
|
||||||
|
{
|
||||||
|
var cert = CreateSelfSignedCertificate("CN=enable-ocsp");
|
||||||
|
var storeDir = MakeTempDir();
|
||||||
|
WriteLocalOcspStatus(storeDir, cert);
|
||||||
|
var server = NewServer(new ServerOptions
|
||||||
|
{
|
||||||
|
StoreDir = storeDir,
|
||||||
|
OcspConfig = new OcspConfig
|
||||||
|
{
|
||||||
|
Mode = ZB.MOM.NatsNet.Server.OcspMode.Always,
|
||||||
|
OverrideUrls = ["https://ocsp.example.test"],
|
||||||
|
},
|
||||||
|
TlsConfig = new SslServerAuthenticationOptions { ServerCertificate = cert },
|
||||||
|
});
|
||||||
|
|
||||||
|
var err = server.EnableOCSP();
|
||||||
|
|
||||||
|
err.ShouldBeNull();
|
||||||
|
server.GetOcspMonitors().Length.ShouldBe(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void StartOCSPMonitoring_NoMonitors_DoesNotThrow()
|
||||||
|
{
|
||||||
|
var server = NewServer(new ServerOptions());
|
||||||
|
|
||||||
|
Should.NotThrow(() => server.StartOCSPMonitoring());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReloadOCSP_WithConfiguredTls_ReplacesMonitors()
|
||||||
|
{
|
||||||
|
var cert = CreateSelfSignedCertificate("CN=reload-ocsp");
|
||||||
|
var storeDir = MakeTempDir();
|
||||||
|
WriteLocalOcspStatus(storeDir, cert);
|
||||||
|
var server = NewServer(new ServerOptions
|
||||||
|
{
|
||||||
|
StoreDir = storeDir,
|
||||||
|
OcspConfig = new OcspConfig
|
||||||
|
{
|
||||||
|
Mode = ZB.MOM.NatsNet.Server.OcspMode.Always,
|
||||||
|
OverrideUrls = ["https://ocsp.example.test"],
|
||||||
|
},
|
||||||
|
TlsConfig = new SslServerAuthenticationOptions { ServerCertificate = cert },
|
||||||
|
});
|
||||||
|
server.EnableOCSP().ShouldBeNull();
|
||||||
|
server.GetOcspMonitors().Length.ShouldBe(1);
|
||||||
|
|
||||||
|
var err = server.ReloadOCSP();
|
||||||
|
|
||||||
|
err.ShouldBeNull();
|
||||||
|
server.GetOcspMonitors().Length.ShouldBe(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HasOCSPStatusRequest_CertificateWithoutExtension_ReturnsFalse()
|
||||||
|
{
|
||||||
|
var cert = CreateSelfSignedCertificate("CN=no-status-request");
|
||||||
|
|
||||||
|
OcspHandler.HasOCSPStatusRequest(cert).ShouldBeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
foreach (var cert in _certs)
|
||||||
|
{
|
||||||
|
cert.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var dir in _tempDirs)
|
||||||
|
{
|
||||||
|
try { Directory.Delete(dir, recursive: true); } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private NatsServer NewServer(ServerOptions options)
|
||||||
|
{
|
||||||
|
var (server, err) = NatsServer.NewServer(options);
|
||||||
|
err.ShouldBeNull();
|
||||||
|
return server!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private X509Certificate2 CreateSelfSignedCertificate(string subject)
|
||||||
|
{
|
||||||
|
var req = new CertificateRequest(
|
||||||
|
subject,
|
||||||
|
RSA.Create(2048),
|
||||||
|
HashAlgorithmName.SHA256,
|
||||||
|
RSASignaturePadding.Pkcs1);
|
||||||
|
req.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true));
|
||||||
|
req.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(req.PublicKey, false));
|
||||||
|
|
||||||
|
var cert = req.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(90));
|
||||||
|
_certs.Add(cert);
|
||||||
|
return cert;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string MakeTempDir()
|
||||||
|
{
|
||||||
|
var path = Path.Combine(Path.GetTempPath(), "nats-ocsp-" + Path.GetRandomFileName());
|
||||||
|
Directory.CreateDirectory(path);
|
||||||
|
_tempDirs.Add(path);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteLocalOcspStatus(string storeDir, X509Certificate2 cert)
|
||||||
|
{
|
||||||
|
var key = Convert.ToHexString(SHA256.HashData(cert.RawData)).ToLowerInvariant();
|
||||||
|
var ocspDir = Path.Combine(storeDir, "ocsp");
|
||||||
|
Directory.CreateDirectory(ocspDir);
|
||||||
|
var payload = JsonSerializer.SerializeToUtf8Bytes(new
|
||||||
|
{
|
||||||
|
Status = 0,
|
||||||
|
ThisUpdate = DateTime.UtcNow.AddMinutes(-5),
|
||||||
|
NextUpdate = DateTime.UtcNow.AddHours(6),
|
||||||
|
});
|
||||||
|
File.WriteAllBytes(Path.Combine(ocspDir, key), payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
porting.db
BIN
porting.db
Binary file not shown.
Reference in New Issue
Block a user