Skip to content

Commit dda36e6

Browse files
authored
Make encryption opt-in on NpgsqlSlimDataSourceBuilder (#4976)
Closes #4966
1 parent 57f09d0 commit dda36e6

13 files changed

+261
-269
lines changed

src/Npgsql/Internal/NpgsqlConnector.cs

Lines changed: 123 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ internal bool PostgresCancellationPerformed
283283
internal bool AttemptPostgresCancellation { get; private set; }
284284
static readonly TimeSpan _cancelImmediatelyTimeout = TimeSpan.FromMilliseconds(-1);
285285

286-
X509Certificate2? _certificate;
286+
IDisposable? _certificate;
287287

288288
internal NpgsqlLoggingConfiguration LoggingConfiguration { get; }
289289

@@ -786,8 +786,12 @@ async Task RawOpen(SslMode sslMode, NpgsqlTimeout timeout, bool async, Cancellat
786786

787787
IsSecure = false;
788788

789-
if (sslMode is SslMode.Prefer or SslMode.Require or SslMode.VerifyCA or SslMode.VerifyFull)
789+
if ((sslMode is SslMode.Prefer && DataSource.EncryptionNegotiator is not null) ||
790+
sslMode is SslMode.Require or SslMode.VerifyCA or SslMode.VerifyFull)
790791
{
792+
if (DataSource.EncryptionNegotiator is null)
793+
throw new InvalidOperationException(NpgsqlStrings.EncryptionDisabled);
794+
791795
WriteSslRequest();
792796
await Flush(async, cancellationToken);
793797

@@ -804,136 +808,152 @@ async Task RawOpen(SslMode sslMode, NpgsqlTimeout timeout, bool async, Cancellat
804808
throw new NpgsqlException("SSL connection requested. No SSL enabled connection from this host is configured.");
805809
break;
806810
case 'S':
807-
var clientCertificates = new X509Certificate2Collection();
808-
var certPath = Settings.SslCertificate ?? PostgresEnvironment.SslCert ?? PostgresEnvironment.SslCertDefault;
811+
await DataSource.EncryptionNegotiator(this, sslMode, timeout, async, isFirstAttempt);
812+
break;
813+
}
809814

810-
if (certPath != null)
811-
{
812-
var password = Settings.SslPassword;
815+
if (ReadBuffer.ReadBytesLeft > 0)
816+
throw new NpgsqlException("Additional unencrypted data received after SSL negotiation - this should never happen, and may be an indication of a man-in-the-middle attack.");
817+
}
813818

814-
if (Path.GetExtension(certPath).ToUpperInvariant() != ".PFX")
815-
{
819+
ConnectionLogger.LogTrace("Socket connected to {Host}:{Port}", Host, Port);
820+
}
821+
catch
822+
{
823+
_stream?.Dispose();
824+
_stream = null!;
825+
826+
_baseStream?.Dispose();
827+
_baseStream = null!;
828+
829+
_socket?.Dispose();
830+
_socket = null!;
831+
832+
throw;
833+
}
834+
}
835+
836+
internal async Task NegotiateEncryption(SslMode sslMode, NpgsqlTimeout timeout, bool async, bool isFirstAttempt)
837+
{
838+
var clientCertificates = new X509Certificate2Collection();
839+
var certPath = Settings.SslCertificate ?? PostgresEnvironment.SslCert ?? PostgresEnvironment.SslCertDefault;
840+
841+
if (certPath != null)
842+
{
843+
var password = Settings.SslPassword;
844+
845+
X509Certificate2? cert = null;
846+
if (Path.GetExtension(certPath).ToUpperInvariant() != ".PFX")
847+
{
816848
#if NET5_0_OR_GREATER
817-
// It's PEM time
818-
var keyPath = Settings.SslKey ?? PostgresEnvironment.SslKey ?? PostgresEnvironment.SslKeyDefault;
819-
_certificate = string.IsNullOrEmpty(password)
820-
? X509Certificate2.CreateFromPemFile(certPath, keyPath)
821-
: X509Certificate2.CreateFromEncryptedPemFile(certPath, password, keyPath);
822-
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
823-
{
824-
// Windows crypto API has a bug with pem certs
825-
// See #3650
826-
using var previousCert = _certificate;
827-
_certificate = new X509Certificate2(_certificate.Export(X509ContentType.Pkcs12));
828-
}
849+
// It's PEM time
850+
var keyPath = Settings.SslKey ?? PostgresEnvironment.SslKey ?? PostgresEnvironment.SslKeyDefault;
851+
cert = string.IsNullOrEmpty(password)
852+
? X509Certificate2.CreateFromPemFile(certPath, keyPath)
853+
: X509Certificate2.CreateFromEncryptedPemFile(certPath, password, keyPath);
854+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
855+
{
856+
// Windows crypto API has a bug with pem certs
857+
// See #3650
858+
using var previousCert = cert;
859+
cert = new X509Certificate2(cert.Export(X509ContentType.Pkcs12));
860+
}
861+
829862
#else
830-
// Technically PEM certificates are supported as of .NET 5 but we don't build for the net5.0
831-
// TFM anymore since .NET 5 is out of support
832-
// This is a breaking change for .NET 5 as of Npgsql 8!
833-
throw new NotSupportedException("PEM certificates are only supported with .NET 6 and higher");
863+
// Technically PEM certificates are supported as of .NET 5 but we don't build for the net5.0
864+
// TFM anymore since .NET 5 is out of support
865+
// This is a breaking change for .NET 5 as of Npgsql 8!
866+
throw new NotSupportedException("PEM certificates are only supported with .NET 6 and higher");
834867
#endif
835-
}
868+
}
836869

837-
_certificate ??= new X509Certificate2(certPath, password);
838-
clientCertificates.Add(_certificate);
839-
}
870+
cert ??= new X509Certificate2(certPath, password);
871+
clientCertificates.Add(cert);
840872

841-
ClientCertificatesCallback?.Invoke(clientCertificates);
873+
_certificate = cert;
874+
}
842875

843-
var checkCertificateRevocation = Settings.CheckCertificateRevocation;
876+
try
877+
{
878+
ClientCertificatesCallback?.Invoke(clientCertificates);
844879

845-
RemoteCertificateValidationCallback? certificateValidationCallback;
846-
X509Certificate2? caCert;
847-
string? certRootPath = null;
880+
var checkCertificateRevocation = Settings.CheckCertificateRevocation;
848881

849-
if (UserCertificateValidationCallback is not null)
850-
{
851-
if (sslMode is SslMode.VerifyCA or SslMode.VerifyFull)
852-
throw new ArgumentException(string.Format(NpgsqlStrings.CannotUseSslVerifyWithUserCallback, sslMode));
882+
RemoteCertificateValidationCallback? certificateValidationCallback;
883+
X509Certificate2? caCert;
884+
string? certRootPath = null;
853885

854-
if (Settings.RootCertificate is not null)
855-
throw new ArgumentException(NpgsqlStrings.CannotUseSslRootCertificateWithUserCallback);
886+
if (UserCertificateValidationCallback is not null)
887+
{
888+
if (sslMode is SslMode.VerifyCA or SslMode.VerifyFull)
889+
throw new ArgumentException(string.Format(NpgsqlStrings.CannotUseSslVerifyWithUserCallback, sslMode));
856890

857-
if (DataSource.RootCertificateCallback is not null)
858-
throw new ArgumentException(NpgsqlStrings.CannotUseValidationRootCertificateCallbackWithUserCallback);
891+
if (Settings.RootCertificate is not null)
892+
throw new ArgumentException(NpgsqlStrings.CannotUseSslRootCertificateWithUserCallback);
859893

860-
certificateValidationCallback = UserCertificateValidationCallback;
861-
}
862-
else if (sslMode is SslMode.Prefer or SslMode.Require)
863-
{
864-
if (isFirstAttempt && sslMode is SslMode.Require && !Settings.TrustServerCertificate)
865-
throw new ArgumentException(NpgsqlStrings.CannotUseSslModeRequireWithoutTrustServerCertificate);
894+
if (DataSource.RootCertificateCallback is not null)
895+
throw new ArgumentException(NpgsqlStrings.CannotUseValidationRootCertificateCallbackWithUserCallback);
866896

867-
certificateValidationCallback = SslTrustServerValidation;
868-
checkCertificateRevocation = false;
869-
}
870-
else if ((caCert = DataSource.RootCertificateCallback?.Invoke()) is not null ||
871-
(certRootPath = Settings.RootCertificate ??
872-
PostgresEnvironment.SslCertRoot ?? PostgresEnvironment.SslCertRootDefault) is not null)
873-
{
874-
certificateValidationCallback = SslRootValidation(sslMode == SslMode.VerifyFull, certRootPath, caCert);
875-
}
876-
else if (sslMode == SslMode.VerifyCA)
877-
{
878-
certificateValidationCallback = SslVerifyCAValidation;
879-
}
880-
else
881-
{
882-
Debug.Assert(sslMode == SslMode.VerifyFull);
883-
certificateValidationCallback = SslVerifyFullValidation;
884-
}
897+
certificateValidationCallback = UserCertificateValidationCallback;
898+
}
899+
else if (sslMode is SslMode.Prefer or SslMode.Require)
900+
{
901+
if (isFirstAttempt && sslMode is SslMode.Require && !Settings.TrustServerCertificate)
902+
throw new ArgumentException(NpgsqlStrings.CannotUseSslModeRequireWithoutTrustServerCertificate);
903+
904+
certificateValidationCallback = SslTrustServerValidation;
905+
checkCertificateRevocation = false;
906+
}
907+
else if ((caCert = DataSource.RootCertificateCallback?.Invoke()) is not null ||
908+
(certRootPath = Settings.RootCertificate ??
909+
PostgresEnvironment.SslCertRoot ?? PostgresEnvironment.SslCertRootDefault) is not null)
910+
{
911+
certificateValidationCallback = SslRootValidation(sslMode == SslMode.VerifyFull, certRootPath, caCert);
912+
}
913+
else if (sslMode == SslMode.VerifyCA)
914+
{
915+
certificateValidationCallback = SslVerifyCAValidation;
916+
}
917+
else
918+
{
919+
Debug.Assert(sslMode == SslMode.VerifyFull);
920+
certificateValidationCallback = SslVerifyFullValidation;
921+
}
885922

886-
timeout.CheckAndApply(this);
923+
timeout.CheckAndApply(this);
887924

888-
try
889-
{
890-
var sslStream = new SslStream(_stream, leaveInnerStreamOpen: false, certificateValidationCallback);
925+
try
926+
{
927+
var sslStream = new SslStream(_stream, leaveInnerStreamOpen: false, certificateValidationCallback);
891928

892-
var sslProtocols = SslProtocols.None;
929+
var sslProtocols = SslProtocols.None;
893930
#if NETSTANDARD2_0
894-
// On .NET Framework SslProtocols.None can be disabled, see #3718
895-
sslProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12;
931+
// On .NET Framework SslProtocols.None can be disabled, see #3718
932+
sslProtocols = SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12;
896933
#endif
897934

898-
if (async)
899-
await sslStream.AuthenticateAsClientAsync(Host, clientCertificates, sslProtocols, checkCertificateRevocation);
900-
else
901-
sslStream.AuthenticateAsClient(Host, clientCertificates, sslProtocols, checkCertificateRevocation);
902-
903-
_stream = sslStream;
904-
}
905-
catch (Exception e)
906-
{
907-
throw new NpgsqlException("Exception while performing SSL handshake", e);
908-
}
909-
910-
ReadBuffer.Underlying = _stream;
911-
WriteBuffer.Underlying = _stream;
912-
IsSecure = true;
913-
ConnectionLogger.LogTrace("SSL negotiation successful");
914-
break;
915-
}
935+
if (async)
936+
await sslStream.AuthenticateAsClientAsync(Host, clientCertificates, sslProtocols, checkCertificateRevocation);
937+
else
938+
sslStream.AuthenticateAsClient(Host, clientCertificates, sslProtocols, checkCertificateRevocation);
916939

917-
if (ReadBuffer.ReadBytesLeft > 0)
918-
throw new NpgsqlException("Additional unencrypted data received after SSL negotiation - this should never happen, and may be an indication of a man-in-the-middle attack.");
940+
_stream = sslStream;
941+
}
942+
catch (Exception e)
943+
{
944+
throw new NpgsqlException("Exception while performing SSL handshake", e);
919945
}
920946

921-
ConnectionLogger.LogTrace("Socket connected to {Host}:{Port}", Host, Port);
947+
ReadBuffer.Underlying = _stream;
948+
WriteBuffer.Underlying = _stream;
949+
IsSecure = true;
950+
ConnectionLogger.LogTrace("SSL negotiation successful");
922951
}
923952
catch
924953
{
925954
_certificate?.Dispose();
926955
_certificate = null;
927956

928-
_stream?.Dispose();
929-
_stream = null!;
930-
931-
_baseStream?.Dispose();
932-
_baseStream = null!;
933-
934-
_socket?.Dispose();
935-
_socket = null!;
936-
937957
throw;
938958
}
939959
}

