diff options
-rwxr-xr-x | configure | 12 | ||||
-rw-r--r-- | configure.ac | 5 | ||||
-rw-r--r-- | doc/src/sgml/libpq.sgml | 66 | ||||
-rw-r--r-- | meson.build | 3 | ||||
-rw-r--r-- | src/include/pg_config.h.in | 3 | ||||
-rw-r--r-- | src/interfaces/libpq/fe-auth.c | 19 | ||||
-rw-r--r-- | src/interfaces/libpq/fe-connect.c | 53 | ||||
-rw-r--r-- | src/interfaces/libpq/fe-secure-openssl.c | 40 | ||||
-rw-r--r-- | src/interfaces/libpq/libpq-int.h | 3 | ||||
-rw-r--r-- | src/test/ssl/t/001_ssltests.pl | 42 | ||||
-rw-r--r-- | src/test/ssl/t/003_sslinfo.pl | 24 | ||||
-rw-r--r-- | src/tools/msvc/Solution.pm | 9 |
12 files changed, 270 insertions, 9 deletions
diff --git a/configure b/configure index e221dd5b0ff..905be9568bf 100755 --- a/configure +++ b/configure @@ -12973,13 +12973,15 @@ else fi fi - # Function introduced in OpenSSL 1.0.2. - for ac_func in X509_get_signature_nid + # Functions introduced in OpenSSL 1.0.2. LibreSSL does not have + # SSL_CTX_set_cert_cb(). + for ac_func in X509_get_signature_nid SSL_CTX_set_cert_cb do : - ac_fn_c_check_func "$LINENO" "X509_get_signature_nid" "ac_cv_func_X509_get_signature_nid" -if test "x$ac_cv_func_X509_get_signature_nid" = xyes; then : + as_ac_var=`$as_echo "ac_cv_func_$ac_func" | $as_tr_sh` +ac_fn_c_check_func "$LINENO" "$ac_func" "$as_ac_var" +if eval test \"x\$"$as_ac_var"\" = x"yes"; then : cat >>confdefs.h <<_ACEOF -#define HAVE_X509_GET_SIGNATURE_NID 1 +#define `$as_echo "HAVE_$ac_func" | $as_tr_cpp` 1 _ACEOF fi diff --git a/configure.ac b/configure.ac index 3aa6c15c13c..8095dfcf1d1 100644 --- a/configure.ac +++ b/configure.ac @@ -1373,8 +1373,9 @@ if test "$with_ssl" = openssl ; then AC_SEARCH_LIBS(CRYPTO_new_ex_data, [eay32 crypto], [], [AC_MSG_ERROR([library 'eay32' or 'crypto' is required for OpenSSL])]) AC_SEARCH_LIBS(SSL_new, [ssleay32 ssl], [], [AC_MSG_ERROR([library 'ssleay32' or 'ssl' is required for OpenSSL])]) fi - # Function introduced in OpenSSL 1.0.2. - AC_CHECK_FUNCS([X509_get_signature_nid]) + # Functions introduced in OpenSSL 1.0.2. LibreSSL does not have + # SSL_CTX_set_cert_cb(). + AC_CHECK_FUNCS([X509_get_signature_nid SSL_CTX_set_cert_cb]) # Functions introduced in OpenSSL 1.1.0. We used to check for # OPENSSL_VERSION_NUMBER, but that didn't work with 1.1.0, because LibreSSL # defines OPENSSL_VERSION_NUMBER to claim version 2.0.0, even though it diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml index 9ee5532c076..8579dcac952 100644 --- a/doc/src/sgml/libpq.sgml +++ b/doc/src/sgml/libpq.sgml @@ -1810,6 +1810,62 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname </listitem> </varlistentry> + <varlistentry id="libpq-connect-sslcertmode" xreflabel="sslcertmode"> + <term><literal>sslcertmode</literal></term> + <listitem> + <para> + This option determines whether a client certificate may be sent to the + server, and whether the server is required to request one. There are + three modes: + + <variablelist> + <varlistentry> + <term><literal>disable</literal></term> + <listitem> + <para> + A client certificate is never sent, even if one is available + (default location or provided via + <xref linkend="libpq-connect-sslcert" />). + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>allow</literal> (default)</term> + <listitem> + <para> + A certificate may be sent, if the server requests one and the + client has one to send. + </para> + </listitem> + </varlistentry> + + <varlistentry> + <term><literal>require</literal></term> + <listitem> + <para> + The server <emphasis>must</emphasis> request a certificate. The + connection will fail if the client does not send a certificate and + the server successfully authenticates the client anyway. + </para> + </listitem> + </varlistentry> + </variablelist> + </para> + + <note> + <para> + <literal>sslcertmode=require</literal> doesn't add any additional + security, since there is no guarantee that the server is validating + the certificate correctly; PostgreSQL servers generally request TLS + certificates from clients whether they validate them or not. The + option may be useful when troubleshooting more complicated TLS + setups. + </para> + </note> + </listitem> + </varlistentry> + <varlistentry id="libpq-connect-sslrootcert" xreflabel="sslrootcert"> <term><literal>sslrootcert</literal></term> <listitem> @@ -7989,6 +8045,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough) <listitem> <para> <indexterm> + <primary><envar>PGSSLCERTMODE</envar></primary> + </indexterm> + <envar>PGSSLCERTMODE</envar> behaves the same as the <xref + linkend="libpq-connect-sslcertmode"/> connection parameter. + </para> + </listitem> + + <listitem> + <para> + <indexterm> <primary><envar>PGSSLROOTCERT</envar></primary> </indexterm> <envar>PGSSLROOTCERT</envar> behaves the same as the <xref diff --git a/meson.build b/meson.build index 09f619b12fd..77ee015ea9c 100644 --- a/meson.build +++ b/meson.build @@ -1221,8 +1221,9 @@ if sslopt in ['auto', 'openssl'] ['CRYPTO_new_ex_data', {'required': true}], ['SSL_new', {'required': true}], - # Function introduced in OpenSSL 1.0.2. + # Functions introduced in OpenSSL 1.0.2. ['X509_get_signature_nid'], + ['SSL_CTX_set_cert_cb'], # not in LibreSSL # Functions introduced in OpenSSL 1.1.0. We used to check for # OPENSSL_VERSION_NUMBER, but that didn't work with 1.1.0, because LibreSSL diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in index 4882c705590..3665e799e7b 100644 --- a/src/include/pg_config.h.in +++ b/src/include/pg_config.h.in @@ -394,6 +394,9 @@ /* Define to 1 if you have spinlocks. */ #undef HAVE_SPINLOCKS +/* Define to 1 if you have the `SSL_CTX_set_cert_cb' function. */ +#undef HAVE_SSL_CTX_SET_CERT_CB + /* Define to 1 if stdbool.h conforms to C99. */ #undef HAVE_STDBOOL_H diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c index fa95f8e6e96..934e3f4f7ca 100644 --- a/src/interfaces/libpq/fe-auth.c +++ b/src/interfaces/libpq/fe-auth.c @@ -798,6 +798,25 @@ check_expected_areq(AuthRequest areq, PGconn *conn) StaticAssertDecl((sizeof(conn->allowed_auth_methods) * CHAR_BIT) > AUTH_REQ_MAX, "AUTH_REQ_MAX overflows the allowed_auth_methods bitmask"); + if (conn->sslcertmode[0] == 'r' /* require */ + && areq == AUTH_REQ_OK) + { + /* + * Trade off a little bit of complexity to try to get these error + * messages as precise as possible. + */ + if (!conn->ssl_cert_requested) + { + libpq_append_conn_error(conn, "server did not request an SSL certificate"); + return false; + } + else if (!conn->ssl_cert_sent) + { + libpq_append_conn_error(conn, "server accepted connection without a valid SSL certificate"); + return false; + } + } + /* * If the user required a specific auth method, or specified an allowed * set, then reject all others here, and make sure the server actually diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 7d2be566648..660775e0198 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -125,8 +125,10 @@ static int ldapServiceLookup(const char *purl, PQconninfoOption *options, #define DefaultTargetSessionAttrs "any" #ifdef USE_SSL #define DefaultSSLMode "prefer" +#define DefaultSSLCertMode "allow" #else #define DefaultSSLMode "disable" +#define DefaultSSLCertMode "disable" #endif #ifdef ENABLE_GSS #include "fe-gssapi-common.h" @@ -283,6 +285,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = { "SSL-Client-Key", "", 64, offsetof(struct pg_conn, sslkey)}, + {"sslcertmode", "PGSSLCERTMODE", NULL, NULL, + "SSL-Client-Cert-Mode", "", 8, /* sizeof("disable") == 8 */ + offsetof(struct pg_conn, sslcertmode)}, + {"sslpassword", NULL, NULL, NULL, "SSL-Client-Key-Password", "*", 20, offsetof(struct pg_conn, sslpassword)}, @@ -1507,6 +1513,52 @@ connectOptions2(PGconn *conn) } /* + * validate sslcertmode option + */ + if (conn->sslcertmode) + { + if (strcmp(conn->sslcertmode, "disable") != 0 && + strcmp(conn->sslcertmode, "allow") != 0 && + strcmp(conn->sslcertmode, "require") != 0) + { + conn->status = CONNECTION_BAD; + libpq_append_conn_error(conn, "invalid %s value: \"%s\"", + "sslcertmode", conn->sslcertmode); + return false; + } +#ifndef USE_SSL + if (strcmp(conn->sslcertmode, "require") == 0) + { + conn->status = CONNECTION_BAD; + libpq_append_conn_error(conn, "%s value \"%s\" invalid when SSL support is not compiled in", + "sslcertmode", conn->sslcertmode); + return false; + } +#endif +#ifndef HAVE_SSL_CTX_SET_CERT_CB + + /* + * Without a certificate callback, the current implementation can't + * figure out if a certificate was actually requested, so "require" is + * useless. + */ + if (strcmp(conn->sslcertmode, "require") == 0) + { + conn->status = CONNECTION_BAD; + libpq_append_conn_error(conn, "%s value \"%s\" is not supported (check OpenSSL version)", + "sslcertmode", conn->sslcertmode); + return false; + } +#endif + } + else + { + conn->sslcertmode = strdup(DefaultSSLCertMode); + if (!conn->sslcertmode) + goto oom_error; + } + + /* * validate gssencmode option */ if (conn->gssencmode) @@ -4238,6 +4290,7 @@ freePGconn(PGconn *conn) explicit_bzero(conn->sslpassword, strlen(conn->sslpassword)); free(conn->sslpassword); } + free(conn->sslcertmode); free(conn->sslrootcert); free(conn->sslcrl); free(conn->sslcrldir); diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c index 6a4431ddfe9..4d1e4009ef1 100644 --- a/src/interfaces/libpq/fe-secure-openssl.c +++ b/src/interfaces/libpq/fe-secure-openssl.c @@ -462,6 +462,34 @@ verify_cb(int ok, X509_STORE_CTX *ctx) return ok; } +#ifdef HAVE_SSL_CTX_SET_CERT_CB +/* + * 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 use 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; +} +#endif /* * OpenSSL-specific wrapper around @@ -953,6 +981,11 @@ initialize_SSL(PGconn *conn) SSL_CTX_set_default_passwd_cb_userdata(SSL_context, conn); } +#ifdef HAVE_SSL_CTX_SET_CERT_CB + /* Set up a certificate selection callback. */ + SSL_CTX_set_cert_cb(SSL_context, cert_cb, conn); +#endif + /* Disable old protocol versions */ SSL_CTX_set_options(SSL_context, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3); @@ -1107,7 +1140,12 @@ initialize_SSL(PGconn *conn) else fnbuf[0] = '\0'; - if (fnbuf[0] == '\0') + if (conn->sslcertmode[0] == 'd') /* disable */ + { + /* don't send a client cert even if we have one */ + have_cert = false; + } + else if (fnbuf[0] == '\0') { /* no home directory, proceed without a client cert */ have_cert = false; diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 1dc264fe544..10187c31b9a 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -384,6 +384,7 @@ struct pg_conn char *sslkey; /* client key filename */ char *sslcert; /* client certificate filename */ char *sslpassword; /* client key file password */ + char *sslcertmode; /* client cert mode (require,allow,disable) */ char *sslrootcert; /* root certificate filename */ char *sslcrl; /* certificate revocation list filename */ char *sslcrldir; /* certificate revocation list directory name */ @@ -527,6 +528,8 @@ struct pg_conn /* 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/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl index 3094e27af3a..dc43b8f81aa 100644 --- a/src/test/ssl/t/001_ssltests.pl +++ b/src/test/ssl/t/001_ssltests.pl @@ -42,6 +42,10 @@ my $SERVERHOSTADDR = '127.0.0.1'; # This is the pattern to use in pg_hba.conf to match incoming connections. my $SERVERHOSTCIDR = '127.0.0.1/32'; +# Determine whether build supports sslcertmode=require. +my $supports_sslcertmode_require = + check_pg_config("#define HAVE_SSL_CTX_SET_CERT_CB 1"); + # Allocation of base connection string shared among multiple tests. my $common_connstr; @@ -191,6 +195,22 @@ $node->connect_ok( "$common_connstr sslrootcert=ssl/both-cas-2.crt sslmode=verify-ca", "cert root file that contains two certificates, order 2"); +# sslcertmode=allow and disable should both work without a client certificate. +$node->connect_ok( + "$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=disable", + "connect with sslcertmode=disable"); +$node->connect_ok( + "$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=allow", + "connect with sslcertmode=allow"); + +# sslcertmode=require, however, should fail. +$node->connect_fails( + "$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=require", + "connect with sslcertmode=require fails without a client certificate", + expected_stderr => $supports_sslcertmode_require + ? qr/server accepted connection without a valid SSL certificate/ + : qr/sslcertmode value "require" is not supported/); + # CRL tests # Invalid CRL filename is the same as no CRL, succeeds @@ -538,6 +558,28 @@ $node->connect_ok( "certificate authorization succeeds with correct client cert in encrypted DER format" ); +# correct client cert with sslcertmode=allow or require +if ($supports_sslcertmode_require) +{ + $node->connect_ok( + "$common_connstr user=ssltestuser sslcertmode=require sslcert=ssl/client.crt " + . sslkey('client.key'), + "certificate authorization succeeds with correct client cert and sslcertmode=require" + ); +} +$node->connect_ok( + "$common_connstr user=ssltestuser sslcertmode=allow sslcert=ssl/client.crt " + . sslkey('client.key'), + "certificate authorization succeeds with correct client cert and sslcertmode=allow" +); + +# client cert is not sent if sslcertmode=disable. +$node->connect_fails( + "$common_connstr user=ssltestuser sslcertmode=disable sslcert=ssl/client.crt " + . sslkey('client.key'), + "certificate authorization fails with correct client cert and sslcertmode=disable", + expected_stderr => qr/connection requires a valid client certificate/); + # correct client cert in encrypted PEM with wrong password $node->connect_fails( "$common_connstr user=ssltestuser sslcert=ssl/client.crt " diff --git a/src/test/ssl/t/003_sslinfo.pl b/src/test/ssl/t/003_sslinfo.pl index 3f498fff704..c073625213e 100644 --- a/src/test/ssl/t/003_sslinfo.pl +++ b/src/test/ssl/t/003_sslinfo.pl @@ -43,6 +43,10 @@ my $SERVERHOSTADDR = '127.0.0.1'; # This is the pattern to use in pg_hba.conf to match incoming connections. my $SERVERHOSTCIDR = '127.0.0.1/32'; +# Determine whether build supports sslcertmode=require. +my $supports_sslcertmode_require = + check_pg_config("#define HAVE_SSL_CTX_SET_CERT_CB 1"); + # Allocation of base connection string shared among multiple tests. my $common_connstr; @@ -166,4 +170,24 @@ $result = $node->safe_psql( connstr => $common_connstr); is($result, 'CA:FALSE|t', 'extract extension from cert'); +# Sanity tests for sslcertmode, using ssl_client_cert_present() +my @cases = ( + { opts => "sslcertmode=allow", present => 't' }, + { opts => "sslcertmode=allow sslcert=invalid", present => 'f' }, + { opts => "sslcertmode=disable", present => 'f' },); +if ($supports_sslcertmode_require) +{ + push(@cases, { opts => "sslcertmode=require", present => 't' }); +} + +foreach my $c (@cases) +{ + $result = $node->safe_psql( + "trustdb", + "SELECT ssl_client_cert_present();", + connstr => "$common_connstr dbname=trustdb $c->{'opts'}"); + is($result, $c->{'present'}, + "ssl_client_cert_present() for $c->{'opts'}"); +} + done_testing(); diff --git a/src/tools/msvc/Solution.pm b/src/tools/msvc/Solution.pm index b59953e5b59..153be7be11c 100644 --- a/src/tools/msvc/Solution.pm +++ b/src/tools/msvc/Solution.pm @@ -327,6 +327,7 @@ sub GenerateFiles HAVE_SETPROCTITLE_FAST => undef, HAVE_SOCKLEN_T => 1, HAVE_SPINLOCKS => 1, + HAVE_SSL_CTX_SET_CERT_CB => undef, HAVE_STDBOOL_H => 1, HAVE_STDINT_H => 1, HAVE_STDLIB_H => 1, @@ -506,6 +507,14 @@ sub GenerateFiles $define{HAVE_HMAC_CTX_NEW} = 1; $define{HAVE_OPENSSL_INIT_SSL} = 1; } + + # Symbols needed with OpenSSL 1.0.2 and above. + if ( ($digit1 >= '3' && $digit2 >= '0' && $digit3 >= '0') + || ($digit1 >= '1' && $digit2 >= '1' && $digit3 >= '0') + || ($digit1 >= '1' && $digit2 >= '0' && $digit3 >= '2')) + { + $define{HAVE_SSL_CTX_SET_CERT_CB} = 1; + } } $self->GenerateConfigHeader('src/include/pg_config.h', \%define, 1); |