diff options
Diffstat (limited to 'src')
-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 |
8 files changed, 192 insertions, 1 deletions
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); |