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