src/Npgsql/Internal/TypeMapping/TypeMapper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,7 @@ internal NpgsqlTypeHandler ResolveByDataTypeName(string typeName)
305305
case PostgresMultirangeType:
306306
return throwOnError
307307
? throw new NotSupportedException(
308-
$"'{pgType}' is a range type; please call {nameof(NpgsqlRangeExtensions.UseRange)} on {nameof(NpgsqlDataSourceBuilder)} or on {nameof(NpgsqlConnection)}.{nameof(NpgsqlConnection.GlobalTypeMapper)} to enable ranges. " +
308+
$"'{pgType}' is a range type; please call {nameof(NpgsqlSlimDataSourceBuilder.EnableRanges)} on {nameof(NpgsqlSlimDataSourceBuilder)} to enable ranges. " +
309309
"See https://www.npgsql.org/doc/types/ranges.html for more information.")
310310
: null;
311311
#pragma warning restore CS0618

src/Npgsql/NpgsqlDataSource.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ public abstract class NpgsqlDataSource : DbDataSource
4343
/// </summary>
4444
internal NpgsqlDatabaseInfo DatabaseInfo { get; set; } = null!; // Initialized at bootstrapping
4545

46+
internal Func<NpgsqlConnector, SslMode, NpgsqlTimeout, bool, bool, Task>? EncryptionNegotiator { get; }
4647
internal RemoteCertificateValidationCallback? UserCertificateValidationCallback { get; }
4748
internal Action<X509CertificateCollection>? ClientCertificatesCallback { get; }
4849

