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

