Skip to content

Commit 36f40ce

Browse files
committed
libpq: Add sslcertmode option to control client certificates
The sslcertmode option controls whether the server is allowed and/or required to request a certificate from the client. There are three modes: - "allow" is the default and follows the current behavior, where a configured client certificate is sent if the server requests one (via one of its default locations or sslcert). With the current implementation, will happen whenever TLS is negotiated. - "disable" causes the client to refuse to send a client certificate even if sslcert is configured or if a client certificate is available in one of its default locations. - "require" causes the client to fail if a client certificate is never sent and the server opens a connection anyway. This doesn't add any additional security, since there is no guarantee that the server is validating the certificate correctly, but it may helpful to troubleshoot more complicated TLS setups. sslcertmode=require requires SSL_CTX_set_cert_cb(), available since OpenSSL 1.0.2. Note that LibreSSL does not include it. Using a connection parameter different than require_auth has come up as the simplest design because certificate authentication does not rely directly on any of the AUTH_REQ_* codes, and one may want to require a certificate to be sent in combination of a given authentication method, like SCRAM-SHA-256. TAP tests are added in src/test/ssl/, some of them relying on sslinfo to check if a certificate has been set. These are compatible across all the versions of OpenSSL supported on HEAD (currently down to 1.0.1). Author: Jacob Champion Reviewed-by: Aleksander Alekseev, Peter Eisentraut, David G. Johnston, Michael Paquier Discussion: https://postgr.es/m/9e5a8ccddb8355ea9fa4b75a1e3a9edc88a70cd3.camel@vmware.com
1 parent e522049 commit 36f40ce

File tree

12 files changed

+270
-9
lines changed

12 files changed

+270
-9
lines changed

configure

+7-5
Original file line numberDiff line numberDiff line change
@@ -12973,13 +12973,15 @@ else
1297312973
fi
1297412974

1297512975
fi
12976-
# Function introduced in OpenSSL 1.0.2.
12977-
for ac_func in X509_get_signature_nid
12976+
# Functions introduced in OpenSSL 1.0.2. LibreSSL does not have
12977+
# SSL_CTX_set_cert_cb().
12978+
for ac_func in X509_get_signature_nid SSL_CTX_set_cert_cb
1297812979
do :
12979-
ac_fn_c_check_func "$LINENO" "X509_get_signature_nid" "ac_cv_func_X509_get_signature_nid"
12980-
if test "x$ac_cv_func_X509_get_signature_nid" = xyes; then :
12980+
as_ac_var=`$as_echo "ac_cv_func_$ac_func" | $as_tr_sh`
12981+
ac_fn_c_check_func "$LINENO" "$ac_func" "$as_ac_var"
12982+
if eval test \"x\$"$as_ac_var"\" = x"yes"; then :
1298112983
cat >>confdefs.h <<_ACEOF
12982-
#define HAVE_X509_GET_SIGNATURE_NID 1
12984+
#define `$as_echo "HAVE_$ac_func" | $as_tr_cpp` 1
1298312985
_ACEOF
1298412986

1298512987
fi

configure.ac

+3-2
Original file line numberDiff line numberDiff line change
@@ -1373,8 +1373,9 @@ if test "$with_ssl" = openssl ; then
13731373
AC_SEARCH_LIBS(CRYPTO_new_ex_data, [eay32 crypto], [], [AC_MSG_ERROR([library 'eay32' or 'crypto' is required for OpenSSL])])
13741374
AC_SEARCH_LIBS(SSL_new, [ssleay32 ssl], [], [AC_MSG_ERROR([library 'ssleay32' or 'ssl' is required for OpenSSL])])
13751375
fi
1376-
# Function introduced in OpenSSL 1.0.2.
1377-
AC_CHECK_FUNCS([X509_get_signature_nid])
1376+
# Functions introduced in OpenSSL 1.0.2. LibreSSL does not have
1377+
# SSL_CTX_set_cert_cb().
1378+
AC_CHECK_FUNCS([X509_get_signature_nid SSL_CTX_set_cert_cb])
13781379
# Functions introduced in OpenSSL 1.1.0. We used to check for
13791380
# OPENSSL_VERSION_NUMBER, but that didn't work with 1.1.0, because LibreSSL
13801381
# defines OPENSSL_VERSION_NUMBER to claim version 2.0.0, even though it

