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

1033 lines
29 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 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;
}