async Task AuthenticateSASL(List <string> mechanisms, string username, bool async, CancellationToken cancellationToken = default) { // At the time of writing PostgreSQL only supports SCRAM-SHA-256 and SCRAM-SHA-256-PLUS var supportsSha256 = mechanisms.Contains("SCRAM-SHA-256"); var supportsSha256Plus = mechanisms.Contains("SCRAM-SHA-256-PLUS"); if (!supportsSha256 && !supportsSha256Plus) { throw new NpgsqlException("No supported SASL mechanism found (only SCRAM-SHA-256 and SCRAM-SHA-256-PLUS are supported for now). " + "Mechanisms received from server: " + string.Join(", ", mechanisms)); } var mechanism = string.Empty; var cbindFlag = string.Empty; var cbind = string.Empty; var successfulBind = false; if (supportsSha256Plus) { var sslStream = (SslStream)_stream; if (sslStream.RemoteCertificate is null) { Log.Warn("Remote certificate null, falling back to SCRAM-SHA-256"); } else { using var remoteCertificate = new X509Certificate2(sslStream.RemoteCertificate); // Checking for hashing algorithms HashAlgorithm?hashAlgorithm = null; var algorithmName = remoteCertificate.SignatureAlgorithm.FriendlyName; if (algorithmName is null) { Log.Warn("Signature algorithm was null, falling back to SCRAM-SHA-256"); } else if (algorithmName.StartsWith("sha1", StringComparison.OrdinalIgnoreCase) || algorithmName.StartsWith("md5", StringComparison.OrdinalIgnoreCase) || algorithmName.StartsWith("sha256", StringComparison.OrdinalIgnoreCase)) { hashAlgorithm = SHA256.Create(); } else if (algorithmName.StartsWith("sha384", StringComparison.OrdinalIgnoreCase)) { hashAlgorithm = SHA384.Create(); } else if (algorithmName.StartsWith("sha512", StringComparison.OrdinalIgnoreCase)) { hashAlgorithm = SHA512.Create(); } else { Log.Warn($"Support for signature algorithm {algorithmName} is not yet implemented, falling back to SCRAM-SHA-256"); } if (hashAlgorithm != null) { using var _ = hashAlgorithm; // RFC 5929 mechanism = "SCRAM-SHA-256-PLUS"; // PostgreSQL only supports tls-server-end-point binding cbindFlag = "p=tls-server-end-point"; // SCRAM-SHA-256-PLUS depends on using ssl stream, so it's fine var cbindFlagBytes = Encoding.UTF8.GetBytes($"{cbindFlag},,"); var certificateHash = hashAlgorithm.ComputeHash(remoteCertificate.GetRawCertData()); var cbindBytes = cbindFlagBytes.Concat(certificateHash).ToArray(); cbind = Convert.ToBase64String(cbindBytes); successfulBind = true; IsScramPlus = true; } } } if (!successfulBind && supportsSha256) { mechanism = "SCRAM-SHA-256"; // We can get here if PostgreSQL supports only SCRAM-SHA-256 or there was an error while binding to SCRAM-SHA-256-PLUS // So, we set 'n' (client does not support binding) if there was an error while binding // or 'y' (client supports but server doesn't) in other case cbindFlag = supportsSha256Plus ? "n" : "y"; cbind = supportsSha256Plus ? "biws" : "eSws"; successfulBind = true; IsScram = true; } if (!successfulBind) { // We can get here if PostgreSQL supports only SCRAM-SHA-256-PLUS but there was an error while binding to it throw new NpgsqlException("Unable to bind to SCRAM-SHA-256-PLUS, check logs for more information"); } var passwd = GetPassword(username) ?? throw new NpgsqlException($"No password has been provided but the backend requires one (in SASL/{mechanism})"); // Assumption: the write buffer is big enough to contain all our outgoing messages var clientNonce = GetNonce(); await WriteSASLInitialResponse(mechanism, PGUtil.UTF8Encoding.GetBytes($"{cbindFlag},,n=*,r={clientNonce}"), async, cancellationToken); await Flush(async, cancellationToken); var saslContinueMsg = Expect <AuthenticationSASLContinueMessage>(await ReadMessage(async), this); if (saslContinueMsg.AuthRequestType != AuthenticationRequestType.AuthenticationSASLContinue) { throw new NpgsqlException("[SASL] AuthenticationSASLContinue message expected"); } var firstServerMsg = AuthenticationSCRAMServerFirstMessage.Load(saslContinueMsg.Payload); if (!firstServerMsg.Nonce.StartsWith(clientNonce)) { throw new NpgsqlException("[SCRAM] Malformed SCRAMServerFirst message: server nonce doesn't start with client nonce"); } var saltBytes = Convert.FromBase64String(firstServerMsg.Salt); var saltedPassword = Hi(passwd.Normalize(NormalizationForm.FormKC), saltBytes, firstServerMsg.Iteration); var clientKey = HMAC(saltedPassword, "Client Key"); byte[] storedKey; using (var sha256 = SHA256.Create()) storedKey = sha256.ComputeHash(clientKey); var clientFirstMessageBare = $"n=*,r={clientNonce}"; var serverFirstMessage = $"r={firstServerMsg.Nonce},s={firstServerMsg.Salt},i={firstServerMsg.Iteration}"; var clientFinalMessageWithoutProof = $"c={cbind},r={firstServerMsg.Nonce}"; var authMessage = $"{clientFirstMessageBare},{serverFirstMessage},{clientFinalMessageWithoutProof}"; var clientSignature = HMAC(storedKey, authMessage); var clientProofBytes = Xor(clientKey, clientSignature); var clientProof = Convert.ToBase64String(clientProofBytes); var serverKey = HMAC(saltedPassword, "Server Key"); var serverSignature = HMAC(serverKey, authMessage); var messageStr = $"{clientFinalMessageWithoutProof},p={clientProof}"; await WriteSASLResponse(Encoding.UTF8.GetBytes(messageStr), async, cancellationToken); await Flush(async, cancellationToken); var saslFinalServerMsg = Expect <AuthenticationSASLFinalMessage>(await ReadMessage(async), this); if (saslFinalServerMsg.AuthRequestType != AuthenticationRequestType.AuthenticationSASLFinal) { throw new NpgsqlException("[SASL] AuthenticationSASLFinal message expected"); } var scramFinalServerMsg = AuthenticationSCRAMServerFinalMessage.Load(saslFinalServerMsg.Payload); if (scramFinalServerMsg.ServerSignature != Convert.ToBase64String(serverSignature)) { throw new NpgsqlException("[SCRAM] Unable to verify server signature"); } var okMsg = Expect <AuthenticationRequestMessage>(await ReadMessage(async), this); if (okMsg.AuthRequestType != AuthenticationRequestType.AuthenticationOk) { throw new NpgsqlException("[SASL] Expected AuthenticationOK message"); }
async Task AuthenticateSASL(List <string> mechanisms, string username, bool async) { // At the time of writing PostgreSQL only supports SCRAM-SHA-256 if (!mechanisms.Contains("SCRAM-SHA-256")) { throw new NpgsqlException("No supported SASL mechanism found (only SCRAM-SHA-256 is supported for now). " + "Mechanisms received from server: " + string.Join(", ", mechanisms)); } var mechanism = "SCRAM-SHA-256"; var passwd = GetPassword(username) ?? throw new NpgsqlException($"No password has been provided but the backend requires one (in SASL/{mechanism})"); // Assumption: the write buffer is big enough to contain all our outgoing messages var clientNonce = GetNonce(); await WriteSASLInitialResponse(mechanism, PGUtil.UTF8Encoding.GetBytes("n,,n=*,r=" + clientNonce), async); await Flush(async); var saslContinueMsg = Expect <AuthenticationSASLContinueMessage>(await ReadMessage(async), this); if (saslContinueMsg.AuthRequestType != AuthenticationRequestType.AuthenticationSASLContinue) { throw new NpgsqlException("[SASL] AuthenticationSASLFinal message expected"); } var firstServerMsg = AuthenticationSCRAMServerFirstMessage.Load(saslContinueMsg.Payload); if (!firstServerMsg.Nonce.StartsWith(clientNonce)) { throw new InvalidOperationException("[SCRAM] Malformed SCRAMServerFirst message: server nonce doesn't start with client nonce"); } var saltBytes = Convert.FromBase64String(firstServerMsg.Salt); var saltedPassword = Hi(passwd.Normalize(NormalizationForm.FormKC), saltBytes, firstServerMsg.Iteration); var clientKey = HMAC(saltedPassword, "Client Key"); byte[] storedKey; using (var sha256 = SHA256.Create()) storedKey = sha256.ComputeHash(clientKey); var clientFirstMessageBare = "n=*,r=" + clientNonce; var serverFirstMessage = $"r={firstServerMsg.Nonce},s={firstServerMsg.Salt},i={firstServerMsg.Iteration}"; var clientFinalMessageWithoutProof = "c=biws,r=" + firstServerMsg.Nonce; var authMessage = $"{clientFirstMessageBare},{serverFirstMessage},{clientFinalMessageWithoutProof}"; var clientSignature = HMAC(storedKey, authMessage); var clientProofBytes = Xor(clientKey, clientSignature); var clientProof = Convert.ToBase64String(clientProofBytes); var serverKey = HMAC(saltedPassword, "Server Key"); var serverSignature = HMAC(serverKey, authMessage); var messageStr = $"{clientFinalMessageWithoutProof},p={clientProof}"; await WriteSASLResponse(Encoding.UTF8.GetBytes(messageStr), async); await Flush(async); var saslFinalServerMsg = Expect <AuthenticationSASLFinalMessage>(await ReadMessage(async), this); if (saslFinalServerMsg.AuthRequestType != AuthenticationRequestType.AuthenticationSASLFinal) { throw new NpgsqlException("[SASL] AuthenticationSASLFinal message expected"); } var scramFinalServerMsg = AuthenticationSCRAMServerFinalMessage.Load(saslFinalServerMsg.Payload); if (scramFinalServerMsg.ServerSignature != Convert.ToBase64String(serverSignature)) { throw new NpgsqlException("[SCRAM] Unable to verify server signature"); } var okMsg = Expect <AuthenticationRequestMessage>(await ReadMessage(async), this); if (okMsg.AuthRequestType != AuthenticationRequestType.AuthenticationOk) { throw new NpgsqlException("[SASL] Expected AuthenticationOK message"); }