aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/include/pg_config.h.in3
-rw-r--r--src/interfaces/libpq/fe-auth.c19
-rw-r--r--src/interfaces/libpq/fe-connect.c53
-rw-r--r--src/interfaces/libpq/fe-secure-openssl.c40
-rw-r--r--src/interfaces/libpq/libpq-int.h3
-rw-r--r--src/test/ssl/t/001_ssltests.pl42
-rw-r--r--src/test/ssl/t/003_sslinfo.pl24
-rw-r--r--src/tools/msvc/Solution.pm9
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);