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
30 changes: 18 additions & 12 deletions src/Npgsql/Internal/NpgsqlConnector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,11 @@ public sealed partial class NpgsqlConnector : IDisposable
/// </summary>
public NpgsqlConnectionStringBuilder Settings { get; }

ProvideClientCertificatesCallback? ProvideClientCertificatesCallback { get; }
Action<X509CertificateCollection>? ClientCertificatesCallback { get; }
RemoteCertificateValidationCallback? UserCertificateValidationCallback { get; }
#pragma warning disable CS0618 // ProvidePasswordCallback is obsolete
ProvidePasswordCallback? ProvidePasswordCallback { get; }
#pragma warning restore CS0618

public Encoding TextEncoding { get; private set; } = default!;

Expand Down Expand Up @@ -302,8 +304,11 @@ internal bool PostgresCancellationPerformed
internal NpgsqlConnector(NpgsqlDataSource dataSource, NpgsqlConnection conn)
: this(dataSource)
{
ProvideClientCertificatesCallback = conn.ProvideClientCertificatesCallback;
UserCertificateValidationCallback = conn.UserCertificateValidationCallback;
if (conn.ProvideClientCertificatesCallback is not null)
ClientCertificatesCallback = certs => conn.ProvideClientCertificatesCallback(certs);
if (conn.UserCertificateValidationCallback is not null)
UserCertificateValidationCallback = conn.UserCertificateValidationCallback;

#pragma warning disable CS0618 // Obsolete
ProvidePasswordCallback = conn.ProvidePasswordCallback;
#pragma warning restore CS0618
Expand All @@ -312,7 +317,7 @@ internal NpgsqlConnector(NpgsqlDataSource dataSource, NpgsqlConnection conn)
NpgsqlConnector(NpgsqlConnector connector)
: this(connector.DataSource)
{
ProvideClientCertificatesCallback = connector.ProvideClientCertificatesCallback;
ClientCertificatesCallback = connector.ClientCertificatesCallback;
UserCertificateValidationCallback = connector.UserCertificateValidationCallback;
ProvidePasswordCallback = connector.ProvidePasswordCallback;
}
Expand All @@ -329,6 +334,9 @@ internal NpgsqlConnector(NpgsqlDataSource dataSource, NpgsqlConnection conn)
TransactionLogger = LoggingConfiguration.TransactionLogger;
CopyLogger = LoggingConfiguration.CopyLogger;

ClientCertificatesCallback = dataSource.ClientCertificatesCallback;
UserCertificateValidationCallback = dataSource.UserCertificateValidationCallback;

State = ConnectorState.Closed;
TransactionStatus = TransactionStatus.Idle;
Settings = dataSource.Settings;
Expand Down Expand Up @@ -771,15 +779,15 @@ async Task RawOpen(SslMode sslMode, NpgsqlTimeout timeout, bool async, Cancellat
cert = new X509Certificate2(cert.Export(X509ContentType.Pkcs12));
}
#else
throw new NotSupportedException("PEM certificates are only supported with .NET 5 and higher");
throw new NotSupportedException("PEM certificates are only supported with .NET 5 and higher");
#endif
}
if (cert is null)
cert = new X509Certificate2(certPath, password);

cert ??= new X509Certificate2(certPath, password);
clientCertificates.Add(cert);
}

ProvideClientCertificatesCallback?.Invoke(clientCertificates);
ClientCertificatesCallback?.Invoke(clientCertificates);

var checkCertificateRevocation = Settings.CheckCertificateRevocation;

Expand Down Expand Up @@ -831,11 +839,9 @@ async Task RawOpen(SslMode sslMode, NpgsqlTimeout timeout, bool async, Cancellat
#endif

if (async)
await sslStream.AuthenticateAsClientAsync(Host, clientCertificates,
sslProtocols, checkCertificateRevocation);
await sslStream.AuthenticateAsClientAsync(Host, clientCertificates, sslProtocols, checkCertificateRevocation);
else
sslStream.AuthenticateAsClient(Host, clientCertificates,
sslProtocols, checkCertificateRevocation);
sslStream.AuthenticateAsClient(Host, clientCertificates, sslProtocols, checkCertificateRevocation);

_stream = sslStream;
}
Expand Down
27 changes: 17 additions & 10 deletions src/Npgsql/NpgsqlConnection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ public override string ConnectionString
/// that was previously opened from the pool.
/// </p>
/// </remarks>
[Obsolete("Use NpgsqlDataSource.UsePeriodicPasswordProvider or UseInlinePasswordProvider")]
[Obsolete("Use NpgsqlDataSourceBuilder.UsePeriodicPasswordProvider or inject passwords directly into NpgsqlDataSource.Password")]
public ProvidePasswordCallback? ProvidePasswordCallback { get; set; }

#endregion Connection string management
Expand Down Expand Up @@ -1029,16 +1029,17 @@ internal void OnNotification(NpgsqlNotificationEventArgs e)
public ProvideClientCertificatesCallback? ProvideClientCertificatesCallback { get; set; }

/// <summary>
/// <para>
/// Verifies the remote Secure Sockets Layer (SSL) certificate used for authentication.
/// </para>
/// When using SSL/TLS, this is a callback that allows customizing how the PostgreSQL-provided certificate is verified. This is an
/// advanced API, consider using <see cref="SslMode.VerifyFull" /> or <see cref="SslMode.VerifyCA" /> instead.
/// </summary>
/// <remarks>
/// <para>
/// Cannot be used in conjunction with <see cref="SslMode.Disable" />, <see cref="SslMode.VerifyCA" /> and
/// <see cref="SslMode.VerifyFull" />.
/// </para>
/// </summary>
/// <remarks>
/// See <see href="https://msdn.microsoft.com/en-us/library/system.net.security.remotecertificatevalidationcallback(v=vs.110).aspx"/>
/// <para>
/// See <see href="https://msdn.microsoft.com/en-us/library/system.net.security.remotecertificatevalidationcallback(v=vs.110).aspx"/>.
/// </para>
/// </remarks>
public RemoteCertificateValidationCallback? UserCertificateValidationCallback { get; set; }

Expand Down Expand Up @@ -1806,9 +1807,14 @@ public NpgsqlConnection CloneWith(string connectionString)
if (csb.PersistSecurityInfo && !Settings.PersistSecurityInfo)
csb.PersistSecurityInfo = false;

