using System.Net.Security;
using System.Formats.Asn1;
using System.Text.RegularExpressions;
using System.Security.Authentication;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
namespace NATS.Server.Tls;
public static class TlsHelper
{
private const string AuthorityInfoAccessOid = "1.3.6.1.5.5.7.1.1";
private const string OcspAccessMethodOid = "1.3.6.1.5.5.7.48.1";
private const string OcspSigningEkuOid = "1.3.6.1.5.5.7.3.9";
public static X509Certificate2 LoadCertificate(string certPath, string? keyPath)
{
if (keyPath != null)
return X509Certificate2.CreateFromPemFile(certPath, keyPath);
return X509CertificateLoader.LoadCertificateFromFile(certPath);
}
public static X509Certificate2Collection LoadCaCertificates(string caPath)
{
var pem = File.ReadAllText(caPath);
return ParseCertPem(pem);
}
///
/// Parses one or more PEM blocks and requires all blocks to be CERTIFICATE.
/// Mirrors Go parseCertPEM behavior by rejecting unexpected block types.
///
public static X509Certificate2Collection ParseCertPem(string pemData)
{
if (string.IsNullOrWhiteSpace(pemData))
throw new InvalidDataException("PEM data is empty.");
var beginMatches = Regex.Matches(pemData, "-----BEGIN ([^-]+)-----");
if (beginMatches.Count == 0)
throw new InvalidDataException("No PEM certificate block found.");
foreach (Match match in beginMatches)
{
var label = match.Groups[1].Value;
if (!string.Equals(label, "CERTIFICATE", StringComparison.Ordinal))
throw new InvalidDataException($"unexpected PEM certificate type: {label}");
}
var certs = new X509Certificate2Collection();
var certMatches = Regex.Matches(
pemData,
"-----BEGIN CERTIFICATE-----\\s*(?
[A-Za-z0-9+/=\\r\\n]+?)\\s*-----END CERTIFICATE-----",
RegexOptions.Singleline);
foreach (Match certMatch in certMatches)
{
var body = certMatch.Groups["body"].Value;
var normalized = Regex.Replace(body, "\\s+", "", RegexOptions.Singleline);
var der = Convert.FromBase64String(normalized);
certs.Add(X509CertificateLoader.LoadCertificate(der));
}
if (certs.Count == 0)
throw new InvalidDataException("No PEM certificate block found.");
return certs;
}
public static SslServerAuthenticationOptions BuildServerAuthOptions(NatsOptions opts)
{
var cert = LoadCertificate(opts.TlsCert!, opts.TlsKey);
var authOpts = new SslServerAuthenticationOptions
{
ServerCertificate = cert,
EnabledSslProtocols = opts.TlsMinVersion,
ClientCertificateRequired = opts.TlsVerify,
};
if (opts.TlsVerify && opts.TlsCaCert != null)
{
var revocationMode = opts.OcspPeerVerify
? X509RevocationMode.Online
: X509RevocationMode.NoCheck;
var caCerts = LoadCaCertificates(opts.TlsCaCert);
authOpts.RemoteCertificateValidationCallback = (_, cert, chain, errors) =>
{
if (cert == null) return false;
using var chain2 = new X509Chain();
chain2.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
foreach (var ca in caCerts)
chain2.ChainPolicy.CustomTrustStore.Add(ca);
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());
return chain2.Build(cert2);
};
}
return authOpts;
}
///
/// Builds an for OCSP stapling.
/// Returns null when TLS is not configured or OCSP mode is Never.
/// When is false the runtime will contact the
/// certificate's OCSP responder to obtain a fresh stapled response.
///
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)
{
var spki = cert.PublicKey.ExportSubjectPublicKeyInfo();
var hash = SHA256.HashData(spki);
return Convert.ToHexStringLower(hash);
}
public static string GenerateFingerprint(X509Certificate2 cert)
{
var hash = SHA256.HashData(cert.RawData);
return Convert.ToBase64String(hash);
}
public static IReadOnlyList GetWebEndpoints(IEnumerable uris)
{
var urls = new List();
foreach (var uri in uris)
{
if (!Uri.TryCreate(uri, UriKind.Absolute, out var endpoint))
continue;
if (!string.Equals(endpoint.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase) &&
!string.Equals(endpoint.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
continue;
urls.Add(endpoint);
}
return urls;
}
public static string GetSubjectDNForm(X509Certificate2? cert)
{
return cert?.SubjectName.Name ?? string.Empty;
}
public static string GetIssuerDNForm(X509Certificate2? cert)
{
return cert?.IssuerName.Name ?? string.Empty;
}
public static bool MatchesPinnedCert(X509Certificate2 cert, HashSet pinned)
{
var hash = GetCertificateHash(cert);
return pinned.Contains(hash);
}
///
/// Checks if a chain link is eligible for OCSP validation by ensuring the leaf
/// certificate includes at least one valid HTTP(S) OCSP AIA endpoint.
///
public static bool CertOCSPEligible(ChainLink? link)
{
if (link?.Leaf is null)
return false;
if (link.Leaf.RawData is null || link.Leaf.RawData.Length == 0)
return false;
var aiaUris = GetOcspResponderUris(link.Leaf);
if (aiaUris.Count == 0)
return false;
var urls = GetWebEndpoints(aiaUris);
if (urls.Count == 0)
return false;
link.OCSPWebEndpoints = urls;
return true;
}
///
/// Returns the positional issuer certificate for a leaf in a verified chain.
///
public static X509Certificate2? GetLeafIssuerCert(IReadOnlyList? chain, int leafPos)
{
if (chain is null || chain.Count == 0 || leafPos < 0 || leafPos >= chain.Count - 1)
return null;
return chain[leafPos + 1];
}
///
/// Equivalent to Go certstore.GetLeafIssuer: verifies the leaf against the
/// supplied trust root and returns the first issuer in the verified chain.
///
public static X509Certificate2? GetLeafIssuer(X509Certificate2 leaf, X509Certificate2 trustedRoot)
{
using var chain = new X509Chain();
chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
chain.ChainPolicy.CustomTrustStore.Add(trustedRoot);
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
if (!chain.Build(leaf) || chain.ChainElements.Count < 2)
return null;
return X509CertificateLoader.LoadCertificate(chain.ChainElements[1].Certificate.RawData);
}
///
/// Checks OCSP response currency semantics with clock skew and fallback TTL.
///
public static bool OcspResponseCurrent(OcspResponseInfo response, OCSPPeerConfig opts)
{
var skew = TimeSpan.FromSeconds(opts.ClockSkew);
if (skew < TimeSpan.Zero)
skew = OCSPPeerConfig.DefaultAllowedClockSkew;
var now = DateTime.UtcNow;
if (response.NextUpdate.HasValue && response.NextUpdate.Value < now - skew)
return false;
if (!response.NextUpdate.HasValue)
{
var ttl = TimeSpan.FromSeconds(opts.TTLUnsetNextUpdate);
if (ttl < TimeSpan.Zero)
ttl = OCSPPeerConfig.DefaultTTLUnsetNextUpdate;
if (response.ThisUpdate + ttl < now - skew)
return false;
}
if (response.ThisUpdate > now + skew)
return false;
return true;
}
///
/// Validates OCSP delegated signer semantics. Direct issuer signatures are valid;
/// delegated certificates must include id-kp-OCSPSigning EKU.
///
public static bool ValidDelegationCheck(X509Certificate2? issuer, X509Certificate2? responderCertificate)
{
if (issuer is null)
return false;
if (responderCertificate is null)
return true;
if (responderCertificate.Thumbprint == issuer.Thumbprint)
return true;
foreach (var extension in responderCertificate.Extensions)
{
if (extension is not X509EnhancedKeyUsageExtension eku)
continue;
foreach (var oid in eku.EnhancedKeyUsages)
{
if (oid.Value == OcspSigningEkuOid)
return true;
}
}
return false;
}
[SlopwatchSuppress("SW003", "AsnContentException on a malformed AIA extension is intentionally swallowed; invalid extension shape means no usable OCSP URI")]
private static IReadOnlyList GetOcspResponderUris(X509Certificate2 cert)
{
var uris = new List();
foreach (var extension in cert.Extensions)
{
if (!string.Equals(extension.Oid?.Value, AuthorityInfoAccessOid, StringComparison.Ordinal))
continue;
try
{
var reader = new AsnReader(extension.RawData, AsnEncodingRules.DER);
var seq = reader.ReadSequence();
while (seq.HasData)
{
var accessDescription = seq.ReadSequence();
var accessMethod = accessDescription.ReadObjectIdentifier();
if (!string.Equals(accessMethod, OcspAccessMethodOid, StringComparison.Ordinal))
{
accessDescription.ThrowIfNotEmpty();
continue;
}
var uri = accessDescription.ReadCharacterString(
UniversalTagNumber.IA5String,
new Asn1Tag(TagClass.ContextSpecific, 6));
accessDescription.ThrowIfNotEmpty();
if (!string.IsNullOrWhiteSpace(uri))
uris.Add(uri);
}
seq.ThrowIfNotEmpty();
reader.ThrowIfNotEmpty();
}
catch (AsnContentException ex)
{
// Invalid AIA extension shape should behave as "no usable OCSP URI" — swallow is intentional.
_ = ex.Message;
}
}
return uris;
}
}