mirror of
https://github.com/postgres/postgres.git
synced 2025-09-02 04:21:28 +03:00
Support SCRAM-SHA-256 authentication (RFC 5802 and 7677).
This introduces a new generic SASL authentication method, similar to the GSS and SSPI methods. The server first tells the client which SASL authentication mechanism to use, and then the mechanism-specific SASL messages are exchanged in AuthenticationSASLcontinue and PasswordMessage messages. Only SCRAM-SHA-256 is supported at the moment, but this allows adding more SASL mechanisms in the future, without changing the overall protocol. Support for channel binding, aka SCRAM-SHA-256-PLUS is left for later. The SASLPrep algorithm, for pre-processing the password, is not yet implemented. That could cause trouble, if you use a password with non-ASCII characters, and a client library that does implement SASLprep. That will hopefully be added later. Authorization identities, as specified in the SCRAM-SHA-256 specification, are ignored. SET SESSION AUTHORIZATION provides more or less the same functionality, anyway. If a user doesn't exist, perform a "mock" authentication, by constructing an authentic-looking challenge on the fly. The challenge is derived from a new system-wide random value, "mock authentication nonce", which is created at initdb, and stored in the control file. We go through these motions, in order to not give away the information on whether the user exists, to unauthenticated users. Bumps PG_CONTROL_VERSION, because of the new field in control file. Patch by Michael Paquier and Heikki Linnakangas, reviewed at different stages by Robert Haas, Stephen Frost, David Steele, Aleksander Alekseev, and many others. Discussion: https://www.postgresql.org/message-id/CAB7nPqRbR3GmFYdedCAhzukfKrgBLTLtMvENOmPrVWREsZkF8g%40mail.gmail.com Discussion: https://www.postgresql.org/message-id/CAB7nPqSMXU35g%3DW9X74HVeQp0uvgJxvYOuA4A-A3M%2B0wfEBv-w%40mail.gmail.com Discussion: https://www.postgresql.org/message-id/55192AFE.6080106@iki.fi
This commit is contained in:
@@ -15,7 +15,7 @@ include $(top_builddir)/src/Makefile.global
|
||||
# be-fsstubs is here for historical reasons, probably belongs elsewhere
|
||||
|
||||
OBJS = be-fsstubs.o be-secure.o auth.o crypt.o hba.o ifaddr.o pqcomm.o \
|
||||
pqformat.o pqmq.o pqsignal.o
|
||||
pqformat.o pqmq.o pqsignal.o auth-scram.o
|
||||
|
||||
ifeq ($(with_openssl),yes)
|
||||
OBJS += be-secure-openssl.o
|
||||
|
1032
src/backend/libpq/auth-scram.c
Normal file
1032
src/backend/libpq/auth-scram.c
Normal file
File diff suppressed because it is too large
Load Diff
@@ -30,10 +30,12 @@
|
||||
#include "libpq/crypt.h"
|
||||
#include "libpq/libpq.h"
|
||||
#include "libpq/pqformat.h"
|
||||
#include "libpq/scram.h"
|
||||
#include "miscadmin.h"
|
||||
#include "replication/walsender.h"
|
||||
#include "storage/ipc.h"
|
||||
#include "utils/backend_random.h"
|
||||
#include "utils/timestamp.h"
|
||||
|
||||
|
||||
/*----------------------------------------------------------------
|
||||
@@ -197,6 +199,12 @@ static int pg_SSPI_make_upn(char *accountname,
|
||||
static int CheckRADIUSAuth(Port *port);
|
||||
|
||||
|
||||
/*----------------------------------------------------------------
|
||||
* SASL authentication
|
||||
*----------------------------------------------------------------
|
||||
*/
|
||||
static int CheckSASLAuth(Port *port, char **logdetail);
|
||||
|
||||
/*
|
||||
* Maximum accepted size of GSS and SSPI authentication tokens.
|
||||
*
|
||||
@@ -212,6 +220,13 @@ static int CheckRADIUSAuth(Port *port);
|
||||
*/
|
||||
#define PG_MAX_AUTH_TOKEN_LENGTH 65535
|
||||
|
||||
/*
|
||||
* Maximum accepted size of SASL messages.
|
||||
*
|
||||
* The messages that the server or libpq generate are much smaller than this,
|
||||
* but have some headroom.
|
||||
*/
|
||||
#define PG_MAX_SASL_MESSAGE_LENGTH 1024
|
||||
|
||||
/*----------------------------------------------------------------
|
||||
* Global authentication functions
|
||||
@@ -275,6 +290,7 @@ auth_failed(Port *port, int status, char *logdetail)
|
||||
break;
|
||||
case uaPassword:
|
||||
case uaMD5:
|
||||
case uaSASL:
|
||||
errstr = gettext_noop("password authentication failed for user \"%s\"");
|
||||
/* We use it to indicate if a .pgpass password failed. */
|
||||
errcode_return = ERRCODE_INVALID_PASSWORD;
|
||||
@@ -542,6 +558,10 @@ ClientAuthentication(Port *port)
|
||||
status = CheckPasswordAuth(port, &logdetail);
|
||||
break;
|
||||
|
||||
case uaSASL:
|
||||
status = CheckSASLAuth(port, &logdetail);
|
||||
break;
|
||||
|
||||
case uaPAM:
|
||||
#ifdef USE_PAM
|
||||
status = CheckPAMAuth(port, port->user_name, "");
|
||||
@@ -762,6 +782,122 @@ CheckPasswordAuth(Port *port, char **logdetail)
|
||||
return result;
|
||||
}
|
||||
|
||||
/*----------------------------------------------------------------
|
||||
* SASL authentication system
|
||||
*----------------------------------------------------------------
|
||||
*/
|
||||
static int
|
||||
CheckSASLAuth(Port *port, char **logdetail)
|
||||
{
|
||||
int mtype;
|
||||
StringInfoData buf;
|
||||
void *scram_opaq;
|
||||
char *output = NULL;
|
||||
int outputlen = 0;
|
||||
int result;
|
||||
char *shadow_pass;
|
||||
bool doomed = false;
|
||||
|
||||
/*
|
||||
* SASL auth is not supported for protocol versions before 3, because it
|
||||
* relies on the overall message length word to determine the SASL payload
|
||||
* size in AuthenticationSASLContinue and PasswordMessage messages. (We
|
||||
* used to have a hard rule that protocol messages must be parsable
|
||||
* without relying on the length word, but we hardly care about older
|
||||
* protocol version anymore.)
|
||||
*/
|
||||
if (PG_PROTOCOL_MAJOR(FrontendProtocol) < 3)
|
||||
ereport(FATAL,
|
||||
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
||||
errmsg("SASL authentication is not supported in protocol version 2")));
|
||||
|
||||
/*
|
||||
* Send first the authentication request to user.
|
||||
*/
|
||||
sendAuthRequest(port, AUTH_REQ_SASL, SCRAM_SHA256_NAME,
|
||||
strlen(SCRAM_SHA256_NAME) + 1);
|
||||
|
||||
/*
|
||||
* If the user doesn't exist, or doesn't have a valid password, or it's
|
||||
* expired, we still go through the motions of SASL authentication, but
|
||||
* tell the authentication method that the authentication is "doomed".
|
||||
* That is, it's going to fail, no matter what.
|
||||
*
|
||||
* This is because we don't want to reveal to an attacker what usernames
|
||||
* are valid, nor which users have a valid password.
|
||||
*/
|
||||
if (get_role_password(port->user_name, &shadow_pass, logdetail) != STATUS_OK)
|
||||
doomed = true;
|
||||
|
||||
/* Initialize the status tracker for message exchanges */
|
||||
scram_opaq = pg_be_scram_init(port->user_name, shadow_pass, doomed);
|
||||
|
||||
/*
|
||||
* Loop through SASL message exchange. This exchange can consist of
|
||||
* multiple messages sent in both directions. First message is always
|
||||
* from the client. All messages from client to server are password
|
||||
* packets (type 'p').
|
||||
*/
|
||||
do
|
||||
{
|
||||
pq_startmsgread();
|
||||
mtype = pq_getbyte();
|
||||
if (mtype != 'p')
|
||||
{
|
||||
/* Only log error if client didn't disconnect. */
|
||||
if (mtype != EOF)
|
||||
{
|
||||
ereport(COMMERROR,
|
||||
(errcode(ERRCODE_PROTOCOL_VIOLATION),
|
||||
errmsg("expected SASL response, got message type %d",
|
||||
mtype)));
|
||||
return STATUS_ERROR;
|
||||
}
|
||||
else
|
||||
return STATUS_EOF;
|
||||
}
|
||||
|
||||
/* Get the actual SASL message */
|
||||
initStringInfo(&buf);
|
||||
if (pq_getmessage(&buf, PG_MAX_SASL_MESSAGE_LENGTH))
|
||||
{
|
||||
/* EOF - pq_getmessage already logged error */
|
||||
pfree(buf.data);
|
||||
return STATUS_ERROR;
|
||||
}
|
||||
|
||||
elog(DEBUG4, "Processing received SASL token of length %d", buf.len);
|
||||
|
||||
/*
|
||||
* we pass 'logdetail' as NULL when doing a mock authentication,
|
||||
* because we should already have a better error message in that case
|
||||
*/
|
||||
result = pg_be_scram_exchange(scram_opaq, buf.data, buf.len,
|
||||
&output, &outputlen,
|
||||
doomed ? NULL : logdetail);
|
||||
|
||||
/* input buffer no longer used */
|
||||
pfree(buf.data);
|
||||
|
||||
if (outputlen > 0)
|
||||
{
|
||||
/*
|
||||
* Negotiation generated data to be sent to the client.
|
||||
*/
|
||||
elog(DEBUG4, "sending SASL response token of length %u", outputlen);
|
||||
|
||||
sendAuthRequest(port, AUTH_REQ_SASL_CONT, output, outputlen);
|
||||
}
|
||||
} while (result == SASL_EXCHANGE_CONTINUE);
|
||||
|
||||
/* Oops, Something bad happened */
|
||||
if (result != SASL_EXCHANGE_SUCCESS)
|
||||
{
|
||||
return STATUS_ERROR;
|
||||
}
|
||||
|
||||
return STATUS_OK;
|
||||
}
|
||||
|
||||
|
||||
/*----------------------------------------------------------------
|
||||
|
@@ -21,6 +21,7 @@
|
||||
#include "catalog/pg_authid.h"
|
||||
#include "common/md5.h"
|
||||
#include "libpq/crypt.h"
|
||||
#include "libpq/scram.h"
|
||||
#include "miscadmin.h"
|
||||
#include "utils/builtins.h"
|
||||
#include "utils/syscache.h"
|
||||
@@ -111,6 +112,8 @@ get_password_type(const char *shadow_pass)
|
||||
{
|
||||
if (strncmp(shadow_pass, "md5", 3) == 0 && strlen(shadow_pass) == MD5_PASSWD_LEN)
|
||||
return PASSWORD_TYPE_MD5;
|
||||
if (strncmp(shadow_pass, "scram-sha-256:", strlen("scram-sha-256:")) == 0)
|
||||
return PASSWORD_TYPE_SCRAM;
|
||||
return PASSWORD_TYPE_PLAINTEXT;
|
||||
}
|
||||
|
||||
@@ -150,9 +153,31 @@ encrypt_password(PasswordType target_type, const char *role,
|
||||
elog(ERROR, "password encryption failed");
|
||||
return encrypted_password;
|
||||
|
||||
case PASSWORD_TYPE_SCRAM:
|
||||
|
||||
/*
|
||||
* cannot convert a SCRAM verifier to an MD5 hash, so fall
|
||||
* through to save the SCRAM verifier instead.
|
||||
*/
|
||||
case PASSWORD_TYPE_MD5:
|
||||
return pstrdup(password);
|
||||
}
|
||||
|
||||
case PASSWORD_TYPE_SCRAM:
|
||||
switch (guessed_type)
|
||||
{
|
||||
case PASSWORD_TYPE_PLAINTEXT:
|
||||
return scram_build_verifier(role, password, 0);
|
||||
|
||||
case PASSWORD_TYPE_MD5:
|
||||
|
||||
/*
|
||||
* cannot convert an MD5 hash to a SCRAM verifier, so fall
|
||||
* through to save the MD5 hash instead.
|
||||
*/
|
||||
case PASSWORD_TYPE_SCRAM:
|
||||
return pstrdup(password);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -160,7 +185,7 @@ encrypt_password(PasswordType target_type, const char *role,
|
||||
* handle every combination of source and target password types.
|
||||
*/
|
||||
elog(ERROR, "cannot encrypt password to requested type");
|
||||
return NULL; /* keep compiler quiet */
|
||||
return NULL; /* keep compiler quiet */
|
||||
}
|
||||
|
||||
/*
|
||||
|
@@ -125,6 +125,7 @@ static const char *const UserAuthName[] =
|
||||
"ident",
|
||||
"password",
|
||||
"md5",
|
||||
"scram",
|
||||
"gss",
|
||||
"sspi",
|
||||
"pam",
|
||||
@@ -1323,6 +1324,8 @@ parse_hba_line(TokenizedLine *tok_line, int elevel)
|
||||
}
|
||||
parsedline->auth_method = uaMD5;
|
||||
}
|
||||
else if (strcmp(token->string, "scram") == 0)
|
||||
parsedline->auth_method = uaSASL;
|
||||
else if (strcmp(token->string, "pam") == 0)
|
||||
#ifdef USE_PAM
|
||||
parsedline->auth_method = uaPAM;
|
||||
|
@@ -42,10 +42,10 @@
|
||||
# or "samenet" to match any address in any subnet that the server is
|
||||
# directly connected to.
|
||||
#
|
||||
# METHOD can be "trust", "reject", "md5", "password", "gss", "sspi",
|
||||
# "ident", "peer", "pam", "ldap", "radius" or "cert". Note that
|
||||
# "password" sends passwords in clear text; "md5" is preferred since
|
||||
# it sends encrypted passwords.
|
||||
# METHOD can be "trust", "reject", "md5", "password", "scram", "gss",
|
||||
# "sspi", "ident", "peer", "pam", "ldap", "radius" or "cert". Note that
|
||||
# "password" sends passwords in clear text; "md5" or "scram" are preferred
|
||||
# since they send encrypted passwords.
|
||||
#
|
||||
# OPTIONS are a set of options for the authentication in the format
|
||||
# NAME=VALUE. The available options depend on the different
|
||||
|
Reference in New Issue
Block a user