v2 rebases over latest, removes the alternate spellings of "password", and implements OR operations with a comma-separated list. For example:
- require_auth=cert means that the server must ask for, and the client must provide, a client certificate. - require_auth=password,md5 means that the server must ask for a plaintext password or an MD5 hash. - require_auth=scram-sha-256,gss means that one of SCRAM, Kerberos authentication, or GSS transport encryption must be successfully negotiated. - require_auth=scram-sha-256,cert means that either a SCRAM handshake must be completed, or the server must request a client certificate. It has a potential pitfall in that it allows a partial SCRAM handshake, as long as a certificate is requested and sent. AND and NOT, discussed upthread, are not yet implemented. I tied myself up in knots trying to make AND generic, so I think I"m going to tackle NOT first, instead. The problem with AND is that it only makes sense when one (and only one) of the options is a form of transport authentication. (E.g. password+md5 never makes sense.) And although cert+<something> and gss+<something> could be useful, the latter case is already handled by gssencmode=require, and the gssencmode option is more powerful since you can disable it (or set it to don't-care). I'm not generally happy with how the "cert" option is working. With the other methods, if you don't include a method in the list, then the connection fails if the server tries to negotiate it. But if you don't include the cert method in the list, we don't forbid the server from asking for a cert, because the server always asks for a client certificate via TLS whether it needs one or not. Behaving in the intuitive way here would effectively break all use of TLS. So I think Tom's recommendation that the cert method be handled by an orthogonal option was a good one, and if that works then maybe we don't need an AND syntax at all. Presumably I can just add an option that parallels gssencmode, and then the current don't-care semantics can be explicitly controlled. Skipping AND also means that I don't have to create a syntax that can handle AND and NOT at the same time, which I was dreading. --Jacob
commit 403d27e07922babcdf6fd5e8ad8524d92811294c Author: Jacob Champion <jchamp...@timescale.com> Date: Mon Jun 6 12:32:03 2022 -0700 squash! libpq: let client reject unexpected auth methods - rebase over new sslkey() test function - remove alternate spellings of "password" - allow comma-separated methods (OR). At least one of the authentication methods in the list must complete for the connection to succeed. diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c index a883959756..04f9bf4831 100644 --- a/src/interfaces/libpq/fe-auth.c +++ b/src/interfaces/libpq/fe-auth.c @@ -870,6 +870,13 @@ auth_description(AuthRequest areq) return libpq_gettext("an unknown authentication type"); } +/* + * Convenience macro for checking the allowed_auth_methods bitmask. Caller must + * ensure that type is not greater than 31 (high bit of the bitmask). + */ +#define auth_allowed(conn, type) \ + (((conn)->allowed_auth_methods & (1 << (type))) != 0) + /* * Verify that the authentication request is expected, given the connection * parameters. This is especially important when the client wishes to @@ -881,7 +888,11 @@ check_expected_areq(AuthRequest areq, PGconn *conn) bool result = true; char *reason = NULL; - /* If the user required a specific auth method, reject all others. */ + /* + * If the user required a specific auth method, or specified an allowed set, + * then reject all others here, and make sure the server actually completes + * an authentication exchange. + */ if (conn->require_auth) { switch (areq) @@ -890,21 +901,43 @@ check_expected_areq(AuthRequest areq, PGconn *conn) /* * Check to make sure we've actually finished our exchange. */ - if (strcmp(conn->require_auth, "cert") == 0) + if (conn->client_finished_auth) + break; + + /* + * No explicit authentication request was made by the server -- + * or perhaps it was made and not completed, in the case of + * SCRAM -- but there are two special cases to check: + * + * 1) If the user allowed "cert", then as long as we sent a + * client certificate to the server in response to its + * TLS CertificateRequest, this check is satisfied. + * + * 2) If the user allowed "gss", then a GSS-encrypted channel + * also satisfies the check. + */ + if (conn->allowed_auth_method_cert) { + /* + * Trade off a little bit of complexity to try to get these + * error messages as precise as possible. + */ if (!conn->ssl_cert_requested) { - reason = libpq_gettext("server did not request a certificate"); + reason = conn->allowed_auth_methods + ? libpq_gettext("server did not complete authentication or request a certificate") + : libpq_gettext("server did not request a certificate"); result = false; } else if (!conn->ssl_cert_sent) { - reason = libpq_gettext("server accepted connection without a valid certificate"); + reason = conn->allowed_auth_methods + ? libpq_gettext("server did not complete authentication and accepted connection without a valid certificate") + : libpq_gettext("server accepted connection without a valid certificate"); result = false; } } - else if (strcmp(conn->require_auth, "gss") == 0 - && conn->gssenc) + else if (auth_allowed(conn, AUTH_REQ_GSS) && conn->gssenc) { /* * Special case: if implicit GSS auth has already been @@ -915,49 +948,28 @@ check_expected_areq(AuthRequest areq, PGconn *conn) * are made in this case? */ } - else if (!conn->client_finished_auth) + else { reason = libpq_gettext("server did not complete authentication"), result = false; } - break; - case AUTH_REQ_PASSWORD: - if (strcmp(conn->require_auth, "bsd") - && strcmp(conn->require_auth, "ldap") - && strcmp(conn->require_auth, "pam") - && strcmp(conn->require_auth, "password") - && strcmp(conn->require_auth, "radius")) - result = false; break; + case AUTH_REQ_PASSWORD: case AUTH_REQ_MD5: - if (strcmp(conn->require_auth, "md5")) - result = false; - break; - case AUTH_REQ_GSS: - if (strcmp(conn->require_auth, "gss")) - result = false; - break; - case AUTH_REQ_SSPI: - if (strcmp(conn->require_auth, "sspi")) - result = false; - break; - case AUTH_REQ_GSS_CONT: - if (strcmp(conn->require_auth, "gss") && - strcmp(conn->require_auth, "sspi")) - result = false; - break; - case AUTH_REQ_SASL: case AUTH_REQ_SASL_CONT: case AUTH_REQ_SASL_FIN: - /* This currently assumes that SCRAM is the only SASL method. */ - if (strcmp(conn->require_auth, "scram-sha-256")) - result = false; + /* + * We don't handle these with the default case, to avoid + * bit-shifting past the end of the allowed_auth_methods mask if + * the server sends an unexpected AuthRequest. + */ + result = auth_allowed(conn, areq); break; default: diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 682f363c2b..c650687c9b 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -1259,6 +1259,93 @@ connectOptions2(PGconn *conn) } } + /* + * parse and validate require_auth option + */ + if (conn->require_auth) + { + char *s = conn->require_auth; + bool more = true; + + while (more) + { + char *method; + uint32 bits; + + method = parse_comma_separated_list(&s, &more); + if (method == NULL) + goto oom_error; + + if (strcmp(method, "password") == 0) + { + bits = (1 << AUTH_REQ_PASSWORD); + } + else if (strcmp(method, "md5") == 0) + { + bits = (1 << AUTH_REQ_MD5); + } + else if (strcmp(method, "gss") == 0) + { + bits = (1 << AUTH_REQ_GSS); + bits |= (1 << AUTH_REQ_GSS_CONT); + } + else if (strcmp(method, "sspi") == 0) + { + bits = (1 << AUTH_REQ_SSPI); + bits |= (1 << AUTH_REQ_GSS_CONT); + } + else if (strcmp(method, "scram-sha-256") == 0) + { + /* This currently assumes that SCRAM is the only SASL method. */ + bits = (1 << AUTH_REQ_SASL); + bits |= (1 << AUTH_REQ_SASL_CONT); + bits |= (1 << AUTH_REQ_SASL_FIN); + } + else if (strcmp(method, "cert") == 0) + { + /* + * Special case: there is no AUTH_REQ code for certificate + * authentication since it's done as part of the TLS handshake. + * Make a note in conn, for check_expected_areq() to see later. + */ + if (conn->allowed_auth_method_cert) + { + conn->status = CONNECTION_BAD; + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("require_auth method \"%s\" is specified more than once"), + method); + return false; + } + + conn->allowed_auth_method_cert = true; + continue; /* avoid the bitmask manipulation below */ + } + else + { + conn->status = CONNECTION_BAD; + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("invalid require_auth method: \"%s\""), + method); + return false; + } + + /* + * Sanity check; a duplicated method probably indicates a typo in a + * setting where typos are extremely risky. + */ + if ((conn->allowed_auth_methods & bits) == bits) + { + conn->status = CONNECTION_BAD; + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("require_auth method \"%s\" is specified more than once"), + method); + return false; + } + + conn->allowed_auth_methods |= bits; + } + } + /* * validate channel_binding option */ diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index b7701c3683..62554fedde 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -455,6 +455,9 @@ struct pg_conn bool write_failed; /* have we had a write failure on sock? */ char *write_err_msg; /* write error message, or NULL if OOM */ + uint32 allowed_auth_methods; /* bitmask of acceptable AuthRequest codes */ + bool allowed_auth_method_cert; /* tracks "cert" which has no AuthRequest code */ + /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ bool try_next_addr; /* time to advance to next address/host? */ diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl index c7062ca357..dab50774c4 100644 --- a/src/test/authentication/t/001_password.pl +++ b/src/test/authentication/t/001_password.pl @@ -95,15 +95,15 @@ $node->connect_fails("user=scram_role require_auth=sspi", $node->connect_fails("user=scram_role require_auth=password", "password authentication can be required: fails with trust auth", expected_stderr => qr/server did not complete authentication/); -$node->connect_fails("user=scram_role require_auth=ldap", - "LDAP authentication can be required: fails with trust auth", - expected_stderr => qr/server did not complete authentication/); $node->connect_fails("user=scram_role require_auth=md5", "md5 authentication can be required: fails with trust auth", expected_stderr => qr/server did not complete authentication/); $node->connect_fails("user=scram_role require_auth=scram-sha-256", "SCRAM authentication can be required: fails with trust auth", expected_stderr => qr/server did not complete authentication/); +$node->connect_fails("user=scram_role require_auth=cert,scram-sha-256", + "multiple authentication types can be required: fails with trust auth", + expected_stderr => qr/server did not complete authentication or request a certificate/); # For plain "password" method, all users should also be able to connect. reset_pg_hba($node, 'password'); @@ -117,6 +117,8 @@ test_role($node, 'md5_role', 'password', 0, # require_auth should succeed with a plaintext password... $node->connect_ok("user=scram_role require_auth=password", "password authentication can be required: works with password auth"); +$node->connect_ok("user=scram_role require_auth=cert,password,md5", + "multiple authentication types can be required: works with password auth"); # ...and fail for other auth types. $node->connect_fails("user=scram_role require_auth=md5", @@ -142,6 +144,8 @@ test_role($node, 'md5_role', 'scram-sha-256', 2, # require_auth should succeed with SCRAM... $node->connect_ok("user=scram_role require_auth=scram-sha-256", "SCRAM authentication can be required: works with SCRAM auth"); +$node->connect_ok("user=scram_role require_auth=password,scram-sha-256,cert", + "multiple authentication types can be required: works with SCRAM auth"); # ...and fail for other auth types. $node->connect_fails("user=scram_role require_auth=password", @@ -170,6 +174,8 @@ test_role($node, 'md5_role', 'md5', 0, # require_auth should succeed with MD5... $node->connect_ok("user=md5_role require_auth=md5", "MD5 authentication can be required: works with MD5 auth"); +$node->connect_ok("user=md5_role require_auth=md5,scram-sha-256,cert", + "multiple authentication types can be required: works with MD5 auth"); # ...and fail for other auth types. $node->connect_fails("user=md5_role require_auth=password", diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl index 84e94e8e81..de4ddcbf69 100644 --- a/src/test/kerberos/t/001_auth.pl +++ b/src/test/kerberos/t/001_auth.pl @@ -353,6 +353,14 @@ test_access( test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable', 'fails with GSS encryption disabled and hostgssenc hba'); +# require_auth=gss should succeed. +$node->connect_ok( + $node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss", + "GSS authentication can be requested: works with GSS encryption"); +$node->connect_ok( + $node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss,scram-sha-256", + "multiple authentication types can be requested: works with GSS encryption"); + unlink($node->data_dir . '/pg_hba.conf'); $node->append_conf('pg_hba.conf', qq{hostnogssenc all all $hostaddr/32 gss map=mymap}); diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl index a6b07051fc..e7c67920a1 100644 --- a/src/test/ldap/t/001_auth.pl +++ b/src/test/ldap/t/001_auth.pl @@ -216,10 +216,7 @@ test_access( qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/ ],); -# require_auth=ldap (and other plaintext password methods) should complete -# successfully; other methods should fail. -$node->connect_ok("user=test1 require_auth=ldap", - "LDAP authentication can be required: works with ldap auth"); +# require_auth=password should complete successfully; other methods should fail. $node->connect_ok("user=test1 require_auth=password", "password authentication can be required: works with ldap auth"); $node->connect_fails("user=test1 require_auth=scram-sha-256", diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl index 10d67eee17..68a888e11a 100644 --- a/src/test/ssl/t/001_ssltests.pl +++ b/src/test/ssl/t/001_ssltests.pl @@ -507,7 +507,8 @@ $common_connstr = # require_auth=cert should succeed against the certdb... $node->connect_ok( - "$common_connstr require_auth=cert user=ssltestuser sslcert=ssl/client.crt sslkey=$key{'client.key'}", + "$common_connstr require_auth=cert user=ssltestuser sslcert=ssl/client.crt " + . sslkey('client.key'), "certificate authentication can be required: works with cert auth"); # ...and fail against the trustdb, if no certificate is provided. @@ -515,6 +516,10 @@ $node->connect_fails( "$common_connstr require_auth=cert dbname=trustdb user=ssltestuser", "certificate authentication can be required: fails with trust auth and no cert", expected_stderr => qr/server accepted connection without a valid certificate/); +$node->connect_fails( + "$common_connstr require_auth=scram-sha-256,cert dbname=trustdb user=ssltestuser", + "multiple authentication types can be required: fails with trust auth and no cert", + expected_stderr => qr/server did not complete authentication and accepted connection without a valid certificate/); # no client cert $node->connect_fails(
From 0a4f31c989f7524116e9ec97ec05db5a0d8ad791 Mon Sep 17 00:00:00 2001 From: Jacob Champion <pchamp...@vmware.com> Date: Mon, 28 Feb 2022 09:40:43 -0800 Subject: [PATCH v2] libpq: let client reject unexpected auth methods The require_auth connection option allows the client to choose a list of acceptable authentication types for use with the server. There is no negotiation: if the server does not present one of the allowed authentication requests, the connection fails. Internally, the patch expands the role of check_expected_areq() to ensure that the incoming request is compatible with conn->require_auth. It also introduces a new flag, conn->client_finished_auth, which is set by various authentication routines when the client side of the handshake is finished. This signals to check_expected_areq() that an OK message from the server is expected, and allows the client to complain if the server forgoes authentication entirely. (Since the client can't generally prove that the server is actually doing the work of authentication, this last part is mostly useful for SCRAM without channel binding. But it could be used to diagnose configuration problems with client certificate authentication. It could also provide a client with a decent signal that, at the very least, it's not connecting to a database with trust auth, and so the connection can be tied to the client in a later audit.) Certificate authentication poses an additional complication. conn->ssl_cert_requested and conn->ssl_cert_sent have been added so that check_expected_areq() can ensure that we sent a certificate during the TLS handshake. Deficiencies: - This feature currently doesn't prevent unexpected certificate exchange or unexpected GSSAPI encryption. - This is unlikely to be very forwards-compatible at the moment, especially with SASL/SCRAM. - SSPI support is "implemented" but untested. --- src/interfaces/libpq/fe-auth-scram.c | 1 + src/interfaces/libpq/fe-auth.c | 158 ++++++++++++++++++++++ src/interfaces/libpq/fe-connect.c | 91 +++++++++++++ src/interfaces/libpq/fe-secure-openssl.c | 28 ++++ src/interfaces/libpq/libpq-int.h | 9 ++ src/test/authentication/t/001_password.pl | 65 +++++++++ src/test/kerberos/t/001_auth.pl | 26 ++++ src/test/ldap/t/001_auth.pl | 6 + src/test/ssl/t/001_ssltests.pl | 16 +++ 9 files changed, 400 insertions(+) diff --git a/src/interfaces/libpq/fe-auth-scram.c b/src/interfaces/libpq/fe-auth-scram.c index e616200704..d918e8b87f 100644 --- a/src/interfaces/libpq/fe-auth-scram.c +++ b/src/interfaces/libpq/fe-auth-scram.c @@ -289,6 +289,7 @@ scram_exchange(void *opaq, char *input, int inputlen, } *done = true; state->state = FE_SCRAM_FINISHED; + state->conn->client_finished_auth = true; break; default: diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c index 0a072a36dc..04f9bf4831 100644 --- a/src/interfaces/libpq/fe-auth.c +++ b/src/interfaces/libpq/fe-auth.c @@ -138,7 +138,10 @@ pg_GSS_continue(PGconn *conn, int payloadlen) } if (maj_stat == GSS_S_COMPLETE) + { + conn->client_finished_auth = true; gss_release_name(&lmin_s, &conn->gtarg_nam); + } return STATUS_OK; } @@ -328,6 +331,9 @@ pg_SSPI_continue(PGconn *conn, int payloadlen) FreeContextBuffer(outbuf.pBuffers[0].pvBuffer); } + if (r == SEC_E_OK) + conn->client_finished_auth = true; + /* Cleanup is handled by the code in freePGconn() */ return STATUS_OK; } @@ -836,6 +842,41 @@ pg_password_sendauth(PGconn *conn, const char *password, AuthRequest areq) return ret; } +/* + * Translate an AuthRequest into a human-readable description. + */ +static const char * +auth_description(AuthRequest areq) +{ + switch (areq) + { + case AUTH_REQ_PASSWORD: + return libpq_gettext("a cleartext password"); + case AUTH_REQ_MD5: + return libpq_gettext("a hashed password"); + case AUTH_REQ_GSS: + case AUTH_REQ_GSS_CONT: + return libpq_gettext("GSSAPI authentication"); + case AUTH_REQ_SSPI: + return libpq_gettext("SSPI authentication"); + case AUTH_REQ_SCM_CREDS: + return libpq_gettext("UNIX socket credentials"); + case AUTH_REQ_SASL: + case AUTH_REQ_SASL_CONT: + case AUTH_REQ_SASL_FIN: + return libpq_gettext("SASL authentication"); + } + + return libpq_gettext("an unknown authentication type"); +} + +/* + * Convenience macro for checking the allowed_auth_methods bitmask. Caller must + * ensure that type is not greater than 31 (high bit of the bitmask). + */ +#define auth_allowed(conn, type) \ + (((conn)->allowed_auth_methods & (1 << (type))) != 0) + /* * Verify that the authentication request is expected, given the connection * parameters. This is especially important when the client wishes to @@ -845,6 +886,120 @@ static bool check_expected_areq(AuthRequest areq, PGconn *conn) { bool result = true; + char *reason = NULL; + + /* + * If the user required a specific auth method, or specified an allowed set, + * then reject all others here, and make sure the server actually completes + * an authentication exchange. + */ + if (conn->require_auth) + { + switch (areq) + { + case AUTH_REQ_OK: + /* + * Check to make sure we've actually finished our exchange. + */ + if (conn->client_finished_auth) + break; + + /* + * No explicit authentication request was made by the server -- + * or perhaps it was made and not completed, in the case of + * SCRAM -- but there are two special cases to check: + * + * 1) If the user allowed "cert", then as long as we sent a + * client certificate to the server in response to its + * TLS CertificateRequest, this check is satisfied. + * + * 2) If the user allowed "gss", then a GSS-encrypted channel + * also satisfies the check. + */ + if (conn->allowed_auth_method_cert) + { + /* + * Trade off a little bit of complexity to try to get these + * error messages as precise as possible. + */ + if (!conn->ssl_cert_requested) + { + reason = conn->allowed_auth_methods + ? libpq_gettext("server did not complete authentication or request a certificate") + : libpq_gettext("server did not request a certificate"); + result = false; + } + else if (!conn->ssl_cert_sent) + { + reason = conn->allowed_auth_methods + ? libpq_gettext("server did not complete authentication and accepted connection without a valid certificate") + : libpq_gettext("server accepted connection without a valid certificate"); + result = false; + } + } + else if (auth_allowed(conn, AUTH_REQ_GSS) && conn->gssenc) + { + /* + * Special case: if implicit GSS auth has already been + * performed via GSS encryption, we don't need to have + * performed an AUTH_REQ_GSS exchange. + * + * TODO: check this assumption. What mutual auth guarantees + * are made in this case? + */ + } + else + { + reason = libpq_gettext("server did not complete authentication"), + result = false; + } + + break; + + case AUTH_REQ_PASSWORD: + case AUTH_REQ_MD5: + case AUTH_REQ_GSS: + case AUTH_REQ_SSPI: + case AUTH_REQ_GSS_CONT: + case AUTH_REQ_SASL: + case AUTH_REQ_SASL_CONT: + case AUTH_REQ_SASL_FIN: + /* + * We don't handle these with the default case, to avoid + * bit-shifting past the end of the allowed_auth_methods mask if + * the server sends an unexpected AuthRequest. + */ + result = auth_allowed(conn, areq); + break; + + default: + result = false; + break; + } + } + + if (!result) + { + if (reason) + { + /* + * XXX double call to libpq_gettext() is probably not easy to + * translate from English + */ + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("auth method \"%s\" required, but %s\n"), + conn->require_auth, reason); + } + else + { + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("auth method \"%s\" required, but server requested %s\n"), + conn->require_auth, + auth_description(areq)); + } + + return result; + } /* * When channel_binding=require, we must protect against two cases: (1) we @@ -1046,6 +1201,9 @@ pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn) "fe_sendauth: error sending password authentication\n"); return STATUS_ERROR; } + + /* We expect no further authentication requests. */ + conn->client_finished_auth = true; break; } diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 6e936bbff3..c650687c9b 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -310,6 +310,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = { "Require-Peer", "", 10, offsetof(struct pg_conn, requirepeer)}, + {"require_auth", NULL, NULL, NULL, + "Require-Auth", "", 14, /* sizeof("scram-sha-256") == 14 */ + offsetof(struct pg_conn, require_auth)}, + {"ssl_min_protocol_version", "PGSSLMINPROTOCOLVERSION", "TLSv1.2", NULL, "SSL-Minimum-Protocol-Version", "", 8, /* sizeof("TLSv1.x") == 8 */ offsetof(struct pg_conn, ssl_min_protocol_version)}, @@ -1255,6 +1259,93 @@ connectOptions2(PGconn *conn) } } + /* + * parse and validate require_auth option + */ + if (conn->require_auth) + { + char *s = conn->require_auth; + bool more = true; + + while (more) + { + char *method; + uint32 bits; + + method = parse_comma_separated_list(&s, &more); + if (method == NULL) + goto oom_error; + + if (strcmp(method, "password") == 0) + { + bits = (1 << AUTH_REQ_PASSWORD); + } + else if (strcmp(method, "md5") == 0) + { + bits = (1 << AUTH_REQ_MD5); + } + else if (strcmp(method, "gss") == 0) + { + bits = (1 << AUTH_REQ_GSS); + bits |= (1 << AUTH_REQ_GSS_CONT); + } + else if (strcmp(method, "sspi") == 0) + { + bits = (1 << AUTH_REQ_SSPI); + bits |= (1 << AUTH_REQ_GSS_CONT); + } + else if (strcmp(method, "scram-sha-256") == 0) + { + /* This currently assumes that SCRAM is the only SASL method. */ + bits = (1 << AUTH_REQ_SASL); + bits |= (1 << AUTH_REQ_SASL_CONT); + bits |= (1 << AUTH_REQ_SASL_FIN); + } + else if (strcmp(method, "cert") == 0) + { + /* + * Special case: there is no AUTH_REQ code for certificate + * authentication since it's done as part of the TLS handshake. + * Make a note in conn, for check_expected_areq() to see later. + */ + if (conn->allowed_auth_method_cert) + { + conn->status = CONNECTION_BAD; + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("require_auth method \"%s\" is specified more than once"), + method); + return false; + } + + conn->allowed_auth_method_cert = true; + continue; /* avoid the bitmask manipulation below */ + } + else + { + conn->status = CONNECTION_BAD; + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("invalid require_auth method: \"%s\""), + method); + return false; + } + + /* + * Sanity check; a duplicated method probably indicates a typo in a + * setting where typos are extremely risky. + */ + if ((conn->allowed_auth_methods & bits) == bits) + { + conn->status = CONNECTION_BAD; + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("require_auth method \"%s\" is specified more than once"), + method); + return false; + } + + conn->allowed_auth_methods |= bits; + } + } + /* * validate channel_binding option */ diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c index 8117cbd40f..6fb9d48896 100644 --- a/src/interfaces/libpq/fe-secure-openssl.c +++ b/src/interfaces/libpq/fe-secure-openssl.c @@ -477,6 +477,31 @@ verify_cb(int ok, X509_STORE_CTX *ctx) return ok; } +/* + * Certificate selection callback + * + * This callback lets us choose the client certificate we send to the server + * after seeing its CertificateRequest. We only support sending a single + * hard-coded certificate via sslcert, so we don't actually set any certificates + * here; we just it to record whether or not the server has actually asked for + * one and whether we have one to send. + */ +static int +cert_cb(SSL *ssl, void *arg) +{ + PGconn *conn = arg; + conn->ssl_cert_requested = true; + + /* Do we have a certificate loaded to send back? */ + if (SSL_get_certificate(ssl)) + conn->ssl_cert_sent = true; + + /* + * Tell OpenSSL that the callback succeeded; we're not required to actually + * make any changes to the SSL handle. + */ + return 1; +} /* * OpenSSL-specific wrapper around @@ -972,6 +997,9 @@ initialize_SSL(PGconn *conn) SSL_CTX_set_default_passwd_cb_userdata(SSL_context, conn); } + /* Set up a certificate selection callback. */ + SSL_CTX_set_cert_cb(SSL_context, cert_cb, conn); + /* Disable old protocol versions */ SSL_CTX_set_options(SSL_context, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3); diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 3db6a17db4..62554fedde 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -393,6 +393,7 @@ struct pg_conn char *ssl_min_protocol_version; /* minimum TLS protocol version */ char *ssl_max_protocol_version; /* maximum TLS protocol version */ char *target_session_attrs; /* desired session properties */ + char *require_auth; /* name of the expected auth method */ /* Optional file to write trace info to */ FILE *Pfdebug; @@ -454,6 +455,9 @@ struct pg_conn bool write_failed; /* have we had a write failure on sock? */ char *write_err_msg; /* write error message, or NULL if OOM */ + uint32 allowed_auth_methods; /* bitmask of acceptable AuthRequest codes */ + bool allowed_auth_method_cert; /* tracks "cert" which has no AuthRequest code */ + /* Transient state needed while establishing connection */ PGTargetServerType target_server_type; /* desired session properties */ bool try_next_addr; /* time to advance to next address/host? */ @@ -509,12 +513,17 @@ struct pg_conn bool error_result; /* do we need to make an ERROR result? */ PGresult *next_result; /* next result (used in single-row mode) */ + bool client_finished_auth; /* have we finished our half of the + * authentication exchange? */ + /* Assorted state for SASL, SSL, GSS, etc */ const pg_fe_sasl_mech *sasl; void *sasl_state; /* SSL structures */ bool ssl_in_use; + bool ssl_cert_requested; /* Did the server ask us for a cert? */ + bool ssl_cert_sent; /* Did we send one in reply? */ #ifdef USE_SSL bool allow_ssl_try; /* Allowed to try SSL negotiation */ diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl index 3e3079c824..dab50774c4 100644 --- a/src/test/authentication/t/001_password.pl +++ b/src/test/authentication/t/001_password.pl @@ -82,6 +82,29 @@ test_role($node, 'scram_role', 'trust', 0, test_role($node, 'md5_role', 'trust', 0, log_unlike => [qr/connection authenticated:/]); +# All require_auth options should fail. +$node->connect_fails("user=scram_role require_auth=cert", + "certificate authentication can be required: fails with trust auth", + expected_stderr => qr/server did not request a certificate/); +$node->connect_fails("user=scram_role require_auth=gss", + "GSS authentication can be required: fails with trust auth", + expected_stderr => qr/server did not complete authentication/); +$node->connect_fails("user=scram_role require_auth=sspi", + "GSS authentication can be required: fails with trust auth", + expected_stderr => qr/server did not complete authentication/); +$node->connect_fails("user=scram_role require_auth=password", + "password authentication can be required: fails with trust auth", + expected_stderr => qr/server did not complete authentication/); +$node->connect_fails("user=scram_role require_auth=md5", + "md5 authentication can be required: fails with trust auth", + expected_stderr => qr/server did not complete authentication/); +$node->connect_fails("user=scram_role require_auth=scram-sha-256", + "SCRAM authentication can be required: fails with trust auth", + expected_stderr => qr/server did not complete authentication/); +$node->connect_fails("user=scram_role require_auth=cert,scram-sha-256", + "multiple authentication types can be required: fails with trust auth", + expected_stderr => qr/server did not complete authentication or request a certificate/); + # For plain "password" method, all users should also be able to connect. reset_pg_hba($node, 'password'); test_role($node, 'scram_role', 'password', 0, @@ -91,6 +114,20 @@ test_role($node, 'md5_role', 'password', 0, log_like => [qr/connection authenticated: identity="md5_role" method=password/]); +# require_auth should succeed with a plaintext password... +$node->connect_ok("user=scram_role require_auth=password", + "password authentication can be required: works with password auth"); +$node->connect_ok("user=scram_role require_auth=cert,password,md5", + "multiple authentication types can be required: works with password auth"); + +# ...and fail for other auth types. +$node->connect_fails("user=scram_role require_auth=md5", + "md5 authentication can be required: fails with password auth", + expected_stderr => qr/server requested a cleartext password/); +$node->connect_fails("user=scram_role require_auth=scram-sha-256", + "SCRAM authentication can be required: fails with password auth", + expected_stderr => qr/server requested a cleartext password/); + # For "scram-sha-256" method, user "scram_role" should be able to connect. reset_pg_hba($node, 'scram-sha-256'); test_role( @@ -104,6 +141,20 @@ test_role( test_role($node, 'md5_role', 'scram-sha-256', 2, log_unlike => [qr/connection authenticated:/]); +# require_auth should succeed with SCRAM... +$node->connect_ok("user=scram_role require_auth=scram-sha-256", + "SCRAM authentication can be required: works with SCRAM auth"); +$node->connect_ok("user=scram_role require_auth=password,scram-sha-256,cert", + "multiple authentication types can be required: works with SCRAM auth"); + +# ...and fail for other auth types. +$node->connect_fails("user=scram_role require_auth=password", + "password authentication can be required: fails with SCRAM auth", + expected_stderr => qr/server requested SASL authentication/); +$node->connect_fails("user=scram_role require_auth=md5", + "md5 authentication can be required: fails with SCRAM auth", + expected_stderr => qr/server requested SASL authentication/); + # Test that bad passwords are rejected. $ENV{"PGPASSWORD"} = 'badpass'; test_role($node, 'scram_role', 'scram-sha-256', 2, @@ -120,6 +171,20 @@ test_role($node, 'md5_role', 'md5', 0, log_like => [qr/connection authenticated: identity="md5_role" method=md5/]); +# require_auth should succeed with MD5... +$node->connect_ok("user=md5_role require_auth=md5", + "MD5 authentication can be required: works with MD5 auth"); +$node->connect_ok("user=md5_role require_auth=md5,scram-sha-256,cert", + "multiple authentication types can be required: works with MD5 auth"); + +# ...and fail for other auth types. +$node->connect_fails("user=md5_role require_auth=password", + "password authentication can be required: fails with MD5 auth", + expected_stderr => qr/server requested a hashed password/); +$node->connect_fails("user=md5_role require_auth=scram-sha-256", + "SCRAM authentication can be required: fails with MD5 auth", + expected_stderr => qr/server requested a hashed password/); + # Tests for channel binding without SSL. # Using the password authentication method; channel binding can't work reset_pg_hba($node, 'password'); diff --git a/src/test/kerberos/t/001_auth.pl b/src/test/kerberos/t/001_auth.pl index 62e0542639..de4ddcbf69 100644 --- a/src/test/kerberos/t/001_auth.pl +++ b/src/test/kerberos/t/001_auth.pl @@ -307,6 +307,24 @@ test_query( 'gssencmode=require', 'sending 100K lines works'); +# require_auth=gss should succeed... +$node->connect_ok( + $node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=disable require_auth=gss", + "GSS authentication can be requested: works with GSS auth without encryption"); +$node->connect_ok( + $node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss", + "GSS authentication can be requested: works with GSS auth with encryption"); + +# ...and require_auth=sspi should fail. +$node->connect_fails( + $node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=disable require_auth=sspi", + "SSPI authentication can be requested: fails with GSS auth without encryption", + expected_stderr => qr/server requested GSSAPI authentication/); +$node->connect_fails( + $node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=sspi", + "SSPI authentication can be requested: fails with GSS auth with encryption", + expected_stderr => qr/server did not complete authentication/); + unlink($node->data_dir . '/pg_hba.conf'); $node->append_conf('pg_hba.conf', qq{hostgssenc all all $hostaddr/32 gss map=mymap}); @@ -335,6 +353,14 @@ test_access( test_access($node, 'test1', 'SELECT true', 2, 'gssencmode=disable', 'fails with GSS encryption disabled and hostgssenc hba'); +# require_auth=gss should succeed. +$node->connect_ok( + $node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss", + "GSS authentication can be requested: works with GSS encryption"); +$node->connect_ok( + $node->connstr('postgres') . " user=test1 host=$host hostaddr=$hostaddr gssencmode=require require_auth=gss,scram-sha-256", + "multiple authentication types can be requested: works with GSS encryption"); + unlink($node->data_dir . '/pg_hba.conf'); $node->append_conf('pg_hba.conf', qq{hostnogssenc all all $hostaddr/32 gss map=mymap}); diff --git a/src/test/ldap/t/001_auth.pl b/src/test/ldap/t/001_auth.pl index 86dff8bd1f..e7c67920a1 100644 --- a/src/test/ldap/t/001_auth.pl +++ b/src/test/ldap/t/001_auth.pl @@ -216,6 +216,12 @@ test_access( qr/connection authenticated: identity="uid=test1,dc=example,dc=net" method=ldap/ ],); +# require_auth=password should complete successfully; other methods should fail. +$node->connect_ok("user=test1 require_auth=password", + "password authentication can be required: works with ldap auth"); +$node->connect_fails("user=test1 require_auth=scram-sha-256", + "SCRAM authentication can be required: fails with ldap auth"); + note "search+bind"; unlink($node->data_dir . '/pg_hba.conf'); diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl index c0b4a5739c..68a888e11a 100644 --- a/src/test/ssl/t/001_ssltests.pl +++ b/src/test/ssl/t/001_ssltests.pl @@ -505,6 +505,22 @@ note "running server tests"; $common_connstr = "$default_ssl_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=certdb hostaddr=$SERVERHOSTADDR host=localhost"; +# require_auth=cert should succeed against the certdb... +$node->connect_ok( + "$common_connstr require_auth=cert user=ssltestuser sslcert=ssl/client.crt " + . sslkey('client.key'), + "certificate authentication can be required: works with cert auth"); + +# ...and fail against the trustdb, if no certificate is provided. +$node->connect_fails( + "$common_connstr require_auth=cert dbname=trustdb user=ssltestuser", + "certificate authentication can be required: fails with trust auth and no cert", + expected_stderr => qr/server accepted connection without a valid certificate/); +$node->connect_fails( + "$common_connstr require_auth=scram-sha-256,cert dbname=trustdb user=ssltestuser", + "multiple authentication types can be required: fails with trust auth and no cert", + expected_stderr => qr/server did not complete authentication and accepted connection without a valid certificate/); + # no client cert $node->connect_fails( "$common_connstr user=ssltestuser sslcert=invalid", -- 2.25.1