mirror of
https://github.com/postgres/postgres.git
synced 2025-07-05 07:21:24 +03:00
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: Peter Eisentraut <peter.eisentraut@2ndquadrant.com>
This commit is contained in:
@ -17,8 +17,6 @@
|
||||
* by the SASLprep profile, we skip the SASLprep pre-processing and use
|
||||
* 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,
|
||||
* StoredKey and ServerKey.
|
||||
@ -112,6 +110,11 @@ typedef struct
|
||||
|
||||
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;
|
||||
char *salt; /* base64-encoded */
|
||||
uint8 StoredKey[SCRAM_KEY_LEN];
|
||||
@ -168,7 +171,11 @@ static char *scram_mock_salt(const char *username);
|
||||
* it will fail, as if an incorrect password was given.
|
||||
*/
|
||||
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;
|
||||
bool got_verifier;
|
||||
@ -176,6 +183,10 @@ pg_be_scram_init(const char *username, const char *shadow_pass)
|
||||
state = (scram_state *) palloc0(sizeof(scram_state));
|
||||
state->state = SCRAM_AUTH_INIT;
|
||||
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.
|
||||
@ -773,31 +784,89 @@ 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)
|
||||
{
|
||||
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++;
|
||||
break;
|
||||
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++;
|
||||
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.
|
||||
* The client requires channel binding. Channel binding type
|
||||
* follows, e.g., "p=tls-unique".
|
||||
*/
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
|
||||
errmsg("client requires SCRAM channel binding, but it is not supported")));
|
||||
{
|
||||
char *channel_binding_type;
|
||||
|
||||
if (!state->ssl_in_use)
|
||||
{
|
||||
/*
|
||||
* Without SSL, we don't support channel binding.
|
||||
*
|
||||
* 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_PROTOCOL_VIOLATION),
|
||||
errmsg("client requires SCRAM channel binding, but it is not supported")));
|
||||
}
|
||||
|
||||
/*
|
||||
* 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,
|
||||
(errcode(ERRCODE_PROTOCOL_VIOLATION),
|
||||
(errmsg("unsupported SCRAM channel-binding type"))));
|
||||
|
||||
/* Save the name for handling of subsequent messages */
|
||||
state->channel_binding_type = pstrdup(channel_binding_type);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_PROTOCOL_VIOLATION),
|
||||
@ -805,13 +874,6 @@ read_client_first_message(scram_state *state, char *input)
|
||||
errdetail("Unexpected channel-binding flag \"%s\".",
|
||||
sanitize_char(*input))));
|
||||
}
|
||||
if (*input != ',')
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_PROTOCOL_VIOLATION),
|
||||
errmsg("malformed SCRAM message"),
|
||||
errdetail("Comma expected, but found character \"%s\".",
|
||||
sanitize_char(*input))));
|
||||
input++;
|
||||
|
||||
/*
|
||||
* Forbid optional authzid (authorization identity). We don't support it.
|
||||
@ -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
|
||||
* expected to always be "biws", which is "n,,", base64-encoded.
|
||||
* Read channel binding. This repeats the channel-binding flags and is
|
||||
* then followed by the actual binding data depending on the type.
|
||||
*/
|
||||
channel_binding = read_attr_value(&p, 'c');
|
||||
if (strcmp(channel_binding, "biws") != 0)
|
||||
ereport(ERROR,
|
||||
(errcode(ERRCODE_PROTOCOL_VIOLATION),
|
||||
(errmsg("unexpected SCRAM channel-binding attribute in client-final-message"))));
|
||||
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,
|
||||
(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 */
|
||||
|
@ -860,6 +860,8 @@ CheckMD5Auth(Port *port, char *shadow_pass, char **logdetail)
|
||||
static int
|
||||
CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
|
||||
{
|
||||
char *sasl_mechs;
|
||||
char *p;
|
||||
int mtype;
|
||||
StringInfoData buf;
|
||||
void *scram_opaq;
|
||||
@ -869,6 +871,8 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
|
||||
int inputlen;
|
||||
int result;
|
||||
bool initial;
|
||||
char *tls_finished = NULL;
|
||||
size_t tls_finished_len = 0;
|
||||
|
||||
/*
|
||||
* SASL auth is not supported for protocol versions before 3, because it
|
||||
@ -885,12 +889,39 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
|
||||
|
||||
/*
|
||||
* Send the SASL authentication request to user. It includes the list of
|
||||
* authentication mechanisms (which is trivial, because we only support
|
||||
* SCRAM-SHA-256 at the moment). The extra "\0" is for an empty string to
|
||||
* terminate the list.
|
||||
* authentication mechanisms that are supported. The order of mechanisms
|
||||
* is advertised in decreasing order of importance. So the
|
||||
* 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",
|
||||
strlen(SCRAM_SHA256_NAME) + 2);
|
||||
sasl_mechs = palloc(strlen(SCRAM_SHA256_PLUS_NAME) +
|
||||
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.
|
||||
@ -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
|
||||
* 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
|
||||
@ -951,12 +986,9 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
|
||||
{
|
||||
const char *selected_mech;
|
||||
|
||||
/*
|
||||
* We only support SCRAM-SHA-256 at the moment, so anything else
|
||||
* is an error.
|
||||
*/
|
||||
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,
|
||||
(errcode(ERRCODE_PROTOCOL_VIOLATION),
|
||||
|
@ -1215,6 +1215,30 @@ be_tls_get_peerdn_name(Port *port, char *ptr, size_t len)
|
||||
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.
|
||||
*
|
||||
|
Reference in New Issue
Block a user