doc/src/sgml/libpq.sgml

+66
Original file line numberDiff line numberDiff line change
@@ -1810,6 +1810,62 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
18101810
</listitem>
18111811
</varlistentry>
18121812

1813+
<varlistentry id="libpq-connect-sslcertmode" xreflabel="sslcertmode">
1814+
<term><literal>sslcertmode</literal></term>
1815+
<listitem>
1816+
<para>
1817+
This option determines whether a client certificate may be sent to the
1818+
server, and whether the server is required to request one. There are
1819+
three modes:
1820+
1821+
<variablelist>
1822+
<varlistentry>
1823+
<term><literal>disable</literal></term>
1824+
<listitem>
1825+
<para>
1826+
A client certificate is never sent, even if one is available
1827+
(default location or provided via
1828+
<xref linkend="libpq-connect-sslcert" />).
1829+
</para>
1830+
</listitem>
1831+
</varlistentry>
1832+
1833+
<varlistentry>
1834+
<term><literal>allow</literal> (default)</term>
1835+
<listitem>
1836+
<para>
1837+
A certificate may be sent, if the server requests one and the
1838+
client has one to send.
1839+
</para>
1840+
</listitem>
1841+
</varlistentry>
1842+
1843+
<varlistentry>
1844+
<term><literal>require</literal></term>
1845+
<listitem>
1846+
<para>
1847+
The server <emphasis>must</emphasis> request a certificate. The
1848+
connection will fail if the client does not send a certificate and
1849+
the server successfully authenticates the client anyway.
1850+
</para>
1851+
</listitem>
1852+
</varlistentry>
1853+
</variablelist>
1854+
</para>
1855+
1856+
<note>
1857+
<para>
1858+
<literal>sslcertmode=require</literal> doesn't add any additional
1859+
security, since there is no guarantee that the server is validating
1860+
the certificate correctly; PostgreSQL servers generally request TLS
1861+
certificates from clients whether they validate them or not. The
1862+
option may be useful when troubleshooting more complicated TLS
1863+
setups.
1864+
</para>
1865+
</note>
1866+
</listitem>
1867+
</varlistentry>
1868+
18131869
<varlistentry id="libpq-connect-sslrootcert" xreflabel="sslrootcert">
18141870
<term><literal>sslrootcert</literal></term>
18151871
<listitem>
@@ -7986,6 +8042,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
79868042
</para>
79878043
</listitem>
79888044

8045+
<listitem>
8046+
<para>
8047+
<indexterm>
8048+
<primary><envar>PGSSLCERTMODE</envar></primary>
8049+
</indexterm>
8050+
<envar>PGSSLCERTMODE</envar> behaves the same as the <xref
8051+
linkend="libpq-connect-sslcertmode"/> connection parameter.
8052+
</para>
8053+
</listitem>
8054+
79898055
<listitem>
79908056
<para>
79918057
<indexterm>

meson.build

+2-1
Original file line numberDiff line numberDiff line change
@@ -1221,8 +1221,9 @@ if sslopt in ['auto', 'openssl']
12211221
['CRYPTO_new_ex_data', {'required': true}],
12221222
['SSL_new', {'required': true}],
12231223

1224-
# Function introduced in OpenSSL 1.0.2.
1224+
# Functions introduced in OpenSSL 1.0.2.
12251225
['X509_get_signature_nid'],
1226+
['SSL_CTX_set_cert_cb'], # not in LibreSSL
12261227