return new NpgsqlConnection(csb.ToString()) {
ProvideClientCertificatesCallback = ProvideClientCertificatesCallback,
UserCertificateValidationCallback = UserCertificateValidationCallback,
return new NpgsqlConnection(csb.ToString())
{
ProvideClientCertificatesCallback =
ProvideClientCertificatesCallback ??
(_dataSource?.ClientCertificatesCallback is { } clientCertificatesCallback
? (ProvideClientCertificatesCallback)(certs => clientCertificatesCallback(certs))
: null),
UserCertificateValidationCallback = UserCertificateValidationCallback ?? _dataSource?.UserCertificateValidationCallback,
#pragma warning disable CS0618 // Obsolete
ProvidePasswordCallback = ProvidePasswordCallback,
#pragma warning restore CS0618
Expand Down Expand Up @@ -2002,6 +2008,7 @@ enum ConnectorBindingScope
/// <param name="database">Database Name</param>
/// <param name="username">User</param>
/// <returns>A valid password for connecting to the database</returns>
[Obsolete("Use NpgsqlDataSourceBuilder.UsePeriodicPasswordProvider or inject passwords directly into NpgsqlDataSource.Password")]
public delegate string ProvidePasswordCallback(string host, int port, string database, string username);

#endregion
7 changes: 7 additions & 0 deletions src/Npgsql/NpgsqlDataSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
using System.Data.Common;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Net.Security;
using System.Runtime.CompilerServices;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using System.Transactions;
Expand Down Expand Up @@ -43,6 +45,9 @@ public abstract class NpgsqlDataSource : DbDataSource
/// </summary>
internal NpgsqlDatabaseInfo DatabaseInfo { get; set; } = null!; // Initialized at bootstrapping

internal RemoteCertificateValidationCallback? UserCertificateValidationCallback { get; }
internal Action<X509CertificateCollection>? ClientCertificatesCallback { get; }

readonly Func<NpgsqlConnectionStringBuilder, CancellationToken, ValueTask<string>>? _periodicPasswordProvider;
readonly TimeSpan _periodicPasswordSuccessRefreshInterval, _periodicPasswordFailureRefreshInterval;

Expand Down Expand Up @@ -84,6 +89,8 @@ internal NpgsqlDataSource(
Configuration = dataSourceConfig;

(LoggingConfiguration,
UserCertificateValidationCallback,
ClientCertificatesCallback,
_periodicPasswordProvider,
_periodicPasswordSuccessRefreshInterval,
_periodicPasswordFailureRefreshInterval,
Expand Down
80 changes: 80 additions & 0 deletions src/Npgsql/NpgsqlDataSourceBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Net.Security;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
Expand All @@ -21,6 +23,9 @@ public class NpgsqlDataSourceBuilder : INpgsqlTypeMapper
ILoggerFactory? _loggerFactory;
bool _sensitiveDataLoggingEnabled;

RemoteCertificateValidationCallback? _userCertificateValidationCallback;
Action<X509CertificateCollection>? _clientCertificatesCallback;

Func<NpgsqlConnectionStringBuilder, CancellationToken, ValueTask<string>>? _periodicPasswordProvider;
TimeSpan _periodicPasswordSuccessRefreshInterval, _periodicPasswordFailureRefreshInterval;

Expand Down Expand Up @@ -77,6 +82,77 @@ public NpgsqlDataSourceBuilder EnableParameterLogging(bool parameterLoggingEnabl
return this;
}

#region Authentication

/// <summary>
/// When using SSL/TLS, this is a callback that allows customizing how the PostgreSQL-provided certificate is verified. This is an
/// advanced API, consider using <see cref="SslMode.VerifyFull" /> or <see cref="SslMode.VerifyCA" /> instead.
/// </summary>
/// <param name="userCertificateValidationCallback">The callback containing custom callback verification logic.</param>
/// <remarks>
/// <para>
/// Cannot be used in conjunction with <see cref="SslMode.Disable" />, <see cref="SslMode.VerifyCA" /> or
/// <see cref="SslMode.VerifyFull" />.
/// </para>
/// <para>
/// See <see href="https://msdn.microsoft.com/en-us/library/system.net.security.remotecertificatevalidationcallback(v=vs.110).aspx"/>.
/// </para>
/// </remarks>
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
public NpgsqlDataSourceBuilder UseUserCertificateValidationCallback(
RemoteCertificateValidationCallback userCertificateValidationCallback)
{
_userCertificateValidationCallback = userCertificateValidationCallback;

return this;
}

/// <summary>
/// Specifies an SSL/TLS certificate which Npgsql will send to PostgreSQL for certificate-based authentication.
/// </summary>
/// <param name="clientCertificate">The client certificate to be sent to PostgreSQL when opening a connection.</param>
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
public NpgsqlDataSourceBuilder UseClientCertificate(X509Certificate? clientCertificate)
{
if (clientCertificate is null)
return UseClientCertificatesCallback(null);

var clientCertificates = new X509CertificateCollection { clientCertificate };
return UseClientCertificates(clientCertificates);
}

/// <summary>
/// Specifies a collection of SSL/TLS certificates which Npgsql will send to PostgreSQL for certificate-based authentication.
/// </summary>
/// <param name="clientCertificates">The client certificate collection to be sent to PostgreSQL when opening a connection.</param>
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
public NpgsqlDataSourceBuilder UseClientCertificates(X509CertificateCollection? clientCertificates)
=> UseClientCertificatesCallback(clientCertificates is null ? null : certs => certs.AddRange(clientCertificates));

/// <summary>
/// Specifies a callback to modify the collection of SSL/TLS client certificates which Npgsql will send to PostgreSQL for
/// certificate-based authentication. This is an advanced API, consider using <see cref="UseClientCertificate" /> or
/// <see cref="UseClientCertificates" /> instead.
/// </summary>
/// <param name="clientCertificatesCallback">The callback to modify the client certificate collection.</param>
/// <remarks>
/// <para>
/// The callback is invoked every time a physical connection is opened, and is therefore suitable for rotating short-lived client
/// certificates. Simply make sure the certificate collection argument has the up-to-date certificate(s).
/// </para>
/// <para>
/// The callback's collection argument already includes any client certificates specified via the connection string or environment
/// variables.
/// </para>
/// </remarks>
/// <returns>The same builder instance so that multiple calls can be chained.</returns>
public NpgsqlDataSourceBuilder UseClientCertificatesCallback(Action<X509CertificateCollection>? clientCertificatesCallback)
{
_clientCertificatesCallback = clientCertificatesCallback;

return this;
}

/// <summary>
/// Configures a periodic password provider, which is automatically called by the data source at some regular interval. This is the
/// recommended way to fetch a rotating access token.
Expand Down Expand Up @@ -116,6 +192,8 @@ public NpgsqlDataSourceBuilder UsePeriodicPasswordProvider(
return this;
}

#endregion Authentication

#region Type mapping

/// <inheritdoc />
Expand Down Expand Up @@ -274,6 +352,8 @@ public NpgsqlDataSource Build()

var config = new NpgsqlDataSourceConfiguration(
loggingConfiguration,
_userCertificateValidationCallback,
_clientCertificatesCallback,
_periodicPasswordProvider,
_periodicPasswordSuccessRefreshInterval,
_periodicPasswordFailureRefreshInterval,
Expand Down
4 changes: 4 additions & 0 deletions src/Npgsql/NpgsqlDataSourceConfiguration.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
using Npgsql.Internal.TypeHandling;
Expand All @@ -9,6 +11,8 @@ namespace Npgsql;

sealed record NpgsqlDataSourceConfiguration(
NpgsqlLoggingConfiguration LoggingConfiguration,
RemoteCertificateValidationCallback? UserCertificateValidationCallback,
Action<X509CertificateCollection>? ClientCertificatesCallback,
Func<NpgsqlConnectionStringBuilder, CancellationToken, ValueTask<string>>? PeriodicPasswordProvider,
TimeSpan PeriodicPasswordSuccessRefreshInterval,
TimeSpan PeriodicPasswordFailureRefreshInterval,
Expand Down
4 changes: 4 additions & 0 deletions src/Npgsql/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ Npgsql.NpgsqlDataSourceBuilder.MapEnum<TEnum>(string? pgName = null, Npgsql.INpg
Npgsql.NpgsqlDataSourceBuilder.UnmapComposite(System.Type! clrType, string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> bool
Npgsql.NpgsqlDataSourceBuilder.UnmapComposite<T>(string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> bool
Npgsql.NpgsqlDataSourceBuilder.UnmapEnum<TEnum>(string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> bool
Npgsql.NpgsqlDataSourceBuilder.UseClientCertificate(System.Security.Cryptography.X509Certificates.X509Certificate? clientCertificate) -> Npgsql.NpgsqlDataSourceBuilder!
Npgsql.NpgsqlDataSourceBuilder.UseClientCertificates(System.Security.Cryptography.X509Certificates.X509CertificateCollection? clientCertificates) -> Npgsql.NpgsqlDataSourceBuilder!
Npgsql.NpgsqlDataSourceBuilder.UseClientCertificatesCallback(System.Action<System.Security.Cryptography.X509Certificates.X509CertificateCollection!>? clientCertificatesCallback) -> Npgsql.NpgsqlDataSourceBuilder!
Npgsql.NpgsqlDataSourceBuilder.UsePhysicalConnectionInitializer(System.Action<Npgsql.NpgsqlConnection!>? connectionInitializer, System.Func<Npgsql.NpgsqlConnection!, System.Threading.Tasks.Task!>? connectionInitializerAsync) -> Npgsql.NpgsqlDataSourceBuilder!
Npgsql.NpgsqlDataSourceBuilder.UseUserCertificateValidationCallback(System.Net.Security.RemoteCertificateValidationCallback! userCertificateValidationCallback) -> Npgsql.NpgsqlDataSourceBuilder!
Npgsql.NpgsqlLoggingConfiguration
Npgsql.Schema.NpgsqlDbColumn.IsIdentity.get -> bool?
Npgsql.Schema.NpgsqlDbColumn.IsIdentity.set -> void
Expand Down
27 changes: 27 additions & 0 deletions test/Npgsql.Tests/ConnectionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.Net;
using System.Net.Security;
using System.Runtime.InteropServices;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -1054,6 +1055,32 @@ public async Task CloneWith_and_data_source_with_password()
await clonedConnection.OpenAsync();
}

[Test]
public async Task CloneWith_and_data_source_with_auth_callbacks()
{
var (userCertificateValidationCallbackCalled, clientCertificatesCallbackCalled) = (false, false);

var dataSourceBuilder = CreateDataSourceBuilder();
dataSourceBuilder.UseUserCertificateValidationCallback(UserCertificateValidationCallback);
dataSourceBuilder.UseClientCertificatesCallback(ClientCertificatesCallback);
await using var dataSource = dataSourceBuilder.Build();
await using var connection = dataSource.CreateConnection();

using var _ = CreateTempPool(ConnectionString, out var tempConnectionString);
await using var clonedConnection = connection.CloneWith(tempConnectionString);

clonedConnection.UserCertificateValidationCallback!(null!, null, null, SslPolicyErrors.None);
Assert.True(userCertificateValidationCallbackCalled);
clonedConnection.ProvideClientCertificatesCallback!(null!);
Assert.True(clientCertificatesCallbackCalled);

bool UserCertificateValidationCallback(object sender, X509Certificate? certificate, X509Chain? chain, SslPolicyErrors errors)
=> userCertificateValidationCallbackCalled = true;

void ClientCertificatesCallback(X509CertificateCollection certs)
=> clientCertificatesCallbackCalled = true;
}

#endregion PersistSecurityInfo

[Test]
Expand Down
Loading