public inbox for [email protected]
help / color / mirror / Atom feedFrom: Daniel Gustafsson <[email protected]>
To: Andres Freund <[email protected]>
Cc: Jacob Champion <[email protected]>
Cc: Michael Paquier <[email protected]>
Cc: Pgsql Hackers <[email protected]>
Subject: Re: Serverside SNI support in libpq
Date: Wed, 27 Aug 2025 21:49:34 +0200
Message-ID: <[email protected]> (raw)
In-Reply-To: <aa7gx3mychf3m2g67mbslzbxjy3if4enpcflstoa5pol3432x5@ugqz45gsvurq>
References: <[email protected]>
<CAOYmi+k_YBsO3jnxx9HBcChNzkzRW=Erm4yiPGsKV2_6rU+-4g@mail.gmail.com>
<[email protected]>
<CAOYmi+nYV6Rr9BY4YfYyVdiQ5TzMZray6QPXwiO3pYSaow+-Tg@mail.gmail.com>
<[email protected]>
<[email protected]>
<[email protected]>
<[email protected]>
<CAOYmi+mSrV8hRaQkvGDf1Df4cmpv5SeTbTxppyxeonMe6MW8nA@mail.gmail.com>
<[email protected]>
<aa7gx3mychf3m2g67mbslzbxjy3if4enpcflstoa5pol3432x5@ugqz45gsvurq>
> On 13 May 2025, at 15:46, Andres Freund <[email protected]> wrote:
> This is not passing CI on windows...
> https://cirrus-ci.com/build/4765059278176256
When looking into why the SNI tests failed on Windows I think I found a
pre-existing issue that we didn't have tests for, which my patch added tests
for and thus broke.
The test I added was to check restarting and reloading with ssl passphrase
commands (which we do have testcoverage for) with a subsequent connection test
to ensure it didn't just work to start the cluster.
When ssl_passphrase_command_supports_reload is set to 'off', the cluster should
allow connections until a reload has been issued. That works fine except on
Windows where our process-model is such that a new connection will re-run the
passphrase command, which inevitably fails as it's not configured for reload.
The test in my patch exposed this out of (happy) accident, but it can be
reproduced in HEAD as well. The attached version modifies the ssl tests to
cover this with a connection attempt. If I'm not mistaken though, there should
probably be a docs patch to make it clear how this works on Windows.
No codechanges on top of the test fix.
--
Daniel Gustafsson
Attachments:
[application/octet-stream] v8-0001-Serverside-SNI-support-for-libpq.patch (56.1K, 2-v8-0001-Serverside-SNI-support-for-libpq.patch)
download | inline diff:
From 42c1c44ebec355d48755d7821782d49a15568c77 Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Mon, 2 Jun 2025 10:25:08 +0200
Subject: [PATCH v8] 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 <[email protected]>
Reviewed-by: Jacob Champion <[email protected]>
Discussion: https://postgr.es/m/[email protected]
---
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/perl/PostgreSQL/Test/Cluster.pm | 35 ++
src/test/ssl/meson.build | 1 +
src/test/ssl/t/001_ssltests.pl | 29 +-
src/test/ssl/t/004_sni.pl | 175 +++++++++
src/test/ssl/t/SSL/Server.pm | 8 +
src/tools/pgindent/typedefs.list | 2 +
23 files changed, 1007 insertions(+), 63 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 0a4b3e55ba5..17ba6538de6 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1694,6 +1694,72 @@ include_dir 'conf.d'
</para>
</listitem>
</varlistentry>
+
+ <varlistentry id="guc-ssl-snimode" xreflabel="ssl_snimode">
+ <term><varname>ssl_snimode</varname> (<type>enum</type>)
+ <indexterm>
+ <primary><varname>ssl_snimode</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ This parameter determines if the server will inspect the <acronym>SNI</acronym> TLS extension
+ when establishing the connection, and how it should be interpreted.
+ Valid values are currently: <literal>off</literal>, <literal>default</literal> and <literal>strict</literal>.
+ </para>
+ <para>
+ <variablelist>
+ <varlistentry id="guc-ssl-snimode-off">
+ <term><literal>off</literal></term>
+ <listitem>
+ <para>
+ SNI is not enabled and no configuration from
+ <filename>pg_hosts.conf</filename> is loaded. Configuration of SSL
+ for all connections is done with <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/> and <xref linkend="guc-ssl-ca-file"/>.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="guc-ssl-snimode-default">
+ <term><literal>default</literal></term>
+ <listitem>
+ <para>
+ SNI is enabled and hostname configuration is loaded from
+ <filename>pg_hosts.conf</filename>. <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/> and <xref linkend="guc-ssl-ca-file"/>
+ are loaded as the default configuration. Connections specifying
+ <xref linkend="libpq-connect-sslsni"/> to <literal>1</literal>
+ will be attempted using the default configuration if the hostname
+ is missing in <filename>pg_hosts.conf</filename>. If the hostname
+ matches an entry from <filename>pg_hosts.conf</filename>, then the
+ configuration from that entry will be used for setting up the
+ connection.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="guc-ssl-snimode-strict">
+ <term><literal>strict</literal></term>
+ <listitem>
+ <para>
+ SNI is enabled and all connections are required to set <xref
+ linkend="libpq-connect-sslsni"/> to <literal>1</literal> and
+ specify a hostname matching an entry in
+ <filename>pg_hosts.conf</filename>. Any connection without <xref
+ linkend="libpq-connect-sslsni"/> or with a hostname missing from
+ <filename>pg_hosts.conf</filename> will be rejected.
+ <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/> and <xref linkend="guc-ssl-ca-file"/>
+ are loaded in order to drive the handshake until the appropriate
+ configuration has been selected.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </para>
+ </listitem>
+ </varlistentry>
</variablelist>
</sect2>
</sect1>
diff --git a/doc/src/sgml/runtime.sgml b/doc/src/sgml/runtime.sgml
index 0c60bafac63..fa6fe07adc0 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
<entry>client certificate must not be on this list</entry>
</row>
+ <row>
+ <entry><filename>$PGDATA/pg_hosts.conf</filename></entry>
+ <entry>SNI configuration</entry>
+ <entry>defines which certificates to use for which server hostname</entry>
+ </row>
+
</tbody>
</tgroup>
</table>
@@ -2572,6 +2578,67 @@ openssl x509 -req -in server.csr -text -days 365 \
</para>
</sect2>
+ <sect2 id="ssl-sni">
+ <title>SNI Configuration</title>
+
+ <para>
+ <productname>PostgreSQL</productname> can be configured for
+ <acronym>SNI</acronym> using the <filename>pg_hosts.conf</filename>
+ configuration file. <productname>PostgreSQL</productname> 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.
+ </para>
+
+ <para>
+ SNI configuration is defined in the hosts configuration file,
+ <filename>pg_hosts.conf</filename>, which is stored in the clusters
+ data directory. The hosts configuration file contains lines of the general
+ forms:
+<synopsis>
+<replaceable>hostname</replaceable> <replaceable>SSL_certificate</replaceable> <replaceable>SSL_key</replaceable> <replaceable>SSL_CA_certificate</replaceable> <replaceable>SSL_passphrase_cmd</replaceable> <replaceable>SSL_passphrase_cmd_reload</replaceable>
+<replaceable>include</replaceable> <replaceable>file</replaceable>
+<replaceable>include_if_exists</replaceable> <replaceable>file</replaceable>
+<replaceable>include_dir</replaceable> <replaceable>directory</replaceable>
+</synopsis>
+ Comments, whitespace and line continuations are handled in the same way as
+ in <filename>pg_hba.conf</filename>. <replaceable>hostname</replaceable>
+ is matched against the hostname TLS extension in the SSL handshake.
+ <replaceable>SSL_certificate</replaceable>,
+ <replaceable>SSL_key</replaceable>,
+ <replaceable>SSL_CA_certificate</replaceable>,
+ <replaceable>SSL_passphrase_cmd</replaceable>, and
+ <replaceable>SSL_passphrase_cmd_reload</replaceable>
+ are treated like
+ <xref linkend="guc-ssl-cert-file"/>,
+ <xref linkend="guc-ssl-key-file"/>,
+ <xref linkend="guc-ssl-ca-file"/>,
+ <xref linkend="guc-ssl-passphrase-command"/>, and
+ <xref linkend="guc-ssl-passphrase-command-supports-reload"/> respectively.
+ All fields except <replaceable>SSL_passphrase_cmd</replaceable> and
+ <replaceable>SSL_passphrase_cmd_reload</replaceable> are required. If
+ <replaceable>SSL_passphrase_cmd</replaceable> is defined but not
+ <replaceable>SSL_passphrase_cmd_reload</replaceable> then the default
+ value for <replaceable>SSL_passphrase_cmd_reload</replaceable> is
+ <literal>off</literal>.
+ </para>
+ <para>
+ The SSL configuration from <filename>postgresql.conf</filename> is used
+ in order to set up the TLS handshake such that the hostname extension can
+ be inspected. When <xref linkend="guc-ssl-snimode"/> is set to
+ <literal>default</literal> this configuration will be the defualt fallback
+ if no matching hostname is found in <filename>pg_hosts.conf</filename>. If
+ <xref linkend="guc-ssl-snimode"/> is set to <literal>strict</literal> it
+ will only be used to for the handshake until the hostname is inspected, it
+ will not be used for the connection.
+ </para>
+ <para>
+ It is currently not possible to set different <literal>clientname</literal>
+ values for the different certificates. Any <literal>clientname</literal>
+ setting in <filename>pg_hba.conf</filename> will be applied during
+ authentication regardless of which set of certificates have been loaded
+ via an SNI enabled connection.
+ </para>
+ </sect2>
</sect1>
<sect1 id="gssapi-enc">
diff --git a/src/backend/Makefile b/src/backend/Makefile
index 7344c8c7f5c..2d1691c7950 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 e8b837d1fa7..67a50c7b24c 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 c8b63ef8249..14ff1d78c40 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -51,9 +51,18 @@
#endif
#include <openssl/x509v3.h>
+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 d723e74e813..1431f92e332 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 31aa2faae1e..4f6ec13bc74 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..5a47f9cae7d
--- /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 46fdefebe35..ed670822ea3 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 f137129209f..3339e829314 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;
@@ -4787,6 +4795,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."),
@@ -5457,6 +5476,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 a9d8293474a..57b1be3c38f 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
@@ -121,6 +123,7 @@
#ssl_dh_params_file = ''
#ssl_passphrase_command = ''
#ssl_passphrase_command_supports_reload = off
+#ssl_snimode = default
#------------------------------------------------------------------------------
diff --git a/src/bin/initdb/initdb.c b/src/bin/initdb/initdb.c
index 92fe2f531f7..087cea4fffc 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);
@@ -2791,6 +2800,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");
@@ -2806,12 +2816,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);
}
@@ -2819,6 +2830,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 3657f182db3..3d8e33533b8 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 d6e671a6382..e1631cb7b5c 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 aeb66ca40cf..5feed0eb0a4 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 f619100467d..025e7e95e90 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 d5992149821..a85d85735cf 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/perl/PostgreSQL/Test/Cluster.pm b/src/test/perl/PostgreSQL/Test/Cluster.pm
index 35413f14019..a46ac325045 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<prove> invocation. If given,
+instead return 0 for failure instead of bailing out.
+
+=item log_unlike => B<pattern>
+
+When defined, the logfile is inspected for the presence of the fragment by
+matching the specified pattern. If the pattern matches agsinst the logfile a
+test failure will be logged.
+
+=item log_like => B<pattern>
+
+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 d8e0fb518e0..e5a9402cd9c 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 b2eb18d3e81..0f0f64b6c7c 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -51,8 +51,15 @@ my $SERVERHOSTCIDR = '127.0.0.1/32';
my $supports_sslcertmode_require =
check_pg_config("#define HAVE_SSL_CTX_SET_CERT_CB 1");
-# Allocation of base connection string shared among multiple tests.
-my $common_connstr;
+# Set of default settings for SSL parameters in connection string. This
+# makes the tests protected against any defaults the environment may have
+# in ~/.postgresql/.
+my $default_ssl_connstr =
+ "sslkey=invalid sslcert=invalid sslrootcert=invalid sslcrl=invalid sslcrldir=invalid";
+
+# Base connection string shared among multiple tests.
+my $common_connstr =
+ "$default_ssl_connstr user=ssltestuser dbname=trustdb hostaddr=$SERVERHOSTADDR host=common-name.pg-ssltest.test";
#### Set up the server.
@@ -85,7 +92,7 @@ switch_server_cert(
passphrase_cmd => 'echo wrongpassword',
restart => 'no');
-$result = $node->restart(fail_ok => 1);
+$result = $node->restart(fail_ok => 1, log_like => qr/could not load private key file/);
is($result, 0,
'restart fails with password-protected key file with wrong password');
@@ -95,11 +102,16 @@ switch_server_cert(
cafile => 'root+client_ca',
keyfile => 'server-password',
passphrase_cmd => 'echo secret1',
+ passphrase_cmd_reload => 'yes',
restart => 'no');
-$result = $node->restart(fail_ok => 1);
+$result = $node->restart(fail_ok => 1, log_unlike => qr/could not load private key file/);
is($result, 1, 'restart succeeds with password-protected key file');
+$node->connect_ok(
+ "$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require");
+
# Test compatibility of SSL protocols.
# TLSv1.1 is lower than TLSv1.2, so it won't work.
$node->append_conf(
@@ -139,15 +151,6 @@ note "running client tests";
switch_server_cert($node, certfile => 'server-cn-only');
-# Set of default settings for SSL parameters in connection string. This
-# makes the tests protected against any defaults the environment may have
-# in ~/.postgresql/.
-my $default_ssl_connstr =
- "sslkey=invalid sslcert=invalid sslrootcert=invalid sslcrl=invalid sslcrldir=invalid";
-
-$common_connstr =
- "$default_ssl_connstr user=ssltestuser dbname=trustdb hostaddr=$SERVERHOSTADDR host=common-name.pg-ssltest.test";
-
SKIP:
{
skip "Keylogging is not supported with LibreSSL", 5 if $libressl;
diff --git a/src/test/ssl/t/004_sni.pl b/src/test/ssl/t/004_sni.pl
new file mode 100644
index 00000000000..b3b2821a2bd
--- /dev/null
+++ b/src/test/ssl/t/004_sni.pl
@@ -0,0 +1,175 @@
+
+# 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=strict",
+ 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");
+$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'
+);
+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",
+ "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");
+
+done_testing();
diff --git a/src/test/ssl/t/SSL/Server.pm b/src/test/ssl/t/SSL/Server.pm
index efbd0dafaf6..31e50c8722f 100644
--- a/src/test/ssl/t/SSL/Server.pm
+++ b/src/test/ssl/t/SSL/Server.pm
@@ -296,6 +296,11 @@ The CRL directory to use. Implementation is SSL backend specific.
The passphrase command to use. If not set, an empty passphrase command will
be set.
+=item passphrase_cmd_reload => B<value>
+
+Whether or not to allow passphrase command reloading. If set the passphrase
+command will set to allow reloading.
+
=item restart => B<value>
If set to 'no', the server won't be restarted after updating the settings.
@@ -327,6 +332,9 @@ sub switch_server_cert
"ssl_passphrase_command='" . $params{passphrase_cmd} . "'")
if defined $params{passphrase_cmd};
+ $node->append_conf('sslconfig.conf', 'ssl_passphrase_command_supports_reload=on')
+ if defined $params{passphrase_cmd_reload};
+
return if (defined($params{restart}) && $params{restart} eq 'no');
$node->restart;
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index a13e8162890..5b61b9a1d4d 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1202,6 +1202,8 @@ HeapTupleHeader
HeapTupleHeaderData
HeapTupleTableSlot
HistControl
+HostContext
+HostsLine
HotStandbyState
I32
ICU_Convert_Func
--
2.39.3 (Apple Git-146)
view thread (58+ messages) latest in thread
reply
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Reply to all the recipients using the --to and --cc options:
reply via email
To: [email protected]
Cc: [email protected], [email protected], [email protected], [email protected], [email protected]
Subject: Re: Serverside SNI support in libpq
In-Reply-To: <[email protected]>
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox