diff --git a/configure b/configure
index 5aec0afa9ab..1d2522483a8 100755
--- a/configure
+++ b/configure
@@ -13177,7 +13177,7 @@ fi
done
# Function introduced in OpenSSL 1.1.1, not in LibreSSL.
- for ac_func in X509_get_signature_info SSL_CTX_set_num_tickets SSL_CTX_set_keylog_callback
+ for ac_func in X509_get_signature_info SSL_CTX_set_num_tickets SSL_CTX_set_keylog_callback SSL_CTX_set_client_hello_cb
do :
as_ac_var=`$as_echo "ac_cv_func_$ac_func" | $as_tr_sh`
ac_fn_c_check_func "$LINENO" "$ac_func" "$as_ac_var"
diff --git a/configure.ac b/configure.ac
index fead9a6ce99..70ddcaabc44 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1443,7 +1443,7 @@ if test "$with_ssl" = openssl ; then
# Function introduced in OpenSSL 1.0.2, not in LibreSSL.
AC_CHECK_FUNCS([SSL_CTX_set_cert_cb])
# Function introduced in OpenSSL 1.1.1, not in LibreSSL.
- AC_CHECK_FUNCS([X509_get_signature_info SSL_CTX_set_num_tickets SSL_CTX_set_keylog_callback])
+ AC_CHECK_FUNCS([X509_get_signature_info SSL_CTX_set_num_tickets SSL_CTX_set_keylog_callback SSL_CTX_set_client_hello_cb])
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/runtime.sgml b/doc/src/sgml/runtime.sgml
index 422a3544f94..cb332913ab1 100644
--- a/doc/src/sgml/runtime.sgml
+++ b/doc/src/sgml/runtime.sgml
@@ -2464,6 +2464,12 @@ pg_dumpall -p 5432 | psql -d postgres -p 5433
client certificate must not be on this list
+
+ $PGDATA/pg_hosts.conf
+ SNI configuration
+ defines which certificates to use for which server hostname
+
+
@@ -2591,6 +2597,123 @@ openssl x509 -req -in server.csr -text -days 365 \
+
+ SNI Configuration
+
+
+ PostgreSQL can be configured for Server Name
+ Indication, SNI, using the pg_hosts.conf
+ configuration file. PostgreSQL inspects the TLS
+ hostname extension in the SSL connection handshake, and selects the right
+ TLS certificate, key and CA certificate to use for the connection based on
+ the hosts which are defined in pg_hosts.conf.
+
+
+
+ SNI configuration is defined in the hosts configuration file,
+ pg_hosts.conf, which is stored in the cluster's
+ data directory. The hosts configuration file contains lines of the general
+ forms:
+
+hostname SSL_certificate SSL_key SSL_CA_certificate SSL_passphrase_cmd SSL_passphrase_cmd_reload
+include file
+include_if_exists file
+include_dir directory
+
+ Comments, whitespace and line continuations are handled in the same way as
+ in pg_hba.conf. hostname
+ is matched against the hostname TLS extension in the SSL handshake.
+ SSL_certificate,
+ SSL_key,
+ SSL_CA_certificate,
+ SSL_passphrase_cmd, and
+ SSL_passphrase_cmd_reload
+ are treated like
+ ,
+ ,
+ ,
+ , and
+ respectively.
+ All fields except SSL_CA_certificate,
+ SSL_passphrase_cmd and
+ SSL_passphrase_cmd_reload are required. If
+ SSL_passphrase_cmd is defined but not
+ SSL_passphrase_cmd_reload then the default
+ value for SSL_passphrase_cmd_reload is
+ off.
+
+
+
+ hostname should either be set to the literal
+ hostname for the connection, /no_sni/ or *.
+ contains details on how these values are
+ used.
+
+ Hostname setting values
+
+
+
+ Host Entry
+ sslsni
+ Description
+
+
+
+
+
+ *
+ Not required
+
+ Default host, matches all connections.
+
+
+
+
+ /no_sni/
+ Not allowed
+
+ Certificate and key to use for connections with no
+ sslsni defined.
+
+
+
+
+ hostname
+ Required
+
+ Certificate and key to use for connections to the host specified in
+ the connection. Multiple hostnames can be defined by using a comma
+ separated list. The certificate and key will be used for connections
+ to all hosts in the list.
+
+
+
+
+
+
+
+
+
+ If pg_hosts.conf is empty, or missing, then the SSL
+ configuration in postgresql.conf will be used for all
+ connections. If pg_hosts.conf is non-empty then it
+ will take precedence over certificate and key settings in
+ postgresql.conf.
+
+
+
+ It is currently not possible to set different clientname
+ values for the different certificates. Any clientname
+ setting in pg_hba.conf will be applied during
+ authentication regardless of which set of certificates have been loaded
+ via an SNI enabled connection.
+
+
+
+ The CRL configuration in postgresql.conf is applied
+ on all connections regardless of if they use SNI or not.
+
+
diff --git a/meson.build b/meson.build
index 7ee900198db..10cdd26cc2a 100644
--- a/meson.build
+++ b/meson.build
@@ -1684,6 +1684,7 @@ if sslopt in ['auto', 'openssl']
['X509_get_signature_info'],
['SSL_CTX_set_num_tickets'],
['SSL_CTX_set_keylog_callback'],
+ ['SSL_CTX_set_client_hello_cb'],
]
are_openssl_funcs_complete = true
diff --git a/src/backend/Makefile b/src/backend/Makefile
index ba53cd9d998..162d3f1f2a9 100644
--- a/src/backend/Makefile
+++ b/src/backend/Makefile
@@ -221,6 +221,7 @@ endif
$(MAKE) -C utils install-data
$(INSTALL_DATA) $(srcdir)/libpq/pg_hba.conf.sample '$(DESTDIR)$(datadir)/pg_hba.conf.sample'
$(INSTALL_DATA) $(srcdir)/libpq/pg_ident.conf.sample '$(DESTDIR)$(datadir)/pg_ident.conf.sample'
+ $(INSTALL_DATA) $(srcdir)/libpq/pg_hosts.conf.sample '$(DESTDIR)$(datadir)/pg_hosts.conf.sample'
$(INSTALL_DATA) $(srcdir)/utils/misc/postgresql.conf.sample '$(DESTDIR)$(datadir)/postgresql.conf.sample'
ifeq ($(with_llvm), yes)
@@ -280,6 +281,7 @@ endif
$(MAKE) -C utils uninstall-data
rm -f '$(DESTDIR)$(datadir)/pg_hba.conf.sample' \
'$(DESTDIR)$(datadir)/pg_ident.conf.sample' \
+ '$(DESTDIR)$(datadir)/pg_hosts.conf.sample' \
'$(DESTDIR)$(datadir)/postgresql.conf.sample'
ifeq ($(with_llvm), yes)
$(call uninstall_llvm_module,postgres)
diff --git a/src/backend/commands/variable.c b/src/backend/commands/variable.c
index 4440aff4925..8afd252fc8c 100644
--- a/src/backend/commands/variable.c
+++ b/src/backend/commands/variable.c
@@ -1258,6 +1258,27 @@ check_ssl(bool *newval, void **extra, GucSource source)
return true;
}
+bool
+check_ssl_sni(bool *newval, void **extra, GucSource source)
+{
+#ifndef USE_SSL
+ if (*newval)
+ {
+ GUC_check_errmsg("SSL is not supported by this build");
+ return false;
+ }
+#else
+#ifndef HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
+ if (*newval)
+ {
+ GUC_check_errmsg("SNI requires OpenSSL 1.1.1 or later");
+ return false;
+ }
+#endif
+#endif
+ return true;
+}
+
bool
check_standard_conforming_strings(bool *newval, void **extra, GucSource source)
{
diff --git a/src/backend/libpq/be-secure-common.c b/src/backend/libpq/be-secure-common.c
index c074556dbfc..ad04bedaa1d 100644
--- a/src/backend/libpq/be-secure-common.c
+++ b/src/backend/libpq/be-secure-common.c
@@ -26,18 +26,25 @@
#include "common/string.h"
#include "libpq/libpq.h"
#include "storage/fd.h"
+#include "utils/builtins.h"
+#include "utils/guc.h"
+
+static HostsLine *parse_hosts_line(TokenizedAuthLine *tok_line, int elevel);
/*
* Run ssl_passphrase_command
*
* prompt will be substituted for %p. is_server_start determines the loglevel
- * of error messages.
+ * of error messages from executing the command, the loglevel for failures in
+ * param substitution will be ERROR regardless of is_server_start. The actual
+ * command used depends on the configuration for the current host.
*
* The result will be put in buffer buf, which is of size size. The return
* value is the length of the actual result.
*/
int
-run_ssl_passphrase_command(const char *prompt, bool is_server_start, char *buf, int size)
+run_ssl_passphrase_command(const char *cmd, const char *prompt,
+ bool is_server_start, char *buf, int size)
{
int loglevel = is_server_start ? ERROR : LOG;
char *command;
@@ -49,7 +56,7 @@ run_ssl_passphrase_command(const char *prompt, bool is_server_start, char *buf,
Assert(size > 0);
buf[0] = '\0';
- command = replace_percent_placeholders(ssl_passphrase_command, "ssl_passphrase_command", "p", prompt);
+ command = replace_percent_placeholders(cmd, "ssl_passphrase_command", "p", prompt);
fh = OpenPipeStream(command, "r");
if (fh == NULL)
@@ -175,3 +182,257 @@ check_ssl_key_file_permissions(const char *ssl_key_file, bool isServerStart)
return true;
}
+
+/*
+ * parse_hosts_line
+ *
+ * Parses a loaded line from the pg_hosts.conf configuration and pulls out the
+ * hostname, certificate, key and CA parts in order to build an SNI config in
+ * the TLS backend. Validation of the parsed values is left for the TLS backend
+ * to implement.
+ */
+static HostsLine *
+parse_hosts_line(TokenizedAuthLine *tok_line, int elevel)
+{
+ HostsLine *parsedline;
+ List *tokens;
+ ListCell *field;
+ AuthToken *token;
+
+ parsedline = palloc0(sizeof(HostsLine));
+ parsedline->sourcefile = pstrdup(tok_line->file_name);
+ parsedline->linenumber = tok_line->line_num;
+ parsedline->rawline = pstrdup(tok_line->raw_line);
+ parsedline->hostnames = NIL;
+
+ /* Initialize optional fields */
+ parsedline->ssl_passphrase_cmd = NULL;
+ parsedline->ssl_passphrase_reload = false;
+
+ /* Hostname */
+ field = list_head(tok_line->fields);
+ tokens = lfirst(field);
+ foreach_ptr(AuthToken, hostname, tokens)
+ {
+ if ((tokens->length > 1) &&
+ (strcmp(hostname->string, "*") == 0 || strcmp(hostname->string, "/no_sni/") == 0))
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("default and non-SNI entries cannot be mixed with other entries"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+
+ parsedline->hostnames = lappend(parsedline->hostnames, pstrdup(hostname->string));
+ }
+
+ /* SSL Certificate (Required) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("missing entry at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ tokens = lfirst(field);
+ if (tokens->length > 1)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("multiple values specified for SSL certificate"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ token = linitial(tokens);
+ parsedline->ssl_cert = pstrdup(token->string);
+
+ /* SSL key (Required) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("missing entry at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ tokens = lfirst(field);
+ if (tokens->length > 1)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("multiple values specified for SSL key"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ token = linitial(tokens);
+ parsedline->ssl_key = pstrdup(token->string);
+
+ /* SSL CA (optional) */
+ field = lnext(tok_line->fields, field);
+ if (!field)
+ return parsedline;
+ tokens = lfirst(field);
+ if (tokens->length > 1)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("multiple values specified for SSL CA"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ token = linitial(tokens);
+ parsedline->ssl_ca = pstrdup(token->string);
+
+ /* SSL Passphrase Command (optional) */
+ field = lnext(tok_line->fields, field);
+ if (field)
+ {
+ tokens = lfirst(field);
+ if (tokens->length > 1)
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("multiple values specified for SSL passphrase command"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ token = linitial(tokens);
+ parsedline->ssl_passphrase_cmd = pstrdup(token->string);
+
+ /*
+ * SSL Passphrase Command support reload (optional). This field is
+ * only supported if there was a passphrase command parsed first, so
+ * nest it under the previous token.
+ */
+ field = lnext(tok_line->fields, field);
+ if (field)
+ {
+ tokens = lfirst(field);
+ token = linitial(tokens);
+
+ /*
+ * There should be no more tokens after this, if there are break
+ * parsing and report error to avoid silently accepting incorrect
+ * config.
+ */
+ if (lnext(tok_line->fields, field))
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("extra fields at end of line"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+
+ if (tokens->length > 1 || !parse_bool(token->string, &parsedline->ssl_passphrase_reload))
+ {
+ ereport(elevel,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("incorrect syntax for boolean value SSL_passphrase_cmd_reload"),
+ errcontext("line %d of configuration file \"%s\"",
+ tok_line->line_num, tok_line->file_name));
+ return NULL;
+ }
+ }
+ }
+
+ return parsedline;
+}
+
+/*
+ * load_hosts
+ *
+ * Reads and parses the pg_hosts.conf configuration file and passes back a List
+ * of HostsLine elements containing the parsed lines, or NIL in case of an empty
+ * file. The list is returned in the hosts parameter. The function will return
+ * a HostsFileLoadResult value detailing the result of the operation. When
+ * the hosts configuration failed to load, the err_msg variable may have more
+ * information in case it was passed as non-NULL.
+ */
+int
+load_hosts(List **hosts, char **err_msg)
+{
+ FILE *file;
+ ListCell *line;
+ List *hosts_lines = NIL;
+ List *parsed_lines = NIL;
+ HostsLine *newline;
+ bool ok = true;
+
+ /*
+ * If we cannot return results then error out immediately. This implies
+ * API misuse or a similar kind of programmer error.
+ */
+ if (!hosts)
+ {
+ if (err_msg)
+ *err_msg = psprintf("cannot load config from \"%s\", return variable missing",
+ HostsFileName);
+ return HOSTSFILE_LOAD_FAILED;
+ }
+ *hosts = NIL;
+
+ /*
+ * This is not an auth file per se, but it is using the same file format
+ * as the pg_hba and pg_ident files and thus the same code infrastructure.
+ * A future TODO might be to rename the supporting code with a more
+ * generic name?
+ */
+ file = open_auth_file(HostsFileName, LOG, 0, err_msg);
+ if (file == NULL)
+ {
+ if (errno == ENOENT)
+ return HOSTSFILE_MISSING;
+
+ return HOSTSFILE_LOAD_FAILED;
+ }
+
+ tokenize_auth_file(HostsFileName, file, &hosts_lines, LOG, 0);
+
+ foreach(line, hosts_lines)
+ {
+ TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line);
+
+ /*
+ * Mark processing as not-ok in case lines are found with errors in
+ * tokenization (.err_msg is set) or during parsing.
+ */
+ if ((tok_line->err_msg != NULL) ||
+ ((newline = parse_hosts_line(tok_line, LOG)) == NULL))
+ {
+ ok = false;
+ continue;
+ }
+
+ parsed_lines = lappend(parsed_lines, newline);
+ }
+
+ /* Free memory from tokenizer */
+ free_auth_file(file, 0);
+ *hosts = parsed_lines;
+
+ if (!ok)
+ {
+ if (err_msg)
+ *err_msg = psprintf("loading config from \"%s\" failed due to parsing error",
+ HostsFileName);
+ return HOSTSFILE_LOAD_FAILED;
+ }
+
+ if (parsed_lines == NIL)
+ return HOSTSFILE_EMPTY;
+
+ return HOSTSFILE_LOAD_OK;
+}
diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c
index 14c6532bb16..a3e222f3a3d 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -27,6 +27,7 @@
#include
#include
+#include "common/hashfn.h"
#include "common/string.h"
#include "libpq/libpq.h"
#include "miscadmin.h"
@@ -52,6 +53,27 @@
#endif
#include
+/*
+ * Simplehash for tracking configured hostnames to guard against duplicate
+ * entries. Each list of hosts is traversed and added to the hash during
+ * parsing and if a duplicate error is detected an error will be thrown.
+ */
+typedef struct
+{
+ uint32 status;
+ const char *hostname;
+} HostCacheEntry;
+static uint32 host_cache_pointer(const char *key);
+#define SH_PREFIX host_cache
+#define SH_ELEMENT_TYPE HostCacheEntry
+#define SH_KEY_TYPE const char *
+#define SH_KEY hostname
+#define SH_HASH_KEY(tb, key) host_cache_pointer(key)
+#define SH_EQUAL(tb, a, b) (pg_strcasecmp(a, b) == 0)
+#define SH_SCOPE static inline
+#define SH_DECLARE
+#define SH_DEFINE
+#include "lib/simplehash.h"
/* default init hook can be overridden by a shared library */
static void default_openssl_tls_init(SSL_CTX *context, bool isServerStart);
@@ -78,10 +100,34 @@ static bool initialize_dh(SSL_CTX *context, bool isServerStart);
static bool initialize_ecdh(SSL_CTX *context, bool isServerStart);
static const char *SSLerrmessageExt(unsigned long ecode, const char *replacement);
static const char *SSLerrmessage(unsigned long ecode);
+static bool init_host_context(HostsLine *host, bool isServerStart);
+static void host_context_cleanup_cb(void *arg);
+#ifdef HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
+static int sni_clienthello_cb(SSL *ssl, int *al, void *arg);
+#endif
static char *X509_NAME_to_cstring(X509_NAME *name);
static SSL_CTX *SSL_context = NULL;
+static MemoryContext SSL_hosts_memcxt = NULL;
+static struct hosts
+{
+ /*
+ * List of HostsLine structures containing SSL configurations for
+ * connections with hostnames defined in the SNI extension.
+ */
+ List *sni;
+
+ /* The SSL configuration to use for connections without SNI */
+ HostsLine *no_sni;
+
+ /*
+ * The default SSL configuration to use as a fallback in case no hostname
+ * matches the supplied hostname in the SNI extension.
+ */
+ HostsLine *default_host;
+} *SSL_hosts;
+
static bool dummy_ssl_passwd_cb_called = false;
static bool ssl_is_server_start;
@@ -104,9 +150,228 @@ struct CallbackErr
int
be_tls_init(bool isServerStart)
{
- SSL_CTX *context;
+ List *pg_hosts = NIL;
+ ListCell *line;
+ MemoryContext oldcxt;
+ MemoryContext host_memcxt = NULL;
+ MemoryContextCallback *host_memcxt_cb;
+ char *err_msg = NULL;
+ int res;
+ struct hosts *new_hosts;
+ SSL_CTX *context = NULL;
int ssl_ver_min = -1;
int ssl_ver_max = -1;
+ host_cache_hash *host_cache = NULL;
+
+ /*
+ * Since we don't know which host we're using until the ClientHello is
+ * sent, ssl_loaded_verify_locations *always* starts out as false. The
+ * only place it's set to true is in sni_clienthello_cb().
+ */
+ ssl_loaded_verify_locations = false;
+
+ host_memcxt = AllocSetContextCreate(CurrentMemoryContext,
+ "hosts file parser context",
+ ALLOCSET_SMALL_SIZES);
+ oldcxt = MemoryContextSwitchTo(host_memcxt);
+
+ /* Allocate a tentative replacement for SSL_hosts. */
+ new_hosts = palloc0_object(struct hosts);
+
+ /*
+ * Register a reset callback for the memory context which is responsible
+ * for freeing OpenSSL managed allocations upon context deletion. The
+ * callback is allocated here to make sure it gets cleaned up along with
+ * the memory context it's registered for.
+ */
+ host_memcxt_cb = palloc0_object(MemoryContextCallback);
+ host_memcxt_cb->func = host_context_cleanup_cb;
+ host_memcxt_cb->arg = new_hosts;
+ MemoryContextRegisterResetCallback(host_memcxt, host_memcxt_cb);
+
+ /*
+ * If ssl_sni is enabled, attempt to load and parse TLS configuration from
+ * the pg_hosts.conf file with the set of hosts returned as a list. If
+ * there are hosts configured they take precedence over the configuration
+ * in postgresql.conf. Make sure to allocate the parsed rows in their own
+ * memory context so that we can delete them easily in case parsing fails.
+ * If ssl_sni is disabled then set the state accordingly to make sure we
+ * instead parse the config from postgresql.conf.
+ *
+ * The reason for not doing everything in this if-else conditional is that
+ * we want to use the same processing of postgresql.conf for when ssl_sni
+ * is off as well as when it's on but the hostsfile is missing etc. Thus
+ * we set res to the state and continue with a new conditional instead of
+ * duplicating logic and risk it diverging over time.
+ */
+ if (ssl_sni)
+ {
+ /*
+ * The GUC check hook should have already blocked this but to be on
+ * the safe side we doublecheck here.
+ */
+#ifndef HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("ssl_sni is not supported with LibreSSL"));
+ goto error;
+#endif
+
+ /* Attempt to load configuration from pg_hosts.conf */
+ res = load_hosts(&pg_hosts, &err_msg);
+
+ /*
+ * pg_hosts.conf is not required to contain configuration, but if it
+ * does we error out in case it fails to load rather than continue to
+ * try the postgresql.conf configuration to avoid silently falling
+ * back on an undesired configuration.
+ */
+ if (res == HOSTSFILE_LOAD_FAILED)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("could not load \"%s\": %s", "pg_hosts.conf",
+ err_msg ? err_msg : "unknown error"));
+ goto error;
+ }
+ }
+ else
+ res = HOSTSFILE_DISABLED;
+
+ /*
+ * Loading and parsing the hosts file was successful, create configs for
+ * each host entry and add to the list of hosts to be checked during
+ * login.
+ */
+ if (res == HOSTSFILE_LOAD_OK)
+ {
+ Assert(ssl_sni);
+
+ foreach(line, pg_hosts)
+ {
+ HostsLine *host = lfirst(line);
+
+ if (!init_host_context(host, isServerStart))
+ goto error;
+
+ /*
+ * The hostname in the config will be set to NULL for the default
+ * host as well as in configs used for non-SNI connections. Lists
+ * of hostnames in pg_hosts.conf are not allowed to contain the
+ * default '*' entry or a '/no_sni/' entry and this is checked
+ * during parsing. Thus we can inspect the head of the hostnames
+ * list for these since they will never be anywhere else.
+ */
+ if (strcmp(linitial(host->hostnames), "*") == 0)
+ {
+ if (new_hosts->default_host)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("multiple default hosts specified"),
+ errcontext("line %d of configuration file \"%s\"",
+ host->linenumber, host->sourcefile));
+ goto error;
+ }
+
+ new_hosts->default_host = host;
+ }
+ else if (strcmp(linitial(host->hostnames), "/no_sni/") == 0)
+ {
+ if (new_hosts->no_sni)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("multiple no_sni hosts specified"),
+ errcontext("line %d of configuration file \"%s\"",
+ host->linenumber, host->sourcefile));
+ goto error;
+ }
+
+ new_hosts->no_sni = host;
+ }
+ else
+ {
+ /* Check the hostnames for duplicates */
+ if (!host_cache)
+ host_cache = host_cache_create(host_memcxt, 32, NULL);
+
+ foreach_ptr(char, hostname, host->hostnames)
+ {
+ HostCacheEntry *entry;
+ bool found;
+
+ entry = host_cache_insert(host_cache, hostname, &found);
+ if (found)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("multiple entries for host \"%s\" specified",
+ hostname),
+ errcontext("line %d of configuration file \"%s\"",
+ host->linenumber, host->sourcefile));
+ goto error;
+ }
+ else
+ entry->hostname = pstrdup(hostname);
+ }
+
+ /*
+ * At this point we know we have a configuration with a list
+ * of distinct 1..n hostnames for literal string matching with
+ * the SNI extension from the user.
+ */
+ new_hosts->sni = lappend(new_hosts->sni, host);
+ }
+ }
+ }
+
+ /*
+ * If SNI is disabled, then we load configuration from postgresql.conf. If
+ * SNI is enabled but the pg_hosts.conf file doesn't exist, or is empty,
+ * then we also load the config from postgresql.conf.
+ */
+ else if (res == HOSTSFILE_DISABLED || res == HOSTSFILE_EMPTY || res == HOSTSFILE_MISSING)
+ {
+ HostsLine *pgconf = palloc0(sizeof(HostsLine));
+
+#ifdef USE_ASSERT_CHECKING
+ if (res == HOSTSFILE_DISABLED)
+ Assert(ssl_sni == false);
+#endif
+
+ pgconf->ssl_cert = ssl_cert_file;
+ pgconf->ssl_key = ssl_key_file;
+ pgconf->ssl_ca = ssl_ca_file;
+ pgconf->ssl_passphrase_cmd = ssl_passphrase_command;
+ pgconf->ssl_passphrase_reload = ssl_passphrase_command_supports_reload;
+
+ if (!init_host_context(pgconf, isServerStart))
+ goto error;
+
+ /*
+ * If postgresql.conf is used to configure SSL then by definition it
+ * will be the default context as we don't have per-host config.
+ */
+ new_hosts->default_host = pgconf;
+ }
+
+ /*
+ * Make sure we have at least one configuration loaded to use, without
+ * that we cannot drive a connection so exit.
+ */
+ if (new_hosts->sni == NIL && !new_hosts->default_host && !new_hosts->no_sni)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("no SSL configurations loaded"),
+ /*- translator: The two %s contain filenames */
+ errhint("If ssl_sni is enabled then add configuration to \"%s\", else \"%s\"",
+ "pg_hosts.conf", "postgresql.conf"));
+ goto error;
+ }
+
+#ifdef HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
/*
* Create a new SSL context into which we'll load all the configuration
@@ -126,6 +391,22 @@ be_tls_init(bool isServerStart)
SSLerrmessage(ERR_get_error()))));
goto error;
}
+#else
+
+ /*
+ * If the client hello callback isn't supported we want to use the default
+ * context as the one to drive the handshake so avoid creating a new one
+ * and use the already existing default one instead.
+ */
+ context = new_hosts->default_host->ssl_ctx;
+
+ /*
+ * Since we don't allocate a new SSL_CTX here like we do when SNI has been
+ * enabled we need to bump the reference count on context to avoid double
+ * free of the context when using the same cleanup logic across the cases.
+ */
+ SSL_CTX_up_ref(context);
+#endif
/*
* Disable OpenSSL's moving-write-buffer sanity check, because it causes
@@ -133,60 +414,6 @@ be_tls_init(bool isServerStart)
*/
SSL_CTX_set_mode(context, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
- /*
- * Call init hook (usually to set password callback)
- */
- (*openssl_tls_init_hook) (context, isServerStart);
-
- /* used by the callback */
- ssl_is_server_start = isServerStart;
-
- /*
- * Load and verify server's certificate and private key
- */
- if (SSL_CTX_use_certificate_chain_file(context, ssl_cert_file) != 1)
- {
- ereport(isServerStart ? FATAL : LOG,
- (errcode(ERRCODE_CONFIG_FILE_ERROR),
- errmsg("could not load server certificate file \"%s\": %s",
- ssl_cert_file, SSLerrmessage(ERR_get_error()))));
- goto error;
- }
-
- if (!check_ssl_key_file_permissions(ssl_key_file, isServerStart))
- goto error;
-
- /*
- * OK, try to load the private key file.
- */
- dummy_ssl_passwd_cb_called = false;
-
- if (SSL_CTX_use_PrivateKey_file(context,
- ssl_key_file,
- SSL_FILETYPE_PEM) != 1)
- {
- if (dummy_ssl_passwd_cb_called)
- ereport(isServerStart ? FATAL : LOG,
- (errcode(ERRCODE_CONFIG_FILE_ERROR),
- errmsg("private key file \"%s\" cannot be reloaded because it requires a passphrase",
- ssl_key_file)));
- else
- ereport(isServerStart ? FATAL : LOG,
- (errcode(ERRCODE_CONFIG_FILE_ERROR),
- errmsg("could not load private key file \"%s\": %s",
- ssl_key_file, SSLerrmessage(ERR_get_error()))));
- goto error;
- }
-
- if (SSL_CTX_check_private_key(context) != 1)
- {
- ereport(isServerStart ? FATAL : LOG,
- (errcode(ERRCODE_CONFIG_FILE_ERROR),
- errmsg("check of private key failed: %s",
- SSLerrmessage(ERR_get_error()))));
- goto error;
- }
-
if (ssl_min_protocol_version)
{
ssl_ver_min = ssl_protocol_version_to_openssl(ssl_min_protocol_version);
@@ -323,20 +550,207 @@ be_tls_init(bool isServerStart)
if (SSLPreferServerCiphers)
SSL_CTX_set_options(context, SSL_OP_CIPHER_SERVER_PREFERENCE);
+ /*
+ * Success! Replace any existing SSL_context and host configurations.
+ */
+ if (SSL_context)
+ {
+ SSL_CTX_free(SSL_context);
+ SSL_context = NULL;
+ }
+
+ MemoryContextSwitchTo(oldcxt);
+
+ if (SSL_hosts_memcxt)
+ MemoryContextDelete(SSL_hosts_memcxt);
+
+ SSL_hosts_memcxt = host_memcxt;
+ SSL_hosts = new_hosts;
+ SSL_context = context;
+
+ return 0;
+
+ /*
+ * Clean up by releasing working SSL contexts as well as allocations
+ * performed during parsing. Since all our allocations are done in a
+ * local memory context all we need to do is delete it.
+ */
+error:
+ if (context)
+ SSL_CTX_free(context);
+
+ MemoryContextSwitchTo(oldcxt);
+ MemoryContextDelete(host_memcxt);
+ return -1;
+}
+
+/*
+ * host_context_cleanup_cb
+ *
+ * Memory context reset callback for clearing OpenSSL managed resources when
+ * hosts are reloaded and the previous set of configured hosts are freed. As
+ * all hosts are allocated in a single context we don't need to free each host
+ * individually, just resources managed by OpenSSL.
+ */
+static void
+host_context_cleanup_cb(void *arg)
+{
+ struct hosts *hosts = arg;
+
+ foreach_ptr(HostsLine, host, hosts->sni)
+ {
+ if (host->ssl_ctx != NULL)
+ SSL_CTX_free(host->ssl_ctx);
+ }
+
+ if (hosts->no_sni && hosts->no_sni->ssl_ctx)
+ SSL_CTX_free(hosts->no_sni->ssl_ctx);
+
+ if (hosts->default_host && hosts->default_host->ssl_ctx)
+ SSL_CTX_free(hosts->default_host->ssl_ctx);
+}
+
+static bool
+init_host_context(HostsLine *host, bool isServerStart)
+{
+ SSL_CTX *ctx = SSL_CTX_new(SSLv23_method());
+ static bool init_warned = false;
+
+ if (!ctx)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ (errmsg("could not create SSL context: %s",
+ SSLerrmessage(ERR_get_error()))));
+ goto error;
+ }
+
+ /*
+ * Call init hook (usually to set password callback) in case SNI hasn't
+ * been enabled. If SNI is enabled the hook won't operate on the actual
+ * TLS context used so it cannot function properly; we warn if one has
+ * been installed.
+ *
+ * If SNI is enabled, we set password callback based what was configured.
+ */
+ if (!ssl_sni)
+ (*openssl_tls_init_hook) (ctx, isServerStart);
+ else
+ {
+ if (openssl_tls_init_hook != default_openssl_tls_init && !init_warned)
+ {
+ ereport(WARNING,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("SNI is enabled; installed TLS init hook will be ignored"),
+ /*- translator: first %s is a GUC, second %s contains a filename */
+ errhint("TLS init hooks are incompatible with SNI. "
+ "Set \"%s\" to \"off\" to make use of the hook "
+ "that is currently installed, or remove the hook "
+ "and use per-host passphrase commands in \"%s\".",
+ "ssl_sni", "pg_hosts.conf"));
+ init_warned = true;
+ }
+
+ /*
+ * Set up the password callback, if configured.
+ */
+ if (isServerStart)
+ {
+ if (host->ssl_passphrase_cmd && host->ssl_passphrase_cmd[0])
+ {
+ SSL_CTX_set_default_passwd_cb(ctx, ssl_external_passwd_cb);
+ SSL_CTX_set_default_passwd_cb_userdata(ctx, host->ssl_passphrase_cmd);
+ }
+ }
+ else
+ {
+ /*
+ * If ssl_passphrase_reload is true then ssl_passphrase_cmd cannot
+ * be NULL due to their parsing order, but just in case and to
+ * self-document the code we replicate the nullness checks.
+ */
+ if (host->ssl_passphrase_reload &&
+ (host->ssl_passphrase_cmd && host->ssl_passphrase_cmd[0]))
+ {
+ SSL_CTX_set_default_passwd_cb(ctx, ssl_external_passwd_cb);
+ SSL_CTX_set_default_passwd_cb_userdata(ctx, host->ssl_passphrase_cmd);
+ }
+ else
+ {
+ /*
+ * If reloading and no external command is configured,
+ * override OpenSSL's default handling of passphrase-protected
+ * files, because we don't want to prompt for a passphrase in
+ * an already-running server.
+ */
+ SSL_CTX_set_default_passwd_cb(ctx, dummy_ssl_passwd_cb);
+ }
+ }
+ }
+
+ /*
+ * Load and verify server's certificate and private key
+ */
+ if (SSL_CTX_use_certificate_chain_file(ctx, host->ssl_cert) != 1)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("could not load server certificate file \"%s\": %s",
+ host->ssl_cert, SSLerrmessage(ERR_get_error()))));
+ goto error;
+ }
+
+ if (!check_ssl_key_file_permissions(host->ssl_key, isServerStart))
+ goto error;
+
+
+ /* used by the callback */
+ ssl_is_server_start = isServerStart;
+
+ /*
+ * OK, try to load the private key file.
+ */
+ dummy_ssl_passwd_cb_called = false;
+
+ if (SSL_CTX_use_PrivateKey_file(ctx,
+ host->ssl_key,
+ SSL_FILETYPE_PEM) != 1)
+ {
+ if (dummy_ssl_passwd_cb_called)
+ ereport(isServerStart ? FATAL : LOG,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("private key file \"%s\" cannot be reloaded because it requires a passphrase",
+ host->ssl_key)));
+ else
+ ereport(isServerStart ? FATAL : LOG,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("could not load private key file \"%s\": %s",
+ host->ssl_key, SSLerrmessage(ERR_get_error()))));
+ goto error;
+ }
+
+ if (SSL_CTX_check_private_key(ctx) != 1)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ (errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("check of private key failed: %s",
+ SSLerrmessage(ERR_get_error()))));
+ goto error;
+ }
+
/*
* Load CA store, so we can verify client certificates if needed.
*/
- if (ssl_ca_file[0])
+ if (host->ssl_ca && host->ssl_ca[0])
{
STACK_OF(X509_NAME) * root_cert_list;
- if (SSL_CTX_load_verify_locations(context, ssl_ca_file, NULL) != 1 ||
- (root_cert_list = SSL_load_client_CA_file(ssl_ca_file)) == NULL)
+ if (SSL_CTX_load_verify_locations(ctx, host->ssl_ca, NULL) != 1 ||
+ (root_cert_list = SSL_load_client_CA_file(host->ssl_ca)) == NULL)
{
ereport(isServerStart ? FATAL : LOG,
(errcode(ERRCODE_CONFIG_FILE_ERROR),
errmsg("could not load root certificate file \"%s\": %s",
- ssl_ca_file, SSLerrmessage(ERR_get_error()))));
+ host->ssl_ca, SSLerrmessage(ERR_get_error()))));
goto error;
}
@@ -347,17 +761,7 @@ be_tls_init(bool isServerStart)
* that the SSL context will "own" the root_cert_list and remember to
* free it when no longer needed.
*/
- SSL_CTX_set_client_CA_list(context, root_cert_list);
-
- /*
- * Always ask for SSL client cert, but don't fail if it's not
- * presented. We might fail such connections later, depending on what
- * we find in pg_hba.conf.
- */
- SSL_CTX_set_verify(context,
- (SSL_VERIFY_PEER |
- SSL_VERIFY_CLIENT_ONCE),
- verify_cb);
+ SSL_CTX_set_client_CA_list(ctx, root_cert_list);
}
/*----------
@@ -367,7 +771,7 @@ be_tls_init(bool isServerStart)
*/
if (ssl_crl_file[0] || ssl_crl_dir[0])
{
- X509_STORE *cvstore = SSL_CTX_get_cert_store(context);
+ X509_STORE *cvstore = SSL_CTX_get_cert_store(ctx);
if (cvstore)
{
@@ -408,29 +812,13 @@ be_tls_init(bool isServerStart)
}
}
- /*
- * Success! Replace any existing SSL_context.
- */
- if (SSL_context)
- SSL_CTX_free(SSL_context);
+ host->ssl_ctx = ctx;
+ return true;
- SSL_context = context;
-
- /*
- * Set flag to remember whether CA store has been loaded into SSL_context.
- */
- if (ssl_ca_file[0])
- ssl_loaded_verify_locations = true;
- else
- ssl_loaded_verify_locations = false;
-
- return 0;
-
- /* Clean up by releasing working context. */
error:
- if (context)
- SSL_CTX_free(context);
- return -1;
+ if (ctx)
+ SSL_CTX_free(ctx);
+ return false;
}
void
@@ -486,6 +874,38 @@ be_tls_open_server(Port *port)
return -1;
}
+ /*
+ * If the underlying TLS library supports the client hello callback we use
+ * that in order to support host based configuration using the SNI TLS
+ * extension. If the user has disabled SNI via the ssl_sni GUC we still
+ * make use of the callback in order to have consistent handling of
+ * OpenSSL contexts, except in that case the callback will install the
+ * default configuration regardless of the hostname sent by the user in
+ * the handshake.
+ *
+ * In case the TLS library does not support the client hello callback, as
+ * of this writing LibreSSL does not, we need to install the client cert
+ * verification callback here (if the user configured a CA) since we
+ * cannot use the OpenSSL context update functionality.
+ */
+#ifdef HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
+ SSL_CTX_set_client_hello_cb(SSL_context, sni_clienthello_cb, NULL);
+#else
+ if (SSL_hosts->default_host->ssl_ca && SSL_hosts->default_host->ssl_ca[0])
+ {
+ /*
+ * Always ask for SSL client cert, but don't fail if it's not
+ * presented. We might fail such connections later, depending on what
+ * we find in pg_hba.conf.
+ */
+ SSL_set_verify(port->ssl,
+ (SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE),
+ verify_cb);
+
+ ssl_loaded_verify_locations = true;
+ }
+#endif
+
err_context.cert_errdetail = NULL;
SSL_set_ex_data(port->ssl, 0, &err_context);
@@ -1142,10 +1562,11 @@ ssl_external_passwd_cb(char *buf, int size, int rwflag, void *userdata)
{
/* same prompt as OpenSSL uses internally */
const char *prompt = "Enter PEM pass phrase:";
+ const char *cmd = userdata;
Assert(rwflag == 0);
- return run_ssl_passphrase_command(prompt, ssl_is_server_start, buf, size);
+ return run_ssl_passphrase_command(cmd, prompt, ssl_is_server_start, buf, size);
}
/*
@@ -1391,6 +1812,254 @@ alpn_cb(SSL *ssl,
}
}
+#ifdef HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
+/*
+ * ssl_update_ssl
+ *
+ * Replace certificate/key and CA in an SSL object to match the, via the SNI
+ * extension, selected host configuration for the connection. The SSL_CTX
+ * object to use should be passed in as ctx. This function will update the
+ * SSL object in-place.
+ */
+static bool
+ssl_update_ssl(SSL *ssl, HostsLine *host_config)
+{
+ SSL_CTX *ctx = host_config->ssl_ctx;
+
+ X509 *cert;
+ EVP_PKEY *key;
+
+ STACK_OF(X509) * chain;
+
+ Assert(ctx != NULL);
+ /*-
+ * Make use of the already-loaded certificate chain and key. At first
+ * glance, SSL_set_SSL_CTX() looks like the easiest way to do this, but
+ * beware -- it has very odd behavior:
+ *
+ * https://github.com/openssl/openssl/issues/6109
+ */
+ cert = SSL_CTX_get0_certificate(ctx);
+ key = SSL_CTX_get0_privatekey(ctx);
+
+ Assert(cert && key);
+
+ if (!SSL_CTX_get0_chain_certs(ctx, &chain)
+ || !SSL_use_cert_and_key(ssl, cert, key, chain, 1 /* override */ )
+ || !SSL_check_private_key(ssl))
+ {
+ /*
+ * This shouldn't really be possible, since the inputs came from a
+ * SSL_CTX that was already populated by OpenSSL.
+ */
+ ereport(COMMERROR,
+ errcode(ERRCODE_INTERNAL_ERROR),
+ errmsg_internal("could not update certificate chain: %s",
+ SSLerrmessage(ERR_get_error())));
+ return false;
+ }
+
+ if (host_config->ssl_ca && host_config->ssl_ca[0])
+ {
+ /*
+ * Copy the trust store and list of roots over from the SSL_CTX.
+ */
+ X509_STORE *ca_store = SSL_CTX_get_cert_store(ctx);
+
+ STACK_OF(X509_NAME) * roots;
+
+ /*
+ * The trust store appears to be the only setting that this function
+ * can't override via the (SSL *) pointer directly. Instead, share it
+ * with the active SSL_CTX (this should always be SSL_context).
+ */
+ Assert(SSL_context == SSL_get_SSL_CTX(ssl));
+ SSL_CTX_set1_cert_store(SSL_context, ca_store);
+
+ /*
+ * SSL_set_client_CA_list() will take ownership of its argument, so we
+ * need to duplicate it.
+ */
+ if ((roots = SSL_CTX_get_client_CA_list(ctx)) == NULL
+ || (roots = SSL_dup_CA_list(roots)) == NULL)
+ {
+ ereport(COMMERROR,
+ errcode(ERRCODE_INTERNAL_ERROR),
+ errmsg_internal("could not duplicate SSL_CTX CA list: %s",
+ SSLerrmessage(ERR_get_error())));
+ return false;
+ }
+
+ SSL_set_client_CA_list(ssl, roots);
+
+ /*
+ * Always ask for SSL client cert, but don't fail if it's not
+ * presented. We might fail such connections later, depending on what
+ * we find in pg_hba.conf.
+ */
+ SSL_set_verify(ssl,
+ (SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE),
+ verify_cb);
+
+ ssl_loaded_verify_locations = true;
+ }
+
+ return true;
+}
+
+/*
+ * sni_clienthello_cb
+ *
+ * Callback for extracting the servername extension from the TLS handshake
+ * during ClientHello. There is a callback in OpenSSL for the servername
+ * specifically but OpenSSL themselves advice against using it as it is more
+ * dependent on ordering for execution.
+ */
+static int
+sni_clienthello_cb(SSL *ssl, int *al, void *arg)
+{
+ const char *tlsext_hostname;
+ const unsigned char *tlsext;
+ size_t left,
+ len;
+ HostsLine *install_config = NULL;
+
+ if (!ssl_sni)
+ {
+ install_config = SSL_hosts->default_host;
+ goto found;
+ }
+
+ if (SSL_client_hello_get0_ext(ssl, TLSEXT_TYPE_server_name, &tlsext, &left))
+ {
+ if (left <= 2)
+ {
+ *al = SSL_AD_DECODE_ERROR;
+ return 0;
+ }
+ len = (*(tlsext++) << 8);
+ len += *(tlsext)++;
+ if (len + 2 != left)
+ {
+ *al = SSL_AD_DECODE_ERROR;
+ return 0;
+ }
+
+ left = len;
+
+ if (left == 0 || *tlsext++ != TLSEXT_NAMETYPE_host_name)
+ {
+ *al = SSL_AD_DECODE_ERROR;
+ return 0;
+ }
+
+ left--;
+
+ /*
+ * Now we can finally pull out the byte array with the actual
+ * hostname.
+ */
+ if (left <= 2)
+ {
+ *al = SSL_AD_DECODE_ERROR;
+ return 0;
+ }
+ len = (*(tlsext++) << 8);
+ len += *(tlsext++);
+ if (len + 2 > left)
+ {
+ *al = SSL_AD_DECODE_ERROR;
+ return 0;
+ }
+ left = len;
+ tlsext_hostname = (const char *) tlsext;
+
+ /*
+ * We have a requested hostname from the client, match against all
+ * entries in the pg_hosts configuration and attempt to find a match.
+ * Matching is done case insensitive as per RFC 952 and RFC 921.
+ */
+ foreach_ptr(HostsLine, host, SSL_hosts->sni)
+ {
+ foreach_ptr(char, hostname, host->hostnames)
+ {
+ if (strlen(hostname) == len &&
+ pg_strncasecmp(hostname, tlsext_hostname, len) == 0)
+ {
+ install_config = host;
+ goto found;
+ }
+ }
+ }
+
+ /*
+ * If no host specific match was found, and there is a default config,
+ * then fall back to using that.
+ */
+ if (!install_config && SSL_hosts->default_host)
+ install_config = SSL_hosts->default_host;
+ }
+
+ /*
+ * No hostname TLS extension in the handshake, use the default or no_sni
+ * configurations if available.
+ */
+ else
+ {
+ tlsext_hostname = NULL;
+
+ if (SSL_hosts->no_sni)
+ install_config = SSL_hosts->no_sni;
+ else if (SSL_hosts->default_host)
+ install_config = SSL_hosts->default_host;
+ else
+ {
+ /*
+ * Reaching here means that we didn't get a hostname in the TLS
+ * extension and the server has been configured to not allow any
+ * connections without a specified hostname.
+ *
+ * The error message for a missing server_name should, according
+ * to RFC 8446, be missing_extension. This isn't entirely ideal
+ * since the user won't be able to tell which extension the server
+ * considered missing. Sending unrecognized_name would be a more
+ * helpful error, but for now we stick to the RFC.
+ */
+ *al = SSL_AD_MISSING_EXTENSION;
+
+ ereport(COMMERROR,
+ (errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("no hostname provided in callback, and no fallback configured")));
+ return SSL_CLIENT_HELLO_ERROR;
+ }
+ }
+
+ /*
+ * If we reach here without a context chosen as the session context then
+ * fail the handshake and terminate the connection.
+ */
+ if (install_config == NULL)
+ {
+ if (tlsext_hostname)
+ *al = SSL_AD_UNRECOGNIZED_NAME;
+ else
+ *al = SSL_AD_MISSING_EXTENSION;
+ return SSL_CLIENT_HELLO_ERROR;
+ }
+
+found:
+ if (!ssl_update_ssl(ssl, install_config))
+ {
+ *al = SSL_AD_INTERNAL_ERROR;
+ ereport(COMMERROR,
+ errcode(ERRCODE_PROTOCOL_VIOLATION),
+ errmsg("failed to switch to SSL configuration for host, terminating connection"));
+ return SSL_CLIENT_HELLO_ERROR;
+ }
+
+ return SSL_CLIENT_HELLO_SUCCESS;
+}
+#endif /* HAVE_SSL_CTX_SET_CLIENT_HELLO_CB */
/*
* Set DH parameters for generating ephemeral DH keys. The
@@ -1791,6 +2460,20 @@ ssl_protocol_version_to_string(int v)
return "(unrecognized)";
}
+static uint32
+host_cache_pointer(const char *key)
+{
+ uint32 hash;
+ char *lkey = pstrdup(key);
+ int len = strlen(key);
+
+ for (int i = 0; i < len; i++)
+ lkey[i] = pg_tolower(lkey[i]);
+
+ hash = string_hash((const void *) lkey, len);
+ pfree(lkey);
+ return hash;
+}
static void
default_openssl_tls_init(SSL_CTX *context, bool isServerStart)
@@ -1798,12 +2481,18 @@ default_openssl_tls_init(SSL_CTX *context, bool isServerStart)
if (isServerStart)
{
if (ssl_passphrase_command[0])
+ {
SSL_CTX_set_default_passwd_cb(context, ssl_external_passwd_cb);
+ SSL_CTX_set_default_passwd_cb_userdata(context, ssl_passphrase_command);
+ }
}
else
{
if (ssl_passphrase_command[0] && ssl_passphrase_command_supports_reload)
+ {
SSL_CTX_set_default_passwd_cb(context, ssl_external_passwd_cb);
+ SSL_CTX_set_default_passwd_cb_userdata(context, ssl_passphrase_command);
+ }
else
/*
diff --git a/src/backend/libpq/be-secure.c b/src/backend/libpq/be-secure.c
index edd69823b92..617704bb993 100644
--- a/src/backend/libpq/be-secure.c
+++ b/src/backend/libpq/be-secure.c
@@ -61,6 +61,9 @@ bool SSLPreferServerCiphers;
int ssl_min_protocol_version = PG_TLS1_2_VERSION;
int ssl_max_protocol_version = PG_TLS_ANY;
+/* GUC variable: if false, discards hostname extensions in handshake */
+bool ssl_sni = false;
+
/* ------------------------------------------------------------ */
/* Procedures common to all secure sessions */
/* ------------------------------------------------------------ */
diff --git a/src/backend/libpq/meson.build b/src/backend/libpq/meson.build
index ee337cf42cc..8571f652844 100644
--- a/src/backend/libpq/meson.build
+++ b/src/backend/libpq/meson.build
@@ -31,5 +31,6 @@ endif
install_data(
'pg_hba.conf.sample',
'pg_ident.conf.sample',
+ 'pg_hosts.conf.sample',
install_dir: dir_data,
)
diff --git a/src/backend/libpq/pg_hosts.conf.sample b/src/backend/libpq/pg_hosts.conf.sample
new file mode 100644
index 00000000000..a31c49b01f7
--- /dev/null
+++ b/src/backend/libpq/pg_hosts.conf.sample
@@ -0,0 +1,4 @@
+# PostgreSQL SNI Hostname mappings
+# ================================
+
+# HOSTNAME SSL CERTIFICATE SSL KEY SSL CA PASSPHRASE COMMAND PASSPHRASE COMMAND RELOAD
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index d77502838c4..e1546d9c97a 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -56,6 +56,7 @@
#define CONFIG_FILENAME "postgresql.conf"
#define HBA_FILENAME "pg_hba.conf"
#define IDENT_FILENAME "pg_ident.conf"
+#define HOSTS_FILENAME "pg_hosts.conf"
#ifdef EXEC_BACKEND
#define CONFIG_EXEC_PARAMS "global/config_exec_params"
@@ -1838,6 +1839,37 @@ SelectConfigFiles(const char *userDoption, const char *progname)
}
SetConfigOption("ident_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE);
+ if (fname_is_malloced)
+ free(fname);
+ else
+ guc_free(fname);
+
+ /*
+ * Likewise for pg_hosts.conf.
+ */
+ if (HostsFileName)
+ {
+ fname = make_absolute_path(HostsFileName);
+ fname_is_malloced = true;
+ }
+ else if (configdir)
+ {
+ fname = guc_malloc(FATAL,
+ strlen(configdir) + strlen(HOSTS_FILENAME) + 2);
+ sprintf(fname, "%s/%s", configdir, HOSTS_FILENAME);
+ fname_is_malloced = false;
+ }
+ else
+ {
+ write_stderr("%s does not know where to find the \"hosts\" configuration file.\n"
+ "This can be specified as \"hosts_file\" in \"%s\", "
+ "or by the -D invocation option, or by the "
+ "PGDATA environment variable.\n",
+ progname, ConfigFileName);
+ goto fail;
+ }
+ SetConfigOption("hosts_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE);
+
if (fname_is_malloced)
free(fname);
else
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index a5a0edf2534..0c9854ad8fc 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -1177,6 +1177,13 @@
boot_val => 'NULL',
},
+{ name => 'hosts_file', type => 'string', context => 'PGC_POSTMASTER', group => 'FILE_LOCATIONS',
+ short_desc => 'Sets the server\'s "hosts" configuration file.',
+ flags => 'GUC_SUPERUSER_ONLY',
+ variable => 'HostsFileName',
+ boot_val => 'NULL',
+},
+
{ name => 'hot_standby', type => 'bool', context => 'PGC_POSTMASTER', group => 'REPLICATION_STANDBY',
short_desc => 'Allows connections and queries during recovery.',
variable => 'EnableHotStandby',
@@ -2764,6 +2771,14 @@
max => '0',
},
+{ name => 'ssl_sni', type => 'bool', context => 'PGC_SIGHUP', group => 'CONN_AUTH_SSL',
+ short_desc => 'Sets whether to interpret SNI extensions in SSL connections.',
+ flags => 'GUC_SUPERUSER_ONLY',
+ variable => 'ssl_sni',
+ boot_val => 'false',
+ check_hook => 'check_ssl_sni',
+},
+
{ name => 'ssl_tls13_ciphers', type => 'string', context => 'PGC_SIGHUP', group => 'CONN_AUTH_SSL',
short_desc => 'Sets the list of allowed TLSv1.3 cipher suites.',
long_desc => 'An empty string means use the default cipher suites.',
diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c
index 38aaf82f120..1e14b7b4af0 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -565,6 +565,7 @@ char *cluster_name = "";
char *ConfigFileName;
char *HbaFileName;
char *IdentFileName;
+char *HostsFileName;
char *external_pid_file;
char *application_name;
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index e686d88afc4..e4abe6c0077 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -45,6 +45,8 @@
# (change requires restart)
#ident_file = 'ConfigDir/pg_ident.conf' # ident configuration file
# (change requires restart)
+#hosts_file = 'ConfigDir/pg_hosts.conf' # hosts configuration file
+ # (change requires restart)
# If external_pid_file is not explicitly set, no extra PID file is written.
#external_pid_file = '' # write an extra PID file
@@ -122,6 +124,7 @@
#ssl_dh_params_file = ''
#ssl_passphrase_command = ''
#ssl_passphrase_command_supports_reload = off
+#ssl_sni = off
#------------------------------------------------------------------------------
diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c
index f3174d79f32..509f1114ef6 100644
--- a/src/bin/initdb/initdb.c
+++ b/src/bin/initdb/initdb.c
@@ -177,6 +177,7 @@ static int encodingid;
static char *bki_file;
static char *hba_file;
static char *ident_file;
+static char *hosts_file;
static char *conf_file;
static char *dictionary_file;
static char *info_schema_file;
@@ -1547,6 +1548,14 @@ setup_config(void)
snprintf(path, sizeof(path), "%s/pg_ident.conf", pg_data);
+ writefile(path, conflines);
+ if (chmod(path, pg_file_create_mode) != 0)
+ pg_fatal("could not change permissions of \"%s\": %m", path);
+
+ /* pg_hosts.conf */
+ conflines = readfile(hosts_file);
+ snprintf(path, sizeof(path), "%s/pg_hosts.conf", pg_data);
+
writefile(path, conflines);
if (chmod(path, pg_file_create_mode) != 0)
pg_fatal("could not change permissions of \"%s\": %m", path);
@@ -2808,6 +2817,7 @@ setup_data_file_paths(void)
set_input(&bki_file, "postgres.bki");
set_input(&hba_file, "pg_hba.conf.sample");
set_input(&ident_file, "pg_ident.conf.sample");
+ set_input(&hosts_file, "pg_hosts.conf.sample");
set_input(&conf_file, "postgresql.conf.sample");
set_input(&dictionary_file, "snowball_create.sql");
set_input(&info_schema_file, "information_schema.sql");
@@ -2823,12 +2833,12 @@ setup_data_file_paths(void)
"PGDATA=%s\nshare_path=%s\nPGPATH=%s\n"
"POSTGRES_SUPERUSERNAME=%s\nPOSTGRES_BKI=%s\n"
"POSTGRESQL_CONF_SAMPLE=%s\n"
- "PG_HBA_SAMPLE=%s\nPG_IDENT_SAMPLE=%s\n",
+ "PG_HBA_SAMPLE=%s\nPG_IDENT_SAMPLE=%s\nPG_HOSTS_SAMPLE=%s\n",
PG_VERSION,
pg_data, share_path, bin_path,
username, bki_file,
conf_file,
- hba_file, ident_file);
+ hba_file, ident_file, hosts_file);
if (show_setting)
exit(0);
}
@@ -2836,6 +2846,7 @@ setup_data_file_paths(void)
check_input(bki_file);
check_input(hba_file);
check_input(ident_file);
+ check_input(hosts_file);
check_input(conf_file);
check_input(dictionary_file);
check_input(info_schema_file);
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 7b93ba4a709..c4570ce9b3f 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -151,6 +151,36 @@ typedef struct IdentLine
AuthToken *pg_user;
} IdentLine;
+typedef struct HostsLine
+{
+ int linenumber;
+
+ char *sourcefile;
+ char *rawline;
+
+ /* Required fields */
+ List *hostnames;
+ char *ssl_key;
+ char *ssl_cert;
+
+ /* Optional fields */
+ char *ssl_ca;
+ char *ssl_passphrase_cmd;
+ bool ssl_passphrase_reload;
+
+ /* Internal bookkeeping */
+ void *ssl_ctx; /* associated SSL_CTX* for the above settings */
+} HostsLine;
+
+typedef enum HostsFileLoadResult
+{
+ HOSTSFILE_LOAD_OK = 0,
+ HOSTSFILE_LOAD_FAILED,
+ HOSTSFILE_EMPTY,
+ HOSTSFILE_MISSING,
+ HOSTSFILE_DISABLED,
+} HostsFileLoadResult;
+
/*
* TokenizedAuthLine represents one line lexed from an authentication
* configuration file. Each item in the "fields" list is a sub-list of
diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h
index 790724b6a0b..c9b934d2321 100644
--- a/src/include/libpq/libpq.h
+++ b/src/include/libpq/libpq.h
@@ -113,6 +113,7 @@ extern PGDLLIMPORT int ssl_max_protocol_version;
extern PGDLLIMPORT char *ssl_passphrase_command;
extern PGDLLIMPORT bool ssl_passphrase_command_supports_reload;
extern PGDLLIMPORT char *ssl_dh_params_file;
+extern PGDLLIMPORT bool ssl_sni;
extern PGDLLIMPORT char *SSLCipherSuites;
extern PGDLLIMPORT char *SSLCipherList;
extern PGDLLIMPORT char *SSLECDHCurve;
@@ -158,9 +159,11 @@ enum ssl_protocol_versions
/*
* prototypes for functions in be-secure-common.c
*/
-extern int run_ssl_passphrase_command(const char *prompt, bool is_server_start,
+extern int run_ssl_passphrase_command(const char *cmd, const char *prompt,
+ bool is_server_start,
char *buf, int size);
extern bool check_ssl_key_file_permissions(const char *ssl_key_file,
bool isServerStart);
+extern int load_hosts(List **hosts, char **err_msg);
#endif /* LIBPQ_H */
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 79379a4d125..d8d61918aff 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -372,6 +372,9 @@
/* Define to 1 if you have the `SSL_CTX_set_ciphersuites' function. */
#undef HAVE_SSL_CTX_SET_CIPHERSUITES
+/* Define to 1 if you have the `SSL_CTX_set_client_hello_cb' function. */
+#undef HAVE_SSL_CTX_SET_CLIENT_HELLO_CB
+
/* Define to 1 if you have the `SSL_CTX_set_keylog_callback' function. */
#undef HAVE_SSL_CTX_SET_KEYLOG_CALLBACK
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index c46203fabfe..dc406d6651a 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -312,6 +312,7 @@ extern PGDLLIMPORT char *cluster_name;
extern PGDLLIMPORT char *ConfigFileName;
extern PGDLLIMPORT char *HbaFileName;
extern PGDLLIMPORT char *IdentFileName;
+extern PGDLLIMPORT char *HostsFileName;
extern PGDLLIMPORT char *external_pid_file;
extern PGDLLIMPORT char *application_name;
diff --git a/src/include/utils/guc_hooks.h b/src/include/utils/guc_hooks.h
index 9c90670d9b8..b01697c1f60 100644
--- a/src/include/utils/guc_hooks.h
+++ b/src/include/utils/guc_hooks.h
@@ -133,6 +133,7 @@ extern void assign_session_authorization(const char *newval, void *extra);
extern void assign_session_replication_role(int newval, void *extra);
extern void assign_stats_fetch_consistency(int newval, void *extra);
extern bool check_ssl(bool *newval, void **extra, GucSource source);
+extern bool check_ssl_sni(bool *newval, void **extra, GucSource source);
extern bool check_stage_log_stats(bool *newval, void **extra, GucSource source);
extern bool check_standard_conforming_strings(bool *newval, void **extra,
GucSource source);
diff --git a/src/test/modules/ssl_passphrase_callback/t/001_testfunc.pl b/src/test/modules/ssl_passphrase_callback/t/001_testfunc.pl
index 676482af0ce..09ff5364f0a 100644
--- a/src/test/modules/ssl_passphrase_callback/t/001_testfunc.pl
+++ b/src/test/modules/ssl_passphrase_callback/t/001_testfunc.pl
@@ -15,6 +15,8 @@ unless (($ENV{with_ssl} || "") eq 'openssl')
plan skip_all => 'OpenSSL not supported by this build';
}
+my $libressl = not check_pg_config("#define HAVE_SSL_CTX_SET_CERT_CB 1");
+
my $rot13pass = "SbbOnE1";
# see the Makefile for how the certificate and key have been generated
@@ -76,4 +78,36 @@ ok(!-e "$ddir/postmaster.pid", "postgres not started with bad passphrase");
# just in case
$node->stop('fast');
+# Make sure the hook is bypassed when SNI is enabled.
+SKIP:
+{
+ skip 'SNI not supported with LibreSSL', 2 if ($libressl);
+
+ $node->append_conf(
+ 'postgresql.conf', qq{
+ssl_passphrase_command = 'echo FooBaR1'
+ssl_sni = on
+});
+ $node->append_conf(
+ 'pg_hosts.conf', qq{
+example.org $ddir/server.crt $ddir/server.key "" "echo FooBaR1" on
+example.com $ddir/server.crt $ddir/server.key "" "echo FooBaR1" on
+});
+
+ # If the servers starts and runs, the bad ssl_passphrase.passphrase was
+ # correctly ignored.
+ $node->start;
+ ok(-e "$ddir/postmaster.pid", "postgres started after SNI");
+
+ $node->stop('fast');
+ $log_contents = slurp_file($log);
+ like(
+ $log_contents,
+ qr/WARNING.*SNI is enabled; installed TLS init hook will be ignored/,
+ "server warns that init hook and SNI are incompatible");
+ # Ensure that the warning was printed once and not once per host line
+ my $count =()= $log_contents =~ m/installed TLS init hook will be ignored/;
+ is($count, 1, 'Only one WARNING');
+}
+
done_testing();
diff --git a/src/test/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index e267ba868fe..b44aefb545a 100644
--- a/src/test/perl/PostgreSQL/Test/Cluster.pm
+++ b/src/test/perl/PostgreSQL/Test/Cluster.pm
@@ -1302,6 +1302,27 @@ Wrapper for pg_ctl restart.
With optional extra param fail_ok => 1, returns 0 for failure
instead of bailing out.
+=over
+
+=item fail_ok => 1
+
+By default, failure terminates the entire F invocation. If given,
+instead return 0 for failure instead of bailing out.
+
+=item log_unlike => B
+
+When defined, the logfile is inspected for the presence of the fragment by
+matching the specified pattern. If the pattern matches against the logfile a
+test failure will be logged.
+
+=item log_like => B
+
+When defined, the logfile is inspected for the presence of the fragment by
+matching the pattern. If the pattern doesn't match a test failure will be
+logged.
+
+=back
+
=cut
sub restart
@@ -1314,6 +1335,8 @@ sub restart
print "### Restarting node \"$name\"\n";
+ my $log_location = -s $self->logfile;
+
# -w is now the default but having it here does no harm and helps
# compatibility with older versions.
$ret = PostgreSQL::Test::Utils::system_log(
@@ -1322,6 +1345,18 @@ sub restart
'--log' => $self->logfile,
'restart');
+ # Check for expected and/or unexpected log fragments if the caller
+ # specified such checks in the params
+ if (defined $params{log_unlike} || defined $params{log_like})
+ {
+ my $log =
+ PostgreSQL::Test::Utils::slurp_file($self->logfile, $log_location);
+ unlike($log, $params{log_unlike}, "unexpected fragment found in log")
+ if defined $params{log_unlike};
+ like($log, $params{log_like}, "expected fragment not found in log")
+ if defined $params{log_like};
+ }
+
if ($ret != 0)
{
print "# pg_ctl restart failed; see logfile for details: "
diff --git a/src/test/ssl/meson.build b/src/test/ssl/meson.build
index 9e5bdbb6136..d7e7ce23433 100644
--- a/src/test/ssl/meson.build
+++ b/src/test/ssl/meson.build
@@ -13,6 +13,7 @@ tests += {
't/001_ssltests.pl',
't/002_scram.pl',
't/003_sslinfo.pl',
+ 't/004_sni.pl',
],
},
}
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 963bfea8ed5..0af887caa63 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -380,11 +380,11 @@ switch_server_cert($node, certfile => 'server-ip-cn-only');
$common_connstr =
"$default_ssl_connstr user=ssltestuser dbname=trustdb sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
-$node->connect_ok("$common_connstr host=192.0.2.1",
+$node->connect_ok("$common_connstr host=192.0.2.1 sslsni=0",
"IP address in the Common Name");
$node->connect_fails(
- "$common_connstr host=192.000.002.001",
+ "$common_connstr host=192.000.002.001 sslsni=0",
"mismatch between host name and server certificate IP address",
expected_stderr =>
qr/\Qserver certificate for "192.0.2.1" does not match host name "192.000.002.001"\E/
@@ -394,7 +394,7 @@ $node->connect_fails(
# long-standing behavior.)
switch_server_cert($node, certfile => 'server-ip-in-dnsname');
-$node->connect_ok("$common_connstr host=192.0.2.1",
+$node->connect_ok("$common_connstr host=192.0.2.1 sslsni=0",
"IP address in a dNSName");
# Test Subject Alternative Names.
diff --git a/src/test/ssl/t/004_sni.pl b/src/test/ssl/t/004_sni.pl
new file mode 100644
index 00000000000..4e06475b125
--- /dev/null
+++ b/src/test/ssl/t/004_sni.pl
@@ -0,0 +1,453 @@
+
+# Copyright (c) 2024, PostgreSQL Global Development Group
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+use FindBin;
+use lib $FindBin::RealBin;
+
+use SSL::Server;
+
+# This is the hostaddr used to connect to the server. This cannot be a
+# hostname, because the server certificate is always for the domain
+# postgresql-ssl-regression.test.
+my $SERVERHOSTADDR = '127.0.0.1';
+# This is the pattern to use in pg_hba.conf to match incoming connections.
+my $SERVERHOSTCIDR = '127.0.0.1/32';
+
+if ($ENV{with_ssl} ne 'openssl')
+{
+ plan skip_all => 'OpenSSL not supported by this build';
+}
+
+if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bssl\b/)
+{
+ plan skip_all =>
+ 'Potentially unsafe test SSL not enabled in PG_TEST_EXTRA';
+}
+
+my $ssl_server = SSL::Server->new();
+
+if ($ssl_server->is_libressl)
+{
+ plan skip_all => 'SNI not supported when building with LibreSSL';
+}
+
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+
+# PGHOST is enforced here to set up the node, subsequent connections
+# will use a dedicated connection string.
+$ENV{PGHOST} = $node->host;
+$ENV{PGPORT} = $node->port;
+$node->start;
+
+$ssl_server->configure_test_server_for_ssl($node, $SERVERHOSTADDR,
+ $SERVERHOSTCIDR, 'trust');
+
+$ssl_server->switch_server_cert($node, certfile => 'server-cn-only');
+
+my $connstr =
+ "user=ssltestuser dbname=trustdb hostaddr=$SERVERHOSTADDR sslsni=1";
+
+##############################################################################
+# postgresql.conf
+##############################################################################
+
+# Connect without any hosts configured in pg_hosts.conf, thus using the cert
+# and key in postgresql.conf. pg_hosts.conf exists at this point but is empty
+# apart from the comments stemming from the sample.
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "pg.conf: connect with correct server CA cert file sslmode=require");
+
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "pg.conf: connect fails without intermediate for sslmode=verify-ca",
+ expected_stderr => qr/certificate verify failed/);
+
+# Add an entry in pg_hosts.conf with no default, and reload. Since ssl_sni is
+# still 'off' we should still be able to connect using the certificates in
+# postgresql.conf
+$node->append_conf('pg_hosts.conf',
+ "example.org server-cn-only.crt server-cn-only.key");
+$node->reload;
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "pg.conf: connect with correct server CA cert file sslmode=require");
+
+# Turn on SNI support and remove pg_hosts.conf and reload to make sure a
+# missing file is treated like an empty file.
+$node->append_conf('postgresql.conf', 'ssl_sni = on');
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->reload;
+
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "pg.conf: connect after deleting pg_hosts.conf");
+
+##############################################################################
+# pg_hosts.conf
+##############################################################################
+
+# Replicate the postgresql.conf configuration into pg_hosts.conf and retry the
+# same tests as above.
+$node->append_conf('pg_hosts.conf',
+ "* server-cn-only.crt server-cn-only.key");
+$node->reload;
+
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "pg_hosts.conf: connect to default, with correct server CA cert file sslmode=require"
+);
+
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "pg_hosts.conf: connect to default, fail without intermediate for sslmode=verify-ca",
+ expected_stderr => qr/certificate verify failed/);
+
+# Add host entry for example.org which serves the server cert and its
+# intermediate CA. The previously existing default host still exists without
+# a CA.
+$node->append_conf('pg_hosts.conf',
+ "example.org server-cn-only+server_ca.crt server-cn-only.key root_ca.crt"
+);
+$node->reload;
+
+$node->connect_ok(
+ "$connstr host=example.org sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "pg_hosts.conf: connect to example.org and verify server CA");
+
+$node->connect_ok(
+ "$connstr host=Example.ORG sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "pg_hosts.conf: connect to Example.ORG and verify server CA");
+
+$node->connect_fails(
+ "$connstr host=example.org sslrootcert=invalid sslmode=verify-ca",
+ "pg_hosts.conf: connect to example.org but without server root cert, sslmode=verify-ca",
+ expected_stderr => qr/root certificate file "invalid" does not exist/);
+
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "pg_hosts.conf: connect to default and fail to verify CA",
+ expected_stderr => qr/certificate verify failed/);
+
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "pg_hosts.conf: connect to default with sslmode=require");
+
+# Use multiple hostnames for a single configuration
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ "example.org,example.com,example.net server-cn-only+server_ca.crt server-cn-only.key root_ca.crt"
+);
+$node->reload;
+
+$node->connect_ok(
+ "$connstr host=example.org sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "pg_hosts.conf: connect to example.org and verify server CA");
+$node->connect_ok(
+ "$connstr host=example.com sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "pg_hosts.conf: connect to example.com and verify server CA");
+$node->connect_ok(
+ "$connstr host=example.net sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ "pg_hosts.conf: connect to example.net and verify server CA");
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=example.se",
+ "pg_hosts.conf: connect to default with sslmode=require",
+ expected_stderr => qr/unrecognized name/);
+
+# Test @-inclusion of hostnames.
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ 'example.org,@hostnames.txt server-cn-only+server_ca.crt server-cn-only.key root_ca.crt'
+);
+$node->append_conf(
+ 'hostnames.txt', qq{
+example.com
+example.net
+});
+$node->reload;
+
+$node->connect_ok(
+ "$connstr host=example.org sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ '@hostnames.txt: connect to example.org and verify server CA');
+$node->connect_ok(
+ "$connstr host=example.com sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ '@hostnames.txt: connect to example.com and verify server CA');
+$node->connect_ok(
+ "$connstr host=example.net sslrootcert=ssl/root_ca.crt sslmode=verify-ca",
+ '@hostnames.txt: connect to example.net and verify server CA');
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=example.se",
+ '@hostnames.txt: connect to default with sslmode=require',
+ expected_stderr => qr/unrecognized name/);
+
+# Add an incorrect entry specifying a default entry combined with hostnames
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ "example.org,*,example.net server-cn-only+server_ca.crt server-cn-only.key root_ca.crt"
+);
+my $result = $node->restart(fail_ok => 1);
+is($result, 0,
+ 'pg_hosts.conf: restart fails with default entry combined with hostnames'
+);
+
+# Add incorrect duplicate entries.
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf(
+ 'pg_hosts.conf', qq{
+* server-cn-only.crt server-cn-only.key
+* server-cn-only.crt server-cn-only.key
+});
+$result = $node->restart(fail_ok => 1);
+is($result, 0, 'pg_hosts.conf: restart fails with two default entries');
+
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf(
+ 'pg_hosts.conf', qq{
+/no_sni/ server-cn-only.crt server-cn-only.key
+/no_sni/ server-cn-only.crt server-cn-only.key
+});
+$result = $node->restart(fail_ok => 1);
+is($result, 0, 'pg_hosts.conf: restart fails with two no_sni entries');
+
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf(
+ 'pg_hosts.conf', qq{
+example.org server-cn-only.crt server-cn-only.key
+example.net server-cn-only.crt server-cn-only.key
+example.org server-cn-only.crt server-cn-only.key
+});
+$result = $node->restart(fail_ok => 1);
+is($result, 0,
+ 'pg_hosts.conf: restart fails with two identical hostname entries');
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf(
+ 'pg_hosts.conf', qq{
+example.org server-cn-only.crt server-cn-only.key
+example.net,example.com,Example.org server-cn-only.crt server-cn-only.key
+});
+$result = $node->restart(fail_ok => 1);
+is($result, 0,
+ 'pg_hosts.conf: restart fails with two identical hostname entries in lists'
+);
+
+# Modify pg_hosts.conf to no longer have the default host entry.
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ "example.org server-cn-only+server_ca.crt server-cn-only.key root_ca.crt"
+);
+$node->restart;
+
+# Connecting without a hostname as well as with a hostname which isn't in the
+# pg_hosts configuration should fail.
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslsni=0",
+ "pg_hosts.conf: connect to default with sslmode=require",
+ expected_stderr => qr/handshake failure/);
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=example.com",
+ "pg_hosts.conf: connect to default with sslmode=require",
+ expected_stderr => qr/unrecognized name/);
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=example",
+ "pg_hosts.conf: connect to 'example' with sslmode=require",
+ expected_stderr => qr/unrecognized name/);
+
+# Reconfigure with broken configuration for the key passphrase, the server
+# should not start up
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo wrongpassword" on'
+);
+$result = $node->restart(fail_ok => 1);
+is($result, 0,
+ 'pg_hosts.conf: restart fails with password-protected key when using the wrong passphrase command'
+);
+
+# Reconfigure again but with the correct passphrase set
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo secret1" on'
+);
+$result = $node->restart(fail_ok => 1);
+is($result, 1,
+ 'pg_hosts.conf: restart succeeds with password-protected key when using the correct passphrase command'
+);
+
+# Make sure connecting works, and try to stress the reload logic by issuing
+# subsequent reloads
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=localhost",
+ "pg_hosts.conf: connect with correct server CA cert file sslmode=require"
+);
+$node->reload;
+$node->reload;
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=localhost",
+ "pg_hosts.conf: connect with correct server CA cert file after reloads");
+$node->reload;
+$node->reload;
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=localhost",
+ "pg_hosts.conf: connect with correct server CA cert file after more reloads"
+);
+
+# Test reloading a passphrase protected key without reloading support in the
+# passphrase hook. Restarting should not give any errors in the log, but the
+# subsequent reload should fail with an error regarding reloading.
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo secret1" off'
+);
+my $node_loglocation = -s $node->logfile;
+$result = $node->restart(fail_ok => 1);
+is($result, 1,
+ 'pg_hosts.conf: restart succeeds with password-protected key when using the correct passphrase command'
+);
+my $log =
+ PostgreSQL::Test::Utils::slurp_file($node->logfile, $node_loglocation);
+unlike(
+ $log,
+ qr/cannot be reloaded because it requires a passphrase/,
+ 'log reload failure due to passphrase command reloading');
+
+SKIP:
+{
+ # Passphrase reloads must be enabled on Windows to succeed even without a
+ # restart
+ skip "Passphrase command reload required on Windows", 1 if ($windows_os);
+
+ $node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=localhost",
+ "pg_hosts.conf: connect with correct server CA cert file sslmode=require"
+ );
+ # Reloading should fail since the passphrase cannot be reloaded, with an
+ # error recorded in the log. Since we keep existing contexts around it
+ # should still work.
+ $node_loglocation = -s $node->logfile;
+ $node->reload;
+ $node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=localhost",
+ "pg_hosts.conf: connect with correct server CA cert file sslmode=require"
+ );
+ $log =
+ PostgreSQL::Test::Utils::slurp_file($node->logfile, $node_loglocation);
+ like(
+ $log,
+ qr/cannot be reloaded because it requires a passphrase/,
+ 'log reload failure due to passphrase command reloading');
+}
+
+# Configure with only non-SNI connections allowed
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf',
+ "/no_sni/ server-cn-only.crt server-cn-only.key");
+$node->restart;
+
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslsni=0",
+ "pg_hosts.conf: only non-SNI connections allowed");
+
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require host=example.org",
+ "pg_hosts.conf: only non-SNI connections allowed, connecting with SNI",
+ expected_stderr => qr/unrecognized name/);
+
+# Test client CAs
+
+# pg_hosts configuration
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+
+# Neither ssl_ca_file nor the default host should have any effect whatsoever on
+# the following tests.
+$node->append_conf('postgresql.conf', "ssl_ca_file = 'root+client_ca.crt'");
+$node->append_conf('pg_hosts.conf',
+ '* server-cn-only.crt server-cn-only.key root+client_ca.crt');
+
+# example.org has an unconfigured CA.
+$node->append_conf('pg_hosts.conf',
+ 'example.org server-cn-only.crt server-cn-only.key');
+# example.com uses the client CA.
+$node->append_conf('pg_hosts.conf',
+ 'example.com server-cn-only.crt server-cn-only.key root+client_ca.crt');
+# example.net uses the server CA (which is wrong).
+$node->append_conf('pg_hosts.conf',
+ 'example.net server-cn-only.crt server-cn-only.key root+server_ca.crt');
+
+$node->restart;
+
+$connstr =
+ "user=ssltestuser dbname=certdb hostaddr=$SERVERHOSTADDR sslmode=require sslsni=1";
+
+# example.org is unconfigured and should fail.
+$node->connect_fails(
+ "$connstr host=example.org sslcertmode=require sslcert=ssl/client.crt"
+ . $ssl_server->sslkey('client.key'),
+ "host: 'example.org', ca: '': connect with sslcert, no client CA configured",
+ expected_stderr =>
+ qr/client certificates can only be checked if a root certificate store is available/
+);
+
+# example.com is configured and should require a valid client cert.
+$node->connect_fails(
+ "$connstr host=example.com sslcertmode=disable",
+ "host: 'example.com', ca: 'root+client_ca.crt': connect fails if no client certificate sent",
+ expected_stderr => qr/connection requires a valid client certificate/);
+
+$node->connect_ok(
+ "$connstr host=example.com sslcertmode=require sslcert=ssl/client.crt "
+ . $ssl_server->sslkey('client.key'),
+ "host: 'example.com', ca: 'root+client_ca.crt': connect with sslcert, client certificate sent"
+);
+
+# example.net is configured and should require a client cert, but will
+# always fail verification.
+$node->connect_fails(
+ "$connstr host=example.net sslcertmode=disable",
+ "host: 'example.net', ca: 'root+server_ca.crt': connect fails if no client certificate sent",
+ expected_stderr => qr/connection requires a valid client certificate/);
+
+$node->connect_fails(
+ "$connstr host=example.net sslcertmode=require sslcert=ssl/client.crt "
+ . $ssl_server->sslkey('client.key'),
+ "host: 'example.net', ca: 'root+server_ca.crt': connect with sslcert, client certificate sent",
+ expected_stderr => qr/unknown ca/);
+
+# Make sure the global CRL dir interacts properly with per-host trust.
+$ssl_server->switch_server_cert(
+ $node,
+ certfile => 'server-cn-only',
+ crldir => 'client-crldir');
+
+$node->connect_fails(
+ "$connstr host=example.com sslcertmode=require sslcert=ssl/client-revoked.crt "
+ . $ssl_server->sslkey('client-revoked.key'),
+ "host: 'example.com', ca: 'root+client_ca.crt': connect fails with revoked client cert",
+ expected_stderr => qr/certificate revoked/);
+
+# pg_hosts configuration with useless data at EOL
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+# example.org has an unconfigured CA.
+$node->append_conf('pg_hosts.conf',
+ 'example.org server-cn-only.crt server-cn-only.key root+client_ca.crt "cmd" on TRAILING_TEXT MORE_TEXT'
+);
+$result = $node->restart(fail_ok => 1);
+is($result, 0, 'pg_hosts.conf: restart fails with extra data at EOL');
+# pg_hosts configuration with useless data at EOL
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+# example.org has an unconfigured CA.
+$node->append_conf('pg_hosts.conf',
+ 'example.org server-cn-only.crt server-cn-only.key root+client_ca.crt "cmd" notabooleanvalue'
+);
+$result = $node->restart(fail_ok => 1);
+is($result, 0,
+ 'pg_hosts.conf: restart fails with non-boolean value in boolean field');
+
+done_testing();
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 52f8603a7be..174e2798443 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1242,6 +1242,8 @@ HeapTupleHeader
HeapTupleHeaderData
HeapTupleTableSlot
HistControl
+HostsFileLoadResult
+HostsLine
HotStandbyState
I32
ICU_Convert_Func