Skip to content

Commit 04de968

Browse files
authored
Add a callback for SslClientAuthenticationOptions (#5483)
Closes #5478
1 parent 30ba2dd commit 04de968

12 files changed

+393
-149
lines changed

src/Npgsql/Internal/NpgsqlConnector.cs

Lines changed: 72 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ public sealed partial class NpgsqlConnector
5555
/// </summary>
5656
public NpgsqlConnectionStringBuilder Settings { get; }
5757

58-
Action<X509CertificateCollection>? ClientCertificatesCallback { get; }
59-
RemoteCertificateValidationCallback? UserCertificateValidationCallback { get; }
58+
Action<SslClientAuthenticationOptions>? SslClientAuthenticationOptionsCallback { get; }
59+
6060
#pragma warning disable CS0618 // ProvidePasswordCallback is obsolete
6161
ProvidePasswordCallback? ProvidePasswordCallback { get; }
6262
#pragma warning restore CS0618
@@ -282,7 +282,11 @@ internal bool PostgresCancellationPerformed
282282
internal bool AttemptPostgresCancellation { get; private set; }
283283
static readonly TimeSpan _cancelImmediatelyTimeout = TimeSpan.FromMilliseconds(-1);
284284

285+
#pragma warning disable CA1859
286+
// We're casting to IDisposable to not explicitly reference X509Certificate2 for NativeAOT
287+
// TODO: probably pointless now, needs to be rechecked
285288
IDisposable? _certificate;
289+
#pragma warning restore CA1859
286290

287291
internal NpgsqlLoggingConfiguration LoggingConfiguration { get; }
288292

@@ -337,21 +341,42 @@ internal bool PostgresCancellationPerformed
337341
internal NpgsqlConnector(NpgsqlDataSource dataSource, NpgsqlConnection conn)
338342
: this(dataSource)
339343
{
340-
if (conn.ProvideClientCertificatesCallback is not null)
341-
ClientCertificatesCallback = certs => conn.ProvideClientCertificatesCallback(certs);
342-
if (conn.UserCertificateValidationCallback is not null)
343-
UserCertificateValidationCallback = conn.UserCertificateValidationCallback;
344-
344+
var sslClientAuthenticationOptionsCallback = conn.SslClientAuthenticationOptionsCallback;
345345
#pragma warning disable CS0618 // Obsolete
346+
var provideClientCertificatesCallback = conn.ProvideClientCertificatesCallback;
347+
var userCertificateValidationCallback = conn.UserCertificateValidationCallback;
348+
if (provideClientCertificatesCallback is not null ||
349+
userCertificateValidationCallback is not null)
350+
{
351+
if (sslClientAuthenticationOptionsCallback is not null)
352+
throw new NotSupportedException(NpgsqlStrings.SslClientAuthenticationOptionsCallbackWithOtherCallbacksNotSupported);
353+
354+
sslClientAuthenticationOptionsCallback = options =>
355+
{
356+
if (provideClientCertificatesCallback is not null)
357+
{
358+
options.ClientCertificates ??= new X509Certificate2Collection();
359+
provideClientCertificatesCallback.Invoke(options.ClientCertificates);
360+
}
361+
362+
if (userCertificateValidationCallback is not null)
363+
{
364+
options.RemoteCertificateValidationCallback = userCertificateValidationCallback;
365+
}
366+
};
367+
}
368+
369+
if (sslClientAuthenticationOptionsCallback is not null)
370+
SslClientAuthenticationOptionsCallback = sslClientAuthenticationOptionsCallback;
371+
346372
ProvidePasswordCallback = conn.ProvidePasswordCallback;
347373
#pragma warning restore CS0618
348374
}
349375

350376
NpgsqlConnector(NpgsqlConnector connector)
351377
: this(connector.DataSource)
352378
{
353-
ClientCertificatesCallback = connector.ClientCertificatesCallback;
354-
UserCertificateValidationCallback = connector.UserCertificateValidationCallback;
379+
SslClientAuthenticationOptionsCallback = connector.SslClientAuthenticationOptionsCallback;
355380
ProvidePasswordCallback = connector.ProvidePasswordCallback;
356381
}
357382

@@ -367,8 +392,7 @@ internal NpgsqlConnector(NpgsqlDataSource dataSource, NpgsqlConnection conn)
367392
TransactionLogger = LoggingConfiguration.TransactionLogger;
368393
CopyLogger = LoggingConfiguration.CopyLogger;
369394

370-
ClientCertificatesCallback = dataSource.ClientCertificatesCallback;
371-
UserCertificateValidationCallback = dataSource.UserCertificateValidationCallback;
395+
SslClientAuthenticationOptionsCallback = dataSource.SslClientAuthenticationOptionsCallback;
372396

373397
#if NET7_0_OR_GREATER
374398
NegotiateOptionsCallback = dataSource.Configuration.NegotiateOptionsCallback;
@@ -777,7 +801,7 @@ async Task RawOpen(SslMode sslMode, NpgsqlTimeout timeout, bool async, Cancellat
777801
throw new NpgsqlException("SSL connection requested. No SSL enabled connection from this host is configured.");
778802
break;
779803
case 'S':
780-
await DataSource.TransportSecurityHandler.NegotiateEncryption(async, this, sslMode, timeout).ConfigureAwait(false);
804+
await DataSource.TransportSecurityHandler.NegotiateEncryption(async, this, sslMode, timeout, cancellationToken).ConfigureAwait(false);
781805
break;
782806
}
783807

@@ -802,7 +826,7 @@ async Task RawOpen(SslMode sslMode, NpgsqlTimeout timeout, bool async, Cancellat
802826
}
803827
}
804828

