Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/libraries/System.Net.Mail/ref/System.Net.Mail.cs
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ public SmtpClient(string? host, int port) { }
public string? PickupDirectoryLocation { get { throw null; } set { } }
public int Port { get { throw null; } set { } }
public System.Net.ServicePoint ServicePoint { get { throw null; } }
public System.Net.Security.SslClientAuthenticationOptions SslOptions { get { throw null; } set { } }
public string? TargetName { get { throw null; } set { } }
public int Timeout { get { throw null; } set { } }
public bool UseDefaultCredentials { get { throw null; } set { } }
Expand Down
1 change: 1 addition & 0 deletions src/libraries/System.Net.Mail/ref/System.Net.Mail.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<ProjectReference Include="$(LibrariesProjectRoot)System.ComponentModel.EventBasedAsync\ref\System.ComponentModel.EventBasedAsync.csproj" />
<ProjectReference Include="$(LibrariesProjectRoot)System.Net.Primitives\ref\System.Net.Primitives.csproj" />
<ProjectReference Include="$(LibrariesProjectRoot)System.Net.Requests\ref\System.Net.Requests.csproj" />
<ProjectReference Include="$(LibrariesProjectRoot)System.Net.Security\ref\System.Net.Security.csproj" />
<ProjectReference Include="$(LibrariesProjectRoot)System.Runtime\ref\System.Runtime.csproj" />
<ProjectReference Include="$(LibrariesProjectRoot)System.Security.Cryptography\ref\System.Security.Cryptography.csproj" />
</ItemGroup>
Expand Down
2 changes: 2 additions & 0 deletions src/libraries/System.Net.Mail/src/System.Net.Mail.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@
<Compile Include="System\Net\Mail\NetEventSource.Mail.cs" />
<Compile Include="$(CommonPath)System\NotImplemented.cs"
Link="Common\System\NotImplemented.cs" />
<Compile Include="$(CommonPath)System\Net\Security\SslClientAuthenticationOptionsExtensions.cs"
Link="Common\System\Net\Security\SslClientAuthenticationOptionsExtensions.cs" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Net.Security;
using System.Runtime.Versioning;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
Expand Down Expand Up @@ -116,6 +117,13 @@ public bool EnableSsl
set => throw new PlatformNotSupportedException();
}

/// <summary>Gets or sets the options used to establish a TLS connection.</summary>
public SslClientAuthenticationOptions SslOptions
{
get => throw new PlatformNotSupportedException();
set => throw new PlatformNotSupportedException();
}

/// <summary>
/// Certificates used by the client for establishing an SSL connection with the server.
/// </summary>
Expand Down
39 changes: 36 additions & 3 deletions src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Globalization;
using System.IO;
using System.Net.NetworkInformation;
using System.Net.Security;
using System.Runtime.ExceptionServices;
using System.Runtime.Versioning;
using System.Security;
Expand Down Expand Up @@ -341,9 +342,41 @@ public bool EnableSsl
}
}

/// <summary>
/// Certificates used by the client for establishing an SSL connection with the server.
/// </summary>
/// <summary>Gets or sets the options used to establish a TLS connection.</summary>
/// <value>The TLS client authentication options. The default is a new <see cref="SslClientAuthenticationOptions"/> instance.</value>
/// <remarks>
/// These options are used only when <see cref="EnableSsl"/> is <see langword="true"/>.
/// When <see cref="SslClientAuthenticationOptions.TargetHost"/> is <see langword="null"/>,
/// the current <see cref="Host"/> is used without modifying the options.
/// Changing this object in place, including its nested objects such as
/// <see cref="SslClientAuthenticationOptions.ClientCertificates"/>, does not invalidate an existing connection.
/// To ensure changes are used for the next send, assign this property again, even to the same instance.
/// Assignment invalidates the cached connection so that the next send establishes a new connection.
/// Do not modify the options or their nested objects while a send is in progress.
/// </remarks>
/// <exception cref="ArgumentNullException">The value is <see langword="null"/>.</exception>
/// <exception cref="InvalidOperationException">A send operation is in progress.</exception>
public SslClientAuthenticationOptions SslOptions
{
get => _transport.SslOptions;
set
{
ArgumentNullException.ThrowIfNull(value);

if (_inCall)
{
throw new InvalidOperationException(SR.SmtpInvalidOperationDuringSend);
}

_transport.SslOptions = value;
}
}

