diff --git a/configure b/configure
index 2a1ee251f2..2d1b24c67a 100755
--- a/configure
+++ b/configure
@@ -12908,6 +12908,17 @@ _ACEOF
fi
done
+ # Function introduced in OpenSSL 1.1.1 to allow client to obtain server's CA list
+ for ac_func in SSL_get0_peer_CA_list
+do :
+ ac_fn_c_check_func "$LINENO" "SSL_get0_peer_CA_list" "ac_cv_func_SSL_get0_peer_CA_list"
+if test "x$ac_cv_func_SSL_get0_peer_CA_list" = xyes; then :
+ cat >>confdefs.h <<_ACEOF
+#define HAVE_SSL_GET0_PEER_CA_LIST 1
+_ACEOF
+
+fi
+done
$as_echo "#define USE_OPENSSL 1" >>confdefs.h
diff --git a/configure.ac b/configure.ac
index 52fd7af446..58ec6d6271 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1381,6 +1381,7 @@ if test "$with_ssl" = openssl ; then
AC_CHECK_FUNCS([CRYPTO_lock])
# Function introduced in OpenSSL 1.1.1.
AC_CHECK_FUNCS([X509_get_signature_info])
+ AC_CHECK_FUNCS([SSL_get0_peer_CA_list])
AC_DEFINE([USE_OPENSSL], 1, [Define to 1 to build with OpenSSL support. (--with-ssl=openssl)])
elif test "$with_ssl" != no ; then
AC_MSG_ERROR([--with-ssl must specify openssl])
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index d0d5aefadc..118a2c3f39 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1778,6 +1778,32 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
+
+ sslcertdir
+
+
+ This parameter specifies the directory to automatically find a suitable
+ client certificate to send to the server if it requests one. The selection
+ is based on the list of trusted CA names sent from the server in the
+ Certificate Request handshake message. A client certificate whose issuer
+ name equals to one of the trusted CA names is considered trusted by the
+ server. This parameter cannot be used with sslcert
+
+
+
+
+
+ sslkeydir
+
+
+ This parameter specifies the directory to find a private key that forms
+ a match with the public key of the client certificate selected from
+ sslcertdir. This parameter cannot be used with
+ sslkey
+
+
+
+
sslpassword
diff --git a/meson.build b/meson.build
index 8ed51b6aae..661bc17f5a 100644
--- a/meson.build
+++ b/meson.build
@@ -1302,6 +1302,7 @@ if sslopt in ['auto', 'openssl']
# Function introduced in OpenSSL 1.1.1
['X509_get_signature_info'],
+ ['SSL_get0_peer_CA_list'],
]
are_openssl_funcs_complete = true
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 07e73567dc..923993626c 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -381,6 +381,9 @@
/* Define to 1 if you have the `SSL_CTX_set_cert_cb' function. */
#undef HAVE_SSL_CTX_SET_CERT_CB
+/* Define to 1 if you have the `SSL_get0_peer_CA_list' function. */
+#undef HAVE_SSL_GET0_PEER_CA_LIST
+
/* Define to 1 if stdbool.h conforms to C99. */
#undef HAVE_STDBOOL_H
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 64c0b628b3..be610b1fb6 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -284,6 +284,14 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
"SSL-Client-Key", "", 64,
offsetof(struct pg_conn, sslkey)},
+ {"sslcertdir", "PGSSLCERTDIR", NULL, NULL,
+ "SSL-Client-Cert-Dir", "", 64,
+ offsetof(struct pg_conn, sslcertdir)},
+
+ {"sslkeydir", "PGSSLKEYDIR", NULL, NULL,
+ "SSL-Client-Key-Dir", "", 64,
+ offsetof(struct pg_conn, sslkeydir)},
+
{"sslcertmode", "PGSSLCERTMODE", NULL, NULL,
"SSL-Client-Cert-Mode", "", 8, /* sizeof("disable") == 8 */
offsetof(struct pg_conn, sslcertmode)},
@@ -1736,6 +1744,48 @@ pqConnectOptions2(PGconn *conn)
goto oom_error;
}
+#ifndef HAVE_SSL_GET0_PEER_CA_LIST
+ /*
+ * Without a SSL_get0_peer_ca_list support, the current implementation can't
+ * select from multiple client certificate based on server's trusted CA list,
+ * so "sslcertdir" and "sslkeydir" options are useless in this case.
+ */
+ if (conn->sslcertdir || conn->sslkeydir)
+ {
+ conn->status = CONNECTION_BAD;
+ libpq_append_conn_error(conn, "%s and %s are not supported (check OpenSSL version). "
+ "Please use %s and %s to specify a client certificate and key",
+ "sslcertdir",
+ "sslkeydir",
+ "sslcert",
+ "sslkey");
+ return false;
+ }
+#else
+ /*
+ * validate sslcertdir and sslkeydir conflicts
+ */
+ if (conn->sslcertdir && conn->sslcert)
+ {
+ conn->status = CONNECTION_BAD;
+ libpq_append_conn_error(conn, "%s and %s cannot be specified"
+ "at the same time",
+ "sslcertdir",
+ "sslcert");
+ return false;
+ }
+
+ if (conn->sslkeydir && conn->sslkey)
+ {
+ conn->status = CONNECTION_BAD;
+ libpq_append_conn_error(conn, "%s and %s cannot be specified"
+ "at the same time",
+ "sslkeydir",
+ "sslkey");
+ return false;
+ }
+#endif
+
/*
* Only if we get this far is it appropriate to try to connect. (We need a
* state flag, rather than just the boolean result of this function, in
@@ -4373,6 +4423,8 @@ freePGconn(PGconn *conn)
free(conn->sslmode);
free(conn->sslcert);
free(conn->sslkey);
+ free(conn->sslcertdir);
+ free(conn->sslkeydir);
if (conn->sslpassword)
{
explicit_bzero(conn->sslpassword, strlen(conn->sslpassword));
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 6bc216956d..cc15c4770f 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -25,6 +25,7 @@
#include
#include
#include
+#include
#include "libpq-fe.h"
#include "fe-auth.h"
@@ -459,23 +460,237 @@ verify_cb(int ok, X509_STORE_CTX *ctx)
return ok;
}
+#ifdef HAVE_SSL_GET0_PEER_CA_LIST
+/*
+ * Helper function to read a certificate file
+ *
+ * This function tries to load 'filename' into X509 structure
+ */
+static X509*
+load_certificate(const char* filename)
+{
+ FILE *file = fopen(filename, "r");
+ X509 *cert = NULL;
+
+ if (!file)
+ return NULL;
+
+ cert = PEM_read_X509(file, NULL, NULL, NULL);
+ fclose(file);
+
+ return cert;
+}
+
+/*
+ * Utility function to find a suitable client certificate
+ *
+ * This function tries to find a client certificate in 'directory' whose issuer
+ * name matches one of the subjects listed in 'peercas'. Returns NULL if none
+ * found.
+ */
+static X509*
+find_client_certificate_by_issuer(const STACK_OF(X509_NAME) * peercas, const char* directory,
+ PGconn * conn)
+{
+ X509 *cert_candidate = NULL;
+ DIR *dir = opendir(directory);
+ struct dirent *entry;
+ int i = 0, numcas = 0;
+
+ if (!dir)
+ {
+ libpq_append_conn_error(conn, "cannot access certificate directory %s"
+ ,directory);
+ return NULL;
+ }
+
+ numcas = sk_X509_NAME_num(peercas);
+
+ while ((entry = readdir(dir)) != NULL)
+ {
+ if (entry->d_type == DT_REG)
+ {
+ char filepath[1024] = {0};
+ snprintf(filepath, sizeof(filepath), "%s/%s", directory, entry->d_name);
+
+ cert_candidate = load_certificate(filepath);
+ if (cert_candidate)
+ {
+ /*
+ * found a x509 certificate file, check if it can be trusted by comparing its
+ * issuer against the list of subjects in peercas
+ */
+ for (i = 0; i < numcas; i++)
+ {
+ X509_NAME *ca_name = sk_X509_NAME_value(peercas, i);
+ if(!X509_NAME_cmp(X509_get_issuer_name(cert_candidate), ca_name))
+ {
+ /*
+ * this candidate certificate's issuer matches one of the peercas's
+ * subject names, it should be trusted by the server
+ */
+ conn->sslcert = strdup(filepath);
+ closedir(dir);
+ return cert_candidate;
+ }
+ }
+ X509_free(cert_candidate);
+ }
+ }
+ }
+ closedir(dir);
+ return NULL; /* no suitable client certificate found */
+}
+
+/*
+ * Helper function to read a private key file
+ *
+ * This function tries to load 'filename' into EVP_PKEY structure
+ */
+static EVP_PKEY*
+load_pkey(const char* filename)
+{
+ FILE *file = fopen(filename, "r");
+ EVP_PKEY *pkey = NULL;
+
+ if (!file)
+ return NULL;
+
+ pkey = PEM_read_PrivateKey(file, NULL, NULL, NULL);
+ fclose(file);
+
+ return pkey;
+}
+
+/*
+ * Utility function to find matching private key
+ *
+ * This function tries to find a private key in 'directory' that matches the public
+ * key inside 'clientcert'. Returns NULL if none is found.
+ */
+static EVP_PKEY*
+find_client_key(X509* clientcert, const char* directory, PGconn * conn)
+{
+ EVP_PKEY *pkey_candidate = NULL;
+ DIR *dir = opendir(directory);
+ struct dirent *entry;
+
+ if (!dir)
+ {
+ libpq_append_conn_error(conn, "cannot access pkey directory %s"
+ ,directory);
+ return NULL;
+ }
+
+ while ((entry = readdir(dir)) != NULL)
+ {
+ if (entry->d_type == DT_REG)
+ {
+ char filepath[1024] = {0};
+ snprintf(filepath, sizeof(filepath), "%s/%s", directory, entry->d_name);
+
+ pkey_candidate = load_pkey(filepath);
+ if (pkey_candidate)
+ {
+ /* make sure this pkey matches clientcert */
+ if (X509_check_private_key(clientcert, pkey_candidate))
+ {
+ /* it is a match */
+ conn->sslkey = strdup(filepath);
+ closedir(dir);
+ return pkey_candidate;
+ }
+ EVP_PKEY_free(pkey_candidate);
+ }
+ }
+ }
+ closedir(dir);
+ return NULL; /* no suitable certificate found */
+}
+#endif
+
#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.
+ * This callback lets us choose a client certificate to send to the server
+ * after seeing its CertificateRequest. If the server sends its CA list while
+ * sslcertdir and sslkeydir are specified, we will be able to select the most
+ * suitable client certificate to send. Otherwise we just use this callback to
+ * record whether or not the server has actually asked for a client certificate
+ * and whether we have one to send.
*/
static int
cert_cb(SSL *ssl, void *arg)
{
PGconn *conn = arg;
+#ifdef HAVE_SSL_GET0_PEER_CA_LIST
+ X509 *clientcert = NULL;
+ EVP_PKEY *clientkey = NULL;
+
+ /* obtain a list of CA list sent from the server, if any */
+ const STACK_OF(X509_NAME) * peercas = NULL;
+ peercas = SSL_get0_peer_CA_list(ssl);
+#endif
+
+ /* mark that the server has requested a client certificate */
conn->ssl_cert_requested = true;
+#ifdef HAVE_SSL_GET0_PEER_CA_LIST
+ /*
+ * if sslcertdir and sslkeydir are specified and the server has sent a CA list,
+ * then we can try to find the most suitable certificate to send to the server.
+ */
+ if (conn->sslcertmode[0] != 'd' && /* sslcertmode is not disabled */
+ peercas && conn->sslcertdir && conn->sslkeydir)
+ {
+ /* try to select a suitable client certificate */
+ clientcert = find_client_certificate_by_issuer(peercas, conn->sslcertdir, conn);
+ if (!clientcert)
+ {
+ libpq_append_conn_error(conn, "Server requests a client certificate but no suitable "
+ "certificate is found from the directory %s", conn->sslcertdir);
+ conn->ssl_cert_sent = false;
+
+ /*
+ * we return 1 here to allow TLS handshake to continue even though no client certificate
+ * is set, making it up to the server to decide if handshake should be aborted with the
+ * absence of client certificate
+ */
+ return 1;
+ }
+
+ /* try to find a matching private key */
+ clientkey = find_client_key(clientcert, conn->sslkeydir, conn);
+ if (!clientkey)
+ {
+ libpq_append_conn_error(conn, "A suitable client certificate exists but "
+ "no matching private key is found from the directory %s", conn->sslkeydir);
+ conn->ssl_cert_sent = false;
+ X509_free(clientcert);
+
+ /*
+ * we return 1 here to allow TLS handshake to continue even though no client certificate
+ * is set, making it up to the server to decide if handshake should be aborted with the
+ * absence of client certificate
+ */
+ return 1;
+ }
+
+ /*
+ * we should now have both the client certificate and private key. Set them to SSL. Note
+ * that we use SSL_use_certificte_chain_file() to load the certificate file instead of using
+ * clientcert because it would load intermediate or root CAs appended in the same client
+ * cert file if any. These may be needed to verify server's certificate in the next handshake
+ * stage
+ */
+ X509_free(clientcert);
+ SSL_use_certificate_chain_file(ssl, conn->sslcert);
+ SSL_use_PrivateKey(ssl, clientkey);
+ }
+#endif
+
/* Do we have a certificate loaded to send back? */
if (SSL_get_certificate(ssl))
conn->ssl_cert_sent = true;
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 82c18f870d..433572ab56 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -391,6 +391,8 @@ struct pg_conn
char *sslcompression; /* SSL compression (0 or 1) */
char *sslkey; /* client key filename */
char *sslcert; /* client certificate filename */
+ char *sslcertdir; /* path to a directory of certificate files */
+ char *sslkeydir; /* path to a directory of key files */
char *sslpassword; /* client key file password */
char *sslcertmode; /* client cert mode (require,allow,disable) */
char *sslrootcert; /* root certificate filename */