bcgit / bc-csharp

BouncyCastle.NET Cryptography Library (Mirror)
https://www.bouncycastle.org/csharp
MIT License
1.64k stars 547 forks source link

TLS 1.3 support in DefaultTlsServer #405

Open darind opened 1 year ago

darind commented 1 year ago

I am trying to implement a TLS 1.3 server using BouncyCastle.Cryptography 2.0.0:

public class BcTlsServer : DefaultTlsServer
{
    private BcTlsServer() : base(new BcTlsCrypto(new SecureRandom()))
    {
    }

    protected override TlsCredentialedSigner GetRsaSignerCredentials()
    {
        ...
    }

    public override ProtocolVersion[] GetProtocolVersions()
    {
        return new[] { ProtocolVersion.TLSv13 };
    }
}

using it like this:

var tlsProto = new TlsServerProtocol(stream);
var server = new BcTlsServer();
tlsProto.Accept(server);

When a TLS 1.3 enabled client attempts to connect it throws a TlsFatalAlert in GetCredentials (The reason for that is because m_context.SecurityParameters.KeyExchangeAlgorithm is set to 0 inside GetCredentials).

What is the proper way to add support for TLS 1.3 and is it supported? What other methods do I need to override?

Since there are no longer key exchanges and signature algorithms in TLS 1.3, I also tried directly returning a TlsCredentials implementation from my certificatein the GetCredentials method:

public override TlsCredentials GetCredentials()
{
    var certStruct = DotNetUtilities.FromX509Certificate(cert).CertificateStructure;
    var tlsCertificate = new BcTlsCertificate(this.crypto, certStruct);
    var bcCertificate = new Certificate(new[] { tlsCertificate });
    return new MyCredentials(bcCertificate);
}
darind commented 1 year ago

I was able to make this work using a X509 certificate with EC private key:

public override TlsCredentials GetCredentials()
{
    // The X509 certificate I am importing is using an EC private key (secp384r1)
    var x509Cert = new X509Certificate2("cert.pfx", "secret", X509KeyStorageFlags.Exportable);

    var crypto = new BcTlsCrypto(new SecureRandom());
    var tlsCertificate = new BcTlsCertificate(crypto, DotNetUtilities.FromX509Certificate(x509Cert).CertificateStructure);
    var bcCertificate = new Certificate(
        CertificateType.X509, 
        TlsUtilities.EmptyBytes,
        new CertificateEntry[] { new CertificateEntry(tlsCertificate, null) });

    var privateKey = x509Cert.GetECDsaPrivateKey();

    var keyPair = DotNetUtilities.GetKeyPair(privateKey);
    var algo = new SignatureAndHashAlgorithm(HashAlgorithm.sha384, SignatureAlgorithm.ecdsa);
    var cryptoParams = new TlsCryptoParameters(this.m_context);
    return new BcDefaultTlsCredentialedSigner(
        cryptoParams,
        crypto,
        keyPair.Private,
        bcCertificate,
        algo);
}

The problem is that this enforces me to use a X509 certificate with EC private key, while in theory it should be possible to use TLS 1.3 with a X509 certificate with RSA private key for authentication and a separate DHE/ECDHE key exchange to get the keys for symmetric encryption. How can this be achieved?

peterdettman commented 1 year ago

@darind Apologies for taking so long to reply. The rules for TLS are quite complicated (TLS 1.3 only simplifies the problem considerably). The best current example is the ProvTlsServer class from the bc-java project. I made a copy of that and simplified it down the example pasted below.

The main points are to record the client's signature schemes and trusted issuers, as well as configuring which signature schemes you wish to support, and the corresponding credentials to use for each.

AbstractTlsServer will call SelectCipherSuite on cipher suites it finds in common b/w client and server (and otherwise prevalidated). It is a convenient method to override to ensure that we have usable credentials for that suite (which includes checks against the client signature schemes and trusted issuers, if any).

This is not well-tested, so if you try it and need help getting it to work, let me know.

using System;
using System.Collections.Generic;
using System.Linq;

using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Math.Raw;
using Org.BouncyCastle.Tls;
using Org.BouncyCastle.Tls.Crypto;
using Org.BouncyCastle.Tls.Crypto.Impl.BC;
using Org.BouncyCastle.X509;

