feat(batch9): implement f4 ocsp peer validation hooks

This commit is contained in:
Joseph Doherty
2026-02-28 12:34:38 -05:00
parent 87b4363eeb
commit 41b01743fd
6 changed files with 573 additions and 0 deletions

View File

@@ -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)
{
foreach (var extension in cert.Extensions)
@@ -249,6 +353,33 @@ internal static class OcspHandler
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
{
public int Status { get; set; }

View File

@@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0
using System.Net.Security;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using ZB.MOM.NatsNet.Server.Auth.CertificateIdentityProvider;
using ZB.MOM.NatsNet.Server.Auth.Ocsp;
@@ -397,4 +398,236 @@ public sealed partial class NatsServer
StartOCSPMonitoring();
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);
}
}

View File

@@ -118,6 +118,7 @@ public class TlsConfigOpts
public List<string> CaCertsMatch { get; set; } = [];
public List<TlsCertPairOpt> Certificates { get; set; } = [];
public SslProtocols MinVersion { get; set; }
public Auth.CertificateIdentityProvider.OcspPeerConfig? OcspPeerConfig { get; set; }
}
/// <summary>