/*------------------------------------------------------------------------- * * 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 5803: https://tools.ietf.org/html/rfc5803 * - 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. * * - If the password isn't valid UTF-8, or contains characters prohibited * by the SASLprep profile, we skip the SASLprep pre-processing and use * the raw bytes in calculating the hash. * * - If channel binding is used, the channel binding type is always * "tls-server-end-point". The spec says the default is "tls-unique" * (RFC 5802, section 6.1. Default Channel Binding), but there are some * problems with that. Firstly, not all SSL libraries provide an API to * get the TLS Finished message, required to use "tls-unique". Secondly, * "tls-unique" is not specified for TLS v1.3, and as of this writing, * it's not clear if there will be a replacement. We could support both * "tls-server-end-point" and "tls-unique", but for our use case, * "tls-unique" doesn't really have any advantages. The main advantage * of "tls-unique" would be that it works even if the server doesn't * have a certificate, but PostgreSQL requires a server certificate * whenever SSL is used, anyway. * * * The password stored in pg_authid consists of the iteration count, salt, * StoredKey and ServerKey. * * SASLprep usage * -------------- * * One notable difference to the SCRAM specification is that while the * specification dictates that the password is in UTF-8, and prohibits * certain characters, we are more lenient. If the password isn't a valid * UTF-8 string, or contains prohibited characters, the raw bytes are used * to calculate the hash instead, without SASLprep processing. This is * because PostgreSQL supports other encodings too, and the encoding being * used during authentication is undefined (client_encoding isn't set until * after authentication). In effect, we try to interpret the password as * UTF-8 and apply SASLprep processing, but if it looks invalid, we assume * that it's in some other encoding. * * In the worst case, we misinterpret a password that's in a different * encoding as being Unicode, because it happens to consists entirely of * valid UTF-8 bytes, and we apply Unicode normalization to it. As long * as we do that consistently, that will not lead to failed logins. * Fortunately, the UTF-8 byte sequences that are ignored by SASLprep * don't correspond to any commonly used characters in any of the other * supported encodings, so it should not lead to any significant loss in * entropy, even if the normalization is incorrectly applied to a * non-UTF-8 password. * * 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 stored secret, 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 * secret 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-2023, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * * src/backend/libpq/auth-scram.c * *------------------------------------------------------------------------- */ #include "postgres.h" #include #include "access/xlog.h" #include "catalog/pg_authid.h" #include "catalog/pg_control.h" #include "common/base64.h" #include "common/hmac.h" #include "common/saslprep.h" #include "common/scram-common.h" #include "common/sha2.h" #include "libpq/auth.h" #include "libpq/crypt.h" #include "libpq/sasl.h" #include "libpq/scram.h" #include "miscadmin.h" #include "utils/builtins.h" #include "utils/timestamp.h" static void scram_get_mechanisms(Port *port, StringInfo buf); static void *scram_init(Port *port, const char *selected_mech, const char *shadow_pass); static int scram_exchange(void *opaq, const char *input, int inputlen, char **output, int *outputlen, const char **logdetail); /* Mechanism declaration */ const pg_be_sasl_mech pg_be_scram_mech = { scram_get_mechanisms, scram_init, scram_exchange }; /* * 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 */ Port *port; bool channel_binding_in_use; /* State data depending on the hash type */ pg_cryptohash_type hash_type; int key_length; int iterations; char *salt; /* base64-encoded */ uint8 StoredKey[SCRAM_MAX_KEY_LEN]; uint8 ServerKey[SCRAM_MAX_KEY_LEN]; /* Fields of the first message from client */ char cbind_flag; 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_MAX_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, const char *input); static void read_client_final_message(scram_state *state, const 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 void mock_scram_secret(const char *username, pg_cryptohash_type *hash_type, int *iterations, int *key_length, char **salt, uint8 *stored_key, uint8 *server_key); static bool is_scram_printable(char *p); static char *sanitize_char(char c); static char *sanitize_str(const char *s); static char *scram_mock_salt(const char *username, pg_cryptohash_type hash_type, int key_length); /* * The number of iterations to use when generating new secrets. */ int scram_sha_256_iterations = SCRAM_SHA_256_DEFAULT_ITERATIONS; /* * Get a list of SASL mechanisms that this module supports. * * For the convenience of building the FE/BE packet that lists the * mechanisms, the names are appended to the given StringInfo buffer, * separated by '\0' bytes. */ static void scram_get_mechanisms(Port *port, StringInfo buf) { /* * Advertise the mechanisms in decreasing order of importance. So the * channel-binding variants go first, if they are supported. Channel * binding is only supported with SSL, and only if the SSL implementation * has a function to get the certificate's hash. */ #ifdef HAVE_BE_TLS_GET_CERTIFICATE_HASH if (port->ssl_in_use) { appendStringInfoString(buf, SCRAM_SHA_256_PLUS_NAME); appendStringInfoChar(buf, '\0'); } #endif appendStringInfoString(buf, SCRAM_SHA_256_NAME); appendStringInfoChar(buf, '\0'); } /* * 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 authentication information. * * 'selected_mech' identifies the SASL mechanism that the client selected. * It should be one of the mechanisms that we support, as returned by * scram_get_mechanisms(). * * 'shadow_pass' is the role's stored secret, from pg_authid.rolpassword. * The username was provided by the client in the startup message, and is * available in port->user_name. If 'shadow_pass' is NULL, we still perform * an authentication exchange, but it will fail, as if an incorrect password * was given. */ static void * scram_init(Port *port, const char *selected_mech, const char *shadow_pass) { scram_state *state; bool got_secret; state = (scram_state *) palloc0(sizeof(scram_state)); state->port = port; state->state = SCRAM_AUTH_INIT; /* * Parse the selected mechanism. * * Note that if we don't support channel binding, either because the SSL * implementation doesn't support it or we're not using SSL at all, we * would not have advertised the PLUS variant in the first place. If the * client nevertheless tries to select it, it's a protocol violation like * selecting any other SASL mechanism we don't support. */ #ifdef HAVE_BE_TLS_GET_CERTIFICATE_HASH if (strcmp(selected_mech, SCRAM_SHA_256_PLUS_NAME) == 0 && port->ssl_in_use) state->channel_binding_in_use = true; else #endif if (strcmp(selected_mech, SCRAM_SHA_256_NAME) == 0) state->channel_binding_in_use = false; else ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("client selected an invalid SASL authentication mechanism"))); /* * Parse the stored secret. */ if (shadow_pass) { int password_type = get_password_type(shadow_pass); if (password_type == PASSWORD_TYPE_SCRAM_SHA_256) { if (parse_scram_secret(shadow_pass, &state->iterations, &state->hash_type, &state->key_length, &state->salt, state->StoredKey, state->ServerKey)) got_secret = true; else { /* * The password looked like a SCRAM secret, but could not be * parsed. */ ereport(LOG, (errmsg("invalid SCRAM secret for user \"%s\"", state->port->user_name))); got_secret = false; } } else { /* * The user doesn't have SCRAM secret. (You cannot do SCRAM * authentication with an MD5 hash.) */ state->logdetail = psprintf(_("User \"%s\" does not have a valid SCRAM secret."), state->port->user_name); got_secret = false; } } else { /* * The caller requested us to perform a dummy authentication. This is * considered normal, since the caller requested it, so don't set log * detail. */ got_secret = false; } /* * If the user did not have a valid SCRAM secret, we still go through the * motions with a mock one, and fail as if the client supplied an * incorrect password. This is to avoid revealing information to an * attacker. */ if (!got_secret) { mock_scram_secret(state->port->user_name, &state->hash_type, &state->iterations, &state->key_length, &state->salt, state->StoredKey, state->ServerKey); state->doomed = true; } return state; } /* * Continue a SCRAM authentication exchange. * * 'input' is the SCRAM payload sent by the client. On the first call, * 'input' contains the "Initial Client Response" that the client sent as * part of the SASLInitialResponse message, or NULL if no Initial Client * Response was given. (The SASL specification distinguishes between an * empty response and non-existing one.) On subsequent calls, 'input' * cannot be NULL. For convenience in this function, the caller must * ensure that there is a null terminator at input[inputlen]. * * 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). */ static int scram_exchange(void *opaq, const char *input, int inputlen, char **output, int *outputlen, const char **logdetail) { scram_state *state = (scram_state *) opaq; int result; *output = NULL; /* * If the client didn't include an "Initial Client Response" in the * SASLInitialResponse message, send an empty challenge, to which the * client will respond with the same data that usually comes in the * Initial Client Response. */ if (input == NULL) { Assert(state->state == SCRAM_AUTH_INIT); *output = pstrdup(""); *outputlen = 0; return PG_SASL_EXCHANGE_CONTINUE; } /* * 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"), errdetail("The message is empty."))); if (inputlen != strlen(input)) ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("malformed SCRAM message"), errdetail("Message length does not match input length."))); 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 = PG_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"), errdetail("Nonce does not match."))); /* * 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. * * The SCRAM specification includes an error code, * "invalid-proof", for authentication failure, but it also allows * erroring out in an application-specific way. We choose to do * the latter, so that the error message for invalid password is * the same for all authentication methods. The caller will call * ereport(), when we return PG_SASL_EXCHANGE_FAILURE with no * output. * * 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) { result = PG_SASL_EXCHANGE_FAILURE; break; } /* Build final message for client */ *output = build_server_final_message(state); /* Success! */ result = PG_SASL_EXCHANGE_SUCCESS; state->state = SCRAM_AUTH_FINISHED; break; default: elog(ERROR, "invalid SCRAM exchange state"); result = PG_SASL_EXCHANGE_FAILURE; } if (result == PG_SASL_EXCHANGE_FAILURE && state->logdetail && logdetail) *logdetail = state->logdetail; if (*output) *outputlen = strlen(*output); return result; } /* * Construct a SCRAM secret, for storing in pg_authid.rolpassword. * * The result is palloc'd, so caller is responsible for freeing it. */ char * pg_be_scram_build_secret(const char *password) { char *prep_password; pg_saslprep_rc rc; char saltbuf[SCRAM_DEFAULT_SALT_LEN]; char *result; const char *errstr = NULL; /* * Normalize the password with SASLprep. If that doesn't work, because * the password isn't valid UTF-8 or contains prohibited characters, just * proceed with the original password. (See comments at top of file.) */ rc = pg_saslprep(password, &prep_password); if (rc == SASLPREP_SUCCESS) password = (const char *) prep_password; /* Generate random salt */ if (!pg_strong_random(saltbuf, SCRAM_DEFAULT_SALT_LEN)) ereport(ERROR, (errcode(ERRCODE_INTERNAL_ERROR), errmsg("could not generate random salt"))); result = scram_build_secret(PG_SHA256, SCRAM_SHA_256_KEY_LEN, saltbuf, SCRAM_DEFAULT_SALT_LEN, scram_sha_256_iterations, password, &errstr); if (prep_password) pfree(prep_password); return result; } /* * Verify a plaintext password against a SCRAM secret. This is used when * performing plaintext password authentication for a user that has a SCRAM * secret stored in pg_authid. */ bool scram_verify_plain_password(const char *username, const char *password, const char *secret) { char *encoded_salt; char *salt; int saltlen; int iterations; int key_length = 0; pg_cryptohash_type hash_type; uint8 salted_password[SCRAM_MAX_KEY_LEN]; uint8 stored_key[SCRAM_MAX_KEY_LEN]; uint8 server_key[SCRAM_MAX_KEY_LEN]; uint8 computed_key[SCRAM_MAX_KEY_LEN]; char *prep_password; pg_saslprep_rc rc; const char *errstr = NULL; if (!parse_scram_secret(secret, &iterations, &hash_type, &key_length, &encoded_salt, stored_key, server_key)) { /* * The password looked like a SCRAM secret, but could not be parsed. */ ereport(LOG, (errmsg("invalid SCRAM secret for user \"%s\"", username))); return false; } saltlen = pg_b64_dec_len(strlen(encoded_salt)); salt = palloc(saltlen); saltlen = pg_b64_decode(encoded_salt, strlen(encoded_salt), salt, saltlen); if (saltlen < 0) { ereport(LOG, (errmsg("invalid SCRAM secret for user \"%s\"", username))); return false; } /* Normalize the password */ rc = pg_saslprep(password, &prep_password); if (rc == SASLPREP_SUCCESS) password = prep_password; /* Compute Server Key based on the user-supplied plaintext password */ if (scram_SaltedPassword(password, hash_type, key_length, salt, saltlen, iterations, salted_password, &errstr) < 0 || scram_ServerKey(salted_password, hash_type, key_length, computed_key, &errstr) < 0) { elog(ERROR, "could not compute server key: %s", errstr); } if (prep_password) pfree(prep_password); /* * Compare the secret's Server Key with the one computed from the * user-supplied password. */ return memcmp(computed_key, server_key, key_length) == 0; } /* * Parse and validate format of given SCRAM secret. * * On success, the iteration count, salt, stored key, and server key are * extracted from the secret, and returned to the caller. For 'stored_key' * and 'server_key', the caller must pass pre-allocated buffers of size * SCRAM_MAX_KEY_LEN. Salt is returned as a base64-encoded, null-terminated * string. The buffer for the salt is palloc'd by this function. * * Returns true if the SCRAM secret has been parsed, and false otherwise. */ bool parse_scram_secret(const char *secret, int *iterations, pg_cryptohash_type *hash_type, int *key_length, char **salt, uint8 *stored_key, uint8 *server_key) { char *v; char *p; char *scheme_str; char *salt_str; char *iterations_str; char *storedkey_str; char *serverkey_str; int decoded_len; char *decoded_salt_buf; char *decoded_stored_buf; char *decoded_server_buf; /* * The secret is of form: * * SCRAM-SHA-256$:$: */ v = pstrdup(secret); if ((scheme_str = strtok(v, "$")) == NULL) goto invalid_secret; if ((iterations_str = strtok(NULL, ":")) == NULL) goto invalid_secret; if ((salt_str = strtok(NULL, "$")) == NULL) goto invalid_secret; if ((storedkey_str = strtok(NULL, ":")) == NULL) goto invalid_secret; if ((serverkey_str = strtok(NULL, "")) == NULL) goto invalid_secret; /* Parse the fields */ if (strcmp(scheme_str, "SCRAM-SHA-256") != 0) goto invalid_secret; *hash_type = PG_SHA256; *key_length = SCRAM_SHA_256_KEY_LEN; errno = 0; *iterations = strtol(iterations_str, &p, 10); if (*p || errno != 0) goto invalid_secret; /* * Verify that the salt is in Base64-encoded format, by decoding it, * although we return the encoded version to the caller. */ decoded_len = pg_b64_dec_len(strlen(salt_str)); decoded_salt_buf = palloc(decoded_len); decoded_len = pg_b64_decode(salt_str, strlen(salt_str), decoded_salt_buf, decoded_len); if (decoded_len < 0) goto invalid_secret; *salt = pstrdup(salt_str); /* * Decode StoredKey and ServerKey. */ decoded_len = pg_b64_dec_len(strlen(storedkey_str)); decoded_stored_buf = palloc(decoded_len); decoded_len = pg_b64_decode(storedkey_str, strlen(storedkey_str), decoded_stored_buf, decoded_len); if (decoded_len != *key_length) goto invalid_secret; memcpy(stored_key, decoded_stored_buf, *key_length); decoded_len = pg_b64_dec_len(strlen(serverkey_str)); decoded_server_buf = palloc(decoded_len); decoded_len = pg_b64_decode(serverkey_str, strlen(serverkey_str), decoded_server_buf, decoded_len); if (decoded_len != *key_length) goto invalid_secret; memcpy(server_key, decoded_server_buf, *key_length); return true; invalid_secret: *salt = NULL; return false; } /* * Generate plausible SCRAM secret parameters for mock authentication. * * In a normal authentication, these are extracted from the secret * stored in the server. This function generates values that look * realistic, for when there is no stored secret, using SCRAM-SHA-256. * * Like in parse_scram_secret(), for 'stored_key' and 'server_key', the * caller must pass pre-allocated buffers of size SCRAM_MAX_KEY_LEN, and * the buffer for the salt is palloc'd by this function. */ static void mock_scram_secret(const char *username, pg_cryptohash_type *hash_type, int *iterations, int *key_length, char **salt, uint8 *stored_key, uint8 *server_key) { char *raw_salt; char *encoded_salt; int encoded_len; /* Enforce the use of SHA-256, which would be realistic enough */ *hash_type = PG_SHA256; *key_length = SCRAM_SHA_256_KEY_LEN; /* * Generate deterministic salt. * * Note that we cannot reveal any information to an attacker here so the * error messages need to remain generic. This should never fail anyway * as the salt generated for mock authentication uses the cluster's nonce * value. */ raw_salt = scram_mock_salt(username, *hash_type, *key_length); if (raw_salt == NULL) elog(ERROR, "could not encode salt"); encoded_len = pg_b64_enc_len(SCRAM_DEFAULT_SALT_LEN); /* don't forget the zero-terminator */ encoded_salt = (char *) palloc(encoded_len + 1); encoded_len = pg_b64_encode(raw_salt, SCRAM_DEFAULT_SALT_LEN, encoded_salt, encoded_len); if (encoded_len < 0) elog(ERROR, "could not encode salt"); encoded_salt[encoded_len] = '\0'; *salt = encoded_salt; *iterations = SCRAM_SHA_256_DEFAULT_ITERATIONS; /* StoredKey and ServerKey are not used in a doomed authentication */ memset(stored_key, 0, SCRAM_MAX_KEY_LEN); memset(server_key, 0, SCRAM_MAX_KEY_LEN); } /* * Read the value in a given SCRAM 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"), errdetail("Expected attribute \"%c\" but found \"%s\".", attr, sanitize_char(*begin)))); begin++; if (*begin != '=') ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("malformed SCRAM message"), errdetail("Expected character \"=\" for attribute \"%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", (unsigned char) c); return buf; } /* * Convert an arbitrary string to printable form, for error messages. * * Anything that's not a printable ASCII character is replaced with * '?', and the string is truncated at 30 characters. * * The returned pointer points to a static buffer. */ static char * sanitize_str(const char *s) { static char buf[30 + 1]; int i; for (i = 0; i < sizeof(buf) - 1; i++) { char c = s[i]; if (c == '\0') break; if (c >= 0x21 && c <= 0x7E) buf[i] = c; else buf[i] = '?'; } buf[i] = '\0'; return buf; } /* * Read the next attribute and value in a SCRAM exchange message. * * The attribute character is set in *attr_p, the attribute value is the * return value. */ static char * read_any_attr(char **input, char *attr_p) { char *begin = *input; char *end; char attr = *begin; if (attr == '\0') ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("malformed SCRAM message"), errdetail("Attribute expected, but found end of string."))); /*------ * 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"), errdetail("Attribute expected, but found invalid character \"%s\".", sanitize_char(attr)))); if (attr_p) *attr_p = attr; begin++; if (*begin != '=') ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("malformed SCRAM message"), errdetail("Expected character \"=\" for attribute \"%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 SCRAM * authentication exchange message. * * At this stage, any errors will be reported directly with ereport(ERROR). */ static void read_client_first_message(scram_state *state, const char *input) { char *p = pstrdup(input); char *channel_binding_type; /*------ * The syntax for the client-first-message is: (RFC 5802) * * saslname = 1*(value-safe-char / "=2C" / "=3D") * ;; Conforms to . * * 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. (For details see also RFC 5802 Section 6 "Channel * Binding".) */ state->cbind_flag = *p; switch (*p) { case 'n': /* * The client does not support channel binding or has simply * decided to not use it. In that case just let it go. */ if (state->channel_binding_in_use) ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("malformed SCRAM message"), errdetail("The client selected SCRAM-SHA-256-PLUS, but the SCRAM message does not include channel binding data."))); p++; if (*p != ',') ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("malformed SCRAM message"), errdetail("Comma expected, but found character \"%s\".", sanitize_char(*p)))); p++; break; case 'y': /* * The client supports channel binding and thinks that the server * does not. In this case, the server must fail authentication if * it supports channel binding. */ if (state->channel_binding_in_use) ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("malformed SCRAM message"), errdetail("The client selected SCRAM-SHA-256-PLUS, but the SCRAM message does not include channel binding data."))); #ifdef HAVE_BE_TLS_GET_CERTIFICATE_HASH if (state->port->ssl_in_use) ereport(ERROR, (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), errmsg("SCRAM channel binding negotiation error"), errdetail("The client supports SCRAM channel binding but thinks the server does not. " "However, this server does support channel binding."))); #endif p++; if (*p != ',') ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("malformed SCRAM message"), errdetail("Comma expected, but found character \"%s\".", sanitize_char(*p)))); p++; break; case 'p': /* * The client requires channel binding. Channel binding type * follows, e.g., "p=tls-server-end-point". */ if (!state->channel_binding_in_use) ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("malformed SCRAM message"), errdetail("The client selected SCRAM-SHA-256 without channel binding, but the SCRAM message includes channel binding data."))); channel_binding_type = read_attr_value(&p, 'p'); /* * The only channel binding type we support is * tls-server-end-point. */ if (strcmp(channel_binding_type, "tls-server-end-point") != 0) ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("unsupported SCRAM channel-binding type \"%s\"", sanitize_str(channel_binding_type)))); break; default: ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("malformed SCRAM message"), errdetail("Unexpected channel-binding flag \"%s\".", sanitize_char(*p)))); } /* * Forbid optional authzid (authorization identity). We don't support it. */ if (*p == 'a') ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("client uses authorization identity, but it is not supported"))); if (*p != ',') ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("malformed SCRAM message"), errdetail("Unexpected attribute \"%s\" in client-first-message.", sanitize_char(*p)))); p++; state->client_first_message_bare = pstrdup(p); /* * 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 (*p == 'm') ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("client requires an unsupported 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(&p, 'n'); /* read nonce and check that it is made of only printable characters */ state->client_nonce = read_attr_value(&p, '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 (*p != '\0') read_any_attr(&p, 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. Returns true if the verification is a success, * or false for a failure. */ static bool verify_client_proof(scram_state *state) { uint8 ClientSignature[SCRAM_MAX_KEY_LEN]; uint8 ClientKey[SCRAM_MAX_KEY_LEN]; uint8 client_StoredKey[SCRAM_MAX_KEY_LEN]; pg_hmac_ctx *ctx = pg_hmac_create(state->hash_type); int i; const char *errstr = NULL; /* * Calculate ClientSignature. Note that we don't log directly a failure * here even when processing the calculations as this could involve a mock * authentication. */ if (pg_hmac_init(ctx, state->StoredKey, state->key_length) < 0 || pg_hmac_update(ctx, (uint8 *) state->client_first_message_bare, strlen(state->client_first_message_bare)) < 0 || pg_hmac_update(ctx, (uint8 *) ",", 1) < 0 || pg_hmac_update(ctx, (uint8 *) state->server_first_message, strlen(state->server_first_message)) < 0 || pg_hmac_update(ctx, (uint8 *) ",", 1) < 0 || pg_hmac_update(ctx, (uint8 *) state->client_final_message_without_proof, strlen(state->client_final_message_without_proof)) < 0 || pg_hmac_final(ctx, ClientSignature, state->key_length) < 0) { elog(ERROR, "could not calculate client signature: %s", pg_hmac_error(ctx)); } pg_hmac_free(ctx); /* Extract the ClientKey that the client calculated from the proof */ for (i = 0; i < state->key_length; i++) ClientKey[i] = state->ClientProof[i] ^ ClientSignature[i]; /* Hash it one more time, and compare with StoredKey */ if (scram_H(ClientKey, state->hash_type, state->key_length, client_StoredKey, &errstr) < 0) elog(ERROR, "could not hash stored key: %s", errstr); if (memcmp(client_StoredKey, state->StoredKey, state->key_length) != 0) return false; return true; } /* * Build the first server-side message sent to the client in a SCRAM * 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_strong_random(raw_nonce, SCRAM_RAW_NONCE_LEN)) ereport(ERROR, (errcode(ERRCODE_INTERNAL_ERROR), errmsg("could not generate random nonce"))); encoded_len = pg_b64_enc_len(SCRAM_RAW_NONCE_LEN); /* don't forget the zero-terminator */ state->server_nonce = palloc(encoded_len + 1); encoded_len = pg_b64_encode(raw_nonce, SCRAM_RAW_NONCE_LEN, state->server_nonce, encoded_len); if (encoded_len < 0) ereport(ERROR, (errcode(ERRCODE_INTERNAL_ERROR), errmsg("could not encode random nonce"))); state->server_nonce[encoded_len] = '\0'; state->server_first_message = psprintf("r=%s%s,s=%s,i=%d", state->client_nonce, state->server_nonce, state->salt, state->iterations); return pstrdup(state->server_first_message); } /* * Read and parse the final message received from client. */ static void read_client_final_message(scram_state *state, const char *input) { char attr; char *channel_binding; char *value; char *begin, *proof; char *p; char *client_proof; int client_proof_len; 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. This repeats the channel-binding flags and is * then followed by the actual binding data depending on the type. */ channel_binding = read_attr_value(&p, 'c'); if (state->channel_binding_in_use) { #ifdef HAVE_BE_TLS_GET_CERTIFICATE_HASH const char *cbind_data = NULL; size_t cbind_data_len = 0; size_t cbind_header_len; char *cbind_input; size_t cbind_input_len; char *b64_message; int b64_message_len; Assert(state->cbind_flag == 'p'); /* Fetch hash data of server's SSL certificate */ cbind_data = be_tls_get_certificate_hash(state->port, &cbind_data_len); /* should not happen */ if (cbind_data == NULL || cbind_data_len == 0) elog(ERROR, "could not get server certificate hash"); cbind_header_len = strlen("p=tls-server-end-point,,"); /* p=type,, */ cbind_input_len = cbind_header_len + cbind_data_len; cbind_input = palloc(cbind_input_len); snprintf(cbind_input, cbind_input_len, "p=tls-server-end-point,,"); memcpy(cbind_input + cbind_header_len, cbind_data, cbind_data_len); b64_message_len = pg_b64_enc_len(cbind_input_len); /* don't forget the zero-terminator */ b64_message = palloc(b64_message_len + 1); b64_message_len = pg_b64_encode(cbind_input, cbind_input_len, b64_message, b64_message_len); if (b64_message_len < 0) elog(ERROR, "could not encode channel binding data"); b64_message[b64_message_len] = '\0'; /* * Compare the value sent by the client with the value expected by the * server. */ if (strcmp(channel_binding, b64_message) != 0) ereport(ERROR, (errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION), errmsg("SCRAM channel binding check failed"))); #else /* shouldn't happen, because we checked this earlier already */ elog(ERROR, "channel binding not supported by this build"); #endif } else { /* * If we are not using channel binding, the binding data is expected * to always be "biws", which is "n,," base64-encoded, or "eSws", * which is "y,,". We also have to check whether the flag is the same * one that the client originally sent. */ if (!(strcmp(channel_binding, "biws") == 0 && state->cbind_flag == 'n') && !(strcmp(channel_binding, "eSws") == 0 && state->cbind_flag == 'y')) 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, read until we find "p" attribute */ do { proof = p - 1; value = read_any_attr(&p, &attr); } while (attr != 'p'); client_proof_len = pg_b64_dec_len(strlen(value)); client_proof = palloc(client_proof_len); if (pg_b64_decode(value, strlen(value), client_proof, client_proof_len) != state->key_length) ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("malformed SCRAM message"), errdetail("Malformed proof in client-final-message."))); memcpy(state->ClientProof, client_proof, state->key_length); pfree(client_proof); if (*p != '\0') ereport(ERROR, (errcode(ERRCODE_PROTOCOL_VIOLATION), errmsg("malformed SCRAM message"), errdetail("Garbage found at the 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_MAX_KEY_LEN]; char *server_signature_base64; int siglen; pg_hmac_ctx *ctx = pg_hmac_create(state->hash_type); /* calculate ServerSignature */ if (pg_hmac_init(ctx, state->ServerKey, state->key_length) < 0 || pg_hmac_update(ctx, (uint8 *) state->client_first_message_bare, strlen(state->client_first_message_bare)) < 0 || pg_hmac_update(ctx, (uint8 *) ",", 1) < 0 || pg_hmac_update(ctx, (uint8 *) state->server_first_message, strlen(state->server_first_message)) < 0 || pg_hmac_update(ctx, (uint8 *) ",", 1) < 0 || pg_hmac_update(ctx, (uint8 *) state->client_final_message_without_proof, strlen(state->client_final_message_without_proof)) < 0 || pg_hmac_final(ctx, ServerSignature, state->key_length) < 0) { elog(ERROR, "could not calculate server signature: %s", pg_hmac_error(ctx)); } pg_hmac_free(ctx); siglen = pg_b64_enc_len(state->key_length); /* don't forget the zero-terminator */ server_signature_base64 = palloc(siglen + 1); siglen = pg_b64_encode((const char *) ServerSignature, state->key_length, server_signature_base64, siglen); if (siglen < 0) elog(ERROR, "could not encode server signature"); 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); } /* * Deterministically 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_DEFAULT_SALT_LEN, or NULL. */ static char * scram_mock_salt(const char *username, pg_cryptohash_type hash_type, int key_length) { pg_cryptohash_ctx *ctx; static uint8 sha_digest[SCRAM_MAX_KEY_LEN]; 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 than the SHA256 digest length. If the salt is smaller, the * caller will just ignore the extra data.) */ StaticAssertDecl(PG_SHA256_DIGEST_LENGTH >= SCRAM_DEFAULT_SALT_LEN, "salt length greater than SHA256 digest length"); /* * This may be worth refreshing if support for more hash methods is\ * added. */ Assert(hash_type == PG_SHA256); ctx = pg_cryptohash_create(hash_type); if (pg_cryptohash_init(ctx) < 0 || pg_cryptohash_update(ctx, (uint8 *) username, strlen(username)) < 0 || pg_cryptohash_update(ctx, (uint8 *) mock_auth_nonce, MOCK_AUTH_NONCE_LEN) < 0 || pg_cryptohash_final(ctx, sha_digest, key_length) < 0) { pg_cryptohash_free(ctx); return NULL; } pg_cryptohash_free(ctx); return (char *) sha_digest; }