feat(batch9): implement f4 ocsp peer validation hooks
This commit is contained in:
@@ -53,6 +53,110 @@ internal static class OcspHandler
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static (OcspPeerConfig? config, Exception? error) ParseOCSPPeer(object? value)
|
||||||
|
{
|
||||||
|
if (value is not IDictionary<string, object?> map)
|
||||||
|
{
|
||||||
|
return (null, new InvalidOperationException(
|
||||||
|
string.Format(CultureInfo.InvariantCulture, OcspMessages.ErrIllegalPeerOptsConfig, value ?? "null")));
|
||||||
|
}
|
||||||
|
|
||||||
|
var config = OcspPeerConfig.Create();
|
||||||
|
foreach (var (key, raw) in map)
|
||||||
|
{
|
||||||
|
switch (key.ToLowerInvariant())
|
||||||
|
{
|
||||||
|
case "verify":
|
||||||
|
if (raw is not bool verify)
|
||||||
|
{
|
||||||
|
return (null, new InvalidOperationException(
|
||||||
|
string.Format(CultureInfo.InvariantCulture, OcspMessages.ErrParsingPeerOptFieldGeneric, key)));
|
||||||
|
}
|
||||||
|
config.Verify = verify;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "allowed_clockskew":
|
||||||
|
var (clockSkew, skewError) = ParsePeerDurationValue(raw);
|
||||||
|
if (skewError != null)
|
||||||
|
{
|
||||||
|
return (null, skewError);
|
||||||
|
}
|
||||||
|
if (clockSkew >= 0)
|
||||||
|
{
|
||||||
|
config.ClockSkew = clockSkew;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "ca_timeout":
|
||||||
|
var (timeout, timeoutError) = ParsePeerDurationValue(raw);
|
||||||
|
if (timeoutError != null)
|
||||||
|
{
|
||||||
|
return (null, timeoutError);
|
||||||
|
}
|
||||||
|
if (timeout >= 0)
|
||||||
|
{
|
||||||
|
config.Timeout = timeout;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "cache_ttl_when_next_update_unset":
|
||||||
|
var (ttl, ttlError) = ParsePeerDurationValue(raw);
|
||||||
|
if (ttlError != null)
|
||||||
|
{
|
||||||
|
return (null, ttlError);
|
||||||
|
}
|
||||||
|
if (ttl >= 0)
|
||||||
|
{
|
||||||
|
config.TTLUnsetNextUpdate = ttl;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "warn_only":
|
||||||
|
if (raw is not bool warnOnly)
|
||||||
|
{
|
||||||
|
return (null, new InvalidOperationException(
|
||||||
|
string.Format(CultureInfo.InvariantCulture, OcspMessages.ErrParsingPeerOptFieldGeneric, key)));
|
||||||
|
}
|
||||||
|
config.WarnOnly = warnOnly;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "unknown_is_good":
|
||||||
|
if (raw is not bool unknownIsGood)
|
||||||
|
{
|
||||||
|
return (null, new InvalidOperationException(
|
||||||
|
string.Format(CultureInfo.InvariantCulture, OcspMessages.ErrParsingPeerOptFieldGeneric, key)));
|
||||||
|
}
|
||||||
|
config.UnknownIsGood = unknownIsGood;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "allow_when_ca_unreachable":
|
||||||
|
if (raw is not bool allowWhenCaUnreachable)
|
||||||
|
{
|
||||||
|
return (null, new InvalidOperationException(
|
||||||
|
string.Format(CultureInfo.InvariantCulture, OcspMessages.ErrParsingPeerOptFieldGeneric, key)));
|
||||||
|
}
|
||||||
|
config.AllowWhenCAUnreachable = allowWhenCaUnreachable;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return (null, new InvalidOperationException(
|
||||||
|
string.Format(CultureInfo.InvariantCulture, OcspMessages.ErrParsingPeerOptFieldGeneric, key)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (config, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static X509Certificate2? PeerFromVerifiedChains(X509Certificate2[][] chains)
|
||||||
|
{
|
||||||
|
if (chains.Length == 0 || chains[0].Length == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return chains[0][0];
|
||||||
|
}
|
||||||
|
|
||||||
internal static bool HasOCSPStatusRequest(X509Certificate2 cert)
|
internal static bool HasOCSPStatusRequest(X509Certificate2 cert)
|
||||||
{
|
{
|
||||||
foreach (var extension in cert.Extensions)
|
foreach (var extension in cert.Extensions)
|
||||||
@@ -249,6 +353,33 @@ internal static class OcspHandler
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static (double value, Exception? error) ParsePeerDurationValue(object? value)
|
||||||
|
{
|
||||||
|
return value switch
|
||||||
|
{
|
||||||
|
null => (0, new InvalidOperationException(
|
||||||
|
string.Format(CultureInfo.InvariantCulture, OcspMessages.ErrParsingPeerOptFieldTypeConversion, "unexpected type"))),
|
||||||
|
int i => (i, null),
|
||||||
|
long l => (l, null),
|
||||||
|
float f => (f, null),
|
||||||
|
double d => (d, null),
|
||||||
|
string s => ParseDurationSeconds(s),
|
||||||
|
_ => (0, new InvalidOperationException(
|
||||||
|
string.Format(CultureInfo.InvariantCulture, OcspMessages.ErrParsingPeerOptFieldTypeConversion, "unexpected type"))),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (double value, Exception? error) ParseDurationSeconds(string duration)
|
||||||
|
{
|
||||||
|
if (TimeSpan.TryParse(duration, CultureInfo.InvariantCulture, out var parsed))
|
||||||
|
{
|
||||||
|
return (parsed.TotalSeconds, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (0, new InvalidOperationException(
|
||||||
|
string.Format(CultureInfo.InvariantCulture, OcspMessages.ErrParsingPeerOptFieldTypeConversion, "unexpected type")));
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class SerializedOcspResponse
|
private sealed class SerializedOcspResponse
|
||||||
{
|
{
|
||||||
public int Status { get; set; }
|
public int Status { get; set; }
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// Licensed under the Apache License, Version 2.0
|
// Licensed under the Apache License, Version 2.0
|
||||||
|
|
||||||
using System.Net.Security;
|
using System.Net.Security;
|
||||||
|
using System.Security.Cryptography;
|
||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Security.Cryptography.X509Certificates;
|
||||||
using ZB.MOM.NatsNet.Server.Auth.CertificateIdentityProvider;
|
using ZB.MOM.NatsNet.Server.Auth.CertificateIdentityProvider;
|
||||||
using ZB.MOM.NatsNet.Server.Auth.Ocsp;
|
using ZB.MOM.NatsNet.Server.Auth.Ocsp;
|
||||||
@@ -397,4 +398,236 @@ public sealed partial class NatsServer
|
|||||||
StartOCSPMonitoring();
|
StartOCSPMonitoring();
|
||||||
return null;
|
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<X509Certificate2>()
|
||||||
|
.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<X509Certificate2>()
|
||||||
|
.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<ChainLink>();
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ public class TlsConfigOpts
|
|||||||
public List<string> CaCertsMatch { get; set; } = [];
|
public List<string> CaCertsMatch { get; set; } = [];
|
||||||
public List<TlsCertPairOpt> Certificates { get; set; } = [];
|
public List<TlsCertPairOpt> Certificates { get; set; } = [];
|
||||||
public SslProtocols MinVersion { get; set; }
|
public SslProtocols MinVersion { get; set; }
|
||||||
|
public Auth.CertificateIdentityProvider.OcspPeerConfig? OcspPeerConfig { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using Shouldly;
|
using Shouldly;
|
||||||
|
using ZB.MOM.NatsNet.Server;
|
||||||
using ZB.MOM.NatsNet.Server.Auth.CertificateIdentityProvider;
|
using ZB.MOM.NatsNet.Server.Auth.CertificateIdentityProvider;
|
||||||
|
|
||||||
namespace ZB.MOM.NatsNet.Server.Tests.Auth.CertificateIdentityProvider;
|
namespace ZB.MOM.NatsNet.Server.Tests.Auth.CertificateIdentityProvider;
|
||||||
@@ -36,4 +37,18 @@ public sealed class CertificateIdentityProviderTests
|
|||||||
var decoded = Convert.FromBase64String(unescaped);
|
var decoded = Convert.FromBase64String(unescaped);
|
||||||
decoded.ShouldBe(data);
|
decoded.ShouldBe(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseOCSPPeer_UnknownField_ReturnsError()
|
||||||
|
{
|
||||||
|
Dictionary<string, object?> map = new()
|
||||||
|
{
|
||||||
|
["unexpected"] = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
var (config, err) = OcspHandler.ParseOCSPPeer(map);
|
||||||
|
|
||||||
|
config.ShouldBeNull();
|
||||||
|
err.ShouldNotBeNull();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
using System.Net.Security;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Security.Cryptography.X509Certificates;
|
||||||
|
using Shouldly;
|
||||||
|
using ZB.MOM.NatsNet.Server;
|
||||||
|
using ZB.MOM.NatsNet.Server.Auth.CertificateIdentityProvider;
|
||||||
|
|
||||||
|
namespace ZB.MOM.NatsNet.Server.Tests.Auth;
|
||||||
|
|
||||||
|
public sealed class OcspPeerValidationTests : IDisposable
|
||||||
|
{
|
||||||
|
private readonly List<X509Certificate2> _certs = [];
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParseOCSPPeer_ValidMap_ReturnsConfig()
|
||||||
|
{
|
||||||
|
Dictionary<string, object?> map = new()
|
||||||
|
{
|
||||||
|
["verify"] = true,
|
||||||
|
["allowed_clockskew"] = 30.0,
|
||||||
|
["ca_timeout"] = 5.0,
|
||||||
|
["cache_ttl_when_next_update_unset"] = 120.0,
|
||||||
|
["warn_only"] = true,
|
||||||
|
["unknown_is_good"] = true,
|
||||||
|
["allow_when_ca_unreachable"] = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
var (config, err) = OcspHandler.ParseOCSPPeer(map);
|
||||||
|
|
||||||
|
err.ShouldBeNull();
|
||||||
|
config.ShouldNotBeNull();
|
||||||
|
config.Verify.ShouldBeTrue();
|
||||||
|
config.ClockSkew.ShouldBe(30.0);
|
||||||
|
config.Timeout.ShouldBe(5.0);
|
||||||
|
config.TTLUnsetNextUpdate.ShouldBe(120.0);
|
||||||
|
config.WarnOnly.ShouldBeTrue();
|
||||||
|
config.UnknownIsGood.ShouldBeTrue();
|
||||||
|
config.AllowWhenCAUnreachable.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PeerFromVerifiedChains_EmptyChains_ReturnsNull()
|
||||||
|
{
|
||||||
|
OcspHandler.PeerFromVerifiedChains([]).ShouldBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PeerFromVerifiedChains_NonEmptyChains_ReturnsFirstLeaf()
|
||||||
|
{
|
||||||
|
var cert = CreateSelfSignedCertificate("CN=peer-first");
|
||||||
|
|
||||||
|
var result = OcspHandler.PeerFromVerifiedChains([[cert]]);
|
||||||
|
|
||||||
|
result.ShouldNotBeNull();
|
||||||
|
result!.Subject.ShouldBe(cert.Subject);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PlugTLSOCSPPeer_ClientWithoutVerify_ReturnsError()
|
||||||
|
{
|
||||||
|
var server = NewServer();
|
||||||
|
var cert = CreateSelfSignedCertificate("CN=client-no-verify");
|
||||||
|
var config = new OcspTlsConfig
|
||||||
|
{
|
||||||
|
Kind = "client",
|
||||||
|
TlsConfig = new SslServerAuthenticationOptions { ServerCertificate = cert },
|
||||||
|
TlsOptions = new TlsConfigOpts
|
||||||
|
{
|
||||||
|
Verify = false,
|
||||||
|
OcspPeerConfig = new OcspPeerConfig { Verify = true },
|
||||||
|
},
|
||||||
|
Apply = _ => { },
|
||||||
|
};
|
||||||
|
|
||||||
|
var (_, plugged, err) = server.PlugTLSOCSPPeer(config);
|
||||||
|
|
||||||
|
plugged.ShouldBeFalse();
|
||||||
|
err.ShouldNotBeNull();
|
||||||
|
err!.Message.ShouldContain("mTLS");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PlugTLSOCSPPeer_ClientWithVerify_ReturnsPlugged()
|
||||||
|
{
|
||||||
|
var server = NewServer();
|
||||||
|
var cert = CreateSelfSignedCertificate("CN=client-verify");
|
||||||
|
var config = new OcspTlsConfig
|
||||||
|
{
|
||||||
|
Kind = "client",
|
||||||
|
TlsConfig = new SslServerAuthenticationOptions { ServerCertificate = cert },
|
||||||
|
TlsOptions = new TlsConfigOpts
|
||||||
|
{
|
||||||
|
Verify = true,
|
||||||
|
OcspPeerConfig = new OcspPeerConfig { Verify = true },
|
||||||
|
},
|
||||||
|
Apply = _ => { },
|
||||||
|
};
|
||||||
|
|
||||||
|
var (tlsConfig, plugged, err) = server.PlugTLSOCSPPeer(config);
|
||||||
|
|
||||||
|
err.ShouldBeNull();
|
||||||
|
plugged.ShouldBeTrue();
|
||||||
|
tlsConfig.ShouldNotBeNull();
|
||||||
|
tlsConfig!.RemoteCertificateValidationCallback.ShouldNotBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PlugTLSOCSPPeer_LeafSpoke_ReturnsPlugged()
|
||||||
|
{
|
||||||
|
var server = NewServer();
|
||||||
|
var cert = CreateSelfSignedCertificate("CN=leaf-spoke");
|
||||||
|
var config = new OcspTlsConfig
|
||||||
|
{
|
||||||
|
Kind = "leaf",
|
||||||
|
IsLeafSpoke = true,
|
||||||
|
TlsConfig = new SslServerAuthenticationOptions { ServerCertificate = cert },
|
||||||
|
TlsOptions = new TlsConfigOpts
|
||||||
|
{
|
||||||
|
Verify = true,
|
||||||
|
OcspPeerConfig = new OcspPeerConfig { Verify = true },
|
||||||
|
},
|
||||||
|
Apply = _ => { },
|
||||||
|
};
|
||||||
|
|
||||||
|
var (_, plugged, err) = server.PlugTLSOCSPPeer(config);
|
||||||
|
|
||||||
|
err.ShouldBeNull();
|
||||||
|
plugged.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TlsServerOCSPValid_SelfSignedChain_ReturnsTrue()
|
||||||
|
{
|
||||||
|
var server = NewServer();
|
||||||
|
var cert = CreateSelfSignedCertificate("CN=selfsigned");
|
||||||
|
var opts = new OcspPeerConfig { Verify = true };
|
||||||
|
|
||||||
|
var valid = server.TlsServerOCSPValid([[cert]], opts);
|
||||||
|
|
||||||
|
valid.ShouldBeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TlsClientOCSPValid_EmptyChains_ReturnsFalse()
|
||||||
|
{
|
||||||
|
var server = NewServer();
|
||||||
|
var opts = new OcspPeerConfig { Verify = true };
|
||||||
|
|
||||||
|
var valid = server.TlsClientOCSPValid([], opts);
|
||||||
|
|
||||||
|
valid.ShouldBeFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CertOCSPGood_EmptyChainLink_ReturnsFalse()
|
||||||
|
{
|
||||||
|
var server = NewServer();
|
||||||
|
var (reason, good) = server.CertOCSPGood(new ChainLink(), new OcspPeerConfig());
|
||||||
|
|
||||||
|
good.ShouldBeFalse();
|
||||||
|
reason.ShouldContain("Empty chainlink");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
foreach (var cert in _certs)
|
||||||
|
{
|
||||||
|
cert.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private NatsServer NewServer()
|
||||||
|
{
|
||||||
|
var (server, err) = NatsServer.NewServer(new ServerOptions());
|
||||||
|
err.ShouldBeNull();
|
||||||
|
return server!;
|
||||||
|
}
|
||||||
|
|
||||||
|
private X509Certificate2 CreateSelfSignedCertificate(string subject)
|
||||||
|
{
|
||||||
|
var request = new CertificateRequest(
|
||||||
|
subject,
|
||||||
|
RSA.Create(2048),
|
||||||
|
HashAlgorithmName.SHA256,
|
||||||
|
RSASignaturePadding.Pkcs1);
|
||||||
|
request.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, true));
|
||||||
|
request.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(request.PublicKey, false));
|
||||||
|
|
||||||
|
var cert = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(30));
|
||||||
|
_certs.Add(cert);
|
||||||
|
return cert;
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
porting.db
BIN
porting.db
Binary file not shown.
Reference in New Issue
Block a user