SmtpServer
SmtpServer copied to clipboard
SNI certificate-selection is not supported
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);
Never mind stream.TargetHostName, it's not working ... Dilettantes at work. Just use stream-extended instead, so far works fine everywhere.
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);