diff options
Diffstat (limited to 'src/backend')
-rw-r--r-- | src/backend/access/transam/xlog.c | 24 | ||||
-rw-r--r-- | src/backend/commands/user.c | 14 | ||||
-rw-r--r-- | src/backend/libpq/Makefile | 2 | ||||
-rw-r--r-- | src/backend/libpq/auth-scram.c | 1032 | ||||
-rw-r--r-- | src/backend/libpq/auth.c | 136 | ||||
-rw-r--r-- | src/backend/libpq/crypt.c | 27 | ||||
-rw-r--r-- | src/backend/libpq/hba.c | 3 | ||||
-rw-r--r-- | src/backend/libpq/pg_hba.conf.sample | 8 | ||||
-rw-r--r-- | src/backend/utils/misc/guc.c | 1 | ||||
-rw-r--r-- | src/backend/utils/misc/postgresql.conf.sample | 2 |
10 files changed, 1240 insertions, 9 deletions
diff --git a/src/backend/access/transam/xlog.c b/src/backend/access/transam/xlog.c index 897358342db..744360c7696 100644 --- a/src/backend/access/transam/xlog.c +++ b/src/backend/access/transam/xlog.c @@ -65,6 +65,7 @@ #include "storage/reinit.h" #include "storage/smgr.h" #include "storage/spin.h" +#include "utils/backend_random.h" #include "utils/builtins.h" #include "utils/guc.h" #include "utils/memutils.h" @@ -4665,6 +4666,16 @@ GetSystemIdentifier(void) } /* + * Returns the random nonce from control file. + */ +char * +GetMockAuthenticationNonce(void) +{ + Assert(ControlFile != NULL); + return ControlFile->mock_authentication_nonce; +} + +/* * Are checksums enabled for data pages? */ bool @@ -4914,6 +4925,7 @@ BootStrapXLOG(void) char *recptr; bool use_existent; uint64 sysidentifier; + char mock_auth_nonce[MOCK_AUTH_NONCE_LEN]; struct timeval tv; pg_crc32c crc; @@ -4934,6 +4946,17 @@ BootStrapXLOG(void) sysidentifier |= ((uint64) tv.tv_usec) << 12; sysidentifier |= getpid() & 0xFFF; + /* + * Generate a random nonce. This is used for authentication requests + * that will fail because the user does not exist. The nonce is used to + * create a genuine-looking password challenge for the non-existent user, + * in lieu of an actual stored password. + */ + if (!pg_backend_random(mock_auth_nonce, MOCK_AUTH_NONCE_LEN)) + ereport(PANIC, + (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("could not generation secret authorization token"))); + /* First timeline ID is always 1 */ ThisTimeLineID = 1; @@ -5040,6 +5063,7 @@ BootStrapXLOG(void) memset(ControlFile, 0, sizeof(ControlFileData)); /* Initialize pg_control status fields */ ControlFile->system_identifier = sysidentifier; + memcpy(ControlFile->mock_authentication_nonce, mock_auth_nonce, MOCK_AUTH_NONCE_LEN); ControlFile->state = DB_SHUTDOWNED; ControlFile->time = checkPoint.time; ControlFile->checkPoint = checkPoint.redo; diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c index 994c093250b..14b97791442 100644 --- a/src/backend/commands/user.c +++ b/src/backend/commands/user.c @@ -139,7 +139,12 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt) parser_errposition(pstate, defel->location))); dpassword = defel; if (strcmp(defel->defname, "encryptedPassword") == 0) - password_type = PASSWORD_TYPE_MD5; + { + if (Password_encryption == PASSWORD_TYPE_SCRAM) + password_type = PASSWORD_TYPE_SCRAM; + else + password_type = PASSWORD_TYPE_MD5; + } else if (strcmp(defel->defname, "unencryptedPassword") == 0) password_type = PASSWORD_TYPE_PLAINTEXT; } @@ -542,7 +547,12 @@ AlterRole(AlterRoleStmt *stmt) errmsg("conflicting or redundant options"))); dpassword = defel; if (strcmp(defel->defname, "encryptedPassword") == 0) - password_type = PASSWORD_TYPE_MD5; + { + if (Password_encryption == PASSWORD_TYPE_SCRAM) + password_type = PASSWORD_TYPE_SCRAM; + else + password_type = PASSWORD_TYPE_MD5; + } else if (strcmp(defel->defname, "unencryptedPassword") == 0) password_type = PASSWORD_TYPE_PLAINTEXT; } diff --git a/src/backend/libpq/Makefile b/src/backend/libpq/Makefile index 1bdd8adde24..7fa2b027433 100644 --- a/src/backend/libpq/Makefile +++ b/src/backend/libpq/Makefile @@ -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 diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c new file mode 100644 index 00000000000..cc4e84403f9 --- /dev/null +++ b/src/backend/libpq/auth-scram.c @@ -0,0 +1,1032 @@ +/*------------------------------------------------------------------------- + * + * auth-scram.c + * Server-side implementation of the SASL SCRAM-SHA-256 mechanism. + * + * See the following RFCs for more details: + * - RFC 5802: https://tools.ietf.org/html/rfc5802 + * - RFC 7677: https://tools.ietf.org/html/rfc7677 + * + * Here are some differences: + * + * - Username from the authentication exchange is not used. The client + * should send an empty string as the username. + * - Password is not processed with the SASLprep algorithm. + * - Channel binding is not supported yet. + * + * The password stored in pg_authid consists of the salt, iteration count, + * StoredKey and ServerKey. + * + * On error handling: + * + * Don't reveal user information to an unauthenticated client. We don't + * want an attacker to be able to probe whether a particular username is + * valid. In SCRAM, the server has to read the salt and iteration count + * from the user's password verifier, and send it to the client. To avoid + * revealing whether a user exists, when the client tries to authenticate + * with a username that doesn't exist, or doesn't have a valid SCRAM + * verifier in pg_authid, we create a fake salt and iteration count + * on-the-fly, and proceed with the authentication with that. In the end, + * we'll reject the attempt, as if an incorrect password was given. When + * we are performing a "mock" authentication, the 'doomed' flag in + * scram_state is set. + * + * In the error messages, avoid printing strings from the client, unless + * you check that they are pure ASCII. We don't want an unauthenticated + * attacker to be able to spam the logs with characters that are not valid + * to the encoding being used, whatever that is. We cannot avoid that in + * general, after logging in, but let's do what we can here. + * + * Portions Copyright (c) 1996-2016, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * src/backend/libpq/auth-scram.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include <unistd.h> + +#include "access/xlog.h" +#include "catalog/pg_authid.h" +#include "catalog/pg_control.h" +#include "common/base64.h" +#include "common/scram-common.h" +#include "common/sha2.h" +#include "libpq/auth.h" +#include "libpq/crypt.h" +#include "libpq/scram.h" +#include "miscadmin.h" +#include "utils/backend_random.h" +#include "utils/builtins.h" +#include "utils/timestamp.h" + +/* + * Status data for a SCRAM authentication exchange. This should be kept + * internal to this file. + */ +typedef enum +{ + SCRAM_AUTH_INIT, + SCRAM_AUTH_SALT_SENT, + SCRAM_AUTH_FINISHED +} scram_state_enum; + +typedef struct +{ + scram_state_enum state; + + const char *username; /* username from startup packet */ + + char *salt; /* base64-encoded */ + int iterations; + uint8 StoredKey[SCRAM_KEY_LEN]; + uint8 ServerKey[SCRAM_KEY_LEN]; + + /* Fields of the first message from client */ + char *client_first_message_bare; + char *client_username; + char *client_nonce; + + /* Fields from the last message from client */ + char *client_final_message_without_proof; + char *client_final_nonce; + char ClientProof[SCRAM_KEY_LEN]; + + /* Fields generated in the server */ + char *server_first_message; + char *server_nonce; + + /* + * If something goes wrong during the authentication, or we are performing + * a "mock" authentication (see comments at top of file), the 'doomed' + * flag is set. A reason for the failure, for the server log, is put in + * 'logdetail'. + */ + bool doomed; + char *logdetail; +} scram_state; + +static void read_client_first_message(scram_state *state, char *input); +static void read_client_final_message(scram_state *state, char *input); +static char *build_server_first_message(scram_state *state); +static char *build_server_final_message(scram_state *state); +static bool verify_client_proof(scram_state *state); +static bool verify_final_nonce(scram_state *state); +static bool parse_scram_verifier(const char *verifier, char **salt, + int *iterations, uint8 *stored_key, uint8 *server_key); +static void mock_scram_verifier(const char *username, char **salt, int *iterations, + uint8 *stored_key, uint8 *server_key); +static bool is_scram_printable(char *p); +static char *sanitize_char(char c); +static char *scram_MockSalt(const char *username); + +/* + * pg_be_scram_init + * + * Initialize a new SCRAM authentication exchange status tracker. This + * needs to be called before doing any exchange. It will be filled later + * after the beginning of the exchange with verifier data. + * + * 'username' is the provided by the client. 'shadow_pass' is the role's + * password verifier, from pg_authid.rolpassword. If 'doomed' is true, the + * authentication must fail, as if an incorrect password was given. + * 'shadow_pass' may be NULL, when 'doomed' is set. + */ +void * +pg_be_scram_init(const char *username, const char *shadow_pass, bool doomed) +{ + scram_state *state; + int password_type; + + state = (scram_state *) palloc0(sizeof(scram_state)); + state->state = SCRAM_AUTH_INIT; + state->username = username; + + /* + * Perform sanity checks on the provided password after catalog lookup. + * The authentication is bound to fail if the lookup itself failed or if + * the password stored is MD5-encrypted. Authentication is possible for + * users with a valid plain password though. + */ + + if (shadow_pass == NULL || doomed) + password_type = -1; + else + password_type = get_password_type(shadow_pass); + + if (password_type == PASSWORD_TYPE_SCRAM) + { + if (!parse_scram_verifier(shadow_pass, &state->salt, &state->iterations, + state->StoredKey, state->ServerKey)) + { + /* + * The password looked like a SCRAM verifier, but could not be + * parsed. + */ + elog(LOG, "invalid SCRAM verifier for user \"%s\"", username); + doomed = true; + } + } + else if (password_type == PASSWORD_TYPE_PLAINTEXT) + { + char *verifier; + + /* + * The password provided is in plain format, in which case a fresh + * SCRAM verifier can be generated and used for the rest of the + * processing. + */ + verifier = scram_build_verifier(username, shadow_pass, 0); + + (void) parse_scram_verifier(verifier, &state->salt, &state->iterations, + state->StoredKey, state->ServerKey); + pfree(verifier); + } + else + doomed = true; + + if (doomed) + { + /* + * We don't have a valid SCRAM verifier, nor could we generate one, or + * the caller requested us to perform a dummy authentication. + * + * The authentication is bound to fail, but to avoid revealing + * information to the attacker, go through the motions with a fake + * SCRAM verifier, and fail as if the password was incorrect. + */ + state->logdetail = psprintf(_("User \"%s\" does not have a valid SCRAM verifier."), + state->username); + mock_scram_verifier(username, &state->salt, &state->iterations, + state->StoredKey, state->ServerKey); + } + state->doomed = doomed; + + return state; +} + +/* + * Continue a SCRAM authentication exchange. + * + * The next message to send to client is saved in "output", for a length + * of "outputlen". In the case of an error, optionally store a palloc'd + * string at *logdetail that will be sent to the postmaster log (but not + * the client). + */ +int +pg_be_scram_exchange(void *opaq, char *input, int inputlen, + char **output, int *outputlen, char **logdetail) +{ + scram_state *state = (scram_state *) opaq; + int result; + + *output = NULL; + + /* + * Check that the input length agrees with the string length of the input. + * We can ignore inputlen after this. + */ + if (inputlen == 0) + ereport(ERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + (errmsg("malformed SCRAM message (empty message)")))); + if (inputlen != strlen(input)) + ereport(ERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + (errmsg("malformed SCRAM message (length mismatch)")))); + + switch (state->state) + { + case SCRAM_AUTH_INIT: + + /* + * Initialization phase. Receive the first message from client + * and be sure that it parsed correctly. Then send the challenge + * to the client. + */ + read_client_first_message(state, input); + + /* prepare message to send challenge */ + *output = build_server_first_message(state); + + state->state = SCRAM_AUTH_SALT_SENT; + result = SASL_EXCHANGE_CONTINUE; + break; + + case SCRAM_AUTH_SALT_SENT: + + /* + * Final phase for the server. Receive the response to the + * challenge previously sent, verify, and let the client know that + * everything went well (or not). + */ + read_client_final_message(state, input); + + if (!verify_final_nonce(state)) + ereport(ERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + (errmsg("invalid SCRAM response (nonce mismatch)")))); + + /* + * Now check the final nonce and the client proof. + * + * If we performed a "mock" authentication that we knew would fail + * from the get go, this is where we fail. + * + * NB: the order of these checks is intentional. We calculate the + * client proof even in a mock authentication, even though it's + * bound to fail, to thwart timing attacks to determine if a role + * with the given name exists or not. + */ + if (!verify_client_proof(state) || state->doomed) + { + /* + * Signal invalid-proof, although the real reason might also + * be e.g. that the password has expired, or the user doesn't + * exist. "e=other-error" might be more correct, but + * "e=invalid-proof" is more likely to give a nice error + * message to the user. + */ + *output = psprintf("e=invalid-proof"); + result = SASL_EXCHANGE_FAILURE; + break; + } + + /* Build final message for client */ + *output = build_server_final_message(state); + + /* Success! */ + result = SASL_EXCHANGE_SUCCESS; + state->state = SCRAM_AUTH_FINISHED; + break; + + default: + elog(ERROR, "invalid SCRAM exchange state"); + result = SASL_EXCHANGE_FAILURE; + } + + if (result == SASL_EXCHANGE_FAILURE && state->logdetail && logdetail) + *logdetail = state->logdetail; + + if (*output) + *outputlen = strlen(*output); + + return result; +} + +/* + * Construct a verifier string for SCRAM, stored in pg_authid.rolpassword. + * + * If iterations is 0, default number of iterations is used. The result is + * palloc'd, so caller is responsible for freeing it. + */ +char * +scram_build_verifier(const char *username, const char *password, + int iterations) +{ + uint8 keybuf[SCRAM_KEY_LEN + 1]; + char storedkey_hex[SCRAM_KEY_LEN * 2 + 1]; + char serverkey_hex[SCRAM_KEY_LEN * 2 + 1]; + char salt[SCRAM_SALT_LEN]; + char *encoded_salt; + int encoded_len; + + if (iterations <= 0) + iterations = SCRAM_ITERATIONS_DEFAULT; + + if (!pg_backend_random(salt, SCRAM_SALT_LEN)) + { + ereport(LOG, + (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("could not generate random salt"))); + return NULL; + } + + encoded_salt = palloc(pg_b64_enc_len(SCRAM_SALT_LEN) + 1); + encoded_len = pg_b64_encode(salt, SCRAM_SALT_LEN, encoded_salt); + encoded_salt[encoded_len] = '\0'; + + /* Calculate StoredKey, and encode it in hex */ + scram_ClientOrServerKey(password, salt, SCRAM_SALT_LEN, + iterations, SCRAM_CLIENT_KEY_NAME, keybuf); + scram_H(keybuf, SCRAM_KEY_LEN, keybuf); /* StoredKey */ + (void) hex_encode((const char *) keybuf, SCRAM_KEY_LEN, storedkey_hex); + storedkey_hex[SCRAM_KEY_LEN * 2] = '\0'; + + /* And same for ServerKey */ + scram_ClientOrServerKey(password, salt, SCRAM_SALT_LEN, iterations, + SCRAM_SERVER_KEY_NAME, keybuf); + (void) hex_encode((const char *) keybuf, SCRAM_KEY_LEN, serverkey_hex); + serverkey_hex[SCRAM_KEY_LEN * 2] = '\0'; + + return psprintf("scram-sha-256:%s:%d:%s:%s", encoded_salt, iterations, storedkey_hex, serverkey_hex); +} + + +/* + * Check if given verifier can be used for SCRAM authentication. + * + * Returns true if it is a SCRAM verifier, and false otherwise. + */ +bool +is_scram_verifier(const char *verifier) +{ + char *salt = NULL; + int iterations; + uint8 stored_key[SCRAM_KEY_LEN]; + uint8 server_key[SCRAM_KEY_LEN]; + bool result; + + result = parse_scram_verifier(verifier, &salt, &iterations, stored_key, server_key); + if (salt) + pfree(salt); + + return result; +} + + +/* + * Parse and validate format of given SCRAM verifier. + * + * Returns true if the SCRAM verifier has been parsed, and false otherwise. + */ +static bool +parse_scram_verifier(const char *verifier, char **salt, int *iterations, + uint8 *stored_key, uint8 *server_key) +{ + char *v; + char *p; + + /* + * The verifier is of form: + * + * scram-sha-256:<salt>:<iterations>:<storedkey>:<serverkey> + */ + if (strncmp(verifier, "scram-sha-256:", strlen("scram-sha-256:")) != 0) + return false; + + v = pstrdup(verifier + strlen("scram-sha-256:")); + + /* salt */ + if ((p = strtok(v, ":")) == NULL) + goto invalid_verifier; + *salt = pstrdup(p); + + /* iterations */ + if ((p = strtok(NULL, ":")) == NULL) + goto invalid_verifier; + errno = 0; + *iterations = strtol(p, &p, SCRAM_ITERATION_LEN); + if (*p || errno != 0) + goto invalid_verifier; + + /* storedkey */ + if ((p = strtok(NULL, ":")) == NULL) + goto invalid_verifier; + if (strlen(p) != SCRAM_KEY_LEN * 2) + goto invalid_verifier; + + hex_decode(p, SCRAM_KEY_LEN * 2, (char *) stored_key); + + /* serverkey */ + if ((p = strtok(NULL, ":")) == NULL) + goto invalid_verifier; + if (strlen(p) != SCRAM_KEY_LEN * 2) + goto invalid_verifier; + hex_decode(p, SCRAM_KEY_LEN * 2, (char *) server_key); + + pfree(v); + return true; + +invalid_verifier: + pfree(v); + return false; +} + +static void +mock_scram_verifier(const char *username, char **salt, int *iterations, + uint8 *stored_key, uint8 *server_key) +{ + char *raw_salt; + char *encoded_salt; + int encoded_len; + + /* Generate deterministic salt */ + raw_salt = scram_MockSalt(username); + + encoded_salt = (char *) palloc(pg_b64_enc_len(SCRAM_SALT_LEN) + 1); + encoded_len = pg_b64_encode(raw_salt, SCRAM_SALT_LEN, encoded_salt); + encoded_salt[encoded_len] = '\0'; + + *salt = encoded_salt; + *iterations = SCRAM_ITERATIONS_DEFAULT; + + /* StoredKey and ServerKey are not used in a doomed authentication */ + memset(stored_key, 0, SCRAM_KEY_LEN); + memset(server_key, 0, SCRAM_KEY_LEN); +} + +/* + * Read the value in a given SASL exchange message for given attribute. + */ +static char * +read_attr_value(char **input, char attr) +{ + char *begin = *input; + char *end; + + if (*begin != attr) + ereport(ERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + (errmsg("malformed SCRAM message (attribute '%c' expected, %s found)", + attr, sanitize_char(*begin))))); + begin++; + + if (*begin != '=') + ereport(ERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + (errmsg("malformed SCRAM message (expected = in attr %c)", attr)))); + begin++; + + end = begin; + while (*end && *end != ',') + end++; + + if (*end) + { + *end = '\0'; + *input = end + 1; + } + else + *input = end; + + return begin; +} + +static bool +is_scram_printable(char *p) +{ + /*------ + * Printable characters, as defined by SCRAM spec: (RFC 5802) + * + * printable = %x21-2B / %x2D-7E + * ;; Printable ASCII except ",". + * ;; Note that any "printable" is also + * ;; a valid "value". + *------ + */ + for (; *p; p++) + { + if (*p < 0x21 || *p > 0x7E || *p == 0x2C /* comma */ ) + return false; + } + return true; +} + +/* + * Convert an arbitrary byte to printable form. For error messages. + * + * If it's a printable ASCII character, print it as a single character. + * otherwise, print it in hex. + * + * The returned pointer points to a static buffer. + */ +static char * +sanitize_char(char c) +{ + static char buf[5]; + + if (c >= 0x21 && c <= 0x7E) + snprintf(buf, sizeof(buf), "'%c'", c); + else + snprintf(buf, sizeof(buf), "0x%02x", c); + return buf; +} + +/* + * Read the next attribute and value in a SASL exchange message. + * + * Returns NULL if there is attribute. + */ +static char * +read_any_attr(char **input, char *attr_p) +{ + char *begin = *input; + char *end; + char attr = *begin; + + /*------ + * attr-val = ALPHA "=" value + * ;; Generic syntax of any attribute sent + * ;; by server or client + *------ + */ + if (!((attr >= 'A' && attr <= 'Z') || + (attr >= 'a' && attr <= 'z'))) + ereport(ERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + (errmsg("malformed SCRAM message (attribute expected, invalid char %s found)", + sanitize_char(attr))))); + if (attr_p) + *attr_p = attr; + begin++; + + if (*begin != '=') + ereport(ERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + (errmsg("malformed SCRAM message (expected = in attr %c)", attr)))); + begin++; + + end = begin; + while (*end && *end != ',') + end++; + + if (*end) + { + *end = '\0'; + *input = end + 1; + } + else + *input = end; + + return begin; +} + +/* + * Read and parse the first message from client in the context of a SASL + * authentication exchange message. + * + * At this stage, any errors will be reported directly with ereport(ERROR). + */ +static void +read_client_first_message(scram_state *state, char *input) +{ + input = pstrdup(input); + + /*------ + * The syntax for the client-first-message is: (RFC 5802) + * + * saslname = 1*(value-safe-char / "=2C" / "=3D") + * ;; Conforms to <value>. + * + * authzid = "a=" saslname + * ;; Protocol specific. + * + * cb-name = 1*(ALPHA / DIGIT / "." / "-") + * ;; See RFC 5056, Section 7. + * ;; E.g., "tls-server-end-point" or + * ;; "tls-unique". + * + * gs2-cbind-flag = ("p=" cb-name) / "n" / "y" + * ;; "n" -> client doesn't support channel binding. + * ;; "y" -> client does support channel binding + * ;; but thinks the server does not. + * ;; "p" -> client requires channel binding. + * ;; The selected channel binding follows "p=". + * + * gs2-header = gs2-cbind-flag "," [ authzid ] "," + * ;; GS2 header for SCRAM + * ;; (the actual GS2 header includes an optional + * ;; flag to indicate that the GSS mechanism is not + * ;; "standard", but since SCRAM is "standard", we + * ;; don't include that flag). + * + * username = "n=" saslname + * ;; Usernames are prepared using SASLprep. + * + * reserved-mext = "m=" 1*(value-char) + * ;; Reserved for signaling mandatory extensions. + * ;; The exact syntax will be defined in + * ;; the future. + * + * nonce = "r=" c-nonce [s-nonce] + * ;; Second part provided by server. + * + * c-nonce = printable + * + * client-first-message-bare = + * [reserved-mext ","] + * username "," nonce ["," extensions] + * + * client-first-message = + * gs2-header client-first-message-bare + * + * For example: + * n,,n=user,r=fyko+d2lbbFgONRv9qkxdawL + * + * The "n,," in the beginning means that the client doesn't support + * channel binding, and no authzid is given. "n=user" is the username. + * However, in PostgreSQL the username is sent in the startup packet, and + * the username in the SCRAM exchange is ignored. libpq always sends it + * as an empty string. The last part, "r=fyko+d2lbbFgONRv9qkxdawL" is + * the client nonce. + *------ + */ + + /* read gs2-cbind-flag */ + switch (*input) + { + case 'n': + /* Client does not support channel binding */ + input++; + break; + case 'y': + /* Client supports channel binding, but we're not doing it today */ + input++; + break; + case 'p': + + /* + * Client requires channel binding. We don't support it. + * + * RFC 5802 specifies a particular error code, + * e=server-does-support-channel-binding, for this. But it can + * only be sent in the server-final message, and we don't want to + * go through the motions of the authentication, knowing it will + * fail, just to send that error message. + */ + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("client requires SCRAM channel binding, but it is not supported"))); + default: + ereport(ERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + (errmsg("malformed SCRAM message (unexpected channel-binding flag %s)", + sanitize_char(*input))))); + } + if (*input != ',') + ereport(ERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("malformed SCRAM message (comma expected, got %s)", + sanitize_char(*input)))); + input++; + + /* + * Forbid optional authzid (authorization identity). We don't support it. + */ + if (*input == 'a') + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("client uses authorization identity, but it is not supported"))); + if (*input != ',') + ereport(ERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("malformed SCRAM message (unexpected attribute %s in client-first-message)", + sanitize_char(*input)))); + input++; + + state->client_first_message_bare = pstrdup(input); + + /* + * Any mandatory extensions would go here. We don't support any. + * + * RFC 5802 specifies error code "e=extensions-not-supported" for this, + * but it can only be sent in the server-final message. We prefer to fail + * immediately (which the RFC also allows). + */ + if (*input == 'm') + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("client requires mandatory SCRAM extension"))); + + /* + * Read username. Note: this is ignored. We use the username from the + * startup message instead, still it is kept around if provided as it + * proves to be useful for debugging purposes. + */ + state->client_username = read_attr_value(&input, 'n'); + + /* read nonce and check that it is made of only printable characters */ + state->client_nonce = read_attr_value(&input, 'r'); + if (!is_scram_printable(state->client_nonce)) + ereport(ERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("non-printable characters in SCRAM nonce"))); + + /* + * There can be any number of optional extensions after this. We don't + * support any extensions, so ignore them. + */ + while (*input != '\0') + read_any_attr(&input, NULL); + + /* success! */ +} + +/* + * Verify the final nonce contained in the last message received from + * client in an exchange. + */ +static bool +verify_final_nonce(scram_state *state) +{ + int client_nonce_len = strlen(state->client_nonce); + int server_nonce_len = strlen(state->server_nonce); + int final_nonce_len = strlen(state->client_final_nonce); + + if (final_nonce_len != client_nonce_len + server_nonce_len) + return false; + if (memcmp(state->client_final_nonce, state->client_nonce, client_nonce_len) != 0) + return false; + if (memcmp(state->client_final_nonce + client_nonce_len, state->server_nonce, server_nonce_len) != 0) + return false; + + return true; +} + +/* + * Verify the client proof contained in the last message received from + * client in an exchange. + */ +static bool +verify_client_proof(scram_state *state) +{ + uint8 ClientSignature[SCRAM_KEY_LEN]; + uint8 ClientKey[SCRAM_KEY_LEN]; + uint8 client_StoredKey[SCRAM_KEY_LEN]; + scram_HMAC_ctx ctx; + int i; + + /* calculate ClientSignature */ + scram_HMAC_init(&ctx, state->StoredKey, SCRAM_KEY_LEN); + scram_HMAC_update(&ctx, + state->client_first_message_bare, + strlen(state->client_first_message_bare)); + scram_HMAC_update(&ctx, ",", 1); + scram_HMAC_update(&ctx, + state->server_first_message, + strlen(state->server_first_message)); + scram_HMAC_update(&ctx, ",", 1); + scram_HMAC_update(&ctx, + state->client_final_message_without_proof, + strlen(state->client_final_message_without_proof)); + scram_HMAC_final(ClientSignature, &ctx); + + /* Extract the ClientKey that the client calculated from the proof */ + for (i = 0; i < SCRAM_KEY_LEN; i++) + ClientKey[i] = state->ClientProof[i] ^ ClientSignature[i]; + + /* Hash it one more time, and compare with StoredKey */ + scram_H(ClientKey, SCRAM_KEY_LEN, client_StoredKey); + + if (memcmp(client_StoredKey, state->StoredKey, SCRAM_KEY_LEN) != 0) + return false; + + return true; +} + +/* + * Build the first server-side message sent to the client in a SASL + * communication exchange. + */ +static char * +build_server_first_message(scram_state *state) +{ + /*------ + * The syntax for the server-first-message is: (RFC 5802) + * + * server-first-message = + * [reserved-mext ","] nonce "," salt "," + * iteration-count ["," extensions] + * + * nonce = "r=" c-nonce [s-nonce] + * ;; Second part provided by server. + * + * c-nonce = printable + * + * s-nonce = printable + * + * salt = "s=" base64 + * + * iteration-count = "i=" posit-number + * ;; A positive number. + * + * Example: + * + * r=fyko+d2lbbFgONRv9qkxdawL3rfcNHYJY1ZVvWVs7j,s=QSXCR+Q6sek8bf92,i=4096 + *------ + */ + + /* + * Per the spec, the nonce may consist of any printable ASCII characters. + * For convenience, however, we don't use the whole range available, + * rather, we generate some random bytes, and base64 encode them. + */ + char raw_nonce[SCRAM_RAW_NONCE_LEN]; + int encoded_len; + + if (!pg_backend_random(raw_nonce, SCRAM_RAW_NONCE_LEN)) + ereport(COMMERROR, + (errcode(ERRCODE_INTERNAL_ERROR), + errmsg("could not generate random nonce"))); + + state->server_nonce = palloc(pg_b64_enc_len(SCRAM_RAW_NONCE_LEN) + 1); + encoded_len = pg_b64_encode(raw_nonce, SCRAM_RAW_NONCE_LEN, state->server_nonce); + state->server_nonce[encoded_len] = '\0'; + + state->server_first_message = + psprintf("r=%s%s,s=%s,i=%u", + state->client_nonce, state->server_nonce, + state->salt, state->iterations); + + return state->server_first_message; +} + + +/* + * Read and parse the final message received from client. + */ +static void +read_client_final_message(scram_state *state, char *input) +{ + char attr; + char *channel_binding; + char *value; + char *begin, + *proof; + char *p; + char *client_proof; + + begin = p = pstrdup(input); + + /*------ + * The syntax for the server-first-message is: (RFC 5802) + * + * gs2-header = gs2-cbind-flag "," [ authzid ] "," + * ;; GS2 header for SCRAM + * ;; (the actual GS2 header includes an optional + * ;; flag to indicate that the GSS mechanism is not + * ;; "standard", but since SCRAM is "standard", we + * ;; don't include that flag). + * + * cbind-input = gs2-header [ cbind-data ] + * ;; cbind-data MUST be present for + * ;; gs2-cbind-flag of "p" and MUST be absent + * ;; for "y" or "n". + * + * channel-binding = "c=" base64 + * ;; base64 encoding of cbind-input. + * + * proof = "p=" base64 + * + * client-final-message-without-proof = + * channel-binding "," nonce ["," + * extensions] + * + * client-final-message = + * client-final-message-without-proof "," proof + *------ + */ + + /* + * Read channel-binding. We don't support channel binding, so it's + * expected to always be "biws", which is "n,,", base64-encoded. + */ + channel_binding = read_attr_value(&p, 'c'); + if (strcmp(channel_binding, "biws") != 0) + ereport(ERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + (errmsg("unexpected SCRAM channel-binding attribute in client-final-message")))); + state->client_final_nonce = read_attr_value(&p, 'r'); + + /* ignore optional extensions */ + do + { + proof = p - 1; + value = read_any_attr(&p, &attr); + } while (attr != 'p'); + + client_proof = palloc(pg_b64_dec_len(strlen(value))); + if (pg_b64_decode(value, strlen(value), client_proof) != SCRAM_KEY_LEN) + ereport(ERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + (errmsg("malformed SCRAM message (malformed proof in client-final-message")))); + memcpy(state->ClientProof, client_proof, SCRAM_KEY_LEN); + pfree(client_proof); + + if (*p != '\0') + ereport(ERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + (errmsg("malformed SCRAM message (garbage at end of client-final-message)")))); + + state->client_final_message_without_proof = palloc(proof - begin + 1); + memcpy(state->client_final_message_without_proof, input, proof - begin); + state->client_final_message_without_proof[proof - begin] = '\0'; +} + +/* + * Build the final server-side message of an exchange. + */ +static char * +build_server_final_message(scram_state *state) +{ + uint8 ServerSignature[SCRAM_KEY_LEN]; + char *server_signature_base64; + int siglen; + scram_HMAC_ctx ctx; + + /* calculate ServerSignature */ + scram_HMAC_init(&ctx, state->ServerKey, SCRAM_KEY_LEN); + scram_HMAC_update(&ctx, + state->client_first_message_bare, + strlen(state->client_first_message_bare)); + scram_HMAC_update(&ctx, ",", 1); + scram_HMAC_update(&ctx, + state->server_first_message, + strlen(state->server_first_message)); + scram_HMAC_update(&ctx, ",", 1); + scram_HMAC_update(&ctx, + state->client_final_message_without_proof, + strlen(state->client_final_message_without_proof)); + scram_HMAC_final(ServerSignature, &ctx); + + server_signature_base64 = palloc(pg_b64_enc_len(SCRAM_KEY_LEN) + 1); + siglen = pg_b64_encode((const char *) ServerSignature, + SCRAM_KEY_LEN, server_signature_base64); + server_signature_base64[siglen] = '\0'; + + /*------ + * The syntax for the server-final-message is: (RFC 5802) + * + * verifier = "v=" base64 + * ;; base-64 encoded ServerSignature. + * + * server-final-message = (server-error / verifier) + * ["," extensions] + * + *------ + */ + return psprintf("v=%s", server_signature_base64); +} + + +/* + * Determinisitcally generate salt for mock authentication, using a SHA256 + * hash based on the username and a cluster-level secret key. Returns a + * pointer to a static buffer of size SCRAM_SALT_LEN. + */ +static char * +scram_MockSalt(const char *username) +{ + pg_sha256_ctx ctx; + static uint8 sha_digest[PG_SHA256_DIGEST_LENGTH]; + char *mock_auth_nonce = GetMockAuthenticationNonce(); + + /* + * Generate salt using a SHA256 hash of the username and the cluster's + * mock authentication nonce. (This works as long as the salt length is + * not larger the SHA256 digest length. If the salt is smaller, the caller + * will just ignore the extra data)) + */ + StaticAssertStmt(PG_SHA256_DIGEST_LENGTH >= SCRAM_SALT_LEN, + "salt length greater than SHA256 digest length"); + + pg_sha256_init(&ctx); + pg_sha256_update(&ctx, (uint8 *) username, strlen(username)); + pg_sha256_update(&ctx, (uint8 *) mock_auth_nonce, MOCK_AUTH_NONCE_LEN); + pg_sha256_final(&ctx, sha_digest); + + return (char *) sha_digest; +} diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c index 824e40837b4..ebf10bbbaef 100644 --- a/src/backend/libpq/auth.c +++ b/src/backend/libpq/auth.c @@ -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; +} /*---------------------------------------------------------------- diff --git a/src/backend/libpq/crypt.c b/src/backend/libpq/crypt.c index e7dd212355d..bd3e936d38c 100644 --- a/src/backend/libpq/crypt.c +++ b/src/backend/libpq/crypt.c @@ -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,7 +153,29 @@ 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 */ } /* diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c index 323bfa858d7..3817d249c44 100644 --- a/src/backend/libpq/hba.c +++ b/src/backend/libpq/hba.c @@ -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; diff --git a/src/backend/libpq/pg_hba.conf.sample b/src/backend/libpq/pg_hba.conf.sample index e0fbfcb0260..73f7973ea22 100644 --- a/src/backend/libpq/pg_hba.conf.sample +++ b/src/backend/libpq/pg_hba.conf.sample @@ -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 diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c index 0707f666311..f8b073d8a97 100644 --- a/src/backend/utils/misc/guc.c +++ b/src/backend/utils/misc/guc.c @@ -409,6 +409,7 @@ static const struct config_enum_entry force_parallel_mode_options[] = { static const struct config_enum_entry password_encryption_options[] = { {"plain", PASSWORD_TYPE_PLAINTEXT, false}, {"md5", PASSWORD_TYPE_MD5, false}, + {"scram", PASSWORD_TYPE_SCRAM, false}, {"off", PASSWORD_TYPE_PLAINTEXT, false}, {"on", PASSWORD_TYPE_MD5, false}, {"true", PASSWORD_TYPE_MD5, true}, diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index 157d775853a..891b16e483b 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -84,7 +84,7 @@ #ssl_key_file = 'server.key' #ssl_ca_file = '' #ssl_crl_file = '' -#password_encryption = md5 # md5 or plain +#password_encryption = md5 # md5, scram or plain #db_user_namespace = off #row_security = on |