Commit 9288d62b authored by Peter Eisentraut's avatar Peter Eisentraut

Support channel binding 'tls-unique' in SCRAM

This is the basic feature set using OpenSSL to support the feature.  In
order to allow the frontend and the backend to fetch the sent and
expected TLS Finished messages, a PG-like API is added to be able to
make the interface pluggable for other SSL implementations.

This commit also adds a infrastructure to facilitate the addition of
future channel binding types as well as libpq parameters to control the
SASL mechanism names and channel binding names.  Those will be added by
upcoming commits.

Some tests are added to the SSL test suite to test SCRAM authentication
with channel binding.

Author: Michael Paquier <michael@paquier.xyz>
Reviewed-by: default avatarPeter Eisentraut <peter.eisentraut@2ndquadrant.com>
parent 611fe7d4
...@@ -1461,10 +1461,11 @@ SELCT 1/0; ...@@ -1461,10 +1461,11 @@ SELCT 1/0;
<para> <para>
<firstterm>SASL</firstterm> is a framework for authentication in connection-oriented <firstterm>SASL</firstterm> is a framework for authentication in connection-oriented
protocols. At the moment, <productname>PostgreSQL</productname> implements only one SASL protocols. At the moment, <productname>PostgreSQL</productname> implements two SASL
authentication mechanism, SCRAM-SHA-256, but more might be added in the authentication mechanisms, SCRAM-SHA-256 and SCRAM-SHA-256-PLUS. More
future. The below steps illustrate how SASL authentication is performed in might be added in the future. The below steps illustrate how SASL
general, while the next subsection gives more details on SCRAM-SHA-256. authentication is performed in general, while the next subsection gives
more details on SCRAM-SHA-256 and SCRAM-SHA-256-PLUS.
</para> </para>
<procedure> <procedure>
...@@ -1518,9 +1519,10 @@ ErrorMessage. ...@@ -1518,9 +1519,10 @@ ErrorMessage.
<title>SCRAM-SHA-256 authentication</title> <title>SCRAM-SHA-256 authentication</title>
<para> <para>
<firstterm>SCRAM-SHA-256</firstterm> (called just <firstterm>SCRAM</firstterm> from now on) is The implemented SASL mechanisms at the moment
the only implemented SASL mechanism, at the moment. It is described in detail are <literal>SCRAM-SHA-256</literal> and its variant with channel
in RFC 7677 and RFC 5802. binding <literal>SCRAM-SHA-256-PLUS</literal>. They are described in
detail in RFC 7677 and RFC 5802.
</para> </para>
<para> <para>
...@@ -1547,7 +1549,10 @@ the password is in. ...@@ -1547,7 +1549,10 @@ the password is in.
</para> </para>
<para> <para>
<firstterm>Channel binding</firstterm> has not been implemented yet. <firstterm>Channel binding</firstterm> is supported in PostgreSQL builds with
SSL support. The SASL mechanism name for SCRAM with channel binding
is <literal>SCRAM-SHA-256-PLUS</literal>. The only channel binding type
supported at the moment is <literal>tls-unique</literal>, defined in RFC 5929.
</para> </para>
<procedure> <procedure>
...@@ -1556,13 +1561,19 @@ the password is in. ...@@ -1556,13 +1561,19 @@ the password is in.
<para> <para>
The server sends an AuthenticationSASL message. It includes a list of The server sends an AuthenticationSASL message. It includes a list of
SASL authentication mechanisms that the server can accept. SASL authentication mechanisms that the server can accept.
This will be <literal>SCRAM-SHA-256-PLUS</literal>
and <literal>SCRAM-SHA-256</literal> if the server is built with SSL
support, or else just the latter.
</para> </para>
</step> </step>
<step id="scram-client-first"> <step id="scram-client-first">
<para> <para>
The client responds by sending a SASLInitialResponse message, which The client responds by sending a SASLInitialResponse message, which
indicates the chosen mechanism, <literal>SCRAM-SHA-256</literal>. In the Initial indicates the chosen mechanism, <literal>SCRAM-SHA-256</literal> or
Client response field, the message contains the SCRAM <literal>SCRAM-SHA-256-PLUS</literal>. (A client is free to choose either
mechanism, but for better security it should choose the channel-binding
variant if it can support it.) In the Initial Client response field,
the message contains the SCRAM
<structname>client-first-message</structname>. <structname>client-first-message</structname>.
</para> </para>
</step> </step>
......
...@@ -17,8 +17,6 @@ ...@@ -17,8 +17,6 @@
* by the SASLprep profile, we skip the SASLprep pre-processing and use * by the SASLprep profile, we skip the SASLprep pre-processing and use
* the raw bytes in calculating the hash. * the raw bytes in calculating the hash.
* *
* - Channel binding is not supported yet.
*
* *
* The password stored in pg_authid consists of the iteration count, salt, * The password stored in pg_authid consists of the iteration count, salt,
* StoredKey and ServerKey. * StoredKey and ServerKey.
...@@ -112,6 +110,11 @@ typedef struct ...@@ -112,6 +110,11 @@ typedef struct
const char *username; /* username from startup packet */ const char *username; /* username from startup packet */
bool ssl_in_use;
const char *tls_finished_message;
size_t tls_finished_len;
char *channel_binding_type;
int iterations; int iterations;
char *salt; /* base64-encoded */ char *salt; /* base64-encoded */
uint8 StoredKey[SCRAM_KEY_LEN]; uint8 StoredKey[SCRAM_KEY_LEN];
...@@ -168,7 +171,11 @@ static char *scram_mock_salt(const char *username); ...@@ -168,7 +171,11 @@ static char *scram_mock_salt(const char *username);
* it will fail, as if an incorrect password was given. * it will fail, as if an incorrect password was given.
*/ */
void * void *
pg_be_scram_init(const char *username, const char *shadow_pass) pg_be_scram_init(const char *username,
const char *shadow_pass,
bool ssl_in_use,
const char *tls_finished_message,
size_t tls_finished_len)
{ {
scram_state *state; scram_state *state;
bool got_verifier; bool got_verifier;
...@@ -176,6 +183,10 @@ pg_be_scram_init(const char *username, const char *shadow_pass) ...@@ -176,6 +183,10 @@ pg_be_scram_init(const char *username, const char *shadow_pass)
state = (scram_state *) palloc0(sizeof(scram_state)); state = (scram_state *) palloc0(sizeof(scram_state));
state->state = SCRAM_AUTH_INIT; state->state = SCRAM_AUTH_INIT;
state->username = username; state->username = username;
state->ssl_in_use = ssl_in_use;
state->tls_finished_message = tls_finished_message;
state->tls_finished_len = tls_finished_len;
state->channel_binding_type = NULL;
/* /*
* Parse the stored password verifier. * Parse the stored password verifier.
...@@ -773,45 +784,96 @@ read_client_first_message(scram_state *state, char *input) ...@@ -773,45 +784,96 @@ read_client_first_message(scram_state *state, char *input)
*------ *------
*/ */
/* read gs2-cbind-flag */ /*
* Read gs2-cbind-flag. (For details see also RFC 5802 Section 6 "Channel
* Binding".)
*/
switch (*input) switch (*input)
{ {
case 'n': case 'n':
/* Client does not support channel binding */ /*
* The client does not support channel binding or has simply
* decided to not use it. In that case just let it go.
*/
input++;
if (*input != ',')
ereport(ERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION),
errmsg("malformed SCRAM message"),
errdetail("Comma expected, but found character \"%s\".",
sanitize_char(*input))));
input++; input++;
break; break;
case 'y': case 'y':
/* Client supports channel binding, but we're not doing it today */ /*
* 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, which in this implementation is
* the case if a connection is using SSL.
*/
if (state->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.")));
input++;
if (*input != ',')
ereport(ERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION),
errmsg("malformed SCRAM message"),
errdetail("Comma expected, but found character \"%s\".",
sanitize_char(*input))));
input++; input++;
break; break;
case 'p': case 'p':
/*
* The client requires channel binding. Channel binding type
* follows, e.g., "p=tls-unique".
*/
{
char *channel_binding_type;
if (!state->ssl_in_use)
{
/* /*
* Client requires channel binding. We don't support it. * Without SSL, we don't support channel binding.
* *
* RFC 5802 specifies a particular error code, * RFC 5802 specifies a particular error code,
* e=server-does-support-channel-binding, for this. But it can * e=server-does-support-channel-binding, for this. But
* only be sent in the server-final message, and we don't want to * it can only be sent in the server-final message, and we
* go through the motions of the authentication, knowing it will * don't want to go through the motions of the
* fail, just to send that error message. * authentication, knowing it will fail, just to send that
* error message.
*/ */
ereport(ERROR, ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED), (errcode(ERRCODE_PROTOCOL_VIOLATION),
errmsg("client requires SCRAM channel binding, but it is not supported"))); errmsg("client requires SCRAM channel binding, but it is not supported")));
default: }
/*
* Read value provided by client; only tls-unique is supported
* for now. (It is not safe to print the name of an
* unsupported binding type in the error message. Pranksters
* could print arbitrary strings into the log that way.)
*/
channel_binding_type = read_attr_value(&input, 'p');
if (strcmp(channel_binding_type, SCRAM_CHANNEL_BINDING_TLS_UNIQUE) != 0)
ereport(ERROR, ereport(ERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION), (errcode(ERRCODE_PROTOCOL_VIOLATION),
errmsg("malformed SCRAM message"), (errmsg("unsupported SCRAM channel-binding type"))));
errdetail("Unexpected channel-binding flag \"%s\".",
sanitize_char(*input)))); /* Save the name for handling of subsequent messages */
state->channel_binding_type = pstrdup(channel_binding_type);
} }
if (*input != ',') break;
default:
ereport(ERROR, ereport(ERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION), (errcode(ERRCODE_PROTOCOL_VIOLATION),
errmsg("malformed SCRAM message"), errmsg("malformed SCRAM message"),
errdetail("Comma expected, but found character \"%s\".", errdetail("Unexpected channel-binding flag \"%s\".",
sanitize_char(*input)))); sanitize_char(*input))));
input++; }
/* /*
* Forbid optional authzid (authorization identity). We don't support it. * Forbid optional authzid (authorization identity). We don't support it.
...@@ -1032,14 +1094,73 @@ read_client_final_message(scram_state *state, char *input) ...@@ -1032,14 +1094,73 @@ read_client_final_message(scram_state *state, char *input)
*/ */
/* /*
* Read channel-binding. We don't support channel binding, so it's * Read channel binding. This repeats the channel-binding flags and is
* expected to always be "biws", which is "n,,", base64-encoded. * then followed by the actual binding data depending on the type.
*/ */
channel_binding = read_attr_value(&p, 'c'); channel_binding = read_attr_value(&p, 'c');
if (strcmp(channel_binding, "biws") != 0) if (state->channel_binding_type)
{
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;
/*
* Fetch data appropriate for channel binding type
*/
if (strcmp(state->channel_binding_type, SCRAM_CHANNEL_BINDING_TLS_UNIQUE) == 0)
{
cbind_data = state->tls_finished_message;
cbind_data_len = state->tls_finished_len;
}
else
{
/* should not happen */
elog(ERROR, "invalid channel binding type");
}
/* should not happen */
if (cbind_data == NULL || cbind_data_len == 0)
elog(ERROR, "empty channel binding data for channel binding type \"%s\"",
state->channel_binding_type);
cbind_header_len = 4 + strlen(state->channel_binding_type); /* p=type,, */
cbind_input_len = cbind_header_len + cbind_data_len;
cbind_input = palloc(cbind_input_len);
snprintf(cbind_input, cbind_input_len, "p=%s,,", state->channel_binding_type);
memcpy(cbind_input + cbind_header_len, cbind_data, cbind_data_len);
b64_message = palloc(pg_b64_enc_len(cbind_input_len) + 1);
b64_message_len = pg_b64_encode(cbind_input, cbind_input_len,
b64_message);
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
{
/*
* 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,,".
*/
if (strcmp(channel_binding, "biws") != 0 &&
strcmp(channel_binding, "eSws") != 0)
ereport(ERROR, ereport(ERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION), (errcode(ERRCODE_PROTOCOL_VIOLATION),
(errmsg("unexpected SCRAM channel-binding attribute in client-final-message")))); (errmsg("unexpected SCRAM channel-binding attribute in client-final-message"))));
}
state->client_final_nonce = read_attr_value(&p, 'r'); state->client_final_nonce = read_attr_value(&p, 'r');
/* ignore optional extensions */ /* ignore optional extensions */
......
...@@ -860,6 +860,8 @@ CheckMD5Auth(Port *port, char *shadow_pass, char **logdetail) ...@@ -860,6 +860,8 @@ CheckMD5Auth(Port *port, char *shadow_pass, char **logdetail)
static int static int
CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail) CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
{ {
char *sasl_mechs;
char *p;
int mtype; int mtype;
StringInfoData buf; StringInfoData buf;
void *scram_opaq; void *scram_opaq;
...@@ -869,6 +871,8 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail) ...@@ -869,6 +871,8 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
int inputlen; int inputlen;
int result; int result;
bool initial; bool initial;
char *tls_finished = NULL;
size_t tls_finished_len = 0;
/* /*
* SASL auth is not supported for protocol versions before 3, because it * SASL auth is not supported for protocol versions before 3, because it
...@@ -885,12 +889,39 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail) ...@@ -885,12 +889,39 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
/* /*
* Send the SASL authentication request to user. It includes the list of * Send the SASL authentication request to user. It includes the list of
* authentication mechanisms (which is trivial, because we only support * authentication mechanisms that are supported. The order of mechanisms
* SCRAM-SHA-256 at the moment). The extra "\0" is for an empty string to * is advertised in decreasing order of importance. So the
* terminate the list. * channel-binding variants go first, if they are supported. Channel
* binding is only supported in SSL builds.
*/ */
sendAuthRequest(port, AUTH_REQ_SASL, SCRAM_SHA256_NAME "\0", sasl_mechs = palloc(strlen(SCRAM_SHA256_PLUS_NAME) +
strlen(SCRAM_SHA256_NAME) + 2); strlen(SCRAM_SHA256_NAME) + 3);
p = sasl_mechs;
if (port->ssl_in_use)
{
strcpy(p, SCRAM_SHA256_PLUS_NAME);
p += strlen(SCRAM_SHA256_PLUS_NAME) + 1;
}
strcpy(p, SCRAM_SHA256_NAME);
p += strlen(SCRAM_SHA256_NAME) + 1;
/* Put another '\0' to mark that list is finished. */
p[0] = '\0';
sendAuthRequest(port, AUTH_REQ_SASL, sasl_mechs, p - sasl_mechs + 1);
pfree(sasl_mechs);
#ifdef USE_SSL
/*
* Get data for channel binding.
*/
if (port->ssl_in_use)
{
tls_finished = be_tls_get_peer_finished(port, &tls_finished_len);
}
#endif
/* /*
* Initialize the status tracker for message exchanges. * Initialize the status tracker for message exchanges.
...@@ -903,7 +934,11 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail) ...@@ -903,7 +934,11 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
* This is because we don't want to reveal to an attacker what usernames * This is because we don't want to reveal to an attacker what usernames
* are valid, nor which users have a valid password. * are valid, nor which users have a valid password.
*/ */
scram_opaq = pg_be_scram_init(port->user_name, shadow_pass); scram_opaq = pg_be_scram_init(port->user_name,
shadow_pass,
port->ssl_in_use,
tls_finished,
tls_finished_len);
/* /*
* Loop through SASL message exchange. This exchange can consist of * Loop through SASL message exchange. This exchange can consist of
...@@ -951,12 +986,9 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail) ...@@ -951,12 +986,9 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
{ {
const char *selected_mech; const char *selected_mech;
/*
* We only support SCRAM-SHA-256 at the moment, so anything else
* is an error.
*/
selected_mech = pq_getmsgrawstring(&buf); selected_mech = pq_getmsgrawstring(&buf);
if (strcmp(selected_mech, SCRAM_SHA256_NAME) != 0) if (strcmp(selected_mech, SCRAM_SHA256_NAME) != 0 &&
strcmp(selected_mech, SCRAM_SHA256_PLUS_NAME) != 0)
{ {
ereport(ERROR, ereport(ERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION), (errcode(ERRCODE_PROTOCOL_VIOLATION),
......
...@@ -1215,6 +1215,30 @@ be_tls_get_peerdn_name(Port *port, char *ptr, size_t len) ...@@ -1215,6 +1215,30 @@ be_tls_get_peerdn_name(Port *port, char *ptr, size_t len)
ptr[0] = '\0'; ptr[0] = '\0';
} }
/*
* Routine to get the expected TLS Finished message information from the
* client, useful for authorization when doing channel binding.
*
* Result is a palloc'd copy of the TLS Finished message with its size.
*/
char *
be_tls_get_peer_finished(Port *port, size_t *len)
{
char dummy[1];
char *result;
/*
* OpenSSL does not offer an API to directly get the length of the
* expected TLS Finished message, so just do a dummy call to grab this
* information to allow caller to do an allocation with a correct size.
*/
*len = SSL_get_peer_finished(port->ssl, dummy, sizeof(dummy));
result = palloc(*len);
(void) SSL_get_peer_finished(port->ssl, result, *len);
return result;
}
/* /*
* Convert an X509 subject name to a cstring. * Convert an X509 subject name to a cstring.
* *
......
...@@ -209,6 +209,7 @@ extern bool be_tls_get_compression(Port *port); ...@@ -209,6 +209,7 @@ extern bool be_tls_get_compression(Port *port);
extern void be_tls_get_version(Port *port, char *ptr, size_t len); extern void be_tls_get_version(Port *port, char *ptr, size_t len);
extern void be_tls_get_cipher(Port *port, char *ptr, size_t len); extern void be_tls_get_cipher(Port *port, char *ptr, size_t len);
extern void be_tls_get_peerdn_name(Port *port, char *ptr, size_t len); extern void be_tls_get_peerdn_name(Port *port, char *ptr, size_t len);
extern char *be_tls_get_peer_finished(Port *port, size_t *len);
#endif #endif
extern ProtocolVersion FrontendProtocol; extern ProtocolVersion FrontendProtocol;
......
...@@ -13,8 +13,12 @@ ...@@ -13,8 +13,12 @@
#ifndef PG_SCRAM_H #ifndef PG_SCRAM_H
#define PG_SCRAM_H #define PG_SCRAM_H
/* Name of SCRAM-SHA-256 per IANA */ /* Name of SCRAM mechanisms per IANA */
#define SCRAM_SHA256_NAME "SCRAM-SHA-256" #define SCRAM_SHA256_NAME "SCRAM-SHA-256"
#define SCRAM_SHA256_PLUS_NAME "SCRAM-SHA-256-PLUS" /* with channel binding */
/* Channel binding types */
#define SCRAM_CHANNEL_BINDING_TLS_UNIQUE "tls-unique"
/* Status codes for message exchange */ /* Status codes for message exchange */
#define SASL_EXCHANGE_CONTINUE 0 #define SASL_EXCHANGE_CONTINUE 0
...@@ -22,7 +26,9 @@ ...@@ -22,7 +26,9 @@
#define SASL_EXCHANGE_FAILURE 2 #define SASL_EXCHANGE_FAILURE 2
/* Routines dedicated to authentication */ /* Routines dedicated to authentication */
extern void *pg_be_scram_init(const char *username, const char *shadow_pass); extern void *pg_be_scram_init(const char *username, const char *shadow_pass,
bool ssl_in_use, const char *tls_finished_message,
size_t tls_finished_len);
extern int pg_be_scram_exchange(void *opaq, char *input, int inputlen, extern int pg_be_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen, char **logdetail); char **output, int *outputlen, char **logdetail);
......
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
#include "common/base64.h" #include "common/base64.h"
#include "common/saslprep.h" #include "common/saslprep.h"
#include "common/scram-common.h" #include "common/scram-common.h"
#include "libpq/scram.h"
#include "fe-auth.h" #include "fe-auth.h"
/* These are needed for getpid(), in the fallback implementation */ /* These are needed for getpid(), in the fallback implementation */
...@@ -44,6 +45,11 @@ typedef struct ...@@ -44,6 +45,11 @@ typedef struct
/* These are supplied by the user */ /* These are supplied by the user */
const char *username; const char *username;
char *password; char *password;
bool ssl_in_use;
char *tls_finished_message;
size_t tls_finished_len;
char *sasl_mechanism;
const char *channel_binding_type;
/* We construct these */ /* We construct these */
uint8 SaltedPassword[SCRAM_KEY_LEN]; uint8 SaltedPassword[SCRAM_KEY_LEN];
...@@ -79,25 +85,50 @@ static bool pg_frontend_random(char *dst, int len); ...@@ -79,25 +85,50 @@ static bool pg_frontend_random(char *dst, int len);
/* /*
* Initialize SCRAM exchange status. * Initialize SCRAM exchange status.
*
* The non-const char* arguments should be passed in malloc'ed. They will be
* freed by pg_fe_scram_free().
*/ */
void * void *
pg_fe_scram_init(const char *username, const char *password) pg_fe_scram_init(const char *username,
const char *password,
bool ssl_in_use,
const char *sasl_mechanism,
char *tls_finished_message,
size_t tls_finished_len)
{ {
fe_scram_state *state; fe_scram_state *state;
char *prep_password; char *prep_password;
pg_saslprep_rc rc; pg_saslprep_rc rc;
Assert(sasl_mechanism != NULL);
state = (fe_scram_state *) malloc(sizeof(fe_scram_state)); state = (fe_scram_state *) malloc(sizeof(fe_scram_state));
if (!state) if (!state)
return NULL; return NULL;
memset(state, 0, sizeof(fe_scram_state)); memset(state, 0, sizeof(fe_scram_state));
state->state = FE_SCRAM_INIT; state->state = FE_SCRAM_INIT;
state->username = username; state->username = username;
state->ssl_in_use = ssl_in_use;
state->tls_finished_message = tls_finished_message;
state->tls_finished_len = tls_finished_len;
state->sasl_mechanism = strdup(sasl_mechanism);
if (!state->sasl_mechanism)
{
free(state);
return NULL;
}
/*
* Store channel binding type. Only one type is currently supported.
*/
state->channel_binding_type = SCRAM_CHANNEL_BINDING_TLS_UNIQUE;
/* Normalize the password with SASLprep, if possible */ /* Normalize the password with SASLprep, if possible */
rc = pg_saslprep(password, &prep_password); rc = pg_saslprep(password, &prep_password);
if (rc == SASLPREP_OOM) if (rc == SASLPREP_OOM)
{ {
free(state->sasl_mechanism);
free(state); free(state);
return NULL; return NULL;
} }
...@@ -106,6 +137,7 @@ pg_fe_scram_init(const char *username, const char *password) ...@@ -106,6 +137,7 @@ pg_fe_scram_init(const char *username, const char *password)
prep_password = strdup(password); prep_password = strdup(password);
if (!prep_password) if (!prep_password)
{ {
free(state->sasl_mechanism);
free(state); free(state);
return NULL; return NULL;
} }
...@@ -125,6 +157,10 @@ pg_fe_scram_free(void *opaq) ...@@ -125,6 +157,10 @@ pg_fe_scram_free(void *opaq)
if (state->password) if (state->password)
free(state->password); free(state->password);
if (state->tls_finished_message)
free(state->tls_finished_message);
if (state->sasl_mechanism)
free(state->sasl_mechanism);
/* client messages */ /* client messages */
if (state->client_nonce) if (state->client_nonce)
...@@ -297,9 +333,10 @@ static char * ...@@ -297,9 +333,10 @@ static char *
build_client_first_message(fe_scram_state *state, PQExpBuffer errormessage) build_client_first_message(fe_scram_state *state, PQExpBuffer errormessage)
{ {
char raw_nonce[SCRAM_RAW_NONCE_LEN + 1]; char raw_nonce[SCRAM_RAW_NONCE_LEN + 1];
char *buf; char *result;
char buflen; int channel_info_len;
int encoded_len; int encoded_len;
PQExpBufferData buf;
/* /*
* Generate a "raw" nonce. This is converted to ASCII-printable form by * Generate a "raw" nonce. This is converted to ASCII-printable form by
...@@ -328,26 +365,61 @@ build_client_first_message(fe_scram_state *state, PQExpBuffer errormessage) ...@@ -328,26 +365,61 @@ build_client_first_message(fe_scram_state *state, PQExpBuffer errormessage)
* prepared with SASLprep, the message parsing would fail if it includes * prepared with SASLprep, the message parsing would fail if it includes
* '=' or ',' characters. * '=' or ',' characters.
*/ */
buflen = 8 + strlen(state->client_nonce) + 1;
buf = malloc(buflen); initPQExpBuffer(&buf);
if (buf == NULL)
/*
* First build the gs2-header with channel binding information.
*/
if (strcmp(state->sasl_mechanism, SCRAM_SHA256_PLUS_NAME) == 0)
{ {
printfPQExpBuffer(errormessage, Assert(state->ssl_in_use);
libpq_gettext("out of memory\n")); appendPQExpBuffer(&buf, "p=%s", state->channel_binding_type);
return NULL;
} }
snprintf(buf, buflen, "n,,n=,r=%s", state->client_nonce); else if (state->ssl_in_use)
{
/*
* Client supports channel binding, but thinks the server does not.
*/
appendPQExpBuffer(&buf, "y");
}
else
{
/*
* Client does not support channel binding.
*/
appendPQExpBuffer(&buf, "n");
}
if (PQExpBufferDataBroken(buf))
goto oom_error;
state->client_first_message_bare = strdup(buf + 3); channel_info_len = buf.len;
appendPQExpBuffer(&buf, ",,n=,r=%s", state->client_nonce);
if (PQExpBufferDataBroken(buf))
goto oom_error;
/*
* The first message content needs to be saved without channel binding
* information.
*/
state->client_first_message_bare = strdup(buf.data + channel_info_len + 2);
if (!state->client_first_message_bare) if (!state->client_first_message_bare)
{ goto oom_error;
free(buf);
result = strdup(buf.data);
if (result == NULL)
goto oom_error;
termPQExpBuffer(&buf);
return result;
oom_error:
termPQExpBuffer(&buf);
printfPQExpBuffer(errormessage, printfPQExpBuffer(errormessage,
libpq_gettext("out of memory\n")); libpq_gettext("out of memory\n"));
return NULL; return NULL;
}
return buf;
} }
/* /*
...@@ -366,7 +438,67 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage) ...@@ -366,7 +438,67 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
* Construct client-final-message-without-proof. We need to remember it * Construct client-final-message-without-proof. We need to remember it
* for verifying the server proof in the final step of authentication. * for verifying the server proof in the final step of authentication.
*/ */
appendPQExpBuffer(&buf, "c=biws,r=%s", state->nonce); if (strcmp(state->sasl_mechanism, SCRAM_SHA256_PLUS_NAME) == 0)
{
char *cbind_data;
size_t cbind_data_len;
size_t cbind_header_len;
char *cbind_input;
size_t cbind_input_len;
if (strcmp(state->channel_binding_type, SCRAM_CHANNEL_BINDING_TLS_UNIQUE) == 0)
{
cbind_data = state->tls_finished_message;
cbind_data_len = state->tls_finished_len;
}
else
{
/* should not happen */
termPQExpBuffer(&buf);
printfPQExpBuffer(errormessage,
libpq_gettext("invalid channel binding type\n"));
return NULL;
}
/* should not happen */
if (cbind_data == NULL || cbind_data_len == 0)
{
termPQExpBuffer(&buf);
printfPQExpBuffer(errormessage,
libpq_gettext("empty channel binding data for channel binding type \"%s\"\n"),
state->channel_binding_type);
return NULL;
}
appendPQExpBuffer(&buf, "c=");
cbind_header_len = 4 + strlen(state->channel_binding_type); /* p=type,, */
cbind_input_len = cbind_header_len + cbind_data_len;
cbind_input = malloc(cbind_input_len);
if (!cbind_input)
goto oom_error;
snprintf(cbind_input, cbind_input_len, "p=%s,,", state->channel_binding_type);
memcpy(cbind_input + cbind_header_len, cbind_data, cbind_data_len);
if (!enlargePQExpBuffer(&buf, pg_b64_enc_len(cbind_input_len)))
{
free(cbind_input);
goto oom_error;
}
buf.len += pg_b64_encode(cbind_input, cbind_input_len, buf.data + buf.len);
buf.data[buf.len] = '\0';
free(cbind_input);
}
else if (state->ssl_in_use)
appendPQExpBuffer(&buf, "c=eSws"); /* base64 of "y,," */
else
appendPQExpBuffer(&buf, "c=biws"); /* base64 of "n,," */
if (PQExpBufferDataBroken(buf))
goto oom_error;
appendPQExpBuffer(&buf, ",r=%s", state->nonce);
if (PQExpBufferDataBroken(buf)) if (PQExpBufferDataBroken(buf))
goto oom_error; goto oom_error;
......
...@@ -491,6 +491,9 @@ pg_SASL_init(PGconn *conn, int payloadlen) ...@@ -491,6 +491,9 @@ pg_SASL_init(PGconn *conn, int payloadlen)
bool success; bool success;
const char *selected_mechanism; const char *selected_mechanism;
PQExpBufferData mechanism_buf; PQExpBufferData mechanism_buf;
char *tls_finished = NULL;
size_t tls_finished_len = 0;
char *password;
initPQExpBuffer(&mechanism_buf); initPQExpBuffer(&mechanism_buf);
...@@ -504,7 +507,8 @@ pg_SASL_init(PGconn *conn, int payloadlen) ...@@ -504,7 +507,8 @@ pg_SASL_init(PGconn *conn, int payloadlen)
/* /*
* Parse the list of SASL authentication mechanisms in the * Parse the list of SASL authentication mechanisms in the
* AuthenticationSASL message, and select the best mechanism that we * AuthenticationSASL message, and select the best mechanism that we
* support. (Only SCRAM-SHA-256 is supported at the moment.) * support. SCRAM-SHA-256-PLUS and SCRAM-SHA-256 are the only ones
* supported at the moment, listed by order of decreasing importance.
*/ */
selected_mechanism = NULL; selected_mechanism = NULL;
for (;;) for (;;)
...@@ -523,19 +527,36 @@ pg_SASL_init(PGconn *conn, int payloadlen) ...@@ -523,19 +527,36 @@ pg_SASL_init(PGconn *conn, int payloadlen)
break; break;
/* /*
* If we have already selected a mechanism, just skip through the rest * Select the mechanism to use. Pick SCRAM-SHA-256-PLUS over anything
* of the list. * else. Pick SCRAM-SHA-256 if nothing else has already been picked.
* If we add more mechanisms, a more refined priority mechanism might
* become necessary.
*/ */
if (selected_mechanism) if (conn->ssl_in_use &&
continue; strcmp(mechanism_buf.data, SCRAM_SHA256_PLUS_NAME) == 0)
selected_mechanism = SCRAM_SHA256_PLUS_NAME;
else if (strcmp(mechanism_buf.data, SCRAM_SHA256_NAME) == 0 &&
!selected_mechanism)
selected_mechanism = SCRAM_SHA256_NAME;
}
if (!selected_mechanism)
{
printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("none of the server's SASL authentication mechanisms are supported\n"));
goto error;
}
/* /*
* Do we support this mechanism? * Now that the SASL mechanism has been chosen for the exchange,
* initialize its state information.
*/ */
if (strcmp(mechanism_buf.data, SCRAM_SHA256_NAME) == 0)
{
char *password;
/*
* First, select the password to use for the exchange, complaining if
* there isn't one. Currently, all supported SASL mechanisms require a
* password, so we can just go ahead here without further distinction.
*/
conn->password_needed = true; conn->password_needed = true;
password = conn->connhost[conn->whichhost].password; password = conn->connhost[conn->whichhost].password;
if (password == NULL) if (password == NULL)
...@@ -547,19 +568,32 @@ pg_SASL_init(PGconn *conn, int payloadlen) ...@@ -547,19 +568,32 @@ pg_SASL_init(PGconn *conn, int payloadlen)
goto error; goto error;
} }
conn->sasl_state = pg_fe_scram_init(conn->pguser, password); #ifdef USE_SSL
if (!conn->sasl_state) /*
* Get data for channel binding.
*/
if (strcmp(selected_mechanism, SCRAM_SHA256_PLUS_NAME) == 0)
{
tls_finished = pgtls_get_finished(conn, &tls_finished_len);
if (tls_finished == NULL)
goto oom_error; goto oom_error;
selected_mechanism = SCRAM_SHA256_NAME;
}
} }
#endif
if (!selected_mechanism) /*
{ * Initialize the SASL state information with all the information
printfPQExpBuffer(&conn->errorMessage, * gathered during the initial exchange.
libpq_gettext("none of the server's SASL authentication mechanisms are supported\n")); *
goto error; * Note: Only tls-unique is supported for the moment.
} */
conn->sasl_state = pg_fe_scram_init(conn->pguser,
password,
conn->ssl_in_use,
selected_mechanism,
tls_finished,
tls_finished_len);
if (!conn->sasl_state)
goto oom_error;
/* Get the mechanism-specific Initial Client Response, if any */ /* Get the mechanism-specific Initial Client Response, if any */
pg_fe_scram_exchange(conn->sasl_state, pg_fe_scram_exchange(conn->sasl_state,
......
...@@ -23,7 +23,12 @@ extern int pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn); ...@@ -23,7 +23,12 @@ extern int pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn);
extern char *pg_fe_getauthname(PQExpBuffer errorMessage); extern char *pg_fe_getauthname(PQExpBuffer errorMessage);
/* Prototypes for functions in fe-auth-scram.c */ /* Prototypes for functions in fe-auth-scram.c */
extern void *pg_fe_scram_init(const char *username, const char *password); extern void *pg_fe_scram_init(const char *username,
const char *password,
bool ssl_in_use,
const char *sasl_mechanism,
char *tls_finished_message,
size_t tls_finished_len);
extern void pg_fe_scram_free(void *opaq); extern void pg_fe_scram_free(void *opaq);
extern void pg_fe_scram_exchange(void *opaq, char *input, int inputlen, extern void pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen, char **output, int *outputlen,
......
...@@ -393,6 +393,33 @@ pgtls_write(PGconn *conn, const void *ptr, size_t len) ...@@ -393,6 +393,33 @@ pgtls_write(PGconn *conn, const void *ptr, size_t len)
return n; return n;
} }
/*
* Get the TLS finish message sent during last handshake
*
* This information is useful for callers doing channel binding during
* authentication.
*/
char *
pgtls_get_finished(PGconn *conn, size_t *len)
{
char dummy[1];
char *result;
/*
* OpenSSL does not offer an API to get directly the length of the TLS
* Finished message sent, so first do a dummy call to grab this
* information and then do an allocation with the correct size.
*/
*len = SSL_get_finished(conn->ssl, dummy, sizeof(dummy));
result = malloc(*len);
if (result == NULL)
return NULL;
(void) SSL_get_finished(conn->ssl, result, *len);
return result;
}
/* ------------------------------------------------------------ */ /* ------------------------------------------------------------ */
/* OpenSSL specific code */ /* OpenSSL specific code */
/* ------------------------------------------------------------ */ /* ------------------------------------------------------------ */
......
...@@ -453,11 +453,13 @@ struct pg_conn ...@@ -453,11 +453,13 @@ struct pg_conn
/* Assorted state for SASL, SSL, GSS, etc */ /* Assorted state for SASL, SSL, GSS, etc */
void *sasl_state; void *sasl_state;
/* SSL structures */
bool ssl_in_use;
#ifdef USE_SSL #ifdef USE_SSL
bool allow_ssl_try; /* Allowed to try SSL negotiation */ bool allow_ssl_try; /* Allowed to try SSL negotiation */
bool wait_ssl_try; /* Delay SSL negotiation until after bool wait_ssl_try; /* Delay SSL negotiation until after
* attempting normal connection */ * attempting normal connection */
bool ssl_in_use;
#ifdef USE_OPENSSL #ifdef USE_OPENSSL
SSL *ssl; /* SSL status, if have SSL connection */ SSL *ssl; /* SSL status, if have SSL connection */
X509 *peer; /* X509 cert of server */ X509 *peer; /* X509 cert of server */
...@@ -668,6 +670,7 @@ extern void pgtls_close(PGconn *conn); ...@@ -668,6 +670,7 @@ extern void pgtls_close(PGconn *conn);
extern ssize_t pgtls_read(PGconn *conn, void *ptr, size_t len); extern ssize_t pgtls_read(PGconn *conn, void *ptr, size_t len);
extern bool pgtls_read_pending(PGconn *conn); extern bool pgtls_read_pending(PGconn *conn);
extern ssize_t pgtls_write(PGconn *conn, const void *ptr, size_t len); extern ssize_t pgtls_write(PGconn *conn, const void *ptr, size_t len);
extern char *pgtls_get_finished(PGconn *conn, size_t *len);
/* /*
* this is so that we can check if a connection is non-blocking internally * this is so that we can check if a connection is non-blocking internally
......
...@@ -57,19 +57,21 @@ sub test_connect_ok ...@@ -57,19 +57,21 @@ sub test_connect_ok
{ {
my $common_connstr = $_[0]; my $common_connstr = $_[0];
my $connstr = $_[1]; my $connstr = $_[1];
my $test_name = $_[2];
my $result = my $result =
run_test_psql("$common_connstr $connstr", "(should succeed)"); run_test_psql("$common_connstr $connstr", "(should succeed)");
ok($result, $connstr); ok($result, $test_name || $connstr);
} }
sub test_connect_fails sub test_connect_fails
{ {
my $common_connstr = $_[0]; my $common_connstr = $_[0];
my $connstr = $_[1]; my $connstr = $_[1];
my $test_name = $_[2];
my $result = run_test_psql("$common_connstr $connstr", "(should fail)"); my $result = run_test_psql("$common_connstr $connstr", "(should fail)");
ok(!$result, "$connstr (should fail)"); ok(!$result, $test_name || "$connstr (should fail)");
} }
# Copy a set of files, taking into account wildcards # Copy a set of files, taking into account wildcards
...@@ -89,8 +91,7 @@ sub copy_files ...@@ -89,8 +91,7 @@ sub copy_files
sub configure_test_server_for_ssl sub configure_test_server_for_ssl
{ {
my $node = $_[0]; my ($node, $serverhost, $authmethod, $password, $password_enc) = @_;
my $serverhost = $_[1];
my $pgdata = $node->data_dir; my $pgdata = $node->data_dir;
...@@ -100,6 +101,15 @@ sub configure_test_server_for_ssl ...@@ -100,6 +101,15 @@ sub configure_test_server_for_ssl
$node->psql('postgres', "CREATE DATABASE trustdb"); $node->psql('postgres', "CREATE DATABASE trustdb");
$node->psql('postgres', "CREATE DATABASE certdb"); $node->psql('postgres', "CREATE DATABASE certdb");
# Update password of each user as needed.
if (defined($password))
{
$node->psql('postgres',
"SET password_encryption='$password_enc'; ALTER USER ssltestuser PASSWORD '$password';");
$node->psql('postgres',
"SET password_encryption='$password_enc'; ALTER USER anotheruser PASSWORD '$password';");
}
# enable logging etc. # enable logging etc.
open my $conf, '>>', "$pgdata/postgresql.conf"; open my $conf, '>>', "$pgdata/postgresql.conf";
print $conf "fsync=off\n"; print $conf "fsync=off\n";
...@@ -129,7 +139,7 @@ sub configure_test_server_for_ssl ...@@ -129,7 +139,7 @@ sub configure_test_server_for_ssl
$node->restart; $node->restart;
# Change pg_hba after restart because hostssl requires ssl=on # Change pg_hba after restart because hostssl requires ssl=on
configure_hba_for_ssl($node, $serverhost); configure_hba_for_ssl($node, $serverhost, $authmethod);
} }
# Change the configuration to use given server cert file, and reload # Change the configuration to use given server cert file, and reload
...@@ -157,8 +167,7 @@ sub switch_server_cert ...@@ -157,8 +167,7 @@ sub switch_server_cert
sub configure_hba_for_ssl sub configure_hba_for_ssl
{ {
my $node = $_[0]; my ($node, $serverhost, $authmethod) = @_;
my $serverhost = $_[1];
my $pgdata = $node->data_dir; my $pgdata = $node->data_dir;
# Only accept SSL connections from localhost. Our tests don't depend on this # Only accept SSL connections from localhost. Our tests don't depend on this
...@@ -169,9 +178,9 @@ sub configure_hba_for_ssl ...@@ -169,9 +178,9 @@ sub configure_hba_for_ssl
print $hba print $hba
"# TYPE DATABASE USER ADDRESS METHOD\n"; "# TYPE DATABASE USER ADDRESS METHOD\n";
print $hba print $hba
"hostssl trustdb ssltestuser $serverhost/32 trust\n"; "hostssl trustdb ssltestuser $serverhost/32 $authmethod\n";
print $hba print $hba
"hostssl trustdb ssltestuser ::1/128 trust\n"; "hostssl trustdb ssltestuser ::1/128 $authmethod\n";
print $hba print $hba
"hostssl certdb ssltestuser $serverhost/32 cert\n"; "hostssl certdb ssltestuser $serverhost/32 cert\n";
print $hba print $hba
......
...@@ -32,7 +32,7 @@ $node->init; ...@@ -32,7 +32,7 @@ $node->init;
$ENV{PGHOST} = $node->host; $ENV{PGHOST} = $node->host;
$ENV{PGPORT} = $node->port; $ENV{PGPORT} = $node->port;
$node->start; $node->start;
configure_test_server_for_ssl($node, $SERVERHOSTADDR); configure_test_server_for_ssl($node, $SERVERHOSTADDR, 'trust');
switch_server_cert($node, 'server-cn-only'); switch_server_cert($node, 'server-cn-only');
### Part 1. Run client-side tests. ### Part 1. Run client-side tests.
......
# Test SCRAM authentication and TLS channel binding types
use strict;
use warnings;
use PostgresNode;
use TestLib;
use Test::More tests => 1;
use ServerSetup;
use File::Copy;
# This is the hostname used to connect to the server.
my $SERVERHOSTADDR = '127.0.0.1';
# Allocation of base connection string shared among multiple tests.
my $common_connstr;
# Set up the server.
note "setting up data directory";
my $node = get_new_node('master');
$node->init;
# PGHOST is enforced here to set up the node, subsequent connections
# will use a dedicated connection string.
$ENV{PGHOST} = $node->host;
$ENV{PGPORT} = $node->port;
$node->start;
# Configure server for SSL connections, with password handling.
configure_test_server_for_ssl($node, $SERVERHOSTADDR, "scram-sha-256",
"pass", "scram-sha-256");
switch_server_cert($node, 'server-cn-only');
$ENV{PGPASSWORD} = "pass";
$common_connstr =
"user=ssltestuser dbname=trustdb sslmode=require hostaddr=$SERVERHOSTADDR";
test_connect_ok($common_connstr, '',
"SCRAM authentication with default channel binding");
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment