Thread

  1. [PATCH] multiple client certificate selection

    Cary Huang <cary.huang@highgo.ca> — 2024-04-11T20:44:13Z

    ---
     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--