12271228
# Functions introduced in OpenSSL 1.1.0. We used to check for
12281229
# OPENSSL_VERSION_NUMBER, but that didn't work with 1.1.0, because LibreSSL

src/include/pg_config.h.in

+3
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,9 @@
394394
/* Define to 1 if you have spinlocks. */
395395
#undef HAVE_SPINLOCKS
396396

397+
/* Define to 1 if you have the `SSL_CTX_set_cert_cb' function. */
398+
#undef HAVE_SSL_CTX_SET_CERT_CB
399+
397400
/* Define to 1 if stdbool.h conforms to C99. */
398401
#undef HAVE_STDBOOL_H
399402

src/interfaces/libpq/fe-auth.c

+19
Original file line numberDiff line numberDiff line change
@@ -798,6 +798,25 @@ check_expected_areq(AuthRequest areq, PGconn *conn)
798798
StaticAssertDecl((sizeof(conn->allowed_auth_methods) * CHAR_BIT) > AUTH_REQ_MAX,
799799
"AUTH_REQ_MAX overflows the allowed_auth_methods bitmask");
800800

801+
if (conn->sslcertmode[0] == 'r' /* require */
802+
&& areq == AUTH_REQ_OK)
803+
{
804+
/*
805+
* Trade off a little bit of complexity to try to get these error
806+
* messages as precise as possible.
807+
*/
808+
if (!conn->ssl_cert_requested)
809+
{
810+
libpq_append_conn_error(conn, "server did not request an SSL certificate");
811+
return false;
812+
}
813+
else if (!conn->ssl_cert_sent)
814+
{
815+
libpq_append_conn_error(conn, "server accepted connection without a valid SSL certificate");
816+
return false;
817+
}
818+
}
819+
801820
/*
802821
* If the user required a specific auth method, or specified an allowed
803822
* set, then reject all others here, and make sure the server actually

src/interfaces/libpq/fe-connect.c

+53
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,10 @@ static int ldapServiceLookup(const char *purl, PQconninfoOption *options,
125125
#define DefaultTargetSessionAttrs "any"
126126
#ifdef USE_SSL
127127
#define DefaultSSLMode "prefer"
128+
#define DefaultSSLCertMode "allow"
128129
#else
129130
#define DefaultSSLMode "disable"
131+
#define DefaultSSLCertMode "disable"
130132
#endif
131133
#ifdef ENABLE_GSS
132134
#include "fe-gssapi-common.h"
@@ -283,6 +285,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
283285
"SSL-Client-Key", "", 64,
284286
offsetof(struct pg_conn, sslkey)},
285287

288+
{"sslcertmode", "PGSSLCERTMODE", NULL, NULL,
289+
"SSL-Client-Cert-Mode", "", 8, /* sizeof("disable") == 8 */
290+
offsetof(struct pg_conn, sslcertmode)},
291+
286292
{"sslpassword", NULL, NULL, NULL,
287293
"SSL-Client-Key-Password", "*", 20,
288294
offsetof(struct pg_conn, sslpassword)},
@@ -1506,6 +1512,52 @@ connectOptions2(PGconn *conn)
15061512
return false;
15071513
}
15081514