namespace Org.BouncyCastle.Example
{
    public class DefaultTls13Server
        : AbstractTlsServer
    {
        private static readonly int[] DefaultCipherSuites = new int[]
        {
            /*
             * TLS 1.3
             */
            CipherSuite.TLS_AES_256_GCM_SHA384,
            CipherSuite.TLS_AES_128_GCM_SHA256,
            CipherSuite.TLS_CHACHA20_POLY1305_SHA256,
        };

        private class Credentials
        {
            internal Certificate Certificate { get; set; }
            internal AsymmetricKeyParameter PrivateKey { get; set; }
        }

        // For each SignatureScheme value we support, store the corresponding credentials
        private readonly Dictionary<int, Credentials> m_credentials = new Dictionary<int, Credentials>();

        private IList<int> m_peerSigSchemes = null;
        private TlsCredentials m_selectedCredentials = null;
        private IList<X509Name> m_clientTrustedIssuers = null;

        public DefaultTls13Server(BcTlsCrypto crypto)
            : base(crypto)
        {
            // TODO Fill in m_credentials according to which SignatureScheme values we support e.g.:
            //m_credentials[SignatureScheme.ecdsa_secp384r1_sha384] = new Credentials { ... };
        }

        protected override int[] GetSupportedCipherSuites()
        {
            return TlsUtilities.GetSupportedCipherSuites(Crypto, DefaultCipherSuites);
        }

        protected override bool SelectCipherSuite(int cipherSuite)
        {
            TlsCredentials cipherSuiteCredentials = null;

            int keyExchangeAlgorithm = TlsUtilities.GetKeyExchangeAlgorithm(cipherSuite);
            if (!KeyExchangeAlgorithm.IsAnonymous(keyExchangeAlgorithm))
            {
                cipherSuiteCredentials = SelectCredentials(keyExchangeAlgorithm);

                if (null == cipherSuiteCredentials)
                    return false;
            }

            bool result = base.SelectCipherSuite(cipherSuite);
            if (result)
            {
                m_selectedCredentials = cipherSuiteCredentials;
            }
            return result;
        }

        public override TlsCredentials GetCredentials()
        {
            return m_selectedCredentials ?? throw new TlsFatalAlert(AlertDescription.internal_error);
        }

        public override int GetSelectedCipherSuite()
        {
            m_peerSigSchemes = m_context.SecurityParameters.ClientSigAlgs.Select(SignatureScheme.From).ToList();

            return base.GetSelectedCipherSuite();
        }

        public override void ProcessClientExtensions(IDictionary<int, byte[]> clientExtensions)
        {
            base.ProcessClientExtensions(clientExtensions);

            m_clientTrustedIssuers = TlsExtensionsUtilities.GetCertificateAuthoritiesExtension(clientExtensions);
        }

        private TlsCredentials SelectCredentials(int keyExchangeAlgorithm)
        {
            switch (keyExchangeAlgorithm)
            {
            case KeyExchangeAlgorithm.NULL:
                return SelectServerCredentials13();

            default:
                return null;
            }
        }

        private TlsCredentials SelectServerCredentials13()
        {
            X509Name[] issuers = m_clientTrustedIssuers.ToArray();
            byte[] certificateRequestContext = TlsUtilities.EmptyBytes;

            foreach (int peerSigScheme in m_peerSigSchemes)
            {
                if (!m_credentials.TryGetValue(peerSigScheme, out var candidateCredentials))
                    continue;

                if (!IsSuitableCredentials(candidateCredentials))
                    continue;

                return CreateCredentialedSigner13(peerSigScheme, candidateCredentials);
            }

            return null;
        }

        private TlsCredentials CreateCredentialedSigner13(int signatureScheme, Credentials credentials)
        {
            return new BcDefaultTlsCredentialedSigner(
                new TlsCryptoParameters(m_context),
                Crypto as BcTlsCrypto,
                credentials.PrivateKey,
                credentials.Certificate,
                SignatureScheme.GetSignatureAndHashAlgorithm(signatureScheme));
        }

        private bool IsSuitableCredentials(Credentials credentials)
        {
            var certificate = credentials.Certificate;
            if (certificate.IsEmpty)
                return false;

            if (TlsUtilities.IsNullOrEmpty(m_clientTrustedIssuers))
                return true;

            var crypto = Crypto as BcTlsCrypto;

            var chain = certificate.GetCertificateList();
            int pos = chain.Length;
            while (--pos >= 0)
            {
                var issuer = BcTlsCertificate.Convert(crypto, chain[pos]).X509CertificateStructure.Issuer;
                if (MatchesIssuers(m_clientTrustedIssuers, issuer))
                    return true;
            }

            var eeCert = new X509Certificate(BcTlsCertificate.Convert(crypto, chain[0]).X509CertificateStructure);
            return eeCert.GetBasicConstraints() >= 0
                && MatchesIssuers(m_clientTrustedIssuers, eeCert.SubjectDN);
        }

        private static bool MatchesIssuers(IList<X509Name> issuers, X509Name name)
        {
            foreach (var issuer in issuers)
            {
                if (name.Equivalent(issuer))
                    return true;
            }
            return false;
        }
    }
}
ylmw0131 commented 9 months ago

Hi @darind, did you resolve for the issue?

I am jamming on same issue now.