using Org.BouncyCastle.Asn1;
using Org.BouncyCastle.Asn1.Pkcs;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.X509;
using XFESystem.Tls;
namespace XFESystem.NativeTls.Tests;
public sealed class TlsTrustStoreImportTests
{
private static X509Certificate ReplacePublicKey(X509Certificate certificate, SubjectPublicKeyInfo info)
{
var original = (Asn1Sequence)certificate.CertificateStructure.TbsCertificate.ToAsn1Object();
var fields = original.Cast<Asn1Encodable>().ToArray();
// This fixture is v3, so its explicit version occupies index zero.
Assert.IsAssignableFrom<Asn1TaggedObject>(fields[0]); fields[6] = info;
var tbs = TbsCertificateStructure.GetInstance(new DerSequence(fields));
var structure = new X509CertificateStructure(tbs, certificate.CertificateStructure.SignatureAlgorithm,
certificate.CertificateStructure.Signature);
return new X509Certificate(structure);
}
private static X509Certificate RsaRoot(X509Certificate original, BigInteger modulus, BigInteger exponent,
Asn1Encodable? parameters = null) => ReplacePublicKey(original,
new SubjectPublicKeyInfo(new AlgorithmIdentifier(PkcsObjectIdentifiers.RsaEncryption, parameters ?? DerNull.Instance),
new DerSequence(new DerInteger(modulus), new DerInteger(exponent))));
[Fact]
public void RootImportDoesNotRunRsaKeyFactoryForEveryAnchor()
{
using var fixture = new CertificateFixtures();
// Import must remain a cheap structural check, even for a large bundle.
var root = new ObservedCertificate(RsaRoot(fixture.RootBc, BigInteger.One.ShiftLeft(2203).Subtract(BigInteger.One), BigInteger.ValueOf(65537)));
for (int count = 0; count < 121; count++) _ = new TlsCertificateValidator([root]);
Assert.Equal(0, root.KeyFactoryCalls);
}
[Theory]
[InlineData(1024, 65537)]
[InlineData(16385, 65537)]
[InlineData(2048, 1)]
[InlineData(2048, 2)]
[InlineData(2048, 0)]
[InlineData(2048, -3)]
public void RootImportRejectsWeakStrengthAndInvalidExponent(int bits, long exponent)
{
using var fixture = new CertificateFixtures();
var root = RsaRoot(fixture.RootBc, BigInteger.One.ShiftLeft(bits - 1).Add(BigInteger.One), BigInteger.ValueOf(exponent));
Assert.Throws<InvalidOperationException>(() => new TlsCertificateValidator([root]));
}
[Fact]
public void RootImportRejectsEvenModulusAndInvalidAlgorithmParameters()
{
using var fixture = new CertificateFixtures();
var odd = BigInteger.One.ShiftLeft(2047).Add(BigInteger.One);
var even = RsaRoot(fixture.RootBc, odd.Add(BigInteger.One), BigInteger.ValueOf(65537));
var invalidParameters = RsaRoot(fixture.RootBc, odd, BigInteger.ValueOf(65537), DerInteger.ValueOf(7));
Assert.Throws<InvalidOperationException>(() => new TlsCertificateValidator([even]));
Assert.Throws<InvalidOperationException>(() => new TlsCertificateValidator([invalidParameters]));
}
[Fact]
public void NegativeAsn1ModulusCannotBeNormalizedIntoAnAcceptedPositiveKey()
{
using var fixture = new CertificateFixtures();
var negative = BigInteger.One.ShiftLeft(2203).Subtract(BigInteger.One).Negate();
var root = RsaRoot(fixture.RootBc, negative, BigInteger.ValueOf(65537));
Assert.Throws<InvalidOperationException>(() => new TlsCertificateValidator([root]));
}
[Fact]
public void UnsupportedRootKeyAlgorithmIsNotAdmitted()
{
using var fixture = new CertificateFixtures();
var root = ReplacePublicKey(fixture.RootBc, new SubjectPublicKeyInfo(
new AlgorithmIdentifier(new DerObjectIdentifier("1.3.6.1.4.1.55555.77.1")), new DerBitString([1, 2, 3])));
Assert.ThrowsAny<Exception>(() => new TlsCertificateValidator([root]));
}
[Fact]
public void SelectedAnchorStillRunsFullRsaValidationAndRejectsPrimeModulus()
{
using var fixture = new CertificateFixtures(); using var leaf = fixture.Leaf();
// 2^2203-1 is a known Mersenne prime, so it meets the structural bit
// policy but cannot be an RSA modulus. The real BC factory must reject.
var root = new ObservedCertificate(RsaRoot(fixture.RootBc, BigInteger.One.ShiftLeft(2203).Subtract(BigInteger.One), BigInteger.ValueOf(65537)));
var validator = new TlsCertificateValidator([root]); Assert.Equal(0, root.KeyFactoryCalls);
Assert.ThrowsAny<Exception>(() => validator.Validate("test.xfe.invalid", [leaf.RawData], fixture.Now));
Assert.True(root.KeyFactoryCalls > 0);
Assert.Contains("not composite", root.KeyFactoryError?.Message ?? "", StringComparison.OrdinalIgnoreCase);
}
private sealed class ObservedCertificate(X509Certificate source) : X509Certificate(source.CertificateStructure)
{
public int KeyFactoryCalls;
public Exception? KeyFactoryError;
public override AsymmetricKeyParameter GetPublicKey()
{
KeyFactoryCalls++;
try { return base.GetPublicKey(); }
catch (Exception error) { KeyFactoryError = error; throw; }
}
}
}
using Org.BouncyCastle.Asn1;
using Org.BouncyCastle.Asn1.Pkcs;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.X509;
using XFESystem.Tls;
namespace XFESystem.NativeTls.Tests;
public sealed class TlsTrustStoreImportTests
{
private static X509Certificate ReplacePublicKey(X509Certificate certificate, SubjectPublicKeyInfo info)
{
var original = (Asn1Sequence)certificate.CertificateStructure.TbsCertificate.ToAsn1Object();
var fields = original.Cast<Asn1Encodable>().ToArray();
// This fixture is v3, so its explicit version occupies index zero.
Assert.IsAssignableFrom<Asn1TaggedObject>(fields[0]); fields[6] = info;
var tbs = TbsCertificateStructure.GetInstance(new DerSequence(fields));
var structure = new X509CertificateStructure(tbs, certificate.CertificateStructure.SignatureAlgorithm,
certificate.CertificateStructure.Signature);
return new X509Certificate(structure);
}
private static X509Certificate RsaRoot(X509Certificate original, BigInteger modulus, BigInteger exponent,
Asn1Encodable? parameters = null) => ReplacePublicKey(original,
new SubjectPublicKeyInfo(new AlgorithmIdentifier(PkcsObjectIdentifiers.RsaEncryption, parameters ?? DerNull.Instance),
new DerSequence(new DerInteger(modulus), new DerInteger(exponent))));
[Fact]
public void RootImportDoesNotRunRsaKeyFactoryForEveryAnchor()
{
using var fixture = new CertificateFixtures();
// Import must remain a cheap structural check, even for a large bundle.
var root = new ObservedCertificate(RsaRoot(fixture.RootBc, BigInteger.One.ShiftLeft(2203).Subtract(BigInteger.One), BigInteger.ValueOf(65537)));
for (int count = 0; count < 121; count++) _ = new TlsCertificateValidator([root]);
Assert.Equal(0, root.KeyFactoryCalls);
}
[Theory]
[InlineData(1024, 65537)]
[InlineData(16385, 65537)]
[InlineData(2048, 1)]
[InlineData(2048, 2)]
[InlineData(2048, 0)]
[InlineData(2048, -3)]
public void RootImportRejectsWeakStrengthAndInvalidExponent(int bits, long exponent)
{
using var fixture = new CertificateFixtures();
var root = RsaRoot(fixture.RootBc, BigInteger.One.ShiftLeft(bits - 1).Add(BigInteger.One), BigInteger.ValueOf(exponent));
Assert.Throws<InvalidOperationException>(() => new TlsCertificateValidator([root]));
}
[Fact]
public void RootImportRejectsEvenModulusAndInvalidAlgorithmParameters()
{
using var fixture = new CertificateFixtures();
var odd = BigInteger.One.ShiftLeft(2047).Add(BigInteger.One);
var even = RsaRoot(fixture.RootBc, odd.Add(BigInteger.One), BigInteger.ValueOf(65537));
var invalidParameters = RsaRoot(fixture.RootBc, odd, BigInteger.ValueOf(65537), DerInteger.ValueOf(7));
Assert.Throws<InvalidOperationException>(() => new TlsCertificateValidator([even]));
Assert.Throws<InvalidOperationException>(() => new TlsCertificateValidator([invalidParameters]));
}
[Fact]
public void NegativeAsn1ModulusCannotBeNormalizedIntoAnAcceptedPositiveKey()
{
using var fixture = new CertificateFixtures();
var negative = BigInteger.One.ShiftLeft(2203).Subtract(BigInteger.One).Negate();
var root = RsaRoot(fixture.RootBc, negative, BigInteger.ValueOf(65537));
Assert.Throws<InvalidOperationException>(() => new TlsCertificateValidator([root]));
}
[Fact]
public void UnsupportedRootKeyAlgorithmIsNotAdmitted()
{
using var fixture = new CertificateFixtures();
var root = ReplacePublicKey(fixture.RootBc, new SubjectPublicKeyInfo(
new AlgorithmIdentifier(new DerObjectIdentifier("1.3.6.1.4.1.55555.77.1")), new DerBitString([1, 2, 3])));
Assert.ThrowsAny<Exception>(() => new TlsCertificateValidator([root]));
}
[Fact]
public void SelectedAnchorStillRunsFullRsaValidationAndRejectsPrimeModulus()
{
using var fixture = new CertificateFixtures(); using var leaf = fixture.Leaf();
// 2^2203-1 is a known Mersenne prime, so it meets the structural bit
// policy but cannot be an RSA modulus. The real BC factory must reject.
var root = new ObservedCertificate(RsaRoot(fixture.RootBc, BigInteger.One.ShiftLeft(2203).Subtract(BigInteger.One), BigInteger.ValueOf(65537)));
var validator = new TlsCertificateValidator([root]); Assert.Equal(0, root.KeyFactoryCalls);
Assert.ThrowsAny<Exception>(() => validator.Validate("test.xfe.invalid", [leaf.RawData], fixture.Now));
Assert.True(root.KeyFactoryCalls > 0);
Assert.Contains("not composite", root.KeyFactoryError?.Message ?? "", StringComparison.OrdinalIgnoreCase);
}
private sealed class ObservedCertificate(X509Certificate source) : X509Certificate(source.CertificateStructure)
{
public int KeyFactoryCalls;
public Exception? KeyFactoryError;
public override AsymmetricKeyParameter GetPublicKey()
{
KeyFactoryCalls++;
try { return base.GetPublicKey(); }
catch (Exception error) { KeyFactoryError = error; throw; }
}
}
}