On Fri, 2019-09-20 at 13:07 +0900, Michael Paquier wrote: > Those are mainly nits, and attached are the changes I would do to > your > patch. Please feel free to consider those changes as you see fit. > Anyway, the base logic looks good to me, so I am switching the patch > as ready for committer.
Thank you, applied. * I also changed the comment above pg_fe_scram_channel_bound() to clarify that the caller must also check that the exchange was successful. * I changed the error message when AUTH_REQ_OK is received without channel binding. It seemed confusing before. I also added a test. Regards, Jeff Davis
From 1266d7bb6c46aa85b3c48ee271115e5ce6f4bad0 Mon Sep 17 00:00:00 2001 From: Jeff Davis <j...@j-davis.com> Date: Tue, 20 Aug 2019 18:59:23 -0700 Subject: [PATCH] Add libpq parameter 'channel_binding'. Allow clients to require channel binding to enhance security against untrusted servers. Author: Jeff Davis Discussion: https://postgr.es/m/227015d8417f2b4fef03f8966dbfa5cbcc4f44da.camel%40j-davis.com --- doc/src/sgml/libpq.sgml | 32 ++++++++++ src/interfaces/libpq/fe-auth-scram.c | 41 ++++++++++-- src/interfaces/libpq/fe-auth.c | 76 +++++++++++++++++++++-- src/interfaces/libpq/fe-auth.h | 1 + src/interfaces/libpq/fe-connect.c | 41 +++++++++++- src/interfaces/libpq/libpq-int.h | 2 + src/test/authentication/t/001_password.pl | 12 +++- src/test/ssl/t/002_scram.pl | 39 +++++++++++- src/test/ssl/t/SSLServer.pm | 9 ++- 9 files changed, 233 insertions(+), 20 deletions(-) diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml index 1189341ca15..c58527b0c3b 100644 --- a/doc/src/sgml/libpq.sgml +++ b/doc/src/sgml/libpq.sgml @@ -1122,6 +1122,28 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname </listitem> </varlistentry> + <varlistentry id="libpq-connect-channel-binding" xreflabel="channel_binding"> + <term><literal>channel_binding</literal></term> + <listitem> + <para> + This option controls the client's use of channel binding. A setting + of <literal>require</literal> means that the connection must employ + channel binding, <literal>prefer</literal> means that the client will + choose channel binding if available, and <literal>disable</literal> + prevents the use of channel binding. The default + is <literal>prefer</literal> if + <productname>PostgreSQL</productname> is compiled with SSL support; + otherwise the default is <literal>disable</literal>. + </para> + <para> + Channel binding is a method for the server to authenticate itself to + the client. It is only supported over SSL connections + with <productname>PostgreSQL</productname> 11 or later servers using + the <literal>SCRAM</literal> authentication method. + </para> + </listitem> + </varlistentry> + <varlistentry id="libpq-connect-connect-timeout" xreflabel="connect_timeout"> <term><literal>connect_timeout</literal></term> <listitem> @@ -6864,6 +6886,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough) </para> </listitem> + <listitem> + <para> + <indexterm> + <primary><envar>PGCHANNELBINDING</envar></primary> + </indexterm> + <envar>PGCHANNELBINDING</envar> behaves the same as the <xref + linkend="libpq-connect-channel-binding"/> connection parameter. + </para> + </listitem> + <listitem> <para> <indexterm> diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c index 7a8335bf9f8..03f42f06030 100644 --- a/src/interfaces/libpq/fe-auth-scram.c +++ b/src/interfaces/libpq/fe-auth-scram.c @@ -119,6 +119,35 @@ pg_fe_scram_init(PGconn *conn, return state; } +/* + * Return true if channel binding was employed and the scram exchange + * completed. This should be used after a successful exchange to determine + * whether the server authenticated itself to the client. + * + * Note that the caller must also ensure that the exchange was actually + * successful. + */ +bool +pg_fe_scram_channel_bound(void *opaq) +{ + fe_scram_state *state = (fe_scram_state *) opaq; + + /* no SCRAM exchange done */ + if (state == NULL) + return false; + + /* SCRAM exchange not completed */ + if (state->state != FE_SCRAM_FINISHED) + return false; + + /* channel binding mechanism not used */ + if (strcmp(state->sasl_mechanism, SCRAM_SHA_256_PLUS_NAME) != 0) + return false; + + /* all clear! */ + return true; +} + /* * Free SCRAM exchange status */ @@ -225,9 +254,7 @@ pg_fe_scram_exchange(void *opaq, char *input, int inputlen, /* * Verify server signature, to make sure we're talking to the - * genuine server. XXX: A fake server could simply not require - * authentication, though. There is currently no option in libpq - * to reject a connection, if SCRAM authentication did not happen. + * genuine server. */ if (verify_server_signature(state)) *success = true; @@ -358,7 +385,8 @@ build_client_first_message(fe_scram_state *state) appendPQExpBufferStr(&buf, "p=tls-server-end-point"); } #ifdef HAVE_PGTLS_GET_PEER_CERTIFICATE_HASH - else if (conn->ssl_in_use) + else if (conn->channel_binding[0] != 'd' && /* disable */ + conn->ssl_in_use) { /* * Client supports channel binding, but thinks the server does not. @@ -369,7 +397,7 @@ build_client_first_message(fe_scram_state *state) else { /* - * Client does not support channel binding. + * Client does not support channel binding, or has disabled it. */ appendPQExpBufferChar(&buf, 'n'); } @@ -498,7 +526,8 @@ build_client_final_message(fe_scram_state *state) #endif /* HAVE_PGTLS_GET_PEER_CERTIFICATE_HASH */ } #ifdef HAVE_PGTLS_GET_PEER_CERTIFICATE_HASH - else if (conn->ssl_in_use) + else if (conn->channel_binding[0] != 'd' && /* disable */ + conn->ssl_in_use) appendPQExpBufferStr(&buf, "c=eSws"); /* base64 of "y,," */ #endif else diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c index ab227421b3b..cd29e8bd126 100644 --- a/src/interfaces/libpq/fe-auth.c +++ b/src/interfaces/libpq/fe-auth.c @@ -423,6 +423,14 @@ pg_SASL_init(PGconn *conn, int payloadlen) initPQExpBuffer(&mechanism_buf); + if (conn->channel_binding[0] == 'r' && /* require */ + !conn->ssl_in_use) + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("Channel binding required, but SSL not in use\n")); + goto error; + } + if (conn->sasl_state) { printfPQExpBuffer(&conn->errorMessage, @@ -454,10 +462,10 @@ pg_SASL_init(PGconn *conn, int payloadlen) /* * Select the mechanism to use. Pick SCRAM-SHA-256-PLUS over anything - * else if a channel binding type is set and if the client supports - * it. 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 set and if the client supports it + * (and did not set channel_binding=disable). 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 (strcmp(mechanism_buf.data, SCRAM_SHA_256_PLUS_NAME) == 0) { @@ -466,10 +474,11 @@ pg_SASL_init(PGconn *conn, int payloadlen) /* * The server has offered SCRAM-SHA-256-PLUS, which is only * supported by the client if a hash of the peer certificate - * can be created. + * can be created, and if channel_binding is not disabled. */ #ifdef HAVE_PGTLS_GET_PEER_CERTIFICATE_HASH - selected_mechanism = SCRAM_SHA_256_PLUS_NAME; + if (conn->channel_binding[0] != 'd') /* disable */ + selected_mechanism = SCRAM_SHA_256_PLUS_NAME; #endif } else @@ -493,6 +502,14 @@ pg_SASL_init(PGconn *conn, int payloadlen) selected_mechanism = SCRAM_SHA_256_NAME; } + if (conn->channel_binding[0] == 'r' && /* require */ + strcmp(selected_mechanism, SCRAM_SHA_256_PLUS_NAME) != 0) + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("channel binding is required, but server did not offer an authentication method that supports channel binding\n")); + goto error; + } + if (!selected_mechanism) { printfPQExpBuffer(&conn->errorMessage, @@ -774,6 +791,50 @@ pg_password_sendauth(PGconn *conn, const char *password, AuthRequest areq) return ret; } +/* + * Verify that the authentication request is expected, given the connection + * parameters. This is especially important when the client wishes to + * authenticate the server before any sensitive information is exchanged. + */ +static bool +check_expected_areq(AuthRequest areq, PGconn *conn) +{ + bool result = true; + + /* + * When channel_binding=require, we must protect against two cases: (1) we + * must not respond to non-SASL authentication requests, which might leak + * information such as the client's password; and (2) even if we receive + * AUTH_REQ_OK, we still must ensure that channel binding has happened in + * order to authenticate the server. + */ + if (conn->channel_binding[0] == 'r' /* require */ ) + { + switch (areq) + { + case AUTH_REQ_SASL: + case AUTH_REQ_SASL_CONT: + case AUTH_REQ_SASL_FIN: + break; + case AUTH_REQ_OK: + if (!pg_fe_scram_channel_bound(conn->sasl_state)) + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("Channel binding required, but server authenticated client without channel binding\n")); + result = false; + } + break; + default: + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("Channel binding required but not supported by server's authentication request\n")); + result = false; + break; + } + } + + return result; +} + /* * pg_fe_sendauth * client demux routine for processing an authentication request @@ -788,6 +849,9 @@ pg_password_sendauth(PGconn *conn, const char *password, AuthRequest areq) int pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn) { + if (!check_expected_areq(areq, conn)) + return STATUS_ERROR; + switch (areq) { case AUTH_REQ_OK: diff --git a/src/interfaces/libpq/fe-auth.h b/src/interfaces/libpq/fe-auth.h index 122ba5ccbaf..2f1af53fb08 100644 --- a/src/interfaces/libpq/fe-auth.h +++ b/src/interfaces/libpq/fe-auth.h @@ -26,6 +26,7 @@ extern char *pg_fe_getauthname(PQExpBuffer errorMessage); extern void *pg_fe_scram_init(PGconn *conn, const char *password, const char *sasl_mechanism); +extern bool pg_fe_scram_channel_bound(void *opaq); 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 8ba0159313c..f91f0f2efe7 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -124,6 +124,11 @@ static int ldapServiceLookup(const char *purl, PQconninfoOption *options, #define DefaultTty "" #define DefaultOption "" #define DefaultAuthtype "" +#ifdef USE_SSL +#define DefaultChannelBinding "prefer" +#else +#define DefaultChannelBinding "disable" +#endif #define DefaultTargetSessionAttrs "any" #ifdef USE_SSL #define DefaultSSLMode "prefer" @@ -211,6 +216,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = { "Database-Password-File", "", 64, offsetof(struct pg_conn, pgpassfile)}, + {"channel_binding", "PGCHANNELBINDING", NULL, NULL, + "Channel-Binding", "", 7, /* sizeof("require") */ + offsetof(struct pg_conn, channel_binding)}, + {"connect_timeout", "PGCONNECT_TIMEOUT", NULL, NULL, "Connect-timeout", "", 10, /* strlen(INT32_MAX) == 10 */ offsetof(struct pg_conn, connect_timeout)}, @@ -1197,6 +1206,29 @@ connectOptions2(PGconn *conn) } } + /* + * validate channel_binding option + */ + if (conn->channel_binding) + { + if (strcmp(conn->channel_binding, "disable") != 0 + && strcmp(conn->channel_binding, "prefer") != 0 + && strcmp(conn->channel_binding, "require") != 0) + { + conn->status = CONNECTION_BAD; + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("invalid channel_binding value: \"%s\"\n"), + conn->channel_binding); + return false; + } + } + else + { + conn->channel_binding = strdup(DefaultChannelBinding); + if (!conn->channel_binding) + goto oom_error; + } + /* * validate sslmode option */ @@ -3485,10 +3517,11 @@ keep_going: /* We will come back to here until there is case CONNECTION_SETENV: { /* - * Do post-connection housekeeping (only needed in protocol 2.0). + * Do post-connection housekeeping (only needed in protocol + * 2.0). * - * We pretend that the connection is OK for the duration of these - * queries. + * We pretend that the connection is OK for the duration of + * these queries. */ conn->status = CONNECTION_OK; @@ -3905,6 +3938,8 @@ freePGconn(PGconn *conn) } if (conn->pgpassfile) free(conn->pgpassfile); + if (conn->channel_binding) + free(conn->channel_binding); if (conn->keepalives) free(conn->keepalives); if (conn->keepalives_idle) diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index d37bb3ce404..64468ab4dab 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -347,6 +347,8 @@ struct pg_conn char *pguser; /* Postgres username and password, if any */ char *pgpass; char *pgpassfile; /* path to a file containing password(s) */ + char *channel_binding; /* channel binding mode + * (require,prefer,disable) */ char *keepalives; /* use TCP keepalives? */ char *keepalives_idle; /* time between TCP keepalives */ char *keepalives_interval; /* time between TCP keepalive diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl index 3a3b0eb7e80..aae6de8b345 100644 --- a/src/test/authentication/t/001_password.pl +++ b/src/test/authentication/t/001_password.pl @@ -17,7 +17,7 @@ if ($windows_os) } else { - plan tests => 8; + plan tests => 10; } @@ -86,3 +86,13 @@ test_role($node, 'md5_role', 'scram-sha-256', 2); reset_pg_hba($node, 'md5'); test_role($node, 'scram_role', 'md5', 0); test_role($node, 'md5_role', 'md5', 0); + +# Tests for channel binding without SSL. +# Using the password authentication method; channel binding can't work +reset_pg_hba($node, 'password'); +$ENV{"PGCHANNELBINDING"} = 'require'; +test_role($node, 'scram_role', 'scram-sha-256', 2); +# SSL not in use; channel binding still can't work +reset_pg_hba($node, 'scram-sha-256'); +$ENV{"PGCHANNELBINDING"} = 'require'; +test_role($node, 'scram_role', 'scram-sha-256', 2); diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl index 7c4b821cb78..5fa2dbde1c1 100644 --- a/src/test/ssl/t/002_scram.pl +++ b/src/test/ssl/t/002_scram.pl @@ -18,7 +18,7 @@ if ($ENV{with_openssl} ne 'yes') plan skip_all => 'SSL not supported by this build'; } -my $number_of_tests = 1; +my $number_of_tests = 9; # This is the hostname used to connect to the server. my $SERVERHOSTADDR = '127.0.0.1'; @@ -44,9 +44,42 @@ configure_test_server_for_ssl($node, $SERVERHOSTADDR, "scram-sha-256", switch_server_cert($node, 'server-cn-only'); $ENV{PGPASSWORD} = "pass"; $common_connstr = - "user=ssltestuser dbname=trustdb sslmode=require sslcert=invalid sslrootcert=invalid hostaddr=$SERVERHOSTADDR"; + "dbname=trustdb sslmode=require sslcert=invalid sslrootcert=invalid hostaddr=$SERVERHOSTADDR"; # Default settings -test_connect_ok($common_connstr, '', "Basic SCRAM authentication with SSL"); +test_connect_ok($common_connstr, "user=ssltestuser", + "Basic SCRAM authentication with SSL"); + +# Test channel_binding +test_connect_fails( + $common_connstr, + "user=ssltestuser channel_binding=invalid_value", + qr/invalid channel_binding value: "invalid_value"/, + "SCRAM with SSL and channel_binding=invalid_value"); +test_connect_ok( + $common_connstr, + "user=ssltestuser channel_binding=disable", + "SCRAM with SSL and channel_binding=disable"); +test_connect_ok( + $common_connstr, + "user=ssltestuser channel_binding=require", + "SCRAM with SSL and channel_binding=require"); + +# Now test when the user has an MD5-encrypted password; should fail +test_connect_fails( + $common_connstr, + "user=md5testuser channel_binding=require", + qr/Channel binding required but not supported by server's authentication request/, + "MD5 with SSL and channel_binding=require"); + +# Now test with auth method 'cert' by connecting to 'certdb'. Should +# fail, because channel binding is not performed. +copy("ssl/client.key", "ssl/client_tmp.key"); +chmod 0600, "ssl/client_tmp.key"; +test_connect_fails( + "sslcert=ssl/client.crt sslkey=ssl/client_tmp.key hostaddr=$SERVERHOSTADDR", + "dbname=certdb user=ssltestuser channel_binding=require", + qr/Channel binding required, but server authenticated client without channel binding/, + "Cert authentication and channel_binding=require"); done_testing($number_of_tests); diff --git a/src/test/ssl/t/SSLServer.pm b/src/test/ssl/t/SSLServer.pm index d25c38dbbc7..005955a2ff7 100644 --- a/src/test/ssl/t/SSLServer.pm +++ b/src/test/ssl/t/SSLServer.pm @@ -102,6 +102,7 @@ sub configure_test_server_for_ssl # Create test users and databases $node->psql('postgres', "CREATE USER ssltestuser"); + $node->psql('postgres', "CREATE USER md5testuser"); $node->psql('postgres', "CREATE USER anotheruser"); $node->psql('postgres', "CREATE USER yetanotheruser"); $node->psql('postgres', "CREATE DATABASE trustdb"); @@ -114,6 +115,10 @@ sub configure_test_server_for_ssl $node->psql('postgres', "SET password_encryption='$password_enc'; ALTER USER ssltestuser PASSWORD '$password';" ); + # A special user that always has an md5-encrypted password + $node->psql('postgres', + "SET password_encryption='md5'; ALTER USER md5testuser PASSWORD '$password';" + ); $node->psql('postgres', "SET password_encryption='$password_enc'; ALTER USER anotheruser PASSWORD '$password';" ); @@ -128,7 +133,7 @@ sub configure_test_server_for_ssl print $conf "log_statement=all\n"; # enable SSL and set up server key - print $conf "include 'sslconfig.conf'"; + print $conf "include 'sslconfig.conf'\n"; close $conf; @@ -186,6 +191,8 @@ sub configure_hba_for_ssl open my $hba, '>', "$pgdata/pg_hba.conf"; print $hba "# TYPE DATABASE USER ADDRESS METHOD OPTIONS\n"; + print $hba + "hostssl trustdb md5testuser $serverhost/32 md5\n"; print $hba "hostssl trustdb all $serverhost/32 $authmethod\n"; print $hba -- 2.17.1