diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml index 53832d0..5ee8cbe 100644 --- a/doc/src/sgml/client-auth.sgml +++ b/doc/src/sgml/client-auth.sgml @@ -563,10 +563,17 @@ hostnossl database user In addition to the method-specific options listed below, there is one method-independent authentication option clientcert, which - can be specified in any hostssl record. When set - to 1, this option requires the client to present a valid - (trusted) SSL certificate, in addition to the other requirements of the - authentication method. + can be specified in any hostssl record. + This option can be set to verify-ca or + verify-full. Both options require the client + to present a valid (trusted) SSL certificate, while + verify-full additionally enforces that the + CN (Common Name) in the certificate matches + the username or an applicable mapping. + This behaviour is similar to the cert autentication method + (see ) but enables pairing + the verification of client certificates with any authentication + method that supports hostssl entries. diff --git a/doc/src/sgml/runtime.sgml b/doc/src/sgml/runtime.sgml index 330e38a..6502df3 100644 --- a/doc/src/sgml/runtime.sgml +++ b/doc/src/sgml/runtime.sgml @@ -2287,13 +2287,25 @@ pg_dumpall -p 5432 | psql -d postgres -p 5433 (CAs) you trust in a file in the data directory, set the parameter in postgresql.conf to the new file name, and add the - authentication option clientcert=1 to the appropriate + authentication option clientcert=verify-ca or + clientcert=verify-full to the appropriate hostssl line(s) in pg_hba.conf. A certificate will then be requested from the client during SSL connection startup. (See for a description - of how to set up certificates on the client.) The server will - verify that the client's certificate is signed by one of the trusted - certificate authorities. + of how to set up certificates on the client.) + + + + For a hostssl entry with + clientcert=verify-ca, the server will verify + that the client's certificate is signed by one of the trusted + certificate authorities. If clientcert=verify-full + is specified, the server will not only verify the certificate + chain, but it will also check whether the username or it's + mapping match the common name (CN) of the provided certificate. + Note that certificate chain validation is always ensured when + cert authentication method is used + (see ). @@ -2317,14 +2329,33 @@ pg_dumpall -p 5432 | psql -d postgres -p 5433 client certificates against its CA file, if one is configured — but it will not insist that a client certificate be presented. + + + Enforcing Client Certificates - If you are setting up client certificates, you may wish to use - the cert authentication method, so that the certificates - control user authentication as well as providing connection security. - See for details. (It is not necessary to - specify clientcert=1 explicitly when using - the cert authentication method.) + There are two approaches to enforce that users provide a certificate during login. + + + + The first approach makes use of the cert authentication + method for hostssl entries in pg_hba.conf, + such that the certificate itself is used for authentication while also + providing ssl connection security. See for details. + (It is not necessary to specify any clientcert + options explicitly when using the cert authentication method.) + In this case, the CN (nommon name) provided in + the certificate is checked against the user name or an applicable mapping. + + + + The second approach combines any authentication method for hostssl + entries with the verification of client certificates by setting the + clientcert authentication option to verify-ca + or verify-full. The former option only enforces that + the certificate is valid, while the latter also ensures that the + CN (Common Name) in the certificate matches + the user name or an applicable mapping. diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c index 3014b17..8330f5c 100644 --- a/src/backend/libpq/auth.c +++ b/src/backend/libpq/auth.c @@ -347,6 +347,7 @@ void ClientAuthentication(Port *port) { int status = STATUS_ERROR; + int status_verify_cert_full = STATUS_ERROR; char *logdetail = NULL; /* @@ -364,7 +365,7 @@ ClientAuthentication(Port *port) * current connection, so perform any verifications based on the hba * options field that should be done *before* the authentication here. */ - if (port->hba->clientcert) + if (port->hba->clientcert != clientCertOff) { /* If we haven't loaded a root certificate store, fail */ if (!secure_loaded_verify_locations()) @@ -600,10 +601,23 @@ ClientAuthentication(Port *port) break; } + if(port->hba->clientcert == clientCertFull && port->hba->auth_method!=uaCert) + { +#ifdef USE_SSL + status_verify_cert_full = CheckCertAuth(port); +#else + Assert(false); +#endif + } + else + { + status_verify_cert_full = STATUS_OK; + } + if (ClientAuthentication_hook) (*ClientAuthentication_hook) (port, status); - if (status == STATUS_OK) + if (status == STATUS_OK && status_verify_cert_full == STATUS_OK) sendAuthRequest(port, AUTH_REQ_OK, NULL, 0); else auth_failed(port, status, logdetail); @@ -2756,6 +2770,7 @@ errdetail_for_ldap(LDAP *ldap) static int CheckCertAuth(Port *port) { + int status_check_usermap = STATUS_ERROR; Assert(port->ssl); /* Make sure we have received a username in the certificate */ @@ -2769,7 +2784,21 @@ CheckCertAuth(Port *port) } /* Just pass the certificate CN to the usermap check */ - return check_usermap(port->hba->usermap, port->user_name, port->peer_cn, false); + status_check_usermap = check_usermap(port->hba->usermap, port->user_name, port->peer_cn, false); + if (status_check_usermap != STATUS_OK) + { + /* + * If clientcert=verify-full was specified and the authentication method + * is other than uaCert, log the reason for rejecting the authentication. + */ + if (port->hba->clientcert == clientCertFull && port->hba->auth_method!=uaCert) + { + ereport(LOG, + (errmsg("certificate validation (clientcert=verify-full) failed for user \"%s\": CN mismatch", + port->user_name))); + } + } + return status_check_usermap; } #endif diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c index acf625e..d17682b 100644 --- a/src/backend/libpq/hba.c +++ b/src/backend/libpq/hba.c @@ -1609,7 +1609,7 @@ parse_hba_line(TokenizedLine *tok_line, int elevel) */ if (parsedline->auth_method == uaCert) { - parsedline->clientcert = true; + parsedline->clientcert = clientCertOn; } return parsedline; @@ -1675,11 +1675,16 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline, *err_msg = "clientcert can only be configured for \"hostssl\" rows"; return false; } - if (strcmp(val, "1") == 0) + if (strcmp(val, "1") == 0 + || strcmp(val, "verify-ca") == 0) { - hbaline->clientcert = true; + hbaline->clientcert = clientCertOn; } - else + else if(strcmp(val, "verify-full") == 0) + { + hbaline->clientcert = clientCertFull; + } + else if (strcmp(val, "0") == 0) { if (hbaline->auth_method == uaCert) { @@ -1691,7 +1696,16 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline, *err_msg = "clientcert can not be set to 0 when using \"cert\" authentication"; return false; } - hbaline->clientcert = false; + hbaline->clientcert = clientCertOff; + } + else + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("invalid value for clientcert: \"%s\"", val), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + return false; } } else if (strcmp(name, "pamservice") == 0) @@ -2250,9 +2264,9 @@ gethba_options(HbaLine *hba) options[noptions++] = CStringGetTextDatum(psprintf("map=%s", hba->usermap)); - if (hba->clientcert) + if (hba->clientcert != clientCertOff) options[noptions++] = - CStringGetTextDatum("clientcert=true"); + CStringGetTextDatum(psprintf("clientcert=%s", (hba->clientcert == clientCertOn) ? "verify-ca" : "verify-full")); if (hba->pamservice) options[noptions++] = diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h index 5f68f4c..ce9a692 100644 --- a/src/include/libpq/hba.h +++ b/src/include/libpq/hba.h @@ -58,6 +58,13 @@ typedef enum ConnType ctHostNoSSL } ConnType; +typedef enum ClientCertMode +{ + clientCertOff, + clientCertOn, + clientCertFull +} ClientCertMode; + typedef struct HbaLine { int linenumber; @@ -86,7 +93,7 @@ typedef struct HbaLine int ldapscope; char *ldapprefix; char *ldapsuffix; - bool clientcert; + ClientCertMode clientcert; char *krb_realm; bool include_realm; bool compat_realm; diff --git a/src/test/ssl/ServerSetup.pm b/src/test/ssl/ServerSetup.pm index e81f4df..4343a1a 100644 --- a/src/test/ssl/ServerSetup.pm +++ b/src/test/ssl/ServerSetup.pm @@ -82,8 +82,10 @@ sub configure_test_server_for_ssl # Create test users and databases $node->psql('postgres', "CREATE USER ssltestuser"); $node->psql('postgres', "CREATE USER anotheruser"); + $node->psql('postgres', "CREATE USER yetanotheruser"); $node->psql('postgres', "CREATE DATABASE trustdb"); $node->psql('postgres', "CREATE DATABASE certdb"); + $node->psql('postgres', "CREATE DATABASE verifydb"); # Update password of each user as needed. if (defined($password)) @@ -157,12 +159,18 @@ sub configure_hba_for_ssl # When connecting to certdb, also check the client certificate. open my $hba, '>', "$pgdata/pg_hba.conf"; print $hba -"# TYPE DATABASE USER ADDRESS METHOD\n"; +"# TYPE DATABASE USER ADDRESS METHOD OPTIONS\n"; print $hba "hostssl trustdb all $serverhost/32 $authmethod\n"; print $hba "hostssl trustdb all ::1/128 $authmethod\n"; print $hba +"hostssl verifydb ssltestuser $serverhost/32 $authmethod clientcert=verify-full\n"; + print $hba +"hostssl verifydb anotheruser $serverhost/32 $authmethod clientcert=verify-full\n"; + print $hba +"hostssl verifydb yetanotheruser $serverhost/32 trust clientcert=verify-ca\n"; + print $hba "hostssl certdb all $serverhost/32 cert\n"; print $hba "hostssl certdb all ::1/128 cert\n"; diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl index 91feac6..b45999e 100644 --- a/src/test/ssl/t/001_ssltests.pl +++ b/src/test/ssl/t/001_ssltests.pl @@ -8,7 +8,7 @@ use File::Copy; if ($ENV{with_openssl} eq 'yes') { - plan tests => 64; + plan tests => 68; } else { @@ -298,6 +298,27 @@ test_connect_fails($common_connstr, qr/SSL error/, "certificate authorization fails with revoked client cert"); +# Check that connecting with auth-optionverify-full in pg_hba : +# works, iff username matches common name +# fails, iff username doesn't match common name. +$common_connstr = +"sslrootcert=ssl/root+server_ca.crt sslmode=require dbname=verifydb hostaddr=$SERVERHOSTADDR"; + +test_connect_ok($common_connstr, + "user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key", + "auth_option clientcert=verify-full succeeds with matching username and common name"); + +test_connect_fails($common_connstr, + "user=anotheruser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key", + qr/FATAL/, + "auth_option clientcert=verify-full fails with mismatching username and common name"); + +# Check that connecting with auth-optionverify-ca in pg_hba : +# works, when username doesn't match common name +test_connect_ok($common_connstr, + "user=yetanotheruser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key", + "auth_option clientcert=verify-ca succeeds with mismatching username and common name"); + # intermediate client_ca.crt is provided by client, and isn't in server's ssl_ca_file switch_server_cert($node, 'server-cn-only', 'root_ca'); $common_connstr =