@@ -89,6 +90,7 @@ internal NpgsqlDataSource(
8990
Configuration = dataSourceConfig;
9091

9192
(LoggingConfiguration,
93+
EncryptionNegotiator,
9294
UserCertificateValidationCallback,
9395
ClientCertificatesCallback,
9496
_periodicPasswordProvider,

src/Npgsql/NpgsqlDataSourceBuilder.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
using System.Diagnostics.CodeAnalysis;
33
using System.Net.Security;
44
using System.Security.Cryptography.X509Certificates;
5+
using System.Text.Json;
56
using System.Threading;
67
using System.Threading.Tasks;
78
using Microsoft.Extensions.Logging;
89
using Npgsql.Internal.TypeHandling;
910
using Npgsql.TypeMapping;
11+
using NpgsqlTypes;
1012

1113
namespace Npgsql;
1214

@@ -200,6 +202,25 @@ public NpgsqlDataSourceBuilder UsePeriodicPasswordProvider(
200202
public void AddTypeResolverFactory(TypeHandlerResolverFactory resolverFactory)
201203
=> _internalBuilder.AddTypeResolverFactory(resolverFactory);
202204

205+
/// <summary>
206+
/// Sets up System.Text.Json mappings for the PostgreSQL <c>json</c> and <c>jsonb</c> types.
207+
/// </summary>
208+
/// <param name="serializerOptions">Options to customize JSON serialization and deserialization.</param>
209+
/// <param name="jsonbClrTypes">
210+
/// A list of CLR types to map to PostgreSQL <c>jsonb</c> (no need to specify <see cref="NpgsqlDbType.Jsonb" />).
211+
/// </param>
212+
/// <param name="jsonClrTypes">
213+
/// A list of CLR types to map to PostgreSQL <c>json</c> (no need to specify <see cref="NpgsqlDbType.Json" />).
214+
/// </param>
215+
public NpgsqlDataSourceBuilder UseSystemTextJson(
216+
JsonSerializerOptions? serializerOptions = null,
217+
Type[]? jsonbClrTypes = null,
218+
Type[]? jsonClrTypes = null)
219+
{
220+
AddTypeResolverFactory(new JsonTypeHandlerResolverFactory(jsonbClrTypes, jsonClrTypes, serializerOptions));
221+
return this;
222+
}
223+
203224
/// <inheritdoc />
204225
public INpgsqlTypeMapper MapEnum<TEnum>(string? pgName = null, INpgsqlNameTranslator? nameTranslator = null)
205226
where TEnum : struct, Enum
@@ -291,6 +312,7 @@ public NpgsqlMultiHostDataSource BuildMultiHost()
291312

292313
void AddDefaultFeatures()
293314
{
315+
_internalBuilder.EnableEncryption();
294316
_internalBuilder.AddDefaultTypeResolverFactory(new JsonTypeHandlerResolverFactory());
295317
_internalBuilder.AddDefaultTypeResolverFactory(new RangeTypeHandlerResolverFactory());
296318
}

src/Npgsql/NpgsqlDataSourceConfiguration.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@
44
using System.Security.Cryptography.X509Certificates;
55
using System.Threading;
66
using System.Threading.Tasks;
7+
using Npgsql.Internal;
78
using Npgsql.Internal.TypeHandling;
89
using Npgsql.Internal.TypeMapping;
10+
using Npgsql.Util;
911

1012
namespace Npgsql;
1113

1214
sealed record NpgsqlDataSourceConfiguration(
1315
NpgsqlLoggingConfiguration LoggingConfiguration,
16+
Func<NpgsqlConnector, SslMode, NpgsqlTimeout, bool, bool, Task>? EncryptionNegotiator,
1417
RemoteCertificateValidationCallback? UserCertificateValidationCallback,
1518
Action<X509CertificateCollection>? ClientCertificatesCallback,
1619
Func<NpgsqlConnectionStringBuilder, CancellationToken, ValueTask<string>>? PeriodicPasswordProvider,

0 commit comments

Comments
 (0)