XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFESystem

【Rust】XFE系统,基于Rust和汇编

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Net;
using Org.BouncyCastle.Asn1;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Pkix;
using Org.BouncyCastle.Utilities.Collections;
using Org.BouncyCastle.X509;
using Org.BouncyCastle.X509.Store;

namespace XFESystem.Tls;

/// <summary>Managed PKIX validation against an explicit root store, with SAN identity and server usage checks.</summary>
public sealed class TlsCertificateValidator
{
    private readonly HashSet<TrustAnchor> anchors;

    public TlsCertificateValidator(IEnumerable<X509Certificate> roots)
    {
        ArgumentNullException.ThrowIfNull(roots);
        anchors = roots.Select(root => new TrustAnchor(root, null)).ToHashSet();
        if (anchors.Count is < 1 or > 512) throw new ArgumentException("HTTPS requires a bounded, nonempty root store.", nameof(roots));
        if (anchors.Any(anchor => anchor.TrustedCert.GetBasicConstraints() < 0))
            throw new ArgumentException("A trusted root must be a CA certificate.", nameof(roots));
        foreach (var anchor in anchors)
        {
            TlsCertificatePolicy.CheckTrustStorePublicKey(anchor.TrustedCert);
            bool[]? usage = anchor.TrustedCert.GetKeyUsage();
            if (usage is not null && (usage.Length <= 5 || !usage[5]))
                throw new ArgumentException("A trusted root must permit certificate signing.", nameof(roots));
        }
    }

    public static TlsCertificateValidator FromPem(string pem)
    {
        ArgumentNullException.ThrowIfNull(pem);
        if (pem.Length > 1_048_576) throw new ArgumentException("The root store exceeds its bound.", nameof(pem));
        var parsed = new X509CertificateParser().ReadCertificates(System.Text.Encoding.ASCII.GetBytes(pem));
        return new(parsed);
    }

    public X509Certificate Validate(string host, IReadOnlyList<byte[]> encodedChain, DateTime utc)
    {
        ArgumentNullException.ThrowIfNull(host);
        ArgumentNullException.ThrowIfNull(encodedChain);
        if (utc.Kind != DateTimeKind.Utc) throw new ArgumentException("Certificate validation requires UTC.", nameof(utc));
        if (encodedChain.Count is < 1 or > 16 || encodedChain.Sum(bytes => (long)bytes.Length) > 131_072)
            throw new InvalidOperationException("The TLS peer certificate chain exceeds its bound.");
        var parser = new X509CertificateParser();
        var chain = encodedChain.Select(bytes => parser.ReadCertificate(bytes)).ToArray();
        foreach (var certificate in chain) certificate.CheckValidity(utc);
        var leaf = chain[0];
        MatchSubjectAlternativeName(leaf, host);
        if (leaf.GetBasicConstraints() >= 0) throw new InvalidOperationException("The HTTPS leaf certificate is a CA.");
        var usage = leaf.GetExtendedKeyUsage();
        if (usage is not null && !usage.Contains(KeyPurposeID.id_kp_serverAuth))
            throw new InvalidOperationException("The HTTPS certificate is not permitted for server authentication.");
        bool[]? keyUsage = leaf.GetKeyUsage();
        if (keyUsage is not null && (keyUsage.Length == 0 || !keyUsage[0]))
            throw new InvalidOperationException("The HTTPS certificate is not permitted to sign a TLS handshake.");

        var parameters = new PkixBuilderParameters(anchors, new X509CertStoreSelector { Certificate = leaf });
        parameters.Date = utc;
        // There is no OCSP/CRL fetcher yet. PKIX signature/constraints, trust,
        // validity, SAN identity, and key usages are still mandatory.
        parameters.IsRevocationEnabled = false;
        parameters.AddStoreCert(CollectionUtilities.CreateStore(chain));
        var built = new PkixCertPathBuilder().Build(parameters);
        var anchor = built.TrustAnchor.TrustedCert;
        anchor.CheckValidity(utc);
        TlsCertificatePolicy.CheckPublicKey(anchor);
        TlsCertificatePolicy.CheckServerUsage(anchor);
        foreach (var certificate in built.CertPath.Certificates)
        {
            TlsCertificatePolicy.CheckPublicKey(certificate);
            TlsCertificatePolicy.CheckServerUsage(certificate);
            TlsCertificatePolicy.CheckSignatureAlgorithm(certificate);
        }
        TlsCertificatePolicy.CheckAnchorConstraints(anchor, built.CertPath.Certificates);
        return leaf;
    }

    private static void MatchSubjectAlternativeName(X509Certificate certificate, string host)
    {
        var extension = certificate.GetExtensionValue(X509Extensions.SubjectAlternativeName);
        if (extension is null) throw new InvalidOperationException("The HTTPS certificate has no subjectAltName.");
        var names = GeneralNames.GetInstance(Asn1Object.FromByteArray(extension.GetOctets())).GetNames();
        if (IPAddress.TryParse(host, out var address))
        {
            byte[] expected = address.GetAddressBytes();
            if (names.Any(name => name.TagNo == GeneralName.IPAddress &&
                Asn1OctetString.GetInstance(name.Name).GetOctets().AsSpan().SequenceEqual(expected))) return;
        }
        else
        {
            string expected = host.TrimEnd('.').ToLowerInvariant();
            if (expected.Length == 0 || expected.Any(character => character > 127 || char.IsControl(character)))
                throw new ArgumentException("HTTPS requires a normalized ASCII DNS name.", nameof(host));
            foreach (var name in names)
            {
                if (name.TagNo != GeneralName.DnsName) continue;
                string candidate = DerIA5String.GetInstance(name.Name).GetString().TrimEnd('.').ToLowerInvariant();
                if (candidate == expected) return;
                // Wildcards match exactly one complete label. Restrict them to
                // at least a two-label suffix; no CN or partial-label fallback.
                if (!candidate.StartsWith("*.", StringComparison.Ordinal) || candidate[2..].Contains('*') ||
                    candidate[2..].Count(character => character == '.') < 1) continue;
                int separator = expected.IndexOf('.');
                if (separator > 0 && !expected.StartsWith("xn--", StringComparison.Ordinal) && expected[(separator + 1)..] == candidate[2..]) return;
            }
        }
        throw new InvalidOperationException("The TLS certificate subjectAltName does not match the HTTPS host.");
    }
}