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; /// Additional server-auth policy for the actual PKIX-selected path. 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 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."); } } }