Skip to content

Commit d9c1b08

Browse files
authored
Implement GSSAPI session encryption (#6131)
Closes #2957
1 parent fe7f775 commit d9c1b08

12 files changed

+522
-34
lines changed

src/Npgsql/Internal/IntegratedSecurityHandler.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ class IntegratedSecurityHandler
1616
return new();
1717
}
1818

19-
public virtual ValueTask NegotiateAuthentication(bool async, NpgsqlConnector connector)
19+
public virtual ValueTask NegotiateAuthentication(bool async, NpgsqlConnector connector, CancellationToken cancellationToken)
20+
=> throw new NotSupportedException(string.Format(NpgsqlStrings.IntegratedSecurityDisabled, nameof(NpgsqlSlimDataSourceBuilder.EnableIntegratedSecurity)));
21+
22+
public virtual ValueTask<GssEncryptionResult> GSSEncrypt(bool async, bool isRequired, NpgsqlConnector connector, CancellationToken cancellationToken)
2023
=> throw new NotSupportedException(string.Format(NpgsqlStrings.IntegratedSecurityDisabled, nameof(NpgsqlSlimDataSourceBuilder.EnableIntegratedSecurity)));
2124
}
2225

@@ -27,6 +30,9 @@ sealed class RealIntegratedSecurityHandler : IntegratedSecurityHandler
2730
public override ValueTask<string?> GetUsername(bool async, bool includeRealm, ILogger connectionLogger, CancellationToken cancellationToken)
2831
=> KerberosUsernameProvider.GetUsername(async, includeRealm, connectionLogger, cancellationToken);
2932

30-
public override ValueTask NegotiateAuthentication(bool async, NpgsqlConnector connector)
31-
=> new(connector.AuthenticateGSS(async));
33+
public override ValueTask NegotiateAuthentication(bool async, NpgsqlConnector connector, CancellationToken cancellationToken)
34+
=> connector.AuthenticateGSS(async, cancellationToken);
35+
36+
public override ValueTask<GssEncryptionResult> GSSEncrypt(bool async, bool isRequired, NpgsqlConnector connector, CancellationToken cancellationToken)
37+
=> connector.GSSEncrypt(async, isRequired, cancellationToken);
3238
}

