From 24f1377f457283dd996e04bef032f13578609333 Mon Sep 17 00:00:00 2001 From: Cary Huang 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