diff options
author | Peter Eisentraut <peter_e@gmx.net> | 2017-11-18 10:07:57 -0500 |
---|---|---|
committer | Peter Eisentraut <peter_e@gmx.net> | 2017-11-18 10:15:54 -0500 |
commit | 9288d62bb4b6f302bf13bb2fed3783b61385f315 (patch) | |
tree | 2b6fa3bf8940b1f8d2ec77fc367fd750de82390d /src/backend/libpq/auth-scram.c | |
parent | 611fe7d4793ba6516e839dc50b5319b990283f4f (diff) |
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>
Diffstat (limited to 'src/backend/libpq/auth-scram.c')
-rw-r--r-- | src/backend/libpq/auth-scram.c | 181 |
1 files changed, 151 insertions, 30 deletions
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c index ec4bb9a88ec..22103ce4795 100644 --- a/src/backend/libpq/auth-scram.c +++ b/src/backend/libpq/auth-scram.c @@ -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 */ |