src/Npgsql/Internal/NpgsqlConnector.Auth.cs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,13 @@ async Task Authenticate(string username, NpgsqlTimeout timeout, bool async, Canc
3333
case AuthenticationRequestType.Ok:
3434
// If we didn't complete authentication, check whether it's allowed
3535
if (!authenticated)
36+
{
37+
// User requested GSS authentication, but server said that no auth is required
38+
// If and only if our connection is gss encrypted, we consider us already authenticated
39+
if (requiredAuthModes.HasFlag(RequireAuthMode.GSS) && IsGssEncrypted)
40+
return;
3641
ThrowIfNotAllowed(requiredAuthModes, RequireAuthMode.None);
42+
}
3743
return;
3844

3945
case AuthenticationRequestType.CleartextPassword:
@@ -55,7 +61,7 @@ await AuthenticateSASL(((AuthenticationSASLMessage)msg).Mechanisms, username, as
5561
case AuthenticationRequestType.GSS:
5662
case AuthenticationRequestType.SSPI:
5763
ThrowIfNotAllowed(requiredAuthModes, msg.AuthRequestType == AuthenticationRequestType.GSS ? RequireAuthMode.GSS : RequireAuthMode.SSPI);
58-
await DataSource.IntegratedSecurityHandler.NegotiateAuthentication(async, this).ConfigureAwait(false);
64+
await DataSource.IntegratedSecurityHandler.NegotiateAuthentication(async, this, cancellationToken).ConfigureAwait(false);
5965
return;
6066

6167
case AuthenticationRequestType.GSSContinue:
@@ -207,7 +213,7 @@ internal void AuthenticateSASLSha256Plus(ref string mechanism, ref string cbindF
207213
// try authenticate without channel binding even though both
208214
// the client and server supported it. The SCRAM exchange
209215
// checks for that, to prevent downgrade attacks.
210-
if (!IsSecure)
216+
if (!IsSslEncrypted)
211217
throw new NpgsqlException("Server offered SCRAM-SHA-256-PLUS authentication over a non-SSL connection");
212218

213219
var sslStream = (SslStream)_stream;
@@ -321,7 +327,7 @@ async Task AuthenticateMD5(string username, byte[] salt, bool async, Cancellatio
321327
await Flush(async, cancellationToken).ConfigureAwait(false);
322328
}
323329

324-
internal async Task AuthenticateGSS(bool async)
330+
internal async ValueTask AuthenticateGSS(bool async, CancellationToken cancellationToken)
325331
{
326332
var targetName = $"{KerberosServiceName}/{Host}";
327333

@@ -331,24 +337,24 @@ internal async Task AuthenticateGSS(bool async)
331337
using var authContext = new NegotiateAuthentication(clientOptions);
332338
var data = authContext.GetOutgoingBlob(ReadOnlySpan<byte>.Empty, out var statusCode)!;
333339
Debug.Assert(statusCode == NegotiateAuthenticationStatusCode.ContinueNeeded);
334-
await WritePassword(data, 0, data.Length, async, UserCancellationToken).ConfigureAwait(false);
335-
await Flush(async, UserCancellationToken).ConfigureAwait(false);
340+
await WritePassword(data, 0, data.Length, async, cancellationToken).ConfigureAwait(false);
341+
await Flush(async, cancellationToken).ConfigureAwait(false);
336342
while (true)
337343
{
338344
var response = ExpectAny<AuthenticationRequestMessage>(await ReadMessage(async).ConfigureAwait(false), this);
339345
if (response.AuthRequestType == AuthenticationRequestType.Ok)
340346
break;
341347
if (response is not AuthenticationGSSContinueMessage gssMsg)
342348
throw new NpgsqlException($"Received unexpected authentication request message {response.AuthRequestType}");
343-
data = authContext.GetOutgoingBlob(gssMsg.AuthenticationData.AsSpan(), out statusCode)!;
349+
data = authContext.GetOutgoingBlob(gssMsg.AuthenticationData.AsSpan(), out statusCode);
344350
if (statusCode is not NegotiateAuthenticationStatusCode.Completed and not NegotiateAuthenticationStatusCode.ContinueNeeded)
345351
throw new NpgsqlException($"Error while authenticating GSS/SSPI: {statusCode}");
346352
// We might get NegotiateAuthenticationStatusCode.Completed but the data will not be null
347353
// This can happen if it's the first cycle, in which case we have to send that data to complete handshake (#4888)
348354
if (data is null)
349355
continue;
350-
await WritePassword(data, 0, data.Length, async, UserCancellationToken).ConfigureAwait(false);
351-
await Flush(async, UserCancellationToken).ConfigureAwait(false);
356+
await WritePassword(data, 0, data.Length, async, cancellationToken).ConfigureAwait(false);
357+
await Flush(async, cancellationToken).ConfigureAwait(false);
352358
}
353359
}
354360

src/Npgsql/Internal/NpgsqlConnector.FrontendMessages.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,19 @@ internal void WriteSslRequest()
396396
WriteBuffer.WriteInt32(80877103);
397397
}
398398

399+
internal void WriteGSSEncryptRequest()
400+
{
401+
const int len = sizeof(int) + // Length
402+
sizeof(int); // GSSEnc request code
403+
404+
WriteBuffer.StartMessage(len);
405+
if (WriteBuffer.WriteSpaceLeft < len)
406+
Flush(false).GetAwaiter().GetResult();
407+
408+
WriteBuffer.WriteInt32(len);
409+
WriteBuffer.WriteInt32(80877104);
410+
}
411+
399412
internal void WriteStartup(Dictionary<string, string> parameters)
400413
{
401414
const int protocolVersion3 = 3 << 16; // 196608

0 commit comments

Comments
 (0)