1515+
/*
1516+
* validate sslcertmode option
1517+
*/
1518+
if (conn->sslcertmode)
1519+
{
1520+
if (strcmp(conn->sslcertmode, "disable") != 0 &&
1521+
strcmp(conn->sslcertmode, "allow") != 0 &&
1522+
strcmp(conn->sslcertmode, "require") != 0)
1523+
{
1524+
conn->status = CONNECTION_BAD;
1525+
libpq_append_conn_error(conn, "invalid %s value: \"%s\"",
1526+
"sslcertmode", conn->sslcertmode);
1527+
return false;
1528+
}
1529+
#ifndef USE_SSL
1530+
if (strcmp(conn->sslcertmode, "require") == 0)
1531+
{
1532+
conn->status = CONNECTION_BAD;
1533+
libpq_append_conn_error(conn, "%s value \"%s\" invalid when SSL support is not compiled in",
1534+
"sslcertmode", conn->sslcertmode);
1535+
return false;
1536+
}
1537+
#endif
1538+
#ifndef HAVE_SSL_CTX_SET_CERT_CB
1539+
1540+
/*
1541+
* Without a certificate callback, the current implementation can't
1542+
* figure out if a certificate was actually requested, so "require" is
1543+
* useless.
1544+
*/
1545+
if (strcmp(conn->sslcertmode, "require") == 0)
1546+
{
1547+
conn->status = CONNECTION_BAD;
1548+
libpq_append_conn_error(conn, "%s value \"%s\" is not supported (check OpenSSL version)",
1549+
"sslcertmode", conn->sslcertmode);
1550+
return false;
1551+
}
1552+
#endif
1553+
}
1554+
else
1555+
{
1556+
conn->sslcertmode = strdup(DefaultSSLCertMode);
1557+
if (!conn->sslcertmode)
1558+
goto oom_error;
1559+
}
1560+
15091561
/*
15101562
* validate gssencmode option
15111563
*/
@@ -4238,6 +4290,7 @@ freePGconn(PGconn *conn)
42384290
explicit_bzero(conn->sslpassword, strlen(conn->sslpassword));
42394291
free(conn->sslpassword);
42404292
}
4293+
free(conn->sslcertmode);
42414294
free(conn->sslrootcert);
42424295
free(conn->sslcrl);
42434296
free(conn->sslcrldir);

src/interfaces/libpq/fe-secure-openssl.c

+39-1
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,34 @@ verify_cb(int ok, X509_STORE_CTX *ctx)
462462
return ok;
463463
}
464464

465+
#ifdef HAVE_SSL_CTX_SET_CERT_CB
466+
/*
467+
* Certificate selection callback
468+
*
469+
* This callback lets us choose the client certificate we send to the server
470+
* after seeing its CertificateRequest. We only support sending a single
471+
* hard-coded certificate via sslcert, so we don't actually set any certificates
472+
* here; we just use it to record whether or not the server has actually asked
473+
* for one and whether we have one to send.
474+
*/
475+
static int
476+
cert_cb(SSL *ssl, void *arg)
477+
{
478+
PGconn *conn = arg;
479+
480+
conn->ssl_cert_requested = true;
481+
482+
/* Do we have a certificate loaded to send back? */
483+
if (SSL_get_certificate(ssl))
484+
conn->ssl_cert_sent = true;
485+
486+
/*
487+
* Tell OpenSSL that the callback succeeded; we're not required to
488+
* actually make any changes to the SSL handle.
489+
*/
490+
return 1;
491+
}
492+
#endif
465493

