mirror of
https://github.com/postgres/postgres.git
synced 2025-04-27 22:56:53 +03:00
The proposed OAUTHBEARER SASL mechanism will need to allow larger messages in the exchange, since tokens are sent directly by the client. Move this limit into the pg_be_sasl_mech struct so that it can be changed per-mechanism. Author: Jacob Champion <jacob.champion@enterprisedb.com> Reviewed-by: Daniel Gustafsson <daniel@yesql.se> Discussion: https://postgr.es/m/CAOYmi+nqX_5=Se0W0Ynrr55Fha3CMzwv_R9P3rkpHb=1kG7ZTQ@mail.gmail.com
1498 lines
45 KiB
C
1498 lines
45 KiB
C
/*-------------------------------------------------------------------------
|
|
*
|
|
* 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-2024, 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_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/crypt.h"
|
|
#include "libpq/sasl.h"
|
|
#include "libpq/scram.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,
|
|
|
|
PG_MAX_SASL_MESSAGE_LENGTH
|
|
};
|
|
|
|
/*
|
|
* 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.
|
|
*/
|
|
#ifdef USE_SSL
|
|
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, or if 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 USE_SSL
|
|
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$<iterations>:<salt>$<storedkey>:<serverkey>
|
|
*/
|
|
v = pstrdup(secret);
|
|
scheme_str = strsep(&v, "$");
|
|
if (v == NULL)
|
|
goto invalid_secret;
|
|
iterations_str = strsep(&v, ":");
|
|
if (v == NULL)
|
|
goto invalid_secret;
|
|
salt_str = strsep(&v, "$");
|
|
if (v == NULL)
|
|
goto invalid_secret;
|
|
storedkey_str = strsep(&v, ":");
|
|
if (v == NULL)
|
|
goto invalid_secret;
|
|
serverkey_str = v;
|
|
|
|
/* 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 <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. (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 USE_SSL
|
|
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 USE_SSL
|
|
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;
|
|
}
|