805-
internal async Task NegotiateEncryption(SslMode sslMode, NpgsqlTimeout timeout, bool async)
829+
internal async Task NegotiateEncryption(SslMode sslMode, NpgsqlTimeout timeout, bool async, CancellationToken cancellationToken)
806830
{
807831
var clientCertificates = new X509Certificate2Collection();
808832
var certPath = Settings.SslCertificate ?? PostgresEnvironment.SslCert ?? PostgresEnvironment.SslCertDefault;
@@ -812,7 +836,7 @@ internal async Task NegotiateEncryption(SslMode sslMode, NpgsqlTimeout timeout,
812836
var password = Settings.SslPassword;
813837

814838
X509Certificate2? cert = null;
815-
if (Path.GetExtension(certPath).ToUpperInvariant() != ".PFX")
839+
if (!string.Equals(Path.GetExtension(certPath), ".pfx", StringComparison.OrdinalIgnoreCase))
816840
{
817841
// It's PEM time
818842
var keyPath = Settings.SslKey ?? PostgresEnvironment.SslKey ?? PostgresEnvironment.SslKeyDefault;
@@ -836,28 +860,13 @@ internal async Task NegotiateEncryption(SslMode sslMode, NpgsqlTimeout timeout,
836860

837861
try
838862
{
839-
ClientCertificatesCallback?.Invoke(clientCertificates);
840-
841863
var checkCertificateRevocation = Settings.CheckCertificateRevocation;
842864

843865
RemoteCertificateValidationCallback? certificateValidationCallback;
844866
X509Certificate2? caCert;
845867
string? certRootPath = null;
846868

847-
if (UserCertificateValidationCallback is not null)
848-
{
849-
if (sslMode is SslMode.VerifyCA or SslMode.VerifyFull)
850-
throw new ArgumentException(string.Format(NpgsqlStrings.CannotUseSslVerifyWithUserCallback, sslMode));
851-
852-
if (Settings.RootCertificate is not null)
853-
throw new ArgumentException(NpgsqlStrings.CannotUseSslRootCertificateWithUserCallback);
854-
855-
if (DataSource.TransportSecurityHandler.RootCertificateCallback is not null)
856-
throw new ArgumentException(NpgsqlStrings.CannotUseValidationRootCertificateCallbackWithUserCallback);
857-
858-
certificateValidationCallback = UserCertificateValidationCallback;
859-
}
860-
else if (sslMode is SslMode.Prefer or SslMode.Require)
869+
if (sslMode is SslMode.Prefer or SslMode.Require)
861870
{
862871
certificateValidationCallback = SslTrustServerValidation;
863872
checkCertificateRevocation = false;
@@ -892,19 +901,48 @@ internal async Task NegotiateEncryption(SslMode sslMode, NpgsqlTimeout timeout,
892901

893902
timeout.CheckAndApply(this);
894903

895-
try
904+
var sslStream = new SslStream(_stream, leaveInnerStreamOpen: false);
905+
906+
var sslStreamOptions = new SslClientAuthenticationOptions
907+
{
908+
TargetHost = host,
909+
ClientCertificates = clientCertificates,
910+
EnabledSslProtocols = SslProtocols.None,
911+
CertificateRevocationCheckMode = checkCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.Offline,
912+
RemoteCertificateValidationCallback = certificateValidationCallback
913+
};
914+
915+
if (SslClientAuthenticationOptionsCallback is not null)
896916
{
897-
var sslStream = new SslStream(_stream, leaveInnerStreamOpen: false, certificateValidationCallback);
917+
SslClientAuthenticationOptionsCallback.Invoke(sslStreamOptions);
898918

919+
// User changed remote certificate validation callback
920+
// Check whether the change doesn't lead to unexpected behavior
921+
if (sslStreamOptions.RemoteCertificateValidationCallback != certificateValidationCallback)
922+
{
923+
if (sslMode is SslMode.VerifyCA or SslMode.VerifyFull)
924+
throw new ArgumentException(string.Format(NpgsqlStrings.CannotUseSslVerifyWithCustomValidationCallback, sslMode));
925+
926+
if (Settings.RootCertificate is not null)
927+
throw new ArgumentException(NpgsqlStrings.CannotUseSslRootCertificateWithCustomValidationCallback);
928+
929+
if (DataSource.TransportSecurityHandler.RootCertificateCallback is not null)
930+
throw new ArgumentException(NpgsqlStrings.CannotUseValidationRootCertificateCallbackWithCustomValidationCallback);
931+
}
932+
}
933+
934+
try
935+
{
899936
if (async)
900-
await sslStream.AuthenticateAsClientAsync(host, clientCertificates, SslProtocols.None, checkCertificateRevocation).ConfigureAwait(false);
937+
await sslStream.AuthenticateAsClientAsync(sslStreamOptions, cancellationToken).ConfigureAwait(false);
901938
else
902-
sslStream.AuthenticateAsClient(host, clientCertificates, SslProtocols.None, checkCertificateRevocation);
939+
sslStream.AuthenticateAsClient(sslStreamOptions);
903940

904941
_stream = sslStream;
905942
}
906943
catch (Exception e)
907944
{
945+
sslStream.Dispose();
908946
throw new NpgsqlException("Exception while performing SSL handshake", e);
909947
}
910948

src/Npgsql/Internal/TransportSecurityHandler.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Security.Cryptography.X509Certificates;
3+
using System.Threading;
34
using System.Threading.Tasks;
45
using Npgsql.Properties;
56
using Npgsql.Util;
@@ -16,7 +17,7 @@ public virtual Func<X509Certificate2?>? RootCertificateCallback
1617
set => throw new NotSupportedException(string.Format(NpgsqlStrings.TransportSecurityDisabled, nameof(NpgsqlSlimDataSourceBuilder.EnableTransportSecurity)));
1718
}
1819

19-
public virtual Task NegotiateEncryption(bool async, NpgsqlConnector connector, SslMode sslMode, NpgsqlTimeout timeout)
20+
public virtual Task NegotiateEncryption(bool async, NpgsqlConnector connector, SslMode sslMode, NpgsqlTimeout timeout, CancellationToken cancellationToken)
2021
=> throw new NotSupportedException(string.Format(NpgsqlStrings.TransportSecurityDisabled, nameof(NpgsqlSlimDataSourceBuilder.EnableTransportSecurity)));
2122

2223
public virtual void AuthenticateSASLSha256Plus(NpgsqlConnector connector, ref string mechanism, ref string cbindFlag, ref string cbind,
@@ -30,8 +31,8 @@ sealed class RealTransportSecurityHandler : TransportSecurityHandler
3031

3132
public override Func<X509Certificate2?>? RootCertificateCallback { get; set; }
3233

33-
public override Task NegotiateEncryption(bool async, NpgsqlConnector connector, SslMode sslMode, NpgsqlTimeout timeout)
34-
=> connector.NegotiateEncryption(sslMode, timeout, async);
34+
public override Task NegotiateEncryption(bool async, NpgsqlConnector connector, SslMode sslMode, NpgsqlTimeout timeout, CancellationToken cancellationToken)
35+
=> connector.NegotiateEncryption(sslMode, timeout, async, cancellationToken);
3536

3637
public override void AuthenticateSASLSha256Plus(NpgsqlConnector connector, ref string mechanism, ref string cbindFlag, ref string cbind,
3738
ref bool successfulBind)

src/Npgsql/NpgsqlConnection.cs

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1017,6 +1017,7 @@ internal void OnNotification(NpgsqlNotificationEventArgs e)
10171017
/// <remarks>
10181018
/// See <see href="https://msdn.microsoft.com/en-us/library/system.net.security.localcertificateselectioncallback(v=vs.110).aspx"/>
10191019
/// </remarks>
1020+
[Obsolete("Use UseSslClientAuthenticationOptionsCallback")]
10201021
public ProvideClientCertificatesCallback? ProvideClientCertificatesCallback { get; set; }
10211022

10221023
/// <summary>
@@ -1032,8 +1033,19 @@ internal void OnNotification(NpgsqlNotificationEventArgs e)
10321033
/// See <see href="https://msdn.microsoft.com/en-us/library/system.net.security.remotecertificatevalidationcallback(v=vs.110).aspx"/>.
10331034
/// </para>
10341035
/// </remarks>
1036+
[Obsolete("Use UseSslClientAuthenticationOptionsCallback")]
10351037
public RemoteCertificateValidationCallback? UserCertificateValidationCallback { get; set; }
10361038

1039+
/// <summary>
1040+
/// When using SSL/TLS, this is a callback that allows customizing SslStream's authentication options.
1041+
/// </summary>
1042+
/// <remarks>
1043+
/// <para>
1044+
/// See <see href="https://learn.microsoft.com/en-us/dotnet/api/system.net.security.sslclientauthenticationoptions?view=net-8.0"/>.
1045+
/// </para>
1046+
/// </remarks>
1047+
public Action<SslClientAuthenticationOptions>? SslClientAuthenticationOptionsCallback { get; set; }
1048+
10371049
#endregion SSL
10381050

10391051
#region Backend version, capabilities, settings
@@ -1747,9 +1759,10 @@ object ICloneable.Clone()
17471759
? _cloningInstantiator!(_connectionString)
17481760
: _dataSource.CreateConnection();
17491761

1762+
conn.SslClientAuthenticationOptionsCallback = SslClientAuthenticationOptionsCallback;
1763+
#pragma warning disable CS0618 // Obsolete
17501764
conn.ProvideClientCertificatesCallback = ProvideClientCertificatesCallback;
17511765
conn.UserCertificateValidationCallback = UserCertificateValidationCallback;
1752-
#pragma warning disable CS0618 // Obsolete
17531766
conn.ProvidePasswordCallback = ProvidePasswordCallback;
17541767
#pragma warning restore CS0618
17551768
conn._userFacingConnectionString = _userFacingConnectionString;
@@ -1773,13 +1786,10 @@ public NpgsqlConnection CloneWith(string connectionString)
17731786

17741787
return new NpgsqlConnection(csb.ToString())
17751788
{
1776-
ProvideClientCertificatesCallback =
1777-
ProvideClientCertificatesCallback ??
1778-
(_dataSource?.ClientCertificatesCallback is { } clientCertificatesCallback
1779-
? (ProvideClientCertificatesCallback)(certs => clientCertificatesCallback(certs))
1780-
: null),
1781-
UserCertificateValidationCallback = UserCertificateValidationCallback ?? _dataSource?.UserCertificateValidationCallback,
1789+
SslClientAuthenticationOptionsCallback = SslClientAuthenticationOptionsCallback ?? _dataSource?.SslClientAuthenticationOptionsCallback,
17821790
#pragma warning disable CS0618 // Obsolete
1791+
ProvideClientCertificatesCallback = ProvideClientCertificatesCallback,
1792+
UserCertificateValidationCallback = UserCertificateValidationCallback,
17831793
ProvidePasswordCallback = ProvidePasswordCallback,
17841794
#pragma warning restore CS0618
17851795
};

src/Npgsql/NpgsqlDataSource.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ public abstract class NpgsqlDataSource : DbDataSource
4040
internal NpgsqlDatabaseInfo DatabaseInfo { get; private set; } = null!; // Initialized at bootstrapping
4141

4242
internal TransportSecurityHandler TransportSecurityHandler { get; }
43-
internal RemoteCertificateValidationCallback? UserCertificateValidationCallback { get; }
44-
internal Action<X509CertificateCollection>? ClientCertificatesCallback { get; }
43+
44+
internal Action<SslClientAuthenticationOptions>? SslClientAuthenticationOptionsCallback { get; }
4545

4646
readonly Func<NpgsqlConnectionStringBuilder, string>? _passwordProvider;
4747
readonly Func<NpgsqlConnectionStringBuilder, CancellationToken, ValueTask<string>>? _passwordProviderAsync;
@@ -98,8 +98,7 @@ internal NpgsqlDataSource(
9898
LoggingConfiguration,
9999
TransportSecurityHandler,
100100
IntegratedSecurityHandler,
101-
UserCertificateValidationCallback,
102-
ClientCertificatesCallback,
101+
SslClientAuthenticationOptionsCallback,
103102
_passwordProvider,
104103
_passwordProviderAsync,
105104
_periodicPasswordProvider,

src/Npgsql/NpgsqlDataSourceBuilder.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ public NpgsqlDataSourceBuilder EnableUnmappedTypes()
194194
/// </para>
195195
/// </remarks>
196196
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
197+
[Obsolete("Use UseSslClientAuthenticationOptionsCallback")]
197198
public NpgsqlDataSourceBuilder UseUserCertificateValidationCallback(RemoteCertificateValidationCallback userCertificateValidationCallback)
198199
{
199200
_internalBuilder.UseUserCertificateValidationCallback(userCertificateValidationCallback);
@@ -205,6 +206,7 @@ public NpgsqlDataSourceBuilder UseUserCertificateValidationCallback(RemoteCertif
205206
/// </summary>
206207
/// <param name="clientCertificate">The client certificate to be sent to PostgreSQL when opening a connection.</param>
207208
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
209+
[Obsolete("Use UseSslClientAuthenticationOptionsCallback")]
208210
public NpgsqlDataSourceBuilder UseClientCertificate(X509Certificate? clientCertificate)
209211
{
210212
_internalBuilder.UseClientCertificate(clientCertificate);
@@ -216,12 +218,29 @@ public NpgsqlDataSourceBuilder UseClientCertificate(X509Certificate? clientCerti
216218
/// </summary>
217219
/// <param name="clientCertificates">The client certificate collection to be sent to PostgreSQL when opening a connection.</param>
218220
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
221+
[Obsolete("Use UseSslClientAuthenticationOptionsCallback")]
219222
public NpgsqlDataSourceBuilder UseClientCertificates(X509CertificateCollection? clientCertificates)
220223
{
221224
_internalBuilder.UseClientCertificates(clientCertificates);
222225
return this;
223226
}
224227

228+
/// <summary>
229+
/// When using SSL/TLS, this is a callback that allows customizing SslStream's authentication options.
230+
/// </summary>
231+
/// <param name="sslClientAuthenticationOptionsCallback">The callback to customize SslStream's authentication options.</param>
232+
/// <remarks>
233+
/// <para>
234+
/// See <see href="https://learn.microsoft.com/en-us/dotnet/api/system.net.security.sslclientauthenticationoptions?view=net-8.0"/>.
235+
/// </para>
236+
/// </remarks>
237+
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
238+
public NpgsqlDataSourceBuilder UseSslClientAuthenticationOptionsCallback(Action<SslClientAuthenticationOptions>? sslClientAuthenticationOptionsCallback)
239+
{
240+
_internalBuilder.UseSslClientAuthenticationOptionsCallback(sslClientAuthenticationOptionsCallback);
241+
return this;
242+
}
243+
225244
/// <summary>
226245
/// Specifies a callback to modify the collection of SSL/TLS client certificates which Npgsql will send to PostgreSQL for
227246
/// certificate-based authentication. This is an advanced API, consider using <see cref="UseClientCertificate" /> or
@@ -239,6 +258,7 @@ public NpgsqlDataSourceBuilder UseClientCertificates(X509CertificateCollection?
239258
/// </para>
240259
/// </remarks>
241260
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
261+
[Obsolete("Use UseSslClientAuthenticationOptionsCallback")]
242262
public NpgsqlDataSourceBuilder UseClientCertificatesCallback(Action<X509CertificateCollection>? clientCertificatesCallback)
243263
{
244264
_internalBuilder.UseClientCertificatesCallback(clientCertificatesCallback);

0 commit comments

Comments
 (0)