466494
/*
467495
* OpenSSL-specific wrapper around
@@ -953,6 +981,11 @@ initialize_SSL(PGconn *conn)
953981
SSL_CTX_set_default_passwd_cb_userdata(SSL_context, conn);
954982
}
955983

984+
#ifdef HAVE_SSL_CTX_SET_CERT_CB
985+
/* Set up a certificate selection callback. */
986+
SSL_CTX_set_cert_cb(SSL_context, cert_cb, conn);
987+
#endif
988+
956989
/* Disable old protocol versions */
957990
SSL_CTX_set_options(SSL_context, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
958991

@@ -1107,7 +1140,12 @@ initialize_SSL(PGconn *conn)
11071140
else
11081141
fnbuf[0] = '\0';
11091142

1110-
if (fnbuf[0] == '\0')
1143+
if (conn->sslcertmode[0] == 'd') /* disable */
1144+
{
1145+
/* don't send a client cert even if we have one */
1146+
have_cert = false;
1147+
}
1148+
else if (fnbuf[0] == '\0')
11111149
{
11121150
/* no home directory, proceed without a client cert */
11131151
have_cert = false;

src/interfaces/libpq/libpq-int.h

+3
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,7 @@ struct pg_conn
384384
char *sslkey; /* client key filename */
385385
char *sslcert; /* client certificate filename */
386386
char *sslpassword; /* client key file password */
387+
char *sslcertmode; /* client cert mode (require,allow,disable) */
387388
char *sslrootcert; /* root certificate filename */
388389
char *sslcrl; /* certificate revocation list filename */
389390
char *sslcrldir; /* certificate revocation list directory name */
@@ -527,6 +528,8 @@ struct pg_conn
527528

528529
/* SSL structures */
529530
bool ssl_in_use;
531+
bool ssl_cert_requested; /* Did the server ask us for a cert? */
532+
bool ssl_cert_sent; /* Did we send one in reply? */
530533

531534
#ifdef USE_SSL
532535
bool allow_ssl_try; /* Allowed to try SSL negotiation */

src/test/ssl/t/001_ssltests.pl

+42
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ sub switch_server_cert
4242
# This is the pattern to use in pg_hba.conf to match incoming connections.
4343
my $SERVERHOSTCIDR = '127.0.0.1/32';
4444

45+
# Determine whether build supports sslcertmode=require.
46+
my $supports_sslcertmode_require =
47+
check_pg_config("#define HAVE_SSL_CTX_SET_CERT_CB 1");
48+
4549
# Allocation of base connection string shared among multiple tests.
4650
my $common_connstr;
4751

@@ -191,6 +195,22 @@ sub switch_server_cert
191195
"$common_connstr sslrootcert=ssl/both-cas-2.crt sslmode=verify-ca",
192196
"cert root file that contains two certificates, order 2");
193197

198+
# sslcertmode=allow and disable should both work without a client certificate.
199+
$node->connect_ok(
200+
"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=disable",
201+
"connect with sslcertmode=disable");
202+
$node->connect_ok(
203+
"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=allow",
204+
"connect with sslcertmode=allow");
205+
206+
# sslcertmode=require, however, should fail.
207+
$node->connect_fails(
208+
"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=require",
209+
"connect with sslcertmode=require fails without a client certificate",
210+
expected_stderr => $supports_sslcertmode_require
211+
? qr/server accepted connection without a valid SSL certificate/
212+
: qr/sslcertmode value "require" is not supported/);
213+
194214
# CRL tests
195215

196216
# Invalid CRL filename is the same as no CRL, succeeds
@@ -538,6 +558,28 @@ sub switch_server_cert
538558
"certificate authorization succeeds with correct client cert in encrypted DER format"
539559
);
540560

561+
# correct client cert with sslcertmode=allow or require
562+
if ($supports_sslcertmode_require)
563+
{
564+
$node->connect_ok(
565+
"$common_connstr user=ssltestuser sslcertmode=require sslcert=ssl/client.crt "
566+
. sslkey('client.key'),
567+
"certificate authorization succeeds with correct client cert and sslcertmode=require"
568+
);
569+
}
570+
$node->connect_ok(
571+
"$common_connstr user=ssltestuser sslcertmode=allow sslcert=ssl/client.crt "
572+
. sslkey('client.key'),
573+
"certificate authorization succeeds with correct client cert and sslcertmode=allow"
574+
);
575+
576+
# client cert is not sent if sslcertmode=disable.
577+
$node->connect_fails(
578+
"$common_connstr user=ssltestuser sslcertmode=disable sslcert=ssl/client.crt "
579+
. sslkey('client.key'),
580+
"certificate authorization fails with correct client cert and sslcertmode=disable",
581+
expected_stderr => qr/connection requires a valid client certificate/);
582+
541583
# correct client cert in encrypted PEM with wrong password
542584
$node->connect_fails(
543585
"$common_connstr user=ssltestuser sslcert=ssl/client.crt "

0 commit comments

Comments
 (0)