postgresql/src/backend/libpq/auth-scram.c

1033 lines
29 KiB
C
Raw Normal View History

Support SCRAM-SHA-256 authentication (RFC 5802 and 7677). This introduces a new generic SASL authentication method, similar to the GSS and SSPI methods. The server first tells the client which SASL authentication mechanism to use, and then the mechanism-specific SASL messages are exchanged in AuthenticationSASLcontinue and PasswordMessage messages. Only SCRAM-SHA-256 is supported at the moment, but this allows adding more SASL mechanisms in the future, without changing the overall protocol. Support for channel binding, aka SCRAM-SHA-256-PLUS is left for later. The SASLPrep algorithm, for pre-processing the password, is not yet implemented. That could cause trouble, if you use a password with non-ASCII characters, and a client library that does implement SASLprep. That will hopefully be added later. Authorization identities, as specified in the SCRAM-SHA-256 specification, are ignored. SET SESSION AUTHORIZATION provides more or less the same functionality, anyway. If a user doesn't exist, perform a "mock" authentication, by constructing an authentic-looking challenge on the fly. The challenge is derived from a new system-wide random value, "mock authentication nonce", which is created at initdb, and stored in the control file. We go through these motions, in order to not give away the information on whether the user exists, to unauthenticated users. Bumps PG_CONTROL_VERSION, because of the new field in control file. Patch by Michael Paquier and Heikki Linnakangas, reviewed at different stages by Robert Haas, Stephen Frost, David Steele, Aleksander Alekseev, and many others. Discussion: https://www.postgresql.org/message-id/CAB7nPqRbR3GmFYdedCAhzukfKrgBLTLtMvENOmPrVWREsZkF8g%40mail.gmail.com Discussion: https://www.postgresql.org/message-id/CAB7nPqSMXU35g%3DW9X74HVeQp0uvgJxvYOuA4A-A3M%2B0wfEBv-w%40mail.gmail.com Discussion: https://www.postgresql.org/message-id/55192AFE.6080106@iki.fi
2017-03-07 13:25:40 +01:00
/*-------------------------------------------------------------------------
*
* 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;
}