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