[Patch] add multiple client certificate selection feature
Hello
I would like to share a patch that adds a feature to libpq to automatically select the best client certificate to send to the server (if it requests one). This feature is inspired by this email discussion years ago: /messages/by-id/200905081539.n48Fdl2Y003286@no.baka.org.%C2%A0This feature is useful if libpq client needs to communicate with multiple TLS-enabled PostgreSQL servers with different TLS certificate setups. Instead of letting the application to figure out the right certificate for the right server, the patch allows libpq library itself to pick the most ideal client certificate to send to the server.
Currently, we rely on options “sslcert” and “sslkey” parameters on the client side to select a client certificate + private key to send to the server, the patch adds 2 new options. “sslcertdir” and “sslkeydir” to specify directories where all possible certificate and private key files are stored. The new options cannot be used with “sslcert” and “sslkey” at the same time.
The most ideal certificate selection is based on the trusted CA names sent by the server in “Certificate Request” handshake message; obtained by the client making a call to “SSL_get0_peer_CA_list()” function. This list of trusted CA names tells the client the list of “issuers” that this server can trust. Inside “sslcertdir”, If a client certificate candidate’s issuer name equals to one of the trusted CA names, then that is the certificate to use. Once a candidate certificate is identified, the patch will then look for a matching private key in “sslkeydir”. These actions are performed in certificate callback function (cert_cb), which gets called when server requests a client certificate during TLS handshake.
This patch requires OpenSSL version 1.1.1 or later to work. The feature will be disabled with older OpenSSL versions. Attached is a POC patch containing the described feature.
Limitations:
One limitation of this feature is that it does not quite support the case where multiple private key files inside “sslkeydir” are encrypted with different passwords. When the client wants to find a matching private key from “sslkeydir”, it will always use the same password supplied by the client (via “sslpassword” option) to decrypt the private key it tries to access.
Also, no tap tests have been added to the patch to test this feature yet. So, to test this feature, we will need to prepare the environment manually:
1. generate 2 root CA certificates (ca1 and ca2), which sign 2 sets of client and server certificates.
2. configure the server to use a server certificate signed by either ca1 or ca2.
3. put all client certificates and private keys (signed by both ca1 and ca2) into a directory (we will point"sslcertdir" and "sslkeydir" to this directory)
4. based on the root CA certificate configured at the server side, the client will pick the certificate that the server can trust from specified "sslcertdir" and "sslkeydir" directories
Please let me know what you think. Any comments / feedback are greatly appreciated.
Best regards
================
Cary Huang
Highgo Software (Canada)
www.highgo.ca
Attachments:
v1-0001-multiple_client_certificate_selection_support.patchapplication/octet-stream; name=v1-0001-multiple_client_certificate_selection_support.patchDownload
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
</listitem>
</varlistentry>
+ <varlistentry id="libpq-connect-sslcertdir" xreflabel="sslcertdir">
+ <term><literal>sslcertdir</literal></term>
+ <listitem>
+ <para>
+ 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 <literal>sslcert</literal>
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="libpq-connect-sslkeydir" xreflabel="sslkeydir">
+ <term><literal>sslkeydir</literal></term>
+ <listitem>
+ <para>
+ This parameter specifies the directory to find a private key that forms
+ a match with the public key of the client certificate selected from
+ <literal>sslcertdir</literal>. This parameter cannot be used with
+ <literal>sslkey</literal>
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="libpq-connect-sslpassword" xreflabel="sslpassword">
<term><literal>sslpassword</literal></term>
<listitem>
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 <signal.h>
#include <fcntl.h>
#include <ctype.h>
+#include <dirent.h>
#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 */
Import Notes
Reply to msg id not found:
Hello
I would like to share a version 2 patch for multiple client certificate selection feature with several enhancements over v1. I removed the extra parameter "sslcertdir" and "sslkeydir". Instead, I reuse the existing sslcert, ssldir and sslpassword parameters but allow multiple entries to be supplied separated by comma. This way, we are able to use a different sslpassword to decrypt different sslkey files based on the selected certificate. This was not possible in v1.
When a client is doing a TLS handshake with a server that requires client certificate, the client will obtain a list of trusted CA names from the server and try to match it from the list of certificates provided via sslcert option. A client certificate is chosen if its issuer matches one of the server’s trusted CA names. Once a certificate is chosen, the corresponding private key and sslpassword (if required) will be used to establish a secured TLS connection.
The feature is useful when a libpq client needs to communicate with multiple TLS-enabled PostgreSQL server instances with different TLS certificate setups. Instead of letting the application to figure out what certificate to send to what server, we can configure all possible certificate candidates to libpq and have it choose the best one to use instead.
Hello Daniel
Sorry to bother. I am just wondering your opinion about this feature? Should this be added to commitfest for review? This feature involves certificates issued by different root CAs to test the its ability to pick the right certificate, so the existing ssl tap test’s certificate generation script needs an update to test this. I have not done so yet, because I would like to discuss with you first.
Any comments and recommendations are welcome. Thank you!
Best regards
Cary Huang
Attachments:
v2-0001-multiple_client_certificate_selection_support.patchapplication/octet-stream; name=v2-0001-multiple_client_certificate_selection_support.patchDownload
diff --git a/configure b/configure
index 6b87e5c9a8..916ec585f4 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 6e64ece11d..7e7dac59d3 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/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 d4e10a0c4f..952001e78a 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -1622,6 +1622,100 @@ pqConnectOptions2(PGconn *conn)
goto oom_error;
}
+ /*
+ * Allocate memory for details about each client certificate candidate that we
+ * could choose to send to the server for verification.
+ */
+ conn->whichcert = 0;
+ if (conn->sslcert && conn->sslcert[0] != '\0')
+ conn->nsslcert = count_comma_separated_elems(conn->sslcert);
+
+ if (conn->sslkey && conn->sslkey[0] != '\0')
+ conn->nsslkey = count_comma_separated_elems(conn->sslkey);
+
+ if (conn->sslpassword && conn->sslpassword[0] != '\0')
+ conn->nsslpassword = count_comma_separated_elems(conn->sslpassword);
+
+ if (conn->nsslcert > 0)
+ {
+ /*
+ * conn->conncert is allocated only when multiple sslcert, sslkey or
+ * sslpassword are provided separated by comma
+ */
+ conn->conncert = (pg_conn_cert *)
+ calloc(conn->nsslcert, sizeof(pg_conn_cert));
+ if (conn->conncert == NULL)
+ goto oom_error;
+
+ /*
+ * Now we have one pg_conn_cert structure per possible certificate candidate.
+ * Fill in the sslcert, sslkey and sslpassword fields for each, by splitting
+ * the parameter strings
+ */
+ if (conn->sslcert && conn->sslcert[0] != '\0')
+ {
+ char *s = conn->sslcert;
+ bool more = true;
+
+ for (i = 0; i < conn->nsslcert && more; i++)
+ {
+ conn->conncert[i].sslcert = parse_comma_separated_list(&s, &more);
+ if (conn->conncert[i].sslcert == NULL)
+ goto oom_error;
+ }
+
+ /*
+ * Ensure the size matches
+ */
+ Assert(!more);
+ Assert(i == conn->nsslcert);
+ }
+
+ if (conn->sslkey && conn->sslkey[0] != '\0')
+ {
+ char *s = conn->sslkey;
+ bool more = true;
+
+ for (i = 0; i < conn->nsslkey && more; i++)
+ {
+ conn->conncert[i].sslkey = parse_comma_separated_list(&s, &more);
+ if (conn->conncert[i].sslkey == NULL)
+ goto oom_error;
+ }
+
+ /* Check for wrong number of sslkey items. */
+ if (more || i != conn->nsslcert)
+ {
+ conn->status = CONNECTION_BAD;
+ libpq_append_conn_error(conn, "could not match %d sslcert to %d sslkey values",
+ conn->nsslcert, conn->nsslkey);
+ return false;
+ }
+ }
+
+ if (conn->sslpassword && conn->sslpassword[0] != '\0')
+ {
+ char *s = conn->sslpassword;
+ bool more = true;
+
+ for (i = 0; i < conn->nsslpassword && more; i++)
+ {
+ conn->conncert[i].sslpassword = parse_comma_separated_list(&s, &more);
+ if (conn->conncert[i].sslpassword == NULL)
+ goto oom_error;
+ }
+
+ /* Check for wrong number of sslkey items. */
+ if (more || i != conn->nsslkey)
+ {
+ conn->status = CONNECTION_BAD;
+ libpq_append_conn_error(conn, "could not match %d sslpassword to %d sslkey values",
+ conn->nsslpassword, conn->nsslkey);
+ return false;
+ }
+ }
+ }
+
/*
* validate gssencmode option
*/
@@ -4403,6 +4497,24 @@ freePGconn(PGconn *conn)
termPQExpBuffer(&conn->errorMessage);
termPQExpBuffer(&conn->workBuffer);
+ /* Clean up conn->conncert structure */
+ if (conn->conncert)
+ {
+ for (int i = 0; i < conn->nsslcert; i++)
+ {
+ free(conn->conncert[i].sslcert);
+ free(conn->conncert[i].sslkey);
+ if (conn->conncert[i].sslpassword != NULL)
+ {
+ explicit_bzero(conn->conncert[i].sslpassword,
+ strlen(conn->conncert[i].sslpassword));
+ free(conn->conncert[i].sslpassword);
+ }
+ }
+ free(conn->conncert);
+ conn->conncert = NULL;
+ }
+
free(conn);
}
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 8110882262..c2deeb2ef1 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -454,23 +454,213 @@ 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 STACK_OF(X509)*
+load_certificate(const char* filename)
+{
+ FILE *file = fopen(filename, "r");
+ STACK_OF(X509) *certchain = NULL;
+ X509 *cert = NULL;
+
+ if (!file)
+ return NULL;
+
+ certchain = sk_X509_new_null();
+
+ cert = PEM_read_X509(file, NULL, NULL, NULL);
+ while (cert != NULL)
+ {
+ sk_X509_push(certchain, cert);
+ cert = PEM_read_X509(file, NULL, NULL, NULL);
+ }
+
+ fclose(file);
+
+ return certchain;
+}
+
+/*
+ * Utility function to find a suitable client certificate
+ *
+ * This function tries to find a client certificate in conn->conncert by
+ * matching issuer name with trusted CA subject name in peercas. Returns
+ * the client certificate index if found, -1 otherwise.
+ */
+static int
+find_client_certificate_by_issuer(const STACK_OF(X509_NAME) * peercas,
+ PGconn * conn)
+{
+ STACK_OF(X509) *cert_candidate = NULL;
+ int numcas = sk_X509_NAME_num(peercas);
+ int i = 0, j = 0, k = 0;
+
+ for (i = 0; i < conn->nsslcert; i++)
+ {
+ cert_candidate = load_certificate(conn->conncert[i].sslcert);
+ if (cert_candidate)
+ {
+ /*
+ * we should have a stack of one client certificate plus all
+ * possible intermediate CA certificates if present in the
+ * same certificate file. We will check if any of these
+ * certificate's issuer matches any of the trusted CA names
+ * provided by the server.
+ */
+ for (j = 0; j < sk_X509_num(cert_candidate); j++)
+ {
+ X509 *cert = sk_X509_value(cert_candidate, j);
+
+ for (k = 0; k < numcas; k++)
+ {
+ X509_NAME *ca_name = sk_X509_NAME_value(peercas, k);
+ if(!X509_NAME_cmp(X509_get_issuer_name(cert), ca_name))
+ {
+ /*
+ * one of the candidate client certificate's or
+ * intermediate certificate's issuer matches one
+ * of the trusted CA names, it should be trusted by
+ * the server.
+ */
+ conn->whichcert = i;
+ sk_X509_pop_free(cert_candidate, X509_free);
+ return conn->whichcert;
+ }
+ }
+
+ }
+ sk_X509_pop_free(cert_candidate, X509_free);
+ }
+ }
+ return -1;
+}
+#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 receiving the CertificateRequest. If the server sends its CA list,
+ * 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
+ int clientcertindex = -1;
+ /* obtain a list of CA list sent from the server, if any */
+ const STACK_OF(X509_NAME) * peercas = SSL_get0_peer_CA_list(ssl);
+ struct stat keystat;
+#endif
+
+ /* mark that the server has requested a client certificate */
conn->ssl_cert_requested = true;
+#ifdef HAVE_SSL_GET0_PEER_CA_LIST
+
+ if (strcmp(conn->sslcertmode, "disable") && conn->nsslcert > 1 && peercas)
+ {
+ clientcertindex = find_client_certificate_by_issuer(peercas, conn);
+ if (clientcertindex == -1)
+ {
+ libpq_append_conn_error(conn, "Server requests a client certificate but no suitable "
+ "certificate is found from sslcert list provided");
+ conn->ssl_cert_sent = false;
+ return 1;
+ }
+
+ /*
+ * found a client certificate, load it to ssl. Note that we do not check
+ * return value here because this client certificate has already been
+ * checked inside find_client_certificate_by_issuer() function.
+ */
+ SSL_use_certificate_chain_file(ssl, conn->conncert[clientcertindex].sslcert);
+
+ /*
+ * we will load the private key at index clientcertindex
+ */
+ if (conn->conncert[clientcertindex].sslkey[0] != '\0')
+ {
+ if (stat(conn->conncert[clientcertindex].sslkey, &keystat) != 0)
+ {
+ if (errno == ENOENT)
+ libpq_append_conn_error(conn, "certificate present, but not private key file \"%s\"",
+ conn->conncert[clientcertindex].sslkey);
+ else
+ libpq_append_conn_error(conn, "could not stat private key file \"%s\": %m",
+ conn->conncert[clientcertindex].sslkey);
+ return 1;
+
+ }
+ /* Key file must be a regular file */
+ if (!S_ISREG(keystat.st_mode))
+ {
+ libpq_append_conn_error(conn, "private key file \"%s\" is not a regular file",
+ conn->conncert[clientcertindex].sslkey);
+ return 1;
+ }
+#if !defined(WIN32) && !defined(__CYGWIN__)
+ if (keystat.st_uid == 0 ?
+ keystat.st_mode & (S_IWGRP | S_IXGRP | S_IRWXO) :
+ keystat.st_mode & (S_IRWXG | S_IRWXO))
+ {
+ libpq_append_conn_error(conn,
+ "private key file \"%s\" has group or world access; file must have permissions u=rw (0600) or less if owned by the current user, or permissions u=rw,g=r (0640) or less if owned by root",
+ conn->conncert[clientcertindex].sslkey);
+ return -1;
+ }
+#endif
+ if (SSL_use_PrivateKey_file(ssl, conn->conncert[clientcertindex].sslkey,
+ SSL_FILETYPE_PEM) != 1)
+ {
+ char *err = SSLerrmessage(ERR_get_error());
+ /*
+ * We'll try to load the file in DER (binary ASN.1) format if PEM
+ * format failed to load.
+ */
+ if (SSL_use_PrivateKey_file(ssl, conn->conncert[clientcertindex].sslkey,
+ SSL_FILETYPE_ASN1) != 1)
+ {
+ libpq_append_conn_error(conn, "could not load private key file \"%s\": %s",
+ conn->conncert[clientcertindex].sslkey, err);
+ SSLerrfree(err);
+ conn->ssl_cert_sent = false;
+ return 1;
+ }
+ SSLerrfree(err);
+ }
+ }
+ else
+ {
+ libpq_append_conn_error(conn, "A suitable client certificate exists but "
+ "no matching private key is supplied");
+ conn->ssl_cert_sent = false;
+ return 1;
+ }
+
+ /* verify that the cert and key go together */
+ if (SSL_check_private_key(ssl) != 1)
+ {
+ char *err = SSLerrmessage(ERR_get_error());
+
+ libpq_append_conn_error(conn, "certificate does not match private key file \"%s\": %s",
+ conn->conncert[clientcertindex].sslcert, err);
+ SSLerrfree(err);
+ return 1;
+ }
+ }
+#endif
+
/* Do we have a certificate loaded to send back? */
if (SSL_get_certificate(ssl))
conn->ssl_cert_sent = true;
@@ -1127,8 +1317,11 @@ initialize_SSL(PGconn *conn)
}
/* Read the client certificate file */
- if (conn->sslcert && strlen(conn->sslcert) > 0)
+ if (conn->sslcert && strlen(conn->sslcert) > 0 && conn->nsslcert == 1)
+ {
+ printf("using %s\n", conn->sslcert);
strlcpy(fnbuf, conn->sslcert, sizeof(fnbuf));
+ }
else if (have_homedir)
snprintf(fnbuf, sizeof(fnbuf), "%s/%s", homedir, USER_CERT_FILE);
else
@@ -1240,8 +1433,9 @@ initialize_SSL(PGconn *conn)
* colon in the name. The exception is if the second character is a colon,
* in which case it can be a Windows filename with drive specification.
*/
- if (have_cert && conn->sslkey && strlen(conn->sslkey) > 0)
+ if (have_cert && conn->sslkey && strlen(conn->sslkey) > 0 && conn->nsslkey == 1)
{
+ printf("using %s\n", conn->sslkey);
#ifdef USE_SSL_ENGINE
if (strchr(conn->sslkey, ':')
#ifdef WIN32
@@ -1967,12 +2161,28 @@ err:
int
PQdefaultSSLKeyPassHook_OpenSSL(char *buf, int size, PGconn *conn)
{
- if (conn && conn->sslpassword)
+ if (conn && conn->sslpassword && conn->nsslpassword > 0)
{
- if (strlen(conn->sslpassword) + 1 > size)
- fprintf(stderr, libpq_gettext("WARNING: sslpassword truncated\n"));
- strncpy(buf, conn->sslpassword, size);
- buf[size - 1] = '\0';
+ /* if there is only one password, use conn->sslpassword */
+ if (conn->nsslpassword == 1)
+ {
+ if (strlen(conn->sslpassword) + 1 > size)
+ fprintf(stderr, libpq_gettext("WARNING: sslpassword truncated\n"));
+ strncpy(buf, conn->sslpassword, size);
+ buf[size - 1] = '\0';
+ }
+ else
+ {
+ /*
+ * if there are more than one passwords, choose one
+ * from conn->conncert based on conn->whichcert
+ */
+ if (conn->whichcert >= 0 &&
+ strlen(conn->conncert[conn->whichcert].sslpassword) + 1 > size)
+ fprintf(stderr, libpq_gettext("WARNING: sslpassword truncated\n"));
+ strncpy(buf, conn->conncert[conn->whichcert].sslpassword, size);
+ buf[size - 1] = '\0';
+ }
return strlen(buf);
}
else
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 82c18f870d..90bba2411e 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -351,6 +351,18 @@ typedef struct pg_conn_host
* found in password file. */
} pg_conn_host;
+/*
+ * pg_conn_host stores all information about each of possibly several hosts
+ * mentioned in the connection string. Most fields are derived by splitting
+ * the relevant connection parameter (e.g., pghost) at commas.
+ */
+typedef struct pg_conn_cert
+{
+ char *sslkey; /* client key filename */
+ char *sslcert; /* client certificate filename */
+ char *sslpassword; /* client key file password */
+} pg_conn_cert;
+
/*
* PGconn stores all the state data associated with a single connection
* to a backend.
@@ -548,6 +560,13 @@ struct pg_conn
bool ssl_cert_requested; /* Did the server ask us for a cert? */
bool ssl_cert_sent; /* Did we send one in reply? */
+ /* Support for multiple client certificate selection */
+ int nsslcert; /* # of client certificates provided */
+ int nsslkey; /* # of client keys provided */
+ int nsslpassword; /* # of client key passwords provided */
+ int whichcert; /* certificate selected to send to the server */
+ pg_conn_cert *conncert; /* details about each certificate group */
+
#ifdef USE_SSL
bool allow_ssl_try; /* Allowed to try SSL negotiation */
bool wait_ssl_try; /* Delay SSL negotiation until after
Hello
I would like to share an updated patch that adds a feature to libpq to automatically select the best client certificate to send to the server (if it requests one). This feature is inspired by this email discussion years ago: /messages/by-id/200905081539.n48Fdl2Y003286@no.baka.org, which makes it easier for a single client to communicate TLS with multiple TLS-enabled PostgreSQL servers with different certificate setups.
Instead of specifying just one sslcert, sslkey, or sslpassword, this patch allows multiple to be specified and libpq is able to pick the matching one to send to the PostgreSQL server based on the trusted CA names sent during TLS handshake.
If anyone finds it useful and would like to give it as try, I wrote a blog on how to test and verify this feature here: https://www.highgo.ca/2024/03/28/procedure-to-multiple-client-certificate-feature/
thank you
Best regards
Cary Huang
Attachments:
v3-0001-multiple_client_certificate_selection_support.patchapplication/octet-stream; name=v3-0001-multiple_client_certificate_selection_support.patchDownload
From 24f1377f457283dd996e04bef032f13578609333 Mon Sep 17 00:00:00 2001
From: Cary Huang <cary.huang@highgo.ca>
Date: Thu, 11 Apr 2024 13:44:13 -0700
Subject: [PATCH] multiple client certificate selection
---
configure | 11 ++
configure.ac | 1 +
meson.build | 1 +
src/include/pg_config.h.in | 3 +
src/interfaces/libpq/fe-connect.c | 112 +++++++++++
src/interfaces/libpq/fe-secure-openssl.c | 232 +++++++++++++++++++++--
src/interfaces/libpq/libpq-int.h | 19 ++
7 files changed, 367 insertions(+), 12 deletions(-)
diff --git a/configure b/configure
index 36feeafbb2..8b26c22be2 100755
--- a/configure
+++ b/configure
@@ -12599,6 +12599,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 57f734879e..685b22268f 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1359,6 +1359,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/meson.build b/meson.build
index 18b5be842e..3d337d3db1 100644
--- a/meson.build
+++ b/meson.build
@@ -1293,6 +1293,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 591e1ca3df..ce94a138f7 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -384,6 +384,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 01e49c6975..bba1144d8b 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -1670,6 +1670,100 @@ pqConnectOptions2(PGconn *conn)
goto oom_error;
}
+ /*
+ * Allocate memory for details about each client certificate candidate that we
+ * could choose to send to the server for verification.
+ */
+ conn->whichcert = 0;
+ if (conn->sslcert && conn->sslcert[0] != '\0')
+ conn->nsslcert = count_comma_separated_elems(conn->sslcert);
+
+ if (conn->sslkey && conn->sslkey[0] != '\0')
+ conn->nsslkey = count_comma_separated_elems(conn->sslkey);
+
+ if (conn->sslpassword && conn->sslpassword[0] != '\0')
+ conn->nsslpassword = count_comma_separated_elems(conn->sslpassword);
+
+ if (conn->nsslcert > 0)
+ {
+ /*
+ * conn->conncert is allocated only when multiple sslcert, sslkey or
+ * sslpassword are provided separated by comma
+ */
+ conn->conncert = (pg_conn_cert *)
+ calloc(conn->nsslcert, sizeof(pg_conn_cert));
+ if (conn->conncert == NULL)
+ goto oom_error;
+
+ /*
+ * Now we have one pg_conn_cert structure per possible certificate candidate.
+ * Fill in the sslcert, sslkey and sslpassword fields for each, by splitting
+ * the parameter strings
+ */
+ if (conn->sslcert && conn->sslcert[0] != '\0')
+ {
+ char *s = conn->sslcert;
+ bool more = true;
+
+ for (i = 0; i < conn->nsslcert && more; i++)
+ {
+ conn->conncert[i].sslcert = parse_comma_separated_list(&s, &more);
+ if (conn->conncert[i].sslcert == NULL)
+ goto oom_error;
+ }
+
+ /*
+ * Ensure the size matches
+ */
+ Assert(!more);
+ Assert(i == conn->nsslcert);
+ }
+
+ if (conn->sslkey && conn->sslkey[0] != '\0')
+ {
+ char *s = conn->sslkey;
+ bool more = true;
+
+ for (i = 0; i < conn->nsslkey && more; i++)
+ {
+ conn->conncert[i].sslkey = parse_comma_separated_list(&s, &more);
+ if (conn->conncert[i].sslkey == NULL)
+ goto oom_error;
+ }
+
+ /* Check for wrong number of sslkey items. */
+ if (more || i != conn->nsslcert)
+ {
+ conn->status = CONNECTION_BAD;
+ libpq_append_conn_error(conn, "could not match %d sslcert to %d sslkey values",
+ conn->nsslcert, conn->nsslkey);
+ return false;
+ }
+ }
+
+ if (conn->sslpassword && conn->sslpassword[0] != '\0')
+ {
+ char *s = conn->sslpassword;
+ bool more = true;
+
+ for (i = 0; i < conn->nsslpassword && more; i++)
+ {
+ conn->conncert[i].sslpassword = parse_comma_separated_list(&s, &more);
+ if (conn->conncert[i].sslpassword == NULL)
+ goto oom_error;
+ }
+
+ /* Check for wrong number of sslkey items. */
+ if (more || i != conn->nsslkey)
+ {
+ conn->status = CONNECTION_BAD;
+ libpq_append_conn_error(conn, "could not match %d sslpassword to %d sslkey values",
+ conn->nsslpassword, conn->nsslkey);
+ return false;
+ }
+ }
+ }
+
/*
* validate gssencmode option
*/
@@ -4496,6 +4590,24 @@ freePGconn(PGconn *conn)
termPQExpBuffer(&conn->errorMessage);
termPQExpBuffer(&conn->workBuffer);
+ /* Clean up conn->conncert structure */
+ if (conn->conncert)
+ {
+ for (int i = 0; i < conn->nsslcert; i++)
+ {
+ free(conn->conncert[i].sslcert);
+ free(conn->conncert[i].sslkey);
+ if (conn->conncert[i].sslpassword != NULL)
+ {
+ explicit_bzero(conn->conncert[i].sslpassword,
+ strlen(conn->conncert[i].sslpassword));
+ free(conn->conncert[i].sslpassword);
+ }
+ }
+ free(conn->conncert);
+ conn->conncert = NULL;
+ }
+
free(conn);
}
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index bf45a8edc3..53d4d2a925 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -454,23 +454,213 @@ 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 STACK_OF(X509)*
+load_certificate(const char* filename)
+{
+ FILE *file = fopen(filename, "r");
+ STACK_OF(X509) *certchain = NULL;
+ X509 *cert = NULL;
+
+ if (!file)
+ return NULL;
+
+ certchain = sk_X509_new_null();
+
+ cert = PEM_read_X509(file, NULL, NULL, NULL);
+ while (cert != NULL)
+ {
+ sk_X509_push(certchain, cert);
+ cert = PEM_read_X509(file, NULL, NULL, NULL);
+ }
+
+ fclose(file);
+
+ return certchain;
+}
+
+/*
+ * Utility function to find a suitable client certificate
+ *
+ * This function tries to find a client certificate in conn->conncert by
+ * matching issuer name with trusted CA subject name in peercas. Returns
+ * the client certificate index if found, -1 otherwise.
+ */
+static int
+find_client_certificate_by_issuer(const STACK_OF(X509_NAME) * peercas,
+ PGconn * conn)
+{
+ STACK_OF(X509) *cert_candidate = NULL;
+ int numcas = sk_X509_NAME_num(peercas);
+ int i = 0, j = 0, k = 0;
+
+ for (i = 0; i < conn->nsslcert; i++)
+ {
+ cert_candidate = load_certificate(conn->conncert[i].sslcert);
+ if (cert_candidate)
+ {
+ /*
+ * we should have a stack of one client certificate plus all
+ * possible intermediate CA certificates if present in the
+ * same certificate file. We will check if any of these
+ * certificate's issuer matches any of the trusted CA names
+ * provided by the server.
+ */
+ for (j = 0; j < sk_X509_num(cert_candidate); j++)
+ {
+ X509 *cert = sk_X509_value(cert_candidate, j);
+
+ for (k = 0; k < numcas; k++)
+ {
+ X509_NAME *ca_name = sk_X509_NAME_value(peercas, k);
+ if(!X509_NAME_cmp(X509_get_issuer_name(cert), ca_name))
+ {
+ /*
+ * one of the candidate client certificate's or
+ * intermediate certificate's issuer matches one
+ * of the trusted CA names, it should be trusted by
+ * the server.
+ */
+ conn->whichcert = i;
+ sk_X509_pop_free(cert_candidate, X509_free);
+ return conn->whichcert;
+ }
+ }
+
+ }
+ sk_X509_pop_free(cert_candidate, X509_free);
+ }
+ }
+ return -1;
+}
+#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 receiving the CertificateRequest. If the server sends its CA list,
+ * 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
+ int clientcertindex = -1;
+ /* obtain a list of CA list sent from the server, if any */
+ const STACK_OF(X509_NAME) * peercas = SSL_get0_peer_CA_list(ssl);
+ struct stat keystat;
+#endif
+
+ /* mark that the server has requested a client certificate */
conn->ssl_cert_requested = true;
+#ifdef HAVE_SSL_GET0_PEER_CA_LIST
+
+ if (strcmp(conn->sslcertmode, "disable") && conn->nsslcert > 1 && peercas)
+ {
+ clientcertindex = find_client_certificate_by_issuer(peercas, conn);
+ if (clientcertindex == -1)
+ {
+ libpq_append_conn_error(conn, "Server requests a client certificate but no suitable "
+ "certificate is found from sslcert list provided");
+ conn->ssl_cert_sent = false;
+ return 1;
+ }
+
+ /*
+ * found a client certificate, load it to ssl. Note that we do not check
+ * return value here because this client certificate has already been
+ * checked inside find_client_certificate_by_issuer() function.
+ */
+ SSL_use_certificate_chain_file(ssl, conn->conncert[clientcertindex].sslcert);
+
+ /*
+ * we will load the private key at index clientcertindex
+ */
+ if (conn->conncert[clientcertindex].sslkey[0] != '\0')
+ {
+ if (stat(conn->conncert[clientcertindex].sslkey, &keystat) != 0)
+ {
+ if (errno == ENOENT)
+ libpq_append_conn_error(conn, "certificate present, but not private key file \"%s\"",
+ conn->conncert[clientcertindex].sslkey);
+ else
+ libpq_append_conn_error(conn, "could not stat private key file \"%s\": %m",
+ conn->conncert[clientcertindex].sslkey);
+ return 1;
+
+ }
+ /* Key file must be a regular file */
+ if (!S_ISREG(keystat.st_mode))
+ {
+ libpq_append_conn_error(conn, "private key file \"%s\" is not a regular file",
+ conn->conncert[clientcertindex].sslkey);
+ return 1;
+ }
+#if !defined(WIN32) && !defined(__CYGWIN__)
+ if (keystat.st_uid == 0 ?
+ keystat.st_mode & (S_IWGRP | S_IXGRP | S_IRWXO) :
+ keystat.st_mode & (S_IRWXG | S_IRWXO))
+ {
+ libpq_append_conn_error(conn,
+ "private key file \"%s\" has group or world access; file must have permissions u=rw (0600) or less if owned by the current user, or permissions u=rw,g=r (0640) or less if owned by root",
+ conn->conncert[clientcertindex].sslkey);
+ return -1;
+ }
+#endif
+ if (SSL_use_PrivateKey_file(ssl, conn->conncert[clientcertindex].sslkey,
+ SSL_FILETYPE_PEM) != 1)
+ {
+ char *err = SSLerrmessage(ERR_get_error());
+ /*
+ * We'll try to load the file in DER (binary ASN.1) format if PEM
+ * format failed to load.
+ */
+ if (SSL_use_PrivateKey_file(ssl, conn->conncert[clientcertindex].sslkey,
+ SSL_FILETYPE_ASN1) != 1)
+ {
+ libpq_append_conn_error(conn, "could not load private key file \"%s\": %s",
+ conn->conncert[clientcertindex].sslkey, err);
+ SSLerrfree(err);
+ conn->ssl_cert_sent = false;
+ return 1;
+ }
+ SSLerrfree(err);
+ }
+ }
+ else
+ {
+ libpq_append_conn_error(conn, "A suitable client certificate exists but "
+ "no matching private key is supplied");
+ conn->ssl_cert_sent = false;
+ return 1;
+ }
+
+ /* verify that the cert and key go together */
+ if (SSL_check_private_key(ssl) != 1)
+ {
+ char *err = SSLerrmessage(ERR_get_error());
+
+ libpq_append_conn_error(conn, "certificate does not match private key file \"%s\": %s",
+ conn->conncert[clientcertindex].sslcert, err);
+ SSLerrfree(err);
+ return 1;
+ }
+ }
+#endif
+
/* Do we have a certificate loaded to send back? */
if (SSL_get_certificate(ssl))
conn->ssl_cert_sent = true;
@@ -1127,8 +1317,10 @@ initialize_SSL(PGconn *conn)
}
/* Read the client certificate file */
- if (conn->sslcert && strlen(conn->sslcert) > 0)
+ if (conn->sslcert && strlen(conn->sslcert) > 0 && conn->nsslcert == 1)
+ {
strlcpy(fnbuf, conn->sslcert, sizeof(fnbuf));
+ }
else if (have_homedir)
snprintf(fnbuf, sizeof(fnbuf), "%s/%s", homedir, USER_CERT_FILE);
else
@@ -1240,7 +1432,7 @@ initialize_SSL(PGconn *conn)
* colon in the name. The exception is if the second character is a colon,
* in which case it can be a Windows filename with drive specification.
*/
- if (have_cert && conn->sslkey && strlen(conn->sslkey) > 0)
+ if (have_cert && conn->sslkey && strlen(conn->sslkey) > 0 && conn->nsslkey == 1)
{
#ifdef USE_SSL_ENGINE
if (strchr(conn->sslkey, ':')
@@ -1984,12 +2176,28 @@ err:
int
PQdefaultSSLKeyPassHook_OpenSSL(char *buf, int size, PGconn *conn)
{
- if (conn && conn->sslpassword)
+ if (conn && conn->sslpassword && conn->nsslpassword > 0)
{
- if (strlen(conn->sslpassword) + 1 > size)
- fprintf(stderr, libpq_gettext("WARNING: sslpassword truncated\n"));
- strncpy(buf, conn->sslpassword, size);
- buf[size - 1] = '\0';
+ /* if there is only one password, use conn->sslpassword */
+ if (conn->nsslpassword == 1)
+ {
+ if (strlen(conn->sslpassword) + 1 > size)
+ fprintf(stderr, libpq_gettext("WARNING: sslpassword truncated\n"));
+ strncpy(buf, conn->sslpassword, size);
+ buf[size - 1] = '\0';
+ }
+ else
+ {
+ /*
+ * if there are more than one passwords, choose one
+ * from conn->conncert based on conn->whichcert
+ */
+ if (conn->whichcert >= 0 &&
+ strlen(conn->conncert[conn->whichcert].sslpassword) + 1 > size)
+ fprintf(stderr, libpq_gettext("WARNING: sslpassword truncated\n"));
+ strncpy(buf, conn->conncert[conn->whichcert].sslpassword, size);
+ buf[size - 1] = '\0';
+ }
return strlen(buf);
}
else
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 9c05f11a6e..5716cb05af 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -351,6 +351,18 @@ typedef struct pg_conn_host
* found in password file. */
} pg_conn_host;
+/*
+ * pg_conn_host stores all information about each of possibly several hosts
+ * mentioned in the connection string. Most fields are derived by splitting
+ * the relevant connection parameter (e.g., pghost) at commas.
+ */
+typedef struct pg_conn_cert
+{
+ char *sslkey; /* client key filename */
+ char *sslcert; /* client certificate filename */
+ char *sslpassword; /* client key file password */
+} pg_conn_cert;
+
/*
* PGconn stores all the state data associated with a single connection
* to a backend.
@@ -552,6 +564,13 @@ struct pg_conn
bool ssl_cert_requested; /* Did the server ask us for a cert? */
bool ssl_cert_sent; /* Did we send one in reply? */
+ /* Support for multiple client certificate selection */
+ int nsslcert; /* # of client certificates provided */
+ int nsslkey; /* # of client keys provided */
+ int nsslpassword; /* # of client key passwords provided */
+ int whichcert; /* certificate selected to send to the server */
+ pg_conn_cert *conncert; /* details about each certificate group */
+
#ifdef USE_SSL
bool allow_ssl_try; /* Allowed to try SSL negotiation */
bool wait_ssl_try; /* Delay SSL negotiation until after
--
2.17.1