using System.Security.Cryptography;
using XFESystem.Tls;
namespace XFESystem.NativeTls.Tests;
public sealed class TlsCertificateTests
{
[Fact]
public void ValidExplicitRootSanAndUtcAreAccepted()
{
using var fixture = new CertificateFixtures(); using var leaf = fixture.Leaf();
var validated = new TlsCertificateValidator([fixture.RootBc]).Validate("TEST.XFE.INVALID", [leaf.RawData], fixture.Now);
Assert.Equal(leaf.RawData, validated.GetEncoded());
}
[Theory]
[InlineData("other.xfe.invalid")]
[InlineData("sub.test.xfe.invalid")]
[InlineData("test.xfe.invalid.attacker.invalid")]
[InlineData("127.0.0.1")]
public void WrongSubjectAlternativeNameIsRejected(string host)
{
using var fixture = new CertificateFixtures(); using var leaf = fixture.Leaf();
Assert.Throws<InvalidOperationException>(() => new TlsCertificateValidator([fixture.RootBc]).Validate(host, [leaf.RawData], fixture.Now));
}
[Fact]
public void UnknownRootIsRejected()
{
using var fixture = new CertificateFixtures(); using var stranger = new CertificateFixtures(); using var leaf = fixture.Leaf();
Assert.ThrowsAny<Exception>(() => new TlsCertificateValidator([stranger.RootBc]).Validate("test.xfe.invalid", [leaf.RawData], fixture.Now));
}
[Fact]
public void WrongSigningKeyIsRejectedEvenWhenIssuerNameMatchesTrustedRoot()
{
using var fixture = new CertificateFixtures(); using var wrongKey = ECDsa.Create(ECCurve.NamedCurves.nistP256);
using var leaf = fixture.Leaf(signingKey: wrongKey);
Assert.ThrowsAny<Exception>(() => new TlsCertificateValidator([fixture.RootBc]).Validate("test.xfe.invalid", [leaf.RawData], fixture.Now));
}
[Theory]
[InlineData(-3, -2)]
[InlineData(2, 3)]
public void ExpiredAndNotYetValidCertificatesAreRejected(int before, int after)
{
using var fixture = new CertificateFixtures(); using var leaf = fixture.Leaf(before: fixture.Now.AddDays(before), after: fixture.Now.AddDays(after));
Assert.ThrowsAny<Exception>(() => new TlsCertificateValidator([fixture.RootBc]).Validate("test.xfe.invalid", [leaf.RawData], fixture.Now));
}
[Fact]
public void SubjectCommonNameDoesNotReplaceMissingSan()
{
using var fixture = new CertificateFixtures(); using var leaf = fixture.Leaf(dns: null);
Assert.Throws<InvalidOperationException>(() => new TlsCertificateValidator([fixture.RootBc]).Validate("never-used-for-name-match.invalid", [leaf.RawData], fixture.Now));
}
[Theory]
[InlineData(true, true, true)]
[InlineData(false, false, true)]
[InlineData(false, true, false)]
public void CaLeafWrongExtendedUsageAndWrongSigningUsageAreRejected(bool ca, bool serverAuth, bool signingUsage)
{
using var fixture = new CertificateFixtures(); using var leaf = fixture.Leaf(ca: ca, serverAuth: serverAuth, signingUsage: signingUsage);
Assert.ThrowsAny<Exception>(() => new TlsCertificateValidator([fixture.RootBc]).Validate("test.xfe.invalid", [leaf.RawData], fixture.Now));
}
[Fact]
public void UnknownCriticalExtensionIsRejectedByPkix()
{
using var fixture = new CertificateFixtures(); using var leaf = fixture.Leaf(unknownCriticalExtension: true);
Assert.ThrowsAny<Exception>(() => new TlsCertificateValidator([fixture.RootBc]).Validate("test.xfe.invalid", [leaf.RawData], fixture.Now));
}
[Theory]
[InlineData("*.xfe.invalid", "test.xfe.invalid", true)]
[InlineData("*.xfe.invalid", "nested.test.xfe.invalid", false)]
[InlineData("*.xfe.invalid", "xfe.invalid", false)]
[InlineData("*.invalid", "test.invalid", false)]
[InlineData("*.xfe.invalid", "xn--test.xfe.invalid", false)]
public void WildcardMatchesExactlyOneCompleteNonIdnaLabel(string name, string host, bool accepted)
{
using var fixture = new CertificateFixtures(); using var leaf = fixture.Leaf(dns: name);
var validation = () => new TlsCertificateValidator([fixture.RootBc]).Validate(host, [leaf.RawData], fixture.Now);
if (accepted) Assert.NotNull(validation()); else Assert.Throws<InvalidOperationException>(validation);
}
[Fact]
public void IpIdentityRequiresIpSan()
{
using var fixture = new CertificateFixtures(); using var ipLeaf = fixture.Leaf(dns: null, ip: "127.0.0.1"); using var dnsLeaf = fixture.Leaf(dns: "127.0.0.1");
var validator = new TlsCertificateValidator([fixture.RootBc]);
Assert.NotNull(validator.Validate("127.0.0.1", [ipLeaf.RawData], fixture.Now));
Assert.Throws<InvalidOperationException>(() => validator.Validate("127.0.0.1", [dnsLeaf.RawData], fixture.Now));
}
[Fact]
public void BoundsEmptyTrustAndNonUtcInputsAreRejected()
{
using var fixture = new CertificateFixtures(); using var leaf = fixture.Leaf();
Assert.Throws<ArgumentException>(() => new TlsCertificateValidator([]));
var validator = new TlsCertificateValidator([fixture.RootBc]);
Assert.Throws<InvalidOperationException>(() => validator.Validate("test.xfe.invalid", [], fixture.Now));
Assert.Throws<InvalidOperationException>(() => validator.Validate("test.xfe.invalid", [new byte[131_073]], fixture.Now));
Assert.Throws<ArgumentException>(() => validator.Validate("test.xfe.invalid", [leaf.RawData], DateTime.SpecifyKind(fixture.Now, DateTimeKind.Unspecified)));
}
[Fact]
public void ExpiredTrustAnchorIsRejectedEvenWhenLeafIsStillValid()
{
using var fixture = new CertificateFixtures();
using var leaf = fixture.Leaf(before: fixture.Now.AddDays(9), after: fixture.Now.AddDays(12));
Assert.ThrowsAny<Exception>(() => new TlsCertificateValidator([fixture.RootBc]).Validate("test.xfe.invalid", [leaf.RawData], fixture.Now.AddDays(11)));
}
[Fact]
public void PemRootBundleIsParsedWithoutHostCertificateStore()
{
using var fixture = new CertificateFixtures(); using var leaf = fixture.Leaf();
var validator = TlsCertificateValidator.FromPem(fixture.Root.ExportCertificatePem());
Assert.NotNull(validator.Validate("test.xfe.invalid", [leaf.RawData], fixture.Now));
}
}
using System.Security.Cryptography;
using XFESystem.Tls;
namespace XFESystem.NativeTls.Tests;
public sealed class TlsCertificateTests
{
[Fact]
public void ValidExplicitRootSanAndUtcAreAccepted()
{
using var fixture = new CertificateFixtures(); using var leaf = fixture.Leaf();
var validated = new TlsCertificateValidator([fixture.RootBc]).Validate("TEST.XFE.INVALID", [leaf.RawData], fixture.Now);
Assert.Equal(leaf.RawData, validated.GetEncoded());
}
[Theory]
[InlineData("other.xfe.invalid")]
[InlineData("sub.test.xfe.invalid")]
[InlineData("test.xfe.invalid.attacker.invalid")]
[InlineData("127.0.0.1")]
public void WrongSubjectAlternativeNameIsRejected(string host)
{
using var fixture = new CertificateFixtures(); using var leaf = fixture.Leaf();
Assert.Throws<InvalidOperationException>(() => new TlsCertificateValidator([fixture.RootBc]).Validate(host, [leaf.RawData], fixture.Now));
}
[Fact]
public void UnknownRootIsRejected()
{
using var fixture = new CertificateFixtures(); using var stranger = new CertificateFixtures(); using var leaf = fixture.Leaf();
Assert.ThrowsAny<Exception>(() => new TlsCertificateValidator([stranger.RootBc]).Validate("test.xfe.invalid", [leaf.RawData], fixture.Now));
}
[Fact]
public void WrongSigningKeyIsRejectedEvenWhenIssuerNameMatchesTrustedRoot()
{
using var fixture = new CertificateFixtures(); using var wrongKey = ECDsa.Create(ECCurve.NamedCurves.nistP256);
using var leaf = fixture.Leaf(signingKey: wrongKey);
Assert.ThrowsAny<Exception>(() => new TlsCertificateValidator([fixture.RootBc]).Validate("test.xfe.invalid", [leaf.RawData], fixture.Now));
}
[Theory]
[InlineData(-3, -2)]
[InlineData(2, 3)]
public void ExpiredAndNotYetValidCertificatesAreRejected(int before, int after)
{
using var fixture = new CertificateFixtures(); using var leaf = fixture.Leaf(before: fixture.Now.AddDays(before), after: fixture.Now.AddDays(after));
Assert.ThrowsAny<Exception>(() => new TlsCertificateValidator([fixture.RootBc]).Validate("test.xfe.invalid", [leaf.RawData], fixture.Now));
}
[Fact]
public void SubjectCommonNameDoesNotReplaceMissingSan()
{
using var fixture = new CertificateFixtures(); using var leaf = fixture.Leaf(dns: null);
Assert.Throws<InvalidOperationException>(() => new TlsCertificateValidator([fixture.RootBc]).Validate("never-used-for-name-match.invalid", [leaf.RawData], fixture.Now));
}
[Theory]
[InlineData(true, true, true)]
[InlineData(false, false, true)]
[InlineData(false, true, false)]
public void CaLeafWrongExtendedUsageAndWrongSigningUsageAreRejected(bool ca, bool serverAuth, bool signingUsage)
{
using var fixture = new CertificateFixtures(); using var leaf = fixture.Leaf(ca: ca, serverAuth: serverAuth, signingUsage: signingUsage);
Assert.ThrowsAny<Exception>(() => new TlsCertificateValidator([fixture.RootBc]).Validate("test.xfe.invalid", [leaf.RawData], fixture.Now));
}
[Fact]
public void UnknownCriticalExtensionIsRejectedByPkix()
{
using var fixture = new CertificateFixtures(); using var leaf = fixture.Leaf(unknownCriticalExtension: true);
Assert.ThrowsAny<Exception>(() => new TlsCertificateValidator([fixture.RootBc]).Validate("test.xfe.invalid", [leaf.RawData], fixture.Now));
}
[Theory]
[InlineData("*.xfe.invalid", "test.xfe.invalid", true)]
[InlineData("*.xfe.invalid", "nested.test.xfe.invalid", false)]
[InlineData("*.xfe.invalid", "xfe.invalid", false)]
[InlineData("*.invalid", "test.invalid", false)]
[InlineData("*.xfe.invalid", "xn--test.xfe.invalid", false)]
public void WildcardMatchesExactlyOneCompleteNonIdnaLabel(string name, string host, bool accepted)
{
using var fixture = new CertificateFixtures(); using var leaf = fixture.Leaf(dns: name);
var validation = () => new TlsCertificateValidator([fixture.RootBc]).Validate(host, [leaf.RawData], fixture.Now);
if (accepted) Assert.NotNull(validation()); else Assert.Throws<InvalidOperationException>(validation);
}
[Fact]
public void IpIdentityRequiresIpSan()
{
using var fixture = new CertificateFixtures(); using var ipLeaf = fixture.Leaf(dns: null, ip: "127.0.0.1"); using var dnsLeaf = fixture.Leaf(dns: "127.0.0.1");
var validator = new TlsCertificateValidator([fixture.RootBc]);
Assert.NotNull(validator.Validate("127.0.0.1", [ipLeaf.RawData], fixture.Now));
Assert.Throws<InvalidOperationException>(() => validator.Validate("127.0.0.1", [dnsLeaf.RawData], fixture.Now));
}
[Fact]
public void BoundsEmptyTrustAndNonUtcInputsAreRejected()
{
using var fixture = new CertificateFixtures(); using var leaf = fixture.Leaf();
Assert.Throws<ArgumentException>(() => new TlsCertificateValidator([]));
var validator = new TlsCertificateValidator([fixture.RootBc]);
Assert.Throws<InvalidOperationException>(() => validator.Validate("test.xfe.invalid", [], fixture.Now));
Assert.Throws<InvalidOperationException>(() => validator.Validate("test.xfe.invalid", [new byte[131_073]], fixture.Now));
Assert.Throws<ArgumentException>(() => validator.Validate("test.xfe.invalid", [leaf.RawData], DateTime.SpecifyKind(fixture.Now, DateTimeKind.Unspecified)));
}
[Fact]
public void ExpiredTrustAnchorIsRejectedEvenWhenLeafIsStillValid()
{
using var fixture = new CertificateFixtures();
using var leaf = fixture.Leaf(before: fixture.Now.AddDays(9), after: fixture.Now.AddDays(12));
Assert.ThrowsAny<Exception>(() => new TlsCertificateValidator([fixture.RootBc]).Validate("test.xfe.invalid", [leaf.RawData], fixture.Now.AddDays(11)));
}
[Fact]
public void PemRootBundleIsParsedWithoutHostCertificateStore()
{
using var fixture = new CertificateFixtures(); using var leaf = fixture.Leaf();
var validator = TlsCertificateValidator.FromPem(fixture.Root.ExportCertificatePem());
Assert.NotNull(validator.Validate("test.xfe.invalid", [leaf.RawData], fixture.Now));
}
}