feat: add OCSP peer verification and stapling support
Wire OcspPeerVerify into the client-cert validation callback in TlsHelper so revocation is checked online when the flag is set. Add TlsHelper.BuildCertificateContext to build an SslStreamCertificateContext with offline:false, enabling the runtime to fetch and staple OCSP responses during the TLS handshake. NatsServer applies the context at startup when OcspConfig.Mode is not Never. Ten unit tests cover the config defaults, mode ordinals, and the null-return invariants of BuildCertificateContext.
This commit is contained in:
@@ -276,6 +276,19 @@ public sealed class NatsServer : IMessageRouter, ISubListAccess, IDisposable
|
|||||||
if (options.HasTls)
|
if (options.HasTls)
|
||||||
{
|
{
|
||||||
_sslOptions = TlsHelper.BuildServerAuthOptions(options);
|
_sslOptions = TlsHelper.BuildServerAuthOptions(options);
|
||||||
|
|
||||||
|
// OCSP stapling: build a certificate context so the runtime can
|
||||||
|
// fetch and cache a fresh OCSP response and staple it during the
|
||||||
|
// TLS handshake. offline:false tells the runtime to contact the
|
||||||
|
// OCSP responder; if the responder is unreachable we fall back to
|
||||||
|
// no stapling rather than refusing all connections.
|
||||||
|
var certContext = TlsHelper.BuildCertificateContext(options, offline: false);
|
||||||
|
if (certContext != null)
|
||||||
|
{
|
||||||
|
_sslOptions.ServerCertificateContext = certContext;
|
||||||
|
_logger.LogInformation("OCSP stapling enabled (mode: {OcspMode})", options.OcspConfig!.Mode);
|
||||||
|
}
|
||||||
|
|
||||||
_serverInfo.TlsRequired = !options.AllowNonTls;
|
_serverInfo.TlsRequired = !options.AllowNonTls;
|
||||||
_serverInfo.TlsAvailable = options.AllowNonTls;
|
_serverInfo.TlsAvailable = options.AllowNonTls;
|
||||||
_serverInfo.TlsVerify = options.TlsVerify;
|
_serverInfo.TlsVerify = options.TlsVerify;
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ public static class TlsHelper
|
|||||||
|
|
||||||
if (opts.TlsVerify && opts.TlsCaCert != null)
|
if (opts.TlsVerify && opts.TlsCaCert != null)
|
||||||
{
|
{
|
||||||
|
var revocationMode = opts.OcspPeerVerify
|
||||||
|
? X509RevocationMode.Online
|
||||||
|
: X509RevocationMode.NoCheck;
|
||||||
|
|
||||||
var caCerts = LoadCaCertificates(opts.TlsCaCert);
|
var caCerts = LoadCaCertificates(opts.TlsCaCert);
|
||||||
authOpts.RemoteCertificateValidationCallback = (_, cert, chain, errors) =>
|
authOpts.RemoteCertificateValidationCallback = (_, cert, chain, errors) =>
|
||||||
{
|
{
|
||||||
@@ -41,7 +45,19 @@ public static class TlsHelper
|
|||||||
chain2.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
|
chain2.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
|
||||||
foreach (var ca in caCerts)
|
foreach (var ca in caCerts)
|
||||||
chain2.ChainPolicy.CustomTrustStore.Add(ca);
|
chain2.ChainPolicy.CustomTrustStore.Add(ca);
|
||||||
chain2.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
|
chain2.ChainPolicy.RevocationMode = revocationMode;
|
||||||
|
var cert2 = cert as X509Certificate2 ?? X509CertificateLoader.LoadCertificate(cert.GetRawCertData());
|
||||||
|
return chain2.Build(cert2);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if (opts.OcspPeerVerify)
|
||||||
|
{
|
||||||
|
// No custom CA — still enable online revocation checking against the system store
|
||||||
|
authOpts.RemoteCertificateValidationCallback = (_, cert, chain, errors) =>
|
||||||
|
{
|
||||||
|
if (cert == null) return false;
|
||||||
|
using var chain2 = new X509Chain();
|
||||||
|
chain2.ChainPolicy.RevocationMode = X509RevocationMode.Online;
|
||||||
var cert2 = cert as X509Certificate2 ?? X509CertificateLoader.LoadCertificate(cert.GetRawCertData());
|
var cert2 = cert as X509Certificate2 ?? X509CertificateLoader.LoadCertificate(cert.GetRawCertData());
|
||||||
return chain2.Build(cert2);
|
return chain2.Build(cert2);
|
||||||
};
|
};
|
||||||
@@ -50,6 +66,25 @@ public static class TlsHelper
|
|||||||
return authOpts;
|
return authOpts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds an <see cref="SslStreamCertificateContext"/> for OCSP stapling.
|
||||||
|
/// Returns null when TLS is not configured or OCSP mode is Never.
|
||||||
|
/// When <paramref name="offline"/> is false the runtime will contact the
|
||||||
|
/// certificate's OCSP responder to obtain a fresh stapled response.
|
||||||
|
/// </summary>
|
||||||
|
public static SslStreamCertificateContext? BuildCertificateContext(NatsOptions opts, bool offline = false)
|
||||||
|
{
|
||||||
|
if (!opts.HasTls) return null;
|
||||||
|
if (opts.OcspConfig is null || opts.OcspConfig.Mode == OcspMode.Never) return null;
|
||||||
|
|
||||||
|
var cert = LoadCertificate(opts.TlsCert!, opts.TlsKey);
|
||||||
|
var chain = new X509Certificate2Collection();
|
||||||
|
if (!string.IsNullOrEmpty(opts.TlsCaCert))
|
||||||
|
chain.ImportFromPemFile(opts.TlsCaCert);
|
||||||
|
|
||||||
|
return SslStreamCertificateContext.Create(cert, chain, offline: offline);
|
||||||
|
}
|
||||||
|
|
||||||
public static string GetCertificateHash(X509Certificate2 cert)
|
public static string GetCertificateHash(X509Certificate2 cert)
|
||||||
{
|
{
|
||||||
var spki = cert.PublicKey.ExportSubjectPublicKeyInfo();
|
var spki = cert.PublicKey.ExportSubjectPublicKeyInfo();
|
||||||
|
|||||||
97
tests/NATS.Server.Tests/OcspStaplingTests.cs
Normal file
97
tests/NATS.Server.Tests/OcspStaplingTests.cs
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
using NATS.Server.Tls;
|
||||||
|
|
||||||
|
namespace NATS.Server.Tests;
|
||||||
|
|
||||||
|
public class OcspStaplingTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void OcspMode_Must_is_strictest()
|
||||||
|
{
|
||||||
|
var config = new OcspConfig { Mode = OcspMode.Must };
|
||||||
|
config.Mode.ShouldBe(OcspMode.Must);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OcspMode_Never_disables_all()
|
||||||
|
{
|
||||||
|
var config = new OcspConfig { Mode = OcspMode.Never };
|
||||||
|
config.Mode.ShouldBe(OcspMode.Never);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OcspPeerVerify_default_is_false()
|
||||||
|
{
|
||||||
|
var options = new NatsOptions();
|
||||||
|
options.OcspPeerVerify.ShouldBeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OcspConfig_default_mode_is_Auto()
|
||||||
|
{
|
||||||
|
var config = new OcspConfig();
|
||||||
|
config.Mode.ShouldBe(OcspMode.Auto);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OcspConfig_default_OverrideUrls_is_empty()
|
||||||
|
{
|
||||||
|
var config = new OcspConfig();
|
||||||
|
config.OverrideUrls.ShouldBeEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildCertificateContext_returns_null_when_no_tls()
|
||||||
|
{
|
||||||
|
var options = new NatsOptions
|
||||||
|
{
|
||||||
|
OcspConfig = new OcspConfig { Mode = OcspMode.Always },
|
||||||
|
};
|
||||||
|
// HasTls is false because TlsCert and TlsKey are not set
|
||||||
|
options.HasTls.ShouldBeFalse();
|
||||||
|
var context = TlsHelper.BuildCertificateContext(options);
|
||||||
|
context.ShouldBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildCertificateContext_returns_null_when_mode_is_Never()
|
||||||
|
{
|
||||||
|
var options = new NatsOptions
|
||||||
|
{
|
||||||
|
TlsCert = "server.pem",
|
||||||
|
TlsKey = "server-key.pem",
|
||||||
|
OcspConfig = new OcspConfig { Mode = OcspMode.Never },
|
||||||
|
};
|
||||||
|
// OcspMode.Never must short-circuit even when TLS cert paths are set
|
||||||
|
var context = TlsHelper.BuildCertificateContext(options);
|
||||||
|
context.ShouldBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void BuildCertificateContext_returns_null_when_OcspConfig_is_null()
|
||||||
|
{
|
||||||
|
var options = new NatsOptions
|
||||||
|
{
|
||||||
|
TlsCert = "server.pem",
|
||||||
|
TlsKey = "server-key.pem",
|
||||||
|
OcspConfig = null,
|
||||||
|
};
|
||||||
|
var context = TlsHelper.BuildCertificateContext(options);
|
||||||
|
context.ShouldBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OcspPeerVerify_can_be_enabled()
|
||||||
|
{
|
||||||
|
var options = new NatsOptions { OcspPeerVerify = true };
|
||||||
|
options.OcspPeerVerify.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OcspMode_values_have_correct_ordinals()
|
||||||
|
{
|
||||||
|
((int)OcspMode.Auto).ShouldBe(0);
|
||||||
|
((int)OcspMode.Always).ShouldBe(1);
|
||||||
|
((int)OcspMode.Must).ShouldBe(2);
|
||||||
|
((int)OcspMode.Never).ShouldBe(3);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user