/// <summary>Gets the certificates used by the client to establish a TLS connection with the server.</summary>
/// <remarks>
/// Returns <see cref="SslClientAuthenticationOptions.ClientCertificates"/> from <see cref="SslOptions"/>,
/// initializing an empty collection if necessary. Modifying the collection does not invalidate an existing connection.
/// </remarks>
public X509CertificateCollection ClientCertificates
{
get
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@
using System.Net.Security;
using System.Net.Sockets;
using System.Runtime.ExceptionServices;
using System.Security.Authentication;
using System.Security.Authentication.ExtendedProtection;
using System.Security.Cryptography.X509Certificates;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
Expand All @@ -33,8 +31,6 @@ internal sealed partial class SmtpConnection

private readonly ICredentialsByHost? _credentials;
private string[]? _extensions;
private bool _enableSsl;
private X509CertificateCollection? _clientCertificates;

internal SmtpConnection(SmtpTransport parent, SmtpClient client, ICredentialsByHost? credentials, ISmtpAuthenticationModule[] authenticationModules)
{
Expand All @@ -54,29 +50,7 @@ internal SmtpConnection(SmtpTransport parent, SmtpClient client, ICredentialsByH

internal SmtpReplyReaderFactory? Reader => _responseReader;

internal bool EnableSsl
{
get
{
return _enableSsl;
}
set
{
_enableSsl = value;
}
}

internal X509CertificateCollection? ClientCertificates
{
get
{
return _clientCertificates;
}
set
{
_clientCertificates = value;
}
}
internal SslClientAuthenticationOptions? SslOptions { get; set; }

internal void InitializeConnection(string host, int port)
{
Expand Down Expand Up @@ -144,7 +118,7 @@ internal async Task GetConnectionAsync<TIOAdapter>(string host, int port, Cancel
}

// Handle SSL/TLS
if (_enableSsl)
if (SslOptions is SslClientAuthenticationOptions sslOptions)
{
if (!_serverSupportsStartTls)
{
Expand All @@ -157,30 +131,19 @@ internal async Task GetConnectionAsync<TIOAdapter>(string host, int port, Cancel

await StartTlsCommand.SendAsync<TIOAdapter>(this, cancellationToken).ConfigureAwait(false);

#pragma warning disable SYSLIB0014 // ServicePointManager is obsolete
SslStream sslStream = new SslStream(_stream!, false, ServicePointManager.ServerCertificateValidationCallback);
SslStream sslStream = new SslStream(_stream!);
_stream = sslStream;
if (isAsync)
{
// If we are using async, we need to use the async version of AuthenticateAsClientAsync
await sslStream.AuthenticateAsClientAsync(
new SslClientAuthenticationOptions
{
TargetHost = host,
ClientCertificates = _clientCertificates,
EnabledSslProtocols = (SslProtocols)ServicePointManager.SecurityProtocol, // enums use same values
CertificateRevocationCheckMode = ServicePointManager.CheckCertificateRevocationList ?
X509RevocationMode.Online : X509RevocationMode.NoCheck,
},
cancellationToken).ConfigureAwait(false);
await sslStream.AuthenticateAsClientAsync(sslOptions, cancellationToken).ConfigureAwait(false);
}
else
{
// Synchronous version
sslStream.AuthenticateAsClient(host, _clientCertificates, (SslProtocols)ServicePointManager.SecurityProtocol, ServicePointManager.CheckCertificateRevocationList);
sslStream.AuthenticateAsClient(sslOptions);
}
#pragma warning restore SYSLIB0014 // ServicePointManager is obsolete

_stream = sslStream;
_responseReader = new SmtpReplyReaderFactory(_stream);

// According to RFC 3207: The client SHOULD send an EHLO command
Expand Down
18 changes: 15 additions & 3 deletions src/libraries/System.Net.Mail/src/System/Net/Mail/SmtpTransport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.IO;
using System.Net.Mime;
using System.Net.Security;
using System.Runtime.ExceptionServices;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
Expand Down Expand Up @@ -79,7 +80,17 @@ internal bool EnableSsl
}
}

internal X509CertificateCollection ClientCertificates => field ??= new X509CertificateCollection();
internal SslClientAuthenticationOptions SslOptions
{
get => field ??= new SslClientAuthenticationOptions();
set
{
field = value;
InvalidateCachedConnection();
}
}

internal X509CertificateCollection ClientCertificates => SslOptions.ClientCertificates ??= new X509CertificateCollection();

internal bool ServerSupportsEai
{
Expand Down Expand Up @@ -122,8 +133,9 @@ internal Task GetConnectionAsync<TIOAdapter>(string host, int port, Cancellation

if (EnableSsl)
{
_connection.EnableSsl = true;
_connection.ClientCertificates = ClientCertificates;
SslClientAuthenticationOptions sslOptions = SslOptions.ShallowClone();
sslOptions.TargetHost ??= host;
_connection.SslOptions = sslOptions;
}

return _connection.GetConnectionAsync<TIOAdapter>(host, port, cancellationToken);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ public class LoopbackSmtpServer : IDisposable
public ParsedMailMessage Message { get; private set; }
public bool IsEncrypted { get; private set; }
public string TlsHostName { get; private set; }
public System.Security.Authentication.SslProtocols TlsProtocol { get; private set; }
public SslApplicationProtocol ApplicationProtocol { get; private set; }

public int ConnectionCount { get; private set; }
public int MessagesReceived { get; private set; }
Expand Down Expand Up @@ -263,6 +265,8 @@ await SendMessageAsync(
await sslStream.AuthenticateAsServerAsync(SslOptions);
IsEncrypted = true;
TlsHostName = sslStream.TargetHostName;
TlsProtocol = sslStream.SslProtocol;
ApplicationProtocol = sslStream.NegotiatedApplicationProtocol;

stream = sslStream;
break;
Expand Down
110 changes: 110 additions & 0 deletions src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@
using System.Globalization;
using System.IO;
using System.Net.NetworkInformation;
using System.Net.Security;
using System.Net.Sockets;
using System.Reflection;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.DotNet.RemoteExecutor;
Expand Down Expand Up @@ -80,6 +83,113 @@ public void EnableSslTest(bool value)
Assert.Equal(value, Smtp.EnableSsl);
}

[Fact]
public void SslOptions_DefaultsAndIdentity()
{
SslClientAuthenticationOptions options = Smtp.SslOptions;
Assert.Same(options, Smtp.SslOptions);
Assert.Null(options.TargetHost);
Assert.Null(options.ClientCertificates);
Assert.Null(options.RemoteCertificateValidationCallback);
Assert.Equal(SslProtocols.None, options.EnabledSslProtocols);
Assert.Equal(X509RevocationMode.NoCheck, options.CertificateRevocationCheckMode);

using var other = new SmtpClient();
Assert.NotSame(options, other.SslOptions);

var replacement = new SslClientAuthenticationOptions();
Smtp.SslOptions = replacement;
Assert.Same(replacement, Smtp.SslOptions);
AssertExtensions.Throws<ArgumentNullException>("value", () => Smtp.SslOptions = null!);
Assert.Same(replacement, Smtp.SslOptions);
}

[Theory]
[InlineData(false)]
[InlineData(true)]
public void ClientCertificates_ForwardsToSslOptions(bool getCertificatesFirst)
{
if (getCertificatesFirst)
{
Assert.Empty(Smtp.ClientCertificates);
}

var certificates = new X509CertificateCollection();
var options = new SslClientAuthenticationOptions { ClientCertificates = certificates };
Smtp.SslOptions = options;
Assert.Same(certificates, Smtp.ClientCertificates);

var replacement = new X509CertificateCollection();
options.ClientCertificates = replacement;
Assert.Same(replacement, Smtp.ClientCertificates);

options.ClientCertificates = null;
X509CertificateCollection initialized = Smtp.ClientCertificates;
Assert.Empty(initialized);
Assert.Same(initialized, options.ClientCertificates);
Assert.Same(initialized, Smtp.ClientCertificates);

Smtp.SslOptions = new SslClientAuthenticationOptions();
Assert.NotSame(initialized, Smtp.ClientCertificates);
Assert.Same(Smtp.ClientCertificates, Smtp.SslOptions.ClientCertificates);
}

[ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
[InlineData(false)]
[InlineData(true)]
public async Task SslOptions_ServicePointManagerIgnored(bool customValidation)
{
await RemoteExecutor.Invoke(async useCustomValidation =>
{
int globalCallbackCalls = 0;
#pragma warning disable SYSLIB0014 // Verify that SMTP no longer uses these global settings.
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls13;
ServicePointManager.CheckCertificateRevocationList = true;
ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, errors) =>
{
globalCallbackCalls++;
return !bool.Parse(useCustomValidation);
};
#pragma warning restore SYSLIB0014

using var certificates = new CertificateSetup();
using var server = new LoopbackSmtpServer();
server.SslOptions = new SslServerAuthenticationOptions
{
ServerCertificateContext = certificates.CreateSslStreamCertificateContext(),
EnabledSslProtocols = SslProtocols.Tls12,
};
using SmtpClient client = server.CreateClient();
client.EnableSsl = true;
Assert.Equal(SslProtocols.None, client.SslOptions.EnabledSslProtocols);
Assert.Equal(X509RevocationMode.NoCheck, client.SslOptions.CertificateRevocationCheckMode);
Assert.Null(client.SslOptions.RemoteCertificateValidationCallback);

if (bool.Parse(useCustomValidation))
{
int clientCallbackCalls = 0;
client.SslOptions.RemoteCertificateValidationCallback = (sender, certificate, chain, errors) =>
{
clientCallbackCalls++;
Assert.Equal(X509RevocationMode.NoCheck, chain.ChainPolicy.RevocationMode);
return true;
};

await client.SendMailAsync("from@example.com", "to@example.com", "subject", "body");
Assert.Equal(1, clientCallbackCalls);
Assert.True(server.IsEncrypted);
}
else
{
SmtpException exception = await Assert.ThrowsAsync<SmtpException>(() =>
client.SendMailAsync("from@example.com", "to@example.com", "subject", "body"));
Assert.IsType<AuthenticationException>(exception.InnerException);
}

Assert.Equal(0, globalCallbackCalls);
}, customValidation.ToString()).DisposeAsync();
}

[Theory]
[InlineData("127.0.0.1")]
[InlineData("smtp.ximian.com")]
Expand Down
Loading
Loading