using System.Formats.Asn1; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using NATS.Server.TestUtilities; using NATS.Server.Tls; namespace NATS.Server.Transport.Tests; public class TlsOcspParityBatch2Tests { [Fact] public void CertOCSPEligible_returns_true_and_populates_endpoints_for_http_ocsp_aia() { using var cert = CreateLeafWithOcspAia("http://ocsp.example.test"); var link = new ChainLink { Leaf = cert }; var eligible = TlsHelper.CertOCSPEligible(link); eligible.ShouldBeTrue(); link.OCSPWebEndpoints.ShouldNotBeNull(); link.OCSPWebEndpoints!.Count.ShouldBe(1); link.OCSPWebEndpoints[0].ToString().ShouldBe("http://ocsp.example.test/"); } [Fact] public void CertOCSPEligible_returns_false_when_leaf_has_no_ocsp_servers() { var (leaf, _) = TestCertHelper.GenerateTestCert(); var link = new ChainLink { Leaf = leaf }; TlsHelper.CertOCSPEligible(link).ShouldBeFalse(); } [Fact] public void GetLeafIssuerCert_returns_positional_issuer_or_null() { using var root = CreateRootCertificate(); using var leaf = CreateLeafSignedBy(root); var chain = new[] { leaf, root }; TlsHelper.GetLeafIssuerCert(chain, 0).ShouldBe(root); TlsHelper.GetLeafIssuerCert(chain, 1).ShouldBeNull(); TlsHelper.GetLeafIssuerCert(chain, -1).ShouldBeNull(); } [Fact] public void GetLeafIssuer_returns_verified_issuer_from_chain() { using var root = CreateRootCertificate(); using var leaf = CreateLeafSignedBy(root); using var issuer = TlsHelper.GetLeafIssuer(leaf, root); issuer.ShouldNotBeNull(); issuer!.Thumbprint.ShouldBe(root.Thumbprint); } [Fact] public void OcspResponseCurrent_applies_skew_and_ttl_rules() { var opts = OCSPPeerConfig.NewOCSPPeerConfig(); var now = DateTime.UtcNow; TlsHelper.OcspResponseCurrent(new OcspResponseInfo { ThisUpdate = now.AddMinutes(-1), NextUpdate = now.AddMinutes(5), }, opts).ShouldBeTrue(); TlsHelper.OcspResponseCurrent(new OcspResponseInfo { ThisUpdate = now.AddHours(-2), NextUpdate = null, }, opts).ShouldBeFalse(); TlsHelper.OcspResponseCurrent(new OcspResponseInfo { ThisUpdate = now.AddMinutes(2), NextUpdate = now.AddHours(1), }, opts).ShouldBeFalse(); } [Fact] public void ValidDelegationCheck_accepts_direct_and_ocsp_signing_delegate() { using var issuer = CreateRootCertificate(); using var delegateCert = CreateOcspSigningDelegate(issuer); TlsHelper.ValidDelegationCheck(issuer, null).ShouldBeTrue(); TlsHelper.ValidDelegationCheck(issuer, issuer).ShouldBeTrue(); TlsHelper.ValidDelegationCheck(issuer, delegateCert).ShouldBeTrue(); } [Fact] public void OcspPeerMessages_exposes_error_and_debug_constants() { OcspPeerMessages.ErrIllegalPeerOptsConfig.ShouldContain("expected map to define OCSP peer options"); OcspPeerMessages.ErrNoAvailOCSPServers.ShouldBe("no available OCSP servers"); OcspPeerMessages.DbgPlugTLSForKind.ShouldBe("Plugging TLS OCSP peer for [%s]"); OcspPeerMessages.DbgCacheSaved.ShouldBe("Saved OCSP peer cache successfully (%d bytes)"); OcspPeerMessages.MsgFailedOCSPResponseFetch.ShouldBe("Failed OCSP response fetch"); } private static X509Certificate2 CreateLeafWithOcspAia(string ocspUri) { using var key = RSA.Create(2048); var req = new CertificateRequest("CN=leaf-with-ocsp", key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); req.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, false)); req.CertificateExtensions.Add(CreateOcspAiaExtension(ocspUri)); return req.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(30)); } private static X509Extension CreateOcspAiaExtension(string ocspUri) { var writer = new AsnWriter(AsnEncodingRules.DER); writer.PushSequence(); writer.PushSequence(); writer.WriteObjectIdentifier("1.3.6.1.5.5.7.48.1"); writer.WriteCharacterString(UniversalTagNumber.IA5String, ocspUri, new Asn1Tag(TagClass.ContextSpecific, 6)); writer.PopSequence(); writer.PopSequence(); return new X509Extension("1.3.6.1.5.5.7.1.1", writer.Encode(), false); } private static X509Certificate2 CreateRootCertificate() { using var rootKey = RSA.Create(2048); var req = new CertificateRequest("CN=Root", rootKey, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); req.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true)); req.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.KeyCertSign | X509KeyUsageFlags.CrlSign, true)); return req.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddYears(5)); } private static X509Certificate2 CreateLeafSignedBy(X509Certificate2 issuer) { using var leafKey = RSA.Create(2048); var req = new CertificateRequest("CN=Leaf", leafKey, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); req.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, true)); req.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(req.PublicKey, false)); var cert = req.Create( issuer, DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddYears(1), Guid.NewGuid().ToByteArray()); return cert.CopyWithPrivateKey(leafKey); } private static X509Certificate2 CreateOcspSigningDelegate(X509Certificate2 issuer) { using var key = RSA.Create(2048); var req = new CertificateRequest("CN=OCSP Delegate", key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); req.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, true)); req.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension( [new Oid("1.3.6.1.5.5.7.3.9")], true)); var cert = req.Create( issuer, DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddYears(1), Guid.NewGuid().ToByteArray()); return cert.CopyWithPrivateKey(key); } }