Channel binding support for SCRAM-SHA-256
Hi all,
Please find attached a patch to add support for channel binding for
SCRAM, to mitigate MITM attacks using this protocol. Per RFC5802
(https://tools.ietf.org/html/rfc5802), servers supporting channel
binding need to add support for tls-unique, and this is what this
patch does.
As defined in RFC5056 (exactly here =>
https://tools.ietf.org/html/rfc5056#section-4.1), servers can use the
TLS finish message to determine if the client is actually the same as
the one who initiated the connection, to eliminate the risk of MITM
attacks. OpenSSL offers two undocumented APIs for this purpose:
- SSL_get_finished() to get the latest TLS finish message that the
client has sent.
- SSL_get_peer_finished(), to get the latest TLS finish message
expected by the server.
So basically what is needed here is saving the latest message
generated by client in libpq using get_finished(), send it to the
server using the SASL exchange messages (value sent with the final
client message), and then compare it with what the server expected.
Connection is successful if what the client has sent and what the
server expects match.
There is also a clear documentation about what is expected from the
client and the server in terms of how both should behave using the
first client message https://tools.ietf.org/html/rfc5802#section-6. So
I have tried to follow it, reviews are welcome particularly regarding
that. The implementation is done in such a way that channel binding is
used in the context of an SSL connection, which is what the RFCs
expect from an implementation.
Before even that, the server needs to send to the client the list of
SASL mechanisms that are supported. This adds SCRAM-SHA-256-PLUS if
the server has been built with SSL support to the list.
Comments are welcome, I am parking that in the next CF for integration in PG11.
Thanks,
--
Michael
Attachments:
scram-channel-binding.patchapplication/octet-stream; name=scram-channel-binding.patchDownload
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index d23df0261c..42263431ed 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1342,10 +1342,11 @@
<para>
<firstterm>SASL</> is a framework for authentication in connection-oriented
-protocols. At the moment, <productname>PostgreSQL</> implements only one SASL
-authentication mechanism, SCRAM-SHA-256, but more might be added in the
-future. The below steps illustrate how SASL authentication is performed in
-general, while the next subsection gives more details on SCRAM-SHA-256.
+protocols. At the moment, <productname>PostgreSQL</> implements only two SASL
+authentication mechanisms, SCRAM-SHA-256 and SCRAM-SHA-256-PLUS, but more
+might be added in the future. The below steps illustrate how SASL
+authentication is performed in general, while the next subsection gives
+more details on SCRAM-SHA-256 and SCRAM-SHA-256-PLUS.
</para>
<procedure>
@@ -1430,7 +1431,9 @@ the password is in.
</para>
<para>
-<firstterm>Channel binding</> has not been implemented yet.
+<firstterm>Channel binding</> is supported in builds with SSL support, and
+uses as mechanism name <literal>SCRAM-SHA-256-PLUS</> for this purpose as
+defined per IANA.
</para>
<procedure>
@@ -1439,14 +1442,18 @@ the password is in.
<para>
The server sends an AuthenticationSASL message. It includes a list of
SASL authentication mechanisms that the server can accept.
+ <literal>SCRAM-SHA-256</> and <literal>SCRAM-SHA-256-PLUS</> are the
+ two mechanism names that the server lists in this message. Support for
+ channel binding is not included if the server is built without SSL
+ support and if the connection attempt is done without SSL.
</para>
</step>
<step id="scram-client-first">
<para>
The client responds by sending a SASLInitialResponse message, which
- indicates the chosen mechanism, <literal>SCRAM-SHA-256</>. In the Initial
- Client response field, the message contains the SCRAM
- <structname>client-first-message</>.
+ indicates the chosen mechanism, <literal>SCRAM-SHA-256</> or
+ <literal>SCRAM-SHA-256-PLUS</>. In the Initial Client response field,
+ the message contains the SCRAM <structname>client-first-message</>.
</para>
</step>
<step id="scram-server-first">
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index 99feb0ce94..a372b08bba 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,10 @@ typedef struct
const char *username; /* username from startup packet */
+ bool ssl_in_use;
+ char *tls_finish_message;
+ int tls_finish_len;
+
int iterations;
char *salt; /* base64-encoded */
uint8 StoredKey[SCRAM_KEY_LEN];
@@ -168,7 +170,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,
+ char *tls_finish_message,
+ int tls_finish_len)
{
scram_state *state;
bool got_verifier;
@@ -176,6 +182,9 @@ 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_finish_message = tls_finish_message;
+ state->tls_finish_len = tls_finish_len;
/*
* Parse the stored password verifier.
@@ -767,43 +776,97 @@ read_client_first_message(scram_state *state, char *input)
*------
*/
- /* read gs2-cbind-flag */
+ /*
+ * Read gs2-cbind-flag. Server handles its value as described in RFC 5802,
+ * section 6 dealing with channel binding.
+ */
switch (*input)
{
case 'n':
- /* Client does not support channel binding */
+ /*
+ * Client does not support channel binding, this is authorized
+ * only in builds not supporting SSL. If SSL is supported, the
+ * server cannot support this option either.
+ */
+#ifdef USE_SSL
+ if (state->ssl_in_use)
+ ereport(ERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("client does not support SCRAM channel binding, but server needs it for SSL connections")));
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("client does not support SCRAM channel binding, but server does")));
+#endif
+ input++;
+ if (*input != ',')
+ ereport(ERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("malformed SCRAM message (comma expected, got %s)",
+ sanitize_char(*input))));
input++;
break;
case 'y':
- /* Client supports channel binding, but we're not doing it today */
+ /*
+ * Client supports channel binding, but we're not doing it today
+ * in the context of a non-SSL connection. Complain though if
+ * the client is trying to trick the server in not doing channel
+ * binding with a downgrade attack if a SSL connection is
+ * attempted.
+ */
+ if (state->ssl_in_use)
+ ereport(ERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("client support SCRAM channel binding, but server needs it for SSL connections")));
+ input++;
+ if (*input != ',')
+ ereport(ERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("malformed SCRAM message (comma expected, got %s)",
+ sanitize_char(*input))));
input++;
break;
case 'p':
+ {
+#ifdef USE_SSL
+ char *channel_name;
- /*
- * 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")));
+ if (!state->ssl_in_use)
+ ereport(ERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("client supports SCRAM channel binding, but server does not need it for non-SSL connections")));
+
+ /*
+ * Read value provided by client, only tls-unique is supported
+ * for now.
+ */
+ channel_name = read_attr_value(&input, 'p');
+ if (strcmp(channel_name, "tls-unique") != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ (errmsg("unexpected SCRAM channel-binding type"))));
+#else
+ /*
+ * 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")));
+#endif
+ }
+ break;
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.
@@ -1023,14 +1086,47 @@ 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. We don't support channel binding for builds
+ * without SSL support, so it's expected to always be "biws" in this case,
+ * which is "n,,", base64-encoded. In builds supporting SSL, "biws" is
+ * used for Non-SSL connection attempts, and for SSL connections the
+ * client has to provide channel binding value.
*/
channel_binding = read_attr_value(&p, 'c');
+#ifdef USE_SSL
+ if (state->ssl_in_use)
+ {
+ char *enc_tls_message;
+ int enc_tls_len;
+
+ enc_tls_message = palloc(pg_b64_enc_len(state->tls_finish_len) + 1);
+ enc_tls_len = pg_b64_encode(state->tls_finish_message,
+ state->tls_finish_len,
+ enc_tls_message);
+ enc_tls_message[enc_tls_len] = '\0';
+
+ /*
+ * Compare the value sent by the client with the TLS finish message
+ * expected by the server.
+ */
+ if (strcmp(channel_binding, enc_tls_message) != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ (errmsg("no match for SCRAM channel-binding attribute in client-final-message"))));
+ }
+ else
+ {
+ if (strcmp(channel_binding, "biws") != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ (errmsg("unexpected SCRAM channel-binding attribute in client-final-message"))));
+ }
+#else
if (strcmp(channel_binding, "biws") != 0)
ereport(ERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION),
(errmsg("unexpected SCRAM channel-binding attribute in client-final-message"))));
+#endif
state->client_final_nonce = read_attr_value(&p, 'r');
/* ignore optional extensions */
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 5b68e3b7a1..89dfa744b1 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -841,8 +841,10 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
void *scram_opaq;
char *output = NULL;
int outputlen = 0;
- char *input;
+ char *input, *p;
+ char *sasl_mechs;
int inputlen;
+ int listlen = 0;
int result;
bool initial;
@@ -861,12 +863,38 @@ 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:
+ * - SCRAM-SHA-256, which is the mechanism with the same name.
+ * - SCRAM-SHA-256-PLUS, which is SCRAM-SHA-256 with channel binding. This
+ * is advertised to the client only if connection is attempted with SSL.
+ * The order of mechanisms is advertised in decreasing order of importance.
+ * The extra "\0" is for an empty string to terminate the list, and each
+ * mechanism listed needs to be separated with "\0".
*/
- sendAuthRequest(port, AUTH_REQ_SASL, SCRAM_SHA256_NAME "\0",
- strlen(SCRAM_SHA256_NAME) + 2);
+ listlen = 0;
+ sasl_mechs = (char *) palloc(strlen(SCRAM_SHA256_PLUS_NAME) +
+ strlen(SCRAM_SHA256_NAME) + 3);
+ p = sasl_mechs;
+
+ /* add SCRAM-SHA-256-PLUS, which depends on if SSL is in use */
+ if (port->ssl_in_use)
+ {
+ strcpy(p, SCRAM_SHA256_PLUS_NAME);
+ listlen += strlen(SCRAM_SHA256_PLUS_NAME) + 1;
+ p += strlen(SCRAM_SHA256_PLUS_NAME) + 1;
+ }
+
+ /* add generic SCRAM-SHA-256 */
+ strcpy(p, SCRAM_SHA256_NAME);
+ listlen += strlen(SCRAM_SHA256_NAME) + 1;
+ p += strlen(SCRAM_SHA256_NAME) + 1;
+
+ /* put "\0" to mark that list is finished */
+ p[0] = '\0';
+ listlen++;
+
+ sendAuthRequest(port, AUTH_REQ_SASL, sasl_mechs, listlen);
+ pfree(sasl_mechs);
/*
* Initialize the status tracker for message exchanges.
@@ -879,7 +907,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,
+ port->tls_finish,
+ port->tls_finish_len);
/*
* Loop through SASL message exchange. This exchange can consist of
@@ -933,7 +965,8 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
* 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(COMMERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION),
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 44c84a7869..eff8b300de 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -458,6 +458,7 @@ be_tls_open_server(Port *port)
int err;
int waitfor;
unsigned long ecode;
+ char tls_finish_buf[20];
Assert(!port->ssl);
Assert(!port->peer);
@@ -616,6 +617,25 @@ aloop:
/* set up debugging/info callback */
SSL_CTX_set_info_callback(SSL_context, info_cb);
+ /*
+ * Save the TLS finish message expected to be found, useful for
+ * authentication checks related to channel binding.
+ * SSL_get_peer_finished() does not offer a way to know the exact length
+ * of a TLS finish message beforehand, so attempt first with a fixed-length
+ * buffer, and try again if the message does not fit.
+ */
+ port->tls_finish_len = SSL_get_peer_finished(port->ssl,
+ tls_finish_buf,
+ sizeof(tls_finish_buf));
+ port->tls_finish = MemoryContextAlloc(TopMemoryContext,
+ port->tls_finish_len);
+ if (port->tls_finish_len > sizeof(tls_finish_buf))
+ memcpy(port->tls_finish, tls_finish_buf, port->tls_finish_len);
+ else
+ (void) SSL_get_peer_finished(port->ssl,
+ port->tls_finish,
+ port->tls_finish_len);
+
return 0;
}
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 0669b924cf..6eb39e0678 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -181,6 +181,8 @@ typedef struct Port
bool ssl_in_use;
char *peer_cn;
bool peer_cert_valid;
+ char *tls_finish; /* TLS message expected from client */
+ int tls_finish_len; /* length expected of TLS message */
/*
* OpenSSL structures. (Keep these last so that the locations of other
diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h
index 14b48af12f..fc6fe2431f 100644
--- a/src/include/libpq/scram.h
+++ b/src/include/libpq/scram.h
@@ -13,8 +13,9 @@
#ifndef 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_PLUS_NAME "SCRAM-SHA-256-PLUS" /* with channel binding */
/* Status codes for message exchange */
#define SASL_EXCHANGE_CONTINUE 0
@@ -22,7 +23,9 @@
#define SASL_EXCHANGE_FAILURE 2
/* 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, char *tls_finish_message,
+ int tls_finish_len);
extern int pg_be_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen, char **logdetail);
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index d2e355a8b8..5e0837a2c9 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -44,6 +44,9 @@ typedef struct
/* These are supplied by the user */
const char *username;
char *password;
+ bool ssl_in_use;
+ char *tls_finish_message;
+ int tls_finish_len;
/* We construct these */
uint8 SaltedPassword[SCRAM_KEY_LEN];
@@ -81,7 +84,11 @@ static bool pg_frontend_random(char *dst, int len);
* Initialize SCRAM exchange status.
*/
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,
+ char *tls_finish_message,
+ int tls_finish_len)
{
fe_scram_state *state;
char *prep_password;
@@ -93,6 +100,9 @@ pg_fe_scram_init(const char *username, const char *password)
memset(state, 0, sizeof(fe_scram_state));
state->state = FE_SCRAM_INIT;
state->username = username;
+ state->ssl_in_use = ssl_in_use;
+ state->tls_finish_message = tls_finish_message;
+ state->tls_finish_len = tls_finish_len;
/* Normalize the password with SASLprep, if possible */
rc = pg_saslprep(password, &prep_password);
@@ -297,9 +307,10 @@ static char *
build_client_first_message(fe_scram_state *state, PQExpBuffer errormessage)
{
char raw_nonce[SCRAM_RAW_NONCE_LEN + 1];
- char *buf;
- char buflen;
+ char *result;
+ int channel_info_len;
int encoded_len;
+ PQExpBufferData buf;
/*
* Generate a "raw" nonce. This is converted to ASCII-printable form by
@@ -328,26 +339,55 @@ build_client_first_message(fe_scram_state *state, PQExpBuffer errormessage)
* prepared with SASLprep, the message parsing would fail if it includes
* '=' or ',' characters.
*/
- buflen = 8 + strlen(state->client_nonce) + 1;
- buf = malloc(buflen);
- if (buf == NULL)
- {
- printfPQExpBuffer(errormessage,
- libpq_gettext("out of memory\n"));
- return NULL;
- }
- snprintf(buf, buflen, "n,,n=,r=%s", state->client_nonce);
+ initPQExpBuffer(&buf);
- state->client_first_message_bare = strdup(buf + 3);
+ /*
+ * First build the query field for channel binding. If the client is not
+ * built with SSL support, it cannot support channel binding so it needs
+ * to use "n" to let the server know. If built with SSL support, client
+ * needs to use "y" to let the server know that client has such support
+ * but that it is not using it as a non-SSL connection is requested.
+ * Finally if a SSL connection is done, use p=cb-name, for which only
+ * "tls-unique" is supported now.
+ */
+#ifdef USE_SSL
+ if (state->ssl_in_use)
+ appendPQExpBuffer(&buf, "p=tls-unique");
+ else
+ appendPQExpBuffer(&buf, "y");
+#else
+ appendPQExpBuffer(&buf, "n");
+#endif
+
+ if (PQExpBufferDataBroken(buf))
+ goto oom_error;
+
+ 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)
- {
- free(buf);
- printfPQExpBuffer(errormessage,
- libpq_gettext("out of memory\n"));
- return NULL;
- }
+ goto oom_error;
- return buf;
+ result = strdup(buf.data);
+ if (result == NULL)
+ goto oom_error;
+
+ termPQExpBuffer(&buf);
+ return result;
+
+oom_error:
+ termPQExpBuffer(&buf);
+ printfPQExpBuffer(errormessage,
+ libpq_gettext("out of memory\n"));
+ return NULL;
}
/*
@@ -365,8 +405,30 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
/*
* Construct client-final-message-without-proof. We need to remember it
* for verifying the server proof in the final step of authentication.
+ * Client needs to provide a b64 encoded string of the TLS finish message
+ * only if a SSL connection is attempted.
*/
- appendPQExpBuffer(&buf, "c=biws,r=%s", state->nonce);
+#ifdef USE_SSL
+ if (state->ssl_in_use)
+ {
+ appendPQExpBuffer(&buf, "c=");
+ if (!enlargePQExpBuffer(&buf, pg_b64_enc_len(state->tls_finish_len)))
+ goto oom_error;
+ buf.len += pg_b64_encode(state->tls_finish_message,
+ state->tls_finish_len,
+ buf.data + buf.len);
+ buf.data[buf.len] = '\0';
+ }
+ else
+ appendPQExpBuffer(&buf, "c=biws");
+#else
+ appendPQExpBuffer(&buf, "c=biws");
+#endif
+
+ if (PQExpBufferDataBroken(buf))
+ goto oom_error;
+
+ appendPQExpBuffer(&buf, ",r=%s", state->nonce);
if (PQExpBufferDataBroken(buf))
goto oom_error;
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index f4397afc64..ea4bbbae95 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -494,7 +494,8 @@ pg_SASL_init(PGconn *conn, int payloadlen)
/*
* Parse the list of SASL authentication mechanisms in the
* AuthenticationSASL message, and select the best mechanism that we
- * support. (Only SCRAM-SHA-256 is supported at the moment.)
+ * support. SCRAM-SHA-256 and SCRAM-SHA-256-PLUS are the only ones
+ * supported at the moment.
*/
selected_mechanism = NULL;
for (;;)
@@ -522,7 +523,8 @@ pg_SASL_init(PGconn *conn, int payloadlen)
/*
* Do we support this mechanism?
*/
- if (strcmp(mechanism_buf.data, SCRAM_SHA256_NAME) == 0)
+ if (strcmp(mechanism_buf.data, SCRAM_SHA256_NAME) == 0 ||
+ strcmp(mechanism_buf.data, SCRAM_SHA256_PLUS_NAME) == 0)
{
char *password;
@@ -537,10 +539,18 @@ pg_SASL_init(PGconn *conn, int payloadlen)
goto error;
}
- conn->sasl_state = pg_fe_scram_init(conn->pguser, password);
+ conn->sasl_state = pg_fe_scram_init(conn->pguser,
+ password,
+ conn->ssl_in_use,
+ conn->tls_finish,
+ conn->tls_finish_len);
if (!conn->sasl_state)
goto oom_error;
- selected_mechanism = SCRAM_SHA256_NAME;
+
+ if (strcmp(mechanism_buf.data, SCRAM_SHA256_NAME) == 0)
+ selected_mechanism = SCRAM_SHA256_NAME;
+ else
+ selected_mechanism = SCRAM_SHA256_PLUS_NAME;
}
}
diff --git a/src/interfaces/libpq/fe-auth.h b/src/interfaces/libpq/fe-auth.h
index 9f4c2a50d8..2ee9c6c48c 100644
--- a/src/interfaces/libpq/fe-auth.h
+++ b/src/interfaces/libpq/fe-auth.h
@@ -23,7 +23,9 @@ extern int pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn);
extern char *pg_fe_getauthname(PQExpBuffer errorMessage);
/* 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, char *tls_finish_message,
+ int tls_finish_len);
extern void pg_fe_scram_free(void *opaq);
extern void pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen,
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index a7c3d7af64..b701b18c1c 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -1297,6 +1297,7 @@ static PostgresPollingStatusType
open_client_SSL(PGconn *conn)
{
int r;
+ char tls_finish_buf[20];
ERR_clear_error();
r = SSL_connect(conn->ssl);
@@ -1376,6 +1377,24 @@ open_client_SSL(PGconn *conn)
return PGRES_POLLING_FAILED;
}
+ /*
+ * Save the TLS finish message sent to the server, useful for
+ * authentication checks related to channel binding. SSL_get_finished()
+ * does not offer a way to know the exact length of a TLS finish message
+ * beforehand, so attempt first with a fixed-length buffer, and try again
+ * if the message does not fit.
+ */
+ conn->tls_finish_len = SSL_get_finished(conn->ssl,
+ tls_finish_buf,
+ sizeof(tls_finish_buf));
+ conn->tls_finish = malloc(conn->tls_finish_len);
+ if (conn->tls_finish_len > sizeof(tls_finish_buf))
+ memcpy(conn->tls_finish, tls_finish_buf, conn->tls_finish_len);
+ else
+ (void) SSL_get_finished(conn->ssl,
+ conn->tls_finish,
+ conn->tls_finish_len);
+
/* SSL handshake is complete */
return PGRES_POLLING_OK;
}
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 335568b790..ab2b9befbf 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -454,11 +454,15 @@ struct pg_conn
/* Assorted state for SASL, SSL, GSS, etc */
void *sasl_state;
+ /* SSL structures */
+ bool ssl_in_use;
+ char *tls_finish; /* TLS finish message sent */
+ int tls_finish_len; /* length of TLS message sent */
+
#ifdef USE_SSL
bool allow_ssl_try; /* Allowed to try SSL negotiation */
bool wait_ssl_try; /* Delay SSL negotiation until after
* attempting normal connection */
- bool ssl_in_use;
#ifdef USE_OPENSSL
SSL *ssl; /* SSL status, if have SSL connection */
X509 *peer; /* X509 cert of server */
[cross-posting to pgjdbc]
On 25/05/17 17:17, Michael Paquier wrote:
Hi all,
Please find attached a patch to add support for channel binding for
SCRAM, to mitigate MITM attacks using this protocol. Per RFC5802
(https://tools.ietf.org/html/rfc5802), servers supporting channel
binding need to add support for tls-unique, and this is what this
patch does.
This is awesome, Michael :) Thank you! You have prevented me
eventually writing the patch and having then PostgreSQL source tainted
with Java-to-C "transpiled" code ^_^
As defined in RFC5056 (exactly here =>
https://tools.ietf.org/html/rfc5056#section-4.1), servers can use the
TLS finish message to determine if the client is actually the same as
the one who initiated the connection, to eliminate the risk of MITM
attacks. OpenSSL offers two undocumented APIs for this purpose:
- SSL_get_finished() to get the latest TLS finish message that the
client has sent.
- SSL_get_peer_finished(), to get the latest TLS finish message
expected by the server.
So basically what is needed here is saving the latest message
generated by client in libpq using get_finished(), send it to the
server using the SASL exchange messages (value sent with the final
client message), and then compare it with what the server expected.
Connection is successful if what the client has sent and what the
server expects match.There is also a clear documentation about what is expected from the
client and the server in terms of how both should behave using the
first client message https://tools.ietf.org/html/rfc5802#section-6. So
I have tried to follow it, reviews are welcome particularly regarding
that. The implementation is done in such a way that channel binding is
used in the context of an SSL connection, which is what the RFCs
expect from an implementation.
After a deeper analysis, I have some concerns/comments here:
- tls-unique, as you mentioned, uses two undocumented APIs. This raises
a small flag about the stability and future of those APIs.
- More importantly, some drivers (not libpq-based) may have unexpected
difficulties implementing tls-unique. In particular, there is not an API
in Java to get the finished message. I expect other languages to maybe
have similar limitations. For Java I see two options:
* Also implement tls-server-end-point, which rather relies on the
server certificate. This information seems to be exposed as part of the
Java APIs:
https://docs.oracle.com/javase/8/docs/api/java/security/cert/Certificate.html#getEncoded--
* Use the BouncyCastle library (http://bouncycastle.org/), which
explicitly supports tls-unique
(https://www.bouncycastle.org/docs/pkixdocs1.5on/org/bouncycastle/est/TLSUniqueProvider.html#getTLSUnique()
,
https://www.bouncycastle.org/docs/tlsdocs1.5on/org/bouncycastle/tls/ChannelBinding.html#tls_unique
). This would require, however, non-trivial changes to the driver and, I
expect, a lot of extra effort.
- It seems to me that tls-unique might be a bit fragile. In particular,
it requires the server to be aware of TSL renegotiations and avoid
performing one while the SCRAM authentication is being performed. I
don't know if this is already guaranteed by the current patch, but it
seems to me it requires complex interactions between levels of
abstraction that are quite separate (SSL and SCRAM). This is explained
by the RFC:
"
server applications MUST NOT request TLS renegotiation during phases of
the application protocol during which application-layer authentication
occurs
"
(https://tools.ietf.org/html/rfc5929#section-3.1)
Based on this facts, I'd suggest to either implement
tls-server-end-point or implement both tls-server-end-point and
tls-unique. The latter would require a more complete protocol message to
advertise separately SCRAM mechanisms and channel binding names. One of
such structures could be the one I suggested earlier:
/messages/by-id/df8c6e27-4d8e-5281-96e5-131a4e638fc8@8kdata.com
Before even that, the server needs to send to the client the list of
SASL mechanisms that are supported. This adds SCRAM-SHA-256-PLUS if
the server has been built with SSL support to the list.
And I'd say the list of channel binding names supported.
Álvaro
--
Álvaro Hernández Tortosa
-----------
<8K>data
--
Sent via pgsql-jdbc mailing list (pgsql-jdbc@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-jdbc
On Sat, May 27, 2017 at 5:59 PM, Álvaro Hernández Tortosa
<aht@8kdata.com> wrote:
On 25/05/17 17:17, Michael Paquier wrote:
Please find attached a patch to add support for channel binding for
SCRAM, to mitigate MITM attacks using this protocol. Per RFC5802
(https://tools.ietf.org/html/rfc5802), servers supporting channel
binding need to add support for tls-unique, and this is what this
patch does.This is awesome, Michael :) Thank you! You have prevented me eventually
writing the patch and having then PostgreSQL source tainted with Java-to-C
"transpiled" code ^_^
The thing would have been reviewed and rewritten a couple of times :)
So that would have unlikely (hopefully) finished in the shape of a
Java-C-like patch.
After a deeper analysis, I have some concerns/comments here:
- tls-unique, as you mentioned, uses two undocumented APIs. This raises a
small flag about the stability and future of those APIs.
Those are 5 lines of code each in OpenSSL, not that hard to maintain.
Those APIs are clunky by the way as there is no way to know in advance
the length of the message for memory allocation.
- More importantly, some drivers (not libpq-based) may have unexpected
difficulties implementing tls-unique. In particular, there is not an API in
Java to get the finished message. I expect other languages to maybe have
similar limitations. For Java I see two options:
* Also implement tls-server-end-point, which rather relies on the server
certificate. This information seems to be exposed as part of the Java APIs:
https://docs.oracle.com/javase/8/docs/api/java/security/cert/Certificate.html#getEncoded--
* Use the BouncyCastle library (http://bouncycastle.org/), which
explicitly supports tls-unique
(https://www.bouncycastle.org/docs/pkixdocs1.5on/org/bouncycastle/est/TLSUniqueProvider.html#getTLSUnique()
,
https://www.bouncycastle.org/docs/tlsdocs1.5on/org/bouncycastle/tls/ChannelBinding.html#tls_unique
). This would require, however, non-trivial changes to the driver and, I
expect, a lot of extra effort.
I am wondering how other languages are regarding that. Python has
added a method in 2011:
https://hg.python.org/cpython/rev/cb44fef5ea1d
- It seems to me that tls-unique might be a bit fragile. In particular, it
requires the server to be aware of TSL renegotiations and avoid performing
one while the SCRAM authentication is being performed. I don't know if this
is already guaranteed by the current patch, but it seems to me it requires
complex interactions between levels of abstraction that are quite separate
(SSL and SCRAM). This is explained by the RFC:
RFC 5802, section 6 gives a good level of details on the matter:
https://tools.ietf.org/html/rfc5802#section-6
There is indeed interaction between SSL and scram, and the patch makes
use of USE_SSL for this purpose.
"
server applications MUST NOT request TLS renegotiation during phases of the
application protocol during which application-layer authentication occurs
"
(https://tools.ietf.org/html/rfc5929#section-3.1)
The SSL hanshake happens only once at connection obtention, before the
authentication exchange happens. So there is no renegociation. SSL
renegotiation has been removed in PG anyway, disabled on
back-branches, and removed as well in TLS 1.3 (right? I don't recall
the spec in details).
Based on this facts, I'd suggest to either implement
tls-server-end-point or implement both tls-server-end-point and tls-unique.
The latter would require a more complete protocol message to advertise
separately SCRAM mechanisms and channel binding names. One of such
structures could be the one I suggested earlier:
/messages/by-id/df8c6e27-4d8e-5281-96e5-131a4e638fc8@8kdata.com
The RFC says that any server implementing channel binding must
implement tls-unique, so having only end-point is not compliant.
Before even that, the server needs to send to the client the list of
SASL mechanisms that are supported. This adds SCRAM-SHA-256-PLUS if
the server has been built with SSL support to the list.And I'd say the list of channel binding names supported.
It seems to me as well that this gives us an indication that it
should be the default channel binding used, or if the exchange list
has no channel names included, the client can assume that tls-unique
will be used. Such an approach has as well the merit to keep the patch
simple. In short, I would advocate an incremental approach that adds
tls-unique first. This has value anyway as there is no need for
certificate configuration. This also does not require a message format
extension.
--
Michael
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Sat, May 27, 2017 at 5:59 PM, Álvaro Hernández Tortosa
<aht@8kdata.com> wrote:
- tls-unique, as you mentioned, uses two undocumented APIs. This raises a
small flag about the stability and future of those APIs.
It seems to me that the question is not just whether those APIs will
be available in future versions of OpenSSL, but whether they will be
available in every current and future version of every SSL
implementation that we may wish to use in core or that any client may
wish to use. We've talked before about being able to use the Windows
native SSL implementation rather than OpenSSL and it seems that there
would be significant advantages in having that capability. Meanwhile,
a client that reimplements the PostgreSQL wire protocol is free to use
any SSL implementation it likes. If channel binding can't work unless
both sides are speaking OpenSSL, then I'd say we've blown it.
Also, Heikki pointed out in his PGCon talk that there's currently no
way for libpq to insist on the use of SCRAM rather than plain password
authentication or md5. So, if someone trojans the server, they only
need to hack it to ask the client for plaintext authentication rather
than SCRAM and the client will cheerfully hand over the password with
no questions asked. Presumably, we need to add a connection option to
insist (a) on SCRAM or (b) SCRAM + channel binding, or else this isn't
going to actually provide us with any increased security. Even
without that, channel binding will still let the server verify that
it's really connected to the client, but that's not really doing much
for us in terms of security because the client (who has the password)
can always let someone else impersonate it perfectly by just handing
over the password. Channel binding can't prevent that. It *could*
prevent someone from impersonating the *server* but not if the server
can disable the check whenever it likes by ceasing to advertise
channel binding as an available option -- with no notification to the
client that anything has changed.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-jdbc mailing list (pgsql-jdbc@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-jdbc
Robert Haas <robertmhaas@gmail.com> writes:
On Sat, May 27, 2017 at 5:59 PM, Álvaro Hernández Tortosa
<aht@8kdata.com> wrote:- tls-unique, as you mentioned, uses two undocumented APIs. This raises a
small flag about the stability and future of those APIs.
It seems to me that the question is not just whether those APIs will
be available in future versions of OpenSSL, but whether they will be
available in every current and future version of every SSL
implementation that we may wish to use in core or that any client may
wish to use. We've talked before about being able to use the Windows
native SSL implementation rather than OpenSSL and it seems that there
would be significant advantages in having that capability.
Another thing of the same sort that should be on our radar is making
use of Apple's TLS code on macOS. The handwriting on the wall is
unmistakable that they intend to stop shipping OpenSSL before long,
and I do not think we really want to be in a position of having to
bundle OpenSSL into our distribution on macOS.
I'm not volunteering to do that, mind you. But +1 for not tying new
features to any single TLS implementation.
regards, tom lane
--
Sent via pgsql-jdbc mailing list (pgsql-jdbc@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-jdbc
Tom, all,
* Tom Lane (tgl@sss.pgh.pa.us) wrote:
Robert Haas <robertmhaas@gmail.com> writes:
On Sat, May 27, 2017 at 5:59 PM, Álvaro Hernández Tortosa
<aht@8kdata.com> wrote:- tls-unique, as you mentioned, uses two undocumented APIs. This raises a
small flag about the stability and future of those APIs.It seems to me that the question is not just whether those APIs will
be available in future versions of OpenSSL, but whether they will be
available in every current and future version of every SSL
implementation that we may wish to use in core or that any client may
wish to use. We've talked before about being able to use the Windows
native SSL implementation rather than OpenSSL and it seems that there
would be significant advantages in having that capability.Another thing of the same sort that should be on our radar is making
use of Apple's TLS code on macOS. The handwriting on the wall is
unmistakable that they intend to stop shipping OpenSSL before long,
and I do not think we really want to be in a position of having to
bundle OpenSSL into our distribution on macOS.I'm not volunteering to do that, mind you. But +1 for not tying new
features to any single TLS implementation.
Work has been done in that area already, as well as for LibNSS support.
I've not had a chance to closely look over the proposed approach here,
but generally speaking, we need to be looking for a standard way to
generate and transmit the channel binding information across the
wire. The specific APIs are, certainly, going to be different between
different TLS implementations and I don't believe we need to be
particularly concerned with that (or if they change in the future), as
long as we abstract those APIs. Perhaps there's some question as to
just how to abstract them, but I'm at least a bit less concerned with
that as I expect we'll be able to adjust those abstraction layers later
(presuming they aren't exposed, which I wouldn't expect them to be).
I skimmed over RFC5929, which looks to be the correct RFC when it comes
to channel binding with TLS. Hopefully the various TLS implementations
work in a similar manner, following that RFC (or whichever is relevant).
If that wasn't the case then, for example, it wouldn't be possible to do
channel binding with a LibNSS client and an OpenSSL server or with other
combinations and I find that rather hard to believe.
Thanks!
Stephen
On Tue, May 30, 2017 at 8:14 AM, Stephen Frost <sfrost@snowman.net> wrote:
Work has been done in that area already, as well as for LibNSS support.
I've not had a chance to closely look over the proposed approach here,
but generally speaking, we need to be looking for a standard way to
generate and transmit the channel binding information across the
wire.
The RFC of SCRAM specifies that the client sends the type of channel
binding in its first message data, and then provides the contents of
the TLS message it generated in the challenge response.
The specific APIs are, certainly, going to be different between
different TLS implementations and I don't believe we need to be
particularly concerned with that (or if they change in the future), as
long as we abstract those APIs. Perhaps there's some question as to
just how to abstract them, but I'm at least a bit less concerned with
that as I expect we'll be able to adjust those abstraction layers later
(presuming they aren't exposed, which I wouldn't expect them to be).
The current patch fetches the TLS finish message data just after the
handshake in be_tls_open_server() using the dedicated OpenSSL API. We
could definitely go with a more generic routine on our side that
sticks with the concepts of be_tls_get_compression():
- For the backend, this routine returns an allocated string with the
contents of the expected TLS message, and its size:
char *be_tls_get_tls_finish(Port *, int *)
- For the frontend, a routine to get the generated TLS finish message:
char *pgtls_get_tls_finish(PGconn *, int *)
I skimmed over RFC5929, which looks to be the correct RFC when it comes
to channel binding with TLS. Hopefully the various TLS implementations
work in a similar manner, following that RFC (or whichever is relevant).
That's the one I use as a reference.
If that wasn't the case then, for example, it wouldn't be possible to do
channel binding with a LibNSS client and an OpenSSL server or with other
combinations and I find that rather hard to believe.
As far as I can see, Windows has some APIs to get the TLS finish message:
https://msdn.microsoft.com/en-us/library/windows/desktop/aa382420(v=vs.85).aspx
On macos though it is another story, I am not seeing anything:
https://developer.apple.com/reference/security/secure_transport#symbols
Depending on the SSL implementation the server is compiled with, it
will be up to the backend to decide if it should advertise to the
client the -PLUS mechanism or not, so we can stay modular here.
--
Michael
--
Sent via pgsql-jdbc mailing list (pgsql-jdbc@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-jdbc
Michael,
* Michael Paquier (michael.paquier@gmail.com) wrote:
On Tue, May 30, 2017 at 8:14 AM, Stephen Frost <sfrost@snowman.net> wrote:
The specific APIs are, certainly, going to be different between
different TLS implementations and I don't believe we need to be
particularly concerned with that (or if they change in the future), as
long as we abstract those APIs. Perhaps there's some question as to
just how to abstract them, but I'm at least a bit less concerned with
that as I expect we'll be able to adjust those abstraction layers later
(presuming they aren't exposed, which I wouldn't expect them to be).The current patch fetches the TLS finish message data just after the
handshake in be_tls_open_server() using the dedicated OpenSSL API. We
could definitely go with a more generic routine on our side that
sticks with the concepts of be_tls_get_compression():
- For the backend, this routine returns an allocated string with the
contents of the expected TLS message, and its size:
char *be_tls_get_tls_finish(Port *, int *)
- For the frontend, a routine to get the generated TLS finish message:
char *pgtls_get_tls_finish(PGconn *, int *)
Those look pretty reasonable to me and seem to go along with the
concepts from the RFC, making them hopefully the right API.
If that wasn't the case then, for example, it wouldn't be possible to do
channel binding with a LibNSS client and an OpenSSL server or with other
combinations and I find that rather hard to believe.As far as I can see, Windows has some APIs to get the TLS finish message:
https://msdn.microsoft.com/en-us/library/windows/desktop/aa382420(v=vs.85).aspx
Good.
On macos though it is another story, I am not seeing anything:
https://developer.apple.com/reference/security/secure_transport#symbols
That's a bit unfortunate, if that's the case. Perhaps Apple will see
fit to address that deficiency though.
Depending on the SSL implementation the server is compiled with, it
will be up to the backend to decide if it should advertise to the
client the -PLUS mechanism or not, so we can stay modular here.
Right, of course.
All-in-all, this sounds like it's heading in the right direction, at
least at a high level. Glad to see that there's been consideration of
other TLS implementations, and as such I don't think we need to be
overly concerned about the specifics of the OpenSSL API here.
Thanks!
Stephen
On 30 May 2017, at 18:25, Michael Paquier <michael.paquier@gmail.com> wrote:
On macos though it is another story, I am not seeing anything:
https://developer.apple.com/reference/security/secure_transport#symbols
The Secure Transport documentation unfortunately excel at being incomplete and
hard to search in. That being said, I think you’re right, AFAICT there is no
published API for this (SSLProcessFinished() seems like the best match but is
not public).
Depending on the SSL implementation the server is compiled with, it
will be up to the backend to decide if it should advertise to the
client the -PLUS mechanism or not, so we can stay modular here.
+1
cheers ./daniel
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 30 May 2017, at 16:50, Tom Lane <tgl@sss.pgh.pa.us> wrote:
Robert Haas <robertmhaas@gmail.com> writes:
On Sat, May 27, 2017 at 5:59 PM, Álvaro Hernández Tortosa
<aht@8kdata.com> wrote:- tls-unique, as you mentioned, uses two undocumented APIs. This raises a
small flag about the stability and future of those APIs.It seems to me that the question is not just whether those APIs will
be available in future versions of OpenSSL, but whether they will be
available in every current and future version of every SSL
implementation that we may wish to use in core or that any client may
wish to use. We've talked before about being able to use the Windows
native SSL implementation rather than OpenSSL and it seems that there
would be significant advantages in having that capability.Another thing of the same sort that should be on our radar is making
use of Apple's TLS code on macOS. The handwriting on the wall is
unmistakable that they intend to stop shipping OpenSSL before long,
and I do not think we really want to be in a position of having to
bundle OpenSSL into our distribution on macOS.I'm not volunteering to do that, mind you. But +1 for not tying new
features to any single TLS implementation.
Big +1. The few settings we have already make it hard to provide other
implementations as drop-in replacements (Secure Transport doesn’t support
.crl files for example, only CRL loaded in Keychains).
cheers ./daniel
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Tue, May 30, 2017 at 1:00 PM, Stephen Frost <sfrost@snowman.net> wrote:
All-in-all, this sounds like it's heading in the right direction, at
least at a high level. Glad to see that there's been consideration of
other TLS implementations, and as such I don't think we need to be
overly concerned about the specifics of the OpenSSL API here.
That sounds like undue optimism to me. Unless somebody's tested that
Michael's proposed implementation, which uses undocumented OpenSSL
APIs, actually interoperates properly with a SCRAM + channel binding
implementation based on some other underlying SSL implementation, we
can't really know that it's going to work. It's not like we're
calling SSL_do_the_right_thing_for_channel_binding_thing_per_rfc5929().
We're calling SSL_do_something_undocumented() and hoping that
something_undocumented ==
the_right_thing_for_channel_binding_thing_per_rfc5929. Could be true,
but without actual interoperability testing it sounds pretty
speculative to me.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Tue, May 30, 2017 at 5:59 PM, Robert Haas <robertmhaas@gmail.com> wrote:
That sounds like undue optimism to me. Unless somebody's tested that
Michael's proposed implementation, which uses undocumented OpenSSL
APIs, actually interoperates properly with a SCRAM + channel binding
implementation based on some other underlying SSL implementation, we
can't really know that it's going to work. It's not like we're
calling SSL_do_the_right_thing_for_channel_binding_thing_per_rfc5929().
We're calling SSL_do_something_undocumented() and hoping that
something_undocumented ==
the_right_thing_for_channel_binding_thing_per_rfc5929. Could be true,
but without actual interoperability testing it sounds pretty
speculative to me.
Hm? Python is using that stuff for a certain amount of years now, for
the same goal of providing channel binding for SSL authentication. You
can look at this thread:
https://bugs.python.org/issue12551
So qualifying that as a speculative method sounds a quite hard word to me.
--
Michael
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 31/05/17 03:39, Michael Paquier wrote:
On Tue, May 30, 2017 at 5:59 PM, Robert Haas <robertmhaas@gmail.com> wrote:
That sounds like undue optimism to me. Unless somebody's tested that
Michael's proposed implementation, which uses undocumented OpenSSL
APIs, actually interoperates properly with a SCRAM + channel binding
implementation based on some other underlying SSL implementation, we
can't really know that it's going to work. It's not like we're
calling SSL_do_the_right_thing_for_channel_binding_thing_per_rfc5929().
We're calling SSL_do_something_undocumented() and hoping that
something_undocumented ==
the_right_thing_for_channel_binding_thing_per_rfc5929. Could be true,
but without actual interoperability testing it sounds pretty
speculative to me.Hm? Python is using that stuff for a certain amount of years now, for
the same goal of providing channel binding for SSL authentication. You
can look at this thread:
https://bugs.python.org/issue12551
So qualifying that as a speculative method sounds a quite hard word to me.
Actually this Python patch seems to ultimately rely on the same
OpenSSL functions as your implementation.
I don't have anything against implementing tls-unique, specially as
per the RFC it is mandatory (if channel binding support is provided).
However, given the use of undocumented API, given that at least the Java
driver would have a very difficult time implementing it (basically
replacing Java's standard SSL stack with a completely external one,
BouncyCastle, which adds load, size and complexity), I would rather
focus the efforts on tls-server-end-point which only needs to access the
certificate data, something that most APIs/frameworks/languages seem to
cover much more naturally than tls-unique.
Both are equally safe and effective and so having support for both
seems sensible to me.
Álvaro
--
Álvaro Hernández Tortosa
-----------
<8K>data
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Robert,
* Robert Haas (robertmhaas@gmail.com) wrote:
but without actual interoperability testing it sounds pretty
speculative to me.
I'm all for interoperability testing.
When we have multiple implementations of TLS using different libraries
with various versions of PostgreSQL and libpq and are able to test those
against other versions of PostgreSQL and libpq compiled with other TLS
libraries, I'll be downright ecstatic. We are a small ways from that
right now, however, and I don't believe that we should be asking the
implementors of channel binding to also implement support for multiple
TLS libraries in PostgreSQL in order to test that their RFC-following
(at least, as far as they can tell) implementation actually works.
I'm not exactly sure what to characterize that as, given that the old
fall-back of "feature creep" feels woefully inadequate as a description.
Thanks!
Stephen
On Tue, May 30, 2017 at 11:49 PM, Stephen Frost <sfrost@snowman.net> wrote:
... and I don't believe that we should be asking the
implementors of channel binding to also implement support for multiple
TLS libraries in PostgreSQL in order to test that their RFC-following
(at least, as far as they can tell) implementation actually works.
You're of course free to believe what you wish, but that sounds
short-sighted to me. If we implement channel binding and it turns out
not to be interoperable with other SSL implementations, then what? We
can't change it later without breaking compatibility with our own
prior implementation. Note that Álvaro Hernández Tortosa said about
two hours before you sent this email that it doesn't seem possible to
implement something comparable in Java's standard SSL stack. If
that's the case, adopting this implementation is dooming everyone who
connects to the database server using JDBC to be unable to use channel
binding. And that's a large percentage of our user base.
And then what happens when we do add support for Windows SSL or macOS
SSL? It sounds like you're equally willing to throw people using
those implementations under the bus. So we'll end up with channel
binding that works only when everybody's running the same operating
system and nobody's using Java. Uggh. At that point you might as
well just go back to using SSL certificates to solve this problem. If
you use SSL certificates, then (1) it should work with any SSL
implementation and (2) the client can force SSL certificate
verification, whereas currently the client cannot force even the use
of SCRAM, let alone channel binding.
So what is being proposed here does not (yet, anyway) provide any
actual security and seems to have poor prospects for interoperability,
whereas we have an existing feature (SSL certificates) that has
neither of those problems. Are you sure you're not putting
buzzword-compliance ahead of real progress?
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-jdbc mailing list (pgsql-jdbc@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-jdbc
On Tue, May 30, 2017 at 6:50 PM, Álvaro Hernández Tortosa
<aht@8kdata.com> wrote:
Actually this Python patch seems to ultimately rely on the same OpenSSL
functions as your implementation.
Yup. My point is that even if those APIs are undocumented, I think
that they are not going to disappear either.
I don't have anything against implementing tls-unique, specially as per
the RFC it is mandatory (if channel binding support is provided). However,
given the use of undocumented API, given that at least the Java driver would
have a very difficult time implementing it (basically replacing Java's
standard SSL stack with a completely external one, BouncyCastle, which adds
load, size and complexity), I would rather focus the efforts on
tls-server-end-point which only needs to access the certificate data,
something that most APIs/frameworks/languages seem to cover much more
naturally than tls-unique.
Point taken. Well, this brings us to the point where we should have
both tls-unique and end-point to allow JDBC to work with it. Now,
thinking about it, do we really need to advertise the available
channel binding types when the client chooses the -PLUS mechanism?
Wouldn't it make sense to let the client choose what it thinks is
better and just fail the exchange with the backend if the channel type
is not supported?
Both are equally safe and effective and so having support for both seems
sensible to me.
I have some automatic setups that would be really simplified by just
using libpq with SSL connections if there is channel binding. And that
involves certificate deployments, I think I am not the only one to see
that present even if JDBC just supports end-point.
--
Michael
--
Sent via pgsql-jdbc mailing list (pgsql-jdbc@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-jdbc
Robert,
* Robert Haas (robertmhaas@gmail.com) wrote:
On Tue, May 30, 2017 at 11:49 PM, Stephen Frost <sfrost@snowman.net> wrote:
... and I don't believe that we should be asking the
implementors of channel binding to also implement support for multiple
TLS libraries in PostgreSQL in order to test that their RFC-following
(at least, as far as they can tell) implementation actually works.You're of course free to believe what you wish, but that sounds
short-sighted to me. If we implement channel binding and it turns out
not to be interoperable with other SSL implementations, then what? We
can't change it later without breaking compatibility with our own
prior implementation. Note that Álvaro Hernández Tortosa said about
two hours before you sent this email that it doesn't seem possible to
implement something comparable in Java's standard SSL stack. If
that's the case, adopting this implementation is dooming everyone who
connects to the database server using JDBC to be unable to use channel
binding. And that's a large percentage of our user base.
I'm hopeful that we're closer to agreement here than we are the
opposite.
It was absolutely not my intent that the ongoing discussion between
Alvaro and Michael be stopped or changed, quite the opposite, in fact.
If your comments regarding the requirement that we have interoperability
testing of this feature before accepting it were intended to mean that
we need to make sure that the JDBC driver is able to work with the
chosen solution (or, perhaps, that we support multuple options, one of
which works with the JDBC changes), and that we review the other TLS
libraries with the goal of making sure that what we agree on in core
should work with those also, then that's great and exactly what I'd like
to see also.
If your comments regarding the requirement that we have interoperability
testing of this feature before accepting it were intended to mean that
we must have a complete alternative TLS solution, while that would
actually play a great deal to a goal I've had for a while to have PG
support multiple TLS implementations (LibNSS being top-of-mind, at least
for me, as I've mentioned a couple times), I can't agree that we should
require that before accepting channel binding as a feature. To do so
would be akin to requiring that we support multiple TLS implementations
before we agreed to support client-side certificates, in my opinion.
I would be rather surprised if the solution which Michael and Alvaro
come to results in a solution which requires us to break compatibility
down the road, though it's of course a risk we need to consider. The
ongoing discussion around finding a way to support both methods outlined
in the RFC sounds exactly correct to me and I encourage further
discussion there.
And then what happens when we do add support for Windows SSL or macOS
SSL? It sounds like you're equally willing to throw people using
those implementations under the bus. So we'll end up with channel
binding that works only when everybody's running the same operating
system and nobody's using Java. Uggh. At that point you might as
well just go back to using SSL certificates to solve this problem. If
you use SSL certificates, then (1) it should work with any SSL
implementation and (2) the client can force SSL certificate
verification, whereas currently the client cannot force even the use
of SCRAM, let alone channel binding.
I hope it's clear that it's not my intent to throw anyone "under the
bus." The only reason that "SSL certificates should work with any SSL
implementation" is because those SSL implementations are based on RFCs
which define how they should work and what I was encouraging up-thread
is exactly that we should be looking at RFCs to determine the right path
forward. There are cases, of course, where the RFCs have alternatives
and the ideal approach is to find a way to work with all of those
alternatives, or at least implement a solution which is flexible, such
that later changes could add support without breaking existing users.
I'm certainly on-board with the general idea of having a way for the
client to require both SCRAM and channel binding and I agree that's a
hole in our current system, but that is really an orthogonal concern.
So what is being proposed here does not (yet, anyway) provide any
actual security and seems to have poor prospects for interoperability,
whereas we have an existing feature (SSL certificates) that has
neither of those problems. Are you sure you're not putting
buzzword-compliance ahead of real progress?
Adding support for channel binding is quite important and valuable, in
its own right. I concur that we want to provide ways for the client to
require SCRAM and to require channel binding, but I don't see any
particular reason that adding support for channel binding has to be held
up behind that.
As for your question, I have to say that I find it inappropriate to
characterize channel binding as a "buzzword" or to suggest that this
particular feature which Michael and Alvaro are working to add to PG is
only being done to fulfill some marketing requirement or checkbox but
without adding any technical merit in its own right. Channel binding
isn't new, it's supported by quite a few different technologies, and we
would absolutely be better off including support for it, from an
entirely technical perspective. If I were one of the individuals
working on this feature, I'd be rather put-off and upset at the
suggestion that the good work they're doing is viewed by the PostgreSQL
community and one of our major committers as only being done to check a
box or to be "buzzword compliant" and ultimately without technical
merit.
Thankfully, I know both Michael and Alvaro and had excellent discussions
with them at PGCon last week, and I don't doubt that they will continue
their efforts regardless, but I worry about the signal it sends to
individuals who are not yet contributors or who have not gone through
the process of trying to contribute to PostgreSQL. We make it more than
difficult enough to get code into core, let's try to avoid discouraging
contributors by casting doubt on the technical merits of their efforts
when they're very clearly adding a useful feature, even if it isn't
perfect until we also add X, Y or Z. Instead, let's encourage their
efforts to complete the work they've started and then work with them to
add those other components necessary to have a complete solution.
Thanks!
Stephen
On Wed, May 31, 2017 at 7:59 PM, Stephen Frost <sfrost@snowman.net> wrote:
If your comments regarding the requirement that we have interoperability
testing of this feature before accepting it were intended to mean that
we need to make sure that the JDBC driver is able to work with the
chosen solution (or, perhaps, that we support multuple options, one of
which works with the JDBC changes), and that we review the other TLS
libraries with the goal of making sure that what we agree on in core
should work with those also, then that's great and exactly what I'd like
to see also.
OK, great. That's what I mean (mostly - see below).
If your comments regarding the requirement that we have interoperability
testing of this feature before accepting it were intended to mean that
we must have a complete alternative TLS solution, while that would
actually play a great deal to a goal I've had for a while to have PG
support multiple TLS implementations (LibNSS being top-of-mind, at least
for me, as I've mentioned a couple times), I can't agree that we should
require that before accepting channel binding as a feature. To do so
would be akin to requiring that we support multiple TLS implementations
before we agreed to support client-side certificates, in my opinion.
I don't think that we need to have a committed patch allowing multiple
SSL implementations before we accept a patch for channel binding, but
I think it might be a good idea for someone to hack either libpq or
the server enough to make it work with some other SSL implementation
(Windows SSL would probably be the best candidate) and test that what
we think will work for channel binding actually does work. It
wouldn't need to be a committable patch, but it should be enough to
make a successful connection in the laboratory. Now, there might also
be other ways besides that of testing that interoperability is
possible, so don't take me as being of the view that someone has to
necessarily do exactly that thing that I just said. But I do think
that we probably should do more than say "hey, we've got this
undocumented SSL API, and this other Windows API, and it looks like
they do about the same thing, so we're good". There's sometimes a big
difference between what sounds like it should work on paper and what
actually does work.
I would be rather surprised if the solution which Michael and Alvaro
come to results in a solution which requires us to break compatibility
down the road, though it's of course a risk we need to consider. The
ongoing discussion around finding a way to support both methods outlined
in the RFC sounds exactly correct to me and I encourage further
discussion there.
Sure, I have no objection to the discussion. Discussion is always
cool; what I'm worried about is a tls-unique implementation that is
supremely unportable, and there's no current evidence that the
currently-proposed patch is anything else. There is some optimism,
but optimism <> evidence.
I'm certainly on-board with the general idea of having a way for the
client to require both SCRAM and channel binding and I agree that's a
hole in our current system, but that is really an orthogonal concern.
Orthogonal but *very* closely related.
entirely technical perspective. If I were one of the individuals
working on this feature, I'd be rather put-off and upset at the
suggestion that the good work they're doing is viewed by the PostgreSQL
community and one of our major committers as only being done to check a
box or to be "buzzword compliant" and ultimately without technical
merit.
I did not say that the feature was "ultimately without technical
merit"; I said that the patch as submitted seemed like it might not
interoperate, and that without a libpq option to force it to be used
it wouldn't add any real security. I stand by those statements and I
think it is 100% appropriate for me to raise those issues. Other
people, including you, do the same thing about other patches all the
time. It is not a broadside against the contributors or the patch; it
is a short, specific list of concerns that are IMHO quite justifiable.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Robert,
* Robert Haas (robertmhaas@gmail.com) wrote:
On Wed, May 31, 2017 at 7:59 PM, Stephen Frost <sfrost@snowman.net> wrote:
If your comments regarding the requirement that we have interoperability
testing of this feature before accepting it were intended to mean that
we need to make sure that the JDBC driver is able to work with the
chosen solution (or, perhaps, that we support multuple options, one of
which works with the JDBC changes), and that we review the other TLS
libraries with the goal of making sure that what we agree on in core
should work with those also, then that's great and exactly what I'd like
to see also.OK, great. That's what I mean (mostly - see below).
Glad to hear it.
If your comments regarding the requirement that we have interoperability
testing of this feature before accepting it were intended to mean that
we must have a complete alternative TLS solution, while that would
actually play a great deal to a goal I've had for a while to have PG
support multiple TLS implementations (LibNSS being top-of-mind, at least
for me, as I've mentioned a couple times), I can't agree that we should
require that before accepting channel binding as a feature. To do so
would be akin to requiring that we support multiple TLS implementations
before we agreed to support client-side certificates, in my opinion.I don't think that we need to have a committed patch allowing multiple
SSL implementations before we accept a patch for channel binding, but
I think it might be a good idea for someone to hack either libpq or
the server enough to make it work with some other SSL implementation
(Windows SSL would probably be the best candidate) and test that what
we think will work for channel binding actually does work. It
wouldn't need to be a committable patch, but it should be enough to
make a successful connection in the laboratory. Now, there might also
be other ways besides that of testing that interoperability is
possible, so don't take me as being of the view that someone has to
necessarily do exactly that thing that I just said. But I do think
that we probably should do more than say "hey, we've got this
undocumented SSL API, and this other Windows API, and it looks like
they do about the same thing, so we're good". There's sometimes a big
difference between what sounds like it should work on paper and what
actually does work.
I certainly wouldn't object to someone working on this, but I feel like
it's a good deal more work than perhaps you're realizing (and I tend to
think trying to use the Windows SSL implementation would increase the
level of effort, not minimize it). Perhaps it wouldn't be too bad to
write a one-off piece of code which just connects and then returns the
channel binding information on each side and then one could hand-check
that what's returned matches what it's supposed to based on the RFC, but
if it doesn't, then what? In the specific case we're talking about,
there's two approaches in the RFC and it seems like we should support
both because at least our current JDBC implementation only works with
one, and ideally we can allow for that to be extended to other methods,
but I wouldn't want to implement a method that only works on Windows SSL
because that implementation, for whatever reason, doesn't follow either
of the methods available in the RFC.
Thanks!
Stephen
On Wed, May 31, 2017 at 09:37:02AM -0400, Robert Haas wrote:
On Tue, May 30, 2017 at 11:49 PM, Stephen Frost <sfrost@snowman.net> wrote:
... and I don't believe that we should be asking the
implementors of channel binding to also implement support for multiple
TLS libraries in PostgreSQL in order to test that their RFC-following
(at least, as far as they can tell) implementation actually works.You're of course free to believe what you wish, but that sounds
short-sighted to me. If we implement channel binding and it turns out
not to be interoperable with other SSL implementations, then what? We
can't change it later without breaking compatibility with our own
prior implementation. Note that �lvaro Hern�ndez Tortosa said about
two hours before you sent this email that it doesn't seem possible to
implement something comparable in Java's standard SSL stack. If
that's the case, adopting this implementation is dooming everyone who
connects to the database server using JDBC to be unable to use channel
binding. And that's a large percentage of our user base.
Just to step back, exactly how does channel binding work? Is each side
of the SSL connection hashing the password hash with the shared SSL
session secret in some way that each side knows the other end knows
the password hash, but not disclosing the secret or password hash? Is
there some other way JDBC can get that information?
--
Bruce Momjian <bruce@momjian.us> http://momjian.us
EnterpriseDB http://enterprisedb.com
+ As you are, so once was I. As I am, so you will be. +
+ Ancient Roman grave inscription +
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Thu, Jun 1, 2017 at 11:50 AM, Stephen Frost <sfrost@snowman.net> wrote:
I certainly wouldn't object to someone working on this, but I feel like
it's a good deal more work than perhaps you're realizing (and I tend to
think trying to use the Windows SSL implementation would increase the
level of effort, not minimize it).
I agree that it's a fair amount of work, but if nobody does it, then I
think it's pretty speculative to suppose that the feature actually
works correctly.
Perhaps it wouldn't be too bad to
write a one-off piece of code which just connects and then returns the
channel binding information on each side and then one could hand-check
that what's returned matches what it's supposed to based on the RFC, but
if it doesn't, then what?
Then something's broken and we need to fix it before we start
committing patches.
In the specific case we're talking about,
there's two approaches in the RFC and it seems like we should support
both because at least our current JDBC implementation only works with
one, and ideally we can allow for that to be extended to other methods,
but I wouldn't want to implement a method that only works on Windows SSL
because that implementation, for whatever reason, doesn't follow either
of the methods available in the RFC.
I am very skeptical that if two people on two different SSL
interpretations both do something that appears to comply with the RFC,
we can just assume those two people will get the same answer. In an
ideal world, that would definitely work, but in the real world, I
think it needs to be tested or you don't really know. I agree that if
a given SSL implementation is such that it can't support either of the
possible channel binding methods, then that's not our problem; people
using that SSL implementation just can't get channel binding, and if
they don't like that they can switch SSL implementations. But I also
think that if two SSL implementations both claim to support what's
needed to make channel binding work and we don't do any testing that
they actually interoperate, then I don't think we can really know that
we've got it right.
Another way of validating Michael's problem which I would find
satisfactory is to go look at some other OpenSSL-based implementations
of channel binding. If in each case they are using the same functions
that Michael used in the same way, then that's good evidence that his
implementation is doing the right thing, especially if any of those
implementations also support other SSL implementations and have
verified that the OpenSSL mechanism does in fact interoperate.
I don't really know why we're arguing about this. It seems blindingly
obvious to me that we can't just take it on faith that the functions
Michael identified for this purpose are the right ones and will work
perfectly in complete compliance with the RFC. We are in general very
reluctant to make such assumptions and if we were going to start,
changes that affect wire protocol compatibility wouldn't be my first
choice. Is it really all that revolutionary or controversial to
suggest that this patch has not yet had enough validation to really
know that it does what we want? To me, it seems like verifying that a
patch which supposedly implements a standard interoperates with
something other than itself is so obvious that it should barely need
to be mentioned, let alone become a bone of contention.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-jdbc mailing list (pgsql-jdbc@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-jdbc
On 01/06/17 18:11, Bruce Momjian wrote:
On Wed, May 31, 2017 at 09:37:02AM -0400, Robert Haas wrote:
On Tue, May 30, 2017 at 11:49 PM, Stephen Frost <sfrost@snowman.net> wrote:
... and I don't believe that we should be asking the
implementors of channel binding to also implement support for multiple
TLS libraries in PostgreSQL in order to test that their RFC-following
(at least, as far as they can tell) implementation actually works.You're of course free to believe what you wish, but that sounds
short-sighted to me. If we implement channel binding and it turns out
not to be interoperable with other SSL implementations, then what? We
can't change it later without breaking compatibility with our own
prior implementation. Note that Álvaro Hernández Tortosa said about
two hours before you sent this email that it doesn't seem possible to
implement something comparable in Java's standard SSL stack. If
that's the case, adopting this implementation is dooming everyone who
connects to the database server using JDBC to be unable to use channel
binding. And that's a large percentage of our user base.Just to step back, exactly how does channel binding work? Is each side
of the SSL connection hashing the password hash with the shared SSL
session secret in some way that each side knows the other end knows
the password hash, but not disclosing the secret or password hash? Is
there some other way JDBC can get that information?
In short, channel binding augments SCRAM (could be also other
authentication methods, I guess) by adding to the mix of data to be
exchanged, data that comes off-band. Normal SCRAM exchange involves
user, a unique token, a salt and some other information. If you add into
the mix off-band information, that is uniquely known by only client and
server, you can avoid MITM attacks. It's not as simple as "hashing" the
password, though. SCRAM involves 4 steps (2 messages each way) with
somehow complex hashing of data in the last 2 steps.
There are basically 2 channel binding options, that is, 2 possible
data pieces that could be taken off-band of the SCRAM authentication and
throw them into this mix:
- tls-unique. It's like a unique identifier for each client-server SSL
connection. It should be get from the SSL channel and there doesn't seem
to be a super consistent way of getting it from the channel (in OpenSSL
is an undocumented API, it's not available in normal Java stack, etc).
- tls-server-end-point. Channel binding data is just a hash of a SSL
certificate, as is. As such, seems to be easier to be supported across
multiple OSs/SSL stacks.
However, SCRAM RFC states that if channel binding is implemented,
at least tls-unique must be implemented, with others being optional
(there is as of today a third one, but only applies to telnet). So in my
opinion, I'd implement both of the above, for maximum flexibility, and
add a field to the required scram authentication febe message with a
list (aka CSV) of the channel binding mechanisms supported by this
server. In this way, I believe covers support for several channel
binding mechanisms and could be extended in the future should other
mechanisms appear.
Álvaro
--
Álvaro Hernández Tortosa
-----------
<8K>data
--
Sent via pgsql-jdbc mailing list (pgsql-jdbc@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-jdbc
On 01/06/17 17:50, Stephen Frost wrote:
Robert,
* Robert Haas (robertmhaas@gmail.com) wrote:
On Wed, May 31, 2017 at 7:59 PM, Stephen Frost <sfrost@snowman.net> wrote:
If your comments regarding the requirement that we have interoperability
testing of this feature before accepting it were intended to mean that
we need to make sure that the JDBC driver is able to work with the
chosen solution (or, perhaps, that we support multuple options, one of
which works with the JDBC changes), and that we review the other TLS
libraries with the goal of making sure that what we agree on in core
should work with those also, then that's great and exactly what I'd like
to see also.OK, great. That's what I mean (mostly - see below).
Glad to hear it.
If your comments regarding the requirement that we have interoperability
testing of this feature before accepting it were intended to mean that
we must have a complete alternative TLS solution, while that would
actually play a great deal to a goal I've had for a while to have PG
support multiple TLS implementations (LibNSS being top-of-mind, at least
for me, as I've mentioned a couple times), I can't agree that we should
require that before accepting channel binding as a feature. To do so
would be akin to requiring that we support multiple TLS implementations
before we agreed to support client-side certificates, in my opinion.I don't think that we need to have a committed patch allowing multiple
SSL implementations before we accept a patch for channel binding, but
I think it might be a good idea for someone to hack either libpq or
the server enough to make it work with some other SSL implementation
(Windows SSL would probably be the best candidate) and test that what
we think will work for channel binding actually does work. It
wouldn't need to be a committable patch, but it should be enough to
make a successful connection in the laboratory. Now, there might also
be other ways besides that of testing that interoperability is
possible, so don't take me as being of the view that someone has to
necessarily do exactly that thing that I just said. But I do think
that we probably should do more than say "hey, we've got this
undocumented SSL API, and this other Windows API, and it looks like
they do about the same thing, so we're good". There's sometimes a big
difference between what sounds like it should work on paper and what
actually does work.I certainly wouldn't object to someone working on this, but I feel like
it's a good deal more work than perhaps you're realizing (and I tend to
think trying to use the Windows SSL implementation would increase the
level of effort, not minimize it). Perhaps it wouldn't be too bad to
write a one-off piece of code which just connects and then returns the
channel binding information on each side and then one could hand-check
that what's returned matches what it's supposed to based on the RFC, but
if it doesn't, then what? In the specific case we're talking about,
there's two approaches in the RFC and it seems like we should support
both because at least our current JDBC implementation only works with
one, and ideally we can allow for that to be extended to other methods,
but I wouldn't want to implement a method that only works on Windows SSL
because that implementation, for whatever reason, doesn't follow either
of the methods available in the RFC.
To make things even more complicated, I think the RFC is not
helping very much, as the definition is not very clear itself:
"
Description: The first TLS Finished message sent (note: the Finished
struct, not the TLS record layer message containing it) in the most
recent TLS handshake of the TLS connection being bound to (note: TLS
connection, not session, so that the channel binding is specific to
each connection regardless of whether session resumption is used).
If TLS renegotiation takes place before the channel binding
operation, then the first TLS Finished message sent of the latest/
inner-most TLS connection is used. Note that for full TLS
handshakes, the first Finished message is sent by the client, while
for abbreviated TLS handshakes (session resumption), the first
Finished message is sent by the server.
"
https://tools.ietf.org/html/rfc5929#section-3.1
If you read further, it becomes even less clear what it is and that
if there are re-negotiations, it gets more complicated. Server-end-point
is kind of better specified:
"
The hash of the TLS server's certificate [RFC5280] as it
appears, octet for octet, in the server's Certificate message.
"
�lvaro
--
�lvaro Hern�ndez Tortosa
-----------
<8K>data
--
Sent via pgsql-jdbc mailing list (pgsql-jdbc@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-jdbc
Robert,
* Robert Haas (robertmhaas@gmail.com) wrote:
On Thu, Jun 1, 2017 at 11:50 AM, Stephen Frost <sfrost@snowman.net> wrote:
I certainly wouldn't object to someone working on this, but I feel like
it's a good deal more work than perhaps you're realizing (and I tend to
think trying to use the Windows SSL implementation would increase the
level of effort, not minimize it).I agree that it's a fair amount of work, but if nobody does it, then I
think it's pretty speculative to suppose that the feature actually
works correctly.
We've considered "works with OpenSSL" (and, to some extent, JDBC, but
I'm pretty sure that came later and just happened to be able to work...)
to be sufficient to include things like client-side certificate based
authentication, so this is raising the bar quite a bit for a feature
that, while important and valuable, frankly isn't as important as
client-side cert auth.
Perhaps it wouldn't be too bad to
write a one-off piece of code which just connects and then returns the
channel binding information on each side and then one could hand-check
that what's returned matches what it's supposed to based on the RFC, but
if it doesn't, then what?Then something's broken and we need to fix it before we start
committing patches.
... Or that implementation doesn't follow the RFC, which is what I was
getting at.
In the specific case we're talking about,
there's two approaches in the RFC and it seems like we should support
both because at least our current JDBC implementation only works with
one, and ideally we can allow for that to be extended to other methods,
but I wouldn't want to implement a method that only works on Windows SSL
because that implementation, for whatever reason, doesn't follow either
of the methods available in the RFC.I am very skeptical that if two people on two different SSL
interpretations both do something that appears to comply with the RFC,
we can just assume those two people will get the same answer. In an
ideal world, that would definitely work, but in the real world, I
think it needs to be tested or you don't really know. I agree that if
a given SSL implementation is such that it can't support either of the
possible channel binding methods, then that's not our problem; people
using that SSL implementation just can't get channel binding, and if
they don't like that they can switch SSL implementations. But I also
think that if two SSL implementations both claim to support what's
needed to make channel binding work and we don't do any testing that
they actually interoperate, then I don't think we can really know that
we've got it right.
I'm all for doing testing, as I've tried to point out a few times, and I
would like to see an implementation which is able to be extended in the
future to other channel binding methods, as we clearly need to support
at least the two listed in the RFC based on this discussion and there
might be a still better way down the road anyway.
Another way of validating Michael's problem which I would find
satisfactory is to go look at some other OpenSSL-based implementations
of channel binding. If in each case they are using the same functions
that Michael used in the same way, then that's good evidence that his
implementation is doing the right thing, especially if any of those
implementations also support other SSL implementations and have
verified that the OpenSSL mechanism does in fact interoperate.
I don't have any issue with asking that Michael, or someone, to go look
at other OpenSSL-using implementations which support channel binding.
I don't really know why we're arguing about this. It seems blindingly
obvious to me that we can't just take it on faith that the functions
Michael identified for this purpose are the right ones and will work
perfectly in complete compliance with the RFC. We are in general very
reluctant to make such assumptions and if we were going to start,
changes that affect wire protocol compatibility wouldn't be my first
choice. Is it really all that revolutionary or controversial to
suggest that this patch has not yet had enough validation to really
know that it does what we want? To me, it seems like verifying that a
patch which supposedly implements a standard interoperates with
something other than itself is so obvious that it should barely need
to be mentioned, let alone become a bone of contention.
As I said in an earlier reply, I'm hopefuly that we're closer to
agreement here than we are disagreement, but there seems to be some
confusion regarding my position on this specific patch. I'm not
advocating for this patch to be committed as-is or even anytime soon,
and I'm unsure of where I gave that impression. I'm encouraged by the
ongoing discussion between Michael and Alvaro and hope to see a new
patch down the road which incorporates the results of their discussion
and works with both OpenSSL and the JDBC implementation, at least.
What I find somewhat objectionable is the notion that if we don't have 5
different TLS/SSL implementations supported in PG and that we've tested
that channel binding works correctly among all combinations of all of
them, then we can't accept a patch implementing it. I'm exaggerating
for effect here, of course, and you've agreed that a full, committable,
implementation isn't necessary, and I agreed with that, but we still
seem to have a different level of expectation regarding what needs doing
here. I don't know that we'll ever be able to nail down the exact
location of the line in the sand that needs to be crossed here, or agree
to it, so I'd suggest that we let them continue their efforts to work
together and provide a patch which they feel is ready to be commented on
by committers. Perhaps we'll find that, regardless of where the line
was drawn exactly, they've crossed it.
Thanks!
Stephen
On Fri, Jun 2, 2017 at 10:08 AM, Stephen Frost <sfrost@snowman.net> wrote:
What I find somewhat objectionable is the notion that if we don't have 5
different TLS/SSL implementations supported in PG and that we've tested
that channel binding works correctly among all combinations of all of
them, then we can't accept a patch implementing it.
It seems to me that any testing in this area won't fly high as long as
there is no way to enforce the list of TLS implementations that a
server allows. There have been discussions about being able to control
that after the OpenSSL vulnerabilities that were protocol-specific and
there were even patches adding GUCs for this purpose. At the end,
everything has been rejected as Postgres enforces the use of the
newest one when doing the SSL handshake.
--
Michael
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Thu, Jun 1, 2017 at 9:13 PM, Michael Paquier
<michael.paquier@gmail.com> wrote:
On Fri, Jun 2, 2017 at 10:08 AM, Stephen Frost <sfrost@snowman.net> wrote:
What I find somewhat objectionable is the notion that if we don't have 5
different TLS/SSL implementations supported in PG and that we've tested
that channel binding works correctly among all combinations of all of
them, then we can't accept a patch implementing it.It seems to me that any testing in this area won't fly high as long as
there is no way to enforce the list of TLS implementations that a
server allows. There have been discussions about being able to control
that after the OpenSSL vulnerabilities that were protocol-specific and
there were even patches adding GUCs for this purpose. At the end,
everything has been rejected as Postgres enforces the use of the
newest one when doing the SSL handshake.
TLS implementations, or TLS versions? What does the TLS version have
to do with this issue?
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-jdbc mailing list (pgsql-jdbc@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-jdbc
On Fri, Jun 2, 2017 at 10:25 AM, Robert Haas <robertmhaas@gmail.com> wrote:
On Thu, Jun 1, 2017 at 9:13 PM, Michael Paquier
<michael.paquier@gmail.com> wrote:It seems to me that any testing in this area won't fly high as long as
there is no way to enforce the list of TLS implementations that a
server allows. There have been discussions about being able to control
that after the OpenSSL vulnerabilities that were protocol-specific and
there were even patches adding GUCs for this purpose. At the end,
everything has been rejected as Postgres enforces the use of the
newest one when doing the SSL handshake.TLS implementations, or TLS versions? What does the TLS version have
to do with this issue?
I really mean *version* here. Unlike OpenSSL, the Windows TLS
implementation does not offer an API to choose the latest TLS version
available:
https://msdn.microsoft.com/en-us/library/windows/desktop/aa380513(v=vs.85).aspx
It is up to the server and the client to negotiate that, so it seems
to me that we want some control in this area, which would be important
for testing as well because the TLS finish message differs a bit
across versions, in length mainly. On top of that per the aggressive
updates that Windows does from time to time they may as well forcibly
expose users to a broken TLS implementation...
MacOS has something similar to OpenSSL, with
SSLGetProtocolVersionMax(), which is nice.
--
Michael
--
Sent via pgsql-jdbc mailing list (pgsql-jdbc@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-jdbc
On Fri, Jun 2, 2017 at 10:08 AM, Stephen Frost <sfrost@snowman.net> wrote:
I don't have any issue with asking that Michael, or someone, to go look
at other OpenSSL-using implementations which support channel binding.
I don't see the implementation of other TLS/SSL as a requirement for
channel binding with OpenSSL, and I have no plans to investigate that.
What is clearly a requirement though is to design a set of API generic
enough for the backend and the frontend to fetch the TLS unique
message, and what's needed for endpoint so as any implementation can
plug in easily with PG's channel binding code.
What I find somewhat objectionable is the notion that if we don't have 5
different TLS/SSL implementations supported in PG and that we've tested
that channel binding works correctly among all combinations of all of
them, then we can't accept a patch implementing it. I'm exaggerating
for effect here, of course, and you've agreed that a full, committable,
implementation isn't necessary, and I agreed with that, but we still
seem to have a different level of expectation regarding what needs doing
here. I don't know that we'll ever be able to nail down the exact
location of the line in the sand that needs to be crossed here, or agree
to it, so I'd suggest that we let them continue their efforts to work
together and provide a patch which they feel is ready to be commented on
by committers. Perhaps we'll find that, regardless of where the line
was drawn exactly, they've crossed it.
As far as I can see, there are a couple of things that I still need to
work on to make people happy:
- Rework the generic APIs for TLS finish and endpoint so as any
implementation can use channel binding without inducing any extra code
footprint to be-secure.c and fe-secure.c.
- Implement endpoint, as Alvaro is saying for JDBC that would be nicer.
- Have a couple of tests for channel binding to allow people to test
the feature easily. Those will be in src/test/ssl/. It would be nice
as well to be able to enforce the channel binding type on libpq-side,
which is useful at least for testing. So we are going to need an
environment variable for this purpose, and a connection parameter.
--
Michael
--
Sent via pgsql-jdbc mailing list (pgsql-jdbc@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-jdbc
Michael,
* Michael Paquier (michael.paquier@gmail.com) wrote:
On Fri, Jun 2, 2017 at 10:25 AM, Robert Haas <robertmhaas@gmail.com> wrote:
On Thu, Jun 1, 2017 at 9:13 PM, Michael Paquier
<michael.paquier@gmail.com> wrote:It seems to me that any testing in this area won't fly high as long as
there is no way to enforce the list of TLS implementations that a
server allows. There have been discussions about being able to control
that after the OpenSSL vulnerabilities that were protocol-specific and
there were even patches adding GUCs for this purpose. At the end,
everything has been rejected as Postgres enforces the use of the
newest one when doing the SSL handshake.TLS implementations, or TLS versions? What does the TLS version have
to do with this issue?I really mean *version* here. Unlike OpenSSL, the Windows TLS
implementation does not offer an API to choose the latest TLS version
available:
https://msdn.microsoft.com/en-us/library/windows/desktop/aa380513(v=vs.85).aspx
It is up to the server and the client to negotiate that, so it seems
to me that we want some control in this area, which would be important
for testing as well because the TLS finish message differs a bit
across versions, in length mainly. On top of that per the aggressive
updates that Windows does from time to time they may as well forcibly
expose users to a broken TLS implementation...
MacOS has something similar to OpenSSL, with
SSLGetProtocolVersionMax(), which is nice.
We mainly need to know what version was used, right..? Isn't that
available?
Thanks!
Stephen
On Tue, Jun 6, 2017 at 2:32 AM, Michael Paquier
<michael.paquier@gmail.com> wrote:
At the end,
everything has been rejected as Postgres enforces the use of the
newest one when doing the SSL handshake.TLS implementations, or TLS versions? What does the TLS version have
to do with this issue?I really mean *version* here.
I don't think it's true that we force the latest TLS version to be
used. The comment says:
/*
* We use SSLv23_method() because it can negotiate use of the highest
* mutually supported protocol version, while alternatives like
* TLSv1_2_method() permit only one specific version. Note
that we don't
* actually allow SSL v2 or v3, only TLS protocols (see below).
*/
IIUC, this is specifically so that we don't force the use of TLS 1.2
or TLS 1.1 or TLS 1.0.
It could well be that there's something I don't understand here.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Robert Haas <robertmhaas@gmail.com> writes:
I don't think it's true that we force the latest TLS version to be
used. The comment says:
/*
* We use SSLv23_method() because it can negotiate use of the highest
* mutually supported protocol version, while alternatives like
* TLSv1_2_method() permit only one specific version. Note
that we don't
* actually allow SSL v2 or v3, only TLS protocols (see below).
*/
IIUC, this is specifically so that we don't force the use of TLS 1.2
or TLS 1.1 or TLS 1.0.
Right. IIUC, there's no way (at least in older OpenSSL versions) to say
directly "we only want TLS >= 1.0", so we have to do it like this.
I found a comment on the web saying "SSLv23_method would be better named
AutoNegotiate_method", which seems accurate.
regards, tom lane
--
Sent via pgsql-jdbc mailing list (pgsql-jdbc@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-jdbc
On Tue, Jun 6, 2017 at 3:40 PM, Michael Paquier
<michael.paquier@gmail.com> wrote:
As far as I can see, there are a couple of things that I still need to
work on to make people happy:
- Rework the generic APIs for TLS finish and endpoint so as any
implementation can use channel binding without inducing any extra code
footprint to be-secure.c and fe-secure.c.
- Implement endpoint, as Alvaro is saying for JDBC that would be nicer.
- Have a couple of tests for channel binding to allow people to test
the feature easily. Those will be in src/test/ssl/. It would be nice
as well to be able to enforce the channel binding type on libpq-side,
which is useful at least for testing. So we are going to need an
environment variable for this purpose, and a connection parameter.
Okay, here we go. Attached is a set of four patches:
- 0001 is some refactoring for the SSL tests so as other test suite in
src/test/ssl can take advantage of the connection routines. There is
nothing fancy here.
- 0002 is the implementation of tls-unique as channel binding. This
has been largely reworked since last submission, I have found on the
way a couple of bugs and some correctness issues.
- 0003 is a patch to add as connection parameters saslname and
saslchannelbinding. With support of more SASL mechanisms (PG10 has
SCRAM-SHA-256, I am adding SCRAM-SHA-256-PLUS here), saslname can be
used to enforce on the client-side the value of the SASL mechanism
chosen. saslchannelbinding does the same for the channel binding name.
This is very useful for testing, and a set of tests are added in
src/test/ssl/ for tls-unique and the SASL mechanisms. The tests cover
many scenarios, like downgrade attacks for example.
- 0004 is the implementation of tls-server-end-point, as Alvaro has
asked. Per RFC 5929, the binding data needs to be a hash of the server
certificate. If the signature algorithm of the certificate is MD5 or
SHA-1, then SHA-256 is used. Other signature algos like SHA-384 or 512
are used to hash the data. The hashed data is then encoded in base64
and sent to the server for verification. Tests using saslchannelname
have been added as well. It took me a while to find out that
OBJ_find_sigid_algs(X509_get_signature_nid(X509*)) needs to be used to
find out the algorithm of a certificate with OpenSSL.
With the tests directly in the patch, things are easy to run. WIth
PG10 stabilization work, of course I don't expect much feedback :)
But this set of patches looks like the direction we want to go so as
JDBC and libpq users can take advantage of channel binding with SCRAM.
--
Michael
Attachments:
0001-Refactor-routine-to-test-connection-to-SSL-server.patchapplication/octet-stream; name=0001-Refactor-routine-to-test-connection-to-SSL-server.patchDownload
From a950cf8d2a57ddffde7a5796997c0e1676352ef7 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 16 Jun 2017 17:00:06 +0900
Subject: [PATCH 1/4] Refactor routine to test connection to SSL server
Move the sub-routines wrappers to check if a connection to a server is
fine or not into the test main module. This is useful for other tests
willing to check connectivity into a server.
---
src/test/ssl/ServerSetup.pm | 45 +++++++++++++-
src/test/ssl/t/001_ssltests.pl | 132 +++++++++++++++++------------------------
2 files changed, 100 insertions(+), 77 deletions(-)
diff --git a/src/test/ssl/ServerSetup.pm b/src/test/ssl/ServerSetup.pm
index f63c81cfc6..ad2e036602 100644
--- a/src/test/ssl/ServerSetup.pm
+++ b/src/test/ssl/ServerSetup.pm
@@ -26,9 +26,52 @@ use Test::More;
use Exporter 'import';
our @EXPORT = qw(
- configure_test_server_for_ssl switch_server_cert
+ configure_test_server_for_ssl
+ run_test_psql
+ switch_server_cert
+ test_connect_fails
+ test_connect_ok
);
+# Define a couple of helper functions to test connecting to the server.
+
+# Attempt connection to server with given connection string.
+sub run_test_psql
+{
+ my $connstr = $_[0];
+ my $logstring = $_[1];
+
+ my $cmd = [
+ 'psql', '-X', '-A', '-t', '-c', "SELECT 'connected with $connstr'",
+ '-d', "$connstr" ];
+
+ my $result = run_log($cmd);
+ return $result;
+}
+
+#
+# The first argument is a base connection string to use for connection.
+# The second argument is a complementary connection string, and it's also
+# printed out as the test case name.
+sub test_connect_ok
+{
+ my $common_connstr = $_[0];
+ my $connstr = $_[1];
+
+ my $result =
+ run_test_psql("$common_connstr $connstr", "(should succeed)");
+ ok($result, $connstr);
+}
+
+sub test_connect_fails
+{
+ my $common_connstr = $_[0];
+ my $connstr = $_[1];
+
+ my $result = run_test_psql("$common_connstr $connstr", "(should fail)");
+ ok(!$result, "$connstr (should fail)");
+}
+
# Copy a set of files, taking into account wildcards
sub copy_files
{
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 32df273929..890e3051a2 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -13,44 +13,9 @@ use File::Copy;
# postgresql-ssl-regression.test.
my $SERVERHOSTADDR = '127.0.0.1';
-# Define a couple of helper functions to test connecting to the server.
-
+# Allocation of base connection string shared among multiple tests.
my $common_connstr;
-sub run_test_psql
-{
- my $connstr = $_[0];
- my $logstring = $_[1];
-
- my $cmd = [
- 'psql', '-X', '-A', '-t', '-c', "SELECT 'connected with $connstr'",
- '-d', "$connstr" ];
-
- my $result = run_log($cmd);
- return $result;
-}
-
-#
-# The first argument is a (part of a) connection string, and it's also printed
-# out as the test case name. It is appended to $common_connstr global variable,
-# which also contains a libpq connection string.
-sub test_connect_ok
-{
- my $connstr = $_[0];
-
- my $result =
- run_test_psql("$common_connstr $connstr", "(should succeed)");
- ok($result, $connstr);
-}
-
-sub test_connect_fails
-{
- my $connstr = $_[0];
-
- my $result = run_test_psql("$common_connstr $connstr", "(should fail)");
- ok(!$result, "$connstr (should fail)");
-}
-
# The client's private key must not be world-readable, so take a copy
# of the key stored in the code tree and update its permissions.
copy("ssl/client.key", "ssl/client_tmp.key");
@@ -83,50 +48,59 @@ $common_connstr =
# The server should not accept non-SSL connections
note "test that the server doesn't accept non-SSL connections";
-test_connect_fails("sslmode=disable");
+test_connect_fails($common_connstr, "sslmode=disable");
# Try without a root cert. In sslmode=require, this should work. In verify-ca
# or verify-full mode it should fail
note "connect without server root cert";
-test_connect_ok("sslrootcert=invalid sslmode=require");
-test_connect_fails("sslrootcert=invalid sslmode=verify-ca");
-test_connect_fails("sslrootcert=invalid sslmode=verify-full");
+test_connect_ok($common_connstr, "sslrootcert=invalid sslmode=require");
+test_connect_fails($common_connstr, "sslrootcert=invalid sslmode=verify-ca");
+test_connect_fails($common_connstr, "sslrootcert=invalid sslmode=verify-full");
# Try with wrong root cert, should fail. (we're using the client CA as the
# root, but the server's key is signed by the server CA)
note "connect without wrong server root cert";
-test_connect_fails("sslrootcert=ssl/client_ca.crt sslmode=require");
-test_connect_fails("sslrootcert=ssl/client_ca.crt sslmode=verify-ca");
-test_connect_fails("sslrootcert=ssl/client_ca.crt sslmode=verify-full");
+test_connect_fails($common_connstr,
+ "sslrootcert=ssl/client_ca.crt sslmode=require");
+test_connect_fails($common_connstr,
+ "sslrootcert=ssl/client_ca.crt sslmode=verify-ca");
+test_connect_fails($common_connstr,
+ "sslrootcert=ssl/client_ca.crt sslmode=verify-full");
# Try with just the server CA's cert. This fails because the root file
# must contain the whole chain up to the root CA.
note "connect with server CA cert, without root CA";
-test_connect_fails("sslrootcert=ssl/server_ca.crt sslmode=verify-ca");
+test_connect_fails($common_connstr,
+ "sslrootcert=ssl/server_ca.crt sslmode=verify-ca");
# And finally, with the correct root cert.
note "connect with correct server CA cert file";
-test_connect_ok("sslrootcert=ssl/root+server_ca.crt sslmode=require");
-test_connect_ok("sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca");
-test_connect_ok("sslrootcert=ssl/root+server_ca.crt sslmode=verify-full");
+test_connect_ok($common_connstr,
+ "sslrootcert=ssl/root+server_ca.crt sslmode=require");
+test_connect_ok($common_connstr,
+ "sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca");
+test_connect_ok($common_connstr,
+ "sslrootcert=ssl/root+server_ca.crt sslmode=verify-full");
# Test with cert root file that contains two certificates. The client should
# be able to pick the right one, regardless of the order in the file.
-test_connect_ok("sslrootcert=ssl/both-cas-1.crt sslmode=verify-ca");
-test_connect_ok("sslrootcert=ssl/both-cas-2.crt sslmode=verify-ca");
+test_connect_ok($common_connstr,
+ "sslrootcert=ssl/both-cas-1.crt sslmode=verify-ca");
+test_connect_ok($common_connstr,
+ "sslrootcert=ssl/both-cas-2.crt sslmode=verify-ca");
note "testing sslcrl option with a non-revoked cert";
# Invalid CRL filename is the same as no CRL, succeeds
-test_connect_ok(
+test_connect_ok($common_connstr,
"sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrl=invalid");
# A CRL belonging to a different CA is not accepted, fails
-test_connect_fails(
+test_connect_fails($common_connstr,
"sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrl=ssl/client.crl");
# With the correct CRL, succeeds (this cert is not revoked)
-test_connect_ok(
+test_connect_ok($common_connstr,
"sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrl=ssl/root+server.crl"
);
@@ -136,9 +110,9 @@ note "test mismatch between hostname and server certificate";
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
-test_connect_ok("sslmode=require host=wronghost.test");
-test_connect_ok("sslmode=verify-ca host=wronghost.test");
-test_connect_fails("sslmode=verify-full host=wronghost.test");
+test_connect_ok($common_connstr, "sslmode=require host=wronghost.test");
+test_connect_ok($common_connstr, "sslmode=verify-ca host=wronghost.test");
+test_connect_fails($common_connstr, "sslmode=verify-full host=wronghost.test");
# Test Subject Alternative Names.
switch_server_cert($node, 'server-multiple-alt-names');
@@ -147,12 +121,13 @@ note "test hostname matching with X.509 Subject Alternative Names";
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
-test_connect_ok("host=dns1.alt-name.pg-ssltest.test");
-test_connect_ok("host=dns2.alt-name.pg-ssltest.test");
-test_connect_ok("host=foo.wildcard.pg-ssltest.test");
+test_connect_ok($common_connstr, "host=dns1.alt-name.pg-ssltest.test");
+test_connect_ok($common_connstr, "host=dns2.alt-name.pg-ssltest.test");
+test_connect_ok($common_connstr, "host=foo.wildcard.pg-ssltest.test");
-test_connect_fails("host=wronghost.alt-name.pg-ssltest.test");
-test_connect_fails("host=deep.subdomain.wildcard.pg-ssltest.test");
+test_connect_fails($common_connstr, "host=wronghost.alt-name.pg-ssltest.test");
+test_connect_fails($common_connstr,
+ "host=deep.subdomain.wildcard.pg-ssltest.test");
# Test certificate with a single Subject Alternative Name. (this gives a
# slightly different error message, that's all)
@@ -162,10 +137,11 @@ note "test hostname matching with a single X.509 Subject Alternative Name";
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
-test_connect_ok("host=single.alt-name.pg-ssltest.test");
+test_connect_ok($common_connstr, "host=single.alt-name.pg-ssltest.test");
-test_connect_fails("host=wronghost.alt-name.pg-ssltest.test");
-test_connect_fails("host=deep.subdomain.wildcard.pg-ssltest.test");
+test_connect_fails($common_connstr, "host=wronghost.alt-name.pg-ssltest.test");
+test_connect_fails($common_connstr,
+ "host=deep.subdomain.wildcard.pg-ssltest.test");
# Test server certificate with a CN and SANs. Per RFCs 2818 and 6125, the CN
# should be ignored when the certificate has both.
@@ -175,9 +151,9 @@ note "test certificate with both a CN and SANs";
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
-test_connect_ok("host=dns1.alt-name.pg-ssltest.test");
-test_connect_ok("host=dns2.alt-name.pg-ssltest.test");
-test_connect_fails("host=common-name.pg-ssltest.test");
+test_connect_ok($common_connstr, "host=dns1.alt-name.pg-ssltest.test");
+test_connect_ok($common_connstr, "host=dns2.alt-name.pg-ssltest.test");
+test_connect_fails($common_connstr, "host=common-name.pg-ssltest.test");
# Finally, test a server certificate that has no CN or SANs. Of course, that's
# not a very sensible certificate, but libpq should handle it gracefully.
@@ -185,8 +161,10 @@ switch_server_cert($node, 'server-no-names');
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR";
-test_connect_ok("sslmode=verify-ca host=common-name.pg-ssltest.test");
-test_connect_fails("sslmode=verify-full host=common-name.pg-ssltest.test");
+test_connect_ok($common_connstr,
+ "sslmode=verify-ca host=common-name.pg-ssltest.test");
+test_connect_fails($common_connstr,
+ "sslmode=verify-full host=common-name.pg-ssltest.test");
# Test that the CRL works
note "testing client-side CRL";
@@ -196,8 +174,9 @@ $common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid hostaddr=$SERVERHOSTADDR host=common-name.pg-ssltest.test";
# Without the CRL, succeeds. With it, fails.
-test_connect_ok("sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca");
-test_connect_fails(
+test_connect_ok($common_connstr,
+ "sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca");
+test_connect_fails($common_connstr,
"sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrl=ssl/root+server.crl"
);
@@ -210,18 +189,18 @@ $common_connstr =
"sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=certdb hostaddr=$SERVERHOSTADDR";
# no client cert
-test_connect_fails("user=ssltestuser sslcert=invalid");
+test_connect_fails($common_connstr, "user=ssltestuser sslcert=invalid");
# correct client cert
-test_connect_ok(
+test_connect_ok($common_connstr,
"user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key");
# client cert belonging to another user
-test_connect_fails(
+test_connect_fails($common_connstr,
"user=anotheruser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key");
# revoked client cert
-test_connect_fails(
+test_connect_fails($common_connstr,
"user=ssltestuser sslcert=ssl/client-revoked.crt sslkey=ssl/client-revoked.key"
);
@@ -230,8 +209,9 @@ switch_server_cert($node, 'server-cn-only', 'root_ca');
$common_connstr =
"user=ssltestuser dbname=certdb sslkey=ssl/client_tmp.key sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR";
-test_connect_ok("sslmode=require sslcert=ssl/client+client_ca.crt");
-test_connect_fails("sslmode=require sslcert=ssl/client.crt");
+test_connect_ok($common_connstr,
+ "sslmode=require sslcert=ssl/client+client_ca.crt");
+test_connect_fails($common_connstr, "sslmode=require sslcert=ssl/client.crt");
# clean up
unlink "ssl/client_tmp.key";
--
2.13.1
0002-Support-channel-binding-tls-unique-in-SCRAM.patchapplication/octet-stream; name=0002-Support-channel-binding-tls-unique-in-SCRAM.patchDownload
From b98d908688abe7529a5800c277930f3523969dd9 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 20 Jun 2017 10:18:58 +0900
Subject: [PATCH 2/4] 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 finish messages, a PG-like API is added to be able to
make the interface pluggable for other SSL implementations.
---
doc/src/sgml/protocol.sgml | 24 +++--
src/backend/libpq/auth-scram.c | 151 +++++++++++++++++++++++++------
src/backend/libpq/auth.c | 86 +++++++++++++++---
src/backend/libpq/be-secure-openssl.c | 24 +++++
src/include/libpq/libpq-be.h | 1 +
src/include/libpq/scram.h | 10 +-
src/interfaces/libpq/fe-auth-scram.c | 105 ++++++++++++++++-----
src/interfaces/libpq/fe-auth.c | 43 ++++++++-
src/interfaces/libpq/fe-auth.h | 4 +-
src/interfaces/libpq/fe-secure-openssl.c | 26 ++++++
src/interfaces/libpq/libpq-int.h | 5 +-
11 files changed, 404 insertions(+), 75 deletions(-)
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index a7a3d3b2f9..0a556ebdc6 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1342,10 +1342,11 @@
<para>
<firstterm>SASL</> is a framework for authentication in connection-oriented
-protocols. At the moment, <productname>PostgreSQL</> implements only one SASL
-authentication mechanism, SCRAM-SHA-256, but more might be added in the
-future. The below steps illustrate how SASL authentication is performed in
-general, while the next subsection gives more details on SCRAM-SHA-256.
+protocols. At the moment, <productname>PostgreSQL</> implements only two SASL
+authentication mechanisms, SCRAM-SHA-256 and SCRAM-SHA-256-PLUS, but more
+might be added in the future. The below steps illustrate how SASL
+authentication is performed in general, while the next subsection gives
+more details on SCRAM-SHA-256 and SCRAM-SHA-256-PLUS.
</para>
<procedure>
@@ -1430,7 +1431,10 @@ the password is in.
</para>
<para>
-<firstterm>Channel binding</> has not been implemented yet.
+<firstterm>Channel binding</> is supported in builds with SSL support, and
+uses as mechanism name <literal>SCRAM-SHA-256-PLUS</> for this purpose as
+defined per IANA. The only channel binding type supported by the server
+is <literal>tls-unique</>.
</para>
<procedure>
@@ -1439,14 +1443,18 @@ the password is in.
<para>
The server sends an AuthenticationSASL message. It includes a list of
SASL authentication mechanisms that the server can accept.
+ <literal>SCRAM-SHA-256</> and <literal>SCRAM-SHA-256-PLUS</> are the
+ two mechanism names that the server lists in this message. Support for
+ channel binding is not included if the server is built without SSL
+ support and if the connection attempt is done without SSL.
</para>
</step>
<step id="scram-client-first">
<para>
The client responds by sending a SASLInitialResponse message, which
- indicates the chosen mechanism, <literal>SCRAM-SHA-256</>. In the Initial
- Client response field, the message contains the SCRAM
- <structname>client-first-message</>.
+ indicates the chosen mechanism, <literal>SCRAM-SHA-256</> or
+ <literal>SCRAM-SHA-256-PLUS</>. In the Initial Client response field,
+ the message contains the SCRAM <structname>client-first-message</>.
</para>
</step>
<step id="scram-server-first">
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index a6042b8013..90c6b20b82 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,10 @@ typedef struct
const char *username; /* username from startup packet */
+ bool ssl_in_use;
+ char *tls_finish_message;
+ int tls_finish_len;
+
int iterations;
char *salt; /* base64-encoded */
uint8 StoredKey[SCRAM_KEY_LEN];
@@ -168,7 +170,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,
+ char *tls_finish_message,
+ int tls_finish_len)
{
scram_state *state;
bool got_verifier;
@@ -176,6 +182,9 @@ 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_finish_message = tls_finish_message;
+ state->tls_finish_len = tls_finish_len;
/*
* Parse the stored password verifier.
@@ -773,31 +782,93 @@ read_client_first_message(scram_state *state, char *input)
*------
*/
- /* read gs2-cbind-flag */
+ /*
+ * Read gs2-cbind-flag. Server handles its value as described in RFC 5802,
+ * section 6 dealing with channel binding.
+ */
switch (*input)
{
case 'n':
- /* Client does not support channel binding */
+ /*
+ * Client does not support channel binding, this is authorized
+ * only in builds not supporting SSL. If SSL is supported, the
+ * server cannot support this option either.
+ */
+#ifdef USE_SSL
+ if (state->ssl_in_use)
+ ereport(ERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("client does not support SCRAM channel binding, but server needs it for SSL connections")));
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("client does not support SCRAM channel binding, but server does")));
+#endif
+ 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 */
+ /*
+ * Client supports channel binding, but we're not doing it today
+ * in the context of a non-SSL connection. Complain though if
+ * the client is trying to trick the server in not doing channel
+ * binding with a downgrade attack if a SSL connection is
+ * attempted.
+ */
+ if (state->ssl_in_use)
+ ereport(ERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("client support SCRAM channel binding, but server needs it for SSL connections")));
+ 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':
+ {
+#ifdef USE_SSL
+ char *channel_name;
- /*
- * 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")));
+ if (!state->ssl_in_use)
+ ereport(ERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("client supports SCRAM channel binding, but server does not need it for non-SSL connections")));
+
+ /*
+ * Read value provided by client, only tls-unique is supported
+ * for now.
+ */
+ channel_name = read_attr_value(&input, 'p');
+ if (strcmp(channel_name, SCRAM_CHANNEL_TLS_UNIQUE) != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ (errmsg("unexpected SCRAM channel-binding type"))));
+#else
+ /*
+ * 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")));
+#endif
+ }
+ break;
default:
ereport(ERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION),
@@ -805,13 +876,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 +1096,47 @@ 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. We don't support channel binding for builds
+ * without SSL support, so it's expected to always be "biws" in this case,
+ * which is "n,,", base64-encoded. In builds supporting SSL, "biws" is
+ * used for Non-SSL connection attempts, and for SSL connections the
+ * client has to provide channel binding value.
*/
channel_binding = read_attr_value(&p, 'c');
+#ifdef USE_SSL
+ if (state->ssl_in_use)
+ {
+ char *enc_tls_message;
+ int enc_tls_len;
+
+ enc_tls_message = palloc(pg_b64_enc_len(state->tls_finish_len) + 1);
+ enc_tls_len = pg_b64_encode(state->tls_finish_message,
+ state->tls_finish_len,
+ enc_tls_message);
+ enc_tls_message[enc_tls_len] = '\0';
+
+ /*
+ * Compare the value sent by the client with the TLS finish message
+ * expected by the server.
+ */
+ if (strcmp(channel_binding, enc_tls_message) != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ (errmsg("no match for SCRAM channel-binding attribute in client-final-message"))));
+ }
+ else
+ {
+ if (strcmp(channel_binding, "biws") != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ (errmsg("unexpected SCRAM channel-binding attribute in client-final-message"))));
+ }
+#else
if (strcmp(channel_binding, "biws") != 0)
ereport(ERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION),
(errmsg("unexpected SCRAM channel-binding attribute in client-final-message"))));
+#endif
state->client_final_nonce = read_attr_value(&p, 'r');
/* ignore optional extensions */
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 081c06a1e6..439e4e8ab9 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -841,10 +841,14 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
void *scram_opaq;
char *output = NULL;
int outputlen = 0;
- char *input;
+ char *input, *p;
+ char *sasl_mechs;
int inputlen;
+ int listlen = 0;
int result;
bool initial;
+ char *tls_finish = NULL;
+ int tls_finish_len = 0;
/*
* SASL auth is not supported for protocol versions before 3, because it
@@ -861,12 +865,49 @@ 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:
+ * - SCRAM-SHA-256, which is the mechanism with the same name.
+ * - SCRAM-SHA-256-PLUS, which is SCRAM-SHA-256 with channel binding. This
+ * is advertised to the client only if connection is attempted with SSL.
+ * The order of mechanisms is advertised in decreasing order of importance.
+ * The extra "\0" is for an empty string to terminate the list, and each
+ * mechanism listed needs to be separated with "\0".
*/
- sendAuthRequest(port, AUTH_REQ_SASL, SCRAM_SHA256_NAME "\0",
- strlen(SCRAM_SHA256_NAME) + 2);
+ listlen = 0;
+ sasl_mechs = (char *) palloc(strlen(SCRAM_SHA256_PLUS_NAME) +
+ strlen(SCRAM_SHA256_NAME) + 3);
+ p = sasl_mechs;
+
+ /* add SCRAM-SHA-256-PLUS, which depends on if SSL is in use */
+ if (port->ssl_in_use)
+ {
+ strcpy(p, SCRAM_SHA256_PLUS_NAME);
+ listlen += strlen(SCRAM_SHA256_PLUS_NAME) + 1;
+ p += strlen(SCRAM_SHA256_PLUS_NAME) + 1;
+ }
+
+ /* add generic SCRAM-SHA-256 */
+ strcpy(p, SCRAM_SHA256_NAME);
+ listlen += strlen(SCRAM_SHA256_NAME) + 1;
+ p += strlen(SCRAM_SHA256_NAME) + 1;
+
+ /* put "\0" to mark that list is finished */
+ p[0] = '\0';
+ listlen++;
+
+ sendAuthRequest(port, AUTH_REQ_SASL, sasl_mechs, listlen);
+ pfree(sasl_mechs);
+
+#ifdef USE_SSL
+ /*
+ * Fetch the data related to the SSL finish message to be used in the
+ * exchange.
+ */
+ if (port->ssl_in_use)
+ {
+ tls_finish = be_tls_get_peer_finish(port, &tls_finish_len);
+ }
+#endif
/*
* Initialize the status tracker for message exchanges.
@@ -879,7 +920,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_finish,
+ tls_finish_len);
/*
* Loop through SASL message exchange. This exchange can consist of
@@ -928,17 +973,36 @@ 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.
+ * We only support SCRAM-SHA-256 and SCRAM-SHA-256-PLUS 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),
- errmsg("client selected an invalid SASL authentication mechanism")));
+ errmsg("client selected an invalid SASL authentication mechanism"),
+ errdetail("Incorrect SASL mechanism name specified")));
}
+#ifdef USE_SSL
+ /*
+ * If an SSL connection is attempted, check for downgrade attacks.
+ * A connection is not allowed to specify SCRAM-SHA-256 if its
+ * -PLUS version has been submitted back to the client, which is
+ * the default.
+ */
+ if (port->ssl_in_use &&
+ strcmp(selected_mech, SCRAM_SHA256_NAME) == 0)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("client selected invalid SASL authentication mechanism"),
+ errdetail("Channel binding published to client but not selected.")));
+ }
+#endif
+
inputlen = pq_getmsgint(&buf, 4);
if (inputlen == -1)
input = NULL;
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 44c84a7869..42c35800aa 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -1275,6 +1275,30 @@ be_tls_get_peerdn_name(Port *port, char *ptr, size_t len)
}
/*
+ * Routine to get the expected TLS finish message information from the
+ * client, useful for authorization when doing channel binding.
+ *
+ * Result is a palloc'd copy of the TLS finish message with its size.
+ */
+char *
+be_tls_get_peer_finish(Port *port, int *len)
+{
+ char dummy[1];
+ char *result;
+
+ /*
+ * OpenSSL does not offer an API to get directly the length of the
+ * expected TLS finish 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 = (char *) palloc(*len * sizeof(char));
+ (void) SSL_get_peer_finished(port->ssl, result, *len);
+
+ return result;
+}
+
+/*
* Convert an X509 subject name to a cstring.
*
*/
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 0669b924cf..3957d04702 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -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_cipher(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_finish(Port *port, int *len);
#endif
extern ProtocolVersion FrontendProtocol;
diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h
index 14b48af12f..58eae11231 100644
--- a/src/include/libpq/scram.h
+++ b/src/include/libpq/scram.h
@@ -13,8 +13,12 @@
#ifndef 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_PLUS_NAME "SCRAM-SHA-256-PLUS" /* with channel binding */
+
+/* Channel binding names */
+#define SCRAM_CHANNEL_TLS_UNIQUE "tls-unique"
/* Status codes for message exchange */
#define SASL_EXCHANGE_CONTINUE 0
@@ -22,7 +26,9 @@
#define SASL_EXCHANGE_FAILURE 2
/* 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, char *tls_finish_message,
+ int tls_finish_len);
extern int pg_be_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen, char **logdetail);
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index d2e355a8b8..3a9fe4e057 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -17,6 +17,7 @@
#include "common/base64.h"
#include "common/saslprep.h"
#include "common/scram-common.h"
+#include "libpq/scram.h"
#include "fe-auth.h"
/* These are needed for getpid(), in the fallback implementation */
@@ -44,6 +45,9 @@ typedef struct
/* These are supplied by the user */
const char *username;
char *password;
+ bool ssl_in_use;
+ char *tls_finish_message;
+ int tls_finish_len;
/* We construct these */
uint8 SaltedPassword[SCRAM_KEY_LEN];
@@ -81,7 +85,11 @@ static bool pg_frontend_random(char *dst, int len);
* Initialize SCRAM exchange status.
*/
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,
+ char *tls_finish_message,
+ int tls_finish_len)
{
fe_scram_state *state;
char *prep_password;
@@ -93,6 +101,9 @@ pg_fe_scram_init(const char *username, const char *password)
memset(state, 0, sizeof(fe_scram_state));
state->state = FE_SCRAM_INIT;
state->username = username;
+ state->ssl_in_use = ssl_in_use;
+ state->tls_finish_message = tls_finish_message;
+ state->tls_finish_len = tls_finish_len;
/* Normalize the password with SASLprep, if possible */
rc = pg_saslprep(password, &prep_password);
@@ -297,9 +308,10 @@ static char *
build_client_first_message(fe_scram_state *state, PQExpBuffer errormessage)
{
char raw_nonce[SCRAM_RAW_NONCE_LEN + 1];
- char *buf;
- char buflen;
+ char *result;
+ int channel_info_len;
int encoded_len;
+ PQExpBufferData buf;
/*
* Generate a "raw" nonce. This is converted to ASCII-printable form by
@@ -328,26 +340,55 @@ build_client_first_message(fe_scram_state *state, PQExpBuffer errormessage)
* prepared with SASLprep, the message parsing would fail if it includes
* '=' or ',' characters.
*/
- buflen = 8 + strlen(state->client_nonce) + 1;
- buf = malloc(buflen);
- if (buf == NULL)
- {
- printfPQExpBuffer(errormessage,
- libpq_gettext("out of memory\n"));
- return NULL;
- }
- snprintf(buf, buflen, "n,,n=,r=%s", state->client_nonce);
+ initPQExpBuffer(&buf);
- state->client_first_message_bare = strdup(buf + 3);
+ /*
+ * First build the query field for channel binding. If the client is not
+ * built with SSL support, it cannot support channel binding so it needs
+ * to use "n" to let the server know. If built with SSL support, client
+ * needs to use "y" to let the server know that client has such support
+ * but that it is not using it as a non-SSL connection is requested.
+ * Finally if a SSL connection is done, use p=cb-name, for which only
+ * "tls-unique" is supported now.
+ */
+#ifdef USE_SSL
+ if (state->ssl_in_use)
+ appendPQExpBuffer(&buf, "p=%s", SCRAM_CHANNEL_TLS_UNIQUE);
+ else
+ appendPQExpBuffer(&buf, "y");
+#else
+ appendPQExpBuffer(&buf, "n");
+#endif
+
+ if (PQExpBufferDataBroken(buf))
+ goto oom_error;
+
+ 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)
- {
- free(buf);
- printfPQExpBuffer(errormessage,
- libpq_gettext("out of memory\n"));
- return NULL;
- }
+ goto oom_error;
- return buf;
+ result = strdup(buf.data);
+ if (result == NULL)
+ goto oom_error;
+
+ termPQExpBuffer(&buf);
+ return result;
+
+oom_error:
+ termPQExpBuffer(&buf);
+ printfPQExpBuffer(errormessage,
+ libpq_gettext("out of memory\n"));
+ return NULL;
}
/*
@@ -365,8 +406,30 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
/*
* Construct client-final-message-without-proof. We need to remember it
* for verifying the server proof in the final step of authentication.
+ * Client needs to provide a b64 encoded string of the TLS finish message
+ * only if a SSL connection is attempted.
*/
- appendPQExpBuffer(&buf, "c=biws,r=%s", state->nonce);
+#ifdef USE_SSL
+ if (state->ssl_in_use)
+ {
+ appendPQExpBuffer(&buf, "c=");
+ if (!enlargePQExpBuffer(&buf, pg_b64_enc_len(state->tls_finish_len)))
+ goto oom_error;
+ buf.len += pg_b64_encode(state->tls_finish_message,
+ state->tls_finish_len,
+ buf.data + buf.len);
+ buf.data[buf.len] = '\0';
+ }
+ else
+ appendPQExpBuffer(&buf, "c=biws");
+#else
+ appendPQExpBuffer(&buf, "c=biws");
+#endif
+
+ if (PQExpBufferDataBroken(buf))
+ goto oom_error;
+
+ appendPQExpBuffer(&buf, ",r=%s", state->nonce);
if (PQExpBufferDataBroken(buf))
goto oom_error;
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 181eeea835..f5e1c27964 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -504,7 +504,8 @@ pg_SASL_init(PGconn *conn, int payloadlen)
/*
* Parse the list of SASL authentication mechanisms in the
* AuthenticationSASL message, and select the best mechanism that we
- * support. (Only SCRAM-SHA-256 is supported at the moment.)
+ * support. SCRAM-SHA-256 and SCRAM-SHA-256-PLUS are the only ones
+ * supported at the moment.
*/
selected_mechanism = NULL;
for (;;)
@@ -532,9 +533,12 @@ pg_SASL_init(PGconn *conn, int payloadlen)
/*
* Do we support this mechanism?
*/
- if (strcmp(mechanism_buf.data, SCRAM_SHA256_NAME) == 0)
+ if (strcmp(mechanism_buf.data, SCRAM_SHA256_NAME) == 0 ||
+ strcmp(mechanism_buf.data, SCRAM_SHA256_PLUS_NAME) == 0)
{
char *password;
+ char *tls_finish = NULL;
+ int tls_finish_len = 0;
conn->password_needed = true;
password = conn->connhost[conn->whichhost].password;
@@ -547,10 +551,41 @@ pg_SASL_init(PGconn *conn, int payloadlen)
goto error;
}
- conn->sasl_state = pg_fe_scram_init(conn->pguser, password);
+#ifdef USE_SSL
+ /* Fetch information about the TLS finish message */
+ if (conn->ssl_in_use)
+ {
+ tls_finish = pgtls_get_finish(conn, &tls_finish_len);
+ if (tls_finish == NULL)
+ goto oom_error;
+ }
+#endif
+
+ conn->sasl_state = pg_fe_scram_init(conn->pguser,
+ password,
+ conn->ssl_in_use,
+ tls_finish,
+ tls_finish_len);
if (!conn->sasl_state)
goto oom_error;
- selected_mechanism = SCRAM_SHA256_NAME;
+
+ /*
+ * Select the mechanism to use by default. If SSL connection
+ * is attempted, the server will expect the -PLUS mechanism.
+ * If not, fallback to SCRAM-SHA-256.
+ */
+#ifdef USE_SSL
+ if (conn->ssl_in_use &&
+ strcmp(mechanism_buf.data, SCRAM_SHA256_PLUS_NAME) == 0)
+ selected_mechanism = SCRAM_SHA256_PLUS_NAME;
+ else if (!conn->ssl_in_use &&
+ strcmp(mechanism_buf.data, SCRAM_SHA256_NAME) == 0)
+ selected_mechanism = SCRAM_SHA256_NAME;
+#else
+ /* No channel binding can be selected without SSL support */
+ if (strcmp(mechanism_buf.data, SCRAM_SHA256_NAME) == 0)
+ selected_mechanism = SCRAM_SHA256_NAME;
+#endif
}
}
diff --git a/src/interfaces/libpq/fe-auth.h b/src/interfaces/libpq/fe-auth.h
index 9f4c2a50d8..2ee9c6c48c 100644
--- a/src/interfaces/libpq/fe-auth.h
+++ b/src/interfaces/libpq/fe-auth.h
@@ -23,7 +23,9 @@ extern int pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn);
extern char *pg_fe_getauthname(PQExpBuffer errorMessage);
/* 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, char *tls_finish_message,
+ int tls_finish_len);
extern void pg_fe_scram_free(void *opaq);
extern void pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen,
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index a7c3d7af64..2da0b27b71 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -393,6 +393,32 @@ pgtls_write(PGconn *conn, const void *ptr, size_t len)
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_finish(PGconn *conn, int *len)
+{
+ char dummy[1];
+ char *result;
+
+ /*
+ * OpenSSL does not offer an API to get directly the length of the
+ * TLS finish 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 */
/* ------------------------------------------------------------ */
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 335568b790..6313d896b7 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -454,11 +454,13 @@ struct pg_conn
/* Assorted state for SASL, SSL, GSS, etc */
void *sasl_state;
+ /* SSL structures */
+ bool ssl_in_use;
+
#ifdef USE_SSL
bool allow_ssl_try; /* Allowed to try SSL negotiation */
bool wait_ssl_try; /* Delay SSL negotiation until after
* attempting normal connection */
- bool ssl_in_use;
#ifdef USE_OPENSSL
SSL *ssl; /* SSL status, if have SSL connection */
X509 *peer; /* X509 cert of server */
@@ -669,6 +671,7 @@ extern void pgtls_close(PGconn *conn);
extern ssize_t pgtls_read(PGconn *conn, void *ptr, size_t len);
extern bool pgtls_read_pending(PGconn *conn);
extern ssize_t pgtls_write(PGconn *conn, const void *ptr, size_t len);
+extern char *pgtls_get_finish(PGconn *conn, int *len);
/*
* this is so that we can check if a connection is non-blocking internally
--
2.13.1
0003-Add-connection-parameters-saslname-and-saslchannelbi.patchapplication/octet-stream; name=0003-Add-connection-parameters-saslname-and-saslchannelbi.patchDownload
From d1ede00d3e1ce086d0dd2ac7976c93a2f22dee77 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 20 Jun 2017 11:41:10 +0900
Subject: [PATCH 3/4] Add connection parameters "saslname" and
"saslchannelbinding"
Those parameters can be used to respectively enforce the value of the
SASL mechanism name and the channel binding name sent to server during
a SASL message exchange.
A set of tests dedicated to SASL and channel binding is added as well
to the SSL test suite, which is handy to check the validity of a patch.
---
doc/src/sgml/libpq.sgml | 24 ++++++++++++++++
src/backend/libpq/auth-scram.c | 41 +++++++++++++++++++++-------
src/interfaces/libpq/fe-auth-scram.c | 53 ++++++++++++++++++++++++++++++++----
src/interfaces/libpq/fe-auth.c | 8 ++++++
src/interfaces/libpq/fe-auth.h | 4 +--
src/interfaces/libpq/fe-connect.c | 13 +++++++++
src/interfaces/libpq/libpq-int.h | 2 ++
src/test/ssl/ServerSetup.pm | 19 +++++++++++--
src/test/ssl/t/001_ssltests.pl | 2 +-
src/test/ssl/t/002_sasl.pl | 52 +++++++++++++++++++++++++++++++++++
10 files changed, 197 insertions(+), 21 deletions(-)
create mode 100644 src/test/ssl/t/002_sasl.pl
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index c3bd4f9b9b..f5de09b0b4 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1171,6 +1171,30 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
</listitem>
</varlistentry>
+ <varlistentry id="libpq-saslname" xreflabel="saslname">
+ <term><literal>saslname</literal></term>
+ <listitem>
+ <para>
+ Controls the name of the SASL mechanism name sent to server when doing
+ a message exchange for a SASL authentication. The list of SASL
+ mechanisms supported by server are listed in
+ <xref linkend="sasl-authentication">.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="libpq-saslchannelbinding" xreflabel="saslchannelbinding">
+ <term><literal>saslchannelbinding</literal></term>
+ <listitem>
+ <para>
+ Controls the name of the channel binding name sent to server when doing
+ a message exchange for a SASL authentication. The list of channel
+ binding names supported by server are listed in
+ <xref linkend="sasl-authentication">.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="libpq-connect-sslmode" xreflabel="sslmode">
<term><literal>sslmode</literal></term>
<listitem>
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index 90c6b20b82..c46bef5bb5 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -113,6 +113,7 @@ typedef struct
bool ssl_in_use;
char *tls_finish_message;
int tls_finish_len;
+ char *channel_binding;
int iterations;
char *salt; /* base64-encoded */
@@ -185,6 +186,7 @@ pg_be_scram_init(const char *username,
state->ssl_in_use = ssl_in_use;
state->tls_finish_message = tls_finish_message;
state->tls_finish_len = tls_finish_len;
+ state->channel_binding = NULL;
/*
* Parse the stored password verifier.
@@ -853,6 +855,9 @@ read_client_first_message(scram_state *state, char *input)
ereport(ERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION),
(errmsg("unexpected SCRAM channel-binding type"))));
+
+ /* Save the name for handling of subsequent messages */
+ state->channel_binding = pstrdup(channel_name);
#else
/*
* Client requires channel binding. We don't support it.
@@ -1106,20 +1111,36 @@ read_client_final_message(scram_state *state, char *input)
#ifdef USE_SSL
if (state->ssl_in_use)
{
- char *enc_tls_message;
- int enc_tls_len;
+ char *b64_message, *raw_data;
+ int b64_message_len, raw_data_len;
+
+ /* Fetch data for each channel binding type */
+ if (strcmp(state->channel_binding, SCRAM_CHANNEL_TLS_UNIQUE) == 0)
+ {
+ raw_data = state->tls_finish_message;
+ raw_data_len = state->tls_finish_len;
+ }
+ else
+ {
+ /* should not happen */
+ elog(ERROR, "invalid channel binding type");
+ }
+
+ /* should not happen, but better safe than sorry */
+ if (raw_data == NULL)
+ elog(ERROR, "empty binding data for channel name \"%s\"",
+ state->channel_binding);
- enc_tls_message = palloc(pg_b64_enc_len(state->tls_finish_len) + 1);
- enc_tls_len = pg_b64_encode(state->tls_finish_message,
- state->tls_finish_len,
- enc_tls_message);
- enc_tls_message[enc_tls_len] = '\0';
+ b64_message = palloc(pg_b64_enc_len(raw_data_len) + 1);
+ b64_message_len = pg_b64_encode(raw_data, raw_data_len,
+ b64_message);
+ b64_message[b64_message_len] = '\0';
/*
- * Compare the value sent by the client with the TLS finish message
- * expected by the server.
+ * Compare the value sent by the client with the value expected by
+ * the server.
*/
- if (strcmp(channel_binding, enc_tls_message) != 0)
+ if (strcmp(channel_binding, b64_message) != 0)
ereport(ERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION),
(errmsg("no match for SCRAM channel-binding attribute in client-final-message"))));
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index 3a9fe4e057..b781349f42 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -48,6 +48,8 @@ typedef struct
bool ssl_in_use;
char *tls_finish_message;
int tls_finish_len;
+ /* enforceable user parameters */
+ char *saslchannelbinding; /* name of channel binding to use */
/* We construct these */
uint8 SaltedPassword[SCRAM_KEY_LEN];
@@ -88,6 +90,7 @@ void *
pg_fe_scram_init(const char *username,
const char *password,
bool ssl_in_use,
+ char *saslchannelbinding,
char *tls_finish_message,
int tls_finish_len)
{
@@ -105,6 +108,15 @@ pg_fe_scram_init(const char *username,
state->tls_finish_message = tls_finish_message;
state->tls_finish_len = tls_finish_len;
+ /*
+ * If user has specified a channel binding to use, enforce the
+ * channel binding sent to it. The default is "tls-unique".
+ */
+ if (saslchannelbinding && strlen(saslchannelbinding) > 0)
+ state->saslchannelbinding = strdup(saslchannelbinding);
+ else
+ state->saslchannelbinding = strdup(SCRAM_CHANNEL_TLS_UNIQUE);
+
/* Normalize the password with SASLprep, if possible */
rc = pg_saslprep(password, &prep_password);
if (rc == SASLPREP_OOM)
@@ -136,6 +148,10 @@ pg_fe_scram_free(void *opaq)
if (state->password)
free(state->password);
+ if (state->tls_finish_message)
+ free(state->tls_finish_message);
+ if (state->saslchannelbinding)
+ free(state->saslchannelbinding);
/* client messages */
if (state->client_nonce)
@@ -353,7 +369,7 @@ build_client_first_message(fe_scram_state *state, PQExpBuffer errormessage)
*/
#ifdef USE_SSL
if (state->ssl_in_use)
- appendPQExpBuffer(&buf, "p=%s", SCRAM_CHANNEL_TLS_UNIQUE);
+ appendPQExpBuffer(&buf, "p=%s", state->saslchannelbinding);
else
appendPQExpBuffer(&buf, "y");
#else
@@ -412,12 +428,39 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
#ifdef USE_SSL
if (state->ssl_in_use)
{
+ char *raw_data;
+ int raw_data_len;
+
+ if (strcmp(state->saslchannelbinding, SCRAM_CHANNEL_TLS_UNIQUE) == 0)
+ {
+ raw_data = state->tls_finish_message;
+ raw_data_len = state->tls_finish_len;
+ }
+ else
+ {
+ /* should not happen */
+ termPQExpBuffer(&buf);
+ printfPQExpBuffer(errormessage,
+ libpq_gettext("incorrect channel binding name\n"));
+ return NULL;
+ }
+
+ /* should not happen, but better safe than sorry */
+ if (raw_data == NULL)
+ {
+ /* should not happen */
+ termPQExpBuffer(&buf);
+ printfPQExpBuffer(errormessage,
+ libpq_gettext("empty binding data for channel name \"%s\"\n"),
+ state->saslchannelbinding);
+ return NULL;
+ }
+
appendPQExpBuffer(&buf, "c=");
- if (!enlargePQExpBuffer(&buf, pg_b64_enc_len(state->tls_finish_len)))
+
+ if (!enlargePQExpBuffer(&buf, pg_b64_enc_len(raw_data_len)))
goto oom_error;
- buf.len += pg_b64_encode(state->tls_finish_message,
- state->tls_finish_len,
- buf.data + buf.len);
+ buf.len += pg_b64_encode(raw_data, raw_data_len, buf.data + buf.len);
buf.data[buf.len] = '\0';
}
else
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index f5e1c27964..96cbf93227 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -564,6 +564,7 @@ pg_SASL_init(PGconn *conn, int payloadlen)
conn->sasl_state = pg_fe_scram_init(conn->pguser,
password,
conn->ssl_in_use,
+ conn->saslchannelbinding,
tls_finish,
tls_finish_len);
if (!conn->sasl_state)
@@ -589,6 +590,13 @@ pg_SASL_init(PGconn *conn, int payloadlen)
}
}
+ /*
+ * If user has asked for a specific mechanism name, enforce the chosen
+ * name to it.
+ */
+ if (conn->saslname && strlen(conn->saslname) > 0)
+ selected_mechanism = conn->saslname;
+
if (!selected_mechanism)
{
printfPQExpBuffer(&conn->errorMessage,
diff --git a/src/interfaces/libpq/fe-auth.h b/src/interfaces/libpq/fe-auth.h
index 2ee9c6c48c..56cafb86cc 100644
--- a/src/interfaces/libpq/fe-auth.h
+++ b/src/interfaces/libpq/fe-auth.h
@@ -24,8 +24,8 @@ extern char *pg_fe_getauthname(PQExpBuffer errorMessage);
/* Prototypes for functions in fe-auth-scram.c */
extern void *pg_fe_scram_init(const char *username, const char *password,
- bool ssl_in_use, char *tls_finish_message,
- int tls_finish_len);
+ bool ssl_in_use, char *saslchannelbinding,
+ char *tls_finish_message, int tls_finish_len);
extern void pg_fe_scram_free(void *opaq);
extern void pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen,
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 02ec8f0cea..ed144cdb3d 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -243,6 +243,15 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
"TCP-Keepalives-Count", "", 10, /* strlen(INT32_MAX) == 10 */
offsetof(struct pg_conn, keepalives_count)},
+ /* Set of options proper to SASL */
+ {"saslname", NULL, NULL, NULL,
+ "SASL-Name", "", 21, /* maximum name size per IANA == 21 */
+ offsetof(struct pg_conn, saslname)},
+
+ {"saslchannelbinding", NULL, NULL, NULL,
+ "SASL-Channel", "", 22, /* sizeof("tls-unique-for-telnet") == 22 */
+ offsetof(struct pg_conn, saslchannelbinding)},
+
/*
* ssl options are allowed even without client SSL support because the
* client can still handle SSL modes "disable" and "allow". Other
@@ -3387,6 +3396,10 @@ freePGconn(PGconn *conn)
free(conn->keepalives_interval);
if (conn->keepalives_count)
free(conn->keepalives_count);
+ if (conn->saslname)
+ free(conn->saslname);
+ if (conn->saslchannelbinding)
+ free(conn->saslchannelbinding);
if (conn->sslmode)
free(conn->sslmode);
if (conn->sslcert)
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 6313d896b7..e2d3f1f4f4 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -348,6 +348,8 @@ struct pg_conn
* retransmits */
char *keepalives_count; /* maximum number of TCP keepalive
* retransmits */
+ char *saslname; /* SASL mechanism name */
+ char *saslchannelbinding; /* channel binding used in SASL */
char *sslmode; /* SSL mode (require,prefer,allow,disable) */
char *sslcompression; /* SSL compression (0 or 1) */
char *sslkey; /* client key filename */
diff --git a/src/test/ssl/ServerSetup.pm b/src/test/ssl/ServerSetup.pm
index ad2e036602..b71969ac75 100644
--- a/src/test/ssl/ServerSetup.pm
+++ b/src/test/ssl/ServerSetup.pm
@@ -91,6 +91,9 @@ sub configure_test_server_for_ssl
{
my $node = $_[0];
my $serverhost = $_[1];
+ my $sslmethod = $_[2];
+ my $passwd = $_[3];
+ my $passwdhash = $_[4];
my $pgdata = $node->data_dir;
@@ -100,6 +103,15 @@ sub configure_test_server_for_ssl
$node->psql('postgres', "CREATE DATABASE trustdb");
$node->psql('postgres', "CREATE DATABASE certdb");
+ # Update password of each user as needed.
+ if (defined($passwd))
+ {
+ $node->psql('postgres',
+"SET password_encryption='$passwdhash'; ALTER USER ssltestuser PASSWORD '$passwd';");
+ $node->psql('postgres',
+"SET password_encryption='$passwdhash'; ALTER USER anotheruser PASSWORD '$passwd';");
+ }
+
# enable logging etc.
open my $conf, '>>', "$pgdata/postgresql.conf";
print $conf "fsync=off\n";
@@ -129,7 +141,7 @@ sub configure_test_server_for_ssl
$node->restart;
# Change pg_hba after restart because hostssl requires ssl=on
- configure_hba_for_ssl($node, $serverhost);
+ configure_hba_for_ssl($node, $serverhost, $sslmethod);
}
# Change the configuration to use given server cert file, and reload
@@ -159,6 +171,7 @@ sub configure_hba_for_ssl
{
my $node = $_[0];
my $serverhost = $_[1];
+ my $sslmethod = $_[2];
my $pgdata = $node->data_dir;
# Only accept SSL connections from localhost. Our tests don't depend on this
@@ -169,9 +182,9 @@ sub configure_hba_for_ssl
print $hba
"# TYPE DATABASE USER ADDRESS METHOD\n";
print $hba
-"hostssl trustdb ssltestuser $serverhost/32 trust\n";
+"hostssl trustdb ssltestuser $serverhost/32 $sslmethod\n";
print $hba
-"hostssl trustdb ssltestuser ::1/128 trust\n";
+"hostssl trustdb ssltestuser ::1/128 $sslmethod\n";
print $hba
"hostssl certdb ssltestuser $serverhost/32 cert\n";
print $hba
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 890e3051a2..e690c1fa15 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -32,7 +32,7 @@ $node->init;
$ENV{PGHOST} = $node->host;
$ENV{PGPORT} = $node->port;
$node->start;
-configure_test_server_for_ssl($node, $SERVERHOSTADDR);
+configure_test_server_for_ssl($node, $SERVERHOSTADDR, "trust");
switch_server_cert($node, 'server-cn-only');
### Part 1. Run client-side tests.
diff --git a/src/test/ssl/t/002_sasl.pl b/src/test/ssl/t/002_sasl.pl
new file mode 100644
index 0000000000..a625f0d473
--- /dev/null
+++ b/src/test/ssl/t/002_sasl.pl
@@ -0,0 +1,52 @@
+use strict;
+use warnings;
+use PostgresNode;
+use TestLib;
+use Test::More tests => 6;
+use ServerSetup;
+use File::Copy;
+
+# test combinations of SASL authentication for SCRAM mechanism:
+# - SCRAM-SHA-256 and SCRAM-SHA-256-PLUS
+# - Channel bindings
+
+# 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;
+
+#### Part 0. 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";
+
+# Tests with default channel binding and SASL mechanism names.
+# tls-unique is used here
+test_connect_ok($common_connstr, "saslname=SCRAM-SHA-256-PLUS");
+test_connect_fails($common_connstr, "saslname=not-exists");
+# Downgrade attack.
+test_connect_fails($common_connstr, "saslname=SCRAM-SHA-256");
+test_connect_fails($common_connstr,
+ "saslname=SCRAM-SHA-256 saslchannelbinding=tls-unique");
+
+# Channel bindings
+test_connect_ok($common_connstr,
+ "saslname=SCRAM-SHA-256-PLUS saslchannelbinding=tls-unique");
+test_connect_fails($common_connstr,
+ "saslname=SCRAM-SHA-256-PLUS saslchannelbinding=not-exists");
--
2.13.1
0004-Implement-channel-binding-tls-server-end-point-for-S.patchapplication/octet-stream; name=0004-Implement-channel-binding-tls-server-end-point-for-S.patchDownload
From 30cbfc05c4a3f0971d640310f80048f413c21c4c Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 20 Jun 2017 12:51:10 +0900
Subject: [PATCH 4/4] Implement channel binding tls-server-end-point for SCRAM
As referenced in RFC 5929, this channel binding is not the default value
and uses a hash of the certificate as binding data. On the frontend, this
can be resumed in getting the data from SSL_get_peer_certificate() and
on the backend SSL_get_certificate().
The hashing algorithm needs also to switch to SHA-256 if the signature
algorithm is MD5 or SHA-1, so let's be careful about that.
---
doc/src/sgml/protocol.sgml | 4 +-
src/backend/libpq/auth-scram.c | 21 ++++++++--
src/backend/libpq/auth.c | 13 ++++--
src/backend/libpq/be-secure-openssl.c | 69 ++++++++++++++++++++++++++++++++
src/include/libpq/libpq-be.h | 1 +
src/include/libpq/scram.h | 4 +-
src/interfaces/libpq/fe-auth-scram.c | 20 ++++++++-
src/interfaces/libpq/fe-auth.c | 18 ++++++++-
src/interfaces/libpq/fe-auth.h | 3 +-
src/interfaces/libpq/fe-secure-openssl.c | 66 ++++++++++++++++++++++++++++++
src/interfaces/libpq/libpq-int.h | 1 +
src/test/ssl/t/002_sasl.pl | 6 ++-
12 files changed, 210 insertions(+), 16 deletions(-)
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 0a556ebdc6..ee25c0c4be 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1433,8 +1433,8 @@ the password is in.
<para>
<firstterm>Channel binding</> is supported in builds with SSL support, and
uses as mechanism name <literal>SCRAM-SHA-256-PLUS</> for this purpose as
-defined per IANA. The only channel binding type supported by the server
-is <literal>tls-unique</>.
+defined per IANA. The channel binding types supported by the server
+are <literal>tls-unique</>, the default, and <literal>tls-server-end-point</>.
</para>
<procedure>
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index c46bef5bb5..7b6aa8b704 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -113,6 +113,8 @@ typedef struct
bool ssl_in_use;
char *tls_finish_message;
int tls_finish_len;
+ char *certificate_hash;
+ int certificate_hash_len;
char *channel_binding;
int iterations;
@@ -175,7 +177,9 @@ pg_be_scram_init(const char *username,
const char *shadow_pass,
bool ssl_in_use,
char *tls_finish_message,
- int tls_finish_len)
+ int tls_finish_len,
+ char *certificate_hash,
+ int certificate_hash_len)
{
scram_state *state;
bool got_verifier;
@@ -186,6 +190,8 @@ pg_be_scram_init(const char *username,
state->ssl_in_use = ssl_in_use;
state->tls_finish_message = tls_finish_message;
state->tls_finish_len = tls_finish_len;
+ state->certificate_hash = certificate_hash;
+ state->certificate_hash_len = certificate_hash_len;
state->channel_binding = NULL;
/*
@@ -847,11 +853,12 @@ read_client_first_message(scram_state *state, char *input)
errmsg("client supports SCRAM channel binding, but server does not need it for non-SSL connections")));
/*
- * Read value provided by client, only tls-unique is supported
- * for now.
+ * Read value provided by client, only tls-unique and
+ * tls-server-end-point are supported for now.
*/
channel_name = read_attr_value(&input, 'p');
- if (strcmp(channel_name, SCRAM_CHANNEL_TLS_UNIQUE) != 0)
+ if (strcmp(channel_name, SCRAM_CHANNEL_TLS_UNIQUE) != 0 &&
+ strcmp(channel_name, SCRAM_CHANNEL_TLS_ENDPOINT) != 0)
ereport(ERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION),
(errmsg("unexpected SCRAM channel-binding type"))));
@@ -1120,6 +1127,12 @@ read_client_final_message(scram_state *state, char *input)
raw_data = state->tls_finish_message;
raw_data_len = state->tls_finish_len;
}
+ else if (strcmp(state->channel_binding,
+ SCRAM_CHANNEL_TLS_ENDPOINT) == 0)
+ {
+ raw_data = state->certificate_hash;
+ raw_data_len = state->certificate_hash_len;
+ }
else
{
/* should not happen */
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 439e4e8ab9..f6b0a64863 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -849,6 +849,8 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
bool initial;
char *tls_finish = NULL;
int tls_finish_len = 0;
+ char *certificate_bash = NULL;
+ int certificate_bash_len = 0;
/*
* SASL auth is not supported for protocol versions before 3, because it
@@ -900,12 +902,15 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
#ifdef USE_SSL
/*
- * Fetch the data related to the SSL finish message to be used in the
- * exchange.
+ * Fetch the data related to the SSL finish message and the client
+ * certificate (if any) to be used in the exchange.
*/
if (port->ssl_in_use)
{
tls_finish = be_tls_get_peer_finish(port, &tls_finish_len);
+ certificate_bash =
+ be_tls_get_certificate_hash(port,
+ &certificate_bash_len);
}
#endif
@@ -924,7 +929,9 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
shadow_pass,
port->ssl_in_use,
tls_finish,
- tls_finish_len);
+ tls_finish_len,
+ certificate_bash,
+ certificate_bash_len);
/*
* Loop through SASL message exchange. This exchange can consist of
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 42c35800aa..0fa85aaeb1 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -1299,6 +1299,75 @@ be_tls_get_peer_finish(Port *port, int *len)
}
/*
+ * Get the server certificate hash for authentication purposes. Per
+ * RFC 5929 and tls-server-end-point, the TLS server's certificate bytes
+ * need to be hashed with SHA-256 if its signature algorithm is MD5 or
+ * SHA-1. If SHA-256 or something else is used, the same hash as the
+ * signature algorithm is used.
+ * The result is a palloc'd hash of the server certificate with its
+ * size, and NULL if there is nothing certificates available.
+ */
+char *
+be_tls_get_certificate_hash(Port *port, int *len)
+{
+ char *cert_hash = NULL;
+ X509 *server_cert;
+
+ *len = 0;
+ server_cert = SSL_get_certificate(port->ssl);
+
+ if (server_cert != NULL)
+ {
+ const EVP_MD *algo_type = NULL;
+ char hash[EVP_MAX_MD_SIZE]; /* size for SHA-512 */
+ unsigned int hash_size;
+ int algo_nid;
+
+ /*
+ * Get the signature algorithm of the certificate to determine the
+ * hash algorithm to use for the result.
+ */
+ if (!OBJ_find_sigid_algs(X509_get_signature_nid(server_cert),
+ &algo_nid, NULL))
+ elog(ERROR, "could not find signature algorithm");
+
+ switch (algo_nid)
+ {
+ case NID_sha512:
+ algo_type = EVP_sha512();
+ break;
+
+ case NID_sha384:
+ algo_type = EVP_sha384();
+ break;
+
+ /*
+ * Fallback to SHA-256 for weaker hashes, and keep them listed
+ * here for reference.
+ */
+ case NID_md5:
+ case NID_sha1:
+ case NID_sha224:
+ case NID_sha256:
+ default:
+ algo_type = EVP_sha256();
+ break;
+ }
+
+ /* generate and save the certificate hash */
+ if (!X509_digest(server_cert, algo_type, (unsigned char *) hash,
+ &hash_size))
+ elog(ERROR, "could not generate server certificate hash");
+
+ cert_hash = (char *) palloc(hash_size);
+ memcpy(cert_hash, hash, hash_size);
+ *len = hash_size;
+ }
+
+ return cert_hash;
+}
+
+/*
* Convert an X509 subject name to a cstring.
*
*/
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 3957d04702..274fd51b67 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -210,6 +210,7 @@ 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_peerdn_name(Port *port, char *ptr, size_t len);
extern char *be_tls_get_peer_finish(Port *port, int *len);
+extern char *be_tls_get_certificate_hash(Port *port, int *len);
#endif
extern ProtocolVersion FrontendProtocol;
diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h
index 58eae11231..6f56aabca3 100644
--- a/src/include/libpq/scram.h
+++ b/src/include/libpq/scram.h
@@ -19,6 +19,7 @@
/* Channel binding names */
#define SCRAM_CHANNEL_TLS_UNIQUE "tls-unique"
+#define SCRAM_CHANNEL_TLS_ENDPOINT "tls-server-end-point"
/* Status codes for message exchange */
#define SASL_EXCHANGE_CONTINUE 0
@@ -28,7 +29,8 @@
/* Routines dedicated to authentication */
extern void *pg_be_scram_init(const char *username, const char *shadow_pass,
bool ssl_in_use, char *tls_finish_message,
- int tls_finish_len);
+ int tls_finish_len, char *certificate_hash,
+ int certificate_hash_len);
extern int pg_be_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen, char **logdetail);
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index b781349f42..6b2215d0e9 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -48,6 +48,8 @@ typedef struct
bool ssl_in_use;
char *tls_finish_message;
int tls_finish_len;
+ char *certificate_hash;
+ int certificate_hash_len;
/* enforceable user parameters */
char *saslchannelbinding; /* name of channel binding to use */
@@ -92,7 +94,9 @@ pg_fe_scram_init(const char *username,
bool ssl_in_use,
char *saslchannelbinding,
char *tls_finish_message,
- int tls_finish_len)
+ int tls_finish_len,
+ char *certificate_hash,
+ int certificate_hash_len)
{
fe_scram_state *state;
char *prep_password;
@@ -107,6 +111,8 @@ pg_fe_scram_init(const char *username,
state->ssl_in_use = ssl_in_use;
state->tls_finish_message = tls_finish_message;
state->tls_finish_len = tls_finish_len;
+ state->certificate_hash = certificate_hash;
+ state->certificate_hash_len = certificate_hash_len;
/*
* If user has specified a channel binding to use, enforce the
@@ -150,6 +156,8 @@ pg_fe_scram_free(void *opaq)
free(state->password);
if (state->tls_finish_message)
free(state->tls_finish_message);
+ if (state->certificate_hash)
+ free(state->certificate_hash);
if (state->saslchannelbinding)
free(state->saslchannelbinding);
@@ -423,7 +431,9 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
* Construct client-final-message-without-proof. We need to remember it
* for verifying the server proof in the final step of authentication.
* Client needs to provide a b64 encoded string of the TLS finish message
- * only if a SSL connection is attempted.
+ * only if a SSL connection is attempted using "tls-unique" as channel
+ * binding. For "tls-server-end-point", a hash of the client certificate
+ * is sent instead.
*/
#ifdef USE_SSL
if (state->ssl_in_use)
@@ -436,6 +446,12 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
raw_data = state->tls_finish_message;
raw_data_len = state->tls_finish_len;
}
+ else if (strcmp(state->saslchannelbinding,
+ SCRAM_CHANNEL_TLS_ENDPOINT) == 0)
+ {
+ raw_data = state->certificate_hash;
+ raw_data_len = state->certificate_hash_len;
+ }
else
{
/* should not happen */
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 96cbf93227..928eef3cbb 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -539,6 +539,8 @@ pg_SASL_init(PGconn *conn, int payloadlen)
char *password;
char *tls_finish = NULL;
int tls_finish_len = 0;
+ char *certificate_hash = NULL;
+ int certificate_hash_len = 0;
conn->password_needed = true;
password = conn->connhost[conn->whichhost].password;
@@ -552,12 +554,21 @@ pg_SASL_init(PGconn *conn, int payloadlen)
}
#ifdef USE_SSL
- /* Fetch information about the TLS finish message */
+ /*
+ * Fetch information about the TLS finish message and client
+ * certificate if any.
+ */
if (conn->ssl_in_use)
{
tls_finish = pgtls_get_finish(conn, &tls_finish_len);
if (tls_finish == NULL)
goto oom_error;
+
+ certificate_hash =
+ pgtls_get_peer_certificate_hash(conn,
+ &certificate_hash_len);
+ if (certificate_hash == NULL)
+ goto oom_error;
}
#endif
@@ -566,7 +577,10 @@ pg_SASL_init(PGconn *conn, int payloadlen)
conn->ssl_in_use,
conn->saslchannelbinding,
tls_finish,
- tls_finish_len);
+ tls_finish_len,
+ certificate_hash,
+ certificate_hash_len);
+
if (!conn->sasl_state)
goto oom_error;
diff --git a/src/interfaces/libpq/fe-auth.h b/src/interfaces/libpq/fe-auth.h
index 56cafb86cc..7faa78fa22 100644
--- a/src/interfaces/libpq/fe-auth.h
+++ b/src/interfaces/libpq/fe-auth.h
@@ -25,7 +25,8 @@ extern char *pg_fe_getauthname(PQExpBuffer errorMessage);
/* Prototypes for functions in fe-auth-scram.c */
extern void *pg_fe_scram_init(const char *username, const char *password,
bool ssl_in_use, char *saslchannelbinding,
- char *tls_finish_message, int tls_finish_len);
+ char *tls_finish_message, int tls_finish_len,
+ char *certificate_hash, int certificate_hash_len);
extern void pg_fe_scram_free(void *opaq);
extern void pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen,
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 2da0b27b71..f6e4ac9ed9 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -418,6 +418,72 @@ pgtls_get_finish(PGconn *conn, int *len)
return result;
}
+/*
+ * Get the hash of the server certificate
+ *
+ * This information is useful for end-point channel binding, where
+ * the client certificate hash is used as a link, per RFC 5929.
+ */
+char *
+pgtls_get_peer_certificate_hash(PGconn *conn, int *len)
+{
+ char *cert_hash = NULL;
+
+ *len = 0;
+
+ if (conn->peer)
+ {
+ X509 *peer_cert = conn->peer;
+ const EVP_MD *algo_type = NULL;
+ char hash[EVP_MAX_MD_SIZE]; /* size for SHA-512 */
+ unsigned int hash_size;
+ int algo_nid;
+
+ /*
+ * Get the signature algorithm of the certificate to determine the
+ * hash algorithm to use for the result.
+ */
+ if (!OBJ_find_sigid_algs(X509_get_signature_nid(peer_cert),
+ &algo_nid, NULL))
+ return NULL;
+
+ switch (algo_nid)
+ {
+ case NID_sha512:
+ algo_type = EVP_sha512();
+ break;
+
+ case NID_sha384:
+ algo_type = EVP_sha384();
+ break;
+
+ /*
+ * Fallback to SHA-256 for weaker hashes, and keep them listed
+ * here for reference.
+ */
+ case NID_md5:
+ case NID_sha1:
+ case NID_sha224:
+ case NID_sha256:
+ default:
+ algo_type = EVP_sha256();
+ break;
+ }
+
+ if (!X509_digest(peer_cert, algo_type, (unsigned char *) hash,
+ &hash_size))
+ return NULL;
+
+ /* save result */
+ cert_hash = (char *) malloc(hash_size);
+ if (cert_hash == NULL)
+ return NULL;
+ memcpy(cert_hash, hash, hash_size);
+ *len = hash_size;
+ }
+
+ return cert_hash;
+}
/* ------------------------------------------------------------ */
/* OpenSSL specific code */
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index e2d3f1f4f4..18a3175f05 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -674,6 +674,7 @@ extern ssize_t pgtls_read(PGconn *conn, void *ptr, size_t len);
extern bool pgtls_read_pending(PGconn *conn);
extern ssize_t pgtls_write(PGconn *conn, const void *ptr, size_t len);
extern char *pgtls_get_finish(PGconn *conn, int *len);
+extern char *pgtls_get_peer_certificate_hash(PGconn *conn, int *len);
/*
* this is so that we can check if a connection is non-blocking internally
diff --git a/src/test/ssl/t/002_sasl.pl b/src/test/ssl/t/002_sasl.pl
index a625f0d473..a4e6dfac3d 100644
--- a/src/test/ssl/t/002_sasl.pl
+++ b/src/test/ssl/t/002_sasl.pl
@@ -2,7 +2,7 @@ use strict;
use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 6;
+use Test::More tests => 8;
use ServerSetup;
use File::Copy;
@@ -44,9 +44,13 @@ test_connect_fails($common_connstr, "saslname=not-exists");
test_connect_fails($common_connstr, "saslname=SCRAM-SHA-256");
test_connect_fails($common_connstr,
"saslname=SCRAM-SHA-256 saslchannelbinding=tls-unique");
+test_connect_fails($common_connstr,
+ "saslname=SCRAM-SHA-256 saslchannelbinding=tls-server-end-point");
# Channel bindings
test_connect_ok($common_connstr,
"saslname=SCRAM-SHA-256-PLUS saslchannelbinding=tls-unique");
+test_connect_ok($common_connstr,
+ "saslname=SCRAM-SHA-256-PLUS saslchannelbinding=tls-server-end-point");
test_connect_fails($common_connstr,
"saslname=SCRAM-SHA-256-PLUS saslchannelbinding=not-exists");
--
2.13.1
On 20/06/17 06:11, Michael Paquier wrote:
On Tue, Jun 6, 2017 at 3:40 PM, Michael Paquier
<michael.paquier@gmail.com> wrote:As far as I can see, there are a couple of things that I still need to
work on to make people happy:
- Rework the generic APIs for TLS finish and endpoint so as any
implementation can use channel binding without inducing any extra code
footprint to be-secure.c and fe-secure.c.
- Implement endpoint, as Alvaro is saying for JDBC that would be nicer.
- Have a couple of tests for channel binding to allow people to test
the feature easily. Those will be in src/test/ssl/. It would be nice
as well to be able to enforce the channel binding type on libpq-side,
which is useful at least for testing. So we are going to need an
environment variable for this purpose, and a connection parameter.Okay, here we go. Attached is a set of four patches:
- 0001 is some refactoring for the SSL tests so as other test suite in
src/test/ssl can take advantage of the connection routines. There is
nothing fancy here.
- 0002 is the implementation of tls-unique as channel binding. This
has been largely reworked since last submission, I have found on the
way a couple of bugs and some correctness issues.
- 0003 is a patch to add as connection parameters saslname and
saslchannelbinding. With support of more SASL mechanisms (PG10 has
SCRAM-SHA-256, I am adding SCRAM-SHA-256-PLUS here), saslname can be
used to enforce on the client-side the value of the SASL mechanism
chosen. saslchannelbinding does the same for the channel binding name.
This is very useful for testing, and a set of tests are added in
src/test/ssl/ for tls-unique and the SASL mechanisms. The tests cover
many scenarios, like downgrade attacks for example.
- 0004 is the implementation of tls-server-end-point, as Alvaro has
asked. Per RFC 5929, the binding data needs to be a hash of the server
certificate. If the signature algorithm of the certificate is MD5 or
SHA-1, then SHA-256 is used. Other signature algos like SHA-384 or 512
are used to hash the data. The hashed data is then encoded in base64
and sent to the server for verification. Tests using saslchannelname
have been added as well. It took me a while to find out that
OBJ_find_sigid_algs(X509_get_signature_nid(X509*)) needs to be used to
find out the algorithm of a certificate with OpenSSL.With the tests directly in the patch, things are easy to run. WIth
PG10 stabilization work, of course I don't expect much feedback :)
But this set of patches looks like the direction we want to go so as
JDBC and libpq users can take advantage of channel binding with SCRAM.
This is awesome, Michael.
In the coming weeks, and once my PR for pgjdbc has been added, I
will work towards another patch to implement channel binding. Should be
reasonably easy now, thanks to this.
Appreciated!
Álvaro
--
Álvaro Hernández Tortosa
-----------
<8K>data
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Wed, Jun 21, 2017 at 4:04 AM, Álvaro Hernández Tortosa
<aht@8kdata.com> wrote:
In the coming weeks, and once my PR for pgjdbc has been added, I will
work towards another patch to implement channel binding. Should be
reasonably easy now, thanks to this.
So you basically have an equivalent of OpenSSL stuff in java, right?
- SSL_get_peer_certificate to get the X509 point of the server.
- X509_digest to hash it.
- OBJ_find_sigid_algs and X509_get_signature_nid to guess the
signature algorithm of a certificate. I think that this part can be
tricky depending on the SSL implementation, but I have designed a
generic API for this purpose.
That's all it took me to get end-point to work. Plus the error
handling of course.
--
Michael
--
Sent via pgsql-jdbc mailing list (pgsql-jdbc@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-jdbc
On Tue, Jun 20, 2017 at 1:11 PM, Michael Paquier
<michael.paquier@gmail.com> wrote:
With the tests directly in the patch, things are easy to run. WIth
PG10 stabilization work, of course I don't expect much feedback :)
But this set of patches looks like the direction we want to go so as
JDBC and libpq users can take advantage of channel binding with SCRAM.
Attached is a new patch set, rebased as of c6293249.
--
Michael
Attachments:
0001-Refactor-routine-to-test-connection-to-SSL-server.patchapplication/octet-stream; name=0001-Refactor-routine-to-test-connection-to-SSL-server.patchDownload
From be3cf0785596aee4cfe2682cc19a6c31eba91f3f Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 16 Jun 2017 17:00:06 +0900
Subject: [PATCH 1/4] Refactor routine to test connection to SSL server
Move the sub-routines wrappers to check if a connection to a server is
fine or not into the test main module. This is useful for other tests
willing to check connectivity into a server.
---
src/test/ssl/ServerSetup.pm | 45 +++++++++++++-
src/test/ssl/t/001_ssltests.pl | 132 +++++++++++++++++------------------------
2 files changed, 100 insertions(+), 77 deletions(-)
diff --git a/src/test/ssl/ServerSetup.pm b/src/test/ssl/ServerSetup.pm
index f63c81cfc6..ad2e036602 100644
--- a/src/test/ssl/ServerSetup.pm
+++ b/src/test/ssl/ServerSetup.pm
@@ -26,9 +26,52 @@ use Test::More;
use Exporter 'import';
our @EXPORT = qw(
- configure_test_server_for_ssl switch_server_cert
+ configure_test_server_for_ssl
+ run_test_psql
+ switch_server_cert
+ test_connect_fails
+ test_connect_ok
);
+# Define a couple of helper functions to test connecting to the server.
+
+# Attempt connection to server with given connection string.
+sub run_test_psql
+{
+ my $connstr = $_[0];
+ my $logstring = $_[1];
+
+ my $cmd = [
+ 'psql', '-X', '-A', '-t', '-c', "SELECT 'connected with $connstr'",
+ '-d', "$connstr" ];
+
+ my $result = run_log($cmd);
+ return $result;
+}
+
+#
+# The first argument is a base connection string to use for connection.
+# The second argument is a complementary connection string, and it's also
+# printed out as the test case name.
+sub test_connect_ok
+{
+ my $common_connstr = $_[0];
+ my $connstr = $_[1];
+
+ my $result =
+ run_test_psql("$common_connstr $connstr", "(should succeed)");
+ ok($result, $connstr);
+}
+
+sub test_connect_fails
+{
+ my $common_connstr = $_[0];
+ my $connstr = $_[1];
+
+ my $result = run_test_psql("$common_connstr $connstr", "(should fail)");
+ ok(!$result, "$connstr (should fail)");
+}
+
# Copy a set of files, taking into account wildcards
sub copy_files
{
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 32df273929..890e3051a2 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -13,44 +13,9 @@ use File::Copy;
# postgresql-ssl-regression.test.
my $SERVERHOSTADDR = '127.0.0.1';
-# Define a couple of helper functions to test connecting to the server.
-
+# Allocation of base connection string shared among multiple tests.
my $common_connstr;
-sub run_test_psql
-{
- my $connstr = $_[0];
- my $logstring = $_[1];
-
- my $cmd = [
- 'psql', '-X', '-A', '-t', '-c', "SELECT 'connected with $connstr'",
- '-d', "$connstr" ];
-
- my $result = run_log($cmd);
- return $result;
-}
-
-#
-# The first argument is a (part of a) connection string, and it's also printed
-# out as the test case name. It is appended to $common_connstr global variable,
-# which also contains a libpq connection string.
-sub test_connect_ok
-{
- my $connstr = $_[0];
-
- my $result =
- run_test_psql("$common_connstr $connstr", "(should succeed)");
- ok($result, $connstr);
-}
-
-sub test_connect_fails
-{
- my $connstr = $_[0];
-
- my $result = run_test_psql("$common_connstr $connstr", "(should fail)");
- ok(!$result, "$connstr (should fail)");
-}
-
# The client's private key must not be world-readable, so take a copy
# of the key stored in the code tree and update its permissions.
copy("ssl/client.key", "ssl/client_tmp.key");
@@ -83,50 +48,59 @@ $common_connstr =
# The server should not accept non-SSL connections
note "test that the server doesn't accept non-SSL connections";
-test_connect_fails("sslmode=disable");
+test_connect_fails($common_connstr, "sslmode=disable");
# Try without a root cert. In sslmode=require, this should work. In verify-ca
# or verify-full mode it should fail
note "connect without server root cert";
-test_connect_ok("sslrootcert=invalid sslmode=require");
-test_connect_fails("sslrootcert=invalid sslmode=verify-ca");
-test_connect_fails("sslrootcert=invalid sslmode=verify-full");
+test_connect_ok($common_connstr, "sslrootcert=invalid sslmode=require");
+test_connect_fails($common_connstr, "sslrootcert=invalid sslmode=verify-ca");
+test_connect_fails($common_connstr, "sslrootcert=invalid sslmode=verify-full");
# Try with wrong root cert, should fail. (we're using the client CA as the
# root, but the server's key is signed by the server CA)
note "connect without wrong server root cert";
-test_connect_fails("sslrootcert=ssl/client_ca.crt sslmode=require");
-test_connect_fails("sslrootcert=ssl/client_ca.crt sslmode=verify-ca");
-test_connect_fails("sslrootcert=ssl/client_ca.crt sslmode=verify-full");
+test_connect_fails($common_connstr,
+ "sslrootcert=ssl/client_ca.crt sslmode=require");
+test_connect_fails($common_connstr,
+ "sslrootcert=ssl/client_ca.crt sslmode=verify-ca");
+test_connect_fails($common_connstr,
+ "sslrootcert=ssl/client_ca.crt sslmode=verify-full");
# Try with just the server CA's cert. This fails because the root file
# must contain the whole chain up to the root CA.
note "connect with server CA cert, without root CA";
-test_connect_fails("sslrootcert=ssl/server_ca.crt sslmode=verify-ca");
+test_connect_fails($common_connstr,
+ "sslrootcert=ssl/server_ca.crt sslmode=verify-ca");
# And finally, with the correct root cert.
note "connect with correct server CA cert file";
-test_connect_ok("sslrootcert=ssl/root+server_ca.crt sslmode=require");
-test_connect_ok("sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca");
-test_connect_ok("sslrootcert=ssl/root+server_ca.crt sslmode=verify-full");
+test_connect_ok($common_connstr,
+ "sslrootcert=ssl/root+server_ca.crt sslmode=require");
+test_connect_ok($common_connstr,
+ "sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca");
+test_connect_ok($common_connstr,
+ "sslrootcert=ssl/root+server_ca.crt sslmode=verify-full");
# Test with cert root file that contains two certificates. The client should
# be able to pick the right one, regardless of the order in the file.
-test_connect_ok("sslrootcert=ssl/both-cas-1.crt sslmode=verify-ca");
-test_connect_ok("sslrootcert=ssl/both-cas-2.crt sslmode=verify-ca");
+test_connect_ok($common_connstr,
+ "sslrootcert=ssl/both-cas-1.crt sslmode=verify-ca");
+test_connect_ok($common_connstr,
+ "sslrootcert=ssl/both-cas-2.crt sslmode=verify-ca");
note "testing sslcrl option with a non-revoked cert";
# Invalid CRL filename is the same as no CRL, succeeds
-test_connect_ok(
+test_connect_ok($common_connstr,
"sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrl=invalid");
# A CRL belonging to a different CA is not accepted, fails
-test_connect_fails(
+test_connect_fails($common_connstr,
"sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrl=ssl/client.crl");
# With the correct CRL, succeeds (this cert is not revoked)
-test_connect_ok(
+test_connect_ok($common_connstr,
"sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrl=ssl/root+server.crl"
);
@@ -136,9 +110,9 @@ note "test mismatch between hostname and server certificate";
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
-test_connect_ok("sslmode=require host=wronghost.test");
-test_connect_ok("sslmode=verify-ca host=wronghost.test");
-test_connect_fails("sslmode=verify-full host=wronghost.test");
+test_connect_ok($common_connstr, "sslmode=require host=wronghost.test");
+test_connect_ok($common_connstr, "sslmode=verify-ca host=wronghost.test");
+test_connect_fails($common_connstr, "sslmode=verify-full host=wronghost.test");
# Test Subject Alternative Names.
switch_server_cert($node, 'server-multiple-alt-names');
@@ -147,12 +121,13 @@ note "test hostname matching with X.509 Subject Alternative Names";
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
-test_connect_ok("host=dns1.alt-name.pg-ssltest.test");
-test_connect_ok("host=dns2.alt-name.pg-ssltest.test");
-test_connect_ok("host=foo.wildcard.pg-ssltest.test");
+test_connect_ok($common_connstr, "host=dns1.alt-name.pg-ssltest.test");
+test_connect_ok($common_connstr, "host=dns2.alt-name.pg-ssltest.test");
+test_connect_ok($common_connstr, "host=foo.wildcard.pg-ssltest.test");
-test_connect_fails("host=wronghost.alt-name.pg-ssltest.test");
-test_connect_fails("host=deep.subdomain.wildcard.pg-ssltest.test");
+test_connect_fails($common_connstr, "host=wronghost.alt-name.pg-ssltest.test");
+test_connect_fails($common_connstr,
+ "host=deep.subdomain.wildcard.pg-ssltest.test");
# Test certificate with a single Subject Alternative Name. (this gives a
# slightly different error message, that's all)
@@ -162,10 +137,11 @@ note "test hostname matching with a single X.509 Subject Alternative Name";
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
-test_connect_ok("host=single.alt-name.pg-ssltest.test");
+test_connect_ok($common_connstr, "host=single.alt-name.pg-ssltest.test");
-test_connect_fails("host=wronghost.alt-name.pg-ssltest.test");
-test_connect_fails("host=deep.subdomain.wildcard.pg-ssltest.test");
+test_connect_fails($common_connstr, "host=wronghost.alt-name.pg-ssltest.test");
+test_connect_fails($common_connstr,
+ "host=deep.subdomain.wildcard.pg-ssltest.test");
# Test server certificate with a CN and SANs. Per RFCs 2818 and 6125, the CN
# should be ignored when the certificate has both.
@@ -175,9 +151,9 @@ note "test certificate with both a CN and SANs";
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
-test_connect_ok("host=dns1.alt-name.pg-ssltest.test");
-test_connect_ok("host=dns2.alt-name.pg-ssltest.test");
-test_connect_fails("host=common-name.pg-ssltest.test");
+test_connect_ok($common_connstr, "host=dns1.alt-name.pg-ssltest.test");
+test_connect_ok($common_connstr, "host=dns2.alt-name.pg-ssltest.test");
+test_connect_fails($common_connstr, "host=common-name.pg-ssltest.test");
# Finally, test a server certificate that has no CN or SANs. Of course, that's
# not a very sensible certificate, but libpq should handle it gracefully.
@@ -185,8 +161,10 @@ switch_server_cert($node, 'server-no-names');
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR";
-test_connect_ok("sslmode=verify-ca host=common-name.pg-ssltest.test");
-test_connect_fails("sslmode=verify-full host=common-name.pg-ssltest.test");
+test_connect_ok($common_connstr,
+ "sslmode=verify-ca host=common-name.pg-ssltest.test");
+test_connect_fails($common_connstr,
+ "sslmode=verify-full host=common-name.pg-ssltest.test");
# Test that the CRL works
note "testing client-side CRL";
@@ -196,8 +174,9 @@ $common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid hostaddr=$SERVERHOSTADDR host=common-name.pg-ssltest.test";
# Without the CRL, succeeds. With it, fails.
-test_connect_ok("sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca");
-test_connect_fails(
+test_connect_ok($common_connstr,
+ "sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca");
+test_connect_fails($common_connstr,
"sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrl=ssl/root+server.crl"
);
@@ -210,18 +189,18 @@ $common_connstr =
"sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=certdb hostaddr=$SERVERHOSTADDR";
# no client cert
-test_connect_fails("user=ssltestuser sslcert=invalid");
+test_connect_fails($common_connstr, "user=ssltestuser sslcert=invalid");
# correct client cert
-test_connect_ok(
+test_connect_ok($common_connstr,
"user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key");
# client cert belonging to another user
-test_connect_fails(
+test_connect_fails($common_connstr,
"user=anotheruser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key");
# revoked client cert
-test_connect_fails(
+test_connect_fails($common_connstr,
"user=ssltestuser sslcert=ssl/client-revoked.crt sslkey=ssl/client-revoked.key"
);
@@ -230,8 +209,9 @@ switch_server_cert($node, 'server-cn-only', 'root_ca');
$common_connstr =
"user=ssltestuser dbname=certdb sslkey=ssl/client_tmp.key sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR";
-test_connect_ok("sslmode=require sslcert=ssl/client+client_ca.crt");
-test_connect_fails("sslmode=require sslcert=ssl/client.crt");
+test_connect_ok($common_connstr,
+ "sslmode=require sslcert=ssl/client+client_ca.crt");
+test_connect_fails($common_connstr, "sslmode=require sslcert=ssl/client.crt");
# clean up
unlink "ssl/client_tmp.key";
--
2.14.1
0002-Support-channel-binding-tls-unique-in-SCRAM.patchapplication/octet-stream; name=0002-Support-channel-binding-tls-unique-in-SCRAM.patchDownload
From 0cdd0e849e673f2abd3ff2d5ebd144959574be69 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 20 Jun 2017 10:18:58 +0900
Subject: [PATCH 2/4] 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 finish messages, a PG-like API is added to be able to
make the interface pluggable for other SSL implementations.
---
doc/src/sgml/protocol.sgml | 24 +++--
src/backend/libpq/auth-scram.c | 151 +++++++++++++++++++++++++------
src/backend/libpq/auth.c | 86 +++++++++++++++---
src/backend/libpq/be-secure-openssl.c | 24 +++++
src/include/libpq/libpq-be.h | 1 +
src/include/libpq/scram.h | 10 +-
src/interfaces/libpq/fe-auth-scram.c | 105 ++++++++++++++++-----
src/interfaces/libpq/fe-auth.c | 43 ++++++++-
src/interfaces/libpq/fe-auth.h | 4 +-
src/interfaces/libpq/fe-secure-openssl.c | 26 ++++++
src/interfaces/libpq/libpq-int.h | 5 +-
11 files changed, 404 insertions(+), 75 deletions(-)
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index c8b083c29c..1863c0905c 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1342,10 +1342,11 @@
<para>
<firstterm>SASL</> is a framework for authentication in connection-oriented
-protocols. At the moment, <productname>PostgreSQL</> implements only one SASL
-authentication mechanism, SCRAM-SHA-256, but more might be added in the
-future. The below steps illustrate how SASL authentication is performed in
-general, while the next subsection gives more details on SCRAM-SHA-256.
+protocols. At the moment, <productname>PostgreSQL</> implements only two SASL
+authentication mechanisms, SCRAM-SHA-256 and SCRAM-SHA-256-PLUS, but more
+might be added in the future. The below steps illustrate how SASL
+authentication is performed in general, while the next subsection gives
+more details on SCRAM-SHA-256 and SCRAM-SHA-256-PLUS.
</para>
<procedure>
@@ -1430,7 +1431,10 @@ the password is in.
</para>
<para>
-<firstterm>Channel binding</> has not been implemented yet.
+<firstterm>Channel binding</> is supported in builds with SSL support, and
+uses as mechanism name <literal>SCRAM-SHA-256-PLUS</> for this purpose as
+defined per IANA. The only channel binding type supported by the server
+is <literal>tls-unique</>.
</para>
<procedure>
@@ -1439,14 +1443,18 @@ the password is in.
<para>
The server sends an AuthenticationSASL message. It includes a list of
SASL authentication mechanisms that the server can accept.
+ <literal>SCRAM-SHA-256</> and <literal>SCRAM-SHA-256-PLUS</> are the
+ two mechanism names that the server lists in this message. Support for
+ channel binding is not included if the server is built without SSL
+ support and if the connection attempt is done without SSL.
</para>
</step>
<step id="scram-client-first">
<para>
The client responds by sending a SASLInitialResponse message, which
- indicates the chosen mechanism, <literal>SCRAM-SHA-256</>. In the Initial
- Client response field, the message contains the SCRAM
- <structname>client-first-message</>.
+ indicates the chosen mechanism, <literal>SCRAM-SHA-256</> or
+ <literal>SCRAM-SHA-256-PLUS</>. In the Initial Client response field,
+ the message contains the SCRAM <structname>client-first-message</>.
</para>
</step>
<step id="scram-server-first">
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index 0b69f106f1..72333c8784 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,10 @@ typedef struct
const char *username; /* username from startup packet */
+ bool ssl_in_use;
+ char *tls_finish_message;
+ int tls_finish_len;
+
int iterations;
char *salt; /* base64-encoded */
uint8 StoredKey[SCRAM_KEY_LEN];
@@ -168,7 +170,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,
+ char *tls_finish_message,
+ int tls_finish_len)
{
scram_state *state;
bool got_verifier;
@@ -176,6 +182,9 @@ 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_finish_message = tls_finish_message;
+ state->tls_finish_len = tls_finish_len;
/*
* Parse the stored password verifier.
@@ -773,31 +782,93 @@ read_client_first_message(scram_state *state, char *input)
*------
*/
- /* read gs2-cbind-flag */
+ /*
+ * Read gs2-cbind-flag. Server handles its value as described in RFC 5802,
+ * section 6 dealing with channel binding.
+ */
switch (*input)
{
case 'n':
- /* Client does not support channel binding */
+ /*
+ * Client does not support channel binding, this is authorized
+ * only in builds not supporting SSL. If SSL is supported, the
+ * server cannot support this option either.
+ */
+#ifdef USE_SSL
+ if (state->ssl_in_use)
+ ereport(ERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("client does not support SCRAM channel binding, but server needs it for SSL connections")));
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("client does not support SCRAM channel binding, but server does")));
+#endif
+ 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 */
+ /*
+ * Client supports channel binding, but we're not doing it today
+ * in the context of a non-SSL connection. Complain though if
+ * the client is trying to trick the server in not doing channel
+ * binding with a downgrade attack if a SSL connection is
+ * attempted.
+ */
+ if (state->ssl_in_use)
+ ereport(ERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("client support SCRAM channel binding, but server needs it for SSL connections")));
+ 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':
+ {
+#ifdef USE_SSL
+ char *channel_name;
- /*
- * 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")));
+ if (!state->ssl_in_use)
+ ereport(ERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("client supports SCRAM channel binding, but server does not need it for non-SSL connections")));
+
+ /*
+ * Read value provided by client, only tls-unique is supported
+ * for now.
+ */
+ channel_name = read_attr_value(&input, 'p');
+ if (strcmp(channel_name, SCRAM_CHANNEL_TLS_UNIQUE) != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ (errmsg("unexpected SCRAM channel-binding type"))));
+#else
+ /*
+ * 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")));
+#endif
+ }
+ break;
default:
ereport(ERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION),
@@ -805,13 +876,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 +1096,47 @@ 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. We don't support channel binding for builds
+ * without SSL support, so it's expected to always be "biws" in this case,
+ * which is "n,,", base64-encoded. In builds supporting SSL, "biws" is
+ * used for Non-SSL connection attempts, and for SSL connections the
+ * client has to provide channel binding value.
*/
channel_binding = read_attr_value(&p, 'c');
+#ifdef USE_SSL
+ if (state->ssl_in_use)
+ {
+ char *enc_tls_message;
+ int enc_tls_len;
+
+ enc_tls_message = palloc(pg_b64_enc_len(state->tls_finish_len) + 1);
+ enc_tls_len = pg_b64_encode(state->tls_finish_message,
+ state->tls_finish_len,
+ enc_tls_message);
+ enc_tls_message[enc_tls_len] = '\0';
+
+ /*
+ * Compare the value sent by the client with the TLS finish message
+ * expected by the server.
+ */
+ if (strcmp(channel_binding, enc_tls_message) != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ (errmsg("no match for SCRAM channel-binding attribute in client-final-message"))));
+ }
+ else
+ {
+ if (strcmp(channel_binding, "biws") != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ (errmsg("unexpected SCRAM channel-binding attribute in client-final-message"))));
+ }
+#else
if (strcmp(channel_binding, "biws") != 0)
ereport(ERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION),
(errmsg("unexpected SCRAM channel-binding attribute in client-final-message"))));
+#endif
state->client_final_nonce = read_attr_value(&p, 'r');
/* ignore optional extensions */
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index cb30fc7b71..ee9bce03a2 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -859,10 +859,14 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
void *scram_opaq;
char *output = NULL;
int outputlen = 0;
- char *input;
+ char *input, *p;
+ char *sasl_mechs;
int inputlen;
+ int listlen = 0;
int result;
bool initial;
+ char *tls_finish = NULL;
+ int tls_finish_len = 0;
/*
* SASL auth is not supported for protocol versions before 3, because it
@@ -879,12 +883,49 @@ 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:
+ * - SCRAM-SHA-256, which is the mechanism with the same name.
+ * - SCRAM-SHA-256-PLUS, which is SCRAM-SHA-256 with channel binding. This
+ * is advertised to the client only if connection is attempted with SSL.
+ * The order of mechanisms is advertised in decreasing order of importance.
+ * The extra "\0" is for an empty string to terminate the list, and each
+ * mechanism listed needs to be separated with "\0".
*/
- sendAuthRequest(port, AUTH_REQ_SASL, SCRAM_SHA256_NAME "\0",
- strlen(SCRAM_SHA256_NAME) + 2);
+ listlen = 0;
+ sasl_mechs = (char *) palloc(strlen(SCRAM_SHA256_PLUS_NAME) +
+ strlen(SCRAM_SHA256_NAME) + 3);
+ p = sasl_mechs;
+
+ /* add SCRAM-SHA-256-PLUS, which depends on if SSL is in use */
+ if (port->ssl_in_use)
+ {
+ strcpy(p, SCRAM_SHA256_PLUS_NAME);
+ listlen += strlen(SCRAM_SHA256_PLUS_NAME) + 1;
+ p += strlen(SCRAM_SHA256_PLUS_NAME) + 1;
+ }
+
+ /* add generic SCRAM-SHA-256 */
+ strcpy(p, SCRAM_SHA256_NAME);
+ listlen += strlen(SCRAM_SHA256_NAME) + 1;
+ p += strlen(SCRAM_SHA256_NAME) + 1;
+
+ /* put "\0" to mark that list is finished */
+ p[0] = '\0';
+ listlen++;
+
+ sendAuthRequest(port, AUTH_REQ_SASL, sasl_mechs, listlen);
+ pfree(sasl_mechs);
+
+#ifdef USE_SSL
+ /*
+ * Fetch the data related to the SSL finish message to be used in the
+ * exchange.
+ */
+ if (port->ssl_in_use)
+ {
+ tls_finish = be_tls_get_peer_finish(port, &tls_finish_len);
+ }
+#endif
/*
* Initialize the status tracker for message exchanges.
@@ -897,7 +938,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_finish,
+ tls_finish_len);
/*
* Loop through SASL message exchange. This exchange can consist of
@@ -946,17 +991,36 @@ 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.
+ * We only support SCRAM-SHA-256 and SCRAM-SHA-256-PLUS 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),
- errmsg("client selected an invalid SASL authentication mechanism")));
+ errmsg("client selected an invalid SASL authentication mechanism"),
+ errdetail("Incorrect SASL mechanism name specified")));
}
+#ifdef USE_SSL
+ /*
+ * If an SSL connection is attempted, check for downgrade attacks.
+ * A connection is not allowed to specify SCRAM-SHA-256 if its
+ * -PLUS version has been submitted back to the client, which is
+ * the default.
+ */
+ if (port->ssl_in_use &&
+ strcmp(selected_mech, SCRAM_SHA256_NAME) == 0)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("client selected invalid SASL authentication mechanism"),
+ errdetail("Channel binding published to client but not selected.")));
+ }
+#endif
+
inputlen = pq_getmsgint(&buf, 4);
if (inputlen == -1)
input = NULL;
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index fe15227a77..d063a587d0 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -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 finish message information from the
+ * client, useful for authorization when doing channel binding.
+ *
+ * Result is a palloc'd copy of the TLS finish message with its size.
+ */
+char *
+be_tls_get_peer_finish(Port *port, int *len)
+{
+ char dummy[1];
+ char *result;
+
+ /*
+ * OpenSSL does not offer an API to get directly the length of the
+ * expected TLS finish 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 = (char *) palloc(*len * sizeof(char));
+ (void) SSL_get_peer_finished(port->ssl, result, *len);
+
+ return result;
+}
+
/*
* Convert an X509 subject name to a cstring.
*
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 7bde744d51..5d59d79822 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -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_cipher(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_finish(Port *port, int *len);
#endif
extern ProtocolVersion FrontendProtocol;
diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h
index 0166e1945d..91cebe9a9b 100644
--- a/src/include/libpq/scram.h
+++ b/src/include/libpq/scram.h
@@ -13,8 +13,12 @@
#ifndef 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_PLUS_NAME "SCRAM-SHA-256-PLUS" /* with channel binding */
+
+/* Channel binding names */
+#define SCRAM_CHANNEL_TLS_UNIQUE "tls-unique"
/* Status codes for message exchange */
#define SASL_EXCHANGE_CONTINUE 0
@@ -22,7 +26,9 @@
#define SASL_EXCHANGE_FAILURE 2
/* 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, char *tls_finish_message,
+ int tls_finish_len);
extern int pg_be_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen, char **logdetail);
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index d1c7037101..8b2a64f213 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -17,6 +17,7 @@
#include "common/base64.h"
#include "common/saslprep.h"
#include "common/scram-common.h"
+#include "libpq/scram.h"
#include "fe-auth.h"
/* These are needed for getpid(), in the fallback implementation */
@@ -44,6 +45,9 @@ typedef struct
/* These are supplied by the user */
const char *username;
char *password;
+ bool ssl_in_use;
+ char *tls_finish_message;
+ int tls_finish_len;
/* We construct these */
uint8 SaltedPassword[SCRAM_KEY_LEN];
@@ -81,7 +85,11 @@ static bool pg_frontend_random(char *dst, int len);
* Initialize SCRAM exchange status.
*/
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,
+ char *tls_finish_message,
+ int tls_finish_len)
{
fe_scram_state *state;
char *prep_password;
@@ -93,6 +101,9 @@ pg_fe_scram_init(const char *username, const char *password)
memset(state, 0, sizeof(fe_scram_state));
state->state = FE_SCRAM_INIT;
state->username = username;
+ state->ssl_in_use = ssl_in_use;
+ state->tls_finish_message = tls_finish_message;
+ state->tls_finish_len = tls_finish_len;
/* Normalize the password with SASLprep, if possible */
rc = pg_saslprep(password, &prep_password);
@@ -297,9 +308,10 @@ static char *
build_client_first_message(fe_scram_state *state, PQExpBuffer errormessage)
{
char raw_nonce[SCRAM_RAW_NONCE_LEN + 1];
- char *buf;
- char buflen;
+ char *result;
+ int channel_info_len;
int encoded_len;
+ PQExpBufferData buf;
/*
* Generate a "raw" nonce. This is converted to ASCII-printable form by
@@ -328,26 +340,55 @@ build_client_first_message(fe_scram_state *state, PQExpBuffer errormessage)
* prepared with SASLprep, the message parsing would fail if it includes
* '=' or ',' characters.
*/
- buflen = 8 + strlen(state->client_nonce) + 1;
- buf = malloc(buflen);
- if (buf == NULL)
- {
- printfPQExpBuffer(errormessage,
- libpq_gettext("out of memory\n"));
- return NULL;
- }
- snprintf(buf, buflen, "n,,n=,r=%s", state->client_nonce);
+ initPQExpBuffer(&buf);
- state->client_first_message_bare = strdup(buf + 3);
+ /*
+ * First build the query field for channel binding. If the client is not
+ * built with SSL support, it cannot support channel binding so it needs
+ * to use "n" to let the server know. If built with SSL support, client
+ * needs to use "y" to let the server know that client has such support
+ * but that it is not using it as a non-SSL connection is requested.
+ * Finally if a SSL connection is done, use p=cb-name, for which only
+ * "tls-unique" is supported now.
+ */
+#ifdef USE_SSL
+ if (state->ssl_in_use)
+ appendPQExpBuffer(&buf, "p=%s", SCRAM_CHANNEL_TLS_UNIQUE);
+ else
+ appendPQExpBuffer(&buf, "y");
+#else
+ appendPQExpBuffer(&buf, "n");
+#endif
+
+ if (PQExpBufferDataBroken(buf))
+ goto oom_error;
+
+ 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)
- {
- free(buf);
- printfPQExpBuffer(errormessage,
- libpq_gettext("out of memory\n"));
- return NULL;
- }
+ goto oom_error;
- return buf;
+ result = strdup(buf.data);
+ if (result == NULL)
+ goto oom_error;
+
+ termPQExpBuffer(&buf);
+ return result;
+
+oom_error:
+ termPQExpBuffer(&buf);
+ printfPQExpBuffer(errormessage,
+ libpq_gettext("out of memory\n"));
+ return NULL;
}
/*
@@ -365,8 +406,30 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
/*
* Construct client-final-message-without-proof. We need to remember it
* for verifying the server proof in the final step of authentication.
+ * Client needs to provide a b64 encoded string of the TLS finish message
+ * only if a SSL connection is attempted.
*/
- appendPQExpBuffer(&buf, "c=biws,r=%s", state->nonce);
+#ifdef USE_SSL
+ if (state->ssl_in_use)
+ {
+ appendPQExpBuffer(&buf, "c=");
+ if (!enlargePQExpBuffer(&buf, pg_b64_enc_len(state->tls_finish_len)))
+ goto oom_error;
+ buf.len += pg_b64_encode(state->tls_finish_message,
+ state->tls_finish_len,
+ buf.data + buf.len);
+ buf.data[buf.len] = '\0';
+ }
+ else
+ appendPQExpBuffer(&buf, "c=biws");
+#else
+ appendPQExpBuffer(&buf, "c=biws");
+#endif
+
+ if (PQExpBufferDataBroken(buf))
+ goto oom_error;
+
+ appendPQExpBuffer(&buf, ",r=%s", state->nonce);
if (PQExpBufferDataBroken(buf))
goto oom_error;
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 382558f3f8..05f04be10a 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -504,7 +504,8 @@ pg_SASL_init(PGconn *conn, int payloadlen)
/*
* Parse the list of SASL authentication mechanisms in the
* AuthenticationSASL message, and select the best mechanism that we
- * support. (Only SCRAM-SHA-256 is supported at the moment.)
+ * support. SCRAM-SHA-256 and SCRAM-SHA-256-PLUS are the only ones
+ * supported at the moment.
*/
selected_mechanism = NULL;
for (;;)
@@ -532,9 +533,12 @@ pg_SASL_init(PGconn *conn, int payloadlen)
/*
* Do we support this mechanism?
*/
- if (strcmp(mechanism_buf.data, SCRAM_SHA256_NAME) == 0)
+ if (strcmp(mechanism_buf.data, SCRAM_SHA256_NAME) == 0 ||
+ strcmp(mechanism_buf.data, SCRAM_SHA256_PLUS_NAME) == 0)
{
char *password;
+ char *tls_finish = NULL;
+ int tls_finish_len = 0;
conn->password_needed = true;
password = conn->connhost[conn->whichhost].password;
@@ -547,10 +551,41 @@ pg_SASL_init(PGconn *conn, int payloadlen)
goto error;
}
- conn->sasl_state = pg_fe_scram_init(conn->pguser, password);
+#ifdef USE_SSL
+ /* Fetch information about the TLS finish message */
+ if (conn->ssl_in_use)
+ {
+ tls_finish = pgtls_get_finish(conn, &tls_finish_len);
+ if (tls_finish == NULL)
+ goto oom_error;
+ }
+#endif
+
+ conn->sasl_state = pg_fe_scram_init(conn->pguser,
+ password,
+ conn->ssl_in_use,
+ tls_finish,
+ tls_finish_len);
if (!conn->sasl_state)
goto oom_error;
- selected_mechanism = SCRAM_SHA256_NAME;
+
+ /*
+ * Select the mechanism to use by default. If SSL connection
+ * is attempted, the server will expect the -PLUS mechanism.
+ * If not, fallback to SCRAM-SHA-256.
+ */
+#ifdef USE_SSL
+ if (conn->ssl_in_use &&
+ strcmp(mechanism_buf.data, SCRAM_SHA256_PLUS_NAME) == 0)
+ selected_mechanism = SCRAM_SHA256_PLUS_NAME;
+ else if (!conn->ssl_in_use &&
+ strcmp(mechanism_buf.data, SCRAM_SHA256_NAME) == 0)
+ selected_mechanism = SCRAM_SHA256_NAME;
+#else
+ /* No channel binding can be selected without SSL support */
+ if (strcmp(mechanism_buf.data, SCRAM_SHA256_NAME) == 0)
+ selected_mechanism = SCRAM_SHA256_NAME;
+#endif
}
}
diff --git a/src/interfaces/libpq/fe-auth.h b/src/interfaces/libpq/fe-auth.h
index 5dc6bb5341..ee3dd7f96c 100644
--- a/src/interfaces/libpq/fe-auth.h
+++ b/src/interfaces/libpq/fe-auth.h
@@ -23,7 +23,9 @@ extern int pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn);
extern char *pg_fe_getauthname(PQExpBuffer errorMessage);
/* 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, char *tls_finish_message,
+ int tls_finish_len);
extern void pg_fe_scram_free(void *opaq);
extern void pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen,
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 2f29820e82..84a6e3c322 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -393,6 +393,32 @@ pgtls_write(PGconn *conn, const void *ptr, size_t len)
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_finish(PGconn *conn, int *len)
+{
+ char dummy[1];
+ char *result;
+
+ /*
+ * OpenSSL does not offer an API to get directly the length of the
+ * TLS finish 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 */
/* ------------------------------------------------------------ */
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 42913604e3..0eb8b60c95 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -453,11 +453,13 @@ struct pg_conn
/* Assorted state for SASL, SSL, GSS, etc */
void *sasl_state;
+ /* SSL structures */
+ bool ssl_in_use;
+
#ifdef USE_SSL
bool allow_ssl_try; /* Allowed to try SSL negotiation */
bool wait_ssl_try; /* Delay SSL negotiation until after
* attempting normal connection */
- bool ssl_in_use;
#ifdef USE_OPENSSL
SSL *ssl; /* SSL status, if have SSL connection */
X509 *peer; /* X509 cert of server */
@@ -668,6 +670,7 @@ extern void pgtls_close(PGconn *conn);
extern ssize_t pgtls_read(PGconn *conn, void *ptr, size_t len);
extern bool pgtls_read_pending(PGconn *conn);
extern ssize_t pgtls_write(PGconn *conn, const void *ptr, size_t len);
+extern char *pgtls_get_finish(PGconn *conn, int *len);
/*
* this is so that we can check if a connection is non-blocking internally
--
2.14.1
0003-Add-connection-parameters-saslname-and-saslchannelbi.patchapplication/octet-stream; name=0003-Add-connection-parameters-saslname-and-saslchannelbi.patchDownload
From 3ab83fc7f28f6590ae8fa1d1115665c91501e76d Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 20 Jun 2017 11:41:10 +0900
Subject: [PATCH 3/4] Add connection parameters "saslname" and
"saslchannelbinding"
Those parameters can be used to respectively enforce the value of the
SASL mechanism name and the channel binding name sent to server during
a SASL message exchange.
A set of tests dedicated to SASL and channel binding is added as well
to the SSL test suite, which is handy to check the validity of a patch.
---
doc/src/sgml/libpq.sgml | 24 ++++++++++++++++
src/backend/libpq/auth-scram.c | 41 +++++++++++++++++++++-------
src/interfaces/libpq/fe-auth-scram.c | 53 ++++++++++++++++++++++++++++++++----
src/interfaces/libpq/fe-auth.c | 8 ++++++
src/interfaces/libpq/fe-auth.h | 4 +--
src/interfaces/libpq/fe-connect.c | 13 +++++++++
src/interfaces/libpq/libpq-int.h | 2 ++
src/test/ssl/ServerSetup.pm | 19 +++++++++++--
src/test/ssl/t/001_ssltests.pl | 2 +-
src/test/ssl/t/002_sasl.pl | 52 +++++++++++++++++++++++++++++++++++
10 files changed, 197 insertions(+), 21 deletions(-)
create mode 100644 src/test/ssl/t/002_sasl.pl
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 8e0b0b8586..5f78fb2f46 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1220,6 +1220,30 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
</listitem>
</varlistentry>
+ <varlistentry id="libpq-saslname" xreflabel="saslname">
+ <term><literal>saslname</literal></term>
+ <listitem>
+ <para>
+ Controls the name of the SASL mechanism name sent to server when doing
+ a message exchange for a SASL authentication. The list of SASL
+ mechanisms supported by server are listed in
+ <xref linkend="sasl-authentication">.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="libpq-saslchannelbinding" xreflabel="saslchannelbinding">
+ <term><literal>saslchannelbinding</literal></term>
+ <listitem>
+ <para>
+ Controls the name of the channel binding name sent to server when doing
+ a message exchange for a SASL authentication. The list of channel
+ binding names supported by server are listed in
+ <xref linkend="sasl-authentication">.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="libpq-connect-sslmode" xreflabel="sslmode">
<term><literal>sslmode</literal></term>
<listitem>
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index 72333c8784..f32cf26a37 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -113,6 +113,7 @@ typedef struct
bool ssl_in_use;
char *tls_finish_message;
int tls_finish_len;
+ char *channel_binding;
int iterations;
char *salt; /* base64-encoded */
@@ -185,6 +186,7 @@ pg_be_scram_init(const char *username,
state->ssl_in_use = ssl_in_use;
state->tls_finish_message = tls_finish_message;
state->tls_finish_len = tls_finish_len;
+ state->channel_binding = NULL;
/*
* Parse the stored password verifier.
@@ -853,6 +855,9 @@ read_client_first_message(scram_state *state, char *input)
ereport(ERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION),
(errmsg("unexpected SCRAM channel-binding type"))));
+
+ /* Save the name for handling of subsequent messages */
+ state->channel_binding = pstrdup(channel_name);
#else
/*
* Client requires channel binding. We don't support it.
@@ -1106,20 +1111,36 @@ read_client_final_message(scram_state *state, char *input)
#ifdef USE_SSL
if (state->ssl_in_use)
{
- char *enc_tls_message;
- int enc_tls_len;
+ char *b64_message, *raw_data;
+ int b64_message_len, raw_data_len;
+
+ /* Fetch data for each channel binding type */
+ if (strcmp(state->channel_binding, SCRAM_CHANNEL_TLS_UNIQUE) == 0)
+ {
+ raw_data = state->tls_finish_message;
+ raw_data_len = state->tls_finish_len;
+ }
+ else
+ {
+ /* should not happen */
+ elog(ERROR, "invalid channel binding type");
+ }
+
+ /* should not happen, but better safe than sorry */
+ if (raw_data == NULL)
+ elog(ERROR, "empty binding data for channel name \"%s\"",
+ state->channel_binding);
- enc_tls_message = palloc(pg_b64_enc_len(state->tls_finish_len) + 1);
- enc_tls_len = pg_b64_encode(state->tls_finish_message,
- state->tls_finish_len,
- enc_tls_message);
- enc_tls_message[enc_tls_len] = '\0';
+ b64_message = palloc(pg_b64_enc_len(raw_data_len) + 1);
+ b64_message_len = pg_b64_encode(raw_data, raw_data_len,
+ b64_message);
+ b64_message[b64_message_len] = '\0';
/*
- * Compare the value sent by the client with the TLS finish message
- * expected by the server.
+ * Compare the value sent by the client with the value expected by
+ * the server.
*/
- if (strcmp(channel_binding, enc_tls_message) != 0)
+ if (strcmp(channel_binding, b64_message) != 0)
ereport(ERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION),
(errmsg("no match for SCRAM channel-binding attribute in client-final-message"))));
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index 8b2a64f213..9be876eac6 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -48,6 +48,8 @@ typedef struct
bool ssl_in_use;
char *tls_finish_message;
int tls_finish_len;
+ /* enforceable user parameters */
+ char *saslchannelbinding; /* name of channel binding to use */
/* We construct these */
uint8 SaltedPassword[SCRAM_KEY_LEN];
@@ -88,6 +90,7 @@ void *
pg_fe_scram_init(const char *username,
const char *password,
bool ssl_in_use,
+ char *saslchannelbinding,
char *tls_finish_message,
int tls_finish_len)
{
@@ -105,6 +108,15 @@ pg_fe_scram_init(const char *username,
state->tls_finish_message = tls_finish_message;
state->tls_finish_len = tls_finish_len;
+ /*
+ * If user has specified a channel binding to use, enforce the
+ * channel binding sent to it. The default is "tls-unique".
+ */
+ if (saslchannelbinding && strlen(saslchannelbinding) > 0)
+ state->saslchannelbinding = strdup(saslchannelbinding);
+ else
+ state->saslchannelbinding = strdup(SCRAM_CHANNEL_TLS_UNIQUE);
+
/* Normalize the password with SASLprep, if possible */
rc = pg_saslprep(password, &prep_password);
if (rc == SASLPREP_OOM)
@@ -136,6 +148,10 @@ pg_fe_scram_free(void *opaq)
if (state->password)
free(state->password);
+ if (state->tls_finish_message)
+ free(state->tls_finish_message);
+ if (state->saslchannelbinding)
+ free(state->saslchannelbinding);
/* client messages */
if (state->client_nonce)
@@ -353,7 +369,7 @@ build_client_first_message(fe_scram_state *state, PQExpBuffer errormessage)
*/
#ifdef USE_SSL
if (state->ssl_in_use)
- appendPQExpBuffer(&buf, "p=%s", SCRAM_CHANNEL_TLS_UNIQUE);
+ appendPQExpBuffer(&buf, "p=%s", state->saslchannelbinding);
else
appendPQExpBuffer(&buf, "y");
#else
@@ -412,12 +428,39 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
#ifdef USE_SSL
if (state->ssl_in_use)
{
+ char *raw_data;
+ int raw_data_len;
+
+ if (strcmp(state->saslchannelbinding, SCRAM_CHANNEL_TLS_UNIQUE) == 0)
+ {
+ raw_data = state->tls_finish_message;
+ raw_data_len = state->tls_finish_len;
+ }
+ else
+ {
+ /* should not happen */
+ termPQExpBuffer(&buf);
+ printfPQExpBuffer(errormessage,
+ libpq_gettext("incorrect channel binding name\n"));
+ return NULL;
+ }
+
+ /* should not happen, but better safe than sorry */
+ if (raw_data == NULL)
+ {
+ /* should not happen */
+ termPQExpBuffer(&buf);
+ printfPQExpBuffer(errormessage,
+ libpq_gettext("empty binding data for channel name \"%s\"\n"),
+ state->saslchannelbinding);
+ return NULL;
+ }
+
appendPQExpBuffer(&buf, "c=");
- if (!enlargePQExpBuffer(&buf, pg_b64_enc_len(state->tls_finish_len)))
+
+ if (!enlargePQExpBuffer(&buf, pg_b64_enc_len(raw_data_len)))
goto oom_error;
- buf.len += pg_b64_encode(state->tls_finish_message,
- state->tls_finish_len,
- buf.data + buf.len);
+ buf.len += pg_b64_encode(raw_data, raw_data_len, buf.data + buf.len);
buf.data[buf.len] = '\0';
}
else
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 05f04be10a..4b26018f65 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -564,6 +564,7 @@ pg_SASL_init(PGconn *conn, int payloadlen)
conn->sasl_state = pg_fe_scram_init(conn->pguser,
password,
conn->ssl_in_use,
+ conn->saslchannelbinding,
tls_finish,
tls_finish_len);
if (!conn->sasl_state)
@@ -589,6 +590,13 @@ pg_SASL_init(PGconn *conn, int payloadlen)
}
}
+ /*
+ * If user has asked for a specific mechanism name, enforce the chosen
+ * name to it.
+ */
+ if (conn->saslname && strlen(conn->saslname) > 0)
+ selected_mechanism = conn->saslname;
+
if (!selected_mechanism)
{
printfPQExpBuffer(&conn->errorMessage,
diff --git a/src/interfaces/libpq/fe-auth.h b/src/interfaces/libpq/fe-auth.h
index ee3dd7f96c..3c699959e0 100644
--- a/src/interfaces/libpq/fe-auth.h
+++ b/src/interfaces/libpq/fe-auth.h
@@ -24,8 +24,8 @@ extern char *pg_fe_getauthname(PQExpBuffer errorMessage);
/* Prototypes for functions in fe-auth-scram.c */
extern void *pg_fe_scram_init(const char *username, const char *password,
- bool ssl_in_use, char *tls_finish_message,
- int tls_finish_len);
+ bool ssl_in_use, char *saslchannelbinding,
+ char *tls_finish_message, int tls_finish_len);
extern void pg_fe_scram_free(void *opaq);
extern void pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen,
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index d0e97ecdd4..9abbdcd4d7 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -262,6 +262,15 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
"TCP-Keepalives-Count", "", 10, /* strlen(INT32_MAX) == 10 */
offsetof(struct pg_conn, keepalives_count)},
+ /* Set of options proper to SASL */
+ {"saslname", NULL, NULL, NULL,
+ "SASL-Name", "", 21, /* maximum name size per IANA == 21 */
+ offsetof(struct pg_conn, saslname)},
+
+ {"saslchannelbinding", NULL, NULL, NULL,
+ "SASL-Channel", "", 22, /* sizeof("tls-unique-for-telnet") == 22 */
+ offsetof(struct pg_conn, saslchannelbinding)},
+
/*
* ssl options are allowed even without client SSL support because the
* client can still handle SSL modes "disable" and "allow". Other
@@ -3470,6 +3479,10 @@ freePGconn(PGconn *conn)
free(conn->keepalives_interval);
if (conn->keepalives_count)
free(conn->keepalives_count);
+ if (conn->saslname)
+ free(conn->saslname);
+ if (conn->saslchannelbinding)
+ free(conn->saslchannelbinding);
if (conn->sslmode)
free(conn->sslmode);
if (conn->sslcert)
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 0eb8b60c95..6d500aa5db 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -349,6 +349,8 @@ struct pg_conn
* retransmits */
char *keepalives_count; /* maximum number of TCP keepalive
* retransmits */
+ char *saslname; /* SASL mechanism name */
+ char *saslchannelbinding; /* channel binding used in SASL */
char *sslmode; /* SSL mode (require,prefer,allow,disable) */
char *sslcompression; /* SSL compression (0 or 1) */
char *sslkey; /* client key filename */
diff --git a/src/test/ssl/ServerSetup.pm b/src/test/ssl/ServerSetup.pm
index ad2e036602..b71969ac75 100644
--- a/src/test/ssl/ServerSetup.pm
+++ b/src/test/ssl/ServerSetup.pm
@@ -91,6 +91,9 @@ sub configure_test_server_for_ssl
{
my $node = $_[0];
my $serverhost = $_[1];
+ my $sslmethod = $_[2];
+ my $passwd = $_[3];
+ my $passwdhash = $_[4];
my $pgdata = $node->data_dir;
@@ -100,6 +103,15 @@ sub configure_test_server_for_ssl
$node->psql('postgres', "CREATE DATABASE trustdb");
$node->psql('postgres', "CREATE DATABASE certdb");
+ # Update password of each user as needed.
+ if (defined($passwd))
+ {
+ $node->psql('postgres',
+"SET password_encryption='$passwdhash'; ALTER USER ssltestuser PASSWORD '$passwd';");
+ $node->psql('postgres',
+"SET password_encryption='$passwdhash'; ALTER USER anotheruser PASSWORD '$passwd';");
+ }
+
# enable logging etc.
open my $conf, '>>', "$pgdata/postgresql.conf";
print $conf "fsync=off\n";
@@ -129,7 +141,7 @@ sub configure_test_server_for_ssl
$node->restart;
# Change pg_hba after restart because hostssl requires ssl=on
- configure_hba_for_ssl($node, $serverhost);
+ configure_hba_for_ssl($node, $serverhost, $sslmethod);
}
# Change the configuration to use given server cert file, and reload
@@ -159,6 +171,7 @@ sub configure_hba_for_ssl
{
my $node = $_[0];
my $serverhost = $_[1];
+ my $sslmethod = $_[2];
my $pgdata = $node->data_dir;
# Only accept SSL connections from localhost. Our tests don't depend on this
@@ -169,9 +182,9 @@ sub configure_hba_for_ssl
print $hba
"# TYPE DATABASE USER ADDRESS METHOD\n";
print $hba
-"hostssl trustdb ssltestuser $serverhost/32 trust\n";
+"hostssl trustdb ssltestuser $serverhost/32 $sslmethod\n";
print $hba
-"hostssl trustdb ssltestuser ::1/128 trust\n";
+"hostssl trustdb ssltestuser ::1/128 $sslmethod\n";
print $hba
"hostssl certdb ssltestuser $serverhost/32 cert\n";
print $hba
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 890e3051a2..e690c1fa15 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -32,7 +32,7 @@ $node->init;
$ENV{PGHOST} = $node->host;
$ENV{PGPORT} = $node->port;
$node->start;
-configure_test_server_for_ssl($node, $SERVERHOSTADDR);
+configure_test_server_for_ssl($node, $SERVERHOSTADDR, "trust");
switch_server_cert($node, 'server-cn-only');
### Part 1. Run client-side tests.
diff --git a/src/test/ssl/t/002_sasl.pl b/src/test/ssl/t/002_sasl.pl
new file mode 100644
index 0000000000..a625f0d473
--- /dev/null
+++ b/src/test/ssl/t/002_sasl.pl
@@ -0,0 +1,52 @@
+use strict;
+use warnings;
+use PostgresNode;
+use TestLib;
+use Test::More tests => 6;
+use ServerSetup;
+use File::Copy;
+
+# test combinations of SASL authentication for SCRAM mechanism:
+# - SCRAM-SHA-256 and SCRAM-SHA-256-PLUS
+# - Channel bindings
+
+# 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;
+
+#### Part 0. 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";
+
+# Tests with default channel binding and SASL mechanism names.
+# tls-unique is used here
+test_connect_ok($common_connstr, "saslname=SCRAM-SHA-256-PLUS");
+test_connect_fails($common_connstr, "saslname=not-exists");
+# Downgrade attack.
+test_connect_fails($common_connstr, "saslname=SCRAM-SHA-256");
+test_connect_fails($common_connstr,
+ "saslname=SCRAM-SHA-256 saslchannelbinding=tls-unique");
+
+# Channel bindings
+test_connect_ok($common_connstr,
+ "saslname=SCRAM-SHA-256-PLUS saslchannelbinding=tls-unique");
+test_connect_fails($common_connstr,
+ "saslname=SCRAM-SHA-256-PLUS saslchannelbinding=not-exists");
--
2.14.1
0004-Implement-channel-binding-tls-server-end-point-for-S.patchapplication/octet-stream; name=0004-Implement-channel-binding-tls-server-end-point-for-S.patchDownload
From 5da12f90e4c4577933f95205d71f62f906694693 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 20 Jun 2017 12:51:10 +0900
Subject: [PATCH 4/4] Implement channel binding tls-server-end-point for SCRAM
As referenced in RFC 5929, this channel binding is not the default value
and uses a hash of the certificate as binding data. On the frontend, this
can be resumed in getting the data from SSL_get_peer_certificate() and
on the backend SSL_get_certificate().
The hashing algorithm needs also to switch to SHA-256 if the signature
algorithm is MD5 or SHA-1, so let's be careful about that.
---
doc/src/sgml/protocol.sgml | 4 +-
src/backend/libpq/auth-scram.c | 21 ++++++++--
src/backend/libpq/auth.c | 13 ++++--
src/backend/libpq/be-secure-openssl.c | 69 ++++++++++++++++++++++++++++++++
src/include/libpq/libpq-be.h | 1 +
src/include/libpq/scram.h | 4 +-
src/interfaces/libpq/fe-auth-scram.c | 20 ++++++++-
src/interfaces/libpq/fe-auth.c | 18 ++++++++-
src/interfaces/libpq/fe-auth.h | 3 +-
src/interfaces/libpq/fe-secure-openssl.c | 66 ++++++++++++++++++++++++++++++
src/interfaces/libpq/libpq-int.h | 1 +
src/test/ssl/t/002_sasl.pl | 6 ++-
12 files changed, 210 insertions(+), 16 deletions(-)
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 1863c0905c..1eebcbffa7 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1433,8 +1433,8 @@ the password is in.
<para>
<firstterm>Channel binding</> is supported in builds with SSL support, and
uses as mechanism name <literal>SCRAM-SHA-256-PLUS</> for this purpose as
-defined per IANA. The only channel binding type supported by the server
-is <literal>tls-unique</>.
+defined per IANA. The channel binding types supported by the server
+are <literal>tls-unique</>, the default, and <literal>tls-server-end-point</>.
</para>
<procedure>
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index f32cf26a37..8bd5cdace5 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -113,6 +113,8 @@ typedef struct
bool ssl_in_use;
char *tls_finish_message;
int tls_finish_len;
+ char *certificate_hash;
+ int certificate_hash_len;
char *channel_binding;
int iterations;
@@ -175,7 +177,9 @@ pg_be_scram_init(const char *username,
const char *shadow_pass,
bool ssl_in_use,
char *tls_finish_message,
- int tls_finish_len)
+ int tls_finish_len,
+ char *certificate_hash,
+ int certificate_hash_len)
{
scram_state *state;
bool got_verifier;
@@ -186,6 +190,8 @@ pg_be_scram_init(const char *username,
state->ssl_in_use = ssl_in_use;
state->tls_finish_message = tls_finish_message;
state->tls_finish_len = tls_finish_len;
+ state->certificate_hash = certificate_hash;
+ state->certificate_hash_len = certificate_hash_len;
state->channel_binding = NULL;
/*
@@ -847,11 +853,12 @@ read_client_first_message(scram_state *state, char *input)
errmsg("client supports SCRAM channel binding, but server does not need it for non-SSL connections")));
/*
- * Read value provided by client, only tls-unique is supported
- * for now.
+ * Read value provided by client, only tls-unique and
+ * tls-server-end-point are supported for now.
*/
channel_name = read_attr_value(&input, 'p');
- if (strcmp(channel_name, SCRAM_CHANNEL_TLS_UNIQUE) != 0)
+ if (strcmp(channel_name, SCRAM_CHANNEL_TLS_UNIQUE) != 0 &&
+ strcmp(channel_name, SCRAM_CHANNEL_TLS_ENDPOINT) != 0)
ereport(ERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION),
(errmsg("unexpected SCRAM channel-binding type"))));
@@ -1120,6 +1127,12 @@ read_client_final_message(scram_state *state, char *input)
raw_data = state->tls_finish_message;
raw_data_len = state->tls_finish_len;
}
+ else if (strcmp(state->channel_binding,
+ SCRAM_CHANNEL_TLS_ENDPOINT) == 0)
+ {
+ raw_data = state->certificate_hash;
+ raw_data_len = state->certificate_hash_len;
+ }
else
{
/* should not happen */
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index ee9bce03a2..bbab65b64b 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -867,6 +867,8 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
bool initial;
char *tls_finish = NULL;
int tls_finish_len = 0;
+ char *certificate_bash = NULL;
+ int certificate_bash_len = 0;
/*
* SASL auth is not supported for protocol versions before 3, because it
@@ -918,12 +920,15 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
#ifdef USE_SSL
/*
- * Fetch the data related to the SSL finish message to be used in the
- * exchange.
+ * Fetch the data related to the SSL finish message and the client
+ * certificate (if any) to be used in the exchange.
*/
if (port->ssl_in_use)
{
tls_finish = be_tls_get_peer_finish(port, &tls_finish_len);
+ certificate_bash =
+ be_tls_get_certificate_hash(port,
+ &certificate_bash_len);
}
#endif
@@ -942,7 +947,9 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
shadow_pass,
port->ssl_in_use,
tls_finish,
- tls_finish_len);
+ tls_finish_len,
+ certificate_bash,
+ certificate_bash_len);
/*
* Loop through SASL message exchange. This exchange can consist of
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index d063a587d0..cea60c6741 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -1239,6 +1239,75 @@ be_tls_get_peer_finish(Port *port, int *len)
return result;
}
+/*
+ * Get the server certificate hash for authentication purposes. Per
+ * RFC 5929 and tls-server-end-point, the TLS server's certificate bytes
+ * need to be hashed with SHA-256 if its signature algorithm is MD5 or
+ * SHA-1. If SHA-256 or something else is used, the same hash as the
+ * signature algorithm is used.
+ * The result is a palloc'd hash of the server certificate with its
+ * size, and NULL if there is nothing certificates available.
+ */
+char *
+be_tls_get_certificate_hash(Port *port, int *len)
+{
+ char *cert_hash = NULL;
+ X509 *server_cert;
+
+ *len = 0;
+ server_cert = SSL_get_certificate(port->ssl);
+
+ if (server_cert != NULL)
+ {
+ const EVP_MD *algo_type = NULL;
+ char hash[EVP_MAX_MD_SIZE]; /* size for SHA-512 */
+ unsigned int hash_size;
+ int algo_nid;
+
+ /*
+ * Get the signature algorithm of the certificate to determine the
+ * hash algorithm to use for the result.
+ */
+ if (!OBJ_find_sigid_algs(X509_get_signature_nid(server_cert),
+ &algo_nid, NULL))
+ elog(ERROR, "could not find signature algorithm");
+
+ switch (algo_nid)
+ {
+ case NID_sha512:
+ algo_type = EVP_sha512();
+ break;
+
+ case NID_sha384:
+ algo_type = EVP_sha384();
+ break;
+
+ /*
+ * Fallback to SHA-256 for weaker hashes, and keep them listed
+ * here for reference.
+ */
+ case NID_md5:
+ case NID_sha1:
+ case NID_sha224:
+ case NID_sha256:
+ default:
+ algo_type = EVP_sha256();
+ break;
+ }
+
+ /* generate and save the certificate hash */
+ if (!X509_digest(server_cert, algo_type, (unsigned char *) hash,
+ &hash_size))
+ elog(ERROR, "could not generate server certificate hash");
+
+ cert_hash = (char *) palloc(hash_size);
+ memcpy(cert_hash, hash, hash_size);
+ *len = hash_size;
+ }
+
+ return cert_hash;
+}
+
/*
* Convert an X509 subject name to a cstring.
*
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 5d59d79822..0392860a87 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -210,6 +210,7 @@ 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_peerdn_name(Port *port, char *ptr, size_t len);
extern char *be_tls_get_peer_finish(Port *port, int *len);
+extern char *be_tls_get_certificate_hash(Port *port, int *len);
#endif
extern ProtocolVersion FrontendProtocol;
diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h
index 91cebe9a9b..55d0568d43 100644
--- a/src/include/libpq/scram.h
+++ b/src/include/libpq/scram.h
@@ -19,6 +19,7 @@
/* Channel binding names */
#define SCRAM_CHANNEL_TLS_UNIQUE "tls-unique"
+#define SCRAM_CHANNEL_TLS_ENDPOINT "tls-server-end-point"
/* Status codes for message exchange */
#define SASL_EXCHANGE_CONTINUE 0
@@ -28,7 +29,8 @@
/* Routines dedicated to authentication */
extern void *pg_be_scram_init(const char *username, const char *shadow_pass,
bool ssl_in_use, char *tls_finish_message,
- int tls_finish_len);
+ int tls_finish_len, char *certificate_hash,
+ int certificate_hash_len);
extern int pg_be_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen, char **logdetail);
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index 9be876eac6..d9a403de8a 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -48,6 +48,8 @@ typedef struct
bool ssl_in_use;
char *tls_finish_message;
int tls_finish_len;
+ char *certificate_hash;
+ int certificate_hash_len;
/* enforceable user parameters */
char *saslchannelbinding; /* name of channel binding to use */
@@ -92,7 +94,9 @@ pg_fe_scram_init(const char *username,
bool ssl_in_use,
char *saslchannelbinding,
char *tls_finish_message,
- int tls_finish_len)
+ int tls_finish_len,
+ char *certificate_hash,
+ int certificate_hash_len)
{
fe_scram_state *state;
char *prep_password;
@@ -107,6 +111,8 @@ pg_fe_scram_init(const char *username,
state->ssl_in_use = ssl_in_use;
state->tls_finish_message = tls_finish_message;
state->tls_finish_len = tls_finish_len;
+ state->certificate_hash = certificate_hash;
+ state->certificate_hash_len = certificate_hash_len;
/*
* If user has specified a channel binding to use, enforce the
@@ -150,6 +156,8 @@ pg_fe_scram_free(void *opaq)
free(state->password);
if (state->tls_finish_message)
free(state->tls_finish_message);
+ if (state->certificate_hash)
+ free(state->certificate_hash);
if (state->saslchannelbinding)
free(state->saslchannelbinding);
@@ -423,7 +431,9 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
* Construct client-final-message-without-proof. We need to remember it
* for verifying the server proof in the final step of authentication.
* Client needs to provide a b64 encoded string of the TLS finish message
- * only if a SSL connection is attempted.
+ * only if a SSL connection is attempted using "tls-unique" as channel
+ * binding. For "tls-server-end-point", a hash of the client certificate
+ * is sent instead.
*/
#ifdef USE_SSL
if (state->ssl_in_use)
@@ -436,6 +446,12 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
raw_data = state->tls_finish_message;
raw_data_len = state->tls_finish_len;
}
+ else if (strcmp(state->saslchannelbinding,
+ SCRAM_CHANNEL_TLS_ENDPOINT) == 0)
+ {
+ raw_data = state->certificate_hash;
+ raw_data_len = state->certificate_hash_len;
+ }
else
{
/* should not happen */
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 4b26018f65..d72d35670f 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -539,6 +539,8 @@ pg_SASL_init(PGconn *conn, int payloadlen)
char *password;
char *tls_finish = NULL;
int tls_finish_len = 0;
+ char *certificate_hash = NULL;
+ int certificate_hash_len = 0;
conn->password_needed = true;
password = conn->connhost[conn->whichhost].password;
@@ -552,12 +554,21 @@ pg_SASL_init(PGconn *conn, int payloadlen)
}
#ifdef USE_SSL
- /* Fetch information about the TLS finish message */
+ /*
+ * Fetch information about the TLS finish message and client
+ * certificate if any.
+ */
if (conn->ssl_in_use)
{
tls_finish = pgtls_get_finish(conn, &tls_finish_len);
if (tls_finish == NULL)
goto oom_error;
+
+ certificate_hash =
+ pgtls_get_peer_certificate_hash(conn,
+ &certificate_hash_len);
+ if (certificate_hash == NULL)
+ goto oom_error;
}
#endif
@@ -566,7 +577,10 @@ pg_SASL_init(PGconn *conn, int payloadlen)
conn->ssl_in_use,
conn->saslchannelbinding,
tls_finish,
- tls_finish_len);
+ tls_finish_len,
+ certificate_hash,
+ certificate_hash_len);
+
if (!conn->sasl_state)
goto oom_error;
diff --git a/src/interfaces/libpq/fe-auth.h b/src/interfaces/libpq/fe-auth.h
index 3c699959e0..8db86f8bcc 100644
--- a/src/interfaces/libpq/fe-auth.h
+++ b/src/interfaces/libpq/fe-auth.h
@@ -25,7 +25,8 @@ extern char *pg_fe_getauthname(PQExpBuffer errorMessage);
/* Prototypes for functions in fe-auth-scram.c */
extern void *pg_fe_scram_init(const char *username, const char *password,
bool ssl_in_use, char *saslchannelbinding,
- char *tls_finish_message, int tls_finish_len);
+ char *tls_finish_message, int tls_finish_len,
+ char *certificate_hash, int certificate_hash_len);
extern void pg_fe_scram_free(void *opaq);
extern void pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen,
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 84a6e3c322..00a14289e4 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -418,6 +418,72 @@ pgtls_get_finish(PGconn *conn, int *len)
return result;
}
+/*
+ * Get the hash of the server certificate
+ *
+ * This information is useful for end-point channel binding, where
+ * the client certificate hash is used as a link, per RFC 5929.
+ */
+char *
+pgtls_get_peer_certificate_hash(PGconn *conn, int *len)
+{
+ char *cert_hash = NULL;
+
+ *len = 0;
+
+ if (conn->peer)
+ {
+ X509 *peer_cert = conn->peer;
+ const EVP_MD *algo_type = NULL;
+ char hash[EVP_MAX_MD_SIZE]; /* size for SHA-512 */
+ unsigned int hash_size;
+ int algo_nid;
+
+ /*
+ * Get the signature algorithm of the certificate to determine the
+ * hash algorithm to use for the result.
+ */
+ if (!OBJ_find_sigid_algs(X509_get_signature_nid(peer_cert),
+ &algo_nid, NULL))
+ return NULL;
+
+ switch (algo_nid)
+ {
+ case NID_sha512:
+ algo_type = EVP_sha512();
+ break;
+
+ case NID_sha384:
+ algo_type = EVP_sha384();
+ break;
+
+ /*
+ * Fallback to SHA-256 for weaker hashes, and keep them listed
+ * here for reference.
+ */
+ case NID_md5:
+ case NID_sha1:
+ case NID_sha224:
+ case NID_sha256:
+ default:
+ algo_type = EVP_sha256();
+ break;
+ }
+
+ if (!X509_digest(peer_cert, algo_type, (unsigned char *) hash,
+ &hash_size))
+ return NULL;
+
+ /* save result */
+ cert_hash = (char *) malloc(hash_size);
+ if (cert_hash == NULL)
+ return NULL;
+ memcpy(cert_hash, hash, hash_size);
+ *len = hash_size;
+ }
+
+ return cert_hash;
+}
/* ------------------------------------------------------------ */
/* OpenSSL specific code */
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 6d500aa5db..f1e2c0bb3c 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -673,6 +673,7 @@ extern ssize_t pgtls_read(PGconn *conn, void *ptr, size_t len);
extern bool pgtls_read_pending(PGconn *conn);
extern ssize_t pgtls_write(PGconn *conn, const void *ptr, size_t len);
extern char *pgtls_get_finish(PGconn *conn, int *len);
+extern char *pgtls_get_peer_certificate_hash(PGconn *conn, int *len);
/*
* this is so that we can check if a connection is non-blocking internally
diff --git a/src/test/ssl/t/002_sasl.pl b/src/test/ssl/t/002_sasl.pl
index a625f0d473..a4e6dfac3d 100644
--- a/src/test/ssl/t/002_sasl.pl
+++ b/src/test/ssl/t/002_sasl.pl
@@ -2,7 +2,7 @@ use strict;
use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 6;
+use Test::More tests => 8;
use ServerSetup;
use File::Copy;
@@ -44,9 +44,13 @@ test_connect_fails($common_connstr, "saslname=not-exists");
test_connect_fails($common_connstr, "saslname=SCRAM-SHA-256");
test_connect_fails($common_connstr,
"saslname=SCRAM-SHA-256 saslchannelbinding=tls-unique");
+test_connect_fails($common_connstr,
+ "saslname=SCRAM-SHA-256 saslchannelbinding=tls-server-end-point");
# Channel bindings
test_connect_ok($common_connstr,
"saslname=SCRAM-SHA-256-PLUS saslchannelbinding=tls-unique");
+test_connect_ok($common_connstr,
+ "saslname=SCRAM-SHA-256-PLUS saslchannelbinding=tls-server-end-point");
test_connect_fails($common_connstr,
"saslname=SCRAM-SHA-256-PLUS saslchannelbinding=not-exists");
--
2.14.1
On Mon, Aug 21, 2017 at 9:51 PM, Michael Paquier
<michael.paquier@gmail.com> wrote:
On Tue, Jun 20, 2017 at 1:11 PM, Michael Paquier
<michael.paquier@gmail.com> wrote:With the tests directly in the patch, things are easy to run. WIth
PG10 stabilization work, of course I don't expect much feedback :)
But this set of patches looks like the direction we want to go so as
JDBC and libpq users can take advantage of channel binding with SCRAM.Attached is a new patch set, rebased as of c6293249.
And again a new set to fix the rotten bits caused by 85f4d63.
--
Michael
Attachments:
0001-Refactor-routine-to-test-connection-to-SSL-server.patchapplication/octet-stream; name=0001-Refactor-routine-to-test-connection-to-SSL-server.patchDownload
From d64e877ff762dd5cf2c47dbce00576dfa070e1f5 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 16 Jun 2017 17:00:06 +0900
Subject: [PATCH 1/4] Refactor routine to test connection to SSL server
Move the sub-routines wrappers to check if a connection to a server is
fine or not into the test main module. This is useful for other tests
willing to check connectivity into a server.
---
src/test/ssl/ServerSetup.pm | 45 +++++++++++++-
src/test/ssl/t/001_ssltests.pl | 132 +++++++++++++++++------------------------
2 files changed, 100 insertions(+), 77 deletions(-)
diff --git a/src/test/ssl/ServerSetup.pm b/src/test/ssl/ServerSetup.pm
index f63c81cfc6..ad2e036602 100644
--- a/src/test/ssl/ServerSetup.pm
+++ b/src/test/ssl/ServerSetup.pm
@@ -26,9 +26,52 @@ use Test::More;
use Exporter 'import';
our @EXPORT = qw(
- configure_test_server_for_ssl switch_server_cert
+ configure_test_server_for_ssl
+ run_test_psql
+ switch_server_cert
+ test_connect_fails
+ test_connect_ok
);
+# Define a couple of helper functions to test connecting to the server.
+
+# Attempt connection to server with given connection string.
+sub run_test_psql
+{
+ my $connstr = $_[0];
+ my $logstring = $_[1];
+
+ my $cmd = [
+ 'psql', '-X', '-A', '-t', '-c', "SELECT 'connected with $connstr'",
+ '-d', "$connstr" ];
+
+ my $result = run_log($cmd);
+ return $result;
+}
+
+#
+# The first argument is a base connection string to use for connection.
+# The second argument is a complementary connection string, and it's also
+# printed out as the test case name.
+sub test_connect_ok
+{
+ my $common_connstr = $_[0];
+ my $connstr = $_[1];
+
+ my $result =
+ run_test_psql("$common_connstr $connstr", "(should succeed)");
+ ok($result, $connstr);
+}
+
+sub test_connect_fails
+{
+ my $common_connstr = $_[0];
+ my $connstr = $_[1];
+
+ my $result = run_test_psql("$common_connstr $connstr", "(should fail)");
+ ok(!$result, "$connstr (should fail)");
+}
+
# Copy a set of files, taking into account wildcards
sub copy_files
{
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 32df273929..890e3051a2 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -13,44 +13,9 @@ use File::Copy;
# postgresql-ssl-regression.test.
my $SERVERHOSTADDR = '127.0.0.1';
-# Define a couple of helper functions to test connecting to the server.
-
+# Allocation of base connection string shared among multiple tests.
my $common_connstr;
-sub run_test_psql
-{
- my $connstr = $_[0];
- my $logstring = $_[1];
-
- my $cmd = [
- 'psql', '-X', '-A', '-t', '-c', "SELECT 'connected with $connstr'",
- '-d', "$connstr" ];
-
- my $result = run_log($cmd);
- return $result;
-}
-
-#
-# The first argument is a (part of a) connection string, and it's also printed
-# out as the test case name. It is appended to $common_connstr global variable,
-# which also contains a libpq connection string.
-sub test_connect_ok
-{
- my $connstr = $_[0];
-
- my $result =
- run_test_psql("$common_connstr $connstr", "(should succeed)");
- ok($result, $connstr);
-}
-
-sub test_connect_fails
-{
- my $connstr = $_[0];
-
- my $result = run_test_psql("$common_connstr $connstr", "(should fail)");
- ok(!$result, "$connstr (should fail)");
-}
-
# The client's private key must not be world-readable, so take a copy
# of the key stored in the code tree and update its permissions.
copy("ssl/client.key", "ssl/client_tmp.key");
@@ -83,50 +48,59 @@ $common_connstr =
# The server should not accept non-SSL connections
note "test that the server doesn't accept non-SSL connections";
-test_connect_fails("sslmode=disable");
+test_connect_fails($common_connstr, "sslmode=disable");
# Try without a root cert. In sslmode=require, this should work. In verify-ca
# or verify-full mode it should fail
note "connect without server root cert";
-test_connect_ok("sslrootcert=invalid sslmode=require");
-test_connect_fails("sslrootcert=invalid sslmode=verify-ca");
-test_connect_fails("sslrootcert=invalid sslmode=verify-full");
+test_connect_ok($common_connstr, "sslrootcert=invalid sslmode=require");
+test_connect_fails($common_connstr, "sslrootcert=invalid sslmode=verify-ca");
+test_connect_fails($common_connstr, "sslrootcert=invalid sslmode=verify-full");
# Try with wrong root cert, should fail. (we're using the client CA as the
# root, but the server's key is signed by the server CA)
note "connect without wrong server root cert";
-test_connect_fails("sslrootcert=ssl/client_ca.crt sslmode=require");
-test_connect_fails("sslrootcert=ssl/client_ca.crt sslmode=verify-ca");
-test_connect_fails("sslrootcert=ssl/client_ca.crt sslmode=verify-full");
+test_connect_fails($common_connstr,
+ "sslrootcert=ssl/client_ca.crt sslmode=require");
+test_connect_fails($common_connstr,
+ "sslrootcert=ssl/client_ca.crt sslmode=verify-ca");
+test_connect_fails($common_connstr,
+ "sslrootcert=ssl/client_ca.crt sslmode=verify-full");
# Try with just the server CA's cert. This fails because the root file
# must contain the whole chain up to the root CA.
note "connect with server CA cert, without root CA";
-test_connect_fails("sslrootcert=ssl/server_ca.crt sslmode=verify-ca");
+test_connect_fails($common_connstr,
+ "sslrootcert=ssl/server_ca.crt sslmode=verify-ca");
# And finally, with the correct root cert.
note "connect with correct server CA cert file";
-test_connect_ok("sslrootcert=ssl/root+server_ca.crt sslmode=require");
-test_connect_ok("sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca");
-test_connect_ok("sslrootcert=ssl/root+server_ca.crt sslmode=verify-full");
+test_connect_ok($common_connstr,
+ "sslrootcert=ssl/root+server_ca.crt sslmode=require");
+test_connect_ok($common_connstr,
+ "sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca");
+test_connect_ok($common_connstr,
+ "sslrootcert=ssl/root+server_ca.crt sslmode=verify-full");
# Test with cert root file that contains two certificates. The client should
# be able to pick the right one, regardless of the order in the file.
-test_connect_ok("sslrootcert=ssl/both-cas-1.crt sslmode=verify-ca");
-test_connect_ok("sslrootcert=ssl/both-cas-2.crt sslmode=verify-ca");
+test_connect_ok($common_connstr,
+ "sslrootcert=ssl/both-cas-1.crt sslmode=verify-ca");
+test_connect_ok($common_connstr,
+ "sslrootcert=ssl/both-cas-2.crt sslmode=verify-ca");
note "testing sslcrl option with a non-revoked cert";
# Invalid CRL filename is the same as no CRL, succeeds
-test_connect_ok(
+test_connect_ok($common_connstr,
"sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrl=invalid");
# A CRL belonging to a different CA is not accepted, fails
-test_connect_fails(
+test_connect_fails($common_connstr,
"sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrl=ssl/client.crl");
# With the correct CRL, succeeds (this cert is not revoked)
-test_connect_ok(
+test_connect_ok($common_connstr,
"sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrl=ssl/root+server.crl"
);
@@ -136,9 +110,9 @@ note "test mismatch between hostname and server certificate";
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
-test_connect_ok("sslmode=require host=wronghost.test");
-test_connect_ok("sslmode=verify-ca host=wronghost.test");
-test_connect_fails("sslmode=verify-full host=wronghost.test");
+test_connect_ok($common_connstr, "sslmode=require host=wronghost.test");
+test_connect_ok($common_connstr, "sslmode=verify-ca host=wronghost.test");
+test_connect_fails($common_connstr, "sslmode=verify-full host=wronghost.test");
# Test Subject Alternative Names.
switch_server_cert($node, 'server-multiple-alt-names');
@@ -147,12 +121,13 @@ note "test hostname matching with X.509 Subject Alternative Names";
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
-test_connect_ok("host=dns1.alt-name.pg-ssltest.test");
-test_connect_ok("host=dns2.alt-name.pg-ssltest.test");
-test_connect_ok("host=foo.wildcard.pg-ssltest.test");
+test_connect_ok($common_connstr, "host=dns1.alt-name.pg-ssltest.test");
+test_connect_ok($common_connstr, "host=dns2.alt-name.pg-ssltest.test");
+test_connect_ok($common_connstr, "host=foo.wildcard.pg-ssltest.test");
-test_connect_fails("host=wronghost.alt-name.pg-ssltest.test");
-test_connect_fails("host=deep.subdomain.wildcard.pg-ssltest.test");
+test_connect_fails($common_connstr, "host=wronghost.alt-name.pg-ssltest.test");
+test_connect_fails($common_connstr,
+ "host=deep.subdomain.wildcard.pg-ssltest.test");
# Test certificate with a single Subject Alternative Name. (this gives a
# slightly different error message, that's all)
@@ -162,10 +137,11 @@ note "test hostname matching with a single X.509 Subject Alternative Name";
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
-test_connect_ok("host=single.alt-name.pg-ssltest.test");
+test_connect_ok($common_connstr, "host=single.alt-name.pg-ssltest.test");
-test_connect_fails("host=wronghost.alt-name.pg-ssltest.test");
-test_connect_fails("host=deep.subdomain.wildcard.pg-ssltest.test");
+test_connect_fails($common_connstr, "host=wronghost.alt-name.pg-ssltest.test");
+test_connect_fails($common_connstr,
+ "host=deep.subdomain.wildcard.pg-ssltest.test");
# Test server certificate with a CN and SANs. Per RFCs 2818 and 6125, the CN
# should be ignored when the certificate has both.
@@ -175,9 +151,9 @@ note "test certificate with both a CN and SANs";
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
-test_connect_ok("host=dns1.alt-name.pg-ssltest.test");
-test_connect_ok("host=dns2.alt-name.pg-ssltest.test");
-test_connect_fails("host=common-name.pg-ssltest.test");
+test_connect_ok($common_connstr, "host=dns1.alt-name.pg-ssltest.test");
+test_connect_ok($common_connstr, "host=dns2.alt-name.pg-ssltest.test");
+test_connect_fails($common_connstr, "host=common-name.pg-ssltest.test");
# Finally, test a server certificate that has no CN or SANs. Of course, that's
# not a very sensible certificate, but libpq should handle it gracefully.
@@ -185,8 +161,10 @@ switch_server_cert($node, 'server-no-names');
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR";
-test_connect_ok("sslmode=verify-ca host=common-name.pg-ssltest.test");
-test_connect_fails("sslmode=verify-full host=common-name.pg-ssltest.test");
+test_connect_ok($common_connstr,
+ "sslmode=verify-ca host=common-name.pg-ssltest.test");
+test_connect_fails($common_connstr,
+ "sslmode=verify-full host=common-name.pg-ssltest.test");
# Test that the CRL works
note "testing client-side CRL";
@@ -196,8 +174,9 @@ $common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid hostaddr=$SERVERHOSTADDR host=common-name.pg-ssltest.test";
# Without the CRL, succeeds. With it, fails.
-test_connect_ok("sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca");
-test_connect_fails(
+test_connect_ok($common_connstr,
+ "sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca");
+test_connect_fails($common_connstr,
"sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrl=ssl/root+server.crl"
);
@@ -210,18 +189,18 @@ $common_connstr =
"sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=certdb hostaddr=$SERVERHOSTADDR";
# no client cert
-test_connect_fails("user=ssltestuser sslcert=invalid");
+test_connect_fails($common_connstr, "user=ssltestuser sslcert=invalid");
# correct client cert
-test_connect_ok(
+test_connect_ok($common_connstr,
"user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key");
# client cert belonging to another user
-test_connect_fails(
+test_connect_fails($common_connstr,
"user=anotheruser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key");
# revoked client cert
-test_connect_fails(
+test_connect_fails($common_connstr,
"user=ssltestuser sslcert=ssl/client-revoked.crt sslkey=ssl/client-revoked.key"
);
@@ -230,8 +209,9 @@ switch_server_cert($node, 'server-cn-only', 'root_ca');
$common_connstr =
"user=ssltestuser dbname=certdb sslkey=ssl/client_tmp.key sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR";
-test_connect_ok("sslmode=require sslcert=ssl/client+client_ca.crt");
-test_connect_fails("sslmode=require sslcert=ssl/client.crt");
+test_connect_ok($common_connstr,
+ "sslmode=require sslcert=ssl/client+client_ca.crt");
+test_connect_fails($common_connstr, "sslmode=require sslcert=ssl/client.crt");
# clean up
unlink "ssl/client_tmp.key";
--
2.14.1
0002-Support-channel-binding-tls-unique-in-SCRAM.patchapplication/octet-stream; name=0002-Support-channel-binding-tls-unique-in-SCRAM.patchDownload
From 43bbbdf9613c46bf1059b92c844e9bc41837f525 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 20 Jun 2017 10:18:58 +0900
Subject: [PATCH 2/4] 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 finish messages, a PG-like API is added to be able to
make the interface pluggable for other SSL implementations.
---
doc/src/sgml/protocol.sgml | 24 +++--
src/backend/libpq/auth-scram.c | 151 +++++++++++++++++++++++++------
src/backend/libpq/auth.c | 86 +++++++++++++++---
src/backend/libpq/be-secure-openssl.c | 24 +++++
src/include/libpq/libpq-be.h | 1 +
src/include/libpq/scram.h | 10 +-
src/interfaces/libpq/fe-auth-scram.c | 105 ++++++++++++++++-----
src/interfaces/libpq/fe-auth.c | 43 ++++++++-
src/interfaces/libpq/fe-auth.h | 4 +-
src/interfaces/libpq/fe-secure-openssl.c | 26 ++++++
src/interfaces/libpq/libpq-int.h | 5 +-
11 files changed, 404 insertions(+), 75 deletions(-)
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 76d1c13cc4..37ae7276e5 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1461,10 +1461,11 @@ SELCT 1/0;
<para>
<firstterm>SASL</> is a framework for authentication in connection-oriented
-protocols. At the moment, <productname>PostgreSQL</> implements only one SASL
-authentication mechanism, SCRAM-SHA-256, but more might be added in the
-future. The below steps illustrate how SASL authentication is performed in
-general, while the next subsection gives more details on SCRAM-SHA-256.
+protocols. At the moment, <productname>PostgreSQL</> implements only two SASL
+authentication mechanisms, SCRAM-SHA-256 and SCRAM-SHA-256-PLUS, but more
+might be added in the future. The below steps illustrate how SASL
+authentication is performed in general, while the next subsection gives
+more details on SCRAM-SHA-256 and SCRAM-SHA-256-PLUS.
</para>
<procedure>
@@ -1549,7 +1550,10 @@ the password is in.
</para>
<para>
-<firstterm>Channel binding</> has not been implemented yet.
+<firstterm>Channel binding</> is supported in builds with SSL support, and
+uses as mechanism name <literal>SCRAM-SHA-256-PLUS</> for this purpose as
+defined per IANA. The only channel binding type supported by the server
+is <literal>tls-unique</>.
</para>
<procedure>
@@ -1558,14 +1562,18 @@ the password is in.
<para>
The server sends an AuthenticationSASL message. It includes a list of
SASL authentication mechanisms that the server can accept.
+ <literal>SCRAM-SHA-256</> and <literal>SCRAM-SHA-256-PLUS</> are the
+ two mechanism names that the server lists in this message. Support for
+ channel binding is not included if the server is built without SSL
+ support and if the connection attempt is done without SSL.
</para>
</step>
<step id="scram-client-first">
<para>
The client responds by sending a SASLInitialResponse message, which
- indicates the chosen mechanism, <literal>SCRAM-SHA-256</>. In the Initial
- Client response field, the message contains the SCRAM
- <structname>client-first-message</>.
+ indicates the chosen mechanism, <literal>SCRAM-SHA-256</> or
+ <literal>SCRAM-SHA-256-PLUS</>. In the Initial Client response field,
+ the message contains the SCRAM <structname>client-first-message</>.
</para>
</step>
<step id="scram-server-first">
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index 9161c885e1..c90fa2981a 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,10 @@ typedef struct
const char *username; /* username from startup packet */
+ bool ssl_in_use;
+ char *tls_finish_message;
+ int tls_finish_len;
+
int iterations;
char *salt; /* base64-encoded */
uint8 StoredKey[SCRAM_KEY_LEN];
@@ -168,7 +170,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,
+ char *tls_finish_message,
+ int tls_finish_len)
{
scram_state *state;
bool got_verifier;
@@ -176,6 +182,9 @@ 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_finish_message = tls_finish_message;
+ state->tls_finish_len = tls_finish_len;
/*
* Parse the stored password verifier.
@@ -773,31 +782,93 @@ read_client_first_message(scram_state *state, char *input)
*------
*/
- /* read gs2-cbind-flag */
+ /*
+ * Read gs2-cbind-flag. Server handles its value as described in RFC 5802,
+ * section 6 dealing with channel binding.
+ */
switch (*input)
{
case 'n':
- /* Client does not support channel binding */
+ /*
+ * Client does not support channel binding, this is authorized
+ * only in builds not supporting SSL. If SSL is supported, the
+ * server cannot support this option either.
+ */
+#ifdef USE_SSL
+ if (state->ssl_in_use)
+ ereport(ERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("client does not support SCRAM channel binding, but server needs it for SSL connections")));
+ else
+ ereport(ERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("client does not support SCRAM channel binding, but server does")));
+#endif
+ 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 */
+ /*
+ * Client supports channel binding, but we're not doing it today
+ * in the context of a non-SSL connection. Complain though if
+ * the client is trying to trick the server in not doing channel
+ * binding with a downgrade attack if a SSL connection is
+ * attempted.
+ */
+ if (state->ssl_in_use)
+ ereport(ERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("client support SCRAM channel binding, but server needs it for SSL connections")));
+ 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':
+ {
+#ifdef USE_SSL
+ char *channel_name;
- /*
- * 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")));
+ if (!state->ssl_in_use)
+ ereport(ERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("client supports SCRAM channel binding, but server does not need it for non-SSL connections")));
+
+ /*
+ * Read value provided by client, only tls-unique is supported
+ * for now.
+ */
+ channel_name = read_attr_value(&input, 'p');
+ if (strcmp(channel_name, SCRAM_CHANNEL_TLS_UNIQUE) != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ (errmsg("unexpected SCRAM channel-binding type"))));
+#else
+ /*
+ * 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")));
+#endif
+ }
+ break;
default:
ereport(ERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION),
@@ -805,13 +876,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 +1096,47 @@ 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. We don't support channel binding for builds
+ * without SSL support, so it's expected to always be "biws" in this case,
+ * which is "n,,", base64-encoded. In builds supporting SSL, "biws" is
+ * used for Non-SSL connection attempts, and for SSL connections the
+ * client has to provide channel binding value.
*/
channel_binding = read_attr_value(&p, 'c');
+#ifdef USE_SSL
+ if (state->ssl_in_use)
+ {
+ char *enc_tls_message;
+ int enc_tls_len;
+
+ enc_tls_message = palloc(pg_b64_enc_len(state->tls_finish_len) + 1);
+ enc_tls_len = pg_b64_encode(state->tls_finish_message,
+ state->tls_finish_len,
+ enc_tls_message);
+ enc_tls_message[enc_tls_len] = '\0';
+
+ /*
+ * Compare the value sent by the client with the TLS finish message
+ * expected by the server.
+ */
+ if (strcmp(channel_binding, enc_tls_message) != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ (errmsg("no match for SCRAM channel-binding attribute in client-final-message"))));
+ }
+ else
+ {
+ if (strcmp(channel_binding, "biws") != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ (errmsg("unexpected SCRAM channel-binding attribute in client-final-message"))));
+ }
+#else
if (strcmp(channel_binding, "biws") != 0)
ereport(ERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION),
(errmsg("unexpected SCRAM channel-binding attribute in client-final-message"))));
+#endif
state->client_final_nonce = read_attr_value(&p, 'r');
/* ignore optional extensions */
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index cb30fc7b71..ee9bce03a2 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -859,10 +859,14 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
void *scram_opaq;
char *output = NULL;
int outputlen = 0;
- char *input;
+ char *input, *p;
+ char *sasl_mechs;
int inputlen;
+ int listlen = 0;
int result;
bool initial;
+ char *tls_finish = NULL;
+ int tls_finish_len = 0;
/*
* SASL auth is not supported for protocol versions before 3, because it
@@ -879,12 +883,49 @@ 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:
+ * - SCRAM-SHA-256, which is the mechanism with the same name.
+ * - SCRAM-SHA-256-PLUS, which is SCRAM-SHA-256 with channel binding. This
+ * is advertised to the client only if connection is attempted with SSL.
+ * The order of mechanisms is advertised in decreasing order of importance.
+ * The extra "\0" is for an empty string to terminate the list, and each
+ * mechanism listed needs to be separated with "\0".
*/
- sendAuthRequest(port, AUTH_REQ_SASL, SCRAM_SHA256_NAME "\0",
- strlen(SCRAM_SHA256_NAME) + 2);
+ listlen = 0;
+ sasl_mechs = (char *) palloc(strlen(SCRAM_SHA256_PLUS_NAME) +
+ strlen(SCRAM_SHA256_NAME) + 3);
+ p = sasl_mechs;
+
+ /* add SCRAM-SHA-256-PLUS, which depends on if SSL is in use */
+ if (port->ssl_in_use)
+ {
+ strcpy(p, SCRAM_SHA256_PLUS_NAME);
+ listlen += strlen(SCRAM_SHA256_PLUS_NAME) + 1;
+ p += strlen(SCRAM_SHA256_PLUS_NAME) + 1;
+ }
+
+ /* add generic SCRAM-SHA-256 */
+ strcpy(p, SCRAM_SHA256_NAME);
+ listlen += strlen(SCRAM_SHA256_NAME) + 1;
+ p += strlen(SCRAM_SHA256_NAME) + 1;
+
+ /* put "\0" to mark that list is finished */
+ p[0] = '\0';
+ listlen++;
+
+ sendAuthRequest(port, AUTH_REQ_SASL, sasl_mechs, listlen);
+ pfree(sasl_mechs);
+
+#ifdef USE_SSL
+ /*
+ * Fetch the data related to the SSL finish message to be used in the
+ * exchange.
+ */
+ if (port->ssl_in_use)
+ {
+ tls_finish = be_tls_get_peer_finish(port, &tls_finish_len);
+ }
+#endif
/*
* Initialize the status tracker for message exchanges.
@@ -897,7 +938,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_finish,
+ tls_finish_len);
/*
* Loop through SASL message exchange. This exchange can consist of
@@ -946,17 +991,36 @@ 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.
+ * We only support SCRAM-SHA-256 and SCRAM-SHA-256-PLUS 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),
- errmsg("client selected an invalid SASL authentication mechanism")));
+ errmsg("client selected an invalid SASL authentication mechanism"),
+ errdetail("Incorrect SASL mechanism name specified")));
}
+#ifdef USE_SSL
+ /*
+ * If an SSL connection is attempted, check for downgrade attacks.
+ * A connection is not allowed to specify SCRAM-SHA-256 if its
+ * -PLUS version has been submitted back to the client, which is
+ * the default.
+ */
+ if (port->ssl_in_use &&
+ strcmp(selected_mech, SCRAM_SHA256_NAME) == 0)
+ {
+ ereport(ERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("client selected invalid SASL authentication mechanism"),
+ errdetail("Channel binding published to client but not selected.")));
+ }
+#endif
+
inputlen = pq_getmsgint(&buf, 4);
if (inputlen == -1)
input = NULL;
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index fe15227a77..d063a587d0 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -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 finish message information from the
+ * client, useful for authorization when doing channel binding.
+ *
+ * Result is a palloc'd copy of the TLS finish message with its size.
+ */
+char *
+be_tls_get_peer_finish(Port *port, int *len)
+{
+ char dummy[1];
+ char *result;
+
+ /*
+ * OpenSSL does not offer an API to get directly the length of the
+ * expected TLS finish 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 = (char *) palloc(*len * sizeof(char));
+ (void) SSL_get_peer_finished(port->ssl, result, *len);
+
+ return result;
+}
+
/*
* Convert an X509 subject name to a cstring.
*
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 7bde744d51..5d59d79822 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -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_cipher(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_finish(Port *port, int *len);
#endif
extern ProtocolVersion FrontendProtocol;
diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h
index 0166e1945d..91cebe9a9b 100644
--- a/src/include/libpq/scram.h
+++ b/src/include/libpq/scram.h
@@ -13,8 +13,12 @@
#ifndef 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_PLUS_NAME "SCRAM-SHA-256-PLUS" /* with channel binding */
+
+/* Channel binding names */
+#define SCRAM_CHANNEL_TLS_UNIQUE "tls-unique"
/* Status codes for message exchange */
#define SASL_EXCHANGE_CONTINUE 0
@@ -22,7 +26,9 @@
#define SASL_EXCHANGE_FAILURE 2
/* 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, char *tls_finish_message,
+ int tls_finish_len);
extern int pg_be_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen, char **logdetail);
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index edfd42df85..e37a0cce27 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -17,6 +17,7 @@
#include "common/base64.h"
#include "common/saslprep.h"
#include "common/scram-common.h"
+#include "libpq/scram.h"
#include "fe-auth.h"
/* These are needed for getpid(), in the fallback implementation */
@@ -44,6 +45,9 @@ typedef struct
/* These are supplied by the user */
const char *username;
char *password;
+ bool ssl_in_use;
+ char *tls_finish_message;
+ int tls_finish_len;
/* We construct these */
uint8 SaltedPassword[SCRAM_KEY_LEN];
@@ -81,7 +85,11 @@ static bool pg_frontend_random(char *dst, int len);
* Initialize SCRAM exchange status.
*/
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,
+ char *tls_finish_message,
+ int tls_finish_len)
{
fe_scram_state *state;
char *prep_password;
@@ -93,6 +101,9 @@ pg_fe_scram_init(const char *username, const char *password)
memset(state, 0, sizeof(fe_scram_state));
state->state = FE_SCRAM_INIT;
state->username = username;
+ state->ssl_in_use = ssl_in_use;
+ state->tls_finish_message = tls_finish_message;
+ state->tls_finish_len = tls_finish_len;
/* Normalize the password with SASLprep, if possible */
rc = pg_saslprep(password, &prep_password);
@@ -297,9 +308,10 @@ static char *
build_client_first_message(fe_scram_state *state, PQExpBuffer errormessage)
{
char raw_nonce[SCRAM_RAW_NONCE_LEN + 1];
- char *buf;
- char buflen;
+ char *result;
+ int channel_info_len;
int encoded_len;
+ PQExpBufferData buf;
/*
* Generate a "raw" nonce. This is converted to ASCII-printable form by
@@ -328,26 +340,55 @@ build_client_first_message(fe_scram_state *state, PQExpBuffer errormessage)
* prepared with SASLprep, the message parsing would fail if it includes
* '=' or ',' characters.
*/
- buflen = 8 + strlen(state->client_nonce) + 1;
- buf = malloc(buflen);
- if (buf == NULL)
- {
- printfPQExpBuffer(errormessage,
- libpq_gettext("out of memory\n"));
- return NULL;
- }
- snprintf(buf, buflen, "n,,n=,r=%s", state->client_nonce);
+ initPQExpBuffer(&buf);
- state->client_first_message_bare = strdup(buf + 3);
+ /*
+ * First build the query field for channel binding. If the client is not
+ * built with SSL support, it cannot support channel binding so it needs
+ * to use "n" to let the server know. If built with SSL support, client
+ * needs to use "y" to let the server know that client has such support
+ * but that it is not using it as a non-SSL connection is requested.
+ * Finally if a SSL connection is done, use p=cb-name, for which only
+ * "tls-unique" is supported now.
+ */
+#ifdef USE_SSL
+ if (state->ssl_in_use)
+ appendPQExpBuffer(&buf, "p=%s", SCRAM_CHANNEL_TLS_UNIQUE);
+ else
+ appendPQExpBuffer(&buf, "y");
+#else
+ appendPQExpBuffer(&buf, "n");
+#endif
+
+ if (PQExpBufferDataBroken(buf))
+ goto oom_error;
+
+ 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)
- {
- free(buf);
- printfPQExpBuffer(errormessage,
- libpq_gettext("out of memory\n"));
- return NULL;
- }
+ goto oom_error;
- return buf;
+ result = strdup(buf.data);
+ if (result == NULL)
+ goto oom_error;
+
+ termPQExpBuffer(&buf);
+ return result;
+
+oom_error:
+ termPQExpBuffer(&buf);
+ printfPQExpBuffer(errormessage,
+ libpq_gettext("out of memory\n"));
+ return NULL;
}
/*
@@ -365,8 +406,30 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
/*
* Construct client-final-message-without-proof. We need to remember it
* for verifying the server proof in the final step of authentication.
+ * Client needs to provide a b64 encoded string of the TLS finish message
+ * only if a SSL connection is attempted.
*/
- appendPQExpBuffer(&buf, "c=biws,r=%s", state->nonce);
+#ifdef USE_SSL
+ if (state->ssl_in_use)
+ {
+ appendPQExpBuffer(&buf, "c=");
+ if (!enlargePQExpBuffer(&buf, pg_b64_enc_len(state->tls_finish_len)))
+ goto oom_error;
+ buf.len += pg_b64_encode(state->tls_finish_message,
+ state->tls_finish_len,
+ buf.data + buf.len);
+ buf.data[buf.len] = '\0';
+ }
+ else
+ appendPQExpBuffer(&buf, "c=biws");
+#else
+ appendPQExpBuffer(&buf, "c=biws");
+#endif
+
+ if (PQExpBufferDataBroken(buf))
+ goto oom_error;
+
+ appendPQExpBuffer(&buf, ",r=%s", state->nonce);
if (PQExpBufferDataBroken(buf))
goto oom_error;
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 382558f3f8..05f04be10a 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -504,7 +504,8 @@ pg_SASL_init(PGconn *conn, int payloadlen)
/*
* Parse the list of SASL authentication mechanisms in the
* AuthenticationSASL message, and select the best mechanism that we
- * support. (Only SCRAM-SHA-256 is supported at the moment.)
+ * support. SCRAM-SHA-256 and SCRAM-SHA-256-PLUS are the only ones
+ * supported at the moment.
*/
selected_mechanism = NULL;
for (;;)
@@ -532,9 +533,12 @@ pg_SASL_init(PGconn *conn, int payloadlen)
/*
* Do we support this mechanism?
*/
- if (strcmp(mechanism_buf.data, SCRAM_SHA256_NAME) == 0)
+ if (strcmp(mechanism_buf.data, SCRAM_SHA256_NAME) == 0 ||
+ strcmp(mechanism_buf.data, SCRAM_SHA256_PLUS_NAME) == 0)
{
char *password;
+ char *tls_finish = NULL;
+ int tls_finish_len = 0;
conn->password_needed = true;
password = conn->connhost[conn->whichhost].password;
@@ -547,10 +551,41 @@ pg_SASL_init(PGconn *conn, int payloadlen)
goto error;
}
- conn->sasl_state = pg_fe_scram_init(conn->pguser, password);
+#ifdef USE_SSL
+ /* Fetch information about the TLS finish message */
+ if (conn->ssl_in_use)
+ {
+ tls_finish = pgtls_get_finish(conn, &tls_finish_len);
+ if (tls_finish == NULL)
+ goto oom_error;
+ }
+#endif
+
+ conn->sasl_state = pg_fe_scram_init(conn->pguser,
+ password,
+ conn->ssl_in_use,
+ tls_finish,
+ tls_finish_len);
if (!conn->sasl_state)
goto oom_error;
- selected_mechanism = SCRAM_SHA256_NAME;
+
+ /*
+ * Select the mechanism to use by default. If SSL connection
+ * is attempted, the server will expect the -PLUS mechanism.
+ * If not, fallback to SCRAM-SHA-256.
+ */
+#ifdef USE_SSL
+ if (conn->ssl_in_use &&
+ strcmp(mechanism_buf.data, SCRAM_SHA256_PLUS_NAME) == 0)
+ selected_mechanism = SCRAM_SHA256_PLUS_NAME;
+ else if (!conn->ssl_in_use &&
+ strcmp(mechanism_buf.data, SCRAM_SHA256_NAME) == 0)
+ selected_mechanism = SCRAM_SHA256_NAME;
+#else
+ /* No channel binding can be selected without SSL support */
+ if (strcmp(mechanism_buf.data, SCRAM_SHA256_NAME) == 0)
+ selected_mechanism = SCRAM_SHA256_NAME;
+#endif
}
}
diff --git a/src/interfaces/libpq/fe-auth.h b/src/interfaces/libpq/fe-auth.h
index 5dc6bb5341..ee3dd7f96c 100644
--- a/src/interfaces/libpq/fe-auth.h
+++ b/src/interfaces/libpq/fe-auth.h
@@ -23,7 +23,9 @@ extern int pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn);
extern char *pg_fe_getauthname(PQExpBuffer errorMessage);
/* 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, char *tls_finish_message,
+ int tls_finish_len);
extern void pg_fe_scram_free(void *opaq);
extern void pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen,
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 2f29820e82..84a6e3c322 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -393,6 +393,32 @@ pgtls_write(PGconn *conn, const void *ptr, size_t len)
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_finish(PGconn *conn, int *len)
+{
+ char dummy[1];
+ char *result;
+
+ /*
+ * OpenSSL does not offer an API to get directly the length of the
+ * TLS finish 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 */
/* ------------------------------------------------------------ */
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 42913604e3..0eb8b60c95 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -453,11 +453,13 @@ struct pg_conn
/* Assorted state for SASL, SSL, GSS, etc */
void *sasl_state;
+ /* SSL structures */
+ bool ssl_in_use;
+
#ifdef USE_SSL
bool allow_ssl_try; /* Allowed to try SSL negotiation */
bool wait_ssl_try; /* Delay SSL negotiation until after
* attempting normal connection */
- bool ssl_in_use;
#ifdef USE_OPENSSL
SSL *ssl; /* SSL status, if have SSL connection */
X509 *peer; /* X509 cert of server */
@@ -668,6 +670,7 @@ extern void pgtls_close(PGconn *conn);
extern ssize_t pgtls_read(PGconn *conn, void *ptr, size_t len);
extern bool pgtls_read_pending(PGconn *conn);
extern ssize_t pgtls_write(PGconn *conn, const void *ptr, size_t len);
+extern char *pgtls_get_finish(PGconn *conn, int *len);
/*
* this is so that we can check if a connection is non-blocking internally
--
2.14.1
0003-Add-connection-parameters-saslname-and-saslchannelbi.patchapplication/octet-stream; name=0003-Add-connection-parameters-saslname-and-saslchannelbi.patchDownload
From 2d8148c3cf62e0e155553fb65efaf5b4c508f8f9 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 20 Jun 2017 11:41:10 +0900
Subject: [PATCH 3/4] Add connection parameters "saslname" and
"saslchannelbinding"
Those parameters can be used to respectively enforce the value of the
SASL mechanism name and the channel binding name sent to server during
a SASL message exchange.
A set of tests dedicated to SASL and channel binding is added as well
to the SSL test suite, which is handy to check the validity of a patch.
---
doc/src/sgml/libpq.sgml | 24 ++++++++++++++++
src/backend/libpq/auth-scram.c | 41 +++++++++++++++++++++-------
src/interfaces/libpq/fe-auth-scram.c | 53 ++++++++++++++++++++++++++++++++----
src/interfaces/libpq/fe-auth.c | 8 ++++++
src/interfaces/libpq/fe-auth.h | 4 +--
src/interfaces/libpq/fe-connect.c | 13 +++++++++
src/interfaces/libpq/libpq-int.h | 2 ++
src/test/ssl/ServerSetup.pm | 19 +++++++++++--
src/test/ssl/t/001_ssltests.pl | 2 +-
src/test/ssl/t/002_sasl.pl | 52 +++++++++++++++++++++++++++++++++++
10 files changed, 197 insertions(+), 21 deletions(-)
create mode 100644 src/test/ssl/t/002_sasl.pl
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 096a8be605..cfcf6ee7c2 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1220,6 +1220,30 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
</listitem>
</varlistentry>
+ <varlistentry id="libpq-saslname" xreflabel="saslname">
+ <term><literal>saslname</literal></term>
+ <listitem>
+ <para>
+ Controls the name of the SASL mechanism name sent to server when doing
+ a message exchange for a SASL authentication. The list of SASL
+ mechanisms supported by server are listed in
+ <xref linkend="sasl-authentication">.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="libpq-saslchannelbinding" xreflabel="saslchannelbinding">
+ <term><literal>saslchannelbinding</literal></term>
+ <listitem>
+ <para>
+ Controls the name of the channel binding name sent to server when doing
+ a message exchange for a SASL authentication. The list of channel
+ binding names supported by server are listed in
+ <xref linkend="sasl-authentication">.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="libpq-connect-sslmode" xreflabel="sslmode">
<term><literal>sslmode</literal></term>
<listitem>
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index c90fa2981a..d5371a2708 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -113,6 +113,7 @@ typedef struct
bool ssl_in_use;
char *tls_finish_message;
int tls_finish_len;
+ char *channel_binding;
int iterations;
char *salt; /* base64-encoded */
@@ -185,6 +186,7 @@ pg_be_scram_init(const char *username,
state->ssl_in_use = ssl_in_use;
state->tls_finish_message = tls_finish_message;
state->tls_finish_len = tls_finish_len;
+ state->channel_binding = NULL;
/*
* Parse the stored password verifier.
@@ -853,6 +855,9 @@ read_client_first_message(scram_state *state, char *input)
ereport(ERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION),
(errmsg("unexpected SCRAM channel-binding type"))));
+
+ /* Save the name for handling of subsequent messages */
+ state->channel_binding = pstrdup(channel_name);
#else
/*
* Client requires channel binding. We don't support it.
@@ -1106,20 +1111,36 @@ read_client_final_message(scram_state *state, char *input)
#ifdef USE_SSL
if (state->ssl_in_use)
{
- char *enc_tls_message;
- int enc_tls_len;
+ char *b64_message, *raw_data;
+ int b64_message_len, raw_data_len;
+
+ /* Fetch data for each channel binding type */
+ if (strcmp(state->channel_binding, SCRAM_CHANNEL_TLS_UNIQUE) == 0)
+ {
+ raw_data = state->tls_finish_message;
+ raw_data_len = state->tls_finish_len;
+ }
+ else
+ {
+ /* should not happen */
+ elog(ERROR, "invalid channel binding type");
+ }
+
+ /* should not happen, but better safe than sorry */
+ if (raw_data == NULL)
+ elog(ERROR, "empty binding data for channel name \"%s\"",
+ state->channel_binding);
- enc_tls_message = palloc(pg_b64_enc_len(state->tls_finish_len) + 1);
- enc_tls_len = pg_b64_encode(state->tls_finish_message,
- state->tls_finish_len,
- enc_tls_message);
- enc_tls_message[enc_tls_len] = '\0';
+ b64_message = palloc(pg_b64_enc_len(raw_data_len) + 1);
+ b64_message_len = pg_b64_encode(raw_data, raw_data_len,
+ b64_message);
+ b64_message[b64_message_len] = '\0';
/*
- * Compare the value sent by the client with the TLS finish message
- * expected by the server.
+ * Compare the value sent by the client with the value expected by
+ * the server.
*/
- if (strcmp(channel_binding, enc_tls_message) != 0)
+ if (strcmp(channel_binding, b64_message) != 0)
ereport(ERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION),
(errmsg("no match for SCRAM channel-binding attribute in client-final-message"))));
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index e37a0cce27..5b8522391d 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -48,6 +48,8 @@ typedef struct
bool ssl_in_use;
char *tls_finish_message;
int tls_finish_len;
+ /* enforceable user parameters */
+ char *saslchannelbinding; /* name of channel binding to use */
/* We construct these */
uint8 SaltedPassword[SCRAM_KEY_LEN];
@@ -88,6 +90,7 @@ void *
pg_fe_scram_init(const char *username,
const char *password,
bool ssl_in_use,
+ char *saslchannelbinding,
char *tls_finish_message,
int tls_finish_len)
{
@@ -105,6 +108,15 @@ pg_fe_scram_init(const char *username,
state->tls_finish_message = tls_finish_message;
state->tls_finish_len = tls_finish_len;
+ /*
+ * If user has specified a channel binding to use, enforce the
+ * channel binding sent to it. The default is "tls-unique".
+ */
+ if (saslchannelbinding && strlen(saslchannelbinding) > 0)
+ state->saslchannelbinding = strdup(saslchannelbinding);
+ else
+ state->saslchannelbinding = strdup(SCRAM_CHANNEL_TLS_UNIQUE);
+
/* Normalize the password with SASLprep, if possible */
rc = pg_saslprep(password, &prep_password);
if (rc == SASLPREP_OOM)
@@ -136,6 +148,10 @@ pg_fe_scram_free(void *opaq)
if (state->password)
free(state->password);
+ if (state->tls_finish_message)
+ free(state->tls_finish_message);
+ if (state->saslchannelbinding)
+ free(state->saslchannelbinding);
/* client messages */
if (state->client_nonce)
@@ -353,7 +369,7 @@ build_client_first_message(fe_scram_state *state, PQExpBuffer errormessage)
*/
#ifdef USE_SSL
if (state->ssl_in_use)
- appendPQExpBuffer(&buf, "p=%s", SCRAM_CHANNEL_TLS_UNIQUE);
+ appendPQExpBuffer(&buf, "p=%s", state->saslchannelbinding);
else
appendPQExpBuffer(&buf, "y");
#else
@@ -412,12 +428,39 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
#ifdef USE_SSL
if (state->ssl_in_use)
{
+ char *raw_data;
+ int raw_data_len;
+
+ if (strcmp(state->saslchannelbinding, SCRAM_CHANNEL_TLS_UNIQUE) == 0)
+ {
+ raw_data = state->tls_finish_message;
+ raw_data_len = state->tls_finish_len;
+ }
+ else
+ {
+ /* should not happen */
+ termPQExpBuffer(&buf);
+ printfPQExpBuffer(errormessage,
+ libpq_gettext("incorrect channel binding name\n"));
+ return NULL;
+ }
+
+ /* should not happen, but better safe than sorry */
+ if (raw_data == NULL)
+ {
+ /* should not happen */
+ termPQExpBuffer(&buf);
+ printfPQExpBuffer(errormessage,
+ libpq_gettext("empty binding data for channel name \"%s\"\n"),
+ state->saslchannelbinding);
+ return NULL;
+ }
+
appendPQExpBuffer(&buf, "c=");
- if (!enlargePQExpBuffer(&buf, pg_b64_enc_len(state->tls_finish_len)))
+
+ if (!enlargePQExpBuffer(&buf, pg_b64_enc_len(raw_data_len)))
goto oom_error;
- buf.len += pg_b64_encode(state->tls_finish_message,
- state->tls_finish_len,
- buf.data + buf.len);
+ buf.len += pg_b64_encode(raw_data, raw_data_len, buf.data + buf.len);
buf.data[buf.len] = '\0';
}
else
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 05f04be10a..4b26018f65 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -564,6 +564,7 @@ pg_SASL_init(PGconn *conn, int payloadlen)
conn->sasl_state = pg_fe_scram_init(conn->pguser,
password,
conn->ssl_in_use,
+ conn->saslchannelbinding,
tls_finish,
tls_finish_len);
if (!conn->sasl_state)
@@ -589,6 +590,13 @@ pg_SASL_init(PGconn *conn, int payloadlen)
}
}
+ /*
+ * If user has asked for a specific mechanism name, enforce the chosen
+ * name to it.
+ */
+ if (conn->saslname && strlen(conn->saslname) > 0)
+ selected_mechanism = conn->saslname;
+
if (!selected_mechanism)
{
printfPQExpBuffer(&conn->errorMessage,
diff --git a/src/interfaces/libpq/fe-auth.h b/src/interfaces/libpq/fe-auth.h
index ee3dd7f96c..3c699959e0 100644
--- a/src/interfaces/libpq/fe-auth.h
+++ b/src/interfaces/libpq/fe-auth.h
@@ -24,8 +24,8 @@ extern char *pg_fe_getauthname(PQExpBuffer errorMessage);
/* Prototypes for functions in fe-auth-scram.c */
extern void *pg_fe_scram_init(const char *username, const char *password,
- bool ssl_in_use, char *tls_finish_message,
- int tls_finish_len);
+ bool ssl_in_use, char *saslchannelbinding,
+ char *tls_finish_message, int tls_finish_len);
extern void pg_fe_scram_free(void *opaq);
extern void pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen,
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index c580d91135..27139579e9 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -262,6 +262,15 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
"TCP-Keepalives-Count", "", 10, /* strlen(INT32_MAX) == 10 */
offsetof(struct pg_conn, keepalives_count)},
+ /* Set of options proper to SASL */
+ {"saslname", NULL, NULL, NULL,
+ "SASL-Name", "", 21, /* maximum name size per IANA == 21 */
+ offsetof(struct pg_conn, saslname)},
+
+ {"saslchannelbinding", NULL, NULL, NULL,
+ "SASL-Channel", "", 22, /* sizeof("tls-unique-for-telnet") == 22 */
+ offsetof(struct pg_conn, saslchannelbinding)},
+
/*
* ssl options are allowed even without client SSL support because the
* client can still handle SSL modes "disable" and "allow". Other
@@ -3470,6 +3479,10 @@ freePGconn(PGconn *conn)
free(conn->keepalives_interval);
if (conn->keepalives_count)
free(conn->keepalives_count);
+ if (conn->saslname)
+ free(conn->saslname);
+ if (conn->saslchannelbinding)
+ free(conn->saslchannelbinding);
if (conn->sslmode)
free(conn->sslmode);
if (conn->sslcert)
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 0eb8b60c95..6d500aa5db 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -349,6 +349,8 @@ struct pg_conn
* retransmits */
char *keepalives_count; /* maximum number of TCP keepalive
* retransmits */
+ char *saslname; /* SASL mechanism name */
+ char *saslchannelbinding; /* channel binding used in SASL */
char *sslmode; /* SSL mode (require,prefer,allow,disable) */
char *sslcompression; /* SSL compression (0 or 1) */
char *sslkey; /* client key filename */
diff --git a/src/test/ssl/ServerSetup.pm b/src/test/ssl/ServerSetup.pm
index ad2e036602..b71969ac75 100644
--- a/src/test/ssl/ServerSetup.pm
+++ b/src/test/ssl/ServerSetup.pm
@@ -91,6 +91,9 @@ sub configure_test_server_for_ssl
{
my $node = $_[0];
my $serverhost = $_[1];
+ my $sslmethod = $_[2];
+ my $passwd = $_[3];
+ my $passwdhash = $_[4];
my $pgdata = $node->data_dir;
@@ -100,6 +103,15 @@ sub configure_test_server_for_ssl
$node->psql('postgres', "CREATE DATABASE trustdb");
$node->psql('postgres', "CREATE DATABASE certdb");
+ # Update password of each user as needed.
+ if (defined($passwd))
+ {
+ $node->psql('postgres',
+"SET password_encryption='$passwdhash'; ALTER USER ssltestuser PASSWORD '$passwd';");
+ $node->psql('postgres',
+"SET password_encryption='$passwdhash'; ALTER USER anotheruser PASSWORD '$passwd';");
+ }
+
# enable logging etc.
open my $conf, '>>', "$pgdata/postgresql.conf";
print $conf "fsync=off\n";
@@ -129,7 +141,7 @@ sub configure_test_server_for_ssl
$node->restart;
# Change pg_hba after restart because hostssl requires ssl=on
- configure_hba_for_ssl($node, $serverhost);
+ configure_hba_for_ssl($node, $serverhost, $sslmethod);
}
# Change the configuration to use given server cert file, and reload
@@ -159,6 +171,7 @@ sub configure_hba_for_ssl
{
my $node = $_[0];
my $serverhost = $_[1];
+ my $sslmethod = $_[2];
my $pgdata = $node->data_dir;
# Only accept SSL connections from localhost. Our tests don't depend on this
@@ -169,9 +182,9 @@ sub configure_hba_for_ssl
print $hba
"# TYPE DATABASE USER ADDRESS METHOD\n";
print $hba
-"hostssl trustdb ssltestuser $serverhost/32 trust\n";
+"hostssl trustdb ssltestuser $serverhost/32 $sslmethod\n";
print $hba
-"hostssl trustdb ssltestuser ::1/128 trust\n";
+"hostssl trustdb ssltestuser ::1/128 $sslmethod\n";
print $hba
"hostssl certdb ssltestuser $serverhost/32 cert\n";
print $hba
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 890e3051a2..e690c1fa15 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -32,7 +32,7 @@ $node->init;
$ENV{PGHOST} = $node->host;
$ENV{PGPORT} = $node->port;
$node->start;
-configure_test_server_for_ssl($node, $SERVERHOSTADDR);
+configure_test_server_for_ssl($node, $SERVERHOSTADDR, "trust");
switch_server_cert($node, 'server-cn-only');
### Part 1. Run client-side tests.
diff --git a/src/test/ssl/t/002_sasl.pl b/src/test/ssl/t/002_sasl.pl
new file mode 100644
index 0000000000..a625f0d473
--- /dev/null
+++ b/src/test/ssl/t/002_sasl.pl
@@ -0,0 +1,52 @@
+use strict;
+use warnings;
+use PostgresNode;
+use TestLib;
+use Test::More tests => 6;
+use ServerSetup;
+use File::Copy;
+
+# test combinations of SASL authentication for SCRAM mechanism:
+# - SCRAM-SHA-256 and SCRAM-SHA-256-PLUS
+# - Channel bindings
+
+# 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;
+
+#### Part 0. 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";
+
+# Tests with default channel binding and SASL mechanism names.
+# tls-unique is used here
+test_connect_ok($common_connstr, "saslname=SCRAM-SHA-256-PLUS");
+test_connect_fails($common_connstr, "saslname=not-exists");
+# Downgrade attack.
+test_connect_fails($common_connstr, "saslname=SCRAM-SHA-256");
+test_connect_fails($common_connstr,
+ "saslname=SCRAM-SHA-256 saslchannelbinding=tls-unique");
+
+# Channel bindings
+test_connect_ok($common_connstr,
+ "saslname=SCRAM-SHA-256-PLUS saslchannelbinding=tls-unique");
+test_connect_fails($common_connstr,
+ "saslname=SCRAM-SHA-256-PLUS saslchannelbinding=not-exists");
--
2.14.1
0004-Implement-channel-binding-tls-server-end-point-for-S.patchapplication/octet-stream; name=0004-Implement-channel-binding-tls-server-end-point-for-S.patchDownload
From df079495c20779a7294a8ced2b62641a3aeebb64 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 20 Jun 2017 12:51:10 +0900
Subject: [PATCH 4/4] Implement channel binding tls-server-end-point for SCRAM
As referenced in RFC 5929, this channel binding is not the default value
and uses a hash of the certificate as binding data. On the frontend, this
can be resumed in getting the data from SSL_get_peer_certificate() and
on the backend SSL_get_certificate().
The hashing algorithm needs also to switch to SHA-256 if the signature
algorithm is MD5 or SHA-1, so let's be careful about that.
---
doc/src/sgml/protocol.sgml | 4 +-
src/backend/libpq/auth-scram.c | 21 ++++++++--
src/backend/libpq/auth.c | 13 ++++--
src/backend/libpq/be-secure-openssl.c | 69 ++++++++++++++++++++++++++++++++
src/include/libpq/libpq-be.h | 1 +
src/include/libpq/scram.h | 4 +-
src/interfaces/libpq/fe-auth-scram.c | 20 ++++++++-
src/interfaces/libpq/fe-auth.c | 18 ++++++++-
src/interfaces/libpq/fe-auth.h | 3 +-
src/interfaces/libpq/fe-secure-openssl.c | 66 ++++++++++++++++++++++++++++++
src/interfaces/libpq/libpq-int.h | 1 +
src/test/ssl/t/002_sasl.pl | 6 ++-
12 files changed, 210 insertions(+), 16 deletions(-)
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 37ae7276e5..62a958ed1d 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1552,8 +1552,8 @@ the password is in.
<para>
<firstterm>Channel binding</> is supported in builds with SSL support, and
uses as mechanism name <literal>SCRAM-SHA-256-PLUS</> for this purpose as
-defined per IANA. The only channel binding type supported by the server
-is <literal>tls-unique</>.
+defined per IANA. The channel binding types supported by the server
+are <literal>tls-unique</>, the default, and <literal>tls-server-end-point</>.
</para>
<procedure>
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index d5371a2708..89b659942b 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -113,6 +113,8 @@ typedef struct
bool ssl_in_use;
char *tls_finish_message;
int tls_finish_len;
+ char *certificate_hash;
+ int certificate_hash_len;
char *channel_binding;
int iterations;
@@ -175,7 +177,9 @@ pg_be_scram_init(const char *username,
const char *shadow_pass,
bool ssl_in_use,
char *tls_finish_message,
- int tls_finish_len)
+ int tls_finish_len,
+ char *certificate_hash,
+ int certificate_hash_len)
{
scram_state *state;
bool got_verifier;
@@ -186,6 +190,8 @@ pg_be_scram_init(const char *username,
state->ssl_in_use = ssl_in_use;
state->tls_finish_message = tls_finish_message;
state->tls_finish_len = tls_finish_len;
+ state->certificate_hash = certificate_hash;
+ state->certificate_hash_len = certificate_hash_len;
state->channel_binding = NULL;
/*
@@ -847,11 +853,12 @@ read_client_first_message(scram_state *state, char *input)
errmsg("client supports SCRAM channel binding, but server does not need it for non-SSL connections")));
/*
- * Read value provided by client, only tls-unique is supported
- * for now.
+ * Read value provided by client, only tls-unique and
+ * tls-server-end-point are supported for now.
*/
channel_name = read_attr_value(&input, 'p');
- if (strcmp(channel_name, SCRAM_CHANNEL_TLS_UNIQUE) != 0)
+ if (strcmp(channel_name, SCRAM_CHANNEL_TLS_UNIQUE) != 0 &&
+ strcmp(channel_name, SCRAM_CHANNEL_TLS_ENDPOINT) != 0)
ereport(ERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION),
(errmsg("unexpected SCRAM channel-binding type"))));
@@ -1120,6 +1127,12 @@ read_client_final_message(scram_state *state, char *input)
raw_data = state->tls_finish_message;
raw_data_len = state->tls_finish_len;
}
+ else if (strcmp(state->channel_binding,
+ SCRAM_CHANNEL_TLS_ENDPOINT) == 0)
+ {
+ raw_data = state->certificate_hash;
+ raw_data_len = state->certificate_hash_len;
+ }
else
{
/* should not happen */
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index ee9bce03a2..bbab65b64b 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -867,6 +867,8 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
bool initial;
char *tls_finish = NULL;
int tls_finish_len = 0;
+ char *certificate_bash = NULL;
+ int certificate_bash_len = 0;
/*
* SASL auth is not supported for protocol versions before 3, because it
@@ -918,12 +920,15 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
#ifdef USE_SSL
/*
- * Fetch the data related to the SSL finish message to be used in the
- * exchange.
+ * Fetch the data related to the SSL finish message and the client
+ * certificate (if any) to be used in the exchange.
*/
if (port->ssl_in_use)
{
tls_finish = be_tls_get_peer_finish(port, &tls_finish_len);
+ certificate_bash =
+ be_tls_get_certificate_hash(port,
+ &certificate_bash_len);
}
#endif
@@ -942,7 +947,9 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
shadow_pass,
port->ssl_in_use,
tls_finish,
- tls_finish_len);
+ tls_finish_len,
+ certificate_bash,
+ certificate_bash_len);
/*
* Loop through SASL message exchange. This exchange can consist of
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index d063a587d0..cea60c6741 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -1239,6 +1239,75 @@ be_tls_get_peer_finish(Port *port, int *len)
return result;
}
+/*
+ * Get the server certificate hash for authentication purposes. Per
+ * RFC 5929 and tls-server-end-point, the TLS server's certificate bytes
+ * need to be hashed with SHA-256 if its signature algorithm is MD5 or
+ * SHA-1. If SHA-256 or something else is used, the same hash as the
+ * signature algorithm is used.
+ * The result is a palloc'd hash of the server certificate with its
+ * size, and NULL if there is nothing certificates available.
+ */
+char *
+be_tls_get_certificate_hash(Port *port, int *len)
+{
+ char *cert_hash = NULL;
+ X509 *server_cert;
+
+ *len = 0;
+ server_cert = SSL_get_certificate(port->ssl);
+
+ if (server_cert != NULL)
+ {
+ const EVP_MD *algo_type = NULL;
+ char hash[EVP_MAX_MD_SIZE]; /* size for SHA-512 */
+ unsigned int hash_size;
+ int algo_nid;
+
+ /*
+ * Get the signature algorithm of the certificate to determine the
+ * hash algorithm to use for the result.
+ */
+ if (!OBJ_find_sigid_algs(X509_get_signature_nid(server_cert),
+ &algo_nid, NULL))
+ elog(ERROR, "could not find signature algorithm");
+
+ switch (algo_nid)
+ {
+ case NID_sha512:
+ algo_type = EVP_sha512();
+ break;
+
+ case NID_sha384:
+ algo_type = EVP_sha384();
+ break;
+
+ /*
+ * Fallback to SHA-256 for weaker hashes, and keep them listed
+ * here for reference.
+ */
+ case NID_md5:
+ case NID_sha1:
+ case NID_sha224:
+ case NID_sha256:
+ default:
+ algo_type = EVP_sha256();
+ break;
+ }
+
+ /* generate and save the certificate hash */
+ if (!X509_digest(server_cert, algo_type, (unsigned char *) hash,
+ &hash_size))
+ elog(ERROR, "could not generate server certificate hash");
+
+ cert_hash = (char *) palloc(hash_size);
+ memcpy(cert_hash, hash, hash_size);
+ *len = hash_size;
+ }
+
+ return cert_hash;
+}
+
/*
* Convert an X509 subject name to a cstring.
*
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 5d59d79822..0392860a87 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -210,6 +210,7 @@ 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_peerdn_name(Port *port, char *ptr, size_t len);
extern char *be_tls_get_peer_finish(Port *port, int *len);
+extern char *be_tls_get_certificate_hash(Port *port, int *len);
#endif
extern ProtocolVersion FrontendProtocol;
diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h
index 91cebe9a9b..55d0568d43 100644
--- a/src/include/libpq/scram.h
+++ b/src/include/libpq/scram.h
@@ -19,6 +19,7 @@
/* Channel binding names */
#define SCRAM_CHANNEL_TLS_UNIQUE "tls-unique"
+#define SCRAM_CHANNEL_TLS_ENDPOINT "tls-server-end-point"
/* Status codes for message exchange */
#define SASL_EXCHANGE_CONTINUE 0
@@ -28,7 +29,8 @@
/* Routines dedicated to authentication */
extern void *pg_be_scram_init(const char *username, const char *shadow_pass,
bool ssl_in_use, char *tls_finish_message,
- int tls_finish_len);
+ int tls_finish_len, char *certificate_hash,
+ int certificate_hash_len);
extern int pg_be_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen, char **logdetail);
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index 5b8522391d..10517d16e5 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -48,6 +48,8 @@ typedef struct
bool ssl_in_use;
char *tls_finish_message;
int tls_finish_len;
+ char *certificate_hash;
+ int certificate_hash_len;
/* enforceable user parameters */
char *saslchannelbinding; /* name of channel binding to use */
@@ -92,7 +94,9 @@ pg_fe_scram_init(const char *username,
bool ssl_in_use,
char *saslchannelbinding,
char *tls_finish_message,
- int tls_finish_len)
+ int tls_finish_len,
+ char *certificate_hash,
+ int certificate_hash_len)
{
fe_scram_state *state;
char *prep_password;
@@ -107,6 +111,8 @@ pg_fe_scram_init(const char *username,
state->ssl_in_use = ssl_in_use;
state->tls_finish_message = tls_finish_message;
state->tls_finish_len = tls_finish_len;
+ state->certificate_hash = certificate_hash;
+ state->certificate_hash_len = certificate_hash_len;
/*
* If user has specified a channel binding to use, enforce the
@@ -150,6 +156,8 @@ pg_fe_scram_free(void *opaq)
free(state->password);
if (state->tls_finish_message)
free(state->tls_finish_message);
+ if (state->certificate_hash)
+ free(state->certificate_hash);
if (state->saslchannelbinding)
free(state->saslchannelbinding);
@@ -423,7 +431,9 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
* Construct client-final-message-without-proof. We need to remember it
* for verifying the server proof in the final step of authentication.
* Client needs to provide a b64 encoded string of the TLS finish message
- * only if a SSL connection is attempted.
+ * only if a SSL connection is attempted using "tls-unique" as channel
+ * binding. For "tls-server-end-point", a hash of the client certificate
+ * is sent instead.
*/
#ifdef USE_SSL
if (state->ssl_in_use)
@@ -436,6 +446,12 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
raw_data = state->tls_finish_message;
raw_data_len = state->tls_finish_len;
}
+ else if (strcmp(state->saslchannelbinding,
+ SCRAM_CHANNEL_TLS_ENDPOINT) == 0)
+ {
+ raw_data = state->certificate_hash;
+ raw_data_len = state->certificate_hash_len;
+ }
else
{
/* should not happen */
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 4b26018f65..d72d35670f 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -539,6 +539,8 @@ pg_SASL_init(PGconn *conn, int payloadlen)
char *password;
char *tls_finish = NULL;
int tls_finish_len = 0;
+ char *certificate_hash = NULL;
+ int certificate_hash_len = 0;
conn->password_needed = true;
password = conn->connhost[conn->whichhost].password;
@@ -552,12 +554,21 @@ pg_SASL_init(PGconn *conn, int payloadlen)
}
#ifdef USE_SSL
- /* Fetch information about the TLS finish message */
+ /*
+ * Fetch information about the TLS finish message and client
+ * certificate if any.
+ */
if (conn->ssl_in_use)
{
tls_finish = pgtls_get_finish(conn, &tls_finish_len);
if (tls_finish == NULL)
goto oom_error;
+
+ certificate_hash =
+ pgtls_get_peer_certificate_hash(conn,
+ &certificate_hash_len);
+ if (certificate_hash == NULL)
+ goto oom_error;
}
#endif
@@ -566,7 +577,10 @@ pg_SASL_init(PGconn *conn, int payloadlen)
conn->ssl_in_use,
conn->saslchannelbinding,
tls_finish,
- tls_finish_len);
+ tls_finish_len,
+ certificate_hash,
+ certificate_hash_len);
+
if (!conn->sasl_state)
goto oom_error;
diff --git a/src/interfaces/libpq/fe-auth.h b/src/interfaces/libpq/fe-auth.h
index 3c699959e0..8db86f8bcc 100644
--- a/src/interfaces/libpq/fe-auth.h
+++ b/src/interfaces/libpq/fe-auth.h
@@ -25,7 +25,8 @@ extern char *pg_fe_getauthname(PQExpBuffer errorMessage);
/* Prototypes for functions in fe-auth-scram.c */
extern void *pg_fe_scram_init(const char *username, const char *password,
bool ssl_in_use, char *saslchannelbinding,
- char *tls_finish_message, int tls_finish_len);
+ char *tls_finish_message, int tls_finish_len,
+ char *certificate_hash, int certificate_hash_len);
extern void pg_fe_scram_free(void *opaq);
extern void pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen,
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 84a6e3c322..00a14289e4 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -418,6 +418,72 @@ pgtls_get_finish(PGconn *conn, int *len)
return result;
}
+/*
+ * Get the hash of the server certificate
+ *
+ * This information is useful for end-point channel binding, where
+ * the client certificate hash is used as a link, per RFC 5929.
+ */
+char *
+pgtls_get_peer_certificate_hash(PGconn *conn, int *len)
+{
+ char *cert_hash = NULL;
+
+ *len = 0;
+
+ if (conn->peer)
+ {
+ X509 *peer_cert = conn->peer;
+ const EVP_MD *algo_type = NULL;
+ char hash[EVP_MAX_MD_SIZE]; /* size for SHA-512 */
+ unsigned int hash_size;
+ int algo_nid;
+
+ /*
+ * Get the signature algorithm of the certificate to determine the
+ * hash algorithm to use for the result.
+ */
+ if (!OBJ_find_sigid_algs(X509_get_signature_nid(peer_cert),
+ &algo_nid, NULL))
+ return NULL;
+
+ switch (algo_nid)
+ {
+ case NID_sha512:
+ algo_type = EVP_sha512();
+ break;
+
+ case NID_sha384:
+ algo_type = EVP_sha384();
+ break;
+
+ /*
+ * Fallback to SHA-256 for weaker hashes, and keep them listed
+ * here for reference.
+ */
+ case NID_md5:
+ case NID_sha1:
+ case NID_sha224:
+ case NID_sha256:
+ default:
+ algo_type = EVP_sha256();
+ break;
+ }
+
+ if (!X509_digest(peer_cert, algo_type, (unsigned char *) hash,
+ &hash_size))
+ return NULL;
+
+ /* save result */
+ cert_hash = (char *) malloc(hash_size);
+ if (cert_hash == NULL)
+ return NULL;
+ memcpy(cert_hash, hash, hash_size);
+ *len = hash_size;
+ }
+
+ return cert_hash;
+}
/* ------------------------------------------------------------ */
/* OpenSSL specific code */
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 6d500aa5db..f1e2c0bb3c 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -673,6 +673,7 @@ extern ssize_t pgtls_read(PGconn *conn, void *ptr, size_t len);
extern bool pgtls_read_pending(PGconn *conn);
extern ssize_t pgtls_write(PGconn *conn, const void *ptr, size_t len);
extern char *pgtls_get_finish(PGconn *conn, int *len);
+extern char *pgtls_get_peer_certificate_hash(PGconn *conn, int *len);
/*
* this is so that we can check if a connection is non-blocking internally
diff --git a/src/test/ssl/t/002_sasl.pl b/src/test/ssl/t/002_sasl.pl
index a625f0d473..a4e6dfac3d 100644
--- a/src/test/ssl/t/002_sasl.pl
+++ b/src/test/ssl/t/002_sasl.pl
@@ -2,7 +2,7 @@ use strict;
use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 6;
+use Test::More tests => 8;
use ServerSetup;
use File::Copy;
@@ -44,9 +44,13 @@ test_connect_fails($common_connstr, "saslname=not-exists");
test_connect_fails($common_connstr, "saslname=SCRAM-SHA-256");
test_connect_fails($common_connstr,
"saslname=SCRAM-SHA-256 saslchannelbinding=tls-unique");
+test_connect_fails($common_connstr,
+ "saslname=SCRAM-SHA-256 saslchannelbinding=tls-server-end-point");
# Channel bindings
test_connect_ok($common_connstr,
"saslname=SCRAM-SHA-256-PLUS saslchannelbinding=tls-unique");
+test_connect_ok($common_connstr,
+ "saslname=SCRAM-SHA-256-PLUS saslchannelbinding=tls-server-end-point");
test_connect_fails($common_connstr,
"saslname=SCRAM-SHA-256-PLUS saslchannelbinding=not-exists");
--
2.14.1
On 9/10/17 22:37, Michael Paquier wrote:
On Mon, Aug 21, 2017 at 9:51 PM, Michael Paquier
<michael.paquier@gmail.com> wrote:On Tue, Jun 20, 2017 at 1:11 PM, Michael Paquier
<michael.paquier@gmail.com> wrote:With the tests directly in the patch, things are easy to run. WIth
PG10 stabilization work, of course I don't expect much feedback :)
But this set of patches looks like the direction we want to go so as
JDBC and libpq users can take advantage of channel binding with SCRAM.Attached is a new patch set, rebased as of c6293249.
And again a new set to fix the rotten bits caused by 85f4d63.
It seems we should start by sorting out the mechanism by which the
client can control what authentication mechanisms it accepts. In your
patch set you introduce a connection parameter saslname. I think we
should expand that to non-SASL mechanisms and have it be some kind of
whitelist or blacklist. It might be reasonable for a client to require
"gssapi" or "cert" for example or do an exclusion like "!password !md5
!ldap".
Thoughts?
--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Tue, Sep 12, 2017 at 11:38 PM, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:
It seems we should start by sorting out the mechanism by which the
client can control what authentication mechanisms it accepts. In your
patch set you introduce a connection parameter saslname. I think we
should expand that to non-SASL mechanisms and have it be some kind of
whitelist or blacklist. It might be reasonable for a client to require
"gssapi" or "cert" for example or do an exclusion like "!password !md5
!ldap".Thoughts?
That looks like a sensible approach to begin with at the end: there
have been complains that a client can be tricked into using MD5 by a
rogue server even if it was willing to use SCRAM. So what about a
parameter called pgauthfilter, which uses a comma-separated list of
keywords. As you say, using an exclamation point to negate an
authentication method is fine for me. For SCRAM, we could just use
"scram-sha-256" as keyword.
Once channel binding is involved though.. This needs to be extended
and this needs careful thoughts:
* "scram-sha-256" means that the version without channel binding is
accepted. "!scram-sha-256" means that scram without channel binding is
refused.
* "scram-sha-256-plus" means that all channel bindings are accepted.
"!scram-sha-256-plus" means that no channel binding are accepted.
After that there is some filtering per channel binding name. Do we
want a separate parameter or just filter with longer names like
"scram-sha-256-plus-tls-unique" and
"scram-sha-256-plus-tls-server-end-point"? The last one gets
particularly long, this does not help users with typos :)
--
Michael
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 9/12/17 19:03, Michael Paquier wrote:
Once channel binding is involved though.. This needs to be extended
and this needs careful thoughts:
* "scram-sha-256" means that the version without channel binding is
accepted. "!scram-sha-256" means that scram without channel binding is
refused.
* "scram-sha-256-plus" means that all channel bindings are accepted.
"!scram-sha-256-plus" means that no channel binding are accepted.
After that there is some filtering per channel binding name. Do we
want a separate parameter or just filter with longer names like
"scram-sha-256-plus-tls-unique" and
"scram-sha-256-plus-tls-server-end-point"? The last one gets
particularly long, this does not help users with typos :)
Second thoughts, to make things simpler. All we need for channel
binding is a connection flag that says "I require channel binding". It
could be modeled after the sslmode parameter, e.g., cbind=disable (maybe
for debugging), cbind=prefer (default), cbind=require. If you specify
"require", then libpq would refuse to proceed unless scram-sha2-256-plus
(or future similar mechanisms) was offered for authentication.
We don't even need a parameter that specifies which channel binding type
to use. If libpq implements tls-unique, it should always use that. We
might need a flag for testing other types, but that should not be an
in-the-user's-face option.
--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Fri, Sep 15, 2017 at 1:58 AM, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:
Second thoughts, to make things simpler. All we need for channel
binding is a connection flag that says "I require channel binding". It
could be modeled after the sslmode parameter, e.g., cbind=disable (maybe
for debugging), cbind=prefer (default), cbind=require. If you specify
"require", then libpq would refuse to proceed unless scram-sha2-256-plus
(or future similar mechanisms) was offered for authentication.We don't even need a parameter that specifies which channel binding type
to use. If libpq implements tls-unique, it should always use that. We
might need a flag for testing other types, but that should not be an
in-the-user's-face option.
JDBC folks are willing to have end-point, and we should have a way to
enforce it in my opinion for at least testing with an implementation
at client-level in Postgres core. I agree that without the JDBC needs
having a on/off switch would be sufficient, and actually RFC compliant
as tls-unique is mandatory.
--
Michael
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Thu, Sep 14, 2017 at 12:58 PM, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:
Second thoughts, to make things simpler. All we need for channel
binding is a connection flag that says "I require channel binding". It
could be modeled after the sslmode parameter, e.g., cbind=disable (maybe
for debugging), cbind=prefer (default), cbind=require. If you specify
"require", then libpq would refuse to proceed unless scram-sha2-256-plus
(or future similar mechanisms) was offered for authentication.
+1, although I think cbind is too brief. I'd spell it out.
We don't even need a parameter that specifies which channel binding type
to use. If libpq implements tls-unique, it should always use that. We
might need a flag for testing other types, but that should not be an
in-the-user's-face option.
I'm not so sure about this part. Why don't we want to let users control this?
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Sat, Sep 16, 2017 at 12:42 AM, Robert Haas <robertmhaas@gmail.com> wrote:
On Thu, Sep 14, 2017 at 12:58 PM, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:Second thoughts, to make things simpler. All we need for channel
binding is a connection flag that says "I require channel binding". It
could be modeled after the sslmode parameter, e.g., cbind=disable (maybe
for debugging), cbind=prefer (default), cbind=require. If you specify
"require", then libpq would refuse to proceed unless scram-sha2-256-plus
(or future similar mechanisms) was offered for authentication.+1, although I think cbind is too brief. I'd spell it out.
I would like to point out that per the RFC, if the client attempts a
SSL connection with SCRAM and that the server supports channel
binding, then it has to publish the SASL mechanism for channel
binding, aka SCRAM-PLUS. If the client tries to force the use of SCRAM
even if SCRAM-PLUS is specified, this is seen as a downgrade attack by
the server which must reject the connection. So this parameter has
meaning only if you try to connect to a PG10 server using a PG11
client (assuming that channel binding gets into PG11). If you connect
with a PG11 client to a PG11 server with SSL, the server publishes
SCRAM-PLUS, the client has to use it, hence this turns out to make
cbind=disable and prefer meaningless in the long-term. If the client
does not use SSL, then there is no channel binding, and cbind=require
loses its value. So cbind's fate is actually linked to sslmode.
We don't even need a parameter that specifies which channel binding type
to use. If libpq implements tls-unique, it should always use that. We
might need a flag for testing other types, but that should not be an
in-the-user's-face option.I'm not so sure about this part. Why don't we want to let users control this?
That's at least useful for at least testing, tls-unique should be the
default, but we need as well end-point which we much likely would like
to be able to enforce from the client, and being able to enforce the
type of channel binding used does not betray the RFC.
--
Michael
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 9/10/17 22:37, Michael Paquier wrote:
With the tests directly in the patch, things are easy to run. WIth
PG10 stabilization work, of course I don't expect much feedback :)
But this set of patches looks like the direction we want to go so as
JDBC and libpq users can take advantage of channel binding with SCRAM.Attached is a new patch set, rebased as of c6293249.
And again a new set to fix the rotten bits caused by 85f4d63.
Here is a review of the meat of the code, leaving aside the discussion
of the libpq connection parameters.
Overall, the structure of the code makes sense and it fits in well with
the existing SCRAM code.
I think the channel-binding negotiation on the client side is wrong.
The logic in the patch is
+#ifdef USE_SSL
+ if (state->ssl_in_use)
+ appendPQExpBuffer(&buf, "p=%s", SCRAM_CHANNEL_TLS_UNIQUE);
+ else
+ appendPQExpBuffer(&buf, "y");
+#else
+ appendPQExpBuffer(&buf, "n");
+#endif
But if SSL is compiled in but not used, the client does not in fact
support channel binding (for that connection), so it should send "n".
The "y" flag should be sent if ssl_in_use but the client did not see the
server advertise SCRAM-SHA256-PLUS. That logic is missing entirely in
this patch.
You have the server reject a client that does not support channel
binding ("n") on all SSL connections. I don't think this is correct.
It is up to the client to use channel binding or not, even on SSL
connections.
We should update pg_hba.conf to allow a method specification of
"scram-sha256-plus", i.e., only advertise the channel binding variant to
the client. Then you could make policy decisions like rejecting clients
that do not use channel binding on SSL connections. This could be a
separate patch later.
The error message in the "p" case if SSL is not in use is a bit
confusing: "but the server does not need it". I think this could be
left at the old message "but it is not supported". This ties into my
interpretation from above that whether channel binding is "supported"
depends on whether SSL is in use for a particular connection.
Some small code things:
- prefer to use size_t over int for length (tls_finish_len etc.)
- tls_finish should be tls_finished
- typos: certificate_bash -> certificate_hash
In the patch for tls-server-end-point, I think the selection of the hash
function deviates slightly from the RFC. The RFC only says to
substitute MD5 and SHA-1. It doesn't say to turn SHA-224 into SHA-256,
for example. There is also the problem that the code as written will
turn any unrecognized hash method into SHA-256. If think the code
should single out MD5 and SHA-1 only and then use EVP_get_digestbynid()
for the rest. (I don't know anything about the details of OpenSSL APIs,
but that function sounded right to me.)
--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Mon, Sep 25, 2017 at 11:22 PM, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:
Here is a review of the meat of the code, leaving aside the discussion
of the libpq connection parameters.Overall, the structure of the code makes sense and it fits in well with
the existing SCRAM code.
Thanks for the review, Peter.
I think the channel-binding negotiation on the client side is wrong.
The logic in the patch is+#ifdef USE_SSL + if (state->ssl_in_use) + appendPQExpBuffer(&buf, "p=%s", SCRAM_CHANNEL_TLS_UNIQUE); + else + appendPQExpBuffer(&buf, "y"); +#else + appendPQExpBuffer(&buf, "n"); +#endifBut if SSL is compiled in but not used, the client does not in fact
support channel binding (for that connection), so it should send "n".
For others, details about this flag are here:
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=".
And channel binding description is here:
https://tools.ietf.org/html/rfc5802#section-6
The "y" flag should be sent if ssl_in_use but the client did not see the
server advertise SCRAM-SHA256-PLUS. That logic is missing entirely in
this patch.
Okay. I think I get your point here. I agree that the client is
deficient here. This needs some more work.
You have the server reject a client that does not support channel
binding ("n") on all SSL connections. I don't think this is correct.
It is up to the client to use channel binding or not, even on SSL
connections.
It seems that I got confused with the meaning of "y" mixed with
ssl_in_use. The server should reject "y" instead only if SSL is in
use.
We should update pg_hba.conf to allow a method specification of
"scram-sha256-plus", i.e., only advertise the channel binding variant to
the client. Then you could make policy decisions like rejecting clients
that do not use channel binding on SSL connections. This could be a
separate patch later.
OK, I agree that there could be some value in that. This complicates a
bit hba rule checks, but nothing really complicated either.
The error message in the "p" case if SSL is not in use is a bit
confusing: "but the server does not need it". I think this could be
left at the old message "but it is not supported". This ties into my
interpretation from above that whether channel binding is "supported"
depends on whether SSL is in use for a particular connection.
Check.
Some small code things:
- prefer to use size_t over int for length (tls_finish_len etc.)
- tls_finish should be tls_finished
- typos: certificate_bash -> certificate_hash
Yes, thanks for spotting those.
In the patch for tls-server-end-point, I think the selection of the hash
function deviates slightly from the RFC. The RFC only says to
substitute MD5 and SHA-1. It doesn't say to turn SHA-224 into SHA-256,
for example. There is also the problem that the code as written will
turn any unrecognized hash method into SHA-256. If think the code
should single out MD5 and SHA-1 only and then use EVP_get_digestbynid()
for the rest. (I don't know anything about the details of OpenSSL APIs,
but that function sounded right to me.)
Relevant bits from the RFC: https://tools.ietf.org/html/rfc5929#section-4.1
o if the certificate's signatureAlgorithm uses a single hash
function, and that hash function is either MD5 [RFC1321] or SHA-1
[RFC3174], then use SHA-256 [FIPS-180-3];
o if the certificate's signatureAlgorithm uses no hash functions or
uses multiple hash functions, then this channel binding type's
channel bindings are undefined at this time (updates to is channel
binding type may occur to address this issue if it ever arises).
OK. Hm. I think that we need as well to check for the case where
EVP_get_digestbynid() returns NULL then and issue an ERROR on it. That
seems to be the second point outlined by the RFC I am quoting here.
This patch got the feedback I was looking for, and this requires some
rework anyway. So I am marking the patch as returned with feedback for
now. This won't make it in time for this CF.
Thanks Peter for looking at it and pointing out some deficiencies.
--
Michael
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Fri, Sep 15, 2017 at 6:29 PM, Michael Paquier
<michael.paquier@gmail.com> wrote:
I would like to point out that per the RFC, if the client attempts a
SSL connection with SCRAM and that the server supports channel
binding, then it has to publish the SASL mechanism for channel
binding, aka SCRAM-PLUS. If the client tries to force the use of SCRAM
even if SCRAM-PLUS is specified, this is seen as a downgrade attack by
the server which must reject the connection. So this parameter has
meaning only if you try to connect to a PG10 server using a PG11
client (assuming that channel binding gets into PG11). If you connect
with a PG11 client to a PG11 server with SSL, the server publishes
SCRAM-PLUS, the client has to use it, hence this turns out to make
cbind=disable and prefer meaningless in the long-term. If the client
does not use SSL, then there is no channel binding, and cbind=require
loses its value. So cbind's fate is actually linked to sslmode.
That seems problematic. What if the client supports SCRAM but not
channel binding?
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Tue, Oct 3, 2017 at 1:30 AM, Robert Haas <robertmhaas@gmail.com> wrote:
On Fri, Sep 15, 2017 at 6:29 PM, Michael Paquier
<michael.paquier@gmail.com> wrote:I would like to point out that per the RFC, if the client attempts a
SSL connection with SCRAM and that the server supports channel
binding, then it has to publish the SASL mechanism for channel
binding, aka SCRAM-PLUS. If the client tries to force the use of SCRAM
even if SCRAM-PLUS is specified, this is seen as a downgrade attack by
the server which must reject the connection. So this parameter has
meaning only if you try to connect to a PG10 server using a PG11
client (assuming that channel binding gets into PG11). If you connect
with a PG11 client to a PG11 server with SSL, the server publishes
SCRAM-PLUS, the client has to use it, hence this turns out to make
cbind=disable and prefer meaningless in the long-term. If the client
does not use SSL, then there is no channel binding, and cbind=require
loses its value. So cbind's fate is actually linked to sslmode.That seems problematic. What if the client supports SCRAM but not
channel binding?
Peter has outlined here that my interpretation of the RFC was wrong on
the client side to begin with:
/messages/by-id/f74525e4-6c53-c653-6860-a8cc8d7c8ad9@2ndquadrant.com
If a client does not support channel binding (it is not compiled with
SSL or the connection is done without SSL), it should not send 'y' but
'n'. It should be up to the client to decide if it wants to use
channel binding or not. libpq is also going to need some extra logic
to send 'y' when it thinks that the server should have channel binding
support. This can be done by looking at the backend version.
--
Michael
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Tue, Sep 26, 2017 at 11:09 AM, Michael Paquier
<michael.paquier@gmail.com> wrote:
On Mon, Sep 25, 2017 at 11:22 PM, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:I think the channel-binding negotiation on the client side is wrong.
The logic in the patch is+#ifdef USE_SSL + if (state->ssl_in_use) + appendPQExpBuffer(&buf, "p=%s", SCRAM_CHANNEL_TLS_UNIQUE); + else + appendPQExpBuffer(&buf, "y"); +#else + appendPQExpBuffer(&buf, "n"); +#endifBut if SSL is compiled in but not used, the client does not in fact
support channel binding (for that connection), so it should send "n".For others, details about this flag are here:
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=".And channel binding description is here:
https://tools.ietf.org/html/rfc5802#section-6
Changed the patch to do that. Note that if the client enforces the
SASL mechanism to SCRAM-SHA-256-PLUS in a non-SSL context then libpq
complains. This can be done in libpq using pgsaslname.
The "y" flag should be sent if ssl_in_use but the client did not see the
server advertise SCRAM-SHA256-PLUS. That logic is missing entirely in
this patch.Okay. I think I get your point here. I agree that the client is
deficient here. This needs some more work.
Hm. I take back the argument that we can use the backend version here.
conn->sversion is only filled at the end of authentication. So the
best thing I think can be done here is to check if channel binding has
been advertised or not, and send "y" if the client thinks that the
server should do support it. If the client has chosen not to use
channel binding, by for example enforcing pgsaslname in libpq to
SCRAM-SHA-256, then "n" should be sent. I have changed the patch
accordingly, doing at the same time some refactoring in pg_SASL_init.
pg_fe_scram_init() gets initialized only when the mechanism is
selected.
You have the server reject a client that does not support channel
binding ("n") on all SSL connections. I don't think this is correct.
It is up to the client to use channel binding or not, even on SSL
connections.It seems that I got confused with the meaning of "y" mixed with
ssl_in_use. The server should reject "y" instead only if SSL is in
use.
Okay. In order to address that, what just needs to be done is to
remove the error message that I have added in the backend when "n" is
sent by the client. So this thing is ripped off. This way, when a v10
libpq connects to a v11 backend, the connection is able to work even
with SSL connections.
We should update pg_hba.conf to allow a method specification of
"scram-sha256-plus", i.e., only advertise the channel binding variant to
the client. Then you could make policy decisions like rejecting clients
that do not use channel binding on SSL connections. This could be a
separate patch later.OK, I agree that there could be some value in that. This complicates a
bit hba rule checks, but nothing really complicated either.
This would be useful, but I have let it for now to not complicate the
series more.
The error message in the "p" case if SSL is not in use is a bit
confusing: "but the server does not need it". I think this could be
left at the old message "but it is not supported". This ties into my
interpretation from above that whether channel binding is "supported"
depends on whether SSL is in use for a particular connection.Check.
Okay, I switched the error message to that (diff with previous patch series):
@@ -850,7 +850,7 @@ read_client_first_message(scram_state *state, char *input)
if (!state->ssl_in_use)
ereport(ERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION),
- errmsg("client supports SCRAM channel
binding, but server does not need it for non-SSL connections")));
+ errmsg("client requires SCRAM channel
binding, but it is not supported")));
Some small code things:
- prefer to use size_t over int for length (tls_finish_len etc.)
- tls_finish should be tls_finished
- typos: certificate_bash -> certificate_hashYes, thanks for spotting those.
Fixed.
In the patch for tls-server-end-point, I think the selection of the hash
function deviates slightly from the RFC. The RFC only says to
substitute MD5 and SHA-1. It doesn't say to turn SHA-224 into SHA-256,
for example. There is also the problem that the code as written will
turn any unrecognized hash method into SHA-256. If think the code
should single out MD5 and SHA-1 only and then use EVP_get_digestbynid()
for the rest. (I don't know anything about the details of OpenSSL APIs,
but that function sounded right to me.)Relevant bits from the RFC: https://tools.ietf.org/html/rfc5929#section-4.1
o if the certificate's signatureAlgorithm uses a single hash
function, and that hash function is either MD5 [RFC1321] or SHA-1
[RFC3174], then use SHA-256 [FIPS-180-3];
o if the certificate's signatureAlgorithm uses no hash functions or
uses multiple hash functions, then this channel binding type's
channel bindings are undefined at this time (updates to is channel
binding type may occur to address this issue if it ever arises).OK. Hm. I think that we need as well to check for the case where
EVP_get_digestbynid() returns NULL then and issue an ERROR on it. That
seems to be the second point outlined by the RFC I am quoting here.
This has been fixed. i have noticed on the way that the error messages
in the frontend when a certificate hash cannot be generated just
complain about an OOM. This is not always correct, so I have made
things more verbose with better error messages for each error code
path.
This patch got the feedback I was looking for, and this requires some
rework anyway. So I am marking the patch as returned with feedback for
now. This won't make it in time for this CF.
Attached is a new patch set with the comments from above. On top of
that, I have changed a couple of things:
- 0001 is unchanged, still the same refactoring for the SSL tests.
- 0002 implements tls-unique, now including tests using the default
channel binding tls-unique with something in the SSL test suite. This
patch also now introduces all the infrastructure to plug in correctly
new libpq parameters and more channel binding types.
- 0003 is shorter, and introduces a set of libpq parameters useful for
tests, taking advantage of 0002. Another case where the connection
parameter saslname is useful is to enforce not using channel binding
when connecting to a v10 server using a SSL context with a v11 libpq.
- 0004 introduces tls-server-end-point.
This has required some work to get it shaped as wanted, I am adding it
to the next CF, as version 2.
--
Michael
Attachments:
0001-Refactor-routine-to-test-connection-to-SSL-server.patchapplication/octet-stream; name=0001-Refactor-routine-to-test-connection-to-SSL-server.patchDownload
From b148bd1fdfee1cdc94ac936a381c3eb085e70821 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 16 Jun 2017 17:00:06 +0900
Subject: [PATCH 1/4] Refactor routine to test connection to SSL server
Move the sub-routines wrappers to check if a connection to a server is
fine or not into the test main module. This is useful for other tests
willing to check connectivity into a server.
---
src/test/ssl/ServerSetup.pm | 45 +++++++++++++-
src/test/ssl/t/001_ssltests.pl | 132 +++++++++++++++++------------------------
2 files changed, 100 insertions(+), 77 deletions(-)
diff --git a/src/test/ssl/ServerSetup.pm b/src/test/ssl/ServerSetup.pm
index f63c81cfc6..ad2e036602 100644
--- a/src/test/ssl/ServerSetup.pm
+++ b/src/test/ssl/ServerSetup.pm
@@ -26,9 +26,52 @@ use Test::More;
use Exporter 'import';
our @EXPORT = qw(
- configure_test_server_for_ssl switch_server_cert
+ configure_test_server_for_ssl
+ run_test_psql
+ switch_server_cert
+ test_connect_fails
+ test_connect_ok
);
+# Define a couple of helper functions to test connecting to the server.
+
+# Attempt connection to server with given connection string.
+sub run_test_psql
+{
+ my $connstr = $_[0];
+ my $logstring = $_[1];
+
+ my $cmd = [
+ 'psql', '-X', '-A', '-t', '-c', "SELECT 'connected with $connstr'",
+ '-d', "$connstr" ];
+
+ my $result = run_log($cmd);
+ return $result;
+}
+
+#
+# The first argument is a base connection string to use for connection.
+# The second argument is a complementary connection string, and it's also
+# printed out as the test case name.
+sub test_connect_ok
+{
+ my $common_connstr = $_[0];
+ my $connstr = $_[1];
+
+ my $result =
+ run_test_psql("$common_connstr $connstr", "(should succeed)");
+ ok($result, $connstr);
+}
+
+sub test_connect_fails
+{
+ my $common_connstr = $_[0];
+ my $connstr = $_[1];
+
+ my $result = run_test_psql("$common_connstr $connstr", "(should fail)");
+ ok(!$result, "$connstr (should fail)");
+}
+
# Copy a set of files, taking into account wildcards
sub copy_files
{
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 32df273929..890e3051a2 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -13,44 +13,9 @@ use File::Copy;
# postgresql-ssl-regression.test.
my $SERVERHOSTADDR = '127.0.0.1';
-# Define a couple of helper functions to test connecting to the server.
-
+# Allocation of base connection string shared among multiple tests.
my $common_connstr;
-sub run_test_psql
-{
- my $connstr = $_[0];
- my $logstring = $_[1];
-
- my $cmd = [
- 'psql', '-X', '-A', '-t', '-c', "SELECT 'connected with $connstr'",
- '-d', "$connstr" ];
-
- my $result = run_log($cmd);
- return $result;
-}
-
-#
-# The first argument is a (part of a) connection string, and it's also printed
-# out as the test case name. It is appended to $common_connstr global variable,
-# which also contains a libpq connection string.
-sub test_connect_ok
-{
- my $connstr = $_[0];
-
- my $result =
- run_test_psql("$common_connstr $connstr", "(should succeed)");
- ok($result, $connstr);
-}
-
-sub test_connect_fails
-{
- my $connstr = $_[0];
-
- my $result = run_test_psql("$common_connstr $connstr", "(should fail)");
- ok(!$result, "$connstr (should fail)");
-}
-
# The client's private key must not be world-readable, so take a copy
# of the key stored in the code tree and update its permissions.
copy("ssl/client.key", "ssl/client_tmp.key");
@@ -83,50 +48,59 @@ $common_connstr =
# The server should not accept non-SSL connections
note "test that the server doesn't accept non-SSL connections";
-test_connect_fails("sslmode=disable");
+test_connect_fails($common_connstr, "sslmode=disable");
# Try without a root cert. In sslmode=require, this should work. In verify-ca
# or verify-full mode it should fail
note "connect without server root cert";
-test_connect_ok("sslrootcert=invalid sslmode=require");
-test_connect_fails("sslrootcert=invalid sslmode=verify-ca");
-test_connect_fails("sslrootcert=invalid sslmode=verify-full");
+test_connect_ok($common_connstr, "sslrootcert=invalid sslmode=require");
+test_connect_fails($common_connstr, "sslrootcert=invalid sslmode=verify-ca");
+test_connect_fails($common_connstr, "sslrootcert=invalid sslmode=verify-full");
# Try with wrong root cert, should fail. (we're using the client CA as the
# root, but the server's key is signed by the server CA)
note "connect without wrong server root cert";
-test_connect_fails("sslrootcert=ssl/client_ca.crt sslmode=require");
-test_connect_fails("sslrootcert=ssl/client_ca.crt sslmode=verify-ca");
-test_connect_fails("sslrootcert=ssl/client_ca.crt sslmode=verify-full");
+test_connect_fails($common_connstr,
+ "sslrootcert=ssl/client_ca.crt sslmode=require");
+test_connect_fails($common_connstr,
+ "sslrootcert=ssl/client_ca.crt sslmode=verify-ca");
+test_connect_fails($common_connstr,
+ "sslrootcert=ssl/client_ca.crt sslmode=verify-full");
# Try with just the server CA's cert. This fails because the root file
# must contain the whole chain up to the root CA.
note "connect with server CA cert, without root CA";
-test_connect_fails("sslrootcert=ssl/server_ca.crt sslmode=verify-ca");
+test_connect_fails($common_connstr,
+ "sslrootcert=ssl/server_ca.crt sslmode=verify-ca");
# And finally, with the correct root cert.
note "connect with correct server CA cert file";
-test_connect_ok("sslrootcert=ssl/root+server_ca.crt sslmode=require");
-test_connect_ok("sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca");
-test_connect_ok("sslrootcert=ssl/root+server_ca.crt sslmode=verify-full");
+test_connect_ok($common_connstr,
+ "sslrootcert=ssl/root+server_ca.crt sslmode=require");
+test_connect_ok($common_connstr,
+ "sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca");
+test_connect_ok($common_connstr,
+ "sslrootcert=ssl/root+server_ca.crt sslmode=verify-full");
# Test with cert root file that contains two certificates. The client should
# be able to pick the right one, regardless of the order in the file.
-test_connect_ok("sslrootcert=ssl/both-cas-1.crt sslmode=verify-ca");
-test_connect_ok("sslrootcert=ssl/both-cas-2.crt sslmode=verify-ca");
+test_connect_ok($common_connstr,
+ "sslrootcert=ssl/both-cas-1.crt sslmode=verify-ca");
+test_connect_ok($common_connstr,
+ "sslrootcert=ssl/both-cas-2.crt sslmode=verify-ca");
note "testing sslcrl option with a non-revoked cert";
# Invalid CRL filename is the same as no CRL, succeeds
-test_connect_ok(
+test_connect_ok($common_connstr,
"sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrl=invalid");
# A CRL belonging to a different CA is not accepted, fails
-test_connect_fails(
+test_connect_fails($common_connstr,
"sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrl=ssl/client.crl");
# With the correct CRL, succeeds (this cert is not revoked)
-test_connect_ok(
+test_connect_ok($common_connstr,
"sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrl=ssl/root+server.crl"
);
@@ -136,9 +110,9 @@ note "test mismatch between hostname and server certificate";
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
-test_connect_ok("sslmode=require host=wronghost.test");
-test_connect_ok("sslmode=verify-ca host=wronghost.test");
-test_connect_fails("sslmode=verify-full host=wronghost.test");
+test_connect_ok($common_connstr, "sslmode=require host=wronghost.test");
+test_connect_ok($common_connstr, "sslmode=verify-ca host=wronghost.test");
+test_connect_fails($common_connstr, "sslmode=verify-full host=wronghost.test");
# Test Subject Alternative Names.
switch_server_cert($node, 'server-multiple-alt-names');
@@ -147,12 +121,13 @@ note "test hostname matching with X.509 Subject Alternative Names";
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
-test_connect_ok("host=dns1.alt-name.pg-ssltest.test");
-test_connect_ok("host=dns2.alt-name.pg-ssltest.test");
-test_connect_ok("host=foo.wildcard.pg-ssltest.test");
+test_connect_ok($common_connstr, "host=dns1.alt-name.pg-ssltest.test");
+test_connect_ok($common_connstr, "host=dns2.alt-name.pg-ssltest.test");
+test_connect_ok($common_connstr, "host=foo.wildcard.pg-ssltest.test");
-test_connect_fails("host=wronghost.alt-name.pg-ssltest.test");
-test_connect_fails("host=deep.subdomain.wildcard.pg-ssltest.test");
+test_connect_fails($common_connstr, "host=wronghost.alt-name.pg-ssltest.test");
+test_connect_fails($common_connstr,
+ "host=deep.subdomain.wildcard.pg-ssltest.test");
# Test certificate with a single Subject Alternative Name. (this gives a
# slightly different error message, that's all)
@@ -162,10 +137,11 @@ note "test hostname matching with a single X.509 Subject Alternative Name";
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
-test_connect_ok("host=single.alt-name.pg-ssltest.test");
+test_connect_ok($common_connstr, "host=single.alt-name.pg-ssltest.test");
-test_connect_fails("host=wronghost.alt-name.pg-ssltest.test");
-test_connect_fails("host=deep.subdomain.wildcard.pg-ssltest.test");
+test_connect_fails($common_connstr, "host=wronghost.alt-name.pg-ssltest.test");
+test_connect_fails($common_connstr,
+ "host=deep.subdomain.wildcard.pg-ssltest.test");
# Test server certificate with a CN and SANs. Per RFCs 2818 and 6125, the CN
# should be ignored when the certificate has both.
@@ -175,9 +151,9 @@ note "test certificate with both a CN and SANs";
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
-test_connect_ok("host=dns1.alt-name.pg-ssltest.test");
-test_connect_ok("host=dns2.alt-name.pg-ssltest.test");
-test_connect_fails("host=common-name.pg-ssltest.test");
+test_connect_ok($common_connstr, "host=dns1.alt-name.pg-ssltest.test");
+test_connect_ok($common_connstr, "host=dns2.alt-name.pg-ssltest.test");
+test_connect_fails($common_connstr, "host=common-name.pg-ssltest.test");
# Finally, test a server certificate that has no CN or SANs. Of course, that's
# not a very sensible certificate, but libpq should handle it gracefully.
@@ -185,8 +161,10 @@ switch_server_cert($node, 'server-no-names');
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR";
-test_connect_ok("sslmode=verify-ca host=common-name.pg-ssltest.test");
-test_connect_fails("sslmode=verify-full host=common-name.pg-ssltest.test");
+test_connect_ok($common_connstr,
+ "sslmode=verify-ca host=common-name.pg-ssltest.test");
+test_connect_fails($common_connstr,
+ "sslmode=verify-full host=common-name.pg-ssltest.test");
# Test that the CRL works
note "testing client-side CRL";
@@ -196,8 +174,9 @@ $common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid hostaddr=$SERVERHOSTADDR host=common-name.pg-ssltest.test";
# Without the CRL, succeeds. With it, fails.
-test_connect_ok("sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca");
-test_connect_fails(
+test_connect_ok($common_connstr,
+ "sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca");
+test_connect_fails($common_connstr,
"sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrl=ssl/root+server.crl"
);
@@ -210,18 +189,18 @@ $common_connstr =
"sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=certdb hostaddr=$SERVERHOSTADDR";
# no client cert
-test_connect_fails("user=ssltestuser sslcert=invalid");
+test_connect_fails($common_connstr, "user=ssltestuser sslcert=invalid");
# correct client cert
-test_connect_ok(
+test_connect_ok($common_connstr,
"user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key");
# client cert belonging to another user
-test_connect_fails(
+test_connect_fails($common_connstr,
"user=anotheruser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key");
# revoked client cert
-test_connect_fails(
+test_connect_fails($common_connstr,
"user=ssltestuser sslcert=ssl/client-revoked.crt sslkey=ssl/client-revoked.key"
);
@@ -230,8 +209,9 @@ switch_server_cert($node, 'server-cn-only', 'root_ca');
$common_connstr =
"user=ssltestuser dbname=certdb sslkey=ssl/client_tmp.key sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR";
-test_connect_ok("sslmode=require sslcert=ssl/client+client_ca.crt");
-test_connect_fails("sslmode=require sslcert=ssl/client.crt");
+test_connect_ok($common_connstr,
+ "sslmode=require sslcert=ssl/client+client_ca.crt");
+test_connect_fails($common_connstr, "sslmode=require sslcert=ssl/client.crt");
# clean up
unlink "ssl/client_tmp.key";
--
2.14.2
0002-Support-channel-binding-tls-unique-in-SCRAM.patchapplication/octet-stream; name=0002-Support-channel-binding-tls-unique-in-SCRAM.patchDownload
From bb80b82d16597f725b4dc79d6b539715b27a2cf0 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 10 Oct 2017 21:51:18 +0900
Subject: [PATCH 2/4] 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 finish messages, a PG-like API is added to be able to
make the interface pluggable for other SSL implementations.
This commit also adds a lot of basic 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.
A set of basic tests to stress default channel binding handling is
added as well, though those are part of the SSL suite.
---
doc/src/sgml/protocol.sgml | 25 +++--
src/backend/libpq/auth-scram.c | 162 +++++++++++++++++++++-----
src/backend/libpq/auth.c | 69 ++++++++++--
src/backend/libpq/be-secure-openssl.c | 24 ++++
src/include/libpq/libpq-be.h | 1 +
src/include/libpq/scram.h | 10 +-
src/interfaces/libpq/fe-auth-scram.c | 187 +++++++++++++++++++++++++++----
src/interfaces/libpq/fe-auth.c | 100 +++++++++++++----
src/interfaces/libpq/fe-auth.h | 5 +-
src/interfaces/libpq/fe-secure-openssl.c | 26 +++++
src/interfaces/libpq/libpq-int.h | 5 +-
src/test/ssl/ServerSetup.pm | 19 +++-
src/test/ssl/t/001_ssltests.pl | 2 +-
src/test/ssl/t/002_sasl.pl | 41 +++++++
14 files changed, 584 insertions(+), 92 deletions(-)
create mode 100644 src/test/ssl/t/002_sasl.pl
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 526e8011de..3ba8575fba 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1461,10 +1461,11 @@ SELCT 1/0;
<para>
<firstterm>SASL</> is a framework for authentication in connection-oriented
-protocols. At the moment, <productname>PostgreSQL</> implements only one SASL
-authentication mechanism, SCRAM-SHA-256, but more might be added in the
-future. The below steps illustrate how SASL authentication is performed in
-general, while the next subsection gives more details on SCRAM-SHA-256.
+protocols. At the moment, <productname>PostgreSQL</> implements only two SASL
+authentication mechanisms, SCRAM-SHA-256 and SCRAM-SHA-256-PLUS, but more
+might be added in the future. The below steps illustrate how SASL
+authentication is performed in general, while the next subsection gives
+more details on SCRAM-SHA-256 and SCRAM-SHA-256-PLUS.
</para>
<procedure>
@@ -1547,7 +1548,10 @@ the password is in.
</para>
<para>
-<firstterm>Channel binding</> has not been implemented yet.
+<firstterm>Channel binding</> is supported in builds with SSL support, and
+uses as mechanism name <literal>SCRAM-SHA-256-PLUS</> for this purpose as
+defined per IANA. The only channel binding type supported by the server
+is <literal>tls-unique</>.
</para>
<procedure>
@@ -1556,14 +1560,19 @@ the password is in.
<para>
The server sends an AuthenticationSASL message. It includes a list of
SASL authentication mechanisms that the server can accept.
+ <literal>SCRAM-SHA-256</> and <literal>SCRAM-SHA-256-PLUS</> are the
+ two mechanism names that the server lists in this message. Support for
+ channel binding is not included if the server is built without SSL
+ support. The client can still choose to not use channel binding for
+ a connection attempt with SSL.
</para>
</step>
<step id="scram-client-first">
<para>
The client responds by sending a SASLInitialResponse message, which
- indicates the chosen mechanism, <literal>SCRAM-SHA-256</>. In the Initial
- Client response field, the message contains the SCRAM
- <structname>client-first-message</>.
+ indicates the chosen mechanism, <literal>SCRAM-SHA-256</> or
+ <literal>SCRAM-SHA-256-PLUS</>. In the Initial Client response field,
+ the message contains the SCRAM <structname>client-first-message</>.
</para>
</step>
<step id="scram-server-first">
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index 9161c885e1..2efc198459 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;
+ char *tls_finished_message;
+ int tls_finished_len;
+ char *channel_binding;
+
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,
+ char *tls_finished_message,
+ int 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 = NULL;
/*
* Parse the stored password verifier.
@@ -773,31 +784,84 @@ read_client_first_message(scram_state *state, char *input)
*------
*/
- /* read gs2-cbind-flag */
+ /*
+ * Read gs2-cbind-flag. Server handles its value as described in RFC 5802,
+ * section 6 dealing with 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 this case just let 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 */
+ /*
+ * 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 is only the case if a connection
+ * is done when SSL is used.
+ */
+ if (state->ssl_in_use)
+ ereport(ERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("client supports SCRAM channel binding, but server needs it for SSL connections")));
+ 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':
+ {
+#ifdef USE_SSL
+ char *channel_name;
- /*
- * 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")));
+ if (!state->ssl_in_use)
+ 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.
+ */
+ channel_name = read_attr_value(&input, 'p');
+ if (strcmp(channel_name, SCRAM_CHANNEL_TLS_UNIQUE) != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ (errmsg("unexpected SCRAM channel-binding type"))));
+
+ /* Save the name for handling of subsequent messages */
+ state->channel_binding = pstrdup(channel_name);
+#else
+ /*
+ * 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")));
+#endif
+ }
+ break;
default:
ereport(ERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION),
@@ -805,13 +869,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 +1089,65 @@ 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. We don't support channel binding for builds
+ * without SSL support or if the client does not want to use channel
+ * binding, so it's expected to always be "biws" in this case, which
+ * is "n,,", base64-encoded. In builds supporting SSL, "biws" is
+ * used for Non-SSL connection attempts, and for SSL connections the
+ * client has to provide channel binding value if needed.
*/
channel_binding = read_attr_value(&p, 'c');
+#ifdef USE_SSL
+ if (state->ssl_in_use &&
+ state->channel_binding)
+ {
+ char *b64_message, *raw_data;
+ int b64_message_len, raw_data_len;
+
+ /* Fetch data for each channel binding type */
+ if (strcmp(state->channel_binding, SCRAM_CHANNEL_TLS_UNIQUE) == 0)
+ {
+ raw_data = state->tls_finished_message;
+ raw_data_len = state->tls_finished_len;
+ }
+ else
+ {
+ /* should not happen */
+ elog(ERROR, "invalid channel binding type");
+ }
+
+ /* should not happen, but better safe than sorry */
+ if (raw_data == NULL)
+ elog(ERROR, "empty binding data for channel name \"%s\"",
+ state->channel_binding);
+
+ b64_message = palloc(pg_b64_enc_len(raw_data_len) + 1);
+ b64_message_len = pg_b64_encode(raw_data, raw_data_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_PROTOCOL_VIOLATION),
+ (errmsg("no match for SCRAM channel-binding attribute in client-final-message"))));
+ }
+ else
+ {
+ if (strcmp(channel_binding, "biws") != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ (errmsg("unexpected SCRAM channel-binding attribute in client-final-message"))));
+ }
+#else
if (strcmp(channel_binding, "biws") != 0)
ereport(ERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION),
(errmsg("unexpected SCRAM channel-binding attribute in client-final-message"))));
+#endif
state->client_final_nonce = read_attr_value(&p, 'r');
/* ignore optional extensions */
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 480e344eb3..5bceae2162 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -859,10 +859,14 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
void *scram_opaq;
char *output = NULL;
int outputlen = 0;
- char *input;
+ char *input, *p;
+ char *sasl_mechs;
int inputlen;
+ int listlen = 0;
int result;
bool initial;
+ char *tls_finished = NULL;
+ int tls_finished_len = 0;
/*
* SASL auth is not supported for protocol versions before 3, because it
@@ -879,12 +883,49 @@ 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:
+ * - SCRAM-SHA-256, which is the mechanism with the same name.
+ * - SCRAM-SHA-256-PLUS, which is SCRAM-SHA-256 with channel binding. This
+ * is advertised to the client only if connection is attempted with SSL.
+ * The order of mechanisms is advertised in decreasing order of importance.
+ * The extra "\0" is for an empty string to terminate the list, and each
+ * mechanism listed needs to be separated with "\0".
*/
- sendAuthRequest(port, AUTH_REQ_SASL, SCRAM_SHA256_NAME "\0",
- strlen(SCRAM_SHA256_NAME) + 2);
+ listlen = 0;
+ sasl_mechs = (char *) palloc(strlen(SCRAM_SHA256_PLUS_NAME) +
+ strlen(SCRAM_SHA256_NAME) + 3);
+ p = sasl_mechs;
+
+ /* add SCRAM-SHA-256-PLUS, which depends on if SSL is in use */
+ if (port->ssl_in_use)
+ {
+ strcpy(p, SCRAM_SHA256_PLUS_NAME);
+ listlen += strlen(SCRAM_SHA256_PLUS_NAME) + 1;
+ p += strlen(SCRAM_SHA256_PLUS_NAME) + 1;
+ }
+
+ /* add generic SCRAM-SHA-256 */
+ strcpy(p, SCRAM_SHA256_NAME);
+ listlen += strlen(SCRAM_SHA256_NAME) + 1;
+ p += strlen(SCRAM_SHA256_NAME) + 1;
+
+ /* put "\0" to mark that list is finished */
+ p[0] = '\0';
+ listlen++;
+
+ sendAuthRequest(port, AUTH_REQ_SASL, sasl_mechs, listlen);
+ pfree(sasl_mechs);
+
+#ifdef USE_SSL
+ /*
+ * Fetch the data related to the SSL finish message to be used in the
+ * exchange.
+ */
+ if (port->ssl_in_use)
+ {
+ tls_finished = be_tls_get_peer_finish(port, &tls_finished_len);
+ }
+#endif
/*
* Initialize the status tracker for message exchanges.
@@ -897,7 +938,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
@@ -946,15 +991,17 @@ 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.
+ * We only support SCRAM-SHA-256 and SCRAM-SHA-256-PLUS 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),
- errmsg("client selected an invalid SASL authentication mechanism")));
+ errmsg("client selected an invalid SASL authentication mechanism"),
+ errdetail("Incorrect SASL mechanism name specified")));
}
inputlen = pq_getmsgint(&buf, 4);
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index fe15227a77..d063a587d0 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -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 finish message information from the
+ * client, useful for authorization when doing channel binding.
+ *
+ * Result is a palloc'd copy of the TLS finish message with its size.
+ */
+char *
+be_tls_get_peer_finish(Port *port, int *len)
+{
+ char dummy[1];
+ char *result;
+
+ /*
+ * OpenSSL does not offer an API to get directly the length of the
+ * expected TLS finish 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 = (char *) palloc(*len * sizeof(char));
+ (void) SSL_get_peer_finished(port->ssl, result, *len);
+
+ return result;
+}
+
/*
* Convert an X509 subject name to a cstring.
*
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 7bde744d51..5d59d79822 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -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_cipher(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_finish(Port *port, int *len);
#endif
extern ProtocolVersion FrontendProtocol;
diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h
index 0166e1945d..43cde0e46e 100644
--- a/src/include/libpq/scram.h
+++ b/src/include/libpq/scram.h
@@ -13,8 +13,12 @@
#ifndef 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_PLUS_NAME "SCRAM-SHA-256-PLUS" /* with channel binding */
+
+/* Channel binding names */
+#define SCRAM_CHANNEL_TLS_UNIQUE "tls-unique"
/* Status codes for message exchange */
#define SASL_EXCHANGE_CONTINUE 0
@@ -22,7 +26,9 @@
#define SASL_EXCHANGE_FAILURE 2
/* 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, char *tls_finished_message,
+ int tls_finished_len);
extern int pg_be_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen, char **logdetail);
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index edfd42df85..9bce4d980a 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -17,6 +17,7 @@
#include "common/base64.h"
#include "common/saslprep.h"
#include "common/scram-common.h"
+#include "libpq/scram.h"
#include "fe-auth.h"
/* These are needed for getpid(), in the fallback implementation */
@@ -44,6 +45,13 @@ typedef struct
/* These are supplied by the user */
const char *username;
char *password;
+ bool ssl_in_use;
+ bool channel_binding_advertised;
+ char *tls_finished_message;
+ int tls_finished_len;
+ /* enforce-able user parameters */
+ char *saslmechanism; /* name of mechanism used */
+ char *saslchannelbinding; /* name of channel binding to use */
/* We construct these */
uint8 SaltedPassword[SCRAM_KEY_LEN];
@@ -81,18 +89,41 @@ static bool pg_frontend_random(char *dst, int len);
* Initialize SCRAM exchange status.
*/
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,
+ bool channel_binding_advertised,
+ const char *saslmechanism,
+ char *saslchannelbinding,
+ char *tls_finished_message,
+ int tls_finished_len)
{
fe_scram_state *state;
char *prep_password;
pg_saslprep_rc rc;
+ Assert(saslmechanism != NULL);
+
state = (fe_scram_state *) malloc(sizeof(fe_scram_state));
if (!state)
return NULL;
memset(state, 0, sizeof(fe_scram_state));
state->state = FE_SCRAM_INIT;
state->username = username;
+ state->ssl_in_use = ssl_in_use;
+ state->channel_binding_advertised = channel_binding_advertised;
+ state->tls_finished_message = tls_finished_message;
+ state->tls_finished_len = tls_finished_len;
+ state->saslmechanism = strdup(saslmechanism);
+
+ /*
+ * If user has specified a channel binding to use, enforce the
+ * channel binding sent to it. The default is "tls-unique".
+ */
+ if (saslchannelbinding && strlen(saslchannelbinding) > 0)
+ state->saslchannelbinding = strdup(saslchannelbinding);
+ else
+ state->saslchannelbinding = strdup(SCRAM_CHANNEL_TLS_UNIQUE);
/* Normalize the password with SASLprep, if possible */
rc = pg_saslprep(password, &prep_password);
@@ -125,6 +156,12 @@ pg_fe_scram_free(void *opaq)
if (state->password)
free(state->password);
+ if (state->tls_finished_message)
+ free(state->tls_finished_message);
+ if (state->saslmechanism)
+ free(state->saslmechanism);
+ if (state->saslchannelbinding)
+ free(state->saslchannelbinding);
/* client messages */
if (state->client_nonce)
@@ -297,9 +334,10 @@ static char *
build_client_first_message(fe_scram_state *state, PQExpBuffer errormessage)
{
char raw_nonce[SCRAM_RAW_NONCE_LEN + 1];
- char *buf;
- char buflen;
+ char *result;
+ int channel_info_len;
int encoded_len;
+ PQExpBufferData buf;
/*
* Generate a "raw" nonce. This is converted to ASCII-printable form by
@@ -328,26 +366,88 @@ build_client_first_message(fe_scram_state *state, PQExpBuffer errormessage)
* prepared with SASLprep, the message parsing would fail if it includes
* '=' or ',' characters.
*/
- buflen = 8 + strlen(state->client_nonce) + 1;
- buf = malloc(buflen);
- if (buf == NULL)
+ initPQExpBuffer(&buf);
+
+ /*
+ * First build the query field for channel binding. Conditions used for
+ * the decision-making are described in more details below.
+ */
+#ifdef USE_SSL
+ if (strcmp(state->saslmechanism, SCRAM_SHA256_PLUS_NAME) == 0)
{
- printfPQExpBuffer(errormessage,
- libpq_gettext("out of memory\n"));
- return NULL;
- }
- snprintf(buf, buflen, "n,,n=,r=%s", state->client_nonce);
+ if (!state->ssl_in_use)
+ {
+ /*
+ * Attempting to use channel binding, but this is not supported
+ * in non-SSL contexts, so complain.
+ */
+ printfPQExpBuffer(errormessage,
+ libpq_gettext("channel binding not supported in non-SSL context.\n"));
+ return NULL;
+ }
- state->client_first_message_bare = strdup(buf + 3);
- if (!state->client_first_message_bare)
+ if (state->channel_binding_advertised)
+ {
+ /*
+ * Channel binding is wanted by client and server supports it
+ * so allow its use.
+ */
+ appendPQExpBuffer(&buf, "p=%s", state->saslchannelbinding);
+ }
+ else
+ {
+ /*
+ * Client supports channel binding and expects the server to
+ * do so as well, so let the server know about that. Note that
+ * as authentication is not complete yet, we do not know the
+ * backend version, so there is not much we can do about it.
+ */
+ appendPQExpBuffer(&buf, "y");
+ return NULL;
+ }
+ }
+ else
{
- free(buf);
- printfPQExpBuffer(errormessage,
- libpq_gettext("out of memory\n"));
- return NULL;
+ /*
+ * Caller may not wish to use channel binding, so let server know
+ * that.
+ */
+ appendPQExpBuffer(&buf, "n");
}
+#else
+ /* Build does not support SSL, so it does not support channel binding */
+ appendPQExpBuffer(&buf, "n");
+#endif
+
+ if (PQExpBufferDataBroken(buf))
+ goto oom_error;
- return buf;
+ 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)
+ goto oom_error;
+
+ result = strdup(buf.data);
+ if (result == NULL)
+ goto oom_error;
+
+ termPQExpBuffer(&buf);
+ return result;
+
+oom_error:
+ termPQExpBuffer(&buf);
+ printfPQExpBuffer(errormessage,
+ libpq_gettext("out of memory\n"));
+ return NULL;
}
/*
@@ -365,8 +465,57 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
/*
* Construct client-final-message-without-proof. We need to remember it
* for verifying the server proof in the final step of authentication.
+ * Client needs to provide a b64 encoded string of the TLS finish message
+ * only if a SSL connection is attempted.
*/
- appendPQExpBuffer(&buf, "c=biws,r=%s", state->nonce);
+#ifdef USE_SSL
+ if (strcmp(state->saslmechanism, SCRAM_SHA256_PLUS_NAME) == 0)
+ {
+ char *raw_data;
+ int raw_data_len;
+
+ if (strcmp(state->saslchannelbinding, SCRAM_CHANNEL_TLS_UNIQUE) == 0)
+ {
+ raw_data = state->tls_finished_message;
+ raw_data_len = state->tls_finished_len;
+ }
+ else
+ {
+ /* should not happen */
+ termPQExpBuffer(&buf);
+ printfPQExpBuffer(errormessage,
+ libpq_gettext("incorrect channel binding name\n"));
+ return NULL;
+ }
+
+ /* should not happen, but better safe than sorry */
+ if (raw_data == NULL)
+ {
+ /* should not happen */
+ termPQExpBuffer(&buf);
+ printfPQExpBuffer(errormessage,
+ libpq_gettext("empty binding data for channel name \"%s\"\n"),
+ state->saslchannelbinding);
+ return NULL;
+ }
+
+ appendPQExpBuffer(&buf, "c=");
+
+ if (!enlargePQExpBuffer(&buf, pg_b64_enc_len(raw_data_len)))
+ goto oom_error;
+ buf.len += pg_b64_encode(raw_data, raw_data_len, buf.data + buf.len);
+ buf.data[buf.len] = '\0';
+ }
+ else
+ appendPQExpBuffer(&buf, "c=biws");
+#else
+ appendPQExpBuffer(&buf, "c=biws");
+#endif
+
+ if (PQExpBufferDataBroken(buf))
+ goto oom_error;
+
+ appendPQExpBuffer(&buf, ",r=%s", state->nonce);
if (PQExpBufferDataBroken(buf))
goto oom_error;
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 382558f3f8..fa24c88522 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -491,6 +491,10 @@ pg_SASL_init(PGconn *conn, int payloadlen)
bool success;
const char *selected_mechanism;
PQExpBufferData mechanism_buf;
+ bool channel_binding_advertised = false;
+ char *tls_finished = NULL;
+ int tls_finished_len = 0;
+ char *password;
initPQExpBuffer(&mechanism_buf);
@@ -504,7 +508,8 @@ pg_SASL_init(PGconn *conn, int payloadlen)
/*
* Parse the list of SASL authentication mechanisms in the
* 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;
for (;;)
@@ -522,6 +527,15 @@ pg_SASL_init(PGconn *conn, int payloadlen)
if (mechanism_buf.data[0] == '\0')
break;
+ /*
+ * The SASL exchange needs to know if channel binding has been
+ * advertized. This prevents attacks from rogue servers that
+ * may have the correct version but are not telling clients that
+ * about channel binding.
+ */
+ if (strcmp(mechanism_buf.data, SCRAM_SHA256_PLUS_NAME) == 0)
+ channel_binding_advertised = true;
+
/*
* If we have already selected a mechanism, just skip through the rest
* of the list.
@@ -532,25 +546,26 @@ pg_SASL_init(PGconn *conn, int payloadlen)
/*
* Do we support this mechanism?
*/
- if (strcmp(mechanism_buf.data, SCRAM_SHA256_NAME) == 0)
+ if (strcmp(mechanism_buf.data, SCRAM_SHA256_NAME) == 0 ||
+ strcmp(mechanism_buf.data, SCRAM_SHA256_PLUS_NAME) == 0)
{
- char *password;
-
- conn->password_needed = true;
- password = conn->connhost[conn->whichhost].password;
- if (password == NULL)
- password = conn->pgpass;
- if (password == NULL || password[0] == '\0')
- {
- printfPQExpBuffer(&conn->errorMessage,
- PQnoPasswordSupplied);
- goto error;
- }
-
- conn->sasl_state = pg_fe_scram_init(conn->pguser, password);
- if (!conn->sasl_state)
- goto oom_error;
- selected_mechanism = SCRAM_SHA256_NAME;
+ /*
+ * Select the mechanism to use by default. If SSL connection
+ * is attempted, the server will expect the -PLUS mechanism.
+ * If not, fallback to SCRAM-SHA-256.
+ */
+#ifdef USE_SSL
+ if (conn->ssl_in_use &&
+ strcmp(mechanism_buf.data, SCRAM_SHA256_PLUS_NAME) == 0)
+ selected_mechanism = SCRAM_SHA256_PLUS_NAME;
+ else if (!conn->ssl_in_use &&
+ strcmp(mechanism_buf.data, SCRAM_SHA256_NAME) == 0)
+ selected_mechanism = SCRAM_SHA256_NAME;
+#else
+ /* No channel binding can be selected without SSL support */
+ if (strcmp(mechanism_buf.data, SCRAM_SHA256_NAME) == 0)
+ selected_mechanism = SCRAM_SHA256_NAME;
+#endif
}
}
@@ -561,6 +576,53 @@ pg_SASL_init(PGconn *conn, int payloadlen)
goto error;
}
+ /*
+ * Now that the SASL mechanism has been chosen for the exchange,
+ * initialize its state information. First select the password to
+ * use for the exchange protocol, complaining back if none are
+ * provided.
+ */
+ conn->password_needed = true;
+ password = conn->connhost[conn->whichhost].password;
+ if (password == NULL)
+ password = conn->pgpass;
+ if (password == NULL || password[0] == '\0')
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ PQnoPasswordSupplied);
+ goto error;
+ }
+
+#ifdef USE_SSL
+ /*
+ * Fetch information about the TLS finish message and client
+ * certificate if any.
+ */
+ if (conn->ssl_in_use)
+ {
+ tls_finished = pgtls_get_finish(conn, &tls_finished_len);
+ if (tls_finished == NULL)
+ goto oom_error;
+ }
+#endif
+
+ /*
+ * Initialize the SASL state information with all the information
+ * gathered during the initial exchange.
+ *
+ * Note: Only tls-unique is supported for the moment.
+ */
+ conn->sasl_state = pg_fe_scram_init(conn->pguser,
+ password,
+ conn->ssl_in_use,
+ channel_binding_advertised,
+ selected_mechanism,
+ NULL, /* default channel binding */
+ tls_finished,
+ tls_finished_len);
+ if (!conn->sasl_state)
+ goto oom_error;
+
/* Get the mechanism-specific Initial Client Response, if any */
pg_fe_scram_exchange(conn->sasl_state,
NULL, -1,
diff --git a/src/interfaces/libpq/fe-auth.h b/src/interfaces/libpq/fe-auth.h
index 5dc6bb5341..81378d0008 100644
--- a/src/interfaces/libpq/fe-auth.h
+++ b/src/interfaces/libpq/fe-auth.h
@@ -23,7 +23,10 @@ extern int pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn);
extern char *pg_fe_getauthname(PQExpBuffer errorMessage);
/* 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, bool channel_binding_advertised,
+ const char *saslmechanism,char *saslchannelbinding,
+ char *tls_finished_message, int tls_finished_len);
extern void pg_fe_scram_free(void *opaq);
extern void pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen,
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 2f29820e82..84a6e3c322 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -393,6 +393,32 @@ pgtls_write(PGconn *conn, const void *ptr, size_t len)
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_finish(PGconn *conn, int *len)
+{
+ char dummy[1];
+ char *result;
+
+ /*
+ * OpenSSL does not offer an API to get directly the length of the
+ * TLS finish 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 */
/* ------------------------------------------------------------ */
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 42913604e3..0eb8b60c95 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -453,11 +453,13 @@ struct pg_conn
/* Assorted state for SASL, SSL, GSS, etc */
void *sasl_state;
+ /* SSL structures */
+ bool ssl_in_use;
+
#ifdef USE_SSL
bool allow_ssl_try; /* Allowed to try SSL negotiation */
bool wait_ssl_try; /* Delay SSL negotiation until after
* attempting normal connection */
- bool ssl_in_use;
#ifdef USE_OPENSSL
SSL *ssl; /* SSL status, if have SSL connection */
X509 *peer; /* X509 cert of server */
@@ -668,6 +670,7 @@ extern void pgtls_close(PGconn *conn);
extern ssize_t pgtls_read(PGconn *conn, void *ptr, size_t len);
extern bool pgtls_read_pending(PGconn *conn);
extern ssize_t pgtls_write(PGconn *conn, const void *ptr, size_t len);
+extern char *pgtls_get_finish(PGconn *conn, int *len);
/*
* this is so that we can check if a connection is non-blocking internally
diff --git a/src/test/ssl/ServerSetup.pm b/src/test/ssl/ServerSetup.pm
index ad2e036602..b71969ac75 100644
--- a/src/test/ssl/ServerSetup.pm
+++ b/src/test/ssl/ServerSetup.pm
@@ -91,6 +91,9 @@ sub configure_test_server_for_ssl
{
my $node = $_[0];
my $serverhost = $_[1];
+ my $sslmethod = $_[2];
+ my $passwd = $_[3];
+ my $passwdhash = $_[4];
my $pgdata = $node->data_dir;
@@ -100,6 +103,15 @@ sub configure_test_server_for_ssl
$node->psql('postgres', "CREATE DATABASE trustdb");
$node->psql('postgres', "CREATE DATABASE certdb");
+ # Update password of each user as needed.
+ if (defined($passwd))
+ {
+ $node->psql('postgres',
+"SET password_encryption='$passwdhash'; ALTER USER ssltestuser PASSWORD '$passwd';");
+ $node->psql('postgres',
+"SET password_encryption='$passwdhash'; ALTER USER anotheruser PASSWORD '$passwd';");
+ }
+
# enable logging etc.
open my $conf, '>>', "$pgdata/postgresql.conf";
print $conf "fsync=off\n";
@@ -129,7 +141,7 @@ sub configure_test_server_for_ssl
$node->restart;
# Change pg_hba after restart because hostssl requires ssl=on
- configure_hba_for_ssl($node, $serverhost);
+ configure_hba_for_ssl($node, $serverhost, $sslmethod);
}
# Change the configuration to use given server cert file, and reload
@@ -159,6 +171,7 @@ sub configure_hba_for_ssl
{
my $node = $_[0];
my $serverhost = $_[1];
+ my $sslmethod = $_[2];
my $pgdata = $node->data_dir;
# Only accept SSL connections from localhost. Our tests don't depend on this
@@ -169,9 +182,9 @@ sub configure_hba_for_ssl
print $hba
"# TYPE DATABASE USER ADDRESS METHOD\n";
print $hba
-"hostssl trustdb ssltestuser $serverhost/32 trust\n";
+"hostssl trustdb ssltestuser $serverhost/32 $sslmethod\n";
print $hba
-"hostssl trustdb ssltestuser ::1/128 trust\n";
+"hostssl trustdb ssltestuser ::1/128 $sslmethod\n";
print $hba
"hostssl certdb ssltestuser $serverhost/32 cert\n";
print $hba
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 890e3051a2..e690c1fa15 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -32,7 +32,7 @@ $node->init;
$ENV{PGHOST} = $node->host;
$ENV{PGPORT} = $node->port;
$node->start;
-configure_test_server_for_ssl($node, $SERVERHOSTADDR);
+configure_test_server_for_ssl($node, $SERVERHOSTADDR, "trust");
switch_server_cert($node, 'server-cn-only');
### Part 1. Run client-side tests.
diff --git a/src/test/ssl/t/002_sasl.pl b/src/test/ssl/t/002_sasl.pl
new file mode 100644
index 0000000000..d43a970b55
--- /dev/null
+++ b/src/test/ssl/t/002_sasl.pl
@@ -0,0 +1,41 @@
+use strict;
+use warnings;
+use PostgresNode;
+use TestLib;
+use Test::More tests => 1;
+use ServerSetup;
+use File::Copy;
+
+# test combinations of SASL authentication for SCRAM mechanism:
+# - SCRAM-SHA-256 and SCRAM-SHA-256-PLUS
+# - Channel bindings
+
+# 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;
+
+#### Part 0. 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";
+
+# Tests with default channel binding and SASL mechanism names.
+# tls-unique is used here with channel binding.
+test_connect_ok($common_connstr, "");
--
2.14.2
0003-Add-connection-parameters-saslname-and-saslchannelbi.patchapplication/octet-stream; name=0003-Add-connection-parameters-saslname-and-saslchannelbi.patchDownload
From d9214f8a63608369bc1e70b8468799dcc8ab718a Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 10 Oct 2017 21:49:37 +0900
Subject: [PATCH 3/4] Add connection parameters "saslname" and
"saslchannelbinding"
Those parameters can be used to respectively enforce the value of the
SASL mechanism name and the channel binding name sent to server during
a SASL message exchange.
A set of tests dedicated to SASL and channel binding is added as well
to the SSL test suite, which is handy to check the validity of a patch.
---
doc/src/sgml/libpq.sgml | 24 ++++++++++++++++++++++++
src/backend/libpq/auth-scram.c | 1 +
src/interfaces/libpq/fe-auth.c | 9 ++++++++-
src/interfaces/libpq/fe-connect.c | 13 +++++++++++++
src/interfaces/libpq/libpq-int.h | 2 ++
src/test/ssl/t/002_sasl.pl | 18 +++++++++++++++---
6 files changed, 63 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 0aedd837dc..5709f4098c 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1222,6 +1222,30 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
</listitem>
</varlistentry>
+ <varlistentry id="libpq-saslname" xreflabel="saslname">
+ <term><literal>saslname</literal></term>
+ <listitem>
+ <para>
+ Controls the name of the SASL mechanism name sent to server when doing
+ a message exchange for a SASL authentication. The list of SASL
+ mechanisms supported by server are listed in
+ <xref linkend="sasl-authentication">.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="libpq-saslchannelbinding" xreflabel="saslchannelbinding">
+ <term><literal>saslchannelbinding</literal></term>
+ <listitem>
+ <para>
+ Controls the name of the channel binding name sent to server when doing
+ a message exchange for a SASL authentication. The list of channel
+ binding names supported by server are listed in
+ <xref linkend="sasl-authentication">.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="libpq-connect-sslmode" xreflabel="sslmode">
<term><literal>sslmode</literal></term>
<listitem>
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index 2efc198459..a90abf4c3b 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -1097,6 +1097,7 @@ read_client_final_message(scram_state *state, char *input)
* client has to provide channel binding value if needed.
*/
channel_binding = read_attr_value(&p, 'c');
+
#ifdef USE_SSL
if (state->ssl_in_use &&
state->channel_binding)
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index fa24c88522..97de9a0a30 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -569,6 +569,13 @@ pg_SASL_init(PGconn *conn, int payloadlen)
}
}
+ /*
+ * If user has asked for a specific mechanism name, enforce the chosen
+ * name to it.
+ */
+ if (conn->saslname && strlen(conn->saslname) > 0)
+ selected_mechanism = conn->saslname;
+
if (!selected_mechanism)
{
printfPQExpBuffer(&conn->errorMessage,
@@ -617,7 +624,7 @@ pg_SASL_init(PGconn *conn, int payloadlen)
conn->ssl_in_use,
channel_binding_advertised,
selected_mechanism,
- NULL, /* default channel binding */
+ conn->saslchannelbinding,
tls_finished,
tls_finished_len);
if (!conn->sasl_state)
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 5f79803607..1d964b0ca0 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -262,6 +262,15 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
"TCP-Keepalives-Count", "", 10, /* strlen(INT32_MAX) == 10 */
offsetof(struct pg_conn, keepalives_count)},
+ /* Set of options proper to SASL */
+ {"saslname", NULL, NULL, NULL,
+ "SASL-Name", "", 21, /* maximum name size per IANA == 21 */
+ offsetof(struct pg_conn, saslname)},
+
+ {"saslchannelbinding", NULL, NULL, NULL,
+ "SASL-Channel", "", 22, /* sizeof("tls-unique-for-telnet") == 22 */
+ offsetof(struct pg_conn, saslchannelbinding)},
+
/*
* ssl options are allowed even without client SSL support because the
* client can still handle SSL modes "disable" and "allow". Other
@@ -3470,6 +3479,10 @@ freePGconn(PGconn *conn)
free(conn->keepalives_interval);
if (conn->keepalives_count)
free(conn->keepalives_count);
+ if (conn->saslname)
+ free(conn->saslname);
+ if (conn->saslchannelbinding)
+ free(conn->saslchannelbinding);
if (conn->sslmode)
free(conn->sslmode);
if (conn->sslcert)
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 0eb8b60c95..6d500aa5db 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -349,6 +349,8 @@ struct pg_conn
* retransmits */
char *keepalives_count; /* maximum number of TCP keepalive
* retransmits */
+ char *saslname; /* SASL mechanism name */
+ char *saslchannelbinding; /* channel binding used in SASL */
char *sslmode; /* SSL mode (require,prefer,allow,disable) */
char *sslcompression; /* SSL compression (0 or 1) */
char *sslkey; /* client key filename */
diff --git a/src/test/ssl/t/002_sasl.pl b/src/test/ssl/t/002_sasl.pl
index d43a970b55..e9226903ca 100644
--- a/src/test/ssl/t/002_sasl.pl
+++ b/src/test/ssl/t/002_sasl.pl
@@ -2,7 +2,7 @@ use strict;
use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 1;
+use Test::More tests => 6;
use ServerSetup;
use File::Copy;
@@ -37,5 +37,17 @@ $common_connstr =
"user=ssltestuser dbname=trustdb sslmode=require hostaddr=$SERVERHOSTADDR";
# Tests with default channel binding and SASL mechanism names.
-# tls-unique is used here with channel binding.
-test_connect_ok($common_connstr, "");
+# tls-unique is used here
+test_connect_ok($common_connstr, "saslname=SCRAM-SHA-256-PLUS");
+test_connect_fails($common_connstr, "saslname=not-exists");
+# Having a client willing to not use channel binding should work, and
+# so should this series.
+test_connect_ok($common_connstr, "saslname=SCRAM-SHA-256");
+test_connect_ok($common_connstr,
+ "saslname=SCRAM-SHA-256 saslchannelbinding=tls-unique");
+
+# Channel bindings
+test_connect_ok($common_connstr,
+ "saslname=SCRAM-SHA-256-PLUS saslchannelbinding=tls-unique");
+test_connect_fails($common_connstr,
+ "saslname=SCRAM-SHA-256-PLUS saslchannelbinding=not-exists");
--
2.14.2
0004-Implement-channel-binding-tls-server-end-point-for-S.patchapplication/octet-stream; name=0004-Implement-channel-binding-tls-server-end-point-for-S.patchDownload
From 2dbffc90b29f1c7d87978d9f8d8177e70b6c9b1f Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 10 Oct 2017 22:04:22 +0900
Subject: [PATCH 4/4] Implement channel binding tls-server-end-point for SCRAM
As referenced in RFC 5929, this channel binding is not the default value
and uses a hash of the certificate as binding data. On the frontend, this
can be resumed in getting the data from SSL_get_peer_certificate() and
on the backend SSL_get_certificate().
The hashing algorithm needs also to switch to SHA-256 if the signature
algorithm is MD5 or SHA-1, so let's be careful about that.
---
doc/src/sgml/protocol.sgml | 4 +-
src/backend/libpq/auth-scram.c | 21 +++++++--
src/backend/libpq/auth.c | 13 ++++--
src/backend/libpq/be-secure-openssl.c | 61 +++++++++++++++++++++++++
src/include/libpq/libpq-be.h | 1 +
src/include/libpq/scram.h | 4 +-
src/interfaces/libpq/fe-auth-scram.c | 20 +++++++-
src/interfaces/libpq/fe-auth.c | 12 ++++-
src/interfaces/libpq/fe-auth.h | 3 +-
src/interfaces/libpq/fe-secure-openssl.c | 78 ++++++++++++++++++++++++++++++++
src/interfaces/libpq/libpq-int.h | 1 +
src/test/ssl/t/002_sasl.pl | 6 ++-
12 files changed, 209 insertions(+), 15 deletions(-)
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 3ba8575fba..d86d354397 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1550,8 +1550,8 @@ the password is in.
<para>
<firstterm>Channel binding</> is supported in builds with SSL support, and
uses as mechanism name <literal>SCRAM-SHA-256-PLUS</> for this purpose as
-defined per IANA. The only channel binding type supported by the server
-is <literal>tls-unique</>.
+defined per IANA. The channel binding types supported by the server
+are <literal>tls-unique</>, the default, and <literal>tls-server-end-point</>.
</para>
<procedure>
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index a90abf4c3b..02fd3332ac 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -113,6 +113,8 @@ typedef struct
bool ssl_in_use;
char *tls_finished_message;
int tls_finished_len;
+ char *certificate_hash;
+ int certificate_hash_len;
char *channel_binding;
int iterations;
@@ -175,7 +177,9 @@ pg_be_scram_init(const char *username,
const char *shadow_pass,
bool ssl_in_use,
char *tls_finished_message,
- int tls_finished_len)
+ int tls_finished_len,
+ char *certificate_hash,
+ int certificate_hash_len)
{
scram_state *state;
bool got_verifier;
@@ -186,6 +190,8 @@ pg_be_scram_init(const char *username,
state->ssl_in_use = ssl_in_use;
state->tls_finished_message = tls_finished_message;
state->tls_finished_len = tls_finished_len;
+ state->certificate_hash = certificate_hash;
+ state->certificate_hash_len = certificate_hash_len;
state->channel_binding = NULL;
/*
@@ -835,11 +841,12 @@ read_client_first_message(scram_state *state, char *input)
errmsg("client requires SCRAM channel binding, but it is not supported")));
/*
- * Read value provided by client, only tls-unique is supported
- * for now.
+ * Read value provided by client, only tls-unique and
+ * tls-server-end-point are supported for now.
*/
channel_name = read_attr_value(&input, 'p');
- if (strcmp(channel_name, SCRAM_CHANNEL_TLS_UNIQUE) != 0)
+ if (strcmp(channel_name, SCRAM_CHANNEL_TLS_UNIQUE) != 0 &&
+ strcmp(channel_name, SCRAM_CHANNEL_TLS_ENDPOINT) != 0)
ereport(ERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION),
(errmsg("unexpected SCRAM channel-binding type"))));
@@ -1111,6 +1118,12 @@ read_client_final_message(scram_state *state, char *input)
raw_data = state->tls_finished_message;
raw_data_len = state->tls_finished_len;
}
+ else if (strcmp(state->channel_binding,
+ SCRAM_CHANNEL_TLS_ENDPOINT) == 0)
+ {
+ raw_data = state->certificate_hash;
+ raw_data_len = state->certificate_hash_len;
+ }
else
{
/* should not happen */
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 5bceae2162..338e3d7a0d 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -867,6 +867,8 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
bool initial;
char *tls_finished = NULL;
int tls_finished_len = 0;
+ char *certificate_hash = NULL;
+ int certificate_hash_len = 0;
/*
* SASL auth is not supported for protocol versions before 3, because it
@@ -918,12 +920,15 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
#ifdef USE_SSL
/*
- * Fetch the data related to the SSL finish message to be used in the
- * exchange.
+ * Fetch the data related to the SSL finish message and the client
+ * certificate (if any) to be used in the exchange.
*/
if (port->ssl_in_use)
{
tls_finished = be_tls_get_peer_finish(port, &tls_finished_len);
+ certificate_hash =
+ be_tls_get_certificate_hash(port,
+ &certificate_hash_len);
}
#endif
@@ -942,7 +947,9 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
shadow_pass,
port->ssl_in_use,
tls_finished,
- tls_finished_len);
+ tls_finished_len,
+ certificate_hash,
+ certificate_hash_len);
/*
* Loop through SASL message exchange. This exchange can consist of
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index d063a587d0..bf9cef7f15 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -1239,6 +1239,67 @@ be_tls_get_peer_finish(Port *port, int *len)
return result;
}
+/*
+ * Get the server certificate hash for authentication purposes. Per
+ * RFC 5929 and tls-server-end-point, the TLS server's certificate bytes
+ * need to be hashed with SHA-256 if its signature algorithm is MD5 or
+ * SHA-1 as per RFC 5929 (https://tools.ietf.org/html/rfc5929#section-4.1).
+ * If something else is used, the same hash as the signature algorithm is
+ * used. The result is a palloc'd hash of the server certificate with its
+ * size, and NULL if there is no certificate available.
+ */
+char *
+be_tls_get_certificate_hash(Port *port, int *len)
+{
+ char *cert_hash = NULL;
+ X509 *server_cert;
+
+ *len = 0;
+ server_cert = SSL_get_certificate(port->ssl);
+
+ if (server_cert != NULL)
+ {
+ const EVP_MD *algo_type = NULL;
+ char hash[EVP_MAX_MD_SIZE]; /* size for SHA-512 */
+ unsigned int hash_size;
+ int algo_nid;
+
+ /*
+ * Get the signature algorithm of the certificate to determine the
+ * hash algorithm to use for the result.
+ */
+ if (!OBJ_find_sigid_algs(X509_get_signature_nid(server_cert),
+ &algo_nid, NULL))
+ elog(ERROR, "could not find signature algorithm");
+
+ switch (algo_nid)
+ {
+ case NID_md5:
+ case NID_sha1:
+ algo_type = EVP_sha256();
+ break;
+
+ default:
+ algo_type = EVP_get_digestbynid(algo_nid);
+ if (algo_type == NULL)
+ elog(ERROR, "could not find digest for NID %s",
+ OBJ_nid2sn(algo_nid));
+ break;
+ }
+
+ /* generate and save the certificate hash */
+ if (!X509_digest(server_cert, algo_type, (unsigned char *) hash,
+ &hash_size))
+ elog(ERROR, "could not generate server certificate hash");
+
+ cert_hash = (char *) palloc(hash_size);
+ memcpy(cert_hash, hash, hash_size);
+ *len = hash_size;
+ }
+
+ return cert_hash;
+}
+
/*
* Convert an X509 subject name to a cstring.
*
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 5d59d79822..0392860a87 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -210,6 +210,7 @@ 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_peerdn_name(Port *port, char *ptr, size_t len);
extern char *be_tls_get_peer_finish(Port *port, int *len);
+extern char *be_tls_get_certificate_hash(Port *port, int *len);
#endif
extern ProtocolVersion FrontendProtocol;
diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h
index 43cde0e46e..6329e111ba 100644
--- a/src/include/libpq/scram.h
+++ b/src/include/libpq/scram.h
@@ -19,6 +19,7 @@
/* Channel binding names */
#define SCRAM_CHANNEL_TLS_UNIQUE "tls-unique"
+#define SCRAM_CHANNEL_TLS_ENDPOINT "tls-server-end-point"
/* Status codes for message exchange */
#define SASL_EXCHANGE_CONTINUE 0
@@ -28,7 +29,8 @@
/* Routines dedicated to authentication */
extern void *pg_be_scram_init(const char *username, const char *shadow_pass,
bool ssl_in_use, char *tls_finished_message,
- int tls_finished_len);
+ int tls_finished_len, char *certificate_hash,
+ int certificate_hash_len);
extern int pg_be_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen, char **logdetail);
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index 9bce4d980a..307e58f586 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -49,6 +49,8 @@ typedef struct
bool channel_binding_advertised;
char *tls_finished_message;
int tls_finished_len;
+ char *certificate_hash;
+ int certificate_hash_len;
/* enforce-able user parameters */
char *saslmechanism; /* name of mechanism used */
char *saslchannelbinding; /* name of channel binding to use */
@@ -96,7 +98,9 @@ pg_fe_scram_init(const char *username,
const char *saslmechanism,
char *saslchannelbinding,
char *tls_finished_message,
- int tls_finished_len)
+ int tls_finished_len,
+ char *certificate_hash,
+ int certificate_hash_len)
{
fe_scram_state *state;
char *prep_password;
@@ -115,6 +119,8 @@ pg_fe_scram_init(const char *username,
state->tls_finished_message = tls_finished_message;
state->tls_finished_len = tls_finished_len;
state->saslmechanism = strdup(saslmechanism);
+ state->certificate_hash = certificate_hash;
+ state->certificate_hash_len = certificate_hash_len;
/*
* If user has specified a channel binding to use, enforce the
@@ -158,6 +164,8 @@ pg_fe_scram_free(void *opaq)
free(state->password);
if (state->tls_finished_message)
free(state->tls_finished_message);
+ if (state->certificate_hash)
+ free(state->certificate_hash);
if (state->saslmechanism)
free(state->saslmechanism);
if (state->saslchannelbinding)
@@ -466,7 +474,9 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
* Construct client-final-message-without-proof. We need to remember it
* for verifying the server proof in the final step of authentication.
* Client needs to provide a b64 encoded string of the TLS finish message
- * only if a SSL connection is attempted.
+ * only if a SSL connection is attempted using "tls-unique" as channel
+ * binding. For "tls-server-end-point", a hash of the client certificate
+ * is sent instead.
*/
#ifdef USE_SSL
if (strcmp(state->saslmechanism, SCRAM_SHA256_PLUS_NAME) == 0)
@@ -479,6 +489,12 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
raw_data = state->tls_finished_message;
raw_data_len = state->tls_finished_len;
}
+ else if (strcmp(state->saslchannelbinding,
+ SCRAM_CHANNEL_TLS_ENDPOINT) == 0)
+ {
+ raw_data = state->certificate_hash;
+ raw_data_len = state->certificate_hash_len;
+ }
else
{
/* should not happen */
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 97de9a0a30..56dbe2be28 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -494,6 +494,8 @@ pg_SASL_init(PGconn *conn, int payloadlen)
bool channel_binding_advertised = false;
char *tls_finished = NULL;
int tls_finished_len = 0;
+ char *certificate_hash = NULL;
+ int certificate_hash_len = 0;
char *password;
initPQExpBuffer(&mechanism_buf);
@@ -610,6 +612,12 @@ pg_SASL_init(PGconn *conn, int payloadlen)
tls_finished = pgtls_get_finish(conn, &tls_finished_len);
if (tls_finished == NULL)
goto oom_error;
+
+ certificate_hash =
+ pgtls_get_peer_certificate_hash(conn,
+ &certificate_hash_len);
+ if (certificate_hash == NULL)
+ goto error; /* error message is set */
}
#endif
@@ -626,7 +634,9 @@ pg_SASL_init(PGconn *conn, int payloadlen)
selected_mechanism,
conn->saslchannelbinding,
tls_finished,
- tls_finished_len);
+ tls_finished_len,
+ certificate_hash,
+ certificate_hash_len);
if (!conn->sasl_state)
goto oom_error;
diff --git a/src/interfaces/libpq/fe-auth.h b/src/interfaces/libpq/fe-auth.h
index 81378d0008..ed574d39e5 100644
--- a/src/interfaces/libpq/fe-auth.h
+++ b/src/interfaces/libpq/fe-auth.h
@@ -26,7 +26,8 @@ extern char *pg_fe_getauthname(PQExpBuffer errorMessage);
extern void *pg_fe_scram_init(const char *username, const char *password,
bool ssl_in_use, bool channel_binding_advertised,
const char *saslmechanism,char *saslchannelbinding,
- char *tls_finished_message, int tls_finished_len);
+ char *tls_finished_message, int tls_finished_len,
+ char *certificate_hash, int certificate_hash_len);
extern void pg_fe_scram_free(void *opaq);
extern void pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen,
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 84a6e3c322..301bde0799 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -418,6 +418,84 @@ pgtls_get_finish(PGconn *conn, int *len)
return result;
}
+/*
+ * Get the hash of the server certificate
+ *
+ * This information is useful for end-point channel binding, where
+ * the client certificate hash is used as a link, per RFC 5929. If
+ * the signature hash algorithm is MD5 or SHA-1, fall back to SHA-256,
+ * as per RFC 5929 (https://tools.ietf.org/html/rfc5929#section-4.1).
+ * NULL is sent back to the caller in the event of an error, with an
+ * error message for the caller to consume.
+ */
+char *
+pgtls_get_peer_certificate_hash(PGconn *conn, int *len)
+{
+ char *cert_hash = NULL;
+
+ *len = 0;
+
+ if (conn->peer)
+ {
+ X509 *peer_cert = conn->peer;
+ const EVP_MD *algo_type = NULL;
+ char hash[EVP_MAX_MD_SIZE]; /* size for SHA-512 */
+ unsigned int hash_size;
+ int algo_nid;
+
+ /*
+ * Get the signature algorithm of the certificate to determine the
+ * hash algorithm to use for the result.
+ */
+ if (!OBJ_find_sigid_algs(X509_get_signature_nid(peer_cert),
+ &algo_nid, NULL))
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("could not find signature algorithm\n"));
+ return NULL;
+ }
+
+ switch (algo_nid)
+ {
+ case NID_md5:
+ case NID_sha1:
+ algo_type = EVP_sha256();
+ break;
+
+ default:
+ algo_type = EVP_get_digestbynid(algo_nid);
+ if (algo_type == NULL)
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("could not find digest for NID %s\n"),
+ OBJ_nid2sn(algo_nid));
+ return NULL;
+ }
+ break;
+ }
+
+ if (!X509_digest(peer_cert, algo_type, (unsigned char *) hash,
+ &hash_size))
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("could not generate peer certificate hash\n"));
+ return NULL;
+ }
+
+ /* save result */
+ cert_hash = (char *) malloc(hash_size);
+ if (cert_hash == NULL)
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("out of memory\n"));
+ return NULL;
+ }
+ memcpy(cert_hash, hash, hash_size);
+ *len = hash_size;
+ }
+
+ return cert_hash;
+}
/* ------------------------------------------------------------ */
/* OpenSSL specific code */
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 6d500aa5db..f1e2c0bb3c 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -673,6 +673,7 @@ extern ssize_t pgtls_read(PGconn *conn, void *ptr, size_t len);
extern bool pgtls_read_pending(PGconn *conn);
extern ssize_t pgtls_write(PGconn *conn, const void *ptr, size_t len);
extern char *pgtls_get_finish(PGconn *conn, int *len);
+extern char *pgtls_get_peer_certificate_hash(PGconn *conn, int *len);
/*
* this is so that we can check if a connection is non-blocking internally
diff --git a/src/test/ssl/t/002_sasl.pl b/src/test/ssl/t/002_sasl.pl
index e9226903ca..fa3cca24a2 100644
--- a/src/test/ssl/t/002_sasl.pl
+++ b/src/test/ssl/t/002_sasl.pl
@@ -2,7 +2,7 @@ use strict;
use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 6;
+use Test::More tests => 8;
use ServerSetup;
use File::Copy;
@@ -45,9 +45,13 @@ test_connect_fails($common_connstr, "saslname=not-exists");
test_connect_ok($common_connstr, "saslname=SCRAM-SHA-256");
test_connect_ok($common_connstr,
"saslname=SCRAM-SHA-256 saslchannelbinding=tls-unique");
+test_connect_ok($common_connstr,
+ "saslname=SCRAM-SHA-256 saslchannelbinding=tls-server-end-point");
# Channel bindings
test_connect_ok($common_connstr,
"saslname=SCRAM-SHA-256-PLUS saslchannelbinding=tls-unique");
+test_connect_ok($common_connstr,
+ "saslname=SCRAM-SHA-256-PLUS saslchannelbinding=tls-server-end-point");
test_connect_fails($common_connstr,
"saslname=SCRAM-SHA-256-PLUS saslchannelbinding=not-exists");
--
2.14.2
On Tue, Oct 10, 2017 at 10:12 PM, Michael Paquier
<michael.paquier@gmail.com> wrote:
Attached is a new patch set with the comments from above. On top of
that, I have changed a couple of things:
- 0001 is unchanged, still the same refactoring for the SSL tests.
- 0002 implements tls-unique, now including tests using the default
channel binding tls-unique with something in the SSL test suite. This
patch also now introduces all the infrastructure to plug in correctly
new libpq parameters and more channel binding types.
- 0003 is shorter, and introduces a set of libpq parameters useful for
tests, taking advantage of 0002. Another case where the connection
parameter saslname is useful is to enforce not using channel binding
when connecting to a v10 server using a SSL context with a v11 libpq.
- 0004 introduces tls-server-end-point.
This has required some work to get it shaped as wanted, I am adding it
to the next CF, as version 2.
Documentation in protocol.sgml has rotten again as markups need proper
handling. So rebased.
--
Michael
Attachments:
0001-Refactor-routine-to-test-connection-to-SSL-server.patchapplication/octet-stream; name=0001-Refactor-routine-to-test-connection-to-SSL-server.patchDownload
From 92bdd0b20786cb1e8353e4fe1d1d67e7ceb48870 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 16 Jun 2017 17:00:06 +0900
Subject: [PATCH 1/4] Refactor routine to test connection to SSL server
Move the sub-routines wrappers to check if a connection to a server is
fine or not into the test main module. This is useful for other tests
willing to check connectivity into a server.
---
src/test/ssl/ServerSetup.pm | 45 +++++++++++++-
src/test/ssl/t/001_ssltests.pl | 132 +++++++++++++++++------------------------
2 files changed, 100 insertions(+), 77 deletions(-)
diff --git a/src/test/ssl/ServerSetup.pm b/src/test/ssl/ServerSetup.pm
index f63c81cfc6..ad2e036602 100644
--- a/src/test/ssl/ServerSetup.pm
+++ b/src/test/ssl/ServerSetup.pm
@@ -26,9 +26,52 @@ use Test::More;
use Exporter 'import';
our @EXPORT = qw(
- configure_test_server_for_ssl switch_server_cert
+ configure_test_server_for_ssl
+ run_test_psql
+ switch_server_cert
+ test_connect_fails
+ test_connect_ok
);
+# Define a couple of helper functions to test connecting to the server.
+
+# Attempt connection to server with given connection string.
+sub run_test_psql
+{
+ my $connstr = $_[0];
+ my $logstring = $_[1];
+
+ my $cmd = [
+ 'psql', '-X', '-A', '-t', '-c', "SELECT 'connected with $connstr'",
+ '-d', "$connstr" ];
+
+ my $result = run_log($cmd);
+ return $result;
+}
+
+#
+# The first argument is a base connection string to use for connection.
+# The second argument is a complementary connection string, and it's also
+# printed out as the test case name.
+sub test_connect_ok
+{
+ my $common_connstr = $_[0];
+ my $connstr = $_[1];
+
+ my $result =
+ run_test_psql("$common_connstr $connstr", "(should succeed)");
+ ok($result, $connstr);
+}
+
+sub test_connect_fails
+{
+ my $common_connstr = $_[0];
+ my $connstr = $_[1];
+
+ my $result = run_test_psql("$common_connstr $connstr", "(should fail)");
+ ok(!$result, "$connstr (should fail)");
+}
+
# Copy a set of files, taking into account wildcards
sub copy_files
{
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 32df273929..890e3051a2 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -13,44 +13,9 @@ use File::Copy;
# postgresql-ssl-regression.test.
my $SERVERHOSTADDR = '127.0.0.1';
-# Define a couple of helper functions to test connecting to the server.
-
+# Allocation of base connection string shared among multiple tests.
my $common_connstr;
-sub run_test_psql
-{
- my $connstr = $_[0];
- my $logstring = $_[1];
-
- my $cmd = [
- 'psql', '-X', '-A', '-t', '-c', "SELECT 'connected with $connstr'",
- '-d', "$connstr" ];
-
- my $result = run_log($cmd);
- return $result;
-}
-
-#
-# The first argument is a (part of a) connection string, and it's also printed
-# out as the test case name. It is appended to $common_connstr global variable,
-# which also contains a libpq connection string.
-sub test_connect_ok
-{
- my $connstr = $_[0];
-
- my $result =
- run_test_psql("$common_connstr $connstr", "(should succeed)");
- ok($result, $connstr);
-}
-
-sub test_connect_fails
-{
- my $connstr = $_[0];
-
- my $result = run_test_psql("$common_connstr $connstr", "(should fail)");
- ok(!$result, "$connstr (should fail)");
-}
-
# The client's private key must not be world-readable, so take a copy
# of the key stored in the code tree and update its permissions.
copy("ssl/client.key", "ssl/client_tmp.key");
@@ -83,50 +48,59 @@ $common_connstr =
# The server should not accept non-SSL connections
note "test that the server doesn't accept non-SSL connections";
-test_connect_fails("sslmode=disable");
+test_connect_fails($common_connstr, "sslmode=disable");
# Try without a root cert. In sslmode=require, this should work. In verify-ca
# or verify-full mode it should fail
note "connect without server root cert";
-test_connect_ok("sslrootcert=invalid sslmode=require");
-test_connect_fails("sslrootcert=invalid sslmode=verify-ca");
-test_connect_fails("sslrootcert=invalid sslmode=verify-full");
+test_connect_ok($common_connstr, "sslrootcert=invalid sslmode=require");
+test_connect_fails($common_connstr, "sslrootcert=invalid sslmode=verify-ca");
+test_connect_fails($common_connstr, "sslrootcert=invalid sslmode=verify-full");
# Try with wrong root cert, should fail. (we're using the client CA as the
# root, but the server's key is signed by the server CA)
note "connect without wrong server root cert";
-test_connect_fails("sslrootcert=ssl/client_ca.crt sslmode=require");
-test_connect_fails("sslrootcert=ssl/client_ca.crt sslmode=verify-ca");
-test_connect_fails("sslrootcert=ssl/client_ca.crt sslmode=verify-full");
+test_connect_fails($common_connstr,
+ "sslrootcert=ssl/client_ca.crt sslmode=require");
+test_connect_fails($common_connstr,
+ "sslrootcert=ssl/client_ca.crt sslmode=verify-ca");
+test_connect_fails($common_connstr,
+ "sslrootcert=ssl/client_ca.crt sslmode=verify-full");
# Try with just the server CA's cert. This fails because the root file
# must contain the whole chain up to the root CA.
note "connect with server CA cert, without root CA";
-test_connect_fails("sslrootcert=ssl/server_ca.crt sslmode=verify-ca");
+test_connect_fails($common_connstr,
+ "sslrootcert=ssl/server_ca.crt sslmode=verify-ca");
# And finally, with the correct root cert.
note "connect with correct server CA cert file";
-test_connect_ok("sslrootcert=ssl/root+server_ca.crt sslmode=require");
-test_connect_ok("sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca");
-test_connect_ok("sslrootcert=ssl/root+server_ca.crt sslmode=verify-full");
+test_connect_ok($common_connstr,
+ "sslrootcert=ssl/root+server_ca.crt sslmode=require");
+test_connect_ok($common_connstr,
+ "sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca");
+test_connect_ok($common_connstr,
+ "sslrootcert=ssl/root+server_ca.crt sslmode=verify-full");
# Test with cert root file that contains two certificates. The client should
# be able to pick the right one, regardless of the order in the file.
-test_connect_ok("sslrootcert=ssl/both-cas-1.crt sslmode=verify-ca");
-test_connect_ok("sslrootcert=ssl/both-cas-2.crt sslmode=verify-ca");
+test_connect_ok($common_connstr,
+ "sslrootcert=ssl/both-cas-1.crt sslmode=verify-ca");
+test_connect_ok($common_connstr,
+ "sslrootcert=ssl/both-cas-2.crt sslmode=verify-ca");
note "testing sslcrl option with a non-revoked cert";
# Invalid CRL filename is the same as no CRL, succeeds
-test_connect_ok(
+test_connect_ok($common_connstr,
"sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrl=invalid");
# A CRL belonging to a different CA is not accepted, fails
-test_connect_fails(
+test_connect_fails($common_connstr,
"sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrl=ssl/client.crl");
# With the correct CRL, succeeds (this cert is not revoked)
-test_connect_ok(
+test_connect_ok($common_connstr,
"sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrl=ssl/root+server.crl"
);
@@ -136,9 +110,9 @@ note "test mismatch between hostname and server certificate";
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
-test_connect_ok("sslmode=require host=wronghost.test");
-test_connect_ok("sslmode=verify-ca host=wronghost.test");
-test_connect_fails("sslmode=verify-full host=wronghost.test");
+test_connect_ok($common_connstr, "sslmode=require host=wronghost.test");
+test_connect_ok($common_connstr, "sslmode=verify-ca host=wronghost.test");
+test_connect_fails($common_connstr, "sslmode=verify-full host=wronghost.test");
# Test Subject Alternative Names.
switch_server_cert($node, 'server-multiple-alt-names');
@@ -147,12 +121,13 @@ note "test hostname matching with X.509 Subject Alternative Names";
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
-test_connect_ok("host=dns1.alt-name.pg-ssltest.test");
-test_connect_ok("host=dns2.alt-name.pg-ssltest.test");
-test_connect_ok("host=foo.wildcard.pg-ssltest.test");
+test_connect_ok($common_connstr, "host=dns1.alt-name.pg-ssltest.test");
+test_connect_ok($common_connstr, "host=dns2.alt-name.pg-ssltest.test");
+test_connect_ok($common_connstr, "host=foo.wildcard.pg-ssltest.test");
-test_connect_fails("host=wronghost.alt-name.pg-ssltest.test");
-test_connect_fails("host=deep.subdomain.wildcard.pg-ssltest.test");
+test_connect_fails($common_connstr, "host=wronghost.alt-name.pg-ssltest.test");
+test_connect_fails($common_connstr,
+ "host=deep.subdomain.wildcard.pg-ssltest.test");
# Test certificate with a single Subject Alternative Name. (this gives a
# slightly different error message, that's all)
@@ -162,10 +137,11 @@ note "test hostname matching with a single X.509 Subject Alternative Name";
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
-test_connect_ok("host=single.alt-name.pg-ssltest.test");
+test_connect_ok($common_connstr, "host=single.alt-name.pg-ssltest.test");
-test_connect_fails("host=wronghost.alt-name.pg-ssltest.test");
-test_connect_fails("host=deep.subdomain.wildcard.pg-ssltest.test");
+test_connect_fails($common_connstr, "host=wronghost.alt-name.pg-ssltest.test");
+test_connect_fails($common_connstr,
+ "host=deep.subdomain.wildcard.pg-ssltest.test");
# Test server certificate with a CN and SANs. Per RFCs 2818 and 6125, the CN
# should be ignored when the certificate has both.
@@ -175,9 +151,9 @@ note "test certificate with both a CN and SANs";
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
-test_connect_ok("host=dns1.alt-name.pg-ssltest.test");
-test_connect_ok("host=dns2.alt-name.pg-ssltest.test");
-test_connect_fails("host=common-name.pg-ssltest.test");
+test_connect_ok($common_connstr, "host=dns1.alt-name.pg-ssltest.test");
+test_connect_ok($common_connstr, "host=dns2.alt-name.pg-ssltest.test");
+test_connect_fails($common_connstr, "host=common-name.pg-ssltest.test");
# Finally, test a server certificate that has no CN or SANs. Of course, that's
# not a very sensible certificate, but libpq should handle it gracefully.
@@ -185,8 +161,10 @@ switch_server_cert($node, 'server-no-names');
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR";
-test_connect_ok("sslmode=verify-ca host=common-name.pg-ssltest.test");
-test_connect_fails("sslmode=verify-full host=common-name.pg-ssltest.test");
+test_connect_ok($common_connstr,
+ "sslmode=verify-ca host=common-name.pg-ssltest.test");
+test_connect_fails($common_connstr,
+ "sslmode=verify-full host=common-name.pg-ssltest.test");
# Test that the CRL works
note "testing client-side CRL";
@@ -196,8 +174,9 @@ $common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid hostaddr=$SERVERHOSTADDR host=common-name.pg-ssltest.test";
# Without the CRL, succeeds. With it, fails.
-test_connect_ok("sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca");
-test_connect_fails(
+test_connect_ok($common_connstr,
+ "sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca");
+test_connect_fails($common_connstr,
"sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca sslcrl=ssl/root+server.crl"
);
@@ -210,18 +189,18 @@ $common_connstr =
"sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=certdb hostaddr=$SERVERHOSTADDR";
# no client cert
-test_connect_fails("user=ssltestuser sslcert=invalid");
+test_connect_fails($common_connstr, "user=ssltestuser sslcert=invalid");
# correct client cert
-test_connect_ok(
+test_connect_ok($common_connstr,
"user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key");
# client cert belonging to another user
-test_connect_fails(
+test_connect_fails($common_connstr,
"user=anotheruser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key");
# revoked client cert
-test_connect_fails(
+test_connect_fails($common_connstr,
"user=ssltestuser sslcert=ssl/client-revoked.crt sslkey=ssl/client-revoked.key"
);
@@ -230,8 +209,9 @@ switch_server_cert($node, 'server-cn-only', 'root_ca');
$common_connstr =
"user=ssltestuser dbname=certdb sslkey=ssl/client_tmp.key sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR";
-test_connect_ok("sslmode=require sslcert=ssl/client+client_ca.crt");
-test_connect_fails("sslmode=require sslcert=ssl/client.crt");
+test_connect_ok($common_connstr,
+ "sslmode=require sslcert=ssl/client+client_ca.crt");
+test_connect_fails($common_connstr, "sslmode=require sslcert=ssl/client.crt");
# clean up
unlink "ssl/client_tmp.key";
--
2.15.0
0002-Support-channel-binding-tls-unique-in-SCRAM.patchapplication/octet-stream; name=0002-Support-channel-binding-tls-unique-in-SCRAM.patchDownload
From 5d9b1106be98d16a9945c4dab19867b164e5cb88 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 14 Nov 2017 17:53:24 +0900
Subject: [PATCH 2/4] 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 finish messages, a PG-like API is added to be able to
make the interface pluggable for other SSL implementations.
This commit also adds a lot of basic 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.
A set of basic tests to stress default channel binding handling is
added as well, though those are part of the SSL suite.
---
doc/src/sgml/protocol.sgml | 24 ++--
src/backend/libpq/auth-scram.c | 162 +++++++++++++++++++++-----
src/backend/libpq/auth.c | 69 ++++++++++--
src/backend/libpq/be-secure-openssl.c | 24 ++++
src/include/libpq/libpq-be.h | 1 +
src/include/libpq/scram.h | 10 +-
src/interfaces/libpq/fe-auth-scram.c | 187 +++++++++++++++++++++++++++----
src/interfaces/libpq/fe-auth.c | 100 +++++++++++++----
src/interfaces/libpq/fe-auth.h | 5 +-
src/interfaces/libpq/fe-secure-openssl.c | 26 +++++
src/interfaces/libpq/libpq-int.h | 5 +-
src/test/ssl/ServerSetup.pm | 19 +++-
src/test/ssl/t/001_ssltests.pl | 2 +-
src/test/ssl/t/002_sasl.pl | 41 +++++++
14 files changed, 584 insertions(+), 91 deletions(-)
create mode 100644 src/test/ssl/t/002_sasl.pl
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 6d4dcf83ac..e2a3d8f4d5 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1461,10 +1461,11 @@ SELCT 1/0;
<para>
<firstterm>SASL</firstterm> is a framework for authentication in connection-oriented
-protocols. At the moment, <productname>PostgreSQL</productname> implements only one SASL
-authentication mechanism, SCRAM-SHA-256, but more might be added in the
-future. The below steps illustrate how SASL authentication is performed in
-general, while the next subsection gives more details on SCRAM-SHA-256.
+protocols. At the moment, <productname>PostgreSQL</productname> implements only two SASL
+authentication mechanisms, SCRAM-SHA-256 and SCRAM-SHA-256-PLUS, but more
+might be added in the future. The below steps illustrate how SASL
+authentication is performed in general, while the next subsection gives
+more details on SCRAM-SHA-256 and SCRAM-SHA-256-PLUS.
</para>
<procedure>
@@ -1547,7 +1548,10 @@ the password is in.
</para>
<para>
-<firstterm>Channel binding</firstterm> has not been implemented yet.
+<firstterm>Channel binding</firstterm> is supported in builds with SSL
+support, and uses as mechanism name <literal>SCRAM-SHA-256-PLUS</literal>
+for this purpose as defined per IANA. The only channel binding type
+supported by the server is <literal>tls-unique</literal>.
</para>
<procedure>
@@ -1556,13 +1560,19 @@ the password is in.
<para>
The server sends an AuthenticationSASL message. It includes a list of
SASL authentication mechanisms that the server can accept.
+ <literal>SCRAM-SHA-256</literal> and <literal>SCRAM-SHA-256-PLUS</literal>
+ are the two mechanism names that the server lists in this message. Support
+ for channel binding is not included if the server is built without SSL
+ support. The client can still choose to not use channel binding for
+ a connection attempt with SSL.
</para>
</step>
<step id="scram-client-first">
<para>
The client responds by sending a SASLInitialResponse message, which
- indicates the chosen mechanism, <literal>SCRAM-SHA-256</literal>. In the Initial
- Client response field, the message contains the SCRAM
+ indicates the chosen mechanism, <literal>SCRAM-SHA-256</literal> or
+ <literal>SCRAM-SHA-256-PLUS</literal>. In the Initial Client response field,
+ the message contains the SCRAM
<structname>client-first-message</structname>.
</para>
</step>
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index ec4bb9a88e..b326c8a39e 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;
+ char *tls_finished_message;
+ int tls_finished_len;
+ char *channel_binding;
+
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,
+ char *tls_finished_message,
+ int 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 = NULL;
/*
* Parse the stored password verifier.
@@ -773,31 +784,84 @@ read_client_first_message(scram_state *state, char *input)
*------
*/
- /* read gs2-cbind-flag */
+ /*
+ * Read gs2-cbind-flag. Server handles its value as described in RFC 5802,
+ * section 6 dealing with 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 this case just let 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 */
+ /*
+ * 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 is only the case if a connection
+ * is done when SSL is used.
+ */
+ if (state->ssl_in_use)
+ ereport(ERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("client supports SCRAM channel binding, but server needs it for SSL connections")));
+ 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':
+ {
+#ifdef USE_SSL
+ char *channel_name;
- /*
- * 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")));
+ if (!state->ssl_in_use)
+ 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.
+ */
+ channel_name = read_attr_value(&input, 'p');
+ if (strcmp(channel_name, SCRAM_CHANNEL_TLS_UNIQUE) != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ (errmsg("unexpected SCRAM channel-binding type"))));
+
+ /* Save the name for handling of subsequent messages */
+ state->channel_binding = pstrdup(channel_name);
+#else
+ /*
+ * 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")));
+#endif
+ }
+ break;
default:
ereport(ERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION),
@@ -805,13 +869,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 +1089,65 @@ 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. We don't support channel binding for builds
+ * without SSL support or if the client does not want to use channel
+ * binding, so it's expected to always be "biws" in this case, which
+ * is "n,,", base64-encoded. In builds supporting SSL, "biws" is
+ * used for Non-SSL connection attempts, and for SSL connections the
+ * client has to provide channel binding value if needed.
*/
channel_binding = read_attr_value(&p, 'c');
+#ifdef USE_SSL
+ if (state->ssl_in_use &&
+ state->channel_binding)
+ {
+ char *b64_message, *raw_data;
+ int b64_message_len, raw_data_len;
+
+ /* Fetch data for each channel binding type */
+ if (strcmp(state->channel_binding, SCRAM_CHANNEL_TLS_UNIQUE) == 0)
+ {
+ raw_data = state->tls_finished_message;
+ raw_data_len = state->tls_finished_len;
+ }
+ else
+ {
+ /* should not happen */
+ elog(ERROR, "invalid channel binding type");
+ }
+
+ /* should not happen, but better safe than sorry */
+ if (raw_data == NULL)
+ elog(ERROR, "empty binding data for channel name \"%s\"",
+ state->channel_binding);
+
+ b64_message = palloc(pg_b64_enc_len(raw_data_len) + 1);
+ b64_message_len = pg_b64_encode(raw_data, raw_data_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_PROTOCOL_VIOLATION),
+ (errmsg("no match for SCRAM channel-binding attribute in client-final-message"))));
+ }
+ else
+ {
+ if (strcmp(channel_binding, "biws") != 0)
+ ereport(ERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ (errmsg("unexpected SCRAM channel-binding attribute in client-final-message"))));
+ }
+#else
if (strcmp(channel_binding, "biws") != 0)
ereport(ERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION),
(errmsg("unexpected SCRAM channel-binding attribute in client-final-message"))));
+#endif
state->client_final_nonce = read_attr_value(&p, 'r');
/* ignore optional extensions */
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 6c915a7289..055c619cb9 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -865,10 +865,14 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
void *scram_opaq;
char *output = NULL;
int outputlen = 0;
- char *input;
+ char *input, *p;
+ char *sasl_mechs;
int inputlen;
+ int listlen = 0;
int result;
bool initial;
+ char *tls_finished = NULL;
+ int tls_finished_len = 0;
/*
* SASL auth is not supported for protocol versions before 3, because it
@@ -885,12 +889,49 @@ 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:
+ * - SCRAM-SHA-256, which is the mechanism with the same name.
+ * - SCRAM-SHA-256-PLUS, which is SCRAM-SHA-256 with channel binding. This
+ * is advertised to the client only if connection is attempted with SSL.
+ * The order of mechanisms is advertised in decreasing order of importance.
+ * The extra "\0" is for an empty string to terminate the list, and each
+ * mechanism listed needs to be separated with "\0".
*/
- sendAuthRequest(port, AUTH_REQ_SASL, SCRAM_SHA256_NAME "\0",
- strlen(SCRAM_SHA256_NAME) + 2);
+ listlen = 0;
+ sasl_mechs = (char *) palloc(strlen(SCRAM_SHA256_PLUS_NAME) +
+ strlen(SCRAM_SHA256_NAME) + 3);
+ p = sasl_mechs;
+
+ /* add SCRAM-SHA-256-PLUS, which depends on if SSL is in use */
+ if (port->ssl_in_use)
+ {
+ strcpy(p, SCRAM_SHA256_PLUS_NAME);
+ listlen += strlen(SCRAM_SHA256_PLUS_NAME) + 1;
+ p += strlen(SCRAM_SHA256_PLUS_NAME) + 1;
+ }
+
+ /* add generic SCRAM-SHA-256 */
+ strcpy(p, SCRAM_SHA256_NAME);
+ listlen += strlen(SCRAM_SHA256_NAME) + 1;
+ p += strlen(SCRAM_SHA256_NAME) + 1;
+
+ /* put "\0" to mark that list is finished */
+ p[0] = '\0';
+ listlen++;
+
+ sendAuthRequest(port, AUTH_REQ_SASL, sasl_mechs, listlen);
+ pfree(sasl_mechs);
+
+#ifdef USE_SSL
+ /*
+ * Fetch the data related to the SSL finish message to be used in the
+ * exchange.
+ */
+ if (port->ssl_in_use)
+ {
+ tls_finished = be_tls_get_peer_finish(port, &tls_finished_len);
+ }
+#endif
/*
* Initialize the status tracker for message exchanges.
@@ -903,7 +944,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
@@ -952,15 +997,17 @@ 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.
+ * We only support SCRAM-SHA-256 and SCRAM-SHA-256-PLUS 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),
- errmsg("client selected an invalid SASL authentication mechanism")));
+ errmsg("client selected an invalid SASL authentication mechanism"),
+ errdetail("Incorrect SASL mechanism name specified")));
}
inputlen = pq_getmsgint(&buf, 4);
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index fe15227a77..d063a587d0 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -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 finish message information from the
+ * client, useful for authorization when doing channel binding.
+ *
+ * Result is a palloc'd copy of the TLS finish message with its size.
+ */
+char *
+be_tls_get_peer_finish(Port *port, int *len)
+{
+ char dummy[1];
+ char *result;
+
+ /*
+ * OpenSSL does not offer an API to get directly the length of the
+ * expected TLS finish 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 = (char *) palloc(*len * sizeof(char));
+ (void) SSL_get_peer_finished(port->ssl, result, *len);
+
+ return result;
+}
+
/*
* Convert an X509 subject name to a cstring.
*
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 7bde744d51..5d59d79822 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -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_cipher(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_finish(Port *port, int *len);
#endif
extern ProtocolVersion FrontendProtocol;
diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h
index 0166e1945d..43cde0e46e 100644
--- a/src/include/libpq/scram.h
+++ b/src/include/libpq/scram.h
@@ -13,8 +13,12 @@
#ifndef 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_PLUS_NAME "SCRAM-SHA-256-PLUS" /* with channel binding */
+
+/* Channel binding names */
+#define SCRAM_CHANNEL_TLS_UNIQUE "tls-unique"
/* Status codes for message exchange */
#define SASL_EXCHANGE_CONTINUE 0
@@ -22,7 +26,9 @@
#define SASL_EXCHANGE_FAILURE 2
/* 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, char *tls_finished_message,
+ int tls_finished_len);
extern int pg_be_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen, char **logdetail);
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index edfd42df85..9bce4d980a 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -17,6 +17,7 @@
#include "common/base64.h"
#include "common/saslprep.h"
#include "common/scram-common.h"
+#include "libpq/scram.h"
#include "fe-auth.h"
/* These are needed for getpid(), in the fallback implementation */
@@ -44,6 +45,13 @@ typedef struct
/* These are supplied by the user */
const char *username;
char *password;
+ bool ssl_in_use;
+ bool channel_binding_advertised;
+ char *tls_finished_message;
+ int tls_finished_len;
+ /* enforce-able user parameters */
+ char *saslmechanism; /* name of mechanism used */
+ char *saslchannelbinding; /* name of channel binding to use */
/* We construct these */
uint8 SaltedPassword[SCRAM_KEY_LEN];
@@ -81,18 +89,41 @@ static bool pg_frontend_random(char *dst, int len);
* Initialize SCRAM exchange status.
*/
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,
+ bool channel_binding_advertised,
+ const char *saslmechanism,
+ char *saslchannelbinding,
+ char *tls_finished_message,
+ int tls_finished_len)
{
fe_scram_state *state;
char *prep_password;
pg_saslprep_rc rc;
+ Assert(saslmechanism != NULL);
+
state = (fe_scram_state *) malloc(sizeof(fe_scram_state));
if (!state)
return NULL;
memset(state, 0, sizeof(fe_scram_state));
state->state = FE_SCRAM_INIT;
state->username = username;
+ state->ssl_in_use = ssl_in_use;
+ state->channel_binding_advertised = channel_binding_advertised;
+ state->tls_finished_message = tls_finished_message;
+ state->tls_finished_len = tls_finished_len;
+ state->saslmechanism = strdup(saslmechanism);
+
+ /*
+ * If user has specified a channel binding to use, enforce the
+ * channel binding sent to it. The default is "tls-unique".
+ */
+ if (saslchannelbinding && strlen(saslchannelbinding) > 0)
+ state->saslchannelbinding = strdup(saslchannelbinding);
+ else
+ state->saslchannelbinding = strdup(SCRAM_CHANNEL_TLS_UNIQUE);
/* Normalize the password with SASLprep, if possible */
rc = pg_saslprep(password, &prep_password);
@@ -125,6 +156,12 @@ pg_fe_scram_free(void *opaq)
if (state->password)
free(state->password);
+ if (state->tls_finished_message)
+ free(state->tls_finished_message);
+ if (state->saslmechanism)
+ free(state->saslmechanism);
+ if (state->saslchannelbinding)
+ free(state->saslchannelbinding);
/* client messages */
if (state->client_nonce)
@@ -297,9 +334,10 @@ static char *
build_client_first_message(fe_scram_state *state, PQExpBuffer errormessage)
{
char raw_nonce[SCRAM_RAW_NONCE_LEN + 1];
- char *buf;
- char buflen;
+ char *result;
+ int channel_info_len;
int encoded_len;
+ PQExpBufferData buf;
/*
* Generate a "raw" nonce. This is converted to ASCII-printable form by
@@ -328,26 +366,88 @@ build_client_first_message(fe_scram_state *state, PQExpBuffer errormessage)
* prepared with SASLprep, the message parsing would fail if it includes
* '=' or ',' characters.
*/
- buflen = 8 + strlen(state->client_nonce) + 1;
- buf = malloc(buflen);
- if (buf == NULL)
+ initPQExpBuffer(&buf);
+
+ /*
+ * First build the query field for channel binding. Conditions used for
+ * the decision-making are described in more details below.
+ */
+#ifdef USE_SSL
+ if (strcmp(state->saslmechanism, SCRAM_SHA256_PLUS_NAME) == 0)
{
- printfPQExpBuffer(errormessage,
- libpq_gettext("out of memory\n"));
- return NULL;
- }
- snprintf(buf, buflen, "n,,n=,r=%s", state->client_nonce);
+ if (!state->ssl_in_use)
+ {
+ /*
+ * Attempting to use channel binding, but this is not supported
+ * in non-SSL contexts, so complain.
+ */
+ printfPQExpBuffer(errormessage,
+ libpq_gettext("channel binding not supported in non-SSL context.\n"));
+ return NULL;
+ }
- state->client_first_message_bare = strdup(buf + 3);
- if (!state->client_first_message_bare)
+ if (state->channel_binding_advertised)
+ {
+ /*
+ * Channel binding is wanted by client and server supports it
+ * so allow its use.
+ */
+ appendPQExpBuffer(&buf, "p=%s", state->saslchannelbinding);
+ }
+ else
+ {
+ /*
+ * Client supports channel binding and expects the server to
+ * do so as well, so let the server know about that. Note that
+ * as authentication is not complete yet, we do not know the
+ * backend version, so there is not much we can do about it.
+ */
+ appendPQExpBuffer(&buf, "y");
+ return NULL;
+ }
+ }
+ else
{
- free(buf);
- printfPQExpBuffer(errormessage,
- libpq_gettext("out of memory\n"));
- return NULL;
+ /*
+ * Caller may not wish to use channel binding, so let server know
+ * that.
+ */
+ appendPQExpBuffer(&buf, "n");
}
+#else
+ /* Build does not support SSL, so it does not support channel binding */
+ appendPQExpBuffer(&buf, "n");
+#endif
+
+ if (PQExpBufferDataBroken(buf))
+ goto oom_error;
- return buf;
+ 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)
+ goto oom_error;
+
+ result = strdup(buf.data);
+ if (result == NULL)
+ goto oom_error;
+
+ termPQExpBuffer(&buf);
+ return result;
+
+oom_error:
+ termPQExpBuffer(&buf);
+ printfPQExpBuffer(errormessage,
+ libpq_gettext("out of memory\n"));
+ return NULL;
}
/*
@@ -365,8 +465,57 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
/*
* Construct client-final-message-without-proof. We need to remember it
* for verifying the server proof in the final step of authentication.
+ * Client needs to provide a b64 encoded string of the TLS finish message
+ * only if a SSL connection is attempted.
*/
- appendPQExpBuffer(&buf, "c=biws,r=%s", state->nonce);
+#ifdef USE_SSL
+ if (strcmp(state->saslmechanism, SCRAM_SHA256_PLUS_NAME) == 0)
+ {
+ char *raw_data;
+ int raw_data_len;
+
+ if (strcmp(state->saslchannelbinding, SCRAM_CHANNEL_TLS_UNIQUE) == 0)
+ {
+ raw_data = state->tls_finished_message;
+ raw_data_len = state->tls_finished_len;
+ }
+ else
+ {
+ /* should not happen */
+ termPQExpBuffer(&buf);
+ printfPQExpBuffer(errormessage,
+ libpq_gettext("incorrect channel binding name\n"));
+ return NULL;
+ }
+
+ /* should not happen, but better safe than sorry */
+ if (raw_data == NULL)
+ {
+ /* should not happen */
+ termPQExpBuffer(&buf);
+ printfPQExpBuffer(errormessage,
+ libpq_gettext("empty binding data for channel name \"%s\"\n"),
+ state->saslchannelbinding);
+ return NULL;
+ }
+
+ appendPQExpBuffer(&buf, "c=");
+
+ if (!enlargePQExpBuffer(&buf, pg_b64_enc_len(raw_data_len)))
+ goto oom_error;
+ buf.len += pg_b64_encode(raw_data, raw_data_len, buf.data + buf.len);
+ buf.data[buf.len] = '\0';
+ }
+ else
+ appendPQExpBuffer(&buf, "c=biws");
+#else
+ appendPQExpBuffer(&buf, "c=biws");
+#endif
+
+ if (PQExpBufferDataBroken(buf))
+ goto oom_error;
+
+ appendPQExpBuffer(&buf, ",r=%s", state->nonce);
if (PQExpBufferDataBroken(buf))
goto oom_error;
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 382558f3f8..fa24c88522 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -491,6 +491,10 @@ pg_SASL_init(PGconn *conn, int payloadlen)
bool success;
const char *selected_mechanism;
PQExpBufferData mechanism_buf;
+ bool channel_binding_advertised = false;
+ char *tls_finished = NULL;
+ int tls_finished_len = 0;
+ char *password;
initPQExpBuffer(&mechanism_buf);
@@ -504,7 +508,8 @@ pg_SASL_init(PGconn *conn, int payloadlen)
/*
* Parse the list of SASL authentication mechanisms in the
* 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;
for (;;)
@@ -522,6 +527,15 @@ pg_SASL_init(PGconn *conn, int payloadlen)
if (mechanism_buf.data[0] == '\0')
break;
+ /*
+ * The SASL exchange needs to know if channel binding has been
+ * advertized. This prevents attacks from rogue servers that
+ * may have the correct version but are not telling clients that
+ * about channel binding.
+ */
+ if (strcmp(mechanism_buf.data, SCRAM_SHA256_PLUS_NAME) == 0)
+ channel_binding_advertised = true;
+
/*
* If we have already selected a mechanism, just skip through the rest
* of the list.
@@ -532,25 +546,26 @@ pg_SASL_init(PGconn *conn, int payloadlen)
/*
* Do we support this mechanism?
*/
- if (strcmp(mechanism_buf.data, SCRAM_SHA256_NAME) == 0)
+ if (strcmp(mechanism_buf.data, SCRAM_SHA256_NAME) == 0 ||
+ strcmp(mechanism_buf.data, SCRAM_SHA256_PLUS_NAME) == 0)
{
- char *password;
-
- conn->password_needed = true;
- password = conn->connhost[conn->whichhost].password;
- if (password == NULL)
- password = conn->pgpass;
- if (password == NULL || password[0] == '\0')
- {
- printfPQExpBuffer(&conn->errorMessage,
- PQnoPasswordSupplied);
- goto error;
- }
-
- conn->sasl_state = pg_fe_scram_init(conn->pguser, password);
- if (!conn->sasl_state)
- goto oom_error;
- selected_mechanism = SCRAM_SHA256_NAME;
+ /*
+ * Select the mechanism to use by default. If SSL connection
+ * is attempted, the server will expect the -PLUS mechanism.
+ * If not, fallback to SCRAM-SHA-256.
+ */
+#ifdef USE_SSL
+ if (conn->ssl_in_use &&
+ strcmp(mechanism_buf.data, SCRAM_SHA256_PLUS_NAME) == 0)
+ selected_mechanism = SCRAM_SHA256_PLUS_NAME;
+ else if (!conn->ssl_in_use &&
+ strcmp(mechanism_buf.data, SCRAM_SHA256_NAME) == 0)
+ selected_mechanism = SCRAM_SHA256_NAME;
+#else
+ /* No channel binding can be selected without SSL support */
+ if (strcmp(mechanism_buf.data, SCRAM_SHA256_NAME) == 0)
+ selected_mechanism = SCRAM_SHA256_NAME;
+#endif
}
}
@@ -561,6 +576,53 @@ pg_SASL_init(PGconn *conn, int payloadlen)
goto error;
}
+ /*
+ * Now that the SASL mechanism has been chosen for the exchange,
+ * initialize its state information. First select the password to
+ * use for the exchange protocol, complaining back if none are
+ * provided.
+ */
+ conn->password_needed = true;
+ password = conn->connhost[conn->whichhost].password;
+ if (password == NULL)
+ password = conn->pgpass;
+ if (password == NULL || password[0] == '\0')
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ PQnoPasswordSupplied);
+ goto error;
+ }
+
+#ifdef USE_SSL
+ /*
+ * Fetch information about the TLS finish message and client
+ * certificate if any.
+ */
+ if (conn->ssl_in_use)
+ {
+ tls_finished = pgtls_get_finish(conn, &tls_finished_len);
+ if (tls_finished == NULL)
+ goto oom_error;
+ }
+#endif
+
+ /*
+ * Initialize the SASL state information with all the information
+ * gathered during the initial exchange.
+ *
+ * Note: Only tls-unique is supported for the moment.
+ */
+ conn->sasl_state = pg_fe_scram_init(conn->pguser,
+ password,
+ conn->ssl_in_use,
+ channel_binding_advertised,
+ selected_mechanism,
+ NULL, /* default channel binding */
+ tls_finished,
+ tls_finished_len);
+ if (!conn->sasl_state)
+ goto oom_error;
+
/* Get the mechanism-specific Initial Client Response, if any */
pg_fe_scram_exchange(conn->sasl_state,
NULL, -1,
diff --git a/src/interfaces/libpq/fe-auth.h b/src/interfaces/libpq/fe-auth.h
index 5dc6bb5341..81378d0008 100644
--- a/src/interfaces/libpq/fe-auth.h
+++ b/src/interfaces/libpq/fe-auth.h
@@ -23,7 +23,10 @@ extern int pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn);
extern char *pg_fe_getauthname(PQExpBuffer errorMessage);
/* 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, bool channel_binding_advertised,
+ const char *saslmechanism,char *saslchannelbinding,
+ char *tls_finished_message, int tls_finished_len);
extern void pg_fe_scram_free(void *opaq);
extern void pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen,
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 2f29820e82..84a6e3c322 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -393,6 +393,32 @@ pgtls_write(PGconn *conn, const void *ptr, size_t len)
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_finish(PGconn *conn, int *len)
+{
+ char dummy[1];
+ char *result;
+
+ /*
+ * OpenSSL does not offer an API to get directly the length of the
+ * TLS finish 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 */
/* ------------------------------------------------------------ */
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 42913604e3..0eb8b60c95 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -453,11 +453,13 @@ struct pg_conn
/* Assorted state for SASL, SSL, GSS, etc */
void *sasl_state;
+ /* SSL structures */
+ bool ssl_in_use;
+
#ifdef USE_SSL
bool allow_ssl_try; /* Allowed to try SSL negotiation */
bool wait_ssl_try; /* Delay SSL negotiation until after
* attempting normal connection */
- bool ssl_in_use;
#ifdef USE_OPENSSL
SSL *ssl; /* SSL status, if have SSL connection */
X509 *peer; /* X509 cert of server */
@@ -668,6 +670,7 @@ extern void pgtls_close(PGconn *conn);
extern ssize_t pgtls_read(PGconn *conn, void *ptr, size_t len);
extern bool pgtls_read_pending(PGconn *conn);
extern ssize_t pgtls_write(PGconn *conn, const void *ptr, size_t len);
+extern char *pgtls_get_finish(PGconn *conn, int *len);
/*
* this is so that we can check if a connection is non-blocking internally
diff --git a/src/test/ssl/ServerSetup.pm b/src/test/ssl/ServerSetup.pm
index ad2e036602..b71969ac75 100644
--- a/src/test/ssl/ServerSetup.pm
+++ b/src/test/ssl/ServerSetup.pm
@@ -91,6 +91,9 @@ sub configure_test_server_for_ssl
{
my $node = $_[0];
my $serverhost = $_[1];
+ my $sslmethod = $_[2];
+ my $passwd = $_[3];
+ my $passwdhash = $_[4];
my $pgdata = $node->data_dir;
@@ -100,6 +103,15 @@ sub configure_test_server_for_ssl
$node->psql('postgres', "CREATE DATABASE trustdb");
$node->psql('postgres', "CREATE DATABASE certdb");
+ # Update password of each user as needed.
+ if (defined($passwd))
+ {
+ $node->psql('postgres',
+"SET password_encryption='$passwdhash'; ALTER USER ssltestuser PASSWORD '$passwd';");
+ $node->psql('postgres',
+"SET password_encryption='$passwdhash'; ALTER USER anotheruser PASSWORD '$passwd';");
+ }
+
# enable logging etc.
open my $conf, '>>', "$pgdata/postgresql.conf";
print $conf "fsync=off\n";
@@ -129,7 +141,7 @@ sub configure_test_server_for_ssl
$node->restart;
# Change pg_hba after restart because hostssl requires ssl=on
- configure_hba_for_ssl($node, $serverhost);
+ configure_hba_for_ssl($node, $serverhost, $sslmethod);
}
# Change the configuration to use given server cert file, and reload
@@ -159,6 +171,7 @@ sub configure_hba_for_ssl
{
my $node = $_[0];
my $serverhost = $_[1];
+ my $sslmethod = $_[2];
my $pgdata = $node->data_dir;
# Only accept SSL connections from localhost. Our tests don't depend on this
@@ -169,9 +182,9 @@ sub configure_hba_for_ssl
print $hba
"# TYPE DATABASE USER ADDRESS METHOD\n";
print $hba
-"hostssl trustdb ssltestuser $serverhost/32 trust\n";
+"hostssl trustdb ssltestuser $serverhost/32 $sslmethod\n";
print $hba
-"hostssl trustdb ssltestuser ::1/128 trust\n";
+"hostssl trustdb ssltestuser ::1/128 $sslmethod\n";
print $hba
"hostssl certdb ssltestuser $serverhost/32 cert\n";
print $hba
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 890e3051a2..e690c1fa15 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -32,7 +32,7 @@ $node->init;
$ENV{PGHOST} = $node->host;
$ENV{PGPORT} = $node->port;
$node->start;
-configure_test_server_for_ssl($node, $SERVERHOSTADDR);
+configure_test_server_for_ssl($node, $SERVERHOSTADDR, "trust");
switch_server_cert($node, 'server-cn-only');
### Part 1. Run client-side tests.
diff --git a/src/test/ssl/t/002_sasl.pl b/src/test/ssl/t/002_sasl.pl
new file mode 100644
index 0000000000..d43a970b55
--- /dev/null
+++ b/src/test/ssl/t/002_sasl.pl
@@ -0,0 +1,41 @@
+use strict;
+use warnings;
+use PostgresNode;
+use TestLib;
+use Test::More tests => 1;
+use ServerSetup;
+use File::Copy;
+
+# test combinations of SASL authentication for SCRAM mechanism:
+# - SCRAM-SHA-256 and SCRAM-SHA-256-PLUS
+# - Channel bindings
+
+# 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;
+
+#### Part 0. 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";
+
+# Tests with default channel binding and SASL mechanism names.
+# tls-unique is used here with channel binding.
+test_connect_ok($common_connstr, "");
--
2.15.0
0003-Add-connection-parameters-saslname-and-saslchannelbi.patchapplication/octet-stream; name=0003-Add-connection-parameters-saslname-and-saslchannelbi.patchDownload
From 2873e223b78eafb2f6168154d4f95161343c57c7 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 10 Oct 2017 21:49:37 +0900
Subject: [PATCH 3/4] Add connection parameters "saslname" and
"saslchannelbinding"
Those parameters can be used to respectively enforce the value of the
SASL mechanism name and the channel binding name sent to server during
a SASL message exchange.
A set of tests dedicated to SASL and channel binding is added as well
to the SSL test suite, which is handy to check the validity of a patch.
---
doc/src/sgml/libpq.sgml | 24 ++++++++++++++++++++++++
src/backend/libpq/auth-scram.c | 1 +
src/interfaces/libpq/fe-auth.c | 9 ++++++++-
src/interfaces/libpq/fe-connect.c | 13 +++++++++++++
src/interfaces/libpq/libpq-int.h | 2 ++
src/test/ssl/t/002_sasl.pl | 18 +++++++++++++++---
6 files changed, 63 insertions(+), 4 deletions(-)
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 694921f212..432589b815 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1222,6 +1222,30 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
</listitem>
</varlistentry>
+ <varlistentry id="libpq-saslname" xreflabel="saslname">
+ <term><literal>saslname</literal></term>
+ <listitem>
+ <para>
+ Controls the name of the SASL mechanism name sent to server when doing
+ a message exchange for a SASL authentication. The list of SASL
+ mechanisms supported by server are listed in
+ <xref linkend="sasl-authentication">.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="libpq-saslchannelbinding" xreflabel="saslchannelbinding">
+ <term><literal>saslchannelbinding</literal></term>
+ <listitem>
+ <para>
+ Controls the name of the channel binding name sent to server when doing
+ a message exchange for a SASL authentication. The list of channel
+ binding names supported by server are listed in
+ <xref linkend="sasl-authentication">.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="libpq-connect-sslmode" xreflabel="sslmode">
<term><literal>sslmode</literal></term>
<listitem>
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index b326c8a39e..b90be7f1ae 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -1097,6 +1097,7 @@ read_client_final_message(scram_state *state, char *input)
* client has to provide channel binding value if needed.
*/
channel_binding = read_attr_value(&p, 'c');
+
#ifdef USE_SSL
if (state->ssl_in_use &&
state->channel_binding)
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index fa24c88522..97de9a0a30 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -569,6 +569,13 @@ pg_SASL_init(PGconn *conn, int payloadlen)
}
}
+ /*
+ * If user has asked for a specific mechanism name, enforce the chosen
+ * name to it.
+ */
+ if (conn->saslname && strlen(conn->saslname) > 0)
+ selected_mechanism = conn->saslname;
+
if (!selected_mechanism)
{
printfPQExpBuffer(&conn->errorMessage,
@@ -617,7 +624,7 @@ pg_SASL_init(PGconn *conn, int payloadlen)
conn->ssl_in_use,
channel_binding_advertised,
selected_mechanism,
- NULL, /* default channel binding */
+ conn->saslchannelbinding,
tls_finished,
tls_finished_len);
if (!conn->sasl_state)
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 2c175a2a24..b092fa6d70 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -262,6 +262,15 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
"TCP-Keepalives-Count", "", 10, /* strlen(INT32_MAX) == 10 */
offsetof(struct pg_conn, keepalives_count)},
+ /* Set of options proper to SASL */
+ {"saslname", NULL, NULL, NULL,
+ "SASL-Name", "", 21, /* maximum name size per IANA == 21 */
+ offsetof(struct pg_conn, saslname)},
+
+ {"saslchannelbinding", NULL, NULL, NULL,
+ "SASL-Channel", "", 22, /* sizeof("tls-unique-for-telnet") == 22 */
+ offsetof(struct pg_conn, saslchannelbinding)},
+
/*
* ssl options are allowed even without client SSL support because the
* client can still handle SSL modes "disable" and "allow". Other
@@ -3469,6 +3478,10 @@ freePGconn(PGconn *conn)
free(conn->keepalives_interval);
if (conn->keepalives_count)
free(conn->keepalives_count);
+ if (conn->saslname)
+ free(conn->saslname);
+ if (conn->saslchannelbinding)
+ free(conn->saslchannelbinding);
if (conn->sslmode)
free(conn->sslmode);
if (conn->sslcert)
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 0eb8b60c95..6d500aa5db 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -349,6 +349,8 @@ struct pg_conn
* retransmits */
char *keepalives_count; /* maximum number of TCP keepalive
* retransmits */
+ char *saslname; /* SASL mechanism name */
+ char *saslchannelbinding; /* channel binding used in SASL */
char *sslmode; /* SSL mode (require,prefer,allow,disable) */
char *sslcompression; /* SSL compression (0 or 1) */
char *sslkey; /* client key filename */
diff --git a/src/test/ssl/t/002_sasl.pl b/src/test/ssl/t/002_sasl.pl
index d43a970b55..e9226903ca 100644
--- a/src/test/ssl/t/002_sasl.pl
+++ b/src/test/ssl/t/002_sasl.pl
@@ -2,7 +2,7 @@ use strict;
use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 1;
+use Test::More tests => 6;
use ServerSetup;
use File::Copy;
@@ -37,5 +37,17 @@ $common_connstr =
"user=ssltestuser dbname=trustdb sslmode=require hostaddr=$SERVERHOSTADDR";
# Tests with default channel binding and SASL mechanism names.
-# tls-unique is used here with channel binding.
-test_connect_ok($common_connstr, "");
+# tls-unique is used here
+test_connect_ok($common_connstr, "saslname=SCRAM-SHA-256-PLUS");
+test_connect_fails($common_connstr, "saslname=not-exists");
+# Having a client willing to not use channel binding should work, and
+# so should this series.
+test_connect_ok($common_connstr, "saslname=SCRAM-SHA-256");
+test_connect_ok($common_connstr,
+ "saslname=SCRAM-SHA-256 saslchannelbinding=tls-unique");
+
+# Channel bindings
+test_connect_ok($common_connstr,
+ "saslname=SCRAM-SHA-256-PLUS saslchannelbinding=tls-unique");
+test_connect_fails($common_connstr,
+ "saslname=SCRAM-SHA-256-PLUS saslchannelbinding=not-exists");
--
2.15.0
0004-Implement-channel-binding-tls-server-end-point-for-S.patchapplication/octet-stream; name=0004-Implement-channel-binding-tls-server-end-point-for-S.patchDownload
From c86373764a0c7344eddf898a27a0ee3c28265b65 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 10 Oct 2017 22:04:22 +0900
Subject: [PATCH 4/4] Implement channel binding tls-server-end-point for SCRAM
As referenced in RFC 5929, this channel binding is not the default value
and uses a hash of the certificate as binding data. On the frontend, this
can be resumed in getting the data from SSL_get_peer_certificate() and
on the backend SSL_get_certificate().
The hashing algorithm needs also to switch to SHA-256 if the signature
algorithm is MD5 or SHA-1, so let's be careful about that.
---
doc/src/sgml/protocol.sgml | 3 +-
src/backend/libpq/auth-scram.c | 21 +++++++--
src/backend/libpq/auth.c | 13 ++++--
src/backend/libpq/be-secure-openssl.c | 61 +++++++++++++++++++++++++
src/include/libpq/libpq-be.h | 1 +
src/include/libpq/scram.h | 4 +-
src/interfaces/libpq/fe-auth-scram.c | 20 +++++++-
src/interfaces/libpq/fe-auth.c | 12 ++++-
src/interfaces/libpq/fe-auth.h | 3 +-
src/interfaces/libpq/fe-secure-openssl.c | 78 ++++++++++++++++++++++++++++++++
src/interfaces/libpq/libpq-int.h | 1 +
src/test/ssl/t/002_sasl.pl | 6 ++-
12 files changed, 209 insertions(+), 14 deletions(-)
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index e2a3d8f4d5..9c3b8b1ded 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1551,7 +1551,8 @@ the password is in.
<firstterm>Channel binding</firstterm> is supported in builds with SSL
support, and uses as mechanism name <literal>SCRAM-SHA-256-PLUS</literal>
for this purpose as defined per IANA. The only channel binding type
-supported by the server is <literal>tls-unique</literal>.
+supported by the server is <literal>tls-unique</literal> and
+<literal>tls-server-end-point</literal>.
</para>
<procedure>
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index b90be7f1ae..ab2ff2f676 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -113,6 +113,8 @@ typedef struct
bool ssl_in_use;
char *tls_finished_message;
int tls_finished_len;
+ char *certificate_hash;
+ int certificate_hash_len;
char *channel_binding;
int iterations;
@@ -175,7 +177,9 @@ pg_be_scram_init(const char *username,
const char *shadow_pass,
bool ssl_in_use,
char *tls_finished_message,
- int tls_finished_len)
+ int tls_finished_len,
+ char *certificate_hash,
+ int certificate_hash_len)
{
scram_state *state;
bool got_verifier;
@@ -186,6 +190,8 @@ pg_be_scram_init(const char *username,
state->ssl_in_use = ssl_in_use;
state->tls_finished_message = tls_finished_message;
state->tls_finished_len = tls_finished_len;
+ state->certificate_hash = certificate_hash;
+ state->certificate_hash_len = certificate_hash_len;
state->channel_binding = NULL;
/*
@@ -835,11 +841,12 @@ read_client_first_message(scram_state *state, char *input)
errmsg("client requires SCRAM channel binding, but it is not supported")));
/*
- * Read value provided by client, only tls-unique is supported
- * for now.
+ * Read value provided by client, only tls-unique and
+ * tls-server-end-point are supported for now.
*/
channel_name = read_attr_value(&input, 'p');
- if (strcmp(channel_name, SCRAM_CHANNEL_TLS_UNIQUE) != 0)
+ if (strcmp(channel_name, SCRAM_CHANNEL_TLS_UNIQUE) != 0 &&
+ strcmp(channel_name, SCRAM_CHANNEL_TLS_ENDPOINT) != 0)
ereport(ERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION),
(errmsg("unexpected SCRAM channel-binding type"))));
@@ -1111,6 +1118,12 @@ read_client_final_message(scram_state *state, char *input)
raw_data = state->tls_finished_message;
raw_data_len = state->tls_finished_len;
}
+ else if (strcmp(state->channel_binding,
+ SCRAM_CHANNEL_TLS_ENDPOINT) == 0)
+ {
+ raw_data = state->certificate_hash;
+ raw_data_len = state->certificate_hash_len;
+ }
else
{
/* should not happen */
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 055c619cb9..e2571da438 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -873,6 +873,8 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
bool initial;
char *tls_finished = NULL;
int tls_finished_len = 0;
+ char *certificate_hash = NULL;
+ int certificate_hash_len = 0;
/*
* SASL auth is not supported for protocol versions before 3, because it
@@ -924,12 +926,15 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
#ifdef USE_SSL
/*
- * Fetch the data related to the SSL finish message to be used in the
- * exchange.
+ * Fetch the data related to the SSL finish message and the client
+ * certificate (if any) to be used in the exchange.
*/
if (port->ssl_in_use)
{
tls_finished = be_tls_get_peer_finish(port, &tls_finished_len);
+ certificate_hash =
+ be_tls_get_certificate_hash(port,
+ &certificate_hash_len);
}
#endif
@@ -948,7 +953,9 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
shadow_pass,
port->ssl_in_use,
tls_finished,
- tls_finished_len);
+ tls_finished_len,
+ certificate_hash,
+ certificate_hash_len);
/*
* Loop through SASL message exchange. This exchange can consist of
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index d063a587d0..bf9cef7f15 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -1239,6 +1239,67 @@ be_tls_get_peer_finish(Port *port, int *len)
return result;
}
+/*
+ * Get the server certificate hash for authentication purposes. Per
+ * RFC 5929 and tls-server-end-point, the TLS server's certificate bytes
+ * need to be hashed with SHA-256 if its signature algorithm is MD5 or
+ * SHA-1 as per RFC 5929 (https://tools.ietf.org/html/rfc5929#section-4.1).
+ * If something else is used, the same hash as the signature algorithm is
+ * used. The result is a palloc'd hash of the server certificate with its
+ * size, and NULL if there is no certificate available.
+ */
+char *
+be_tls_get_certificate_hash(Port *port, int *len)
+{
+ char *cert_hash = NULL;
+ X509 *server_cert;
+
+ *len = 0;
+ server_cert = SSL_get_certificate(port->ssl);
+
+ if (server_cert != NULL)
+ {
+ const EVP_MD *algo_type = NULL;
+ char hash[EVP_MAX_MD_SIZE]; /* size for SHA-512 */
+ unsigned int hash_size;
+ int algo_nid;
+
+ /*
+ * Get the signature algorithm of the certificate to determine the
+ * hash algorithm to use for the result.
+ */
+ if (!OBJ_find_sigid_algs(X509_get_signature_nid(server_cert),
+ &algo_nid, NULL))
+ elog(ERROR, "could not find signature algorithm");
+
+ switch (algo_nid)
+ {
+ case NID_md5:
+ case NID_sha1:
+ algo_type = EVP_sha256();
+ break;
+
+ default:
+ algo_type = EVP_get_digestbynid(algo_nid);
+ if (algo_type == NULL)
+ elog(ERROR, "could not find digest for NID %s",
+ OBJ_nid2sn(algo_nid));
+ break;
+ }
+
+ /* generate and save the certificate hash */
+ if (!X509_digest(server_cert, algo_type, (unsigned char *) hash,
+ &hash_size))
+ elog(ERROR, "could not generate server certificate hash");
+
+ cert_hash = (char *) palloc(hash_size);
+ memcpy(cert_hash, hash, hash_size);
+ *len = hash_size;
+ }
+
+ return cert_hash;
+}
+
/*
* Convert an X509 subject name to a cstring.
*
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 5d59d79822..0392860a87 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -210,6 +210,7 @@ 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_peerdn_name(Port *port, char *ptr, size_t len);
extern char *be_tls_get_peer_finish(Port *port, int *len);
+extern char *be_tls_get_certificate_hash(Port *port, int *len);
#endif
extern ProtocolVersion FrontendProtocol;
diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h
index 43cde0e46e..6329e111ba 100644
--- a/src/include/libpq/scram.h
+++ b/src/include/libpq/scram.h
@@ -19,6 +19,7 @@
/* Channel binding names */
#define SCRAM_CHANNEL_TLS_UNIQUE "tls-unique"
+#define SCRAM_CHANNEL_TLS_ENDPOINT "tls-server-end-point"
/* Status codes for message exchange */
#define SASL_EXCHANGE_CONTINUE 0
@@ -28,7 +29,8 @@
/* Routines dedicated to authentication */
extern void *pg_be_scram_init(const char *username, const char *shadow_pass,
bool ssl_in_use, char *tls_finished_message,
- int tls_finished_len);
+ int tls_finished_len, char *certificate_hash,
+ int certificate_hash_len);
extern int pg_be_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen, char **logdetail);
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index 9bce4d980a..307e58f586 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -49,6 +49,8 @@ typedef struct
bool channel_binding_advertised;
char *tls_finished_message;
int tls_finished_len;
+ char *certificate_hash;
+ int certificate_hash_len;
/* enforce-able user parameters */
char *saslmechanism; /* name of mechanism used */
char *saslchannelbinding; /* name of channel binding to use */
@@ -96,7 +98,9 @@ pg_fe_scram_init(const char *username,
const char *saslmechanism,
char *saslchannelbinding,
char *tls_finished_message,
- int tls_finished_len)
+ int tls_finished_len,
+ char *certificate_hash,
+ int certificate_hash_len)
{
fe_scram_state *state;
char *prep_password;
@@ -115,6 +119,8 @@ pg_fe_scram_init(const char *username,
state->tls_finished_message = tls_finished_message;
state->tls_finished_len = tls_finished_len;
state->saslmechanism = strdup(saslmechanism);
+ state->certificate_hash = certificate_hash;
+ state->certificate_hash_len = certificate_hash_len;
/*
* If user has specified a channel binding to use, enforce the
@@ -158,6 +164,8 @@ pg_fe_scram_free(void *opaq)
free(state->password);
if (state->tls_finished_message)
free(state->tls_finished_message);
+ if (state->certificate_hash)
+ free(state->certificate_hash);
if (state->saslmechanism)
free(state->saslmechanism);
if (state->saslchannelbinding)
@@ -466,7 +474,9 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
* Construct client-final-message-without-proof. We need to remember it
* for verifying the server proof in the final step of authentication.
* Client needs to provide a b64 encoded string of the TLS finish message
- * only if a SSL connection is attempted.
+ * only if a SSL connection is attempted using "tls-unique" as channel
+ * binding. For "tls-server-end-point", a hash of the client certificate
+ * is sent instead.
*/
#ifdef USE_SSL
if (strcmp(state->saslmechanism, SCRAM_SHA256_PLUS_NAME) == 0)
@@ -479,6 +489,12 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
raw_data = state->tls_finished_message;
raw_data_len = state->tls_finished_len;
}
+ else if (strcmp(state->saslchannelbinding,
+ SCRAM_CHANNEL_TLS_ENDPOINT) == 0)
+ {
+ raw_data = state->certificate_hash;
+ raw_data_len = state->certificate_hash_len;
+ }
else
{
/* should not happen */
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 97de9a0a30..56dbe2be28 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -494,6 +494,8 @@ pg_SASL_init(PGconn *conn, int payloadlen)
bool channel_binding_advertised = false;
char *tls_finished = NULL;
int tls_finished_len = 0;
+ char *certificate_hash = NULL;
+ int certificate_hash_len = 0;
char *password;
initPQExpBuffer(&mechanism_buf);
@@ -610,6 +612,12 @@ pg_SASL_init(PGconn *conn, int payloadlen)
tls_finished = pgtls_get_finish(conn, &tls_finished_len);
if (tls_finished == NULL)
goto oom_error;
+
+ certificate_hash =
+ pgtls_get_peer_certificate_hash(conn,
+ &certificate_hash_len);
+ if (certificate_hash == NULL)
+ goto error; /* error message is set */
}
#endif
@@ -626,7 +634,9 @@ pg_SASL_init(PGconn *conn, int payloadlen)
selected_mechanism,
conn->saslchannelbinding,
tls_finished,
- tls_finished_len);
+ tls_finished_len,
+ certificate_hash,
+ certificate_hash_len);
if (!conn->sasl_state)
goto oom_error;
diff --git a/src/interfaces/libpq/fe-auth.h b/src/interfaces/libpq/fe-auth.h
index 81378d0008..ed574d39e5 100644
--- a/src/interfaces/libpq/fe-auth.h
+++ b/src/interfaces/libpq/fe-auth.h
@@ -26,7 +26,8 @@ extern char *pg_fe_getauthname(PQExpBuffer errorMessage);
extern void *pg_fe_scram_init(const char *username, const char *password,
bool ssl_in_use, bool channel_binding_advertised,
const char *saslmechanism,char *saslchannelbinding,
- char *tls_finished_message, int tls_finished_len);
+ char *tls_finished_message, int tls_finished_len,
+ char *certificate_hash, int certificate_hash_len);
extern void pg_fe_scram_free(void *opaq);
extern void pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen,
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 84a6e3c322..301bde0799 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -418,6 +418,84 @@ pgtls_get_finish(PGconn *conn, int *len)
return result;
}
+/*
+ * Get the hash of the server certificate
+ *
+ * This information is useful for end-point channel binding, where
+ * the client certificate hash is used as a link, per RFC 5929. If
+ * the signature hash algorithm is MD5 or SHA-1, fall back to SHA-256,
+ * as per RFC 5929 (https://tools.ietf.org/html/rfc5929#section-4.1).
+ * NULL is sent back to the caller in the event of an error, with an
+ * error message for the caller to consume.
+ */
+char *
+pgtls_get_peer_certificate_hash(PGconn *conn, int *len)
+{
+ char *cert_hash = NULL;
+
+ *len = 0;
+
+ if (conn->peer)
+ {
+ X509 *peer_cert = conn->peer;
+ const EVP_MD *algo_type = NULL;
+ char hash[EVP_MAX_MD_SIZE]; /* size for SHA-512 */
+ unsigned int hash_size;
+ int algo_nid;
+
+ /*
+ * Get the signature algorithm of the certificate to determine the
+ * hash algorithm to use for the result.
+ */
+ if (!OBJ_find_sigid_algs(X509_get_signature_nid(peer_cert),
+ &algo_nid, NULL))
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("could not find signature algorithm\n"));
+ return NULL;
+ }
+
+ switch (algo_nid)
+ {
+ case NID_md5:
+ case NID_sha1:
+ algo_type = EVP_sha256();
+ break;
+
+ default:
+ algo_type = EVP_get_digestbynid(algo_nid);
+ if (algo_type == NULL)
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("could not find digest for NID %s\n"),
+ OBJ_nid2sn(algo_nid));
+ return NULL;
+ }
+ break;
+ }
+
+ if (!X509_digest(peer_cert, algo_type, (unsigned char *) hash,
+ &hash_size))
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("could not generate peer certificate hash\n"));
+ return NULL;
+ }
+
+ /* save result */
+ cert_hash = (char *) malloc(hash_size);
+ if (cert_hash == NULL)
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("out of memory\n"));
+ return NULL;
+ }
+ memcpy(cert_hash, hash, hash_size);
+ *len = hash_size;
+ }
+
+ return cert_hash;
+}
/* ------------------------------------------------------------ */
/* OpenSSL specific code */
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 6d500aa5db..f1e2c0bb3c 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -673,6 +673,7 @@ extern ssize_t pgtls_read(PGconn *conn, void *ptr, size_t len);
extern bool pgtls_read_pending(PGconn *conn);
extern ssize_t pgtls_write(PGconn *conn, const void *ptr, size_t len);
extern char *pgtls_get_finish(PGconn *conn, int *len);
+extern char *pgtls_get_peer_certificate_hash(PGconn *conn, int *len);
/*
* this is so that we can check if a connection is non-blocking internally
diff --git a/src/test/ssl/t/002_sasl.pl b/src/test/ssl/t/002_sasl.pl
index e9226903ca..fa3cca24a2 100644
--- a/src/test/ssl/t/002_sasl.pl
+++ b/src/test/ssl/t/002_sasl.pl
@@ -2,7 +2,7 @@ use strict;
use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 6;
+use Test::More tests => 8;
use ServerSetup;
use File::Copy;
@@ -45,9 +45,13 @@ test_connect_fails($common_connstr, "saslname=not-exists");
test_connect_ok($common_connstr, "saslname=SCRAM-SHA-256");
test_connect_ok($common_connstr,
"saslname=SCRAM-SHA-256 saslchannelbinding=tls-unique");
+test_connect_ok($common_connstr,
+ "saslname=SCRAM-SHA-256 saslchannelbinding=tls-server-end-point");
# Channel bindings
test_connect_ok($common_connstr,
"saslname=SCRAM-SHA-256-PLUS saslchannelbinding=tls-unique");
+test_connect_ok($common_connstr,
+ "saslname=SCRAM-SHA-256-PLUS saslchannelbinding=tls-server-end-point");
test_connect_fails($common_connstr,
"saslname=SCRAM-SHA-256-PLUS saslchannelbinding=not-exists");
--
2.15.0
On 11/14/17 03:55, Michael Paquier wrote:
On Tue, Oct 10, 2017 at 10:12 PM, Michael Paquier
<michael.paquier@gmail.com> wrote:Attached is a new patch set with the comments from above. On top of
that, I have changed a couple of things:
- 0001 is unchanged, still the same refactoring for the SSL tests.
- 0002 implements tls-unique, now including tests using the default
channel binding tls-unique with something in the SSL test suite. This
patch also now introduces all the infrastructure to plug in correctly
new libpq parameters and more channel binding types.
- 0003 is shorter, and introduces a set of libpq parameters useful for
tests, taking advantage of 0002. Another case where the connection
parameter saslname is useful is to enforce not using channel binding
when connecting to a v10 server using a SSL context with a v11 libpq.
- 0004 introduces tls-server-end-point.
This has required some work to get it shaped as wanted, I am adding it
to the next CF, as version 2.Documentation in protocol.sgml has rotten again as markups need proper
handling. So rebased.
Pushed 0001, will continue with 0002.
--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
On Thu, Nov 16, 2017 at 10:47 PM, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:
Pushed 0001, will continue with 0002.
Thanks!
--
Michael
On 11/16/17 16:50, Michael Paquier wrote:
On Thu, Nov 16, 2017 at 10:47 PM, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:Pushed 0001, will continue with 0002.
Thanks!
I have combed through the 0002 patch in detail now. I have attached my
result.
I cleaned up a bunch of variable names to be more precise. I also
removed some unnecessary code. For example the whole deal about
channel_binding_advertised was not necessary, because it's implied by
the chosen SASL mechanism. We also don't need to have separate USE_SSL
sections in most cases, because state->ssl_in_use already takes care of
that.
I made some significant changes to the logic.
The selection of the channel binding flag (n/y/p) in the client seemed
wrong. Your code treated 'y' as an error, but I think that is a
legitimate case, for example a PG11 client connecting to a PG10 server
over SSL. The client supports channel binding in that case and
(correctly) thinks the server does not, so we use the 'y' flag and
proceed normally without channel binding.
The selection of the mechanism in the client was similarly incorrect, I
think. The code would not tolerate a situation were an SSL connection
is in use but the server does not advertise the -PLUS mechanism, which
again could be from a PG10 server.
The creation of the channel binding data didn't match the spec, because
the gs2-header (p=type,,) was not included in the data put through
base64. This was done incorrectly on both server and client, so the
protocol still worked. (However, in the non-channel-binding case we
hardcode "biws", which is exactly the base64-encoding of the gs2-header.
So that was inconsistent.)
I think we also need to backpatch a bug fix into PG10 so that the server
can accept base64("y,,") as channel binding data. Otherwise, the above
scenario of a PG11 client connecting to a PG10 server over SSL will
currently fail because the server will not accept the channel binding data.
Please check my patch and think through these changes. I'm happy to
commit the patch as is if there are no additional insights.
--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
Attachments:
0001-Support-channel-binding-tls-unique-in-SCRAM.patchtext/plain; charset=UTF-8; name=0001-Support-channel-binding-tls-unique-in-SCRAM.patch; x-mac-creator=0; x-mac-type=0Download
From ae28f935473afe9f754a7a0ec2a6eca0162ab445 Mon Sep 17 00:00:00 2001
From: Peter Eisentraut <peter_e@gmx.net>
Date: Fri, 17 Nov 2017 14:15:50 -0500
Subject: [PATCH] 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 finish messages, a PG-like API is added to be able to make
the interface pluggable for other SSL implementations.
This commit also adds a lot of basic 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.
A set of basic tests to stress default channel binding handling is added
as well, though those are part of the SSL suite.
---
doc/src/sgml/protocol.sgml | 31 ++++--
src/backend/libpq/auth-scram.c | 180 +++++++++++++++++++++++++------
src/backend/libpq/auth.c | 54 ++++++++--
src/backend/libpq/be-secure-openssl.c | 24 +++++
src/include/libpq/libpq-be.h | 1 +
src/include/libpq/scram.h | 10 +-
src/interfaces/libpq/fe-auth-scram.c | 170 +++++++++++++++++++++++++----
src/interfaces/libpq/fe-auth.c | 90 +++++++++++-----
src/interfaces/libpq/fe-auth.h | 7 +-
src/interfaces/libpq/fe-secure-openssl.c | 27 +++++
src/interfaces/libpq/libpq-int.h | 5 +-
src/test/ssl/ServerSetup.pm | 27 +++--
src/test/ssl/t/001_ssltests.pl | 2 +-
src/test/ssl/t/002_scram.pl | 38 +++++++
14 files changed, 554 insertions(+), 112 deletions(-)
create mode 100644 src/test/ssl/t/002_scram.pl
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 6d4dcf83ac..4d3b6446c4 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1461,10 +1461,11 @@ <title>SASL Authentication</title>
<para>
<firstterm>SASL</firstterm> is a framework for authentication in connection-oriented
-protocols. At the moment, <productname>PostgreSQL</productname> implements only one SASL
-authentication mechanism, SCRAM-SHA-256, but more might be added in the
-future. The below steps illustrate how SASL authentication is performed in
-general, while the next subsection gives more details on SCRAM-SHA-256.
+protocols. At the moment, <productname>PostgreSQL</productname> implements two SASL
+authentication mechanisms, SCRAM-SHA-256 and SCRAM-SHA-256-PLUS. More
+might be added in the future. The below steps illustrate how SASL
+authentication is performed in general, while the next subsection gives
+more details on SCRAM-SHA-256 and SCRAM-SHA-256-PLUS.
</para>
<procedure>
@@ -1518,9 +1519,10 @@ <title>SASL Authentication Message Flow</title>
<title>SCRAM-SHA-256 authentication</title>
<para>
- <firstterm>SCRAM-SHA-256</firstterm> (called just <firstterm>SCRAM</firstterm> from now on) is
- the only implemented SASL mechanism, at the moment. It is described in detail
- in RFC 7677 and RFC 5802.
+ The implemented SASL mechanisms at the moment
+ are <literal>SCRAM-SHA-256</literal> and its variant with channel
+ binding <literal>SCRAM-SHA-256-PLUS</literal>. They are described in
+ detail in RFC 7677 and RFC 5802.
</para>
<para>
@@ -1547,7 +1549,10 @@ <title>SCRAM-SHA-256 authentication</title>
</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>
<procedure>
@@ -1556,13 +1561,19 @@ <title>Example</title>
<para>
The server sends an AuthenticationSASL message. It includes a list of
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>
</step>
<step id="scram-client-first">
<para>
The client responds by sending a SASLInitialResponse message, which
- indicates the chosen mechanism, <literal>SCRAM-SHA-256</literal>. In the Initial
- Client response field, the message contains the SCRAM
+ indicates the chosen mechanism, <literal>SCRAM-SHA-256</literal> or
+ <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>.
</para>
</step>
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index ec4bb9a88e..62c8d67e56 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,88 @@ 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 biding. 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. XXX Not sure whether it would be 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 +873,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 +1093,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 */
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 6c915a7289..2dd3328d71 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -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),
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index fe15227a77..1e3e19f5e0 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -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.
*
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 7bde744d51..856e0439d5 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -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_cipher(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
extern ProtocolVersion FrontendProtocol;
diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h
index 0166e1945d..99560d3d2f 100644
--- a/src/include/libpq/scram.h
+++ b/src/include/libpq/scram.h
@@ -13,8 +13,12 @@
#ifndef 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_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 */
#define SASL_EXCHANGE_CONTINUE 0
@@ -22,7 +26,9 @@
#define SASL_EXCHANGE_FAILURE 2
/* 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,
char **output, int *outputlen, char **logdetail);
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index edfd42df85..e633e56434 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -17,6 +17,7 @@
#include "common/base64.h"
#include "common/saslprep.h"
#include "common/scram-common.h"
+#include "libpq/scram.h"
#include "fe-auth.h"
/* These are needed for getpid(), in the fallback implementation */
@@ -44,6 +45,11 @@ typedef struct
/* These are supplied by the user */
const char *username;
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 */
uint8 SaltedPassword[SCRAM_KEY_LEN];
@@ -79,25 +85,50 @@ static bool pg_frontend_random(char *dst, int len);
/*
* 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 *
-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;
char *prep_password;
pg_saslprep_rc rc;
+ Assert(sasl_mechanism != NULL);
+
state = (fe_scram_state *) malloc(sizeof(fe_scram_state));
if (!state)
return NULL;
memset(state, 0, sizeof(fe_scram_state));
state->state = FE_SCRAM_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->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 */
rc = pg_saslprep(password, &prep_password);
if (rc == SASLPREP_OOM)
{
+ free(state->sasl_mechanism);
free(state);
return NULL;
}
@@ -106,6 +137,7 @@ pg_fe_scram_init(const char *username, const char *password)
prep_password = strdup(password);
if (!prep_password)
{
+ free(state->sasl_mechanism);
free(state);
return NULL;
}
@@ -125,6 +157,10 @@ pg_fe_scram_free(void *opaq)
if (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 */
if (state->client_nonce)
@@ -297,9 +333,10 @@ static char *
build_client_first_message(fe_scram_state *state, PQExpBuffer errormessage)
{
char raw_nonce[SCRAM_RAW_NONCE_LEN + 1];
- char *buf;
- char buflen;
+ char *result;
+ int channel_info_len;
int encoded_len;
+ PQExpBufferData buf;
/*
* 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)
* prepared with SASLprep, the message parsing would fail if it includes
* '=' or ',' characters.
*/
- buflen = 8 + strlen(state->client_nonce) + 1;
- buf = malloc(buflen);
- if (buf == NULL)
+
+ initPQExpBuffer(&buf);
+
+ /*
+ * First build the gs2-header with channel binding information.
+ */
+ if (strcmp(state->sasl_mechanism, SCRAM_SHA256_PLUS_NAME) == 0)
{
- printfPQExpBuffer(errormessage,
- libpq_gettext("out of memory\n"));
- return NULL;
+ Assert(state->ssl_in_use);
+ appendPQExpBuffer(&buf, "p=%s", state->channel_binding_type);
}
- snprintf(buf, buflen, "n,,n=,r=%s", state->client_nonce);
-
- state->client_first_message_bare = strdup(buf + 3);
- if (!state->client_first_message_bare)
+ else if (state->ssl_in_use)
{
- free(buf);
- printfPQExpBuffer(errormessage,
- libpq_gettext("out of memory\n"));
- return NULL;
+ /*
+ * 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;
+
+ 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)
+ goto oom_error;
+
+ result = strdup(buf.data);
+ if (result == NULL)
+ goto oom_error;
+
+ termPQExpBuffer(&buf);
+ return result;
- return buf;
+oom_error:
+ termPQExpBuffer(&buf);
+ printfPQExpBuffer(errormessage,
+ libpq_gettext("out of memory\n"));
+ return NULL;
}
/*
@@ -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
* 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))
goto oom_error;
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 382558f3f8..9d394919ef 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -491,6 +491,9 @@ pg_SASL_init(PGconn *conn, int payloadlen)
bool success;
const char *selected_mechanism;
PQExpBufferData mechanism_buf;
+ char *tls_finished = NULL;
+ size_t tls_finished_len = 0;
+ char *password;
initPQExpBuffer(&mechanism_buf);
@@ -504,7 +507,8 @@ pg_SASL_init(PGconn *conn, int payloadlen)
/*
* Parse the list of SASL authentication mechanisms in the
* 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;
for (;;)
@@ -523,35 +527,17 @@ pg_SASL_init(PGconn *conn, int payloadlen)
break;
/*
- * If we have already selected a mechanism, just skip through the rest
- * of the list.
+ * Select the mechanism to use. Pick SCRAM-SHA-256-PLUS over anything
+ * 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)
- continue;
-
- /*
- * Do we support this mechanism?
- */
- if (strcmp(mechanism_buf.data, SCRAM_SHA256_NAME) == 0)
- {
- char *password;
-
- conn->password_needed = true;
- password = conn->connhost[conn->whichhost].password;
- if (password == NULL)
- password = conn->pgpass;
- if (password == NULL || password[0] == '\0')
- {
- printfPQExpBuffer(&conn->errorMessage,
- PQnoPasswordSupplied);
- goto error;
- }
-
- conn->sasl_state = pg_fe_scram_init(conn->pguser, password);
- if (!conn->sasl_state)
- goto oom_error;
+ if (conn->ssl_in_use &&
+ 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)
@@ -561,6 +547,54 @@ pg_SASL_init(PGconn *conn, int payloadlen)
goto error;
}
+ /*
+ * Now that the SASL mechanism has been chosen for the exchange,
+ * initialize its state information.
+ */
+
+ /*
+ * 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;
+ password = conn->connhost[conn->whichhost].password;
+ if (password == NULL)
+ password = conn->pgpass;
+ if (password == NULL || password[0] == '\0')
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ PQnoPasswordSupplied);
+ goto error;
+ }
+
+#ifdef USE_SSL
+ /*
+ * 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;
+ }
+#endif
+
+ /*
+ * Initialize the SASL state information with all the information
+ * gathered during the initial exchange.
+ *
+ * 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 */
pg_fe_scram_exchange(conn->sasl_state,
NULL, -1,
diff --git a/src/interfaces/libpq/fe-auth.h b/src/interfaces/libpq/fe-auth.h
index 5dc6bb5341..1525a52742 100644
--- a/src/interfaces/libpq/fe-auth.h
+++ b/src/interfaces/libpq/fe-auth.h
@@ -23,7 +23,12 @@ extern int pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn);
extern char *pg_fe_getauthname(PQExpBuffer errorMessage);
/* 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_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen,
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 2f29820e82..61d161b367 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -393,6 +393,33 @@ pgtls_write(PGconn *conn, const void *ptr, size_t len)
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 */
/* ------------------------------------------------------------ */
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 42913604e3..8412ee8160 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -453,11 +453,13 @@ struct pg_conn
/* Assorted state for SASL, SSL, GSS, etc */
void *sasl_state;
+ /* SSL structures */
+ bool ssl_in_use;
+
#ifdef USE_SSL
bool allow_ssl_try; /* Allowed to try SSL negotiation */
bool wait_ssl_try; /* Delay SSL negotiation until after
* attempting normal connection */
- bool ssl_in_use;
#ifdef USE_OPENSSL
SSL *ssl; /* SSL status, if have SSL connection */
X509 *peer; /* X509 cert of server */
@@ -668,6 +670,7 @@ extern void pgtls_close(PGconn *conn);
extern ssize_t pgtls_read(PGconn *conn, void *ptr, size_t len);
extern bool pgtls_read_pending(PGconn *conn);
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
diff --git a/src/test/ssl/ServerSetup.pm b/src/test/ssl/ServerSetup.pm
index ad2e036602..02f8028b2b 100644
--- a/src/test/ssl/ServerSetup.pm
+++ b/src/test/ssl/ServerSetup.pm
@@ -57,19 +57,21 @@ sub test_connect_ok
{
my $common_connstr = $_[0];
my $connstr = $_[1];
+ my $test_name = $_[2];
my $result =
run_test_psql("$common_connstr $connstr", "(should succeed)");
- ok($result, $connstr);
+ ok($result, $test_name || $connstr);
}
sub test_connect_fails
{
my $common_connstr = $_[0];
my $connstr = $_[1];
+ my $test_name = $_[2];
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
@@ -89,8 +91,7 @@ sub copy_files
sub configure_test_server_for_ssl
{
- my $node = $_[0];
- my $serverhost = $_[1];
+ my ($node, $serverhost, $authmethod, $password, $password_enc) = @_;
my $pgdata = $node->data_dir;
@@ -100,6 +101,15 @@ sub configure_test_server_for_ssl
$node->psql('postgres', "CREATE DATABASE trustdb");
$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.
open my $conf, '>>', "$pgdata/postgresql.conf";
print $conf "fsync=off\n";
@@ -129,7 +139,7 @@ sub configure_test_server_for_ssl
$node->restart;
# 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
@@ -157,8 +167,7 @@ sub switch_server_cert
sub configure_hba_for_ssl
{
- my $node = $_[0];
- my $serverhost = $_[1];
+ my ($node, $serverhost, $authmethod) = @_;
my $pgdata = $node->data_dir;
# Only accept SSL connections from localhost. Our tests don't depend on this
@@ -169,9 +178,9 @@ sub configure_hba_for_ssl
print $hba
"# TYPE DATABASE USER ADDRESS METHOD\n";
print $hba
-"hostssl trustdb ssltestuser $serverhost/32 trust\n";
+"hostssl trustdb ssltestuser $serverhost/32 $authmethod\n";
print $hba
-"hostssl trustdb ssltestuser ::1/128 trust\n";
+"hostssl trustdb ssltestuser ::1/128 $authmethod\n";
print $hba
"hostssl certdb ssltestuser $serverhost/32 cert\n";
print $hba
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 890e3051a2..a0a06825c6 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -32,7 +32,7 @@
$ENV{PGHOST} = $node->host;
$ENV{PGPORT} = $node->port;
$node->start;
-configure_test_server_for_ssl($node, $SERVERHOSTADDR);
+configure_test_server_for_ssl($node, $SERVERHOSTADDR, 'trust');
switch_server_cert($node, 'server-cn-only');
### Part 1. Run client-side tests.
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
new file mode 100644
index 0000000000..25f75bd52a
--- /dev/null
+++ b/src/test/ssl/t/002_scram.pl
@@ -0,0 +1,38 @@
+# 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");
--
2.15.0
On Sat, Nov 18, 2017 at 4:31 AM, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:
I made some significant changes to the logic.
Thanks!
The selection of the channel binding flag (n/y/p) in the client seemed
wrong. Your code treated 'y' as an error, but I think that is a
legitimate case, for example a PG11 client connecting to a PG10 server
over SSL. The client supports channel binding in that case and
(correctly) thinks the server does not, so we use the 'y' flag and
proceed normally without channel binding.The selection of the mechanism in the client was similarly incorrect, I
think. The code would not tolerate a situation were an SSL connection
is in use but the server does not advertise the -PLUS mechanism, which
again could be from a PG10 server.
Hm, OK. I have been likely confused by the fact that eSws is a valid
b64-encoded cbind-input on v10 servers. And the spec has no direct
mention of the matter, only of biws.
The creation of the channel binding data didn't match the spec, because
the gs2-header (p=type,,) was not included in the data put through
base64. This was done incorrectly on both server and client, so the
protocol still worked. (However, in the non-channel-binding case we
hardcode "biws", which is exactly the base64-encoding of the gs2-header.
So that was inconsistent.)
Meh-to-self, you are right. Still it seems to me that your version is
forgetting something.. Please see below.
I think we also need to backpatch a bug fix into PG10 so that the server
can accept base64("y,,") as channel binding data. Otherwise, the above
scenario of a PG11 client connecting to a PG10 server over SSL will
currently fail because the server will not accept the channel binding data.
Yes, without the additional comparison, the failure is weird for the
user. Here is what I have with an unpatched v10 server:
psql: FATAL: unexpected SCRAM channel-binding attribute in client-final-message
This is going to need a one-liner in read_client_final_message()'s auth-scram.c.
Please check my patch and think through these changes. I'm happy to
commit the patch as is if there are no additional insights.
Here are some comments.
+ * The client requires channel biding. Channel binding type
s/biding/binding/.
if (!state->ssl_in_use)
+ /*
+ * Without SSL, we don't support channel binding.
+ *
Better to add brackets if adding a comment.
+ * Read value provided by client; only tls-unique is supported
+ * for now. XXX Not sure whether it would be safe to print
+ * the name of an unsupported binding type in the error
+ * message. Pranksters could print arbitrary strings into the
+ * log that way.
That's why I didn't show those strings in the error messages of the
previous versions. Those are useless as well, except for debugging the
feature and the protocol.
+ 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);
By looking at RFC5802, a base64 encoding of cbind-input is used:
cbind-input = gs2-header [ cbind-data ]
gs2-cbind-flag "," [ authzid ] ","
However you are missing two commands after p=%s, no?
--
Michael
On 11/18/17 06:32, Michael Paquier wrote:
Here are some comments.
+ * The client requires channel biding. Channel binding type
s/biding/binding/.
fixed
if (!state->ssl_in_use) + /* + * Without SSL, we don't support channel binding. + * Better to add brackets if adding a comment.
done
+ * Read value provided by client; only tls-unique is supported + * for now. XXX Not sure whether it would be safe to print + * the name of an unsupported binding type in the error + * message. Pranksters could print arbitrary strings into the + * log that way. That's why I didn't show those strings in the error messages of the previous versions. Those are useless as well, except for debugging the feature and the protocol.
Right. I left the comment in there as a note to future hackers who want
to improve error messages (often myself).
+ 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); By looking at RFC5802, a base64 encoding of cbind-input is used: cbind-input = gs2-header [ cbind-data ] gs2-cbind-flag "," [ authzid ] "," However you are missing two commands after p=%s, no?
fixed
I have committed the patch with the above fixes.
I'll be off for a week, so perhaps by that time you could make a rebased
version of the rest? I'm not sure how much more time I'll have, so
maybe it will end up being moved to the next CF.
--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
On Sun, Nov 19, 2017 at 12:56 AM, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:
On 11/18/17 06:32, Michael Paquier wrote:
+ 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); By looking at RFC5802, a base64 encoding of cbind-input is used: cbind-input = gs2-header [ cbind-data ] gs2-cbind-flag "," [ authzid ] "," However you are missing two commands after p=%s, no?fixed
s/commands/commas/. You caught my words correctly.
I have committed the patch with the above fixes.
Thanks, Peter!
I'll be off for a week, so perhaps by that time you could make a rebased
version of the rest? I'm not sure how much more time I'll have, so
maybe it will end up being moved to the next CF.
OK, let's see then. That's not an issue for me if this gets bumped.
--
Michael
On Sun, Nov 19, 2017 at 8:13 AM, Michael Paquier
<michael.paquier@gmail.com> wrote:
On Sun, Nov 19, 2017 at 12:56 AM, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:I'll be off for a week, so perhaps by that time you could make a rebased
version of the rest? I'm not sure how much more time I'll have, so
maybe it will end up being moved to the next CF.OK, let's see then. That's not an issue for me if this gets bumped.
I have been considering the set of connection parameters introduced
previously, and I am thinking that the one to enforce the SASL
mechanism name shold be dropped. It becomes useful if willing to test
the code path where an incorrect mechanism name is used, or to enforce
the client to not ask for channel binding. Still I think that we
should keep the code simple and always assume that channel binding is
supported if the SSL implementation allows it to not give users the
option to use something less secure. Perhaps this could be replaced
with a on/off switch to disable channel binding, but I am not much
into the idea either. This also keeps libpq slightly cleaner.
So attached are rebased patches:
- 0001 to introduce the connection parameter saslchannelbinding, which
allows libpq to enforce the type of channel binding used during an
exchange.
- 0002 to add tls-endpoint as channel binding type, which is where 0001 shines.
I have created a different thread with a patch about the bug of v10
servers not able to accept "y,," as valid input for channel binding as
this needs its own discussion:
/messages/by-id/CAB7nPqSFcNsuQcWcqhX8QSz0R8oKz8ZM4Yw4ky=cfO9rpVdTUA@mail.gmail.com
Peter, also in CheckSCRAMAuth() the server publishes
SCRAM-SHA-256-PLUS is SSL is being used. However, seeing recent
patches to add more SSL implementations (gnuTLS, macos and windows
channel), it seems to me that the choice of publishing
SCRAM-SHA-256-PLUS should be done depending on if there is any binding
data available instead, either TLS finish message data or certificate
hash. On the client-side, it seems also to me that the "y" flag should
not be sent if there is no binding data available because the client
may not support it. All things stand now because OpenSSL is the only
implementation in core and can support both tls-unique and
tls-server-end-point, this may not stand true should any other
implementations get added. For example SSL implementation of MacOS has
no documented API to get a hash of the server or the TLS-finished
message bytes.
--
Michael
Attachments:
0002-Implement-channel-binding-tls-server-end-point-for-S.patchtext/x-patch; charset=US-ASCII; name=0002-Implement-channel-binding-tls-server-end-point-for-S.patchDownload
From 03a55729f3581d6962c59de82e0e3cc41138f021 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 10 Oct 2017 22:04:22 +0900
Subject: [PATCH 2/2] Implement channel binding tls-server-end-point for SCRAM
As referenced in RFC 5929, this channel binding is not the default value
and uses a hash of the certificate as binding data. On the frontend, this
can be resumed in getting the data from SSL_get_peer_certificate() and
on the backend SSL_get_certificate().
The hashing algorithm needs also to switch to SHA-256 if the signature
algorithm is MD5 or SHA-1, so let's be careful about that.
---
doc/src/sgml/protocol.sgml | 5 +-
src/backend/libpq/auth-scram.c | 26 ++++++++---
src/backend/libpq/auth.c | 8 +++-
src/backend/libpq/be-secure-openssl.c | 61 +++++++++++++++++++++++++
src/include/libpq/libpq-be.h | 1 +
src/include/libpq/scram.h | 4 +-
src/interfaces/libpq/fe-auth-scram.c | 24 +++++++---
src/interfaces/libpq/fe-auth.c | 12 ++++-
src/interfaces/libpq/fe-auth.h | 4 +-
src/interfaces/libpq/fe-secure-openssl.c | 78 ++++++++++++++++++++++++++++++++
src/interfaces/libpq/libpq-int.h | 1 +
src/test/ssl/t/002_scram.pl | 5 +-
12 files changed, 210 insertions(+), 19 deletions(-)
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 4d3b6446c4..b00a51a8ca 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1551,8 +1551,9 @@ the password is in.
<para>
<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.
+is <literal>SCRAM-SHA-256-PLUS</literal>. Two channel binding types are
+supported at the moment: <literal>tls-unique</literal>, which is the default,
+and <literal>tls-server-end-point</literal>, both defined in RFC 5929.
</para>
<procedure>
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index 22103ce479..8f96e3927e 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -113,6 +113,8 @@ typedef struct
bool ssl_in_use;
const char *tls_finished_message;
size_t tls_finished_len;
+ const char *certificate_hash;
+ size_t certificate_hash_len;
char *channel_binding_type;
int iterations;
@@ -175,7 +177,9 @@ 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)
+ size_t tls_finished_len,
+ const char *certificate_hash,
+ size_t certificate_hash_len)
{
scram_state *state;
bool got_verifier;
@@ -186,6 +190,8 @@ pg_be_scram_init(const char *username,
state->ssl_in_use = ssl_in_use;
state->tls_finished_message = tls_finished_message;
state->tls_finished_len = tls_finished_len;
+ state->certificate_hash = certificate_hash;
+ state->certificate_hash_len = certificate_hash_len;
state->channel_binding_type = NULL;
/*
@@ -852,13 +858,15 @@ read_client_first_message(scram_state *state, char *input)
}
/*
- * 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.)
+ * Read value provided by client; only tls-unique and
+ * tls-server-end-point are 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)
+ if (strcmp(channel_binding_type, SCRAM_CHANNEL_BINDING_TLS_UNIQUE) != 0 &&
+ strcmp(channel_binding_type, SCRAM_CHANNEL_BINDING_TLS_ENDPOINT) != 0)
ereport(ERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION),
(errmsg("unsupported SCRAM channel-binding type"))));
@@ -1116,6 +1124,12 @@ read_client_final_message(scram_state *state, char *input)
cbind_data = state->tls_finished_message;
cbind_data_len = state->tls_finished_len;
}
+ else if (strcmp(state->channel_binding_type,
+ SCRAM_CHANNEL_BINDING_TLS_ENDPOINT) == 0)
+ {
+ cbind_data = state->certificate_hash;
+ cbind_data_len = state->certificate_hash_len;
+ }
else
{
/* should not happen */
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 2dd3328d71..76fe2645b9 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -873,6 +873,8 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
bool initial;
char *tls_finished = NULL;
size_t tls_finished_len = 0;
+ char *certificate_hash = NULL;
+ size_t certificate_hash_len = 0;
/*
* SASL auth is not supported for protocol versions before 3, because it
@@ -920,6 +922,8 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
if (port->ssl_in_use)
{
tls_finished = be_tls_get_peer_finished(port, &tls_finished_len);
+ certificate_hash = be_tls_get_certificate_hash(port,
+ &certificate_hash_len);
}
#endif
@@ -938,7 +942,9 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
shadow_pass,
port->ssl_in_use,
tls_finished,
- tls_finished_len);
+ tls_finished_len,
+ certificate_hash,
+ certificate_hash_len);
/*
* Loop through SASL message exchange. This exchange can consist of
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 1e3e19f5e0..e3e8a535c8 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -1239,6 +1239,67 @@ be_tls_get_peer_finished(Port *port, size_t *len)
return result;
}
+/*
+ * Get the server certificate hash for authentication purposes. Per
+ * RFC 5929 and tls-server-end-point, the TLS server's certificate bytes
+ * need to be hashed with SHA-256 if its signature algorithm is MD5 or
+ * SHA-1 as per RFC 5929 (https://tools.ietf.org/html/rfc5929#section-4.1).
+ * If something else is used, the same hash as the signature algorithm is
+ * used. The result is a palloc'd hash of the server certificate with its
+ * size, and NULL if there is no certificate available.
+ */
+char *
+be_tls_get_certificate_hash(Port *port, size_t *len)
+{
+ char *cert_hash = NULL;
+ X509 *server_cert;
+
+ *len = 0;
+ server_cert = SSL_get_certificate(port->ssl);
+
+ if (server_cert != NULL)
+ {
+ const EVP_MD *algo_type = NULL;
+ char hash[EVP_MAX_MD_SIZE]; /* size for SHA-512 */
+ unsigned int hash_size;
+ int algo_nid;
+
+ /*
+ * Get the signature algorithm of the certificate to determine the
+ * hash algorithm to use for the result.
+ */
+ if (!OBJ_find_sigid_algs(X509_get_signature_nid(server_cert),
+ &algo_nid, NULL))
+ elog(ERROR, "could not find signature algorithm");
+
+ switch (algo_nid)
+ {
+ case NID_md5:
+ case NID_sha1:
+ algo_type = EVP_sha256();
+ break;
+
+ default:
+ algo_type = EVP_get_digestbynid(algo_nid);
+ if (algo_type == NULL)
+ elog(ERROR, "could not find digest for NID %s",
+ OBJ_nid2sn(algo_nid));
+ break;
+ }
+
+ /* generate and save the certificate hash */
+ if (!X509_digest(server_cert, algo_type, (unsigned char *) hash,
+ &hash_size))
+ elog(ERROR, "could not generate server certificate hash");
+
+ cert_hash = (char *) palloc(hash_size);
+ memcpy(cert_hash, hash, hash_size);
+ *len = hash_size;
+ }
+
+ return cert_hash;
+}
+
/*
* Convert an X509 subject name to a cstring.
*
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 856e0439d5..cf9d8b7870 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -210,6 +210,7 @@ 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_peerdn_name(Port *port, char *ptr, size_t len);
extern char *be_tls_get_peer_finished(Port *port, size_t *len);
+extern char *be_tls_get_certificate_hash(Port *port, size_t *len);
#endif
extern ProtocolVersion FrontendProtocol;
diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h
index 99560d3d2f..d4e585b3bf 100644
--- a/src/include/libpq/scram.h
+++ b/src/include/libpq/scram.h
@@ -19,6 +19,7 @@
/* Channel binding types */
#define SCRAM_CHANNEL_BINDING_TLS_UNIQUE "tls-unique"
+#define SCRAM_CHANNEL_BINDING_TLS_ENDPOINT "tls-server-end-point"
/* Status codes for message exchange */
#define SASL_EXCHANGE_CONTINUE 0
@@ -28,7 +29,8 @@
/* Routines dedicated to authentication */
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);
+ size_t tls_finished_len, const char *certificate_hash,
+ size_t certificate_hash_len);
extern int pg_be_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen, char **logdetail);
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index 9860f6b15e..38de1f14cb 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -48,6 +48,8 @@ typedef struct
bool ssl_in_use;
char *tls_finished_message;
size_t tls_finished_len;
+ char *certificate_hash;
+ size_t certificate_hash_len;
char *sasl_mechanism;
const char *channel_binding_type;
@@ -96,7 +98,9 @@ pg_fe_scram_init(const char *username,
const char *sasl_mechanism,
const char *channel_binding_type,
char *tls_finished_message,
- size_t tls_finished_len)
+ size_t tls_finished_len,
+ char *certificate_hash,
+ size_t certificate_hash_len)
{
fe_scram_state *state;
char *prep_password;
@@ -113,6 +117,8 @@ pg_fe_scram_init(const char *username,
state->ssl_in_use = ssl_in_use;
state->tls_finished_message = tls_finished_message;
state->tls_finished_len = tls_finished_len;
+ state->certificate_hash = certificate_hash;
+ state->certificate_hash_len = certificate_hash_len;
state->sasl_mechanism = strdup(sasl_mechanism);
if (!state->sasl_mechanism)
{
@@ -121,9 +127,9 @@ pg_fe_scram_init(const char *username,
}
/*
- * Store channel binding type. Only one type is currently supported,
- * tls-unique, which is also the default. Anything defined by the caller
- * is forcibly used.
+ * Store channel binding type. Two types are currently supported,
+ * tls-unique, which is also the default, and tls-server-end-point.
+ * Anything defined by the caller is forcibly used.
*/
if (channel_binding_type && strlen(channel_binding_type) > 0)
state->channel_binding_type = channel_binding_type;
@@ -165,8 +171,8 @@ pg_fe_scram_free(void *opaq)
free(state->password);
if (state->tls_finished_message)
free(state->tls_finished_message);
- if (state->sasl_mechanism)
- free(state->sasl_mechanism);
+ if (state->certificate_hash)
+ free(state->certificate_hash);
/* client messages */
if (state->client_nonce)
@@ -457,6 +463,12 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
cbind_data = state->tls_finished_message;
cbind_data_len = state->tls_finished_len;
}
+ else if (strcmp(state->channel_binding_type,
+ SCRAM_CHANNEL_BINDING_TLS_ENDPOINT) == 0)
+ {
+ cbind_data = state->certificate_hash;
+ cbind_data_len = state->certificate_hash_len;
+ }
else
{
/* should not happen */
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 2f34340f45..38cde81d55 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -493,6 +493,8 @@ pg_SASL_init(PGconn *conn, int payloadlen)
PQExpBufferData mechanism_buf;
char *tls_finished = NULL;
size_t tls_finished_len = 0;
+ char *certificate_hash = NULL;
+ size_t certificate_hash_len = 0;
char *password;
initPQExpBuffer(&mechanism_buf);
@@ -577,6 +579,12 @@ pg_SASL_init(PGconn *conn, int payloadlen)
tls_finished = pgtls_get_finished(conn, &tls_finished_len);
if (tls_finished == NULL)
goto oom_error;
+
+ certificate_hash =
+ pgtls_get_peer_certificate_hash(conn,
+ &certificate_hash_len);
+ if (certificate_hash == NULL)
+ goto error; /* error message is set */
}
#endif
@@ -592,7 +600,9 @@ pg_SASL_init(PGconn *conn, int payloadlen)
selected_mechanism,
conn->saslchannelbinding,
tls_finished,
- tls_finished_len);
+ tls_finished_len,
+ certificate_hash,
+ certificate_hash_len);
if (!conn->sasl_state)
goto oom_error;
diff --git a/src/interfaces/libpq/fe-auth.h b/src/interfaces/libpq/fe-auth.h
index 96094b0c1e..17e85d90f4 100644
--- a/src/interfaces/libpq/fe-auth.h
+++ b/src/interfaces/libpq/fe-auth.h
@@ -29,7 +29,9 @@ extern void *pg_fe_scram_init(const char *username,
const char *sasl_mechanism,
const char *channel_binding_type,
char *tls_finished_message,
- size_t tls_finished_len);
+ size_t tls_finished_len,
+ char *certificate_hash,
+ size_t certificate_hash_len);
extern void pg_fe_scram_free(void *opaq);
extern void pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen,
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 61d161b367..99077c3d9a 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -419,6 +419,84 @@ pgtls_get_finished(PGconn *conn, size_t *len)
return result;
}
+/*
+ * Get the hash of the server certificate
+ *
+ * This information is useful for end-point channel binding, where the
+ * client certificate hash is used as a link, per RFC 5929. If the
+ * signature hash algorithm is MD5 or SHA-1, fall back to SHA-256,
+ * as per RFC 5929 (https://tools.ietf.org/html/rfc5929#section-4.1).
+ * NULL is sent back to the caller in the event of an error, with an
+ * error message for the caller to consume.
+ */
+char *
+pgtls_get_peer_certificate_hash(PGconn *conn, size_t *len)
+{
+ char *cert_hash = NULL;
+
+ *len = 0;
+
+ if (conn->peer)
+ {
+ X509 *peer_cert = conn->peer;
+ const EVP_MD *algo_type = NULL;
+ char hash[EVP_MAX_MD_SIZE]; /* size for SHA-512 */
+ unsigned int hash_size;
+ int algo_nid;
+
+ /*
+ * Get the signature algorithm of the certificate to determine the
+ * hash algorithm to use for the result.
+ */
+ if (!OBJ_find_sigid_algs(X509_get_signature_nid(peer_cert),
+ &algo_nid, NULL))
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("could not find signature algorithm\n"));
+ return NULL;
+ }
+
+ switch (algo_nid)
+ {
+ case NID_md5:
+ case NID_sha1:
+ algo_type = EVP_sha256();
+ break;
+
+ default:
+ algo_type = EVP_get_digestbynid(algo_nid);
+ if (algo_type == NULL)
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("could not find digest for NID %s\n"),
+ OBJ_nid2sn(algo_nid));
+ return NULL;
+ }
+ break;
+ }
+
+ if (!X509_digest(peer_cert, algo_type, (unsigned char *) hash,
+ &hash_size))
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("could not generate peer certificate hash\n"));
+ return NULL;
+ }
+
+ /* save result */
+ cert_hash = (char *) malloc(hash_size);
+ if (cert_hash == NULL)
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("out of memory\n"));
+ return NULL;
+ }
+ memcpy(cert_hash, hash, hash_size);
+ *len = hash_size;
+ }
+
+ return cert_hash;
+}
/* ------------------------------------------------------------ */
/* OpenSSL specific code */
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 1cb096f1ef..df37fc9b7f 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -672,6 +672,7 @@ extern ssize_t pgtls_read(PGconn *conn, void *ptr, size_t len);
extern bool pgtls_read_pending(PGconn *conn);
extern ssize_t pgtls_write(PGconn *conn, const void *ptr, size_t len);
extern char *pgtls_get_finished(PGconn *conn, size_t *len);
+extern char *pgtls_get_peer_certificate_hash(PGconn *conn, size_t *len);
/*
* this is so that we can check if a connection is non-blocking internally
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index c0b9599920..5a2a24164e 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -4,7 +4,7 @@ use strict;
use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 3;
+use Test::More tests => 4;
use ServerSetup;
use File::Copy;
@@ -42,6 +42,9 @@ test_connect_ok($common_connstr, '',
test_connect_ok($common_connstr,
"saslchannelbinding=tls-unique",
"SCRAM authentication with tls-unique as channel binding");
+test_connect_ok($common_connstr,
+ "saslchannelbinding=tls-server-end-point",
+ "SCRAM authentication with tls-server-end-point as channel binding");
test_connect_fails($common_connstr,
"saslchannelbinding=not-exists",
"SCRAM authentication with invalid channel binding");
--
2.15.0
0001-Add-connection-parameter-saslchannelbinding.patchtext/x-patch; charset=US-ASCII; name=0001-Add-connection-parameter-saslchannelbinding.patchDownload
From 19b5728a5452d99c7ee6108ab5033d7a483351e2 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 21 Nov 2017 12:55:29 +0900
Subject: [PATCH 1/2] Add connection parameter "saslchannelbinding"
This parameter can be used to enforce the value of the type of channel
binding used during a SASL message exchange. This proves to be useful
now to check code paths where an invalid channel binding type is used
by a client, which is limited, but will be more useful to allow clients
to enforce the channel binding type to tls-enpoint once it gets added.
More tests dedicated to SASL and channel binding are added as well to
the SSL test suite, which is handy to check the validity of this patch.
---
doc/src/sgml/libpq.sgml | 12 ++++++++++++
src/interfaces/libpq/fe-auth-scram.c | 10 ++++++++--
src/interfaces/libpq/fe-auth.c | 1 +
src/interfaces/libpq/fe-auth.h | 1 +
src/interfaces/libpq/fe-connect.c | 7 +++++++
src/interfaces/libpq/libpq-int.h | 1 +
src/test/ssl/t/002_scram.pl | 11 ++++++++++-
7 files changed, 40 insertions(+), 3 deletions(-)
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 694921f212..5db947b2b5 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1222,6 +1222,18 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
</listitem>
</varlistentry>
+ <varlistentry id="libpq-saslchannelbinding" xreflabel="saslchannelbinding">
+ <term><literal>saslchannelbinding</literal></term>
+ <listitem>
+ <para>
+ Controls the name of the channel binding type sent to server when doing
+ a message exchange for a SASL authentication. The list of channel
+ binding names supported by server are listed in
+ <xref linkend="sasl-authentication">.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="libpq-connect-sslmode" xreflabel="sslmode">
<term><literal>sslmode</literal></term>
<listitem>
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index f2403147ca..9860f6b15e 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -94,6 +94,7 @@ pg_fe_scram_init(const char *username,
const char *password,
bool ssl_in_use,
const char *sasl_mechanism,
+ const char *channel_binding_type,
char *tls_finished_message,
size_t tls_finished_len)
{
@@ -120,9 +121,14 @@ pg_fe_scram_init(const char *username,
}
/*
- * Store channel binding type. Only one type is currently supported.
+ * Store channel binding type. Only one type is currently supported,
+ * tls-unique, which is also the default. Anything defined by the caller
+ * is forcibly used.
*/
- state->channel_binding_type = SCRAM_CHANNEL_BINDING_TLS_UNIQUE;
+ if (channel_binding_type && strlen(channel_binding_type) > 0)
+ state->channel_binding_type = channel_binding_type;
+ else
+ state->channel_binding_type = SCRAM_CHANNEL_BINDING_TLS_UNIQUE;
/* Normalize the password with SASLprep, if possible */
rc = pg_saslprep(password, &prep_password);
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 9d394919ef..2f34340f45 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -590,6 +590,7 @@ pg_SASL_init(PGconn *conn, int payloadlen)
password,
conn->ssl_in_use,
selected_mechanism,
+ conn->saslchannelbinding,
tls_finished,
tls_finished_len);
if (!conn->sasl_state)
diff --git a/src/interfaces/libpq/fe-auth.h b/src/interfaces/libpq/fe-auth.h
index 1525a52742..96094b0c1e 100644
--- a/src/interfaces/libpq/fe-auth.h
+++ b/src/interfaces/libpq/fe-auth.h
@@ -27,6 +27,7 @@ extern void *pg_fe_scram_init(const char *username,
const char *password,
bool ssl_in_use,
const char *sasl_mechanism,
+ const char *channel_binding_type,
char *tls_finished_message,
size_t tls_finished_len);
extern void pg_fe_scram_free(void *opaq);
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 2c175a2a24..3c85501754 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -262,6 +262,11 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
"TCP-Keepalives-Count", "", 10, /* strlen(INT32_MAX) == 10 */
offsetof(struct pg_conn, keepalives_count)},
+ /* Set of options proper to SASL */
+ {"saslchannelbinding", NULL, NULL, NULL,
+ "SASL-Channel", "", 22, /* sizeof("tls-unique-for-telnet") == 22 */
+ offsetof(struct pg_conn, saslchannelbinding)},
+
/*
* ssl options are allowed even without client SSL support because the
* client can still handle SSL modes "disable" and "allow". Other
@@ -3469,6 +3474,8 @@ freePGconn(PGconn *conn)
free(conn->keepalives_interval);
if (conn->keepalives_count)
free(conn->keepalives_count);
+ if (conn->saslchannelbinding)
+ free(conn->saslchannelbinding);
if (conn->sslmode)
free(conn->sslmode);
if (conn->sslcert)
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 8412ee8160..1cb096f1ef 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -349,6 +349,7 @@ struct pg_conn
* retransmits */
char *keepalives_count; /* maximum number of TCP keepalive
* retransmits */
+ char *saslchannelbinding; /* channel binding type used in SASL */
char *sslmode; /* SSL mode (require,prefer,allow,disable) */
char *sslcompression; /* SSL compression (0 or 1) */
char *sslkey; /* client key filename */
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 25f75bd52a..c0b9599920 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -4,7 +4,7 @@ use strict;
use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 1;
+use Test::More tests => 3;
use ServerSetup;
use File::Copy;
@@ -34,5 +34,14 @@ $ENV{PGPASSWORD} = "pass";
$common_connstr =
"user=ssltestuser dbname=trustdb sslmode=require hostaddr=$SERVERHOSTADDR";
+# Defaut settings
test_connect_ok($common_connstr, '',
"SCRAM authentication with default channel binding");
+
+# Channel bindings
+test_connect_ok($common_connstr,
+ "saslchannelbinding=tls-unique",
+ "SCRAM authentication with tls-unique as channel binding");
+test_connect_fails($common_connstr,
+ "saslchannelbinding=not-exists",
+ "SCRAM authentication with invalid channel binding");
--
2.15.0
On Tue, Nov 21, 2017 at 1:36 PM, Michael Paquier
<michael.paquier@gmail.com> wrote:
So attached are rebased patches:
- 0001 to introduce the connection parameter saslchannelbinding, which
allows libpq to enforce the type of channel binding used during an
exchange.
- 0002 to add tls-endpoint as channel binding type, which is where 0001 shines.
Attached is a rebased patch set, documentation failing to compile. I
am moving at the same time this patch set to the next commit fest.
--
Michael
Attachments:
0002-Implement-channel-binding-tls-server-end-point-for-S.patchtext/x-patch; charset=US-ASCII; name=0002-Implement-channel-binding-tls-server-end-point-for-S.patchDownload
From bdd25121ba7c1916d280f97c8e1a280ad26ea60c Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 10 Oct 2017 22:04:22 +0900
Subject: [PATCH 2/2] Implement channel binding tls-server-end-point for SCRAM
As referenced in RFC 5929, this channel binding is not the default value
and uses a hash of the certificate as binding data. On the frontend, this
can be resumed in getting the data from SSL_get_peer_certificate() and
on the backend SSL_get_certificate().
The hashing algorithm needs also to switch to SHA-256 if the signature
algorithm is MD5 or SHA-1, so let's be careful about that.
---
doc/src/sgml/protocol.sgml | 5 +-
src/backend/libpq/auth-scram.c | 26 ++++++++---
src/backend/libpq/auth.c | 8 +++-
src/backend/libpq/be-secure-openssl.c | 61 +++++++++++++++++++++++++
src/include/libpq/libpq-be.h | 1 +
src/include/libpq/scram.h | 4 +-
src/interfaces/libpq/fe-auth-scram.c | 24 +++++++---
src/interfaces/libpq/fe-auth.c | 12 ++++-
src/interfaces/libpq/fe-auth.h | 4 +-
src/interfaces/libpq/fe-secure-openssl.c | 78 ++++++++++++++++++++++++++++++++
src/interfaces/libpq/libpq-int.h | 1 +
src/test/ssl/t/002_scram.pl | 5 +-
12 files changed, 210 insertions(+), 19 deletions(-)
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 8174e3defa..365f72b51d 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1576,8 +1576,9 @@ the password is in.
<para>
<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.
+is <literal>SCRAM-SHA-256-PLUS</literal>. Two channel binding types are
+supported at the moment: <literal>tls-unique</literal>, which is the default,
+and <literal>tls-server-end-point</literal>, both defined in RFC 5929.
</para>
<procedure>
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index 22103ce479..8f96e3927e 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -113,6 +113,8 @@ typedef struct
bool ssl_in_use;
const char *tls_finished_message;
size_t tls_finished_len;
+ const char *certificate_hash;
+ size_t certificate_hash_len;
char *channel_binding_type;
int iterations;
@@ -175,7 +177,9 @@ 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)
+ size_t tls_finished_len,
+ const char *certificate_hash,
+ size_t certificate_hash_len)
{
scram_state *state;
bool got_verifier;
@@ -186,6 +190,8 @@ pg_be_scram_init(const char *username,
state->ssl_in_use = ssl_in_use;
state->tls_finished_message = tls_finished_message;
state->tls_finished_len = tls_finished_len;
+ state->certificate_hash = certificate_hash;
+ state->certificate_hash_len = certificate_hash_len;
state->channel_binding_type = NULL;
/*
@@ -852,13 +858,15 @@ read_client_first_message(scram_state *state, char *input)
}
/*
- * 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.)
+ * Read value provided by client; only tls-unique and
+ * tls-server-end-point are 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)
+ if (strcmp(channel_binding_type, SCRAM_CHANNEL_BINDING_TLS_UNIQUE) != 0 &&
+ strcmp(channel_binding_type, SCRAM_CHANNEL_BINDING_TLS_ENDPOINT) != 0)
ereport(ERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION),
(errmsg("unsupported SCRAM channel-binding type"))));
@@ -1116,6 +1124,12 @@ read_client_final_message(scram_state *state, char *input)
cbind_data = state->tls_finished_message;
cbind_data_len = state->tls_finished_len;
}
+ else if (strcmp(state->channel_binding_type,
+ SCRAM_CHANNEL_BINDING_TLS_ENDPOINT) == 0)
+ {
+ cbind_data = state->certificate_hash;
+ cbind_data_len = state->certificate_hash_len;
+ }
else
{
/* should not happen */
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 2dd3328d71..76fe2645b9 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -873,6 +873,8 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
bool initial;
char *tls_finished = NULL;
size_t tls_finished_len = 0;
+ char *certificate_hash = NULL;
+ size_t certificate_hash_len = 0;
/*
* SASL auth is not supported for protocol versions before 3, because it
@@ -920,6 +922,8 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
if (port->ssl_in_use)
{
tls_finished = be_tls_get_peer_finished(port, &tls_finished_len);
+ certificate_hash = be_tls_get_certificate_hash(port,
+ &certificate_hash_len);
}
#endif
@@ -938,7 +942,9 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
shadow_pass,
port->ssl_in_use,
tls_finished,
- tls_finished_len);
+ tls_finished_len,
+ certificate_hash,
+ certificate_hash_len);
/*
* Loop through SASL message exchange. This exchange can consist of
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 1e3e19f5e0..e3e8a535c8 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -1239,6 +1239,67 @@ be_tls_get_peer_finished(Port *port, size_t *len)
return result;
}
+/*
+ * Get the server certificate hash for authentication purposes. Per
+ * RFC 5929 and tls-server-end-point, the TLS server's certificate bytes
+ * need to be hashed with SHA-256 if its signature algorithm is MD5 or
+ * SHA-1 as per RFC 5929 (https://tools.ietf.org/html/rfc5929#section-4.1).
+ * If something else is used, the same hash as the signature algorithm is
+ * used. The result is a palloc'd hash of the server certificate with its
+ * size, and NULL if there is no certificate available.
+ */
+char *
+be_tls_get_certificate_hash(Port *port, size_t *len)
+{
+ char *cert_hash = NULL;
+ X509 *server_cert;
+
+ *len = 0;
+ server_cert = SSL_get_certificate(port->ssl);
+
+ if (server_cert != NULL)
+ {
+ const EVP_MD *algo_type = NULL;
+ char hash[EVP_MAX_MD_SIZE]; /* size for SHA-512 */
+ unsigned int hash_size;
+ int algo_nid;
+
+ /*
+ * Get the signature algorithm of the certificate to determine the
+ * hash algorithm to use for the result.
+ */
+ if (!OBJ_find_sigid_algs(X509_get_signature_nid(server_cert),
+ &algo_nid, NULL))
+ elog(ERROR, "could not find signature algorithm");
+
+ switch (algo_nid)
+ {
+ case NID_md5:
+ case NID_sha1:
+ algo_type = EVP_sha256();
+ break;
+
+ default:
+ algo_type = EVP_get_digestbynid(algo_nid);
+ if (algo_type == NULL)
+ elog(ERROR, "could not find digest for NID %s",
+ OBJ_nid2sn(algo_nid));
+ break;
+ }
+
+ /* generate and save the certificate hash */
+ if (!X509_digest(server_cert, algo_type, (unsigned char *) hash,
+ &hash_size))
+ elog(ERROR, "could not generate server certificate hash");
+
+ cert_hash = (char *) palloc(hash_size);
+ memcpy(cert_hash, hash, hash_size);
+ *len = hash_size;
+ }
+
+ return cert_hash;
+}
+
/*
* Convert an X509 subject name to a cstring.
*
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 856e0439d5..cf9d8b7870 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -210,6 +210,7 @@ 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_peerdn_name(Port *port, char *ptr, size_t len);
extern char *be_tls_get_peer_finished(Port *port, size_t *len);
+extern char *be_tls_get_certificate_hash(Port *port, size_t *len);
#endif
extern ProtocolVersion FrontendProtocol;
diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h
index 99560d3d2f..d4e585b3bf 100644
--- a/src/include/libpq/scram.h
+++ b/src/include/libpq/scram.h
@@ -19,6 +19,7 @@
/* Channel binding types */
#define SCRAM_CHANNEL_BINDING_TLS_UNIQUE "tls-unique"
+#define SCRAM_CHANNEL_BINDING_TLS_ENDPOINT "tls-server-end-point"
/* Status codes for message exchange */
#define SASL_EXCHANGE_CONTINUE 0
@@ -28,7 +29,8 @@
/* Routines dedicated to authentication */
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);
+ size_t tls_finished_len, const char *certificate_hash,
+ size_t certificate_hash_len);
extern int pg_be_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen, char **logdetail);
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index 9860f6b15e..38de1f14cb 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -48,6 +48,8 @@ typedef struct
bool ssl_in_use;
char *tls_finished_message;
size_t tls_finished_len;
+ char *certificate_hash;
+ size_t certificate_hash_len;
char *sasl_mechanism;
const char *channel_binding_type;
@@ -96,7 +98,9 @@ pg_fe_scram_init(const char *username,
const char *sasl_mechanism,
const char *channel_binding_type,
char *tls_finished_message,
- size_t tls_finished_len)
+ size_t tls_finished_len,
+ char *certificate_hash,
+ size_t certificate_hash_len)
{
fe_scram_state *state;
char *prep_password;
@@ -113,6 +117,8 @@ pg_fe_scram_init(const char *username,
state->ssl_in_use = ssl_in_use;
state->tls_finished_message = tls_finished_message;
state->tls_finished_len = tls_finished_len;
+ state->certificate_hash = certificate_hash;
+ state->certificate_hash_len = certificate_hash_len;
state->sasl_mechanism = strdup(sasl_mechanism);
if (!state->sasl_mechanism)
{
@@ -121,9 +127,9 @@ pg_fe_scram_init(const char *username,
}
/*
- * Store channel binding type. Only one type is currently supported,
- * tls-unique, which is also the default. Anything defined by the caller
- * is forcibly used.
+ * Store channel binding type. Two types are currently supported,
+ * tls-unique, which is also the default, and tls-server-end-point.
+ * Anything defined by the caller is forcibly used.
*/
if (channel_binding_type && strlen(channel_binding_type) > 0)
state->channel_binding_type = channel_binding_type;
@@ -165,8 +171,8 @@ pg_fe_scram_free(void *opaq)
free(state->password);
if (state->tls_finished_message)
free(state->tls_finished_message);
- if (state->sasl_mechanism)
- free(state->sasl_mechanism);
+ if (state->certificate_hash)
+ free(state->certificate_hash);
/* client messages */
if (state->client_nonce)
@@ -457,6 +463,12 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
cbind_data = state->tls_finished_message;
cbind_data_len = state->tls_finished_len;
}
+ else if (strcmp(state->channel_binding_type,
+ SCRAM_CHANNEL_BINDING_TLS_ENDPOINT) == 0)
+ {
+ cbind_data = state->certificate_hash;
+ cbind_data_len = state->certificate_hash_len;
+ }
else
{
/* should not happen */
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 2f34340f45..38cde81d55 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -493,6 +493,8 @@ pg_SASL_init(PGconn *conn, int payloadlen)
PQExpBufferData mechanism_buf;
char *tls_finished = NULL;
size_t tls_finished_len = 0;
+ char *certificate_hash = NULL;
+ size_t certificate_hash_len = 0;
char *password;
initPQExpBuffer(&mechanism_buf);
@@ -577,6 +579,12 @@ pg_SASL_init(PGconn *conn, int payloadlen)
tls_finished = pgtls_get_finished(conn, &tls_finished_len);
if (tls_finished == NULL)
goto oom_error;
+
+ certificate_hash =
+ pgtls_get_peer_certificate_hash(conn,
+ &certificate_hash_len);
+ if (certificate_hash == NULL)
+ goto error; /* error message is set */
}
#endif
@@ -592,7 +600,9 @@ pg_SASL_init(PGconn *conn, int payloadlen)
selected_mechanism,
conn->saslchannelbinding,
tls_finished,
- tls_finished_len);
+ tls_finished_len,
+ certificate_hash,
+ certificate_hash_len);
if (!conn->sasl_state)
goto oom_error;
diff --git a/src/interfaces/libpq/fe-auth.h b/src/interfaces/libpq/fe-auth.h
index 96094b0c1e..17e85d90f4 100644
--- a/src/interfaces/libpq/fe-auth.h
+++ b/src/interfaces/libpq/fe-auth.h
@@ -29,7 +29,9 @@ extern void *pg_fe_scram_init(const char *username,
const char *sasl_mechanism,
const char *channel_binding_type,
char *tls_finished_message,
- size_t tls_finished_len);
+ size_t tls_finished_len,
+ char *certificate_hash,
+ size_t certificate_hash_len);
extern void pg_fe_scram_free(void *opaq);
extern void pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen,
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 61d161b367..99077c3d9a 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -419,6 +419,84 @@ pgtls_get_finished(PGconn *conn, size_t *len)
return result;
}
+/*
+ * Get the hash of the server certificate
+ *
+ * This information is useful for end-point channel binding, where the
+ * client certificate hash is used as a link, per RFC 5929. If the
+ * signature hash algorithm is MD5 or SHA-1, fall back to SHA-256,
+ * as per RFC 5929 (https://tools.ietf.org/html/rfc5929#section-4.1).
+ * NULL is sent back to the caller in the event of an error, with an
+ * error message for the caller to consume.
+ */
+char *
+pgtls_get_peer_certificate_hash(PGconn *conn, size_t *len)
+{
+ char *cert_hash = NULL;
+
+ *len = 0;
+
+ if (conn->peer)
+ {
+ X509 *peer_cert = conn->peer;
+ const EVP_MD *algo_type = NULL;
+ char hash[EVP_MAX_MD_SIZE]; /* size for SHA-512 */
+ unsigned int hash_size;
+ int algo_nid;
+
+ /*
+ * Get the signature algorithm of the certificate to determine the
+ * hash algorithm to use for the result.
+ */
+ if (!OBJ_find_sigid_algs(X509_get_signature_nid(peer_cert),
+ &algo_nid, NULL))
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("could not find signature algorithm\n"));
+ return NULL;
+ }
+
+ switch (algo_nid)
+ {
+ case NID_md5:
+ case NID_sha1:
+ algo_type = EVP_sha256();
+ break;
+
+ default:
+ algo_type = EVP_get_digestbynid(algo_nid);
+ if (algo_type == NULL)
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("could not find digest for NID %s\n"),
+ OBJ_nid2sn(algo_nid));
+ return NULL;
+ }
+ break;
+ }
+
+ if (!X509_digest(peer_cert, algo_type, (unsigned char *) hash,
+ &hash_size))
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("could not generate peer certificate hash\n"));
+ return NULL;
+ }
+
+ /* save result */
+ cert_hash = (char *) malloc(hash_size);
+ if (cert_hash == NULL)
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("out of memory\n"));
+ return NULL;
+ }
+ memcpy(cert_hash, hash, hash_size);
+ *len = hash_size;
+ }
+
+ return cert_hash;
+}
/* ------------------------------------------------------------ */
/* OpenSSL specific code */
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 1cb096f1ef..df37fc9b7f 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -672,6 +672,7 @@ extern ssize_t pgtls_read(PGconn *conn, void *ptr, size_t len);
extern bool pgtls_read_pending(PGconn *conn);
extern ssize_t pgtls_write(PGconn *conn, const void *ptr, size_t len);
extern char *pgtls_get_finished(PGconn *conn, size_t *len);
+extern char *pgtls_get_peer_certificate_hash(PGconn *conn, size_t *len);
/*
* this is so that we can check if a connection is non-blocking internally
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index c0b9599920..5a2a24164e 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -4,7 +4,7 @@ use strict;
use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 3;
+use Test::More tests => 4;
use ServerSetup;
use File::Copy;
@@ -42,6 +42,9 @@ test_connect_ok($common_connstr, '',
test_connect_ok($common_connstr,
"saslchannelbinding=tls-unique",
"SCRAM authentication with tls-unique as channel binding");
+test_connect_ok($common_connstr,
+ "saslchannelbinding=tls-server-end-point",
+ "SCRAM authentication with tls-server-end-point as channel binding");
test_connect_fails($common_connstr,
"saslchannelbinding=not-exists",
"SCRAM authentication with invalid channel binding");
--
2.15.0
0001-Add-connection-parameter-saslchannelbinding.patchtext/x-patch; charset=US-ASCII; name=0001-Add-connection-parameter-saslchannelbinding.patchDownload
From b9ee2d9432a0f16cdd59c9789a064d68f0220ad0 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 21 Nov 2017 12:55:29 +0900
Subject: [PATCH 1/2] Add connection parameter "saslchannelbinding"
This parameter can be used to enforce the value of the type of channel
binding used during a SASL message exchange. This proves to be useful
now to check code paths where an invalid channel binding type is used
by a client, which is limited, but will be more useful to allow clients
to enforce the channel binding type to tls-enpoint once it gets added.
More tests dedicated to SASL and channel binding are added as well to
the SSL test suite, which is handy to check the validity of this patch.
---
doc/src/sgml/libpq.sgml | 12 ++++++++++++
src/interfaces/libpq/fe-auth-scram.c | 10 ++++++++--
src/interfaces/libpq/fe-auth.c | 1 +
src/interfaces/libpq/fe-auth.h | 1 +
src/interfaces/libpq/fe-connect.c | 7 +++++++
src/interfaces/libpq/libpq-int.h | 1 +
src/test/ssl/t/002_scram.pl | 11 ++++++++++-
7 files changed, 40 insertions(+), 3 deletions(-)
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 4703309254..2ad2061e9e 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1222,6 +1222,18 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
</listitem>
</varlistentry>
+ <varlistentry id="libpq-saslchannelbinding" xreflabel="saslchannelbinding">
+ <term><literal>saslchannelbinding</literal></term>
+ <listitem>
+ <para>
+ Controls the name of the channel binding type sent to server when doing
+ a message exchange for a SASL authentication. The list of channel
+ binding names supported by server are listed in
+ <xref linkend="sasl-authentication"/>.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="libpq-connect-sslmode" xreflabel="sslmode">
<term><literal>sslmode</literal></term>
<listitem>
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index f2403147ca..9860f6b15e 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -94,6 +94,7 @@ pg_fe_scram_init(const char *username,
const char *password,
bool ssl_in_use,
const char *sasl_mechanism,
+ const char *channel_binding_type,
char *tls_finished_message,
size_t tls_finished_len)
{
@@ -120,9 +121,14 @@ pg_fe_scram_init(const char *username,
}
/*
- * Store channel binding type. Only one type is currently supported.
+ * Store channel binding type. Only one type is currently supported,
+ * tls-unique, which is also the default. Anything defined by the caller
+ * is forcibly used.
*/
- state->channel_binding_type = SCRAM_CHANNEL_BINDING_TLS_UNIQUE;
+ if (channel_binding_type && strlen(channel_binding_type) > 0)
+ state->channel_binding_type = channel_binding_type;
+ else
+ state->channel_binding_type = SCRAM_CHANNEL_BINDING_TLS_UNIQUE;
/* Normalize the password with SASLprep, if possible */
rc = pg_saslprep(password, &prep_password);
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 9d394919ef..2f34340f45 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -590,6 +590,7 @@ pg_SASL_init(PGconn *conn, int payloadlen)
password,
conn->ssl_in_use,
selected_mechanism,
+ conn->saslchannelbinding,
tls_finished,
tls_finished_len);
if (!conn->sasl_state)
diff --git a/src/interfaces/libpq/fe-auth.h b/src/interfaces/libpq/fe-auth.h
index 1525a52742..96094b0c1e 100644
--- a/src/interfaces/libpq/fe-auth.h
+++ b/src/interfaces/libpq/fe-auth.h
@@ -27,6 +27,7 @@ extern void *pg_fe_scram_init(const char *username,
const char *password,
bool ssl_in_use,
const char *sasl_mechanism,
+ const char *channel_binding_type,
char *tls_finished_message,
size_t tls_finished_len);
extern void pg_fe_scram_free(void *opaq);
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 2c175a2a24..3c85501754 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -262,6 +262,11 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
"TCP-Keepalives-Count", "", 10, /* strlen(INT32_MAX) == 10 */
offsetof(struct pg_conn, keepalives_count)},
+ /* Set of options proper to SASL */
+ {"saslchannelbinding", NULL, NULL, NULL,
+ "SASL-Channel", "", 22, /* sizeof("tls-unique-for-telnet") == 22 */
+ offsetof(struct pg_conn, saslchannelbinding)},
+
/*
* ssl options are allowed even without client SSL support because the
* client can still handle SSL modes "disable" and "allow". Other
@@ -3469,6 +3474,8 @@ freePGconn(PGconn *conn)
free(conn->keepalives_interval);
if (conn->keepalives_count)
free(conn->keepalives_count);
+ if (conn->saslchannelbinding)
+ free(conn->saslchannelbinding);
if (conn->sslmode)
free(conn->sslmode);
if (conn->sslcert)
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 8412ee8160..1cb096f1ef 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -349,6 +349,7 @@ struct pg_conn
* retransmits */
char *keepalives_count; /* maximum number of TCP keepalive
* retransmits */
+ char *saslchannelbinding; /* channel binding type used in SASL */
char *sslmode; /* SSL mode (require,prefer,allow,disable) */
char *sslcompression; /* SSL compression (0 or 1) */
char *sslkey; /* client key filename */
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 25f75bd52a..c0b9599920 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -4,7 +4,7 @@ use strict;
use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 1;
+use Test::More tests => 3;
use ServerSetup;
use File::Copy;
@@ -34,5 +34,14 @@ $ENV{PGPASSWORD} = "pass";
$common_connstr =
"user=ssltestuser dbname=trustdb sslmode=require hostaddr=$SERVERHOSTADDR";
+# Defaut settings
test_connect_ok($common_connstr, '',
"SCRAM authentication with default channel binding");
+
+# Channel bindings
+test_connect_ok($common_connstr,
+ "saslchannelbinding=tls-unique",
+ "SCRAM authentication with tls-unique as channel binding");
+test_connect_fails($common_connstr,
+ "saslchannelbinding=not-exists",
+ "SCRAM authentication with invalid channel binding");
--
2.15.0
On 11/26/17 06:59, Michael Paquier wrote:
On Tue, Nov 21, 2017 at 1:36 PM, Michael Paquier
<michael.paquier@gmail.com> wrote:So attached are rebased patches:
- 0001 to introduce the connection parameter saslchannelbinding, which
allows libpq to enforce the type of channel binding used during an
exchange.
- 0002 to add tls-endpoint as channel binding type, which is where 0001 shines.Attached is a rebased patch set, documentation failing to compile. I
am moving at the same time this patch set to the next commit fest.
I think these are SCRAM channel bindings, not SASL channel bindings, so
the parameter should be named accordingly.
I also wonder whether there should be a mechanism to turn off channel
binding from the client. Right now, there is no way to test the
non-PLUS mechanism in an SSL build.
--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
On Tue, Nov 28, 2017 at 11:10 AM, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:
I also wonder whether there should be a mechanism to turn off channel
binding from the client. Right now, there is no way to test the
non-PLUS mechanism in an SSL build.
I think that would be a good thing to have.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
On Wed, Nov 29, 2017 at 2:41 AM, Robert Haas <robertmhaas@gmail.com> wrote:
On Tue, Nov 28, 2017 at 11:10 AM, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:I also wonder whether there should be a mechanism to turn off channel
binding from the client. Right now, there is no way to test the
non-PLUS mechanism in an SSL build.I think that would be a good thing to have.
Sure. How do we shape that though? I would think about an extra option
for a scram-sha-256 entry with channel-binding=on|off|choice, choice
being what is currently on HEAD with letting the client decide to use
it or not.
--
Michael
On Wed, Nov 29, 2017 at 7:08 AM, Michael Paquier
<michael.paquier@gmail.com> wrote:
On Wed, Nov 29, 2017 at 2:41 AM, Robert Haas <robertmhaas@gmail.com> wrote:
On Tue, Nov 28, 2017 at 11:10 AM, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:I also wonder whether there should be a mechanism to turn off channel
binding from the client. Right now, there is no way to test the
non-PLUS mechanism in an SSL build.I think that would be a good thing to have.
Sure. How do we shape that though? I would think about an extra option
for a scram-sha-256 entry with channel-binding=on|off|choice, choice
being what is currently on HEAD with letting the client decide to use
it or not.
Sorry, mind-slipping of the morning. Having an option from the server
would help in restricting access, so there could be some use for it
but not for testing coverage. Still how do we want to shape that for
the client? I can think of two possibilities:
1) Have a special value in the parameter saslchannelbinding proposed
in patch 0001. For example by specifying "none" then no channel
binding is used.
2) Use a dedicated parameter which is a on-off switch.
Any thoughts?
--
Michael
On 11/28/17 17:33, Michael Paquier wrote:
1) Have a special value in the parameter saslchannelbinding proposed
in patch 0001. For example by specifying "none" then no channel
binding is used.
I was thinking if it's empty then don't use channel binding. Right now,
empty means the same thing as tls-unique. In any case, some variant of
that should be fine. I don't think we need a separate server option
that this point.
--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
On Wed, Nov 29, 2017 at 7:42 AM, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:
On 11/28/17 17:33, Michael Paquier wrote:
1) Have a special value in the parameter saslchannelbinding proposed
in patch 0001. For example by specifying "none" then no channel
binding is used.I was thinking if it's empty then don't use channel binding. Right now,
empty means the same thing as tls-unique. In any case, some variant of
that should be fine. I don't think we need a separate server option
that this point.
OK, here is a reworked version with the following changes:
- renamed saslchannelbinding to scramchannelbinding, with a default
set to tls-unique.
- An empty value of scramchannelbinding allows client to not use
channel binding, or in short use use SCRAM-SHA-256 and cbind-flag set
to 'n'.
While reviewing the code, I have found something a bit disturbing with
the header definitions: the libpq frontend code includes scram.h,
which references backend-side routines. So I think that the definition
of the SCRAM mechanisms as well as the channel binding types should be
moved to scram-common.h. This cleanup is included in 0001.
--
Michael
Attachments:
0001-Move-SCRAM-related-name-definitions-to-scram-common..patchapplication/octet-stream; name=0001-Move-SCRAM-related-name-definitions-to-scram-common..patchDownload
From e1f98fdbaa1debf932c874af47b5d635f2ab0531 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 1 Dec 2017 09:59:35 +0900
Subject: [PATCH 1/3] Move SCRAM-related name definitions to scram-common.h
Mechanism names for SCRAM and channel binding names have been included
in scram.h by the libpq frontend code, and this header references a set
of routines which are only used by the backend. scram-common.h is on the
contrary usable by both the backend and libpq, so referencing those
names there is more adapted.
---
src/backend/libpq/auth.c | 1 +
src/include/common/scram-common.h | 7 +++++++
src/include/libpq/scram.h | 7 -------
src/interfaces/libpq/fe-auth-scram.c | 1 -
src/interfaces/libpq/fe-auth.c | 2 +-
5 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 19a91ca67d..b7f9bb1669 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -26,6 +26,7 @@
#include "commands/user.h"
#include "common/ip.h"
#include "common/md5.h"
+#include "common/scram-common.h"
#include "libpq/auth.h"
#include "libpq/crypt.h"
#include "libpq/libpq.h"
diff --git a/src/include/common/scram-common.h b/src/include/common/scram-common.h
index 0c5ee04f26..857a60e71f 100644
--- a/src/include/common/scram-common.h
+++ b/src/include/common/scram-common.h
@@ -15,6 +15,13 @@
#include "common/sha2.h"
+/* Name of SCRAM mechanisms per IANA */
+#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"
+
/* Length of SCRAM keys (client and server) */
#define SCRAM_KEY_LEN PG_SHA256_DIGEST_LENGTH
diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h
index 91f1e0f2c7..2c245813d6 100644
--- a/src/include/libpq/scram.h
+++ b/src/include/libpq/scram.h
@@ -13,13 +13,6 @@
#ifndef PG_SCRAM_H
#define PG_SCRAM_H
-/* Name of SCRAM mechanisms per IANA */
-#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 */
#define SASL_EXCHANGE_CONTINUE 0
#define SASL_EXCHANGE_SUCCESS 1
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index 97db0b1faa..8acad6f150 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -17,7 +17,6 @@
#include "common/base64.h"
#include "common/saslprep.h"
#include "common/scram-common.h"
-#include "libpq/scram.h"
#include "fe-auth.h"
/* These are needed for getpid(), in the fallback implementation */
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index f54ad8e0cc..2cfdb7c125 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -39,8 +39,8 @@
#endif
#include "common/md5.h"
+#include "common/scram-common.h"
#include "libpq-fe.h"
-#include "libpq/scram.h"
#include "fe-auth.h"
--
2.15.0
0002-Add-connection-parameter-scramchannelbinding.patchapplication/octet-stream; name=0002-Add-connection-parameter-scramchannelbinding.patchDownload
From 8649faeff121d4e7d11030c62056609e401b3e55 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 1 Dec 2017 10:47:51 +0900
Subject: [PATCH 2/3] Add connection parameter "scramchannelbinding"
This parameter can be used to enforce the value of the type of channel
binding used during a SASL message exchange. This proves to be useful
now to check code paths where an invalid channel binding type is used
by a client, which is limited, but will be more useful to allow clients
to enforce the channel binding type to tls-enpoint once it gets added.
The default value is tls-unique, which is what RFC 5802 specifies. Clients
can optionally specify an empty value which has as effect to not use
channel binding and use SCRAM-SHA-256 as SASL mechanism chosen.
More tests dedicated to SASL and channel binding are added as well to
the SSL test suite, which is handy to check the validity of this patch.
---
doc/src/sgml/libpq.sgml | 14 ++++++++++++++
src/interfaces/libpq/fe-auth-scram.c | 20 +++++++++++++++-----
src/interfaces/libpq/fe-auth.c | 9 ++++++---
src/interfaces/libpq/fe-auth.h | 1 +
src/interfaces/libpq/fe-connect.c | 10 ++++++++++
src/interfaces/libpq/libpq-int.h | 1 +
src/test/ssl/t/002_scram.pl | 14 +++++++++++++-
7 files changed, 60 insertions(+), 9 deletions(-)
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 4703309254..8f1588d0c6 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1222,6 +1222,20 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
</listitem>
</varlistentry>
+ <varlistentry id="libpq-scramchannelbinding" xreflabel="scramchannelbinding">
+ <term><literal>scramchannelbinding</literal></term>
+ <listitem>
+ <para>
+ Controls the name of the channel binding type sent to server when doing
+ a message exchange for a SCRAM authentication. The list of channel
+ binding names supported by server are listed in
+ <xref linkend="sasl-authentication"/>. An empty value allows client
+ to not use channel binding. The default value is
+ <literal>tls-unique</literal>.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="libpq-connect-sslmode" xreflabel="sslmode">
<term><literal>sslmode</literal></term>
<listitem>
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index 8acad6f150..66e01aefaa 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -93,6 +93,7 @@ pg_fe_scram_init(const char *username,
const char *password,
bool ssl_in_use,
const char *sasl_mechanism,
+ const char *channel_binding_type,
char *tls_finished_message,
size_t tls_finished_len)
{
@@ -112,17 +113,14 @@ pg_fe_scram_init(const char *username,
state->tls_finished_message = tls_finished_message;
state->tls_finished_len = tls_finished_len;
state->sasl_mechanism = strdup(sasl_mechanism);
+ state->channel_binding_type = channel_binding_type;
+
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 */
rc = pg_saslprep(password, &prep_password);
if (rc == SASLPREP_OOM)
@@ -375,6 +373,15 @@ build_client_first_message(fe_scram_state *state, PQExpBuffer errormessage)
Assert(state->ssl_in_use);
appendPQExpBuffer(&buf, "p=%s", state->channel_binding_type);
}
+ else if (state->channel_binding_type == NULL ||
+ strlen(state->channel_binding_type) == 0)
+ {
+ /*
+ * Client has chosen to not show to server that it supports channel
+ * binding.
+ */
+ appendPQExpBuffer(&buf, "n");
+ }
else if (state->ssl_in_use)
{
/*
@@ -489,6 +496,9 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
free(cbind_input);
}
+ else if (state->channel_binding_type == NULL ||
+ strlen(state->channel_binding_type) == 0)
+ appendPQExpBuffer(&buf, "c=biws"); /* base64 of "n,," */
else if (state->ssl_in_use)
appendPQExpBuffer(&buf, "c=eSws"); /* base64 of "y,," */
else
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 2cfdb7c125..5828d4207c 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -528,11 +528,13 @@ pg_SASL_init(PGconn *conn, int payloadlen)
/*
* Select the mechanism to use. Pick SCRAM-SHA-256-PLUS over anything
- * 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.
+ * else if a channel binding type is defined. 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 (conn->ssl_in_use &&
+ conn->scramchannelbinding &&
+ strlen(conn->scramchannelbinding) > 0 &&
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 &&
@@ -591,6 +593,7 @@ pg_SASL_init(PGconn *conn, int payloadlen)
password,
conn->ssl_in_use,
selected_mechanism,
+ conn->scramchannelbinding,
tls_finished,
tls_finished_len);
if (!conn->sasl_state)
diff --git a/src/interfaces/libpq/fe-auth.h b/src/interfaces/libpq/fe-auth.h
index 3e92410eae..db319ac071 100644
--- a/src/interfaces/libpq/fe-auth.h
+++ b/src/interfaces/libpq/fe-auth.h
@@ -27,6 +27,7 @@ extern void *pg_fe_scram_init(const char *username,
const char *password,
bool ssl_in_use,
const char *sasl_mechanism,
+ const char *channel_binding_type,
char *tls_finished_message,
size_t tls_finished_len);
extern void pg_fe_scram_free(void *opaq);
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 2c175a2a24..ecaa7a541e 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -71,6 +71,7 @@ static int ldapServiceLookup(const char *purl, PQconninfoOption *options,
#endif
#include "common/ip.h"
+#include "common/scram-common.h"
#include "mb/pg_wchar.h"
#include "port/pg_bswap.h"
@@ -122,6 +123,7 @@ static int ldapServiceLookup(const char *purl, PQconninfoOption *options,
#define DefaultOption ""
#define DefaultAuthtype ""
#define DefaultTargetSessionAttrs "any"
+#define DefaultSCRAMChannelBinding SCRAM_CHANNEL_BINDING_TLS_UNIQUE
#ifdef USE_SSL
#define DefaultSSLMode "prefer"
#else
@@ -262,6 +264,12 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
"TCP-Keepalives-Count", "", 10, /* strlen(INT32_MAX) == 10 */
offsetof(struct pg_conn, keepalives_count)},
+ /* Set of options proper to SCRAM */
+ {"scramchannelbinding", NULL, DefaultSCRAMChannelBinding, NULL,
+ "SCRAM-Channel-Binding", "",
+ 22, /* sizeof("tls-unique-for-telnet") == 22 */
+ offsetof(struct pg_conn, scramchannelbinding)},
+
/*
* ssl options are allowed even without client SSL support because the
* client can still handle SSL modes "disable" and "allow". Other
@@ -3469,6 +3477,8 @@ freePGconn(PGconn *conn)
free(conn->keepalives_interval);
if (conn->keepalives_count)
free(conn->keepalives_count);
+ if (conn->scramchannelbinding)
+ free(conn->scramchannelbinding);
if (conn->sslmode)
free(conn->sslmode);
if (conn->sslcert)
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 8412ee8160..b116ff8032 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -349,6 +349,7 @@ struct pg_conn
* retransmits */
char *keepalives_count; /* maximum number of TCP keepalive
* retransmits */
+ char *scramchannelbinding; /* channel binding type used in SASL */
char *sslmode; /* SSL mode (require,prefer,allow,disable) */
char *sslcompression; /* SSL compression (0 or 1) */
char *sslkey; /* client key filename */
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 25f75bd52a..c8e9114d64 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -4,7 +4,7 @@ use strict;
use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 1;
+use Test::More tests => 4;
use ServerSetup;
use File::Copy;
@@ -34,5 +34,17 @@ $ENV{PGPASSWORD} = "pass";
$common_connstr =
"user=ssltestuser dbname=trustdb sslmode=require hostaddr=$SERVERHOSTADDR";
+# Default settings
test_connect_ok($common_connstr, '',
"SCRAM authentication with default channel binding");
+
+# Channel bindings
+test_connect_ok($common_connstr,
+ "scramchannelbinding=tls-unique",
+ "SCRAM authentication with tls-unique as channel binding");
+test_connect_ok($common_connstr,
+ "scramchannelbinding=''",
+ "SCRAM authentication without channel binding");
+test_connect_fails($common_connstr,
+ "scramchannelbinding=not-exists",
+ "SCRAM authentication with invalid channel binding");
--
2.15.0
0003-Implement-channel-binding-tls-server-end-point-for-S.patchapplication/octet-stream; name=0003-Implement-channel-binding-tls-server-end-point-for-S.patchDownload
From 806c0e4d574513fc33e7bc108b0e393f5973d172 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 10 Oct 2017 22:04:22 +0900
Subject: [PATCH 3/3] Implement channel binding tls-server-end-point for SCRAM
As referenced in RFC 5929, this channel binding is not the default value
and uses a hash of the certificate as binding data. On the frontend, this
can be resumed in getting the data from SSL_get_peer_certificate() and
on the backend SSL_get_certificate().
The hashing algorithm needs also to switch to SHA-256 if the signature
algorithm is MD5 or SHA-1, so let's be careful about that.
---
doc/src/sgml/protocol.sgml | 5 +-
src/backend/libpq/auth-scram.c | 26 ++++++++---
src/backend/libpq/auth.c | 8 +++-
src/backend/libpq/be-secure-openssl.c | 61 +++++++++++++++++++++++++
src/include/common/scram-common.h | 1 +
src/include/libpq/libpq-be.h | 1 +
src/include/libpq/scram.h | 3 +-
src/interfaces/libpq/fe-auth-scram.c | 18 ++++++--
src/interfaces/libpq/fe-auth.c | 12 ++++-
src/interfaces/libpq/fe-auth.h | 4 +-
src/interfaces/libpq/fe-secure-openssl.c | 78 ++++++++++++++++++++++++++++++++
src/interfaces/libpq/libpq-int.h | 1 +
src/test/ssl/t/002_scram.pl | 5 +-
13 files changed, 207 insertions(+), 16 deletions(-)
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 8174e3defa..365f72b51d 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1576,8 +1576,9 @@ the password is in.
<para>
<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.
+is <literal>SCRAM-SHA-256-PLUS</literal>. Two channel binding types are
+supported at the moment: <literal>tls-unique</literal>, which is the default,
+and <literal>tls-server-end-point</literal>, both defined in RFC 5929.
</para>
<procedure>
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index 15c3857f57..76a5fcbd65 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -113,6 +113,8 @@ typedef struct
bool ssl_in_use;
const char *tls_finished_message;
size_t tls_finished_len;
+ const char *certificate_hash;
+ size_t certificate_hash_len;
char *channel_binding_type;
int iterations;
@@ -175,7 +177,9 @@ 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)
+ size_t tls_finished_len,
+ const char *certificate_hash,
+ size_t certificate_hash_len)
{
scram_state *state;
bool got_verifier;
@@ -186,6 +190,8 @@ pg_be_scram_init(const char *username,
state->ssl_in_use = ssl_in_use;
state->tls_finished_message = tls_finished_message;
state->tls_finished_len = tls_finished_len;
+ state->certificate_hash = certificate_hash;
+ state->certificate_hash_len = certificate_hash_len;
state->channel_binding_type = NULL;
/*
@@ -855,13 +861,15 @@ read_client_first_message(scram_state *state, char *input)
}
/*
- * 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.)
+ * Read value provided by client; only tls-unique and
+ * tls-server-end-point are 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)
+ if (strcmp(channel_binding_type, SCRAM_CHANNEL_BINDING_TLS_UNIQUE) != 0 &&
+ strcmp(channel_binding_type, SCRAM_CHANNEL_BINDING_TLS_ENDPOINT) != 0)
ereport(ERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION),
(errmsg("unsupported SCRAM channel-binding type"))));
@@ -1119,6 +1127,12 @@ read_client_final_message(scram_state *state, char *input)
cbind_data = state->tls_finished_message;
cbind_data_len = state->tls_finished_len;
}
+ else if (strcmp(state->channel_binding_type,
+ SCRAM_CHANNEL_BINDING_TLS_ENDPOINT) == 0)
+ {
+ cbind_data = state->certificate_hash;
+ cbind_data_len = state->certificate_hash_len;
+ }
else
{
/* should not happen */
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index b7f9bb1669..700a3bffa4 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -875,6 +875,8 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
bool initial;
char *tls_finished = NULL;
size_t tls_finished_len = 0;
+ char *certificate_hash = NULL;
+ size_t certificate_hash_len = 0;
/*
* SASL auth is not supported for protocol versions before 3, because it
@@ -923,6 +925,8 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
if (port->ssl_in_use)
{
tls_finished = be_tls_get_peer_finished(port, &tls_finished_len);
+ certificate_hash = be_tls_get_certificate_hash(port,
+ &certificate_hash_len);
}
#endif
@@ -941,7 +945,9 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
shadow_pass,
port->ssl_in_use,
tls_finished,
- tls_finished_len);
+ tls_finished_len,
+ certificate_hash,
+ certificate_hash_len);
/*
* Loop through SASL message exchange. This exchange can consist of
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 1e3e19f5e0..e3e8a535c8 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -1239,6 +1239,67 @@ be_tls_get_peer_finished(Port *port, size_t *len)
return result;
}
+/*
+ * Get the server certificate hash for authentication purposes. Per
+ * RFC 5929 and tls-server-end-point, the TLS server's certificate bytes
+ * need to be hashed with SHA-256 if its signature algorithm is MD5 or
+ * SHA-1 as per RFC 5929 (https://tools.ietf.org/html/rfc5929#section-4.1).
+ * If something else is used, the same hash as the signature algorithm is
+ * used. The result is a palloc'd hash of the server certificate with its
+ * size, and NULL if there is no certificate available.
+ */
+char *
+be_tls_get_certificate_hash(Port *port, size_t *len)
+{
+ char *cert_hash = NULL;
+ X509 *server_cert;
+
+ *len = 0;
+ server_cert = SSL_get_certificate(port->ssl);
+
+ if (server_cert != NULL)
+ {
+ const EVP_MD *algo_type = NULL;
+ char hash[EVP_MAX_MD_SIZE]; /* size for SHA-512 */
+ unsigned int hash_size;
+ int algo_nid;
+
+ /*
+ * Get the signature algorithm of the certificate to determine the
+ * hash algorithm to use for the result.
+ */
+ if (!OBJ_find_sigid_algs(X509_get_signature_nid(server_cert),
+ &algo_nid, NULL))
+ elog(ERROR, "could not find signature algorithm");
+
+ switch (algo_nid)
+ {
+ case NID_md5:
+ case NID_sha1:
+ algo_type = EVP_sha256();
+ break;
+
+ default:
+ algo_type = EVP_get_digestbynid(algo_nid);
+ if (algo_type == NULL)
+ elog(ERROR, "could not find digest for NID %s",
+ OBJ_nid2sn(algo_nid));
+ break;
+ }
+
+ /* generate and save the certificate hash */
+ if (!X509_digest(server_cert, algo_type, (unsigned char *) hash,
+ &hash_size))
+ elog(ERROR, "could not generate server certificate hash");
+
+ cert_hash = (char *) palloc(hash_size);
+ memcpy(cert_hash, hash, hash_size);
+ *len = hash_size;
+ }
+
+ return cert_hash;
+}
+
/*
* Convert an X509 subject name to a cstring.
*
diff --git a/src/include/common/scram-common.h b/src/include/common/scram-common.h
index 857a60e71f..5aec5cadb8 100644
--- a/src/include/common/scram-common.h
+++ b/src/include/common/scram-common.h
@@ -21,6 +21,7 @@
/* Channel binding types */
#define SCRAM_CHANNEL_BINDING_TLS_UNIQUE "tls-unique"
+#define SCRAM_CHANNEL_BINDING_TLS_ENDPOINT "tls-server-end-point"
/* Length of SCRAM keys (client and server) */
#define SCRAM_KEY_LEN PG_SHA256_DIGEST_LENGTH
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 856e0439d5..cf9d8b7870 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -210,6 +210,7 @@ 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_peerdn_name(Port *port, char *ptr, size_t len);
extern char *be_tls_get_peer_finished(Port *port, size_t *len);
+extern char *be_tls_get_certificate_hash(Port *port, size_t *len);
#endif
extern ProtocolVersion FrontendProtocol;
diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h
index 2c245813d6..7c8f009a3b 100644
--- a/src/include/libpq/scram.h
+++ b/src/include/libpq/scram.h
@@ -21,7 +21,8 @@
/* Routines dedicated to authentication */
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);
+ size_t tls_finished_len, const char *certificate_hash,
+ size_t certificate_hash_len);
extern int pg_be_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen, char **logdetail);
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index 66e01aefaa..6e037df20c 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -47,6 +47,8 @@ typedef struct
bool ssl_in_use;
char *tls_finished_message;
size_t tls_finished_len;
+ char *certificate_hash;
+ size_t certificate_hash_len;
char *sasl_mechanism;
const char *channel_binding_type;
@@ -95,7 +97,9 @@ pg_fe_scram_init(const char *username,
const char *sasl_mechanism,
const char *channel_binding_type,
char *tls_finished_message,
- size_t tls_finished_len)
+ size_t tls_finished_len,
+ char *certificate_hash,
+ size_t certificate_hash_len)
{
fe_scram_state *state;
char *prep_password;
@@ -112,6 +116,8 @@ pg_fe_scram_init(const char *username,
state->ssl_in_use = ssl_in_use;
state->tls_finished_message = tls_finished_message;
state->tls_finished_len = tls_finished_len;
+ state->certificate_hash = certificate_hash;
+ state->certificate_hash_len = certificate_hash_len;
state->sasl_mechanism = strdup(sasl_mechanism);
state->channel_binding_type = channel_binding_type;
@@ -156,8 +162,8 @@ pg_fe_scram_free(void *opaq)
free(state->password);
if (state->tls_finished_message)
free(state->tls_finished_message);
- if (state->sasl_mechanism)
- free(state->sasl_mechanism);
+ if (state->certificate_hash)
+ free(state->certificate_hash);
/* client messages */
if (state->client_nonce)
@@ -457,6 +463,12 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
cbind_data = state->tls_finished_message;
cbind_data_len = state->tls_finished_len;
}
+ else if (strcmp(state->channel_binding_type,
+ SCRAM_CHANNEL_BINDING_TLS_ENDPOINT) == 0)
+ {
+ cbind_data = state->certificate_hash;
+ cbind_data_len = state->certificate_hash_len;
+ }
else
{
/* should not happen */
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 5828d4207c..a2cfc12691 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -493,6 +493,8 @@ pg_SASL_init(PGconn *conn, int payloadlen)
PQExpBufferData mechanism_buf;
char *tls_finished = NULL;
size_t tls_finished_len = 0;
+ char *certificate_hash = NULL;
+ size_t certificate_hash_len = 0;
char *password;
initPQExpBuffer(&mechanism_buf);
@@ -580,6 +582,12 @@ pg_SASL_init(PGconn *conn, int payloadlen)
tls_finished = pgtls_get_finished(conn, &tls_finished_len);
if (tls_finished == NULL)
goto oom_error;
+
+ certificate_hash =
+ pgtls_get_peer_certificate_hash(conn,
+ &certificate_hash_len);
+ if (certificate_hash == NULL)
+ goto error; /* error message is set */
}
#endif
@@ -595,7 +603,9 @@ pg_SASL_init(PGconn *conn, int payloadlen)
selected_mechanism,
conn->scramchannelbinding,
tls_finished,
- tls_finished_len);
+ tls_finished_len,
+ certificate_hash,
+ certificate_hash_len);
if (!conn->sasl_state)
goto oom_error;
diff --git a/src/interfaces/libpq/fe-auth.h b/src/interfaces/libpq/fe-auth.h
index db319ac071..68de8b6e32 100644
--- a/src/interfaces/libpq/fe-auth.h
+++ b/src/interfaces/libpq/fe-auth.h
@@ -29,7 +29,9 @@ extern void *pg_fe_scram_init(const char *username,
const char *sasl_mechanism,
const char *channel_binding_type,
char *tls_finished_message,
- size_t tls_finished_len);
+ size_t tls_finished_len,
+ char *certificate_hash,
+ size_t certificate_hash_len);
extern void pg_fe_scram_free(void *opaq);
extern void pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen,
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 61d161b367..99077c3d9a 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -419,6 +419,84 @@ pgtls_get_finished(PGconn *conn, size_t *len)
return result;
}
+/*
+ * Get the hash of the server certificate
+ *
+ * This information is useful for end-point channel binding, where the
+ * client certificate hash is used as a link, per RFC 5929. If the
+ * signature hash algorithm is MD5 or SHA-1, fall back to SHA-256,
+ * as per RFC 5929 (https://tools.ietf.org/html/rfc5929#section-4.1).
+ * NULL is sent back to the caller in the event of an error, with an
+ * error message for the caller to consume.
+ */
+char *
+pgtls_get_peer_certificate_hash(PGconn *conn, size_t *len)
+{
+ char *cert_hash = NULL;
+
+ *len = 0;
+
+ if (conn->peer)
+ {
+ X509 *peer_cert = conn->peer;
+ const EVP_MD *algo_type = NULL;
+ char hash[EVP_MAX_MD_SIZE]; /* size for SHA-512 */
+ unsigned int hash_size;
+ int algo_nid;
+
+ /*
+ * Get the signature algorithm of the certificate to determine the
+ * hash algorithm to use for the result.
+ */
+ if (!OBJ_find_sigid_algs(X509_get_signature_nid(peer_cert),
+ &algo_nid, NULL))
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("could not find signature algorithm\n"));
+ return NULL;
+ }
+
+ switch (algo_nid)
+ {
+ case NID_md5:
+ case NID_sha1:
+ algo_type = EVP_sha256();
+ break;
+
+ default:
+ algo_type = EVP_get_digestbynid(algo_nid);
+ if (algo_type == NULL)
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("could not find digest for NID %s\n"),
+ OBJ_nid2sn(algo_nid));
+ return NULL;
+ }
+ break;
+ }
+
+ if (!X509_digest(peer_cert, algo_type, (unsigned char *) hash,
+ &hash_size))
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("could not generate peer certificate hash\n"));
+ return NULL;
+ }
+
+ /* save result */
+ cert_hash = (char *) malloc(hash_size);
+ if (cert_hash == NULL)
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("out of memory\n"));
+ return NULL;
+ }
+ memcpy(cert_hash, hash, hash_size);
+ *len = hash_size;
+ }
+
+ return cert_hash;
+}
/* ------------------------------------------------------------ */
/* OpenSSL specific code */
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index b116ff8032..bea8b9da06 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -672,6 +672,7 @@ extern ssize_t pgtls_read(PGconn *conn, void *ptr, size_t len);
extern bool pgtls_read_pending(PGconn *conn);
extern ssize_t pgtls_write(PGconn *conn, const void *ptr, size_t len);
extern char *pgtls_get_finished(PGconn *conn, size_t *len);
+extern char *pgtls_get_peer_certificate_hash(PGconn *conn, size_t *len);
/*
* this is so that we can check if a connection is non-blocking internally
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index c8e9114d64..b56116fc34 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -4,7 +4,7 @@ use strict;
use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 4;
+use Test::More tests => 5;
use ServerSetup;
use File::Copy;
@@ -45,6 +45,9 @@ test_connect_ok($common_connstr,
test_connect_ok($common_connstr,
"scramchannelbinding=''",
"SCRAM authentication without channel binding");
+test_connect_ok($common_connstr,
+ "scramchannelbinding=tls-server-end-point",
+ "SCRAM authentication with tls-server-end-point as channel binding");
test_connect_fails($common_connstr,
"scramchannelbinding=not-exists",
"SCRAM authentication with invalid channel binding");
--
2.15.0
On 11/30/17 21:11, Michael Paquier wrote:
OK, here is a reworked version with the following changes:
- renamed saslchannelbinding to scramchannelbinding, with a default
set to tls-unique.
- An empty value of scramchannelbinding allows client to not use
channel binding, or in short use use SCRAM-SHA-256 and cbind-flag set
to 'n'.While reviewing the code, I have found something a bit disturbing with
the header definitions: the libpq frontend code includes scram.h,
which references backend-side routines. So I think that the definition
of the SCRAM mechanisms as well as the channel binding types should be
moved to scram-common.h. This cleanup is included in 0001.
I have committed 0001 and 0002 (renaming to scram_channel_binding).
The 0003 patch looks mostly fine as well. The only concern I have is
that the way it is set up now, we make the server compute the channel
binding data for both tls-unique and tls-server-end-point, even though
we only end up using one. This might need some restructuring so that we
only get the data we need once we have learned which channel binding
type the client requested.
--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
On Wed, Dec 20, 2017 at 1:19 AM, Peter Eisentraut
<peter.eisentraut@2ndquadrant.com> wrote:
I have committed 0001 and 0002 (renaming to scram_channel_binding).
Thanks!
The 0003 patch looks mostly fine as well. The only concern I have is
that the way it is set up now, we make the server compute the channel
binding data for both tls-unique and tls-server-end-point, even though
we only end up using one. This might need some restructuring so that we
only get the data we need once we have learned which channel binding
type the client requested.
The current patch is focused on simplicity and it has the advantage
that there is no need to depend on any SSL structures or Port* in
fe-auth-scram.c and auth-scram.c. So I would really like to keep the
code simple with this goal in mind.
Speaking of which, making the server-side code is going to be in my
opinion grotty, because the server only knows about the channel
binding type used by the client after reading its first message, so we
would need to call directly the SSL-related APIs in auth-scram.c, and
update scram_state with the appropriate data in the middle of the
exchange. This causes the addition of two dependencies to Port* and
the SSL APIs into auth-scram.c.
However, it is possible to simply optimize the frontend code as in
pg_SASL_init() we already know the channel binding type selected when
calling pgtls_get_finished() and pgtls_get_peer_certificate_hash(). So
while I agree with your point, my opinion is to keep the code as
simple as possible, and then just optimize the frontend code. What do
you think?
--
Michael
On Wed, Dec 20, 2017 at 09:35:55AM +0900, Michael Paquier wrote:
However, it is possible to simply optimize the frontend code as in
pg_SASL_init() we already know the channel binding type selected when
calling pgtls_get_finished() and pgtls_get_peer_certificate_hash(). So
while I agree with your point, my opinion is to keep the code as
simple as possible, and then just optimize the frontend code. What do
you think?
I have looked at how things could be done in symmetry for both the frontend
and backend code, and I have produced the attached patch 0002, which
can be applied on top of 0001 implementing tls-server-end-point. This
simplifies the interfaces to initialize the SCRAM status data by saving
into scram_state and fe_scram_state respectively Port* and PGconn* which
holds most of the data needed for the exchange. With this patch, cbind_data
is generated only if a specific channel binding type is used with the
appropriate data. So if no channel binding is used there is no additional
SSL call done to get the TLS finished data or the server certificate hash.
0001 has no real changes compared to the last versions.
Peter, thoughts?
--
Michael
Attachments:
0001-Implement-channel-binding-tls-server-end-point-for-S.patchtext/plain; charset=us-asciiDownload
From 69dcf31f5ce5938f9f56a94bf55c8439ea53ed27 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 22 Dec 2017 10:49:10 +0900
Subject: [PATCH 1/2] Implement channel binding tls-server-end-point for SCRAM
As referenced in RFC 5929, this channel binding is not the default value
and uses a hash of the certificate as binding data. On the frontend, this
can be resumed in getting the data from SSL_get_peer_certificate() and
on the backend SSL_get_certificate().
The hashing algorithm needs also to switch to SHA-256 if the signature
algorithm is MD5 or SHA-1, so let's be careful about that.
---
doc/src/sgml/protocol.sgml | 5 +-
src/backend/libpq/auth-scram.c | 26 ++++++++---
src/backend/libpq/auth.c | 8 +++-
src/backend/libpq/be-secure-openssl.c | 61 +++++++++++++++++++++++++
src/include/common/scram-common.h | 1 +
src/include/libpq/libpq-be.h | 1 +
src/include/libpq/scram.h | 3 +-
src/interfaces/libpq/fe-auth-scram.c | 18 ++++++--
src/interfaces/libpq/fe-auth.c | 12 ++++-
src/interfaces/libpq/fe-auth.h | 4 +-
src/interfaces/libpq/fe-secure-openssl.c | 78 ++++++++++++++++++++++++++++++++
src/interfaces/libpq/libpq-int.h | 1 +
src/test/ssl/t/002_scram.pl | 5 +-
13 files changed, 207 insertions(+), 16 deletions(-)
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 8174e3defa..365f72b51d 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1576,8 +1576,9 @@ the password is in.
<para>
<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.
+is <literal>SCRAM-SHA-256-PLUS</literal>. Two channel binding types are
+supported at the moment: <literal>tls-unique</literal>, which is the default,
+and <literal>tls-server-end-point</literal>, both defined in RFC 5929.
</para>
<procedure>
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index d52a763457..849587d141 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -114,6 +114,8 @@ typedef struct
bool ssl_in_use;
const char *tls_finished_message;
size_t tls_finished_len;
+ const char *certificate_hash;
+ size_t certificate_hash_len;
char *channel_binding_type;
int iterations;
@@ -176,7 +178,9 @@ 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)
+ size_t tls_finished_len,
+ const char *certificate_hash,
+ size_t certificate_hash_len)
{
scram_state *state;
bool got_verifier;
@@ -187,6 +191,8 @@ pg_be_scram_init(const char *username,
state->ssl_in_use = ssl_in_use;
state->tls_finished_message = tls_finished_message;
state->tls_finished_len = tls_finished_len;
+ state->certificate_hash = certificate_hash;
+ state->certificate_hash_len = certificate_hash_len;
state->channel_binding_type = NULL;
/*
@@ -857,13 +863,15 @@ read_client_first_message(scram_state *state, char *input)
}
/*
- * 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.)
+ * Read value provided by client; only tls-unique and
+ * tls-server-end-point are 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)
+ if (strcmp(channel_binding_type, SCRAM_CHANNEL_BINDING_TLS_UNIQUE) != 0 &&
+ strcmp(channel_binding_type, SCRAM_CHANNEL_BINDING_TLS_ENDPOINT) != 0)
ereport(ERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION),
(errmsg("unsupported SCRAM channel-binding type"))));
@@ -1123,6 +1131,12 @@ read_client_final_message(scram_state *state, char *input)
cbind_data = state->tls_finished_message;
cbind_data_len = state->tls_finished_len;
}
+ else if (strcmp(state->channel_binding_type,
+ SCRAM_CHANNEL_BINDING_TLS_ENDPOINT) == 0)
+ {
+ cbind_data = state->certificate_hash;
+ cbind_data_len = state->certificate_hash_len;
+ }
else
{
/* should not happen */
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index b7f9bb1669..700a3bffa4 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -875,6 +875,8 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
bool initial;
char *tls_finished = NULL;
size_t tls_finished_len = 0;
+ char *certificate_hash = NULL;
+ size_t certificate_hash_len = 0;
/*
* SASL auth is not supported for protocol versions before 3, because it
@@ -923,6 +925,8 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
if (port->ssl_in_use)
{
tls_finished = be_tls_get_peer_finished(port, &tls_finished_len);
+ certificate_hash = be_tls_get_certificate_hash(port,
+ &certificate_hash_len);
}
#endif
@@ -941,7 +945,9 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
shadow_pass,
port->ssl_in_use,
tls_finished,
- tls_finished_len);
+ tls_finished_len,
+ certificate_hash,
+ certificate_hash_len);
/*
* Loop through SASL message exchange. This exchange can consist of
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 1e3e19f5e0..e3e8a535c8 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -1239,6 +1239,67 @@ be_tls_get_peer_finished(Port *port, size_t *len)
return result;
}
+/*
+ * Get the server certificate hash for authentication purposes. Per
+ * RFC 5929 and tls-server-end-point, the TLS server's certificate bytes
+ * need to be hashed with SHA-256 if its signature algorithm is MD5 or
+ * SHA-1 as per RFC 5929 (https://tools.ietf.org/html/rfc5929#section-4.1).
+ * If something else is used, the same hash as the signature algorithm is
+ * used. The result is a palloc'd hash of the server certificate with its
+ * size, and NULL if there is no certificate available.
+ */
+char *
+be_tls_get_certificate_hash(Port *port, size_t *len)
+{
+ char *cert_hash = NULL;
+ X509 *server_cert;
+
+ *len = 0;
+ server_cert = SSL_get_certificate(port->ssl);
+
+ if (server_cert != NULL)
+ {
+ const EVP_MD *algo_type = NULL;
+ char hash[EVP_MAX_MD_SIZE]; /* size for SHA-512 */
+ unsigned int hash_size;
+ int algo_nid;
+
+ /*
+ * Get the signature algorithm of the certificate to determine the
+ * hash algorithm to use for the result.
+ */
+ if (!OBJ_find_sigid_algs(X509_get_signature_nid(server_cert),
+ &algo_nid, NULL))
+ elog(ERROR, "could not find signature algorithm");
+
+ switch (algo_nid)
+ {
+ case NID_md5:
+ case NID_sha1:
+ algo_type = EVP_sha256();
+ break;
+
+ default:
+ algo_type = EVP_get_digestbynid(algo_nid);
+ if (algo_type == NULL)
+ elog(ERROR, "could not find digest for NID %s",
+ OBJ_nid2sn(algo_nid));
+ break;
+ }
+
+ /* generate and save the certificate hash */
+ if (!X509_digest(server_cert, algo_type, (unsigned char *) hash,
+ &hash_size))
+ elog(ERROR, "could not generate server certificate hash");
+
+ cert_hash = (char *) palloc(hash_size);
+ memcpy(cert_hash, hash, hash_size);
+ *len = hash_size;
+ }
+
+ return cert_hash;
+}
+
/*
* Convert an X509 subject name to a cstring.
*
diff --git a/src/include/common/scram-common.h b/src/include/common/scram-common.h
index 857a60e71f..5aec5cadb8 100644
--- a/src/include/common/scram-common.h
+++ b/src/include/common/scram-common.h
@@ -21,6 +21,7 @@
/* Channel binding types */
#define SCRAM_CHANNEL_BINDING_TLS_UNIQUE "tls-unique"
+#define SCRAM_CHANNEL_BINDING_TLS_ENDPOINT "tls-server-end-point"
/* Length of SCRAM keys (client and server) */
#define SCRAM_KEY_LEN PG_SHA256_DIGEST_LENGTH
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 856e0439d5..cf9d8b7870 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -210,6 +210,7 @@ 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_peerdn_name(Port *port, char *ptr, size_t len);
extern char *be_tls_get_peer_finished(Port *port, size_t *len);
+extern char *be_tls_get_certificate_hash(Port *port, size_t *len);
#endif
extern ProtocolVersion FrontendProtocol;
diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h
index 2c245813d6..7c8f009a3b 100644
--- a/src/include/libpq/scram.h
+++ b/src/include/libpq/scram.h
@@ -21,7 +21,8 @@
/* Routines dedicated to authentication */
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);
+ size_t tls_finished_len, const char *certificate_hash,
+ size_t certificate_hash_len);
extern int pg_be_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen, char **logdetail);
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index b8f7a6b5be..a56fccf12e 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -47,6 +47,8 @@ typedef struct
bool ssl_in_use;
char *tls_finished_message;
size_t tls_finished_len;
+ char *certificate_hash;
+ size_t certificate_hash_len;
char *sasl_mechanism;
const char *channel_binding_type;
@@ -95,7 +97,9 @@ pg_fe_scram_init(const char *username,
const char *sasl_mechanism,
const char *channel_binding_type,
char *tls_finished_message,
- size_t tls_finished_len)
+ size_t tls_finished_len,
+ char *certificate_hash,
+ size_t certificate_hash_len)
{
fe_scram_state *state;
char *prep_password;
@@ -112,6 +116,8 @@ pg_fe_scram_init(const char *username,
state->ssl_in_use = ssl_in_use;
state->tls_finished_message = tls_finished_message;
state->tls_finished_len = tls_finished_len;
+ state->certificate_hash = certificate_hash;
+ state->certificate_hash_len = certificate_hash_len;
state->sasl_mechanism = strdup(sasl_mechanism);
state->channel_binding_type = channel_binding_type;
@@ -156,8 +162,8 @@ pg_fe_scram_free(void *opaq)
free(state->password);
if (state->tls_finished_message)
free(state->tls_finished_message);
- if (state->sasl_mechanism)
- free(state->sasl_mechanism);
+ if (state->certificate_hash)
+ free(state->certificate_hash);
/* client messages */
if (state->client_nonce)
@@ -461,6 +467,12 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
cbind_data = state->tls_finished_message;
cbind_data_len = state->tls_finished_len;
}
+ else if (strcmp(state->channel_binding_type,
+ SCRAM_CHANNEL_BINDING_TLS_ENDPOINT) == 0)
+ {
+ cbind_data = state->certificate_hash;
+ cbind_data_len = state->certificate_hash_len;
+ }
else
{
/* should not happen */
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 3340a9ad93..bb9b0573d1 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -493,6 +493,8 @@ pg_SASL_init(PGconn *conn, int payloadlen)
PQExpBufferData mechanism_buf;
char *tls_finished = NULL;
size_t tls_finished_len = 0;
+ char *certificate_hash = NULL;
+ size_t certificate_hash_len = 0;
char *password;
initPQExpBuffer(&mechanism_buf);
@@ -580,6 +582,12 @@ pg_SASL_init(PGconn *conn, int payloadlen)
tls_finished = pgtls_get_finished(conn, &tls_finished_len);
if (tls_finished == NULL)
goto oom_error;
+
+ certificate_hash =
+ pgtls_get_peer_certificate_hash(conn,
+ &certificate_hash_len);
+ if (certificate_hash == NULL)
+ goto error; /* error message is set */
}
#endif
@@ -595,7 +603,9 @@ pg_SASL_init(PGconn *conn, int payloadlen)
selected_mechanism,
conn->scram_channel_binding,
tls_finished,
- tls_finished_len);
+ tls_finished_len,
+ certificate_hash,
+ certificate_hash_len);
if (!conn->sasl_state)
goto oom_error;
diff --git a/src/interfaces/libpq/fe-auth.h b/src/interfaces/libpq/fe-auth.h
index db319ac071..68de8b6e32 100644
--- a/src/interfaces/libpq/fe-auth.h
+++ b/src/interfaces/libpq/fe-auth.h
@@ -29,7 +29,9 @@ extern void *pg_fe_scram_init(const char *username,
const char *sasl_mechanism,
const char *channel_binding_type,
char *tls_finished_message,
- size_t tls_finished_len);
+ size_t tls_finished_len,
+ char *certificate_hash,
+ size_t certificate_hash_len);
extern void pg_fe_scram_free(void *opaq);
extern void pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen,
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 61d161b367..99077c3d9a 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -419,6 +419,84 @@ pgtls_get_finished(PGconn *conn, size_t *len)
return result;
}
+/*
+ * Get the hash of the server certificate
+ *
+ * This information is useful for end-point channel binding, where the
+ * client certificate hash is used as a link, per RFC 5929. If the
+ * signature hash algorithm is MD5 or SHA-1, fall back to SHA-256,
+ * as per RFC 5929 (https://tools.ietf.org/html/rfc5929#section-4.1).
+ * NULL is sent back to the caller in the event of an error, with an
+ * error message for the caller to consume.
+ */
+char *
+pgtls_get_peer_certificate_hash(PGconn *conn, size_t *len)
+{
+ char *cert_hash = NULL;
+
+ *len = 0;
+
+ if (conn->peer)
+ {
+ X509 *peer_cert = conn->peer;
+ const EVP_MD *algo_type = NULL;
+ char hash[EVP_MAX_MD_SIZE]; /* size for SHA-512 */
+ unsigned int hash_size;
+ int algo_nid;
+
+ /*
+ * Get the signature algorithm of the certificate to determine the
+ * hash algorithm to use for the result.
+ */
+ if (!OBJ_find_sigid_algs(X509_get_signature_nid(peer_cert),
+ &algo_nid, NULL))
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("could not find signature algorithm\n"));
+ return NULL;
+ }
+
+ switch (algo_nid)
+ {
+ case NID_md5:
+ case NID_sha1:
+ algo_type = EVP_sha256();
+ break;
+
+ default:
+ algo_type = EVP_get_digestbynid(algo_nid);
+ if (algo_type == NULL)
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("could not find digest for NID %s\n"),
+ OBJ_nid2sn(algo_nid));
+ return NULL;
+ }
+ break;
+ }
+
+ if (!X509_digest(peer_cert, algo_type, (unsigned char *) hash,
+ &hash_size))
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("could not generate peer certificate hash\n"));
+ return NULL;
+ }
+
+ /* save result */
+ cert_hash = (char *) malloc(hash_size);
+ if (cert_hash == NULL)
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("out of memory\n"));
+ return NULL;
+ }
+ memcpy(cert_hash, hash, hash_size);
+ *len = hash_size;
+ }
+
+ return cert_hash;
+}
/* ------------------------------------------------------------ */
/* OpenSSL specific code */
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index f6c1023f37..756c4d61e1 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -672,6 +672,7 @@ extern ssize_t pgtls_read(PGconn *conn, void *ptr, size_t len);
extern bool pgtls_read_pending(PGconn *conn);
extern ssize_t pgtls_write(PGconn *conn, const void *ptr, size_t len);
extern char *pgtls_get_finished(PGconn *conn, size_t *len);
+extern char *pgtls_get_peer_certificate_hash(PGconn *conn, size_t *len);
/*
* this is so that we can check if a connection is non-blocking internally
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 324b4888d4..3f425e00f0 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -4,7 +4,7 @@ use strict;
use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 4;
+use Test::More tests => 5;
use ServerSetup;
use File::Copy;
@@ -45,6 +45,9 @@ test_connect_ok($common_connstr,
test_connect_ok($common_connstr,
"scram_channel_binding=''",
"SCRAM authentication without channel binding");
+test_connect_ok($common_connstr,
+ "scram_channel_binding=tls-server-end-point",
+ "SCRAM authentication with tls-server-end-point as channel binding");
test_connect_fails($common_connstr,
"scram_channel_binding=not-exists",
"SCRAM authentication with invalid channel binding");
--
2.15.1
0002-Refactor-channel-binding-code-to-fetch-cbind_data-on.patchtext/plain; charset=us-asciiDownload
From d175df57f8df1f0ff497ae61b0c768f3bc9ca0a2 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 22 Dec 2017 11:46:16 +0900
Subject: [PATCH 2/2] Refactor channel binding code to fetch cbind_data only
when necessary
As things stand now, channel binding data is fetched from OpenSSL and saved
into the SASL exchange context for any SSL connection attempted for a SCRAM
authentication, resulting in data fetched but not used if no channel binding
is used or if a different channel binding type is used than what the data
is here for.
Refactor the code in such a way that binding data is only fetched from the
SSL stack only when a specific channel binding is used for both the frontend
and the backend. In order to achieve that, save the libpq connection context
directly in the SCRAM exchange state, and add a dependency to SSL in the
low-level SCRAM routines.
This makes the interface in charge of initializing the SCRAM context cleaner
as all its data comes from either PGconn* (for frontend) or Port* (for the
backend).
---
src/backend/libpq/auth-scram.c | 47 +++++++----------
src/backend/libpq/auth.c | 25 +--------
src/include/libpq/scram.h | 7 ++-
src/interfaces/libpq/fe-auth-scram.c | 91 ++++++++++++++++----------------
src/interfaces/libpq/fe-auth.c | 33 +-----------
src/interfaces/libpq/fe-auth.h | 10 +---
src/interfaces/libpq/fe-secure-openssl.c | 14 +++--
src/interfaces/libpq/libpq-int.h | 3 +-
8 files changed, 84 insertions(+), 146 deletions(-)
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index 849587d141..0a50f815ab 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -110,12 +110,8 @@ typedef struct
const char *username; /* username from startup packet */
+ Port *port;
char cbind_flag;
- bool ssl_in_use;
- const char *tls_finished_message;
- size_t tls_finished_len;
- const char *certificate_hash;
- size_t certificate_hash_len;
char *channel_binding_type;
int iterations;
@@ -174,25 +170,15 @@ 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,
- bool ssl_in_use,
- const char *tls_finished_message,
- size_t tls_finished_len,
- const char *certificate_hash,
- size_t certificate_hash_len)
+pg_be_scram_init(Port *port,
+ const char *shadow_pass)
{
scram_state *state;
bool got_verifier;
state = (scram_state *) palloc0(sizeof(scram_state));
+ state->port = port;
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->certificate_hash = certificate_hash;
- state->certificate_hash_len = certificate_hash_len;
state->channel_binding_type = NULL;
/*
@@ -215,7 +201,7 @@ pg_be_scram_init(const char *username,
*/
ereport(LOG,
(errmsg("invalid SCRAM verifier for user \"%s\"",
- username)));
+ state->port->user_name)));
got_verifier = false;
}
}
@@ -226,7 +212,7 @@ pg_be_scram_init(const char *username,
* authentication with an MD5 hash.)
*/
state->logdetail = psprintf(_("User \"%s\" does not have a valid SCRAM verifier."),
- state->username);
+ state->port->user_name);
got_verifier = false;
}
}
@@ -248,8 +234,8 @@ pg_be_scram_init(const char *username,
*/
if (!got_verifier)
{
- mock_scram_verifier(username, &state->iterations, &state->salt,
- state->StoredKey, state->ServerKey);
+ mock_scram_verifier(state->port->user_name, &state->iterations,
+ &state->salt, state->StoredKey, state->ServerKey);
state->doomed = true;
}
@@ -821,7 +807,7 @@ read_client_first_message(scram_state *state, char *input)
* it supports channel binding, which in this implementation is
* the case if a connection is using SSL.
*/
- if (state->ssl_in_use)
+ if (state->port->ssl_in_use)
ereport(ERROR,
(errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION),
errmsg("SCRAM channel binding negotiation error"),
@@ -845,7 +831,7 @@ read_client_first_message(scram_state *state, char *input)
{
char *channel_binding_type;
- if (!state->ssl_in_use)
+ if (!state->port->ssl_in_use)
{
/*
* Without SSL, we don't support channel binding.
@@ -1128,14 +1114,19 @@ read_client_final_message(scram_state *state, char *input)
*/
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;
+ /* Fetch data from TLS finished message */
+#ifdef USE_SSL
+ cbind_data = be_tls_get_peer_finished(state->port, &cbind_data_len);
+#endif
}
else if (strcmp(state->channel_binding_type,
SCRAM_CHANNEL_BINDING_TLS_ENDPOINT) == 0)
{
- cbind_data = state->certificate_hash;
- cbind_data_len = state->certificate_hash_len;
+ /* Fetch hash data of server's SSL certificate */
+#ifdef USE_SSL
+ cbind_data = be_tls_get_certificate_hash(state->port,
+ &cbind_data_len);
+#endif
}
else
{
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 700a3bffa4..bd91e1cd18 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -873,10 +873,6 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
int inputlen;
int result;
bool initial;
- char *tls_finished = NULL;
- size_t tls_finished_len = 0;
- char *certificate_hash = NULL;
- size_t certificate_hash_len = 0;
/*
* SASL auth is not supported for protocol versions before 3, because it
@@ -917,19 +913,6 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
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);
- certificate_hash = be_tls_get_certificate_hash(port,
- &certificate_hash_len);
- }
-#endif
-
/*
* Initialize the status tracker for message exchanges.
*
@@ -941,13 +924,7 @@ 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,
- port->ssl_in_use,
- tls_finished,
- tls_finished_len,
- certificate_hash,
- certificate_hash_len);
+ scram_opaq = pg_be_scram_init(port, shadow_pass);
/*
* Loop through SASL message exchange. This exchange can consist of
diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h
index 7c8f009a3b..f404f57253 100644
--- a/src/include/libpq/scram.h
+++ b/src/include/libpq/scram.h
@@ -13,16 +13,15 @@
#ifndef PG_SCRAM_H
#define PG_SCRAM_H
+#include "libpq/libpq-be.h"
+
/* Status codes for message exchange */
#define SASL_EXCHANGE_CONTINUE 0
#define SASL_EXCHANGE_SUCCESS 1
#define SASL_EXCHANGE_FAILURE 2
/* Routines dedicated to authentication */
-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, const char *certificate_hash,
- size_t certificate_hash_len);
+extern void *pg_be_scram_init(Port *port, const char *shadow_pass);
extern int pg_be_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen, char **logdetail);
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index a56fccf12e..a44338f0f9 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -42,15 +42,9 @@ typedef struct
fe_scram_state_enum state;
/* These are supplied by the user */
- const char *username;
+ PGconn *conn;
char *password;
- bool ssl_in_use;
- char *tls_finished_message;
- size_t tls_finished_len;
- char *certificate_hash;
- size_t certificate_hash_len;
char *sasl_mechanism;
- const char *channel_binding_type;
/* We construct these */
uint8 SaltedPassword[SCRAM_KEY_LEN];
@@ -91,15 +85,9 @@ static bool pg_frontend_random(char *dst, int len);
* freed by pg_fe_scram_free().
*/
void *
-pg_fe_scram_init(const char *username,
+pg_fe_scram_init(PGconn *conn,
const char *password,
- bool ssl_in_use,
- const char *sasl_mechanism,
- const char *channel_binding_type,
- char *tls_finished_message,
- size_t tls_finished_len,
- char *certificate_hash,
- size_t certificate_hash_len)
+ const char *sasl_mechanism)
{
fe_scram_state *state;
char *prep_password;
@@ -111,15 +99,9 @@ pg_fe_scram_init(const char *username,
if (!state)
return NULL;
memset(state, 0, sizeof(fe_scram_state));
+ state->conn = conn;
state->state = FE_SCRAM_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->certificate_hash = certificate_hash;
- state->certificate_hash_len = certificate_hash_len;
state->sasl_mechanism = strdup(sasl_mechanism);
- state->channel_binding_type = channel_binding_type;
if (!state->sasl_mechanism)
{
@@ -160,10 +142,6 @@ pg_fe_scram_free(void *opaq)
if (state->password)
free(state->password);
- if (state->tls_finished_message)
- free(state->tls_finished_message);
- if (state->certificate_hash)
- free(state->certificate_hash);
/* client messages */
if (state->client_nonce)
@@ -376,11 +354,11 @@ build_client_first_message(fe_scram_state *state, PQExpBuffer errormessage)
*/
if (strcmp(state->sasl_mechanism, SCRAM_SHA256_PLUS_NAME) == 0)
{
- Assert(state->ssl_in_use);
- appendPQExpBuffer(&buf, "p=%s", state->channel_binding_type);
+ Assert(state->conn->ssl_in_use);
+ appendPQExpBuffer(&buf, "p=%s", state->conn->scram_channel_binding);
}
- else if (state->channel_binding_type == NULL ||
- strlen(state->channel_binding_type) == 0)
+ else if (state->conn->scram_channel_binding == NULL ||
+ strlen(state->conn->scram_channel_binding) == 0)
{
/*
* Client has chosen to not show to server that it supports channel
@@ -388,7 +366,7 @@ build_client_first_message(fe_scram_state *state, PQExpBuffer errormessage)
*/
appendPQExpBuffer(&buf, "n");
}
- else if (state->ssl_in_use)
+ else if (state->conn->ssl_in_use)
{
/*
* Client supports channel binding, but thinks the server does not.
@@ -456,22 +434,36 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
*/
if (strcmp(state->sasl_mechanism, SCRAM_SHA256_PLUS_NAME) == 0)
{
- char *cbind_data;
- size_t cbind_data_len;
+ char *cbind_data = NULL;
+ size_t cbind_data_len = 0;
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)
+ if (strcmp(state->conn->scram_channel_binding, SCRAM_CHANNEL_BINDING_TLS_UNIQUE) == 0)
{
- cbind_data = state->tls_finished_message;
- cbind_data_len = state->tls_finished_len;
+ /* Fetch data from TLS finished message */
+#ifdef USE_SSL
+ cbind_data = pgtls_get_finished(state->conn, &cbind_data_len);
+ if (cbind_data == NULL)
+ goto oom_error;
+#endif
}
- else if (strcmp(state->channel_binding_type,
+ else if (strcmp(state->conn->scram_channel_binding,
SCRAM_CHANNEL_BINDING_TLS_ENDPOINT) == 0)
{
- cbind_data = state->certificate_hash;
- cbind_data_len = state->certificate_hash_len;
+ /* Fetch hash data of server's SSL certificate */
+#ifdef USE_SSL
+ cbind_data =
+ pgtls_get_peer_certificate_hash(state->conn,
+ &cbind_data_len,
+ errormessage);
+ if (cbind_data == NULL)
+ {
+ /* error message is already set on error */
+ return NULL;
+ }
+#endif
}
else
{
@@ -485,37 +477,46 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
/* should not happen */
if (cbind_data == NULL || cbind_data_len == 0)
{
+ if (cbind_data != NULL)
+ free(cbind_data);
termPQExpBuffer(&buf);
printfPQExpBuffer(errormessage,
libpq_gettext("empty channel binding data for channel binding type \"%s\"\n"),
- state->channel_binding_type);
+ state->conn->scram_channel_binding);
return NULL;
}
appendPQExpBuffer(&buf, "c=");
- cbind_header_len = 4 + strlen(state->channel_binding_type); /* p=type,, */
+ /* p=type,, */
+ cbind_header_len = 4 + strlen(state->conn->scram_channel_binding);
cbind_input_len = cbind_header_len + cbind_data_len;
cbind_input = malloc(cbind_input_len);
if (!cbind_input)
+ {
+ free(cbind_data);
goto oom_error;
- snprintf(cbind_input, cbind_input_len, "p=%s,,", state->channel_binding_type);
+ }
+ snprintf(cbind_input, cbind_input_len, "p=%s,,",
+ state->conn->scram_channel_binding);
memcpy(cbind_input + cbind_header_len, cbind_data, cbind_data_len);
if (!enlargePQExpBuffer(&buf, pg_b64_enc_len(cbind_input_len)))
{
+ free(cbind_data);
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_data);
free(cbind_input);
}
- else if (state->channel_binding_type == NULL ||
- strlen(state->channel_binding_type) == 0)
+ else if (state->conn->scram_channel_binding == NULL ||
+ strlen(state->conn->scram_channel_binding) == 0)
appendPQExpBuffer(&buf, "c=biws"); /* base64 of "n,," */
- else if (state->ssl_in_use)
+ else if (state->conn->ssl_in_use)
appendPQExpBuffer(&buf, "c=eSws"); /* base64 of "y,," */
else
appendPQExpBuffer(&buf, "c=biws"); /* base64 of "n,," */
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index bb9b0573d1..b1ea0e7cef 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -491,10 +491,6 @@ pg_SASL_init(PGconn *conn, int payloadlen)
bool success;
const char *selected_mechanism;
PQExpBufferData mechanism_buf;
- char *tls_finished = NULL;
- size_t tls_finished_len = 0;
- char *certificate_hash = NULL;
- size_t certificate_hash_len = 0;
char *password;
initPQExpBuffer(&mechanism_buf);
@@ -572,40 +568,15 @@ pg_SASL_init(PGconn *conn, int payloadlen)
goto error;
}
-#ifdef USE_SSL
-
- /*
- * 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;
-
- certificate_hash =
- pgtls_get_peer_certificate_hash(conn,
- &certificate_hash_len);
- if (certificate_hash == NULL)
- goto error; /* error message is set */
- }
-#endif
-
/*
* Initialize the SASL state information with all the information gathered
* during the initial exchange.
*
* Note: Only tls-unique is supported for the moment.
*/
- conn->sasl_state = pg_fe_scram_init(conn->pguser,
+ conn->sasl_state = pg_fe_scram_init(conn,
password,
- conn->ssl_in_use,
- selected_mechanism,
- conn->scram_channel_binding,
- tls_finished,
- tls_finished_len,
- certificate_hash,
- certificate_hash_len);
+ selected_mechanism);
if (!conn->sasl_state)
goto oom_error;
diff --git a/src/interfaces/libpq/fe-auth.h b/src/interfaces/libpq/fe-auth.h
index 68de8b6e32..4658a12837 100644
--- a/src/interfaces/libpq/fe-auth.h
+++ b/src/interfaces/libpq/fe-auth.h
@@ -23,15 +23,9 @@ extern int pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn);
extern char *pg_fe_getauthname(PQExpBuffer errorMessage);
/* Prototypes for functions in fe-auth-scram.c */
-extern void *pg_fe_scram_init(const char *username,
+extern void *pg_fe_scram_init(PGconn *conn,
const char *password,
- bool ssl_in_use,
- const char *sasl_mechanism,
- const char *channel_binding_type,
- char *tls_finished_message,
- size_t tls_finished_len,
- char *certificate_hash,
- size_t certificate_hash_len);
+ const char *sasl_mechanism);
extern void pg_fe_scram_free(void *opaq);
extern void pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen,
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 99077c3d9a..1acaffc488 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -428,9 +428,13 @@ pgtls_get_finished(PGconn *conn, size_t *len)
* as per RFC 5929 (https://tools.ietf.org/html/rfc5929#section-4.1).
* NULL is sent back to the caller in the event of an error, with an
* error message for the caller to consume.
+ * If an error happens while processing, fill in errorMessage but do
+ * not append it to the connection's error message buffer as this
+ * gets passed down later on.
*/
char *
-pgtls_get_peer_certificate_hash(PGconn *conn, size_t *len)
+pgtls_get_peer_certificate_hash(PGconn *conn, size_t *len,
+ PQExpBuffer errorMessage)
{
char *cert_hash = NULL;
@@ -451,7 +455,7 @@ pgtls_get_peer_certificate_hash(PGconn *conn, size_t *len)
if (!OBJ_find_sigid_algs(X509_get_signature_nid(peer_cert),
&algo_nid, NULL))
{
- printfPQExpBuffer(&conn->errorMessage,
+ printfPQExpBuffer(errorMessage,
libpq_gettext("could not find signature algorithm\n"));
return NULL;
}
@@ -467,7 +471,7 @@ pgtls_get_peer_certificate_hash(PGconn *conn, size_t *len)
algo_type = EVP_get_digestbynid(algo_nid);
if (algo_type == NULL)
{
- printfPQExpBuffer(&conn->errorMessage,
+ printfPQExpBuffer(errorMessage,
libpq_gettext("could not find digest for NID %s\n"),
OBJ_nid2sn(algo_nid));
return NULL;
@@ -478,7 +482,7 @@ pgtls_get_peer_certificate_hash(PGconn *conn, size_t *len)
if (!X509_digest(peer_cert, algo_type, (unsigned char *) hash,
&hash_size))
{
- printfPQExpBuffer(&conn->errorMessage,
+ printfPQExpBuffer(errorMessage,
libpq_gettext("could not generate peer certificate hash\n"));
return NULL;
}
@@ -487,7 +491,7 @@ pgtls_get_peer_certificate_hash(PGconn *conn, size_t *len)
cert_hash = (char *) malloc(hash_size);
if (cert_hash == NULL)
{
- printfPQExpBuffer(&conn->errorMessage,
+ printfPQExpBuffer(errorMessage,
libpq_gettext("out of memory\n"));
return NULL;
}
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 756c4d61e1..a946aa4048 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -672,7 +672,8 @@ extern ssize_t pgtls_read(PGconn *conn, void *ptr, size_t len);
extern bool pgtls_read_pending(PGconn *conn);
extern ssize_t pgtls_write(PGconn *conn, const void *ptr, size_t len);
extern char *pgtls_get_finished(PGconn *conn, size_t *len);
-extern char *pgtls_get_peer_certificate_hash(PGconn *conn, size_t *len);
+extern char *pgtls_get_peer_certificate_hash(PGconn *conn, size_t *len,
+ PQExpBuffer errorMessage);
/*
* this is so that we can check if a connection is non-blocking internally
--
2.15.1
On Fri, Dec 22, 2017 at 11:59 AM, Michael Paquier
<michael.paquier@gmail.com> wrote:
I have looked at how things could be done in symmetry for both the frontend
and backend code, and I have produced the attached patch 0002, which
can be applied on top of 0001 implementing tls-server-end-point. This
simplifies the interfaces to initialize the SCRAM status data by saving
into scram_state and fe_scram_state respectively Port* and PGconn* which
holds most of the data needed for the exchange. With this patch, cbind_data
is generated only if a specific channel binding type is used with the
appropriate data. So if no channel binding is used there is no additional
SSL call done to get the TLS finished data or the server certificate hash.0001 has no real changes compared to the last versions.
Second thoughts on 0002 as there is actually no need to move around
errorMessage if the PGconn* pointer is saved in the SCRAM status data
as both are linked. The attached simplifies the logic even more.
--
Michael
Attachments:
0001-Implement-channel-binding-tls-server-end-point-for-S.patchtext/x-patch; charset=US-ASCII; name=0001-Implement-channel-binding-tls-server-end-point-for-S.patchDownload
From 69dcf31f5ce5938f9f56a94bf55c8439ea53ed27 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 22 Dec 2017 10:49:10 +0900
Subject: [PATCH 1/2] Implement channel binding tls-server-end-point for SCRAM
As referenced in RFC 5929, this channel binding is not the default value
and uses a hash of the certificate as binding data. On the frontend, this
can be resumed in getting the data from SSL_get_peer_certificate() and
on the backend SSL_get_certificate().
The hashing algorithm needs also to switch to SHA-256 if the signature
algorithm is MD5 or SHA-1, so let's be careful about that.
---
doc/src/sgml/protocol.sgml | 5 +-
src/backend/libpq/auth-scram.c | 26 ++++++++---
src/backend/libpq/auth.c | 8 +++-
src/backend/libpq/be-secure-openssl.c | 61 +++++++++++++++++++++++++
src/include/common/scram-common.h | 1 +
src/include/libpq/libpq-be.h | 1 +
src/include/libpq/scram.h | 3 +-
src/interfaces/libpq/fe-auth-scram.c | 18 ++++++--
src/interfaces/libpq/fe-auth.c | 12 ++++-
src/interfaces/libpq/fe-auth.h | 4 +-
src/interfaces/libpq/fe-secure-openssl.c | 78 ++++++++++++++++++++++++++++++++
src/interfaces/libpq/libpq-int.h | 1 +
src/test/ssl/t/002_scram.pl | 5 +-
13 files changed, 207 insertions(+), 16 deletions(-)
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 8174e3defa..365f72b51d 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1576,8 +1576,9 @@ the password is in.
<para>
<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.
+is <literal>SCRAM-SHA-256-PLUS</literal>. Two channel binding types are
+supported at the moment: <literal>tls-unique</literal>, which is the default,
+and <literal>tls-server-end-point</literal>, both defined in RFC 5929.
</para>
<procedure>
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index d52a763457..849587d141 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -114,6 +114,8 @@ typedef struct
bool ssl_in_use;
const char *tls_finished_message;
size_t tls_finished_len;
+ const char *certificate_hash;
+ size_t certificate_hash_len;
char *channel_binding_type;
int iterations;
@@ -176,7 +178,9 @@ 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)
+ size_t tls_finished_len,
+ const char *certificate_hash,
+ size_t certificate_hash_len)
{
scram_state *state;
bool got_verifier;
@@ -187,6 +191,8 @@ pg_be_scram_init(const char *username,
state->ssl_in_use = ssl_in_use;
state->tls_finished_message = tls_finished_message;
state->tls_finished_len = tls_finished_len;
+ state->certificate_hash = certificate_hash;
+ state->certificate_hash_len = certificate_hash_len;
state->channel_binding_type = NULL;
/*
@@ -857,13 +863,15 @@ read_client_first_message(scram_state *state, char *input)
}
/*
- * 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.)
+ * Read value provided by client; only tls-unique and
+ * tls-server-end-point are 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)
+ if (strcmp(channel_binding_type, SCRAM_CHANNEL_BINDING_TLS_UNIQUE) != 0 &&
+ strcmp(channel_binding_type, SCRAM_CHANNEL_BINDING_TLS_ENDPOINT) != 0)
ereport(ERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION),
(errmsg("unsupported SCRAM channel-binding type"))));
@@ -1123,6 +1131,12 @@ read_client_final_message(scram_state *state, char *input)
cbind_data = state->tls_finished_message;
cbind_data_len = state->tls_finished_len;
}
+ else if (strcmp(state->channel_binding_type,
+ SCRAM_CHANNEL_BINDING_TLS_ENDPOINT) == 0)
+ {
+ cbind_data = state->certificate_hash;
+ cbind_data_len = state->certificate_hash_len;
+ }
else
{
/* should not happen */
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index b7f9bb1669..700a3bffa4 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -875,6 +875,8 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
bool initial;
char *tls_finished = NULL;
size_t tls_finished_len = 0;
+ char *certificate_hash = NULL;
+ size_t certificate_hash_len = 0;
/*
* SASL auth is not supported for protocol versions before 3, because it
@@ -923,6 +925,8 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
if (port->ssl_in_use)
{
tls_finished = be_tls_get_peer_finished(port, &tls_finished_len);
+ certificate_hash = be_tls_get_certificate_hash(port,
+ &certificate_hash_len);
}
#endif
@@ -941,7 +945,9 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
shadow_pass,
port->ssl_in_use,
tls_finished,
- tls_finished_len);
+ tls_finished_len,
+ certificate_hash,
+ certificate_hash_len);
/*
* Loop through SASL message exchange. This exchange can consist of
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 1e3e19f5e0..e3e8a535c8 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -1239,6 +1239,67 @@ be_tls_get_peer_finished(Port *port, size_t *len)
return result;
}
+/*
+ * Get the server certificate hash for authentication purposes. Per
+ * RFC 5929 and tls-server-end-point, the TLS server's certificate bytes
+ * need to be hashed with SHA-256 if its signature algorithm is MD5 or
+ * SHA-1 as per RFC 5929 (https://tools.ietf.org/html/rfc5929#section-4.1).
+ * If something else is used, the same hash as the signature algorithm is
+ * used. The result is a palloc'd hash of the server certificate with its
+ * size, and NULL if there is no certificate available.
+ */
+char *
+be_tls_get_certificate_hash(Port *port, size_t *len)
+{
+ char *cert_hash = NULL;
+ X509 *server_cert;
+
+ *len = 0;
+ server_cert = SSL_get_certificate(port->ssl);
+
+ if (server_cert != NULL)
+ {
+ const EVP_MD *algo_type = NULL;
+ char hash[EVP_MAX_MD_SIZE]; /* size for SHA-512 */
+ unsigned int hash_size;
+ int algo_nid;
+
+ /*
+ * Get the signature algorithm of the certificate to determine the
+ * hash algorithm to use for the result.
+ */
+ if (!OBJ_find_sigid_algs(X509_get_signature_nid(server_cert),
+ &algo_nid, NULL))
+ elog(ERROR, "could not find signature algorithm");
+
+ switch (algo_nid)
+ {
+ case NID_md5:
+ case NID_sha1:
+ algo_type = EVP_sha256();
+ break;
+
+ default:
+ algo_type = EVP_get_digestbynid(algo_nid);
+ if (algo_type == NULL)
+ elog(ERROR, "could not find digest for NID %s",
+ OBJ_nid2sn(algo_nid));
+ break;
+ }
+
+ /* generate and save the certificate hash */
+ if (!X509_digest(server_cert, algo_type, (unsigned char *) hash,
+ &hash_size))
+ elog(ERROR, "could not generate server certificate hash");
+
+ cert_hash = (char *) palloc(hash_size);
+ memcpy(cert_hash, hash, hash_size);
+ *len = hash_size;
+ }
+
+ return cert_hash;
+}
+
/*
* Convert an X509 subject name to a cstring.
*
diff --git a/src/include/common/scram-common.h b/src/include/common/scram-common.h
index 857a60e71f..5aec5cadb8 100644
--- a/src/include/common/scram-common.h
+++ b/src/include/common/scram-common.h
@@ -21,6 +21,7 @@
/* Channel binding types */
#define SCRAM_CHANNEL_BINDING_TLS_UNIQUE "tls-unique"
+#define SCRAM_CHANNEL_BINDING_TLS_ENDPOINT "tls-server-end-point"
/* Length of SCRAM keys (client and server) */
#define SCRAM_KEY_LEN PG_SHA256_DIGEST_LENGTH
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 856e0439d5..cf9d8b7870 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -210,6 +210,7 @@ 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_peerdn_name(Port *port, char *ptr, size_t len);
extern char *be_tls_get_peer_finished(Port *port, size_t *len);
+extern char *be_tls_get_certificate_hash(Port *port, size_t *len);
#endif
extern ProtocolVersion FrontendProtocol;
diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h
index 2c245813d6..7c8f009a3b 100644
--- a/src/include/libpq/scram.h
+++ b/src/include/libpq/scram.h
@@ -21,7 +21,8 @@
/* Routines dedicated to authentication */
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);
+ size_t tls_finished_len, const char *certificate_hash,
+ size_t certificate_hash_len);
extern int pg_be_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen, char **logdetail);
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index b8f7a6b5be..a56fccf12e 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -47,6 +47,8 @@ typedef struct
bool ssl_in_use;
char *tls_finished_message;
size_t tls_finished_len;
+ char *certificate_hash;
+ size_t certificate_hash_len;
char *sasl_mechanism;
const char *channel_binding_type;
@@ -95,7 +97,9 @@ pg_fe_scram_init(const char *username,
const char *sasl_mechanism,
const char *channel_binding_type,
char *tls_finished_message,
- size_t tls_finished_len)
+ size_t tls_finished_len,
+ char *certificate_hash,
+ size_t certificate_hash_len)
{
fe_scram_state *state;
char *prep_password;
@@ -112,6 +116,8 @@ pg_fe_scram_init(const char *username,
state->ssl_in_use = ssl_in_use;
state->tls_finished_message = tls_finished_message;
state->tls_finished_len = tls_finished_len;
+ state->certificate_hash = certificate_hash;
+ state->certificate_hash_len = certificate_hash_len;
state->sasl_mechanism = strdup(sasl_mechanism);
state->channel_binding_type = channel_binding_type;
@@ -156,8 +162,8 @@ pg_fe_scram_free(void *opaq)
free(state->password);
if (state->tls_finished_message)
free(state->tls_finished_message);
- if (state->sasl_mechanism)
- free(state->sasl_mechanism);
+ if (state->certificate_hash)
+ free(state->certificate_hash);
/* client messages */
if (state->client_nonce)
@@ -461,6 +467,12 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
cbind_data = state->tls_finished_message;
cbind_data_len = state->tls_finished_len;
}
+ else if (strcmp(state->channel_binding_type,
+ SCRAM_CHANNEL_BINDING_TLS_ENDPOINT) == 0)
+ {
+ cbind_data = state->certificate_hash;
+ cbind_data_len = state->certificate_hash_len;
+ }
else
{
/* should not happen */
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 3340a9ad93..bb9b0573d1 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -493,6 +493,8 @@ pg_SASL_init(PGconn *conn, int payloadlen)
PQExpBufferData mechanism_buf;
char *tls_finished = NULL;
size_t tls_finished_len = 0;
+ char *certificate_hash = NULL;
+ size_t certificate_hash_len = 0;
char *password;
initPQExpBuffer(&mechanism_buf);
@@ -580,6 +582,12 @@ pg_SASL_init(PGconn *conn, int payloadlen)
tls_finished = pgtls_get_finished(conn, &tls_finished_len);
if (tls_finished == NULL)
goto oom_error;
+
+ certificate_hash =
+ pgtls_get_peer_certificate_hash(conn,
+ &certificate_hash_len);
+ if (certificate_hash == NULL)
+ goto error; /* error message is set */
}
#endif
@@ -595,7 +603,9 @@ pg_SASL_init(PGconn *conn, int payloadlen)
selected_mechanism,
conn->scram_channel_binding,
tls_finished,
- tls_finished_len);
+ tls_finished_len,
+ certificate_hash,
+ certificate_hash_len);
if (!conn->sasl_state)
goto oom_error;
diff --git a/src/interfaces/libpq/fe-auth.h b/src/interfaces/libpq/fe-auth.h
index db319ac071..68de8b6e32 100644
--- a/src/interfaces/libpq/fe-auth.h
+++ b/src/interfaces/libpq/fe-auth.h
@@ -29,7 +29,9 @@ extern void *pg_fe_scram_init(const char *username,
const char *sasl_mechanism,
const char *channel_binding_type,
char *tls_finished_message,
- size_t tls_finished_len);
+ size_t tls_finished_len,
+ char *certificate_hash,
+ size_t certificate_hash_len);
extern void pg_fe_scram_free(void *opaq);
extern void pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen,
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 61d161b367..99077c3d9a 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -419,6 +419,84 @@ pgtls_get_finished(PGconn *conn, size_t *len)
return result;
}
+/*
+ * Get the hash of the server certificate
+ *
+ * This information is useful for end-point channel binding, where the
+ * client certificate hash is used as a link, per RFC 5929. If the
+ * signature hash algorithm is MD5 or SHA-1, fall back to SHA-256,
+ * as per RFC 5929 (https://tools.ietf.org/html/rfc5929#section-4.1).
+ * NULL is sent back to the caller in the event of an error, with an
+ * error message for the caller to consume.
+ */
+char *
+pgtls_get_peer_certificate_hash(PGconn *conn, size_t *len)
+{
+ char *cert_hash = NULL;
+
+ *len = 0;
+
+ if (conn->peer)
+ {
+ X509 *peer_cert = conn->peer;
+ const EVP_MD *algo_type = NULL;
+ char hash[EVP_MAX_MD_SIZE]; /* size for SHA-512 */
+ unsigned int hash_size;
+ int algo_nid;
+
+ /*
+ * Get the signature algorithm of the certificate to determine the
+ * hash algorithm to use for the result.
+ */
+ if (!OBJ_find_sigid_algs(X509_get_signature_nid(peer_cert),
+ &algo_nid, NULL))
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("could not find signature algorithm\n"));
+ return NULL;
+ }
+
+ switch (algo_nid)
+ {
+ case NID_md5:
+ case NID_sha1:
+ algo_type = EVP_sha256();
+ break;
+
+ default:
+ algo_type = EVP_get_digestbynid(algo_nid);
+ if (algo_type == NULL)
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("could not find digest for NID %s\n"),
+ OBJ_nid2sn(algo_nid));
+ return NULL;
+ }
+ break;
+ }
+
+ if (!X509_digest(peer_cert, algo_type, (unsigned char *) hash,
+ &hash_size))
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("could not generate peer certificate hash\n"));
+ return NULL;
+ }
+
+ /* save result */
+ cert_hash = (char *) malloc(hash_size);
+ if (cert_hash == NULL)
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("out of memory\n"));
+ return NULL;
+ }
+ memcpy(cert_hash, hash, hash_size);
+ *len = hash_size;
+ }
+
+ return cert_hash;
+}
/* ------------------------------------------------------------ */
/* OpenSSL specific code */
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index f6c1023f37..756c4d61e1 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -672,6 +672,7 @@ extern ssize_t pgtls_read(PGconn *conn, void *ptr, size_t len);
extern bool pgtls_read_pending(PGconn *conn);
extern ssize_t pgtls_write(PGconn *conn, const void *ptr, size_t len);
extern char *pgtls_get_finished(PGconn *conn, size_t *len);
+extern char *pgtls_get_peer_certificate_hash(PGconn *conn, size_t *len);
/*
* this is so that we can check if a connection is non-blocking internally
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 324b4888d4..3f425e00f0 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -4,7 +4,7 @@ use strict;
use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 4;
+use Test::More tests => 5;
use ServerSetup;
use File::Copy;
@@ -45,6 +45,9 @@ test_connect_ok($common_connstr,
test_connect_ok($common_connstr,
"scram_channel_binding=''",
"SCRAM authentication without channel binding");
+test_connect_ok($common_connstr,
+ "scram_channel_binding=tls-server-end-point",
+ "SCRAM authentication with tls-server-end-point as channel binding");
test_connect_fails($common_connstr,
"scram_channel_binding=not-exists",
"SCRAM authentication with invalid channel binding");
--
2.15.1
0002-Refactor-channel-binding-code-to-fetch-cbind_data-on.patchtext/x-patch; charset=US-ASCII; name=0002-Refactor-channel-binding-code-to-fetch-cbind_data-on.patchDownload
From 006093fc8bf27e1bfe142dbcd79dadea4257909f Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 22 Dec 2017 17:04:19 +0900
Subject: [PATCH 2/2] Refactor channel binding code to fetch cbind_data only
when necessary
As things stand now, channel binding data is fetched from OpenSSL and saved
into the SASL exchange context for any SSL connection attempted for a SCRAM
authentication, resulting in data fetched but not used if no channel binding
is used or if a different channel binding type is used than what the data
is here for.
Refactor the code in such a way that binding data is only fetched from the
SSL stack only when a specific channel binding is used for both the frontend
and the backend. In order to achieve that, save the libpq connection context
directly in the SCRAM exchange state, and add a dependency to SSL in the
low-level SCRAM routines.
This makes the interface in charge of initializing the SCRAM context cleaner
as all its data comes from either PGconn* (for frontend) or Port* (for the
backend).
---
src/backend/libpq/auth-scram.c | 47 ++++-----
src/backend/libpq/auth.c | 25 +----
src/include/libpq/scram.h | 7 +-
src/interfaces/libpq/fe-auth-scram.c | 180 ++++++++++++++++++-----------------
src/interfaces/libpq/fe-auth.c | 37 +------
src/interfaces/libpq/fe-auth.h | 12 +--
6 files changed, 121 insertions(+), 187 deletions(-)
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index 849587d141..0a50f815ab 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -110,12 +110,8 @@ typedef struct
const char *username; /* username from startup packet */
+ Port *port;
char cbind_flag;
- bool ssl_in_use;
- const char *tls_finished_message;
- size_t tls_finished_len;
- const char *certificate_hash;
- size_t certificate_hash_len;
char *channel_binding_type;
int iterations;
@@ -174,25 +170,15 @@ 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,
- bool ssl_in_use,
- const char *tls_finished_message,
- size_t tls_finished_len,
- const char *certificate_hash,
- size_t certificate_hash_len)
+pg_be_scram_init(Port *port,
+ const char *shadow_pass)
{
scram_state *state;
bool got_verifier;
state = (scram_state *) palloc0(sizeof(scram_state));
+ state->port = port;
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->certificate_hash = certificate_hash;
- state->certificate_hash_len = certificate_hash_len;
state->channel_binding_type = NULL;
/*
@@ -215,7 +201,7 @@ pg_be_scram_init(const char *username,
*/
ereport(LOG,
(errmsg("invalid SCRAM verifier for user \"%s\"",
- username)));
+ state->port->user_name)));
got_verifier = false;
}
}
@@ -226,7 +212,7 @@ pg_be_scram_init(const char *username,
* authentication with an MD5 hash.)
*/
state->logdetail = psprintf(_("User \"%s\" does not have a valid SCRAM verifier."),
- state->username);
+ state->port->user_name);
got_verifier = false;
}
}
@@ -248,8 +234,8 @@ pg_be_scram_init(const char *username,
*/
if (!got_verifier)
{
- mock_scram_verifier(username, &state->iterations, &state->salt,
- state->StoredKey, state->ServerKey);
+ mock_scram_verifier(state->port->user_name, &state->iterations,
+ &state->salt, state->StoredKey, state->ServerKey);
state->doomed = true;
}
@@ -821,7 +807,7 @@ read_client_first_message(scram_state *state, char *input)
* it supports channel binding, which in this implementation is
* the case if a connection is using SSL.
*/
- if (state->ssl_in_use)
+ if (state->port->ssl_in_use)
ereport(ERROR,
(errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION),
errmsg("SCRAM channel binding negotiation error"),
@@ -845,7 +831,7 @@ read_client_first_message(scram_state *state, char *input)
{
char *channel_binding_type;
- if (!state->ssl_in_use)
+ if (!state->port->ssl_in_use)
{
/*
* Without SSL, we don't support channel binding.
@@ -1128,14 +1114,19 @@ read_client_final_message(scram_state *state, char *input)
*/
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;
+ /* Fetch data from TLS finished message */
+#ifdef USE_SSL
+ cbind_data = be_tls_get_peer_finished(state->port, &cbind_data_len);
+#endif
}
else if (strcmp(state->channel_binding_type,
SCRAM_CHANNEL_BINDING_TLS_ENDPOINT) == 0)
{
- cbind_data = state->certificate_hash;
- cbind_data_len = state->certificate_hash_len;
+ /* Fetch hash data of server's SSL certificate */
+#ifdef USE_SSL
+ cbind_data = be_tls_get_certificate_hash(state->port,
+ &cbind_data_len);
+#endif
}
else
{
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 700a3bffa4..bd91e1cd18 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -873,10 +873,6 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
int inputlen;
int result;
bool initial;
- char *tls_finished = NULL;
- size_t tls_finished_len = 0;
- char *certificate_hash = NULL;
- size_t certificate_hash_len = 0;
/*
* SASL auth is not supported for protocol versions before 3, because it
@@ -917,19 +913,6 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
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);
- certificate_hash = be_tls_get_certificate_hash(port,
- &certificate_hash_len);
- }
-#endif
-
/*
* Initialize the status tracker for message exchanges.
*
@@ -941,13 +924,7 @@ 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,
- port->ssl_in_use,
- tls_finished,
- tls_finished_len,
- certificate_hash,
- certificate_hash_len);
+ scram_opaq = pg_be_scram_init(port, shadow_pass);
/*
* Loop through SASL message exchange. This exchange can consist of
diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h
index 7c8f009a3b..f404f57253 100644
--- a/src/include/libpq/scram.h
+++ b/src/include/libpq/scram.h
@@ -13,16 +13,15 @@
#ifndef PG_SCRAM_H
#define PG_SCRAM_H
+#include "libpq/libpq-be.h"
+
/* Status codes for message exchange */
#define SASL_EXCHANGE_CONTINUE 0
#define SASL_EXCHANGE_SUCCESS 1
#define SASL_EXCHANGE_FAILURE 2
/* Routines dedicated to authentication */
-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, const char *certificate_hash,
- size_t certificate_hash_len);
+extern void *pg_be_scram_init(Port *port, const char *shadow_pass);
extern int pg_be_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen, char **logdetail);
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index a56fccf12e..65817411b1 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -42,15 +42,9 @@ typedef struct
fe_scram_state_enum state;
/* These are supplied by the user */
- const char *username;
+ PGconn *conn;
char *password;
- bool ssl_in_use;
- char *tls_finished_message;
- size_t tls_finished_len;
- char *certificate_hash;
- size_t certificate_hash_len;
char *sasl_mechanism;
- const char *channel_binding_type;
/* We construct these */
uint8 SaltedPassword[SCRAM_KEY_LEN];
@@ -70,14 +64,10 @@ typedef struct
char ServerSignature[SCRAM_KEY_LEN];
} fe_scram_state;
-static bool read_server_first_message(fe_scram_state *state, char *input,
- PQExpBuffer errormessage);
-static bool read_server_final_message(fe_scram_state *state, char *input,
- PQExpBuffer errormessage);
-static char *build_client_first_message(fe_scram_state *state,
- PQExpBuffer errormessage);
-static char *build_client_final_message(fe_scram_state *state,
- PQExpBuffer errormessage);
+static bool read_server_first_message(fe_scram_state *state, char *input);
+static bool read_server_final_message(fe_scram_state *state, char *input);
+static char *build_client_first_message(fe_scram_state *state);
+static char *build_client_final_message(fe_scram_state *state);
static bool verify_server_signature(fe_scram_state *state);
static void calculate_client_proof(fe_scram_state *state,
const char *client_final_message_without_proof,
@@ -91,15 +81,9 @@ static bool pg_frontend_random(char *dst, int len);
* freed by pg_fe_scram_free().
*/
void *
-pg_fe_scram_init(const char *username,
+pg_fe_scram_init(PGconn *conn,
const char *password,
- bool ssl_in_use,
- const char *sasl_mechanism,
- const char *channel_binding_type,
- char *tls_finished_message,
- size_t tls_finished_len,
- char *certificate_hash,
- size_t certificate_hash_len)
+ const char *sasl_mechanism)
{
fe_scram_state *state;
char *prep_password;
@@ -111,15 +95,9 @@ pg_fe_scram_init(const char *username,
if (!state)
return NULL;
memset(state, 0, sizeof(fe_scram_state));
+ state->conn = conn;
state->state = FE_SCRAM_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->certificate_hash = certificate_hash;
- state->certificate_hash_len = certificate_hash_len;
state->sasl_mechanism = strdup(sasl_mechanism);
- state->channel_binding_type = channel_binding_type;
if (!state->sasl_mechanism)
{
@@ -160,10 +138,6 @@ pg_fe_scram_free(void *opaq)
if (state->password)
free(state->password);
- if (state->tls_finished_message)
- free(state->tls_finished_message);
- if (state->certificate_hash)
- free(state->certificate_hash);
/* client messages */
if (state->client_nonce)
@@ -194,9 +168,10 @@ pg_fe_scram_free(void *opaq)
void
pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen,
- bool *done, bool *success, PQExpBuffer errorMessage)
+ bool *done, bool *success)
{
fe_scram_state *state = (fe_scram_state *) opaq;
+ PGconn *conn = state->conn;
*done = false;
*success = false;
@@ -211,13 +186,13 @@ pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
{
if (inputlen == 0)
{
- printfPQExpBuffer(errorMessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("malformed SCRAM message (empty message)\n"));
goto error;
}
if (inputlen != strlen(input))
{
- printfPQExpBuffer(errorMessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("malformed SCRAM message (length mismatch)\n"));
goto error;
}
@@ -227,7 +202,7 @@ pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
{
case FE_SCRAM_INIT:
/* Begin the SCRAM handshake, by sending client nonce */
- *output = build_client_first_message(state, errorMessage);
+ *output = build_client_first_message(state);
if (*output == NULL)
goto error;
@@ -238,10 +213,10 @@ pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
case FE_SCRAM_NONCE_SENT:
/* Receive salt and server nonce, send response. */
- if (!read_server_first_message(state, input, errorMessage))
+ if (!read_server_first_message(state, input))
goto error;
- *output = build_client_final_message(state, errorMessage);
+ *output = build_client_final_message(state);
if (*output == NULL)
goto error;
@@ -252,7 +227,7 @@ pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
case FE_SCRAM_PROOF_SENT:
/* Receive server signature */
- if (!read_server_final_message(state, input, errorMessage))
+ if (!read_server_final_message(state, input))
goto error;
/*
@@ -266,7 +241,7 @@ pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
else
{
*success = false;
- printfPQExpBuffer(errorMessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("incorrect server signature\n"));
}
*done = true;
@@ -275,7 +250,7 @@ pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
default:
/* shouldn't happen */
- printfPQExpBuffer(errorMessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("invalid SCRAM exchange state\n"));
goto error;
}
@@ -333,8 +308,9 @@ read_attr_value(char **input, char attr, PQExpBuffer errorMessage)
* Build the first exchange message sent by the client.
*/
static char *
-build_client_first_message(fe_scram_state *state, PQExpBuffer errormessage)
+build_client_first_message(fe_scram_state *state)
{
+ PGconn *conn = state->conn;
char raw_nonce[SCRAM_RAW_NONCE_LEN + 1];
char *result;
int channel_info_len;
@@ -347,7 +323,7 @@ build_client_first_message(fe_scram_state *state, PQExpBuffer errormessage)
*/
if (!pg_frontend_random(raw_nonce, SCRAM_RAW_NONCE_LEN))
{
- printfPQExpBuffer(errormessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("could not generate nonce\n"));
return NULL;
}
@@ -355,7 +331,7 @@ build_client_first_message(fe_scram_state *state, PQExpBuffer errormessage)
state->client_nonce = malloc(pg_b64_enc_len(SCRAM_RAW_NONCE_LEN) + 1);
if (state->client_nonce == NULL)
{
- printfPQExpBuffer(errormessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("out of memory\n"));
return NULL;
}
@@ -376,11 +352,11 @@ build_client_first_message(fe_scram_state *state, PQExpBuffer errormessage)
*/
if (strcmp(state->sasl_mechanism, SCRAM_SHA256_PLUS_NAME) == 0)
{
- Assert(state->ssl_in_use);
- appendPQExpBuffer(&buf, "p=%s", state->channel_binding_type);
+ Assert(conn->ssl_in_use);
+ appendPQExpBuffer(&buf, "p=%s", conn->scram_channel_binding);
}
- else if (state->channel_binding_type == NULL ||
- strlen(state->channel_binding_type) == 0)
+ else if (conn->scram_channel_binding == NULL ||
+ strlen(conn->scram_channel_binding) == 0)
{
/*
* Client has chosen to not show to server that it supports channel
@@ -388,7 +364,7 @@ build_client_first_message(fe_scram_state *state, PQExpBuffer errormessage)
*/
appendPQExpBuffer(&buf, "n");
}
- else if (state->ssl_in_use)
+ else if (conn->ssl_in_use)
{
/*
* Client supports channel binding, but thinks the server does not.
@@ -429,7 +405,7 @@ build_client_first_message(fe_scram_state *state, PQExpBuffer errormessage)
oom_error:
termPQExpBuffer(&buf);
- printfPQExpBuffer(errormessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("out of memory\n"));
return NULL;
}
@@ -438,9 +414,10 @@ oom_error:
* Build the final exchange message sent from the client.
*/
static char *
-build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
+build_client_final_message(fe_scram_state *state)
{
PQExpBufferData buf;
+ PGconn *conn = state->conn;
uint8 client_proof[SCRAM_KEY_LEN];
char *result;
@@ -456,28 +433,41 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
*/
if (strcmp(state->sasl_mechanism, SCRAM_SHA256_PLUS_NAME) == 0)
{
- char *cbind_data;
- size_t cbind_data_len;
+ char *cbind_data = NULL;
+ size_t cbind_data_len = 0;
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)
+ if (strcmp(conn->scram_channel_binding, SCRAM_CHANNEL_BINDING_TLS_UNIQUE) == 0)
{
- cbind_data = state->tls_finished_message;
- cbind_data_len = state->tls_finished_len;
+ /* Fetch data from TLS finished message */
+#ifdef USE_SSL
+ cbind_data = pgtls_get_finished(state->conn, &cbind_data_len);
+ if (cbind_data == NULL)
+ goto oom_error;
+#endif
}
- else if (strcmp(state->channel_binding_type,
+ else if (strcmp(conn->scram_channel_binding,
SCRAM_CHANNEL_BINDING_TLS_ENDPOINT) == 0)
{
- cbind_data = state->certificate_hash;
- cbind_data_len = state->certificate_hash_len;
+ /* Fetch hash data of server's SSL certificate */
+#ifdef USE_SSL
+ cbind_data =
+ pgtls_get_peer_certificate_hash(state->conn,
+ &cbind_data_len);
+ if (cbind_data == NULL)
+ {
+ /* error message is already set on error */
+ return NULL;
+ }
+#endif
}
else
{
/* should not happen */
termPQExpBuffer(&buf);
- printfPQExpBuffer(errormessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("invalid channel binding type\n"));
return NULL;
}
@@ -485,37 +475,46 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
/* should not happen */
if (cbind_data == NULL || cbind_data_len == 0)
{
+ if (cbind_data != NULL)
+ free(cbind_data);
termPQExpBuffer(&buf);
- printfPQExpBuffer(errormessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("empty channel binding data for channel binding type \"%s\"\n"),
- state->channel_binding_type);
+ conn->scram_channel_binding);
return NULL;
}
appendPQExpBuffer(&buf, "c=");
- cbind_header_len = 4 + strlen(state->channel_binding_type); /* p=type,, */
+ /* p=type,, */
+ cbind_header_len = 4 + strlen(conn->scram_channel_binding);
cbind_input_len = cbind_header_len + cbind_data_len;
cbind_input = malloc(cbind_input_len);
if (!cbind_input)
+ {
+ free(cbind_data);
goto oom_error;
- snprintf(cbind_input, cbind_input_len, "p=%s,,", state->channel_binding_type);
+ }
+ snprintf(cbind_input, cbind_input_len, "p=%s,,",
+ conn->scram_channel_binding);
memcpy(cbind_input + cbind_header_len, cbind_data, cbind_data_len);
if (!enlargePQExpBuffer(&buf, pg_b64_enc_len(cbind_input_len)))
{
+ free(cbind_data);
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_data);
free(cbind_input);
}
- else if (state->channel_binding_type == NULL ||
- strlen(state->channel_binding_type) == 0)
+ else if (conn->scram_channel_binding == NULL ||
+ strlen(conn->scram_channel_binding) == 0)
appendPQExpBuffer(&buf, "c=biws"); /* base64 of "n,," */
- else if (state->ssl_in_use)
+ else if (conn->ssl_in_use)
appendPQExpBuffer(&buf, "c=eSws"); /* base64 of "y,," */
else
appendPQExpBuffer(&buf, "c=biws"); /* base64 of "n,," */
@@ -553,7 +552,7 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
oom_error:
termPQExpBuffer(&buf);
- printfPQExpBuffer(errormessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("out of memory\n"));
return NULL;
}
@@ -562,9 +561,9 @@ oom_error:
* Read the first exchange message coming from the server.
*/
static bool
-read_server_first_message(fe_scram_state *state, char *input,
- PQExpBuffer errormessage)
+read_server_first_message(fe_scram_state *state, char *input)
{
+ PGconn *conn = state->conn;
char *iterations_str;
char *endptr;
char *encoded_salt;
@@ -573,13 +572,14 @@ read_server_first_message(fe_scram_state *state, char *input,
state->server_first_message = strdup(input);
if (state->server_first_message == NULL)
{
- printfPQExpBuffer(errormessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("out of memory\n"));
return false;
}
/* parse the message */
- nonce = read_attr_value(&input, 'r', errormessage);
+ nonce = read_attr_value(&input, 'r',
+ &conn->errorMessage);
if (nonce == NULL)
{
/* read_attr_value() has generated an error string */
@@ -590,7 +590,7 @@ read_server_first_message(fe_scram_state *state, char *input,
if (strlen(nonce) < strlen(state->client_nonce) ||
memcmp(nonce, state->client_nonce, strlen(state->client_nonce)) != 0)
{
- printfPQExpBuffer(errormessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("invalid SCRAM response (nonce mismatch)\n"));
return false;
}
@@ -598,12 +598,12 @@ read_server_first_message(fe_scram_state *state, char *input,
state->nonce = strdup(nonce);
if (state->nonce == NULL)
{
- printfPQExpBuffer(errormessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("out of memory\n"));
return false;
}
- encoded_salt = read_attr_value(&input, 's', errormessage);
+ encoded_salt = read_attr_value(&input, 's', &conn->errorMessage);
if (encoded_salt == NULL)
{
/* read_attr_value() has generated an error string */
@@ -612,7 +612,7 @@ read_server_first_message(fe_scram_state *state, char *input,
state->salt = malloc(pg_b64_dec_len(strlen(encoded_salt)));
if (state->salt == NULL)
{
- printfPQExpBuffer(errormessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("out of memory\n"));
return false;
}
@@ -620,7 +620,7 @@ read_server_first_message(fe_scram_state *state, char *input,
strlen(encoded_salt),
state->salt);
- iterations_str = read_attr_value(&input, 'i', errormessage);
+ iterations_str = read_attr_value(&input, 'i', &conn->errorMessage);
if (iterations_str == NULL)
{
/* read_attr_value() has generated an error string */
@@ -629,13 +629,13 @@ read_server_first_message(fe_scram_state *state, char *input,
state->iterations = strtol(iterations_str, &endptr, 10);
if (*endptr != '\0' || state->iterations < 1)
{
- printfPQExpBuffer(errormessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("malformed SCRAM message (invalid iteration count)\n"));
return false;
}
if (*input != '\0')
- printfPQExpBuffer(errormessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("malformed SCRAM message (garbage at end of server-first-message)\n"));
return true;
@@ -645,16 +645,16 @@ read_server_first_message(fe_scram_state *state, char *input,
* Read the final exchange message coming from the server.
*/
static bool
-read_server_final_message(fe_scram_state *state, char *input,
- PQExpBuffer errormessage)
+read_server_final_message(fe_scram_state *state, char *input)
{
+ PGconn *conn = state->conn;
char *encoded_server_signature;
int server_signature_len;
state->server_final_message = strdup(input);
if (!state->server_final_message)
{
- printfPQExpBuffer(errormessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("out of memory\n"));
return false;
}
@@ -662,16 +662,18 @@ read_server_final_message(fe_scram_state *state, char *input,
/* Check for error result. */
if (*input == 'e')
{
- char *errmsg = read_attr_value(&input, 'e', errormessage);
+ char *errmsg = read_attr_value(&input, 'e',
+ &conn->errorMessage);
- printfPQExpBuffer(errormessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("error received from server in SCRAM exchange: %s\n"),
errmsg);
return false;
}
/* Parse the message. */
- encoded_server_signature = read_attr_value(&input, 'v', errormessage);
+ encoded_server_signature = read_attr_value(&input, 'v',
+ &conn->errorMessage);
if (encoded_server_signature == NULL)
{
/* read_attr_value() has generated an error message */
@@ -679,7 +681,7 @@ read_server_final_message(fe_scram_state *state, char *input,
}
if (*input != '\0')
- printfPQExpBuffer(errormessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("malformed SCRAM message (garbage at end of server-final-message)\n"));
server_signature_len = pg_b64_decode(encoded_server_signature,
@@ -687,7 +689,7 @@ read_server_final_message(fe_scram_state *state, char *input,
state->ServerSignature);
if (server_signature_len != SCRAM_KEY_LEN)
{
- printfPQExpBuffer(errormessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("malformed SCRAM message (invalid server signature)\n"));
return false;
}
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index bb9b0573d1..9c3524e553 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -491,10 +491,6 @@ pg_SASL_init(PGconn *conn, int payloadlen)
bool success;
const char *selected_mechanism;
PQExpBufferData mechanism_buf;
- char *tls_finished = NULL;
- size_t tls_finished_len = 0;
- char *certificate_hash = NULL;
- size_t certificate_hash_len = 0;
char *password;
initPQExpBuffer(&mechanism_buf);
@@ -572,40 +568,15 @@ pg_SASL_init(PGconn *conn, int payloadlen)
goto error;
}
-#ifdef USE_SSL
-
- /*
- * 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;
-
- certificate_hash =
- pgtls_get_peer_certificate_hash(conn,
- &certificate_hash_len);
- if (certificate_hash == NULL)
- goto error; /* error message is set */
- }
-#endif
-
/*
* Initialize the SASL state information with all the information gathered
* during the initial exchange.
*
* Note: Only tls-unique is supported for the moment.
*/
- conn->sasl_state = pg_fe_scram_init(conn->pguser,
+ conn->sasl_state = pg_fe_scram_init(conn,
password,
- conn->ssl_in_use,
- selected_mechanism,
- conn->scram_channel_binding,
- tls_finished,
- tls_finished_len,
- certificate_hash,
- certificate_hash_len);
+ selected_mechanism);
if (!conn->sasl_state)
goto oom_error;
@@ -613,7 +584,7 @@ pg_SASL_init(PGconn *conn, int payloadlen)
pg_fe_scram_exchange(conn->sasl_state,
NULL, -1,
&initialresponse, &initialresponselen,
- &done, &success, &conn->errorMessage);
+ &done, &success);
if (done && !success)
goto error;
@@ -694,7 +665,7 @@ pg_SASL_continue(PGconn *conn, int payloadlen, bool final)
pg_fe_scram_exchange(conn->sasl_state,
challenge, payloadlen,
&output, &outputlen,
- &done, &success, &conn->errorMessage);
+ &done, &success);
free(challenge); /* don't need the input anymore */
if (final && !done)
diff --git a/src/interfaces/libpq/fe-auth.h b/src/interfaces/libpq/fe-auth.h
index 68de8b6e32..1265d0d2f7 100644
--- a/src/interfaces/libpq/fe-auth.h
+++ b/src/interfaces/libpq/fe-auth.h
@@ -23,19 +23,13 @@ extern int pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn);
extern char *pg_fe_getauthname(PQExpBuffer errorMessage);
/* Prototypes for functions in fe-auth-scram.c */
-extern void *pg_fe_scram_init(const char *username,
+extern void *pg_fe_scram_init(PGconn *conn,
const char *password,
- bool ssl_in_use,
- const char *sasl_mechanism,
- const char *channel_binding_type,
- char *tls_finished_message,
- size_t tls_finished_len,
- char *certificate_hash,
- size_t certificate_hash_len);
+ const char *sasl_mechanism);
extern void pg_fe_scram_free(void *opaq);
extern void pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen,
- bool *done, bool *success, PQExpBuffer errorMessage);
+ bool *done, bool *success);
extern char *pg_fe_scram_build_verifier(const char *password);
#endif /* FE_AUTH_H */
--
2.15.1
On 12/22/17 03:10, Michael Paquier wrote:
On Fri, Dec 22, 2017 at 11:59 AM, Michael Paquier
<michael.paquier@gmail.com> wrote:I have looked at how things could be done in symmetry for both the frontend
and backend code, and I have produced the attached patch 0002, which
can be applied on top of 0001 implementing tls-server-end-point. This
simplifies the interfaces to initialize the SCRAM status data by saving
into scram_state and fe_scram_state respectively Port* and PGconn* which
holds most of the data needed for the exchange. With this patch, cbind_data
is generated only if a specific channel binding type is used with the
appropriate data. So if no channel binding is used there is no additional
SSL call done to get the TLS finished data or the server certificate hash.0001 has no real changes compared to the last versions.
Second thoughts on 0002 as there is actually no need to move around
errorMessage if the PGconn* pointer is saved in the SCRAM status data
as both are linked. The attached simplifies the logic even more.
That all looks pretty reasonable.
I'm working through patch 0001 now. I haven't found any documentation
on the function OBJ_find_sigid_algs(). What does it do? One might
think that the nid returned by X509_get_signature_nid() is already the
algo_nid we want to use, but there appears to be more to this.
--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
On Tue, Dec 26, 2017 at 03:28:09PM -0500, Peter Eisentraut wrote:
On 12/22/17 03:10, Michael Paquier wrote:
Second thoughts on 0002 as there is actually no need to move around
errorMessage if the PGconn* pointer is saved in the SCRAM status data
as both are linked. The attached simplifies the logic even more.That all looks pretty reasonable.
Thanks for the review. Don't you think that the the refactoring
simplifications should be done first though? This would result in
producing the patch set in reverse order. I'll be fine to produce them
if need be.
I'm working through patch 0001 now. I haven't found any documentation
on the function OBJ_find_sigid_algs(). What does it do? One might
think that the nid returned by X509_get_signature_nid() is already the
algo_nid we want to use, but there appears to be more to this.
All the objects returned by X509_get_signature_nid() are listed in
crypto/objects/obj_dat.h which may include more information than just
the algorithm type, like for example if RSA encryption is used or not,
etc. I found about the low-level OBJ_find_sigid_algs() to actually get
the real hashing algorithm after diving into X509* informations. And by
looking at X509_signature_print() I found out that this returns the
information we are looking for. This has the damn advantage that we rely
on a minimal lists of algorithms and we don't need to worry about any
future options linked with X509_get_signature_nid(), so this simplifies
Postgres code as well as long-term maintenance.
--
Michael
On Wed, Dec 27, 2017 at 09:27:40AM +0900, Michael Paquier wrote:
On Tue, Dec 26, 2017 at 03:28:09PM -0500, Peter Eisentraut wrote:
On 12/22/17 03:10, Michael Paquier wrote:
Second thoughts on 0002 as there is actually no need to move around
errorMessage if the PGconn* pointer is saved in the SCRAM status data
as both are linked. The attached simplifies the logic even more.That all looks pretty reasonable.
Thanks for the review. Don't you think that the the refactoring
simplifications should be done first though? This would result in
producing the patch set in reverse order. I'll be fine to produce them
if need be.
Well, here is a patch set doing the reverse operation: refactoring does
first in 0001 and support for tls-server-end-point is in 0002. Hope this
helps.
--
Michael
Attachments:
0001-Refactor-channel-binding-code-to-fetch-cbind_data-on.patchtext/plain; charset=us-asciiDownload
From 551f9a037d6e38036998337f703758b41d2e1c72 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Fri, 22 Dec 2017 17:04:19 +0900
Subject: [PATCH 1/2] Refactor channel binding code to fetch cbind_data only
when necessary
As things stand now, channel binding data is fetched from OpenSSL and
saved into the SASL exchange context for any SSL connection attempted
for a SCRAM authentication, resulting in data fetched but not used if no
channel binding is used or if a different channel binding type is used
than what the data is here for.
Refactor the code in such a way that binding data is only fetched from
the SSL stack only when a specific channel binding is used for both the
frontend and the backend. In order to achieve that, save the libpq
connection context directly in the SCRAM exchange state, and add a
dependency to SSL in the low-level SCRAM routines.
This makes the interface in charge of initializing the SCRAM context
cleaner as all its data comes from either PGconn* (for frontend) or
Port* (for the backend).
---
src/backend/libpq/auth-scram.c | 34 +++-----
src/backend/libpq/auth.c | 19 +----
src/include/libpq/scram.h | 6 +-
src/interfaces/libpq/fe-auth-scram.c | 159 +++++++++++++++++------------------
src/interfaces/libpq/fe-auth.c | 27 +-----
src/interfaces/libpq/fe-auth.h | 10 +--
6 files changed, 104 insertions(+), 151 deletions(-)
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index d52a763457..72973d3789 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -110,10 +110,8 @@ typedef struct
const char *username; /* username from startup packet */
+ Port *port;
char cbind_flag;
- bool ssl_in_use;
- const char *tls_finished_message;
- size_t tls_finished_len;
char *channel_binding_type;
int iterations;
@@ -172,21 +170,15 @@ 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,
- bool ssl_in_use,
- const char *tls_finished_message,
- size_t tls_finished_len)
+pg_be_scram_init(Port *port,
+ const char *shadow_pass)
{
scram_state *state;
bool got_verifier;
state = (scram_state *) palloc0(sizeof(scram_state));
+ state->port = port;
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;
/*
@@ -209,7 +201,7 @@ pg_be_scram_init(const char *username,
*/
ereport(LOG,
(errmsg("invalid SCRAM verifier for user \"%s\"",
- username)));
+ state->port->user_name)));
got_verifier = false;
}
}
@@ -220,7 +212,7 @@ pg_be_scram_init(const char *username,
* authentication with an MD5 hash.)
*/
state->logdetail = psprintf(_("User \"%s\" does not have a valid SCRAM verifier."),
- state->username);
+ state->port->user_name);
got_verifier = false;
}
}
@@ -242,8 +234,8 @@ pg_be_scram_init(const char *username,
*/
if (!got_verifier)
{
- mock_scram_verifier(username, &state->iterations, &state->salt,
- state->StoredKey, state->ServerKey);
+ mock_scram_verifier(state->port->user_name, &state->iterations,
+ &state->salt, state->StoredKey, state->ServerKey);
state->doomed = true;
}
@@ -815,7 +807,7 @@ read_client_first_message(scram_state *state, char *input)
* it supports channel binding, which in this implementation is
* the case if a connection is using SSL.
*/
- if (state->ssl_in_use)
+ if (state->port->ssl_in_use)
ereport(ERROR,
(errcode(ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION),
errmsg("SCRAM channel binding negotiation error"),
@@ -839,7 +831,7 @@ read_client_first_message(scram_state *state, char *input)
{
char *channel_binding_type;
- if (!state->ssl_in_use)
+ if (!state->port->ssl_in_use)
{
/*
* Without SSL, we don't support channel binding.
@@ -1120,8 +1112,10 @@ read_client_final_message(scram_state *state, char *input)
*/
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;
+ /* Fetch data from TLS finished message */
+#ifdef USE_SSL
+ cbind_data = be_tls_get_peer_finished(state->port, &cbind_data_len);
+#endif
}
else
{
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index b7f9bb1669..bd91e1cd18 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -873,8 +873,6 @@ 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
@@ -915,17 +913,6 @@ CheckSCRAMAuth(Port *port, char *shadow_pass, char **logdetail)
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.
*
@@ -937,11 +924,7 @@ 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,
- port->ssl_in_use,
- tls_finished,
- tls_finished_len);
+ scram_opaq = pg_be_scram_init(port, shadow_pass);
/*
* Loop through SASL message exchange. This exchange can consist of
diff --git a/src/include/libpq/scram.h b/src/include/libpq/scram.h
index 2c245813d6..f404f57253 100644
--- a/src/include/libpq/scram.h
+++ b/src/include/libpq/scram.h
@@ -13,15 +13,15 @@
#ifndef PG_SCRAM_H
#define PG_SCRAM_H
+#include "libpq/libpq-be.h"
+
/* Status codes for message exchange */
#define SASL_EXCHANGE_CONTINUE 0
#define SASL_EXCHANGE_SUCCESS 1
#define SASL_EXCHANGE_FAILURE 2
/* Routines dedicated to authentication */
-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 void *pg_be_scram_init(Port *port, const char *shadow_pass);
extern int pg_be_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen, char **logdetail);
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index b8f7a6b5be..e8fc33c72f 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -42,13 +42,9 @@ typedef struct
fe_scram_state_enum state;
/* These are supplied by the user */
- const char *username;
+ PGconn *conn;
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 */
uint8 SaltedPassword[SCRAM_KEY_LEN];
@@ -68,14 +64,10 @@ typedef struct
char ServerSignature[SCRAM_KEY_LEN];
} fe_scram_state;
-static bool read_server_first_message(fe_scram_state *state, char *input,
- PQExpBuffer errormessage);
-static bool read_server_final_message(fe_scram_state *state, char *input,
- PQExpBuffer errormessage);
-static char *build_client_first_message(fe_scram_state *state,
- PQExpBuffer errormessage);
-static char *build_client_final_message(fe_scram_state *state,
- PQExpBuffer errormessage);
+static bool read_server_first_message(fe_scram_state *state, char *input);
+static bool read_server_final_message(fe_scram_state *state, char *input);
+static char *build_client_first_message(fe_scram_state *state);
+static char *build_client_final_message(fe_scram_state *state);
static bool verify_server_signature(fe_scram_state *state);
static void calculate_client_proof(fe_scram_state *state,
const char *client_final_message_without_proof,
@@ -89,13 +81,9 @@ static bool pg_frontend_random(char *dst, int len);
* freed by pg_fe_scram_free().
*/
void *
-pg_fe_scram_init(const char *username,
+pg_fe_scram_init(PGconn *conn,
const char *password,
- bool ssl_in_use,
- const char *sasl_mechanism,
- const char *channel_binding_type,
- char *tls_finished_message,
- size_t tls_finished_len)
+ const char *sasl_mechanism)
{
fe_scram_state *state;
char *prep_password;
@@ -107,13 +95,9 @@ pg_fe_scram_init(const char *username,
if (!state)
return NULL;
memset(state, 0, sizeof(fe_scram_state));
+ state->conn = conn;
state->state = FE_SCRAM_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->sasl_mechanism = strdup(sasl_mechanism);
- state->channel_binding_type = channel_binding_type;
if (!state->sasl_mechanism)
{
@@ -154,10 +138,6 @@ pg_fe_scram_free(void *opaq)
if (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 */
if (state->client_nonce)
@@ -188,9 +168,10 @@ pg_fe_scram_free(void *opaq)
void
pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen,
- bool *done, bool *success, PQExpBuffer errorMessage)
+ bool *done, bool *success)
{
fe_scram_state *state = (fe_scram_state *) opaq;
+ PGconn *conn = state->conn;
*done = false;
*success = false;
@@ -205,13 +186,13 @@ pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
{
if (inputlen == 0)
{
- printfPQExpBuffer(errorMessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("malformed SCRAM message (empty message)\n"));
goto error;
}
if (inputlen != strlen(input))
{
- printfPQExpBuffer(errorMessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("malformed SCRAM message (length mismatch)\n"));
goto error;
}
@@ -221,7 +202,7 @@ pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
{
case FE_SCRAM_INIT:
/* Begin the SCRAM handshake, by sending client nonce */
- *output = build_client_first_message(state, errorMessage);
+ *output = build_client_first_message(state);
if (*output == NULL)
goto error;
@@ -232,10 +213,10 @@ pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
case FE_SCRAM_NONCE_SENT:
/* Receive salt and server nonce, send response. */
- if (!read_server_first_message(state, input, errorMessage))
+ if (!read_server_first_message(state, input))
goto error;
- *output = build_client_final_message(state, errorMessage);
+ *output = build_client_final_message(state);
if (*output == NULL)
goto error;
@@ -246,7 +227,7 @@ pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
case FE_SCRAM_PROOF_SENT:
/* Receive server signature */
- if (!read_server_final_message(state, input, errorMessage))
+ if (!read_server_final_message(state, input))
goto error;
/*
@@ -260,7 +241,7 @@ pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
else
{
*success = false;
- printfPQExpBuffer(errorMessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("incorrect server signature\n"));
}
*done = true;
@@ -269,7 +250,7 @@ pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
default:
/* shouldn't happen */
- printfPQExpBuffer(errorMessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("invalid SCRAM exchange state\n"));
goto error;
}
@@ -327,8 +308,9 @@ read_attr_value(char **input, char attr, PQExpBuffer errorMessage)
* Build the first exchange message sent by the client.
*/
static char *
-build_client_first_message(fe_scram_state *state, PQExpBuffer errormessage)
+build_client_first_message(fe_scram_state *state)
{
+ PGconn *conn = state->conn;
char raw_nonce[SCRAM_RAW_NONCE_LEN + 1];
char *result;
int channel_info_len;
@@ -341,7 +323,7 @@ build_client_first_message(fe_scram_state *state, PQExpBuffer errormessage)
*/
if (!pg_frontend_random(raw_nonce, SCRAM_RAW_NONCE_LEN))
{
- printfPQExpBuffer(errormessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("could not generate nonce\n"));
return NULL;
}
@@ -349,7 +331,7 @@ build_client_first_message(fe_scram_state *state, PQExpBuffer errormessage)
state->client_nonce = malloc(pg_b64_enc_len(SCRAM_RAW_NONCE_LEN) + 1);
if (state->client_nonce == NULL)
{
- printfPQExpBuffer(errormessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("out of memory\n"));
return NULL;
}
@@ -370,11 +352,11 @@ build_client_first_message(fe_scram_state *state, PQExpBuffer errormessage)
*/
if (strcmp(state->sasl_mechanism, SCRAM_SHA256_PLUS_NAME) == 0)
{
- Assert(state->ssl_in_use);
- appendPQExpBuffer(&buf, "p=%s", state->channel_binding_type);
+ Assert(conn->ssl_in_use);
+ appendPQExpBuffer(&buf, "p=%s", conn->scram_channel_binding);
}
- else if (state->channel_binding_type == NULL ||
- strlen(state->channel_binding_type) == 0)
+ else if (conn->scram_channel_binding == NULL ||
+ strlen(conn->scram_channel_binding) == 0)
{
/*
* Client has chosen to not show to server that it supports channel
@@ -382,7 +364,7 @@ build_client_first_message(fe_scram_state *state, PQExpBuffer errormessage)
*/
appendPQExpBuffer(&buf, "n");
}
- else if (state->ssl_in_use)
+ else if (conn->ssl_in_use)
{
/*
* Client supports channel binding, but thinks the server does not.
@@ -423,7 +405,7 @@ build_client_first_message(fe_scram_state *state, PQExpBuffer errormessage)
oom_error:
termPQExpBuffer(&buf);
- printfPQExpBuffer(errormessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("out of memory\n"));
return NULL;
}
@@ -432,9 +414,10 @@ oom_error:
* Build the final exchange message sent from the client.
*/
static char *
-build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
+build_client_final_message(fe_scram_state *state)
{
PQExpBufferData buf;
+ PGconn *conn = state->conn;
uint8 client_proof[SCRAM_KEY_LEN];
char *result;
@@ -450,22 +433,26 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
*/
if (strcmp(state->sasl_mechanism, SCRAM_SHA256_PLUS_NAME) == 0)
{
- char *cbind_data;
- size_t cbind_data_len;
+ char *cbind_data = NULL;
+ size_t cbind_data_len = 0;
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)
+ if (strcmp(conn->scram_channel_binding, SCRAM_CHANNEL_BINDING_TLS_UNIQUE) == 0)
{
- cbind_data = state->tls_finished_message;
- cbind_data_len = state->tls_finished_len;
+ /* Fetch data from TLS finished message */
+#ifdef USE_SSL
+ cbind_data = pgtls_get_finished(state->conn, &cbind_data_len);
+ if (cbind_data == NULL)
+ goto oom_error;
+#endif
}
else
{
/* should not happen */
termPQExpBuffer(&buf);
- printfPQExpBuffer(errormessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("invalid channel binding type\n"));
return NULL;
}
@@ -473,37 +460,46 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
/* should not happen */
if (cbind_data == NULL || cbind_data_len == 0)
{
+ if (cbind_data != NULL)
+ free(cbind_data);
termPQExpBuffer(&buf);
- printfPQExpBuffer(errormessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("empty channel binding data for channel binding type \"%s\"\n"),
- state->channel_binding_type);
+ conn->scram_channel_binding);
return NULL;
}
appendPQExpBuffer(&buf, "c=");
- cbind_header_len = 4 + strlen(state->channel_binding_type); /* p=type,, */
+ /* p=type,, */
+ cbind_header_len = 4 + strlen(conn->scram_channel_binding);
cbind_input_len = cbind_header_len + cbind_data_len;
cbind_input = malloc(cbind_input_len);
if (!cbind_input)
+ {
+ free(cbind_data);
goto oom_error;
- snprintf(cbind_input, cbind_input_len, "p=%s,,", state->channel_binding_type);
+ }
+ snprintf(cbind_input, cbind_input_len, "p=%s,,",
+ conn->scram_channel_binding);
memcpy(cbind_input + cbind_header_len, cbind_data, cbind_data_len);
if (!enlargePQExpBuffer(&buf, pg_b64_enc_len(cbind_input_len)))
{
+ free(cbind_data);
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_data);
free(cbind_input);
}
- else if (state->channel_binding_type == NULL ||
- strlen(state->channel_binding_type) == 0)
+ else if (conn->scram_channel_binding == NULL ||
+ strlen(conn->scram_channel_binding) == 0)
appendPQExpBuffer(&buf, "c=biws"); /* base64 of "n,," */
- else if (state->ssl_in_use)
+ else if (conn->ssl_in_use)
appendPQExpBuffer(&buf, "c=eSws"); /* base64 of "y,," */
else
appendPQExpBuffer(&buf, "c=biws"); /* base64 of "n,," */
@@ -541,7 +537,7 @@ build_client_final_message(fe_scram_state *state, PQExpBuffer errormessage)
oom_error:
termPQExpBuffer(&buf);
- printfPQExpBuffer(errormessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("out of memory\n"));
return NULL;
}
@@ -550,9 +546,9 @@ oom_error:
* Read the first exchange message coming from the server.
*/
static bool
-read_server_first_message(fe_scram_state *state, char *input,
- PQExpBuffer errormessage)
+read_server_first_message(fe_scram_state *state, char *input)
{
+ PGconn *conn = state->conn;
char *iterations_str;
char *endptr;
char *encoded_salt;
@@ -561,13 +557,14 @@ read_server_first_message(fe_scram_state *state, char *input,
state->server_first_message = strdup(input);
if (state->server_first_message == NULL)
{
- printfPQExpBuffer(errormessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("out of memory\n"));
return false;
}
/* parse the message */
- nonce = read_attr_value(&input, 'r', errormessage);
+ nonce = read_attr_value(&input, 'r',
+ &conn->errorMessage);
if (nonce == NULL)
{
/* read_attr_value() has generated an error string */
@@ -578,7 +575,7 @@ read_server_first_message(fe_scram_state *state, char *input,
if (strlen(nonce) < strlen(state->client_nonce) ||
memcmp(nonce, state->client_nonce, strlen(state->client_nonce)) != 0)
{
- printfPQExpBuffer(errormessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("invalid SCRAM response (nonce mismatch)\n"));
return false;
}
@@ -586,12 +583,12 @@ read_server_first_message(fe_scram_state *state, char *input,
state->nonce = strdup(nonce);
if (state->nonce == NULL)
{
- printfPQExpBuffer(errormessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("out of memory\n"));
return false;
}
- encoded_salt = read_attr_value(&input, 's', errormessage);
+ encoded_salt = read_attr_value(&input, 's', &conn->errorMessage);
if (encoded_salt == NULL)
{
/* read_attr_value() has generated an error string */
@@ -600,7 +597,7 @@ read_server_first_message(fe_scram_state *state, char *input,
state->salt = malloc(pg_b64_dec_len(strlen(encoded_salt)));
if (state->salt == NULL)
{
- printfPQExpBuffer(errormessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("out of memory\n"));
return false;
}
@@ -608,7 +605,7 @@ read_server_first_message(fe_scram_state *state, char *input,
strlen(encoded_salt),
state->salt);
- iterations_str = read_attr_value(&input, 'i', errormessage);
+ iterations_str = read_attr_value(&input, 'i', &conn->errorMessage);
if (iterations_str == NULL)
{
/* read_attr_value() has generated an error string */
@@ -617,13 +614,13 @@ read_server_first_message(fe_scram_state *state, char *input,
state->iterations = strtol(iterations_str, &endptr, 10);
if (*endptr != '\0' || state->iterations < 1)
{
- printfPQExpBuffer(errormessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("malformed SCRAM message (invalid iteration count)\n"));
return false;
}
if (*input != '\0')
- printfPQExpBuffer(errormessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("malformed SCRAM message (garbage at end of server-first-message)\n"));
return true;
@@ -633,16 +630,16 @@ read_server_first_message(fe_scram_state *state, char *input,
* Read the final exchange message coming from the server.
*/
static bool
-read_server_final_message(fe_scram_state *state, char *input,
- PQExpBuffer errormessage)
+read_server_final_message(fe_scram_state *state, char *input)
{
+ PGconn *conn = state->conn;
char *encoded_server_signature;
int server_signature_len;
state->server_final_message = strdup(input);
if (!state->server_final_message)
{
- printfPQExpBuffer(errormessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("out of memory\n"));
return false;
}
@@ -650,16 +647,18 @@ read_server_final_message(fe_scram_state *state, char *input,
/* Check for error result. */
if (*input == 'e')
{
- char *errmsg = read_attr_value(&input, 'e', errormessage);
+ char *errmsg = read_attr_value(&input, 'e',
+ &conn->errorMessage);
- printfPQExpBuffer(errormessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("error received from server in SCRAM exchange: %s\n"),
errmsg);
return false;
}
/* Parse the message. */
- encoded_server_signature = read_attr_value(&input, 'v', errormessage);
+ encoded_server_signature = read_attr_value(&input, 'v',
+ &conn->errorMessage);
if (encoded_server_signature == NULL)
{
/* read_attr_value() has generated an error message */
@@ -667,7 +666,7 @@ read_server_final_message(fe_scram_state *state, char *input,
}
if (*input != '\0')
- printfPQExpBuffer(errormessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("malformed SCRAM message (garbage at end of server-final-message)\n"));
server_signature_len = pg_b64_decode(encoded_server_signature,
@@ -675,7 +674,7 @@ read_server_final_message(fe_scram_state *state, char *input,
state->ServerSignature);
if (server_signature_len != SCRAM_KEY_LEN)
{
- printfPQExpBuffer(errormessage,
+ printfPQExpBuffer(&conn->errorMessage,
libpq_gettext("malformed SCRAM message (invalid server signature)\n"));
return false;
}
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 3340a9ad93..9c3524e553 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -491,8 +491,6 @@ pg_SASL_init(PGconn *conn, int payloadlen)
bool success;
const char *selected_mechanism;
PQExpBufferData mechanism_buf;
- char *tls_finished = NULL;
- size_t tls_finished_len = 0;
char *password;
initPQExpBuffer(&mechanism_buf);
@@ -570,32 +568,15 @@ pg_SASL_init(PGconn *conn, int payloadlen)
goto error;
}
-#ifdef USE_SSL
-
- /*
- * 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;
- }
-#endif
-
/*
* Initialize the SASL state information with all the information gathered
* during the initial exchange.
*
* Note: Only tls-unique is supported for the moment.
*/
- conn->sasl_state = pg_fe_scram_init(conn->pguser,
+ conn->sasl_state = pg_fe_scram_init(conn,
password,
- conn->ssl_in_use,
- selected_mechanism,
- conn->scram_channel_binding,
- tls_finished,
- tls_finished_len);
+ selected_mechanism);
if (!conn->sasl_state)
goto oom_error;
@@ -603,7 +584,7 @@ pg_SASL_init(PGconn *conn, int payloadlen)
pg_fe_scram_exchange(conn->sasl_state,
NULL, -1,
&initialresponse, &initialresponselen,
- &done, &success, &conn->errorMessage);
+ &done, &success);
if (done && !success)
goto error;
@@ -684,7 +665,7 @@ pg_SASL_continue(PGconn *conn, int payloadlen, bool final)
pg_fe_scram_exchange(conn->sasl_state,
challenge, payloadlen,
&output, &outputlen,
- &done, &success, &conn->errorMessage);
+ &done, &success);
free(challenge); /* don't need the input anymore */
if (final && !done)
diff --git a/src/interfaces/libpq/fe-auth.h b/src/interfaces/libpq/fe-auth.h
index db319ac071..1265d0d2f7 100644
--- a/src/interfaces/libpq/fe-auth.h
+++ b/src/interfaces/libpq/fe-auth.h
@@ -23,17 +23,13 @@ extern int pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn);
extern char *pg_fe_getauthname(PQExpBuffer errorMessage);
/* Prototypes for functions in fe-auth-scram.c */
-extern void *pg_fe_scram_init(const char *username,
+extern void *pg_fe_scram_init(PGconn *conn,
const char *password,
- bool ssl_in_use,
- const char *sasl_mechanism,
- const char *channel_binding_type,
- char *tls_finished_message,
- size_t tls_finished_len);
+ const char *sasl_mechanism);
extern void pg_fe_scram_free(void *opaq);
extern void pg_fe_scram_exchange(void *opaq, char *input, int inputlen,
char **output, int *outputlen,
- bool *done, bool *success, PQExpBuffer errorMessage);
+ bool *done, bool *success);
extern char *pg_fe_scram_build_verifier(const char *password);
#endif /* FE_AUTH_H */
--
2.15.1
0002-Implement-channel-binding-tls-server-end-point-for-S.patchtext/plain; charset=us-asciiDownload
From db3c278177ded3a37431585bc85f60f646b976a8 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 28 Dec 2017 16:12:54 +0900
Subject: [PATCH 2/2] Implement channel binding tls-server-end-point for SCRAM
As referenced in RFC 5929, this channel binding is not the default value
and uses a hash of the certificate as binding data. On the frontend,
this
can be resumed in getting the data from SSL_get_peer_certificate() and
on the backend SSL_get_certificate().
The hashing algorithm needs also to switch to SHA-256 if the signature
algorithm is MD5 or SHA-1, so let's be careful about that.
---
doc/src/sgml/protocol.sgml | 5 +-
src/backend/libpq/auth-scram.c | 21 +++++++--
src/backend/libpq/be-secure-openssl.c | 61 +++++++++++++++++++++++++
src/include/common/scram-common.h | 1 +
src/include/libpq/libpq-be.h | 1 +
src/interfaces/libpq/fe-auth-scram.c | 15 ++++++
src/interfaces/libpq/fe-secure-openssl.c | 78 ++++++++++++++++++++++++++++++++
src/interfaces/libpq/libpq-int.h | 1 +
src/test/ssl/t/002_scram.pl | 5 +-
9 files changed, 180 insertions(+), 8 deletions(-)
diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml
index 8174e3defa..365f72b51d 100644
--- a/doc/src/sgml/protocol.sgml
+++ b/doc/src/sgml/protocol.sgml
@@ -1576,8 +1576,9 @@ the password is in.
<para>
<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.
+is <literal>SCRAM-SHA-256-PLUS</literal>. Two channel binding types are
+supported at the moment: <literal>tls-unique</literal>, which is the default,
+and <literal>tls-server-end-point</literal>, both defined in RFC 5929.
</para>
<procedure>
diff --git a/src/backend/libpq/auth-scram.c b/src/backend/libpq/auth-scram.c
index 72973d3789..0a50f815ab 100644
--- a/src/backend/libpq/auth-scram.c
+++ b/src/backend/libpq/auth-scram.c
@@ -849,13 +849,15 @@ read_client_first_message(scram_state *state, char *input)
}
/*
- * 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.)
+ * Read value provided by client; only tls-unique and
+ * tls-server-end-point are 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)
+ if (strcmp(channel_binding_type, SCRAM_CHANNEL_BINDING_TLS_UNIQUE) != 0 &&
+ strcmp(channel_binding_type, SCRAM_CHANNEL_BINDING_TLS_ENDPOINT) != 0)
ereport(ERROR,
(errcode(ERRCODE_PROTOCOL_VIOLATION),
(errmsg("unsupported SCRAM channel-binding type"))));
@@ -1115,6 +1117,15 @@ read_client_final_message(scram_state *state, char *input)
/* Fetch data from TLS finished message */
#ifdef USE_SSL
cbind_data = be_tls_get_peer_finished(state->port, &cbind_data_len);
+#endif
+ }
+ else if (strcmp(state->channel_binding_type,
+ SCRAM_CHANNEL_BINDING_TLS_ENDPOINT) == 0)
+ {
+ /* Fetch hash data of server's SSL certificate */
+#ifdef USE_SSL
+ cbind_data = be_tls_get_certificate_hash(state->port,
+ &cbind_data_len);
#endif
}
else
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 1e3e19f5e0..e3e8a535c8 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -1239,6 +1239,67 @@ be_tls_get_peer_finished(Port *port, size_t *len)
return result;
}
+/*
+ * Get the server certificate hash for authentication purposes. Per
+ * RFC 5929 and tls-server-end-point, the TLS server's certificate bytes
+ * need to be hashed with SHA-256 if its signature algorithm is MD5 or
+ * SHA-1 as per RFC 5929 (https://tools.ietf.org/html/rfc5929#section-4.1).
+ * If something else is used, the same hash as the signature algorithm is
+ * used. The result is a palloc'd hash of the server certificate with its
+ * size, and NULL if there is no certificate available.
+ */
+char *
+be_tls_get_certificate_hash(Port *port, size_t *len)
+{
+ char *cert_hash = NULL;
+ X509 *server_cert;
+
+ *len = 0;
+ server_cert = SSL_get_certificate(port->ssl);
+
+ if (server_cert != NULL)
+ {
+ const EVP_MD *algo_type = NULL;
+ char hash[EVP_MAX_MD_SIZE]; /* size for SHA-512 */
+ unsigned int hash_size;
+ int algo_nid;
+
+ /*
+ * Get the signature algorithm of the certificate to determine the
+ * hash algorithm to use for the result.
+ */
+ if (!OBJ_find_sigid_algs(X509_get_signature_nid(server_cert),
+ &algo_nid, NULL))
+ elog(ERROR, "could not find signature algorithm");
+
+ switch (algo_nid)
+ {
+ case NID_md5:
+ case NID_sha1:
+ algo_type = EVP_sha256();
+ break;
+
+ default:
+ algo_type = EVP_get_digestbynid(algo_nid);
+ if (algo_type == NULL)
+ elog(ERROR, "could not find digest for NID %s",
+ OBJ_nid2sn(algo_nid));
+ break;
+ }
+
+ /* generate and save the certificate hash */
+ if (!X509_digest(server_cert, algo_type, (unsigned char *) hash,
+ &hash_size))
+ elog(ERROR, "could not generate server certificate hash");
+
+ cert_hash = (char *) palloc(hash_size);
+ memcpy(cert_hash, hash, hash_size);
+ *len = hash_size;
+ }
+
+ return cert_hash;
+}
+
/*
* Convert an X509 subject name to a cstring.
*
diff --git a/src/include/common/scram-common.h b/src/include/common/scram-common.h
index 857a60e71f..5aec5cadb8 100644
--- a/src/include/common/scram-common.h
+++ b/src/include/common/scram-common.h
@@ -21,6 +21,7 @@
/* Channel binding types */
#define SCRAM_CHANNEL_BINDING_TLS_UNIQUE "tls-unique"
+#define SCRAM_CHANNEL_BINDING_TLS_ENDPOINT "tls-server-end-point"
/* Length of SCRAM keys (client and server) */
#define SCRAM_KEY_LEN PG_SHA256_DIGEST_LENGTH
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 856e0439d5..cf9d8b7870 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -210,6 +210,7 @@ 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_peerdn_name(Port *port, char *ptr, size_t len);
extern char *be_tls_get_peer_finished(Port *port, size_t *len);
+extern char *be_tls_get_certificate_hash(Port *port, size_t *len);
#endif
extern ProtocolVersion FrontendProtocol;
diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c
index e8fc33c72f..65817411b1 100644
--- a/src/interfaces/libpq/fe-auth-scram.c
+++ b/src/interfaces/libpq/fe-auth-scram.c
@@ -446,6 +446,21 @@ build_client_final_message(fe_scram_state *state)
cbind_data = pgtls_get_finished(state->conn, &cbind_data_len);
if (cbind_data == NULL)
goto oom_error;
+#endif
+ }
+ else if (strcmp(conn->scram_channel_binding,
+ SCRAM_CHANNEL_BINDING_TLS_ENDPOINT) == 0)
+ {
+ /* Fetch hash data of server's SSL certificate */
+#ifdef USE_SSL
+ cbind_data =
+ pgtls_get_peer_certificate_hash(state->conn,
+ &cbind_data_len);
+ if (cbind_data == NULL)
+ {
+ /* error message is already set on error */
+ return NULL;
+ }
#endif
}
else
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 61d161b367..99077c3d9a 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -419,6 +419,84 @@ pgtls_get_finished(PGconn *conn, size_t *len)
return result;
}
+/*
+ * Get the hash of the server certificate
+ *
+ * This information is useful for end-point channel binding, where the
+ * client certificate hash is used as a link, per RFC 5929. If the
+ * signature hash algorithm is MD5 or SHA-1, fall back to SHA-256,
+ * as per RFC 5929 (https://tools.ietf.org/html/rfc5929#section-4.1).
+ * NULL is sent back to the caller in the event of an error, with an
+ * error message for the caller to consume.
+ */
+char *
+pgtls_get_peer_certificate_hash(PGconn *conn, size_t *len)
+{
+ char *cert_hash = NULL;
+
+ *len = 0;
+
+ if (conn->peer)
+ {
+ X509 *peer_cert = conn->peer;
+ const EVP_MD *algo_type = NULL;
+ char hash[EVP_MAX_MD_SIZE]; /* size for SHA-512 */
+ unsigned int hash_size;
+ int algo_nid;
+
+ /*
+ * Get the signature algorithm of the certificate to determine the
+ * hash algorithm to use for the result.
+ */
+ if (!OBJ_find_sigid_algs(X509_get_signature_nid(peer_cert),
+ &algo_nid, NULL))
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("could not find signature algorithm\n"));
+ return NULL;
+ }
+
+ switch (algo_nid)
+ {
+ case NID_md5:
+ case NID_sha1:
+ algo_type = EVP_sha256();
+ break;
+
+ default:
+ algo_type = EVP_get_digestbynid(algo_nid);
+ if (algo_type == NULL)
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("could not find digest for NID %s\n"),
+ OBJ_nid2sn(algo_nid));
+ return NULL;
+ }
+ break;
+ }
+
+ if (!X509_digest(peer_cert, algo_type, (unsigned char *) hash,
+ &hash_size))
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("could not generate peer certificate hash\n"));
+ return NULL;
+ }
+
+ /* save result */
+ cert_hash = (char *) malloc(hash_size);
+ if (cert_hash == NULL)
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("out of memory\n"));
+ return NULL;
+ }
+ memcpy(cert_hash, hash, hash_size);
+ *len = hash_size;
+ }
+
+ return cert_hash;
+}
/* ------------------------------------------------------------ */
/* OpenSSL specific code */
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index f6c1023f37..756c4d61e1 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -672,6 +672,7 @@ extern ssize_t pgtls_read(PGconn *conn, void *ptr, size_t len);
extern bool pgtls_read_pending(PGconn *conn);
extern ssize_t pgtls_write(PGconn *conn, const void *ptr, size_t len);
extern char *pgtls_get_finished(PGconn *conn, size_t *len);
+extern char *pgtls_get_peer_certificate_hash(PGconn *conn, size_t *len);
/*
* this is so that we can check if a connection is non-blocking internally
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 324b4888d4..3f425e00f0 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -4,7 +4,7 @@ use strict;
use warnings;
use PostgresNode;
use TestLib;
-use Test::More tests => 4;
+use Test::More tests => 5;
use ServerSetup;
use File::Copy;
@@ -45,6 +45,9 @@ test_connect_ok($common_connstr,
test_connect_ok($common_connstr,
"scram_channel_binding=''",
"SCRAM authentication without channel binding");
+test_connect_ok($common_connstr,
+ "scram_channel_binding=tls-server-end-point",
+ "SCRAM authentication with tls-server-end-point as channel binding");
test_connect_fails($common_connstr,
"scram_channel_binding=not-exists",
"SCRAM authentication with invalid channel binding");
--
2.15.1
On 12/28/17 02:19, Michael Paquier wrote:
On Wed, Dec 27, 2017 at 09:27:40AM +0900, Michael Paquier wrote:
On Tue, Dec 26, 2017 at 03:28:09PM -0500, Peter Eisentraut wrote:
On 12/22/17 03:10, Michael Paquier wrote:
Second thoughts on 0002 as there is actually no need to move around
errorMessage if the PGconn* pointer is saved in the SCRAM status data
as both are linked. The attached simplifies the logic even more.That all looks pretty reasonable.
Thanks for the review. Don't you think that the the refactoring
simplifications should be done first though? This would result in
producing the patch set in reverse order. I'll be fine to produce them
if need be.Well, here is a patch set doing the reverse operation: refactoring does
first in 0001 and support for tls-server-end-point is in 0002. Hope this
helps.
committed
I reorganized the be_tls_get_certificate_hash() and
pgtls_get_peer_certificate_hash() functions a bit to not have most of
the code in a big if statement.
--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
On 1/4/18 15:41, Peter Eisentraut wrote:
On 12/28/17 02:19, Michael Paquier wrote:
On Wed, Dec 27, 2017 at 09:27:40AM +0900, Michael Paquier wrote:
On Tue, Dec 26, 2017 at 03:28:09PM -0500, Peter Eisentraut wrote:
On 12/22/17 03:10, Michael Paquier wrote:
Second thoughts on 0002 as there is actually no need to move around
errorMessage if the PGconn* pointer is saved in the SCRAM status data
as both are linked. The attached simplifies the logic even more.That all looks pretty reasonable.
Thanks for the review. Don't you think that the the refactoring
simplifications should be done first though? This would result in
producing the patch set in reverse order. I'll be fine to produce them
if need be.Well, here is a patch set doing the reverse operation: refactoring does
first in 0001 and support for tls-server-end-point is in 0002. Hope this
helps.committed
Some hosts don't seem to have X509_get_signature_nid(). Looking into
that ...
--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
Peter Eisentraut <peter.eisentraut@2ndquadrant.com> writes:
Some hosts don't seem to have X509_get_signature_nid(). Looking into
that ...
dromedary is whinging about OBJ_find_sigid_algs, as well.
regards, tom lane
On 1/4/18 16:17, Tom Lane wrote:
Peter Eisentraut <peter.eisentraut@2ndquadrant.com> writes:
Some hosts don't seem to have X509_get_signature_nid(). Looking into
that ...dromedary is whinging about OBJ_find_sigid_algs, as well.
Yeah, it seems like we might need to fine-tune this a bit more to make
it work across all OpenSSL versions. I'm going to let the buildfarm
take a run through the current code and see what other issues arise.
--
Peter Eisentraut http://www.2ndQuadrant.com/
PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
Peter Eisentraut <peter.eisentraut@2ndquadrant.com> writes:
On 1/4/18 16:17, Tom Lane wrote:
dromedary is whinging about OBJ_find_sigid_algs, as well.
Yeah, it seems like we might need to fine-tune this a bit more to make
it work across all OpenSSL versions. I'm going to let the buildfarm
take a run through the current code and see what other issues arise.
Well, it looks like the older machines don't like OBJ_find_sigid_algs,
and the newest machines don't like what you're trying to pass to it:
/home/andres/build/buildfarm-calliphoridae/HEAD/pgsql.build/../pgsql/src/backend/libpq/be-secure-openssl.c: In function ‘be_tls_get_certificate_hash’:
/home/andres/build/buildfarm-calliphoridae/HEAD/pgsql.build/../pgsql/src/backend/libpq/be-secure-openssl.c:1268:50: error: dereferencing pointer to incomplete type ‘X509 {aka struct x509_st}’
if (!OBJ_find_sigid_algs(OBJ_obj2nid(server_cert->sig_alg->algorithm),
^~
so this is looking mighty like a crashed and burned patch from here :-(
regards, tom lane
On Fri, Jan 5, 2018 at 7:12 AM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
so this is looking mighty like a crashed and burned patch from here :-(
Sorry for arriving late to the party, timezone and such..
The lack of access to the signature algorithm type is being covered by
this commit from upstream which introduced X509_get_signature_nid():
commit: dfcf48f499f19fd17a3aee03151ea301814ea6ec
author: Dr. Stephen Henson <steve@openssl.org>
date: Wed, 13 Jun 2012 13:08:12 +0000
New functions to retrieve certificate signatures and signature OID NID.
So any versions of OpenSSL older than 1.0.1 included would not compile
on that. There is only X509_get_signature_type() before that, but this
returns the signature type, and that's the hashing type we are looking
for here. RFC 5929, which defines the channel binding types, is from
July 2010. I have not checked the OpenSSL threads, but I would bet a
nickel that one of the reasons why X509_get_signature_nid() has been
introduced is to support cases similar to tls-server-end-point where
you want to know what's the hash function used for a certificate.
That's my fault at the end, my apologies. I can reproduce manually the
compilation failure of this code when compiling by myself past
versions of OpenSSL. So I think that 054e8c6c is doing the right move.
Thanks Peter and all others involved.
--
Michael