SmtpServer icon indicating copy to clipboard operation
SmtpServer copied to clipboard

SNI certificate-selection is not supported

Open ststeiger opened this issue 3 years ago • 2 comments

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 avatar Oct 14 '21 19:10 ststeiger

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

ststeiger avatar Oct 14 '21 21:10 ststeiger

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);

ststeiger avatar Oct 14 '21 22:10 ststeiger