From 69b7f4788351e3a2f25b5aede7dc412e85b2c395 Mon Sep 17 00:00:00 2001 From: Daniel Gustafsson Date: Thu, 27 Feb 2025 14:03:31 +0100 Subject: [PATCH] Serverside SNI support for libpq Experimental support for serverside SNI support in libpq, a new config file $datadir/pg_hosts.conf is used for configuring which certicate and key should be used for which hostname. A new GUC, ssl_snimode, is added which controls how the hostname TLS extension is handled. The possible values are off, default and strict: - off: pg_hosts.conf is not parsed and the hostname TLS extension is not inspected at all. The normal SSL GUCs for certificates and keys are used. - default: pg_hosts.conf is loaded as well as the normal GUCs. If no match for the TLS extension hostname is found in pg_hosts the cert and key from the postgresql.conf GUCs is used as the default (used as a wildcard host). - strict: only pg_hosts.conf is loaded and the TLS extension hostname MUST be passed and MUST have a match in the configuration, else the connection is refused. CRL file(s) are applied from postgresql.conf to all configured hostnames. Reviewed-by: Cary Huang Reviewed-by: Jacob Champion Discussion: https://postgr.es/m/1C81CD0D-407E-44F9-833A-DD0331C202E5@yesql.se --- doc/src/sgml/config.sgml | 66 ++++ doc/src/sgml/runtime.sgml | 67 ++++ src/backend/Makefile | 1 + src/backend/libpq/be-secure-common.c | 203 +++++++++- src/backend/libpq/be-secure-openssl.c | 356 ++++++++++++++++-- src/backend/libpq/be-secure.c | 8 +- src/backend/libpq/meson.build | 1 + src/backend/libpq/pg_hosts.conf.sample | 4 + src/backend/utils/misc/guc.c | 26 ++ src/backend/utils/misc/guc_tables.c | 31 ++ src/backend/utils/misc/postgresql.conf.sample | 3 + src/bin/initdb/initdb.c | 16 +- src/include/libpq/hba.h | 19 + src/include/libpq/libpq-be.h | 3 +- src/include/libpq/libpq.h | 11 +- src/include/utils/guc.h | 1 + .../ssl_passphrase_func.c | 4 +- src/test/ssl/meson.build | 1 + src/test/ssl/t/004_sni.pl | 164 ++++++++ src/tools/pgindent/typedefs.list | 2 + 20 files changed, 937 insertions(+), 50 deletions(-) create mode 100644 src/backend/libpq/pg_hosts.conf.sample create mode 100644 src/test/ssl/t/004_sni.pl diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 23d2b1be424b..6ecd53685e71 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -1678,6 +1678,72 @@ include_dir 'conf.d' + + + ssl_snimode (enum) + + ssl_snimode configuration parameter + + + + + This parameter determines if the server will inspect the SNI TLS extension + when establishing the connection, and how it should be interpreted. + Valid values are currently: off, default and strict. + + + + + off + + + SNI is not enabled and no configuration from + pg_hosts.conf is loaded. Configuration of SSL + for all connections is done with , + and . + + + + + + default + + + SNI is enabled and hostname configuration is loaded from + pg_hosts.conf. , + and + are loaded as the default configuration. Connections specifying + to 1 + will be attempted using the default configuration if the hostname + is missing in pg_hosts.conf. If the hostname + matches an entry from pg_hosts.conf, then the + configuration from that entry will be used for setting up the + connection. + + + + + + strict + + + SNI is enabled and all connections are required to set to 1 and + specify a hostname matching an entry in + pg_hosts.conf. Any connection without or with a hostname missing from + pg_hosts.conf will be rejected. + , + and + are loaded in order to drive the handshake until the appropriate + configuration has been selected. + + + + + + + diff --git a/doc/src/sgml/runtime.sgml b/doc/src/sgml/runtime.sgml index 0c60bafac635..fa6fe07adc08 100644 --- a/doc/src/sgml/runtime.sgml +++ b/doc/src/sgml/runtime.sgml @@ -2445,6 +2445,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 + + @@ -2572,6 +2578,67 @@ openssl x509 -req -in server.csr -text -days 365 \ + + SNI Configuration + + + PostgreSQL can be configured for + 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. + + + + SNI configuration is defined in the hosts configuration file, + pg_hosts.conf, which is stored in the clusters + 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_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. + + + The SSL configuration from postgresql.conf is used + in order to set up the TLS handshake such that the hostname extension can + be inspected. When is set to + default this configuration will be the defualt fallback + if no matching hostname is found in pg_hosts.conf. If + is set to strict it + will only be used to for the handshake until the hostname is inspected, it + will not be used for the connection. + + + 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. + + diff --git a/src/backend/Makefile b/src/backend/Makefile index 7344c8c7f5c6..2d1691c7950a 100644 --- a/src/backend/Makefile +++ b/src/backend/Makefile @@ -187,6 +187,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) diff --git a/src/backend/libpq/be-secure-common.c b/src/backend/libpq/be-secure-common.c index e8b837d1fa78..67a50c7b24c3 100644 --- a/src/backend/libpq/be-secure-common.c +++ b/src/backend/libpq/be-secure-common.c @@ -24,8 +24,13 @@ #include "common/percentrepl.h" #include "common/string.h" +#include "libpq/hba.h" #include "libpq/libpq.h" #include "storage/fd.h" +#include "utils/guc.h" +#include "utils/memutils.h" + +static HostsLine *parse_hosts_line(TokenizedAuthLine *tok_line, int elevel); /* * Run ssl_passphrase_command @@ -37,19 +42,20 @@ * 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 *prompt, bool is_server_start, char *buf, int size, void *userdata) { int loglevel = is_server_start ? ERROR : LOG; char *command; FILE *fh; int pclose_rc; size_t len = 0; + char *cmd = (char *) userdata; Assert(prompt); 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 +181,196 @@ 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); + + /* Initialize optional fields */ + parsedline->ssl_passphrase_cmd = NULL; + parsedline->ssl_passphrase_reload = false; + + /* Hostname */ + field = list_head(tok_line->fields); + tokens = lfirst(field); + token = linitial(tokens); + parsedline->hostname = pstrdup(token->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); + 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); + token = linitial(tokens); + parsedline->ssl_key = pstrdup(token->string); + + /* SSL CA (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); + token = linitial(tokens); + parsedline->ssl_ca = pstrdup(token->string); + + /* SSL Passphrase Command (optional) */ + field = lnext(tok_line->fields, field); + if (field) + { + tokens = lfirst(field); + 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); + + if (token->string[0] == '1' + || pg_strcasecmp(token->string, "true") == 0 + || pg_strcasecmp(token->string, "on") == 0 + || pg_strcasecmp(token->string, "yes") == 0) + parsedline->ssl_passphrase_reload = true; + else if (token->string[0] == '0' + || pg_strcasecmp(token->string, "false") == 0 + || pg_strcasecmp(token->string, "off") == 0 + || pg_strcasecmp(token->string, "no") == 0) + parsedline->ssl_passphrase_reload = false; + else + 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 parsedline; +} + +/* + * load_hosts + * + * Reads pg_hosts.conf and passes back a List of parsed lines, or NIL in case + * of errors. + */ +List * +load_hosts(void) +{ + FILE *file; + ListCell *line; + List *hosts_lines = NIL; + List *parsed_lines = NIL; + HostsLine *newline; + bool ok = true; + MemoryContext oldcxt; + MemoryContext hostcxt; + + file = open_auth_file(HostsFileName, LOG, 0, NULL); + if (file == NULL) + { + /* An error has already been logged so no need to add one here */ + return NIL; + } + + tokenize_auth_file(HostsFileName, file, &hosts_lines, LOG, 0); + + hostcxt = AllocSetContextCreate(PostmasterContext, + "hosts file parser context", + ALLOCSET_SMALL_SIZES); + oldcxt = MemoryContextSwitchTo(hostcxt); + + foreach(line, hosts_lines) + { + TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line); + + if (tok_line->err_msg != NULL) + { + ok = false; + continue; + } + + if ((newline = parse_hosts_line(tok_line, LOG)) == NULL) + { + ok = false; + continue; + } + + parsed_lines = lappend(parsed_lines, newline); + } + + free_auth_file(file, 0); + MemoryContextSwitchTo(oldcxt); + + /* + * If we didn't find any SNI configuration then that's not an error since + * the pg_hosts file is additive to the default SSL configuration. + */ + if (ok && parsed_lines == NIL) + { + ereport(DEBUG1, + errmsg("no SNI configuration added from configuration file \"%s\"", + HostsFileName)); + MemoryContextDelete(hostcxt); + return NIL; + } + + if (!ok) + { + MemoryContextDelete(hostcxt); + return NIL; + } + + return parsed_lines; +} diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c index 64ff3ce3d6a7..29544efa6674 100644 --- a/src/backend/libpq/be-secure-openssl.c +++ b/src/backend/libpq/be-secure-openssl.c @@ -51,9 +51,18 @@ #endif #include +typedef struct HostContext +{ + const char *hostname; + const char *ssl_passphrase; + SSL_CTX *context; + bool default_host; + bool ssl_loaded_verify_locations; + bool ssl_passphrase_support_reload; +} HostContext; /* default init hook can be overridden by a shared library */ -static void default_openssl_tls_init(SSL_CTX *context, bool isServerStart); +static void default_openssl_tls_init(SSL_CTX *context, bool isServerStart, HostsLine *hosts); openssl_tls_init_hook_typ openssl_tls_init_hook = default_openssl_tls_init; static int port_bio_read(BIO *h, char *buf, int size); @@ -73,6 +82,7 @@ static int alpn_cb(SSL *ssl, const unsigned char *in, unsigned int inlen, void *userdata); +static int sni_servername_cb(SSL *ssl, int *al, void *arg); 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); @@ -80,12 +90,17 @@ static const char *SSLerrmessage(unsigned long ecode); static char *X509_NAME_to_cstring(X509_NAME *name); +static List *contexts = NIL; static SSL_CTX *SSL_context = NULL; +static HostContext *Default_context = NULL; +static HostContext *Host_context = NULL; static bool dummy_ssl_passwd_cb_called = false; static bool ssl_is_server_start; static int ssl_protocol_version_to_openssl(int v); static const char *ssl_protocol_version_to_string(int v); +static SSL_CTX *ssl_init_context(bool isServerStart, HostsLine *host); +static void free_contexts(void); /* for passing data back from verify_cb() */ static const char *cert_errdetail; @@ -96,11 +111,160 @@ static const char *cert_errdetail; int be_tls_init(bool isServerStart) +{ + SSL_CTX *ctx; + List *sni_hosts = NIL; + HostsLine line; + + /* + * If there are contexts loaded when we init they must be released. + */ + if (contexts != NIL) + { + free_contexts(); + Host_context = NULL; + SSL_context = NULL; + Default_context = NULL; + } + + /* + * Load the default configuration from postgresql.conf such that we have a + * context to either be used for the entire connection, or drive the + * handshake until the SNI callback replace it with a configuration from + * the pg_hosts.conf file. + */ + line.ssl_cert = ssl_cert_file; + line.ssl_key = ssl_key_file; + line.ssl_ca = ssl_ca_file; + line.ssl_passphrase_cmd = ssl_passphrase_command; + line.ssl_passphrase_reload = ssl_passphrase_command_supports_reload; + + ctx = ssl_init_context(isServerStart, &line); + if (ctx == NULL) + { + ereport(isServerStart ? FATAL : LOG, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("could not load default certificate"))); + return -1; + } + + Default_context = palloc0(sizeof(HostContext)); + Default_context->hostname = pstrdup("*"); + Default_context->context = ctx; + Default_context->default_host = true; + + /* + * Set flag to remember whether CA store has been loaded into SSL_context. + */ + if (ssl_ca_file[0]) + Default_context->ssl_loaded_verify_locations = true; + + /* + * While the default context isn't matched against when searching for host + * contexts we still add it to the list to ensure that cleanup code can + * iterate over a single structure to clean up everything. + */ + contexts = lappend(contexts, Default_context); + + /* + * Install the default context to use as the initial context for the + * connection. This might be replaced in the SNI callback if there is a + * host/snimode match, but we need something to drive the hand- shake till + * then. + */ + Host_context = Default_context; + SSL_context = Host_context->context; + + /* + * In default or strict ssl_snimode we load all certificates/keys which + * are configured in pg_hosts.conf. In strict mode it is considered a + * fatal error in case there are no configured entries. + */ + if (ssl_snimode == SSL_SNIMODE_STRICT || ssl_snimode == SSL_SNIMODE_DEFAULT) + { + ListCell *line; + + /* + * Load pg_hosts.conf and parse each row, returning the set of hosts + * as a list. + */ + sni_hosts = load_hosts(); + + /* + * In strict ssl_snimode there needs to be at least one configured + * host in the pg_hosts file since the default fallback context isn't + * allowed to connect with. + */ + if (sni_hosts == NIL && ssl_snimode == SSL_SNIMODE_STRICT) + { + ereport(isServerStart ? FATAL : LOG, + errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("could not load %s", "pg_hosts.conf")); + return -1; + } + + foreach(line, sni_hosts) + { + HostContext *host_context; + HostsLine *host = lfirst(line); + static SSL_CTX *tmp_context = NULL; + + tmp_context = ssl_init_context(isServerStart, host); + if (tmp_context == NULL) + { + ereport(isServerStart ? FATAL : LOG, + errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("unable to load certificate from pg_hosts.conf file")); + return -1; + } + + /* + * The parsing logic has already verified that the hostname exist + * so we need not check that. The passphrase command fields are + * however optional so we need to check whether those were set. + */ + host_context = palloc0(sizeof(HostContext)); + host_context->hostname = pstrdup(host->hostname); + host_context->context = tmp_context; + host_context->default_host = false; + if (host->ssl_passphrase_cmd != NULL) + host_context->ssl_passphrase = pstrdup(host->ssl_passphrase_cmd); + host_context->ssl_passphrase_support_reload = host->ssl_passphrase_reload; + + /* + * Set flag to remember whether CA store has been loaded into this + * SSL_context. + */ + if (host->ssl_ca) + host_context->ssl_loaded_verify_locations = true; + + contexts = lappend(contexts, host_context); + } + } + + /* Make sure we have at least one certificate loaded */ + if (list_length(contexts) < 1) + { + ereport(isServerStart ? FATAL : LOG, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("no SSL contexts loaded"))); + return -1; + } + + return 0; +} + +static SSL_CTX * +ssl_init_context(bool isServerStart, HostsLine *host_line) { SSL_CTX *context; int ssl_ver_min = -1; int ssl_ver_max = -1; + const char *ctx_ssl_cert_file = host_line->ssl_cert; + const char *ctx_ssl_key_file = host_line->ssl_key; + const char *ctx_ssl_ca_file = host_line->ssl_ca; + /* * Create a new SSL context into which we'll load all the configuration * settings. If we fail partway through, we can avoid memory leakage by @@ -126,10 +290,17 @@ be_tls_init(bool isServerStart) */ SSL_CTX_set_mode(context, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER); + /* + * Install SNI TLS extension callback in case the server is configured to + * validate hostnames. + */ + if (ssl_snimode != SSL_SNIMODE_OFF) + SSL_CTX_set_tlsext_servername_callback(context, sni_servername_cb); + /* * Call init hook (usually to set password callback) */ - (*openssl_tls_init_hook) (context, isServerStart); + (*openssl_tls_init_hook) (context, isServerStart, host_line); /* used by the callback */ ssl_is_server_start = isServerStart; @@ -137,16 +308,16 @@ be_tls_init(bool isServerStart) /* * Load and verify server's certificate and private key */ - if (SSL_CTX_use_certificate_chain_file(context, ssl_cert_file) != 1) + if (SSL_CTX_use_certificate_chain_file(context, ctx_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())))); + ctx_ssl_cert_file, SSLerrmessage(ERR_get_error())))); goto error; } - if (!check_ssl_key_file_permissions(ssl_key_file, isServerStart)) + if (!check_ssl_key_file_permissions(ctx_ssl_key_file, isServerStart)) goto error; /* @@ -155,19 +326,19 @@ be_tls_init(bool isServerStart) dummy_ssl_passwd_cb_called = false; if (SSL_CTX_use_PrivateKey_file(context, - ssl_key_file, + ctx_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))); + ctx_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())))); + ctx_ssl_key_file, SSLerrmessage(ERR_get_error())))); goto error; } @@ -319,17 +490,17 @@ be_tls_init(bool isServerStart) /* * Load CA store, so we can verify client certificates if needed. */ - if (ssl_ca_file[0]) + if (ctx_ssl_ca_file[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(context, ctx_ssl_ca_file, NULL) != 1 || + (root_cert_list = SSL_load_client_CA_file(ctx_ssl_ca_file)) == 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())))); + ctx_ssl_ca_file, SSLerrmessage(ERR_get_error())))); goto error; } @@ -401,38 +572,29 @@ be_tls_init(bool isServerStart) } } - /* - * Success! Replace any existing SSL_context. - */ - if (SSL_context) - SSL_CTX_free(SSL_context); - - 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; + return context; /* Clean up by releasing working context. */ error: if (context) SSL_CTX_free(context); - return -1; + return NULL; } void be_tls_destroy(void) { - if (SSL_context) - SSL_CTX_free(SSL_context); + ListCell *cell; + + foreach(cell, contexts) + { + HostContext *host_context = lfirst(cell); + + SSL_CTX_free(host_context->context); + pfree(host_context); + } + SSL_context = NULL; - ssl_loaded_verify_locations = false; } int @@ -759,6 +921,9 @@ be_tls_close(Port *port) pfree(port->peer_dn); port->peer_dn = NULL; } + + Host_context = NULL; + SSL_context = NULL; } ssize_t @@ -1132,7 +1297,7 @@ ssl_external_passwd_cb(char *buf, int size, int rwflag, void *userdata) Assert(rwflag == 0); - return run_ssl_passphrase_command(prompt, ssl_is_server_start, buf, size); + return run_ssl_passphrase_command(prompt, ssl_is_server_start, buf, size, userdata); } /* @@ -1369,6 +1534,88 @@ alpn_cb(SSL *ssl, } } +static int +sni_servername_cb(SSL *ssl, int *al, void *arg) +{ + const char *tlsext_hostname; + + /* + * Executing this callback when SNI is turned off indicates a programmer + * error or something worse. + */ + Assert(ssl_snimode != SSL_SNIMODE_OFF); + + tlsext_hostname = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name); + + /* + * If there is no hostname set in the TLS extension, we have two options. + * For ssl_snimode strict we error out since we cannot match a host config + * for the connection. For the default mode we fall back on the default + * hostname configuration. + */ + if (!tlsext_hostname) + { + if (ssl_snimode == SSL_SNIMODE_STRICT) + { + ereport(COMMERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("no hostname provided in callback"))); + return SSL_TLSEXT_ERR_ALERT_FATAL; + } + else + { + Host_context = Default_context; + SSL_context = Host_context->context; + SSL_set_SSL_CTX(ssl, SSL_context); + return SSL_TLSEXT_ERR_OK; + } + } + + /* + * We have a requested hostname from the client, match against all entries + * in the pg_hosts configuration to find a match. + */ + foreach_ptr(HostContext, host, contexts) + { + /* + * For strict mode we will never want the default host so we can skip + * past it immediately. + */ + if (ssl_snimode == SSL_SNIMODE_STRICT && host->default_host) + continue; + + if (strcmp(host->hostname, tlsext_hostname) == 0) + { + Host_context = host; + SSL_context = host->context; + SSL_set_SSL_CTX(ssl, SSL_context); + return SSL_TLSEXT_ERR_OK; + } + } + + /* + * In ssl_snimode "strict" it's an error if there was no match for the + * hostname in the TLS extension. Terminate the connection. + */ + if (ssl_snimode == SSL_SNIMODE_STRICT) + { + ereport(COMMERROR, + (errcode(ERRCODE_PROTOCOL_VIOLATION), + errmsg("no matching pg_hosts entry found for hostname: \"%s\"", + tlsext_hostname))); + return SSL_TLSEXT_ERR_ALERT_FATAL; + } + + /* + * In ssl_snimode "default" we fall back on the default host configured in + * postgresql.conf when no match is found in pg_hosts.conf. + */ + Host_context = Default_context; + SSL_context = Host_context->context; + SSL_set_SSL_CTX(ssl, SSL_context); + Assert(SSL_context); + return SSL_TLSEXT_ERR_OK; +} /* * Set DH parameters for generating ephemeral DH keys. The @@ -1578,6 +1825,12 @@ be_tls_get_peer_serial(Port *port, char *ptr, size_t len) ptr[0] = '\0'; } +bool +be_tls_loaded_verify_locations(void) +{ + return Host_context->ssl_loaded_verify_locations; +} + char * be_tls_get_certificate_hash(Port *port, size_t *len) { @@ -1771,17 +2024,23 @@ ssl_protocol_version_to_string(int v) static void -default_openssl_tls_init(SSL_CTX *context, bool isServerStart) +default_openssl_tls_init(SSL_CTX *context, bool isServerStart, HostsLine *host) { if (isServerStart) { - if (ssl_passphrase_command[0]) + if (host->ssl_passphrase_cmd != NULL) + { SSL_CTX_set_default_passwd_cb(context, ssl_external_passwd_cb); + SSL_CTX_set_default_passwd_cb_userdata(context, host->ssl_passphrase_cmd); + } } else { - if (ssl_passphrase_command[0] && ssl_passphrase_command_supports_reload) + if (host->ssl_passphrase_cmd != NULL && host->ssl_passphrase_reload) + { SSL_CTX_set_default_passwd_cb(context, ssl_external_passwd_cb); + SSL_CTX_set_default_passwd_cb_userdata(context, host->ssl_passphrase_cmd); + } else /* @@ -1793,3 +2052,26 @@ default_openssl_tls_init(SSL_CTX *context, bool isServerStart) SSL_CTX_set_default_passwd_cb(context, dummy_ssl_passwd_cb); } } + +/* + * Cleanup function for when hostname configuration is reloaded from the + * pg_hosts.conf file, at that point we Must discard all existing contexts. + */ +static void +free_contexts(void) +{ + if (contexts == NIL) + return; + + foreach_ptr(HostContext, host, contexts) + { + if (host->hostname) + pfree(unconstify(char *, host->hostname)); + if (host->ssl_passphrase) + pfree(unconstify(char *, host->ssl_passphrase)); + SSL_CTX_free(host->context); + } + + list_free_deep(contexts); + contexts = NIL; +} diff --git a/src/backend/libpq/be-secure.c b/src/backend/libpq/be-secure.c index d723e74e8137..1431f92e3321 100644 --- a/src/backend/libpq/be-secure.c +++ b/src/backend/libpq/be-secure.c @@ -43,10 +43,6 @@ char *ssl_dh_params_file; char *ssl_passphrase_command; bool ssl_passphrase_command_supports_reload; -#ifdef USE_SSL -bool ssl_loaded_verify_locations = false; -#endif - /* GUC variable controlling SSL cipher list */ char *SSLCipherSuites = NULL; char *SSLCipherList = NULL; @@ -60,6 +56,8 @@ bool SSLPreferServerCiphers; int ssl_min_protocol_version = PG_TLS1_2_VERSION; int ssl_max_protocol_version = PG_TLS_ANY; +int ssl_snimode = SSL_SNIMODE_DEFAULT; + /* ------------------------------------------------------------ */ /* Procedures common to all secure sessions */ /* ------------------------------------------------------------ */ @@ -99,7 +97,7 @@ bool secure_loaded_verify_locations(void) { #ifdef USE_SSL - return ssl_loaded_verify_locations; + return be_tls_loaded_verify_locations(); #else return false; #endif diff --git a/src/backend/libpq/meson.build b/src/backend/libpq/meson.build index 31aa2faae1ec..4f6ec13bc741 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 000000000000..5a47f9cae7dc --- /dev/null +++ b/src/backend/libpq/pg_hosts.conf.sample @@ -0,0 +1,4 @@ +# PostgreSQL SNI Hostname mappings +# ================================ + +# HOSTNAME SSL CERTIFICATE SSL KEY diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c index 667df448732f..c43a259d82e9 100644 --- a/src/backend/utils/misc/guc.c +++ b/src/backend/utils/misc/guc.c @@ -55,6 +55,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" @@ -1968,6 +1969,31 @@ SelectConfigFiles(const char *userDoption, const char *progname) } SetConfigOption("ident_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE); + /* + * 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); + } + SetConfigOption("hosts_file", fname, PGC_POSTMASTER, PGC_S_OVERRIDE); + if (fname_is_malloced) free(fname); else diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index 2f8cbd867599..4dd0eea09144 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -491,6 +491,13 @@ static const struct config_enum_entry file_copy_method_options[] = { {NULL, 0, false} }; +static const struct config_enum_entry ssl_snimode_options[] = { + {"off", SSL_SNIMODE_OFF, false}, + {"default", SSL_SNIMODE_DEFAULT, false}, + {"strict", SSL_SNIMODE_STRICT, false}, + {NULL, 0, false} +}; + /* * Options for enum values stored in other modules */ @@ -555,6 +562,7 @@ char *cluster_name = ""; char *ConfigFileName; char *HbaFileName; char *IdentFileName; +char *HostsFileName; char *external_pid_file; char *application_name; @@ -4711,6 +4719,17 @@ struct config_string ConfigureNamesString[] = NULL, NULL, NULL }, + { + {"hosts_file", PGC_POSTMASTER, FILE_LOCATIONS, + gettext_noop("Sets the server's \"hosts\" configuration file."), + NULL, + GUC_SUPERUSER_ONLY + }, + &HostsFileName, + NULL, + NULL, NULL, NULL + }, + { {"external_pid_file", PGC_POSTMASTER, FILE_LOCATIONS, gettext_noop("Writes the postmaster PID to the specified file."), @@ -5386,6 +5405,18 @@ struct config_enum ConfigureNamesEnum[] = NULL, NULL, NULL }, + { + {"ssl_snimode", PGC_SIGHUP, CONN_AUTH_SSL, + gettext_noop("Sets the SNI mode to use."), + NULL, + GUC_SUPERUSER_ONLY, + }, + &ssl_snimode, + SSL_SNIMODE_DEFAULT, + ssl_snimode_options, + NULL, NULL, NULL + }, + { {"recovery_init_sync_method", PGC_SIGHUP, ERROR_HANDLING_OPTIONS, gettext_noop("Sets the method for synchronizing the data directory before crash recovery."), diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index 34826d01380b..6e6f97c254bd 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 @@ -120,6 +122,7 @@ #ssl_dh_params_file = '' #ssl_passphrase_command = '' #ssl_passphrase_command_supports_reload = off +#ssl_snimode = default # OAuth #oauth_validator_libraries = '' # comma-separated list of trusted validator modules diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c index 62bbd08d9f65..a4e2b36759bc 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; @@ -1530,6 +1531,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); @@ -2794,6 +2803,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"); @@ -2809,12 +2819,13 @@ 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\n" + "PG_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); } @@ -2822,6 +2833,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 3657f182db3e..3d8e33533b85 100644 --- a/src/include/libpq/hba.h +++ b/src/include/libpq/hba.h @@ -151,6 +151,25 @@ typedef struct IdentLine AuthToken *pg_user; } IdentLine; +typedef struct HostsLine +{ + int linenumber; + + char *sourcefile; + char *rawline; + + /* Required fields */ + bool default_host; + char *hostname; + char *ssl_key; + char *ssl_cert; + char *ssl_ca; + + /* Optional fields */ + char *ssl_passphrase_cmd; + bool ssl_passphrase_reload; +} HostsLine; + /* * 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-be.h b/src/include/libpq/libpq-be.h index d6e671a63825..e1631cb7b5c5 100644 --- a/src/include/libpq/libpq-be.h +++ b/src/include/libpq/libpq-be.h @@ -320,6 +320,7 @@ extern const char *be_tls_get_cipher(Port *port); extern void be_tls_get_peer_subject_name(Port *port, char *ptr, size_t len); extern void be_tls_get_peer_issuer_name(Port *port, char *ptr, size_t len); extern void be_tls_get_peer_serial(Port *port, char *ptr, size_t len); +extern bool be_tls_loaded_verify_locations(void); /* * Get the server certificate hash for SCRAM channel binding type @@ -332,7 +333,7 @@ extern char *be_tls_get_certificate_hash(Port *port, size_t *len); /* init hook for SSL, the default sets the password callback if appropriate */ #ifdef USE_OPENSSL -typedef void (*openssl_tls_init_hook_typ) (SSL_CTX *context, bool isServerStart); +typedef void (*openssl_tls_init_hook_typ) (SSL_CTX *context, bool isServerStart, HostsLine *host); extern PGDLLIMPORT openssl_tls_init_hook_typ openssl_tls_init_hook; #endif diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h index aeb66ca40cf3..5feed0eb0a46 100644 --- a/src/include/libpq/libpq.h +++ b/src/include/libpq/libpq.h @@ -107,6 +107,7 @@ extern PGDLLIMPORT char *ssl_crl_dir; extern PGDLLIMPORT char *ssl_key_file; extern PGDLLIMPORT int ssl_min_protocol_version; extern PGDLLIMPORT int ssl_max_protocol_version; +extern PGDLLIMPORT int ssl_snimode; extern PGDLLIMPORT char *ssl_passphrase_command; extern PGDLLIMPORT bool ssl_passphrase_command_supports_reload; extern PGDLLIMPORT char *ssl_dh_params_file; @@ -134,12 +135,20 @@ enum ssl_protocol_versions PG_TLS1_3_VERSION, }; +enum ssl_snimode +{ + SSL_SNIMODE_OFF = 0, + SSL_SNIMODE_DEFAULT, + SSL_SNIMODE_STRICT +}; + /* * prototypes for functions in be-secure-common.c */ extern int run_ssl_passphrase_command(const char *prompt, bool is_server_start, - char *buf, int size); + char *buf, int size, void *userdata); extern bool check_ssl_key_file_permissions(const char *ssl_key_file, bool isServerStart); +extern List *load_hosts(void); #endif /* LIBPQ_H */ diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h index f619100467df..025e7e95e906 100644 --- a/src/include/utils/guc.h +++ b/src/include/utils/guc.h @@ -288,6 +288,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/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c b/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c index d5992149821d..a85d85735cf2 100644 --- a/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c +++ b/src/test/modules/ssl_passphrase_callback/ssl_passphrase_func.c @@ -26,7 +26,7 @@ static char *ssl_passphrase = NULL; static int rot13_passphrase(char *buf, int size, int rwflag, void *userdata); /* hook function to set the callback */ -static void set_rot13(SSL_CTX *context, bool isServerStart); +static void set_rot13(SSL_CTX *context, bool isServerStart, HostsLine *host); /* * Module load callback @@ -53,7 +53,7 @@ _PG_init(void) } static void -set_rot13(SSL_CTX *context, bool isServerStart) +set_rot13(SSL_CTX *context, bool isServerStart, HostsLine *host) { /* warn if the user has set ssl_passphrase_command */ if (ssl_passphrase_command[0]) diff --git a/src/test/ssl/meson.build b/src/test/ssl/meson.build index cf8b2b9303a0..7a2a5b8ca8c2 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/004_sni.pl b/src/test/ssl/t/004_sni.pl new file mode 100644 index 000000000000..f0ce048273ac --- /dev/null +++ b/src/test/ssl/t/004_sni.pl @@ -0,0 +1,164 @@ + +# 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 hostname 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(); + +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 host=localhost sslsni=1"; + +$node->append_conf('postgresql.conf', "ssl_snimode=default"); +$node->reload; + +$node->connect_ok( + "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require", + "connect with correct server CA cert file sslmode=require"); + +$node->connect_fails( + "$connstr sslrootcert=ssl/root_ca.crt sslmode=verify-ca", + "connect fails with fallback hostname, without intermediate", + expected_stderr => qr/certificate verify failed/); + +# example.org serves the server cert and its intermediate 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", + "connect with configured hostname, serving intermediate server CA"); + +$node->connect_fails( + "$connstr sslrootcert=invalid sslmode=verify-ca", + "connect 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", + "connect still fails with fallback hostname, without intermediate", + expected_stderr => qr/certificate verify failed/); + +$node->connect_ok( + "$connstr host=localhost sslrootcert=ssl/root+server_ca.crt sslmode=verify-ca", + "connect with fallback hostname, intermediate included"); + +ok(unlink($node->data_dir . '/pg_hosts.conf')); +$node->append_conf('pg_hosts.conf', + "localhost server-cn-only.crt server-cn-only.key root_ca.crt"); +$node->append_conf('postgresql.conf', "ssl_snimode=strict"); +$node->reload; + +$node->connect_fails( + "$connstr host=example.org sslrootcert=ssl/root+server_ca.crt sslmode=require", + "connect with missing hostconfig and snimode=struct", + expected_stderr => qr/tlsv1 unrecognized name/); + +$node->connect_ok( + "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslsni=1", + "connect with correct server CA cert file sslmode=require"); + +# Attempts at connecting without SNI when the server is using strict mode should +# result in connection failure. +$node->connect_fails( + "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslsni=0", + "connect with correct server CA cert file without SNI for strict mode", + expected_stderr => qr/tlsv1 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' +); +my $result = $node->restart(fail_ok => 1); +is($result, 0, + '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, + '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", + "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", + "1 connect with correct server CA cert file sslmode=require"); + +# Test reloading a passphrase protected key without reloading support in the +# passphrase hook. Connecting after restart should succeed but not after the +# following reload. +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' +); +$result = $node->restart(fail_ok => 1); +is($result, 1, + 'restart succeeds with password-protected key when using the correct passphrase command' +); +$node->connect_ok( + "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require", + "connect with correct server CA cert file sslmode=require"); + +$node->reload; +$node->connect_fails( + "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require", + "connect fails since the passphrase protected key cannot be reloaded", + expected_stderr => qr/tlsv1 unrecognized name/); + +done_testing(); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9ea573fae210..75c345081b21 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1199,6 +1199,8 @@ HeapTupleHeader HeapTupleHeaderData HeapTupleTableSlot HistControl +HostContext +HostsLine HotStandbyState I32 ICU_Convert_Func