cosullivan / SmtpServer

A SMTP Server component written in C#
MIT License
690 stars 163 forks source link

SNI certificate-selection is not supported #172

Closed ststeiger closed 11 months ago

ststeiger commented 2 years ago

The SSL-certificate should not be "static". If host-header based SSL is used (SNI), for multiple domains on the same IP, then the certificate would need to include them all. This is problematic, as LetsEncrypt does not allow more than 100 aliases in one certificate .

Therefore, we need a System.Net.Security.ServerCertificateSelectionCallback in IEndpointDefinition, instead of // X509Certificate ServerCertificate { get; }

Now, to get to the host-name is more difficult. You can either switch to .NET 5 (instead of NetStandard 2.0), and use

// System.Net.Security.SslStream stream; stream.TargetHostName // .NET 5.0 only

or you can stay on NetStandard 2.0, and use stream-extended. This might (or might not) incur performance-loss or instability. Example:

             using (System.Net.Sockets.TcpClient socket = await tcp.AcceptTcpClientAsync())
                {
                    System.Console.WriteLine("Client connected");
                    // SslStream stream = new SslStream(socket.GetStream());
                    // NoValidateServerCertificate
                    // https://stackoverflow.com/questions/57399520/set-sni-in-a-client-for-a-streamsocket-or-sslstream
                    // https://www.iana.org/assignments/tls-extensiontype-values/tls-extensiontype-values.xhtml
                    // SslStream stream = new SslStream(socket.GetStream(), false, new RemoteCertificateValidationCallback(ValidateServerCertificate))
                    // SslStream stream = new SslStream(socket.GetStream(), false, new RemoteCertificateValidationCallback(NoValidateServerCertificate) ,new LocalCertificateSelectionCallback(My))

                    // ((System.Net.IPEndPoint)socket.Client.RemoteEndPoint).Address.ToString();

#if true
                    StreamExtended.DefaultBufferPool bufferPool = new StreamExtended.DefaultBufferPool();

                    StreamExtended.Network.CustomBufferedStream yourClientStream = 
                        new StreamExtended.Network.CustomBufferedStream(socket.GetStream(), bufferPool, 4096);

                    StreamExtended.ClientHelloInfo clientSslHelloInfo = 
                        await StreamExtended.SslTools.PeekClientHello(yourClientStream, bufferPool);

                    //will be null if no client hello was received (not a SSL connection)
                    if (clientSslHelloInfo != null)
                    {
                        string sniHostName = clientSslHelloInfo.Extensions?.FirstOrDefault(x => x.Key == "server_name").Value?.Data;
                        System.Console.WriteLine(sniHostName);
                    }
                    else
                        System.Console.WriteLine("ciao");
#else
                    System.Net.Sockets.NetworkStream yourClientStream = socket.GetStream();
#endif

                    System.Net.Security.SslStream stream = new System.Net.Security.SslStream(yourClientStream, false
                        , new System.Net.Security.RemoteCertificateValidationCallback(ValidateServerCertificate))
                    {
                        ReadTimeout = IOTimeout,
                        WriteTimeout = IOTimeout
                    };

                    // System.Net.Security.SslStream stream;
                    // .NET 5.0 only stream.TargetHostName

                    // https://docs.microsoft.com/en-us/aspnet/core/security/authentication/certauth?view=aspnetcore-5.0
                    // System.Net.Security.ServerCertificateSelectionCallback
                    // System.Net.Security.SslServerAuthenticationOptions

                    await stream.AuthenticateAsServerAsync(cert);
ststeiger commented 2 years ago

Never mind stream.TargetHostName, it's not working ... Dilettantes at work. Just use stream-extended instead, so far works fine everywhere.

ststeiger commented 2 years ago

Never mind, it does work with stream.TargetHostName. But you need to set ServerCertificateSelectionCallback and use SslServerAuthenticationOptions: https://github.com/dotnet/runtime/issues/57105

System.Net.Security.SslServerAuthenticationOptions sslOptions = 
    new System.Net.Security.SslServerAuthenticationOptions
{
    // ServerCertificate = certificate,
    ServerCertificateSelectionCallback = (sender, name) => cert ,
    CertificateRevocationCheckMode = System.Security.Cryptography.X509Certificates.X509RevocationMode.Offline,
    EnabledSslProtocols = System.Security.Authentication.SslProtocols.Tls12
};
stream.AuthenticateAsServer(sslOptions);