[PATCH] multiple client certificate selection
Cary Huang <cary.huang@highgo.ca>
From: Cary Huang <cary.huang@highgo.ca>
To:
Date: 2024-04-11T20:44:13Z
Lists: pgsql-hackers
---
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
------=_Part_589517_1962907805.1712870640529--