示例#1
0
        async Task AuthenticateSASL(List <string> mechanisms, 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() ??
                         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 new SASLInitialResponseMessage(mechanism, PGUtil.UTF8Encoding.GetBytes("n,,n=*,r=" + clientNonce))
            .Write(WriteBuffer, async);
            await WriteBuffer.Flush(async);

            var saslContinueMsg = Expect <AuthenticationSASLContinueMessage>(await ReadMessage(async));

            if (saslContinueMsg.AuthRequestType != AuthenticationRequestType.AuthenticationSASLContinue)
            {
                throw new NpgsqlException("[SASL] AuthenticationSASLFinal message expected");
            }
            var firstServerMsg = new AuthenticationSCRAMServerFirstMessage(saslContinueMsg.Payload);

            if (!firstServerMsg.Nonce.StartsWith(clientNonce))
            {
                throw new InvalidOperationException("[SCRAM] Malformed SCRAMServerFirst message: server nonce doesn't start with client nonce");
            }

            var scramFinalClientMsg = new SCRAMClientFinalMessage(passwd, firstServerMsg.Nonce, firstServerMsg.Salt, firstServerMsg.Iteration, clientNonce);
            await scramFinalClientMsg.Write(WriteBuffer, async);

            await WriteBuffer.Flush(async);

            var saslFinalServerMsg = Expect <AuthenticationSASLFinalMessage>(await ReadMessage(async));

            if (saslFinalServerMsg.AuthRequestType != AuthenticationRequestType.AuthenticationSASLFinal)
            {
                throw new NpgsqlException("[SASL] AuthenticationSASLFinal message expected");
            }
            var scramFinalServerMsg = new AuthenticationSCRAMServerFinalMessage(saslFinalServerMsg.Payload);

            if (scramFinalServerMsg.ServerSignature != Convert.ToBase64String(scramFinalClientMsg.ServerSignature))
            {
                throw new NpgsqlException("[SCRAM] Unable to verify server signature");
            }

            var okMsg = Expect <AuthenticationRequestMessage>(await ReadMessage(async));

            if (okMsg.AuthRequestType != AuthenticationRequestType.AuthenticationOk)
            {
                throw new NpgsqlException("[SASL] Expected AuthenticationOK message");
            }

            string GetNonce()
            {
                var nonceLength = 18;
                var rncProvider = RandomNumberGenerator.Create();
                var nonceBytes  = new byte[nonceLength];

                rncProvider.GetBytes(nonceBytes);
                return(Convert.ToBase64String(nonceBytes));
            }
        }
示例#2
0
        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");
            }