using Org.BouncyCastle.Asn1;
using Org.BouncyCastle.Asn1.Pkcs;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Pkix;
using Org.BouncyCastle.X509;
namespace XFESystem.Tls;
/// <summary>Additional server-auth policy for the actual PKIX-selected path.</summary>
internal static class TlsCertificatePolicy
{
internal static void CheckTrustStorePublicKey(X509Certificate certificate)
{
var info = certificate.SubjectPublicKeyInfo;
if (!info.Algorithm.Algorithm.Equals(PkcsObjectIdentifiers.RsaEncryption))
{
CheckPublicKey(certificate);
return;
}
// These are explicitly trusted, pinned input roots, not peer keys.
// Import checks their ASN.1/strength without instantiating BC RSA keys:
// that constructor runs a randomized modulus compositeness test for
// every anchor. Full BC validation remains mandatory for the selected
// anchor and all certificates in the actual PKIX path below it.
if (info.Algorithm.Parameters is not null && info.Algorithm.Parameters.ToAsn1Object() is not DerNull ||
info.PublicKey.PadBits != 0)
throw new InvalidOperationException("The trusted RSA key has invalid algorithm parameters or padding.");
var key = Asn1Sequence.GetInstance(info.ParsePublicKey());
if (key.Count != 2) throw new InvalidOperationException("The trusted RSA key must encode exactly a modulus and exponent.");
// Use signed ASN.1 values: RsaPublicKeyStructure normalizes through
// PositiveValue, which would hide an invalid negative INTEGER encoding.
var modulus = DerInteger.GetInstance(key[0]).Value;
var exponent = DerInteger.GetInstance(key[1]).Value;
if (modulus.SignValue <= 0 || modulus.BitLength is < 2048 or > 16384 || !modulus.TestBit(0) ||
exponent.SignValue <= 0 || exponent.BitLength < 2 || !exponent.TestBit(0) || exponent.CompareTo(modulus) >= 0)
throw new InvalidOperationException("The trusted RSA key has unsupported strength, modulus or exponent.");
}
internal static void CheckPublicKey(X509Certificate certificate)
{
bool permitted = certificate.GetPublicKey() switch
{
RsaKeyParameters rsa => !rsa.IsPrivate && rsa.Modulus.BitLength >= 2048,
ECPublicKeyParameters ec => ec.Parameters.Curve.FieldSize >= 256,
Ed25519PublicKeyParameters or Ed448PublicKeyParameters => true,
_ => false
};
if (!permitted) throw new InvalidOperationException("The certificate uses an unsupported or weak authentication key.");
}
internal static void CheckServerUsage(X509Certificate certificate)
{
var usages = certificate.GetExtendedKeyUsage();
if (usages is not null && !usages.Contains(KeyPurposeID.id_kp_serverAuth) && !usages.Contains(KeyPurposeID.AnyExtendedKeyUsage))
throw new InvalidOperationException("A certificate in the selected path restricts use away from TLS server authentication.");
}
internal static void CheckSignatureAlgorithm(X509Certificate certificate)
{
// A trust anchor's self-signature is not used to authenticate the path;
// callers apply this policy only to the path below the explicit anchor.
switch (certificate.SigAlgOid)
{
case "1.2.840.113549.1.1.11": // SHA-256/384/512 with RSA
case "1.2.840.113549.1.1.12":
case "1.2.840.113549.1.1.13":
case "1.2.840.10045.4.3.2": // ECDSA SHA-256/384/512
case "1.2.840.10045.4.3.3":
case "1.2.840.10045.4.3.4":
case "1.3.101.112": // Ed25519 / Ed448
case "1.3.101.113":
return;
case "1.2.840.113549.1.1.10": // RSASSA-PSS, with explicit modern parameters
var pss = RsassaPssParameters.GetInstance(Asn1Object.FromByteArray(certificate.GetSigAlgParams()));
int digestBytes = pss.HashAlgorithm.Algorithm.Id switch
{
"2.16.840.1.101.3.4.2.1" => 32,
"2.16.840.1.101.3.4.2.2" => 48,
"2.16.840.1.101.3.4.2.3" => 64,
_ => 0
};
if (digestBytes != 0 && pss.MaskGenAlgorithm.Algorithm.Equals(PkcsObjectIdentifiers.IdMgf1) &&
AlgorithmIdentifier.GetInstance(pss.MaskGenAlgorithm.Parameters).Algorithm.Equals(pss.HashAlgorithm.Algorithm) &&
pss.SaltLength.Value.CompareTo(Org.BouncyCastle.Math.BigInteger.ValueOf(digestBytes)) >= 0 &&
pss.TrailerField.Value.Equals(Org.BouncyCastle.Math.BigInteger.One)) return;
break;
}
throw new InvalidOperationException("The certificate signature algorithm does not meet the TLS authentication policy.");
}
internal static void CheckAnchorConstraints(X509Certificate anchor, IEnumerable<X509Certificate> path)
{
var extension = anchor.GetExtensionValue(X509Extensions.NameConstraints);
if (extension is null) return;
// BC's path validator handles intermediate constraints, but its fixed
// version does not initialize them from TrustAnchor.GetNameConstraints.
// Apply the explicit anchor's constraints to every subordinate name.
var constraints = NameConstraints.GetInstance(Asn1Object.FromByteArray(extension.GetOctets()));
var validator = new PkixNameConstraintValidator();
if (constraints.PermittedSubtreesValue is { } permitted)
{
CheckDistances((Asn1Sequence)permitted.ToAsn1Object());
validator.IntersectPermittedSubtree((Asn1Sequence)permitted.ToAsn1Object());
}
if (constraints.ExcludedSubtreesValue is { } excluded)
{
var sequence = (Asn1Sequence)excluded.ToAsn1Object();
CheckDistances(sequence);
foreach (var item in sequence) validator.AddExcludedSubtree(GeneralSubtree.GetInstance(item));
}
foreach (var certificate in path)
{
validator.CheckDN(certificate.SubjectDN);
var names = certificate.GetExtensionValue(X509Extensions.SubjectAlternativeName);
if (names is not null)
foreach (var name in GeneralNames.GetInstance(Asn1Object.FromByteArray(names.GetOctets())).GetNames()) validator.CheckName(name);
}
}
private static void CheckDistances(Asn1Sequence subtrees)
{
foreach (var item in subtrees)
{
var subtree = GeneralSubtree.GetInstance(item);
if (subtree.MinimumObject.Value.SignValue != 0 || subtree.MaximumObject is not null)
throw new InvalidOperationException("Non-default trust-anchor name-constraint distances are unsupported.");
}
}
}
using Org.BouncyCastle.Asn1;
using Org.BouncyCastle.Asn1.Pkcs;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Pkix;
using Org.BouncyCastle.X509;
namespace XFESystem.Tls;
/// <summary>Additional server-auth policy for the actual PKIX-selected path.</summary>
internal static class TlsCertificatePolicy
{
internal static void CheckTrustStorePublicKey(X509Certificate certificate)
{
var info = certificate.SubjectPublicKeyInfo;
if (!info.Algorithm.Algorithm.Equals(PkcsObjectIdentifiers.RsaEncryption))
{
CheckPublicKey(certificate);
return;
}
// These are explicitly trusted, pinned input roots, not peer keys.
// Import checks their ASN.1/strength without instantiating BC RSA keys:
// that constructor runs a randomized modulus compositeness test for
// every anchor. Full BC validation remains mandatory for the selected
// anchor and all certificates in the actual PKIX path below it.
if (info.Algorithm.Parameters is not null && info.Algorithm.Parameters.ToAsn1Object() is not DerNull ||
info.PublicKey.PadBits != 0)
throw new InvalidOperationException("The trusted RSA key has invalid algorithm parameters or padding.");
var key = Asn1Sequence.GetInstance(info.ParsePublicKey());
if (key.Count != 2) throw new InvalidOperationException("The trusted RSA key must encode exactly a modulus and exponent.");
// Use signed ASN.1 values: RsaPublicKeyStructure normalizes through
// PositiveValue, which would hide an invalid negative INTEGER encoding.
var modulus = DerInteger.GetInstance(key[0]).Value;
var exponent = DerInteger.GetInstance(key[1]).Value;
if (modulus.SignValue <= 0 || modulus.BitLength is < 2048 or > 16384 || !modulus.TestBit(0) ||
exponent.SignValue <= 0 || exponent.BitLength < 2 || !exponent.TestBit(0) || exponent.CompareTo(modulus) >= 0)
throw new InvalidOperationException("The trusted RSA key has unsupported strength, modulus or exponent.");
}
internal static void CheckPublicKey(X509Certificate certificate)
{
bool permitted = certificate.GetPublicKey() switch
{
RsaKeyParameters rsa => !rsa.IsPrivate && rsa.Modulus.BitLength >= 2048,
ECPublicKeyParameters ec => ec.Parameters.Curve.FieldSize >= 256,
Ed25519PublicKeyParameters or Ed448PublicKeyParameters => true,
_ => false
};
if (!permitted) throw new InvalidOperationException("The certificate uses an unsupported or weak authentication key.");
}
internal static void CheckServerUsage(X509Certificate certificate)
{
var usages = certificate.GetExtendedKeyUsage();
if (usages is not null && !usages.Contains(KeyPurposeID.id_kp_serverAuth) && !usages.Contains(KeyPurposeID.AnyExtendedKeyUsage))
throw new InvalidOperationException("A certificate in the selected path restricts use away from TLS server authentication.");
}
internal static void CheckSignatureAlgorithm(X509Certificate certificate)
{
// A trust anchor's self-signature is not used to authenticate the path;
// callers apply this policy only to the path below the explicit anchor.
switch (certificate.SigAlgOid)
{
case "1.2.840.113549.1.1.11": // SHA-256/384/512 with RSA
case "1.2.840.113549.1.1.12":
case "1.2.840.113549.1.1.13":
case "1.2.840.10045.4.3.2": // ECDSA SHA-256/384/512
case "1.2.840.10045.4.3.3":
case "1.2.840.10045.4.3.4":
case "1.3.101.112": // Ed25519 / Ed448
case "1.3.101.113":
return;
case "1.2.840.113549.1.1.10": // RSASSA-PSS, with explicit modern parameters
var pss = RsassaPssParameters.GetInstance(Asn1Object.FromByteArray(certificate.GetSigAlgParams()));
int digestBytes = pss.HashAlgorithm.Algorithm.Id switch
{
"2.16.840.1.101.3.4.2.1" => 32,
"2.16.840.1.101.3.4.2.2" => 48,
"2.16.840.1.101.3.4.2.3" => 64,
_ => 0
};
if (digestBytes != 0 && pss.MaskGenAlgorithm.Algorithm.Equals(PkcsObjectIdentifiers.IdMgf1) &&
AlgorithmIdentifier.GetInstance(pss.MaskGenAlgorithm.Parameters).Algorithm.Equals(pss.HashAlgorithm.Algorithm) &&
pss.SaltLength.Value.CompareTo(Org.BouncyCastle.Math.BigInteger.ValueOf(digestBytes)) >= 0 &&
pss.TrailerField.Value.Equals(Org.BouncyCastle.Math.BigInteger.One)) return;
break;
}
throw new InvalidOperationException("The certificate signature algorithm does not meet the TLS authentication policy.");
}
internal static void CheckAnchorConstraints(X509Certificate anchor, IEnumerable<X509Certificate> path)
{
var extension = anchor.GetExtensionValue(X509Extensions.NameConstraints);
if (extension is null) return;
// BC's path validator handles intermediate constraints, but its fixed
// version does not initialize them from TrustAnchor.GetNameConstraints.
// Apply the explicit anchor's constraints to every subordinate name.
var constraints = NameConstraints.GetInstance(Asn1Object.FromByteArray(extension.GetOctets()));
var validator = new PkixNameConstraintValidator();
if (constraints.PermittedSubtreesValue is { } permitted)
{
CheckDistances((Asn1Sequence)permitted.ToAsn1Object());
validator.IntersectPermittedSubtree((Asn1Sequence)permitted.ToAsn1Object());
}
if (constraints.ExcludedSubtreesValue is { } excluded)
{
var sequence = (Asn1Sequence)excluded.ToAsn1Object();
CheckDistances(sequence);
foreach (var item in sequence) validator.AddExcludedSubtree(GeneralSubtree.GetInstance(item));
}
foreach (var certificate in path)
{
validator.CheckDN(certificate.SubjectDN);
var names = certificate.GetExtensionValue(X509Extensions.SubjectAlternativeName);
if (names is not null)
foreach (var name in GeneralNames.GetInstance(Asn1Object.FromByteArray(names.GetOctets())).GetNames()) validator.CheckName(name);
}
}
private static void CheckDistances(Asn1Sequence subtrees)
{
foreach (var item in subtrees)
{
var subtree = GeneralSubtree.GetInstance(item);
if (subtree.MinimumObject.Value.SignValue != 0 || subtree.MaximumObject is not null)
throw new InvalidOperationException("Non-default trust-anchor name-constraint distances are unsupported.");
}
}
}