public inbox for [email protected]
help / color / mirror / Atom feedFrom: Daniel Gustafsson <[email protected]>
To: Jacob Champion <[email protected]>
Cc: Pgsql Hackers <[email protected]>
Subject: Re: Serverside SNI support in libpq
Date: Wed, 4 Dec 2024 14:44:18 +0100
Message-ID: <[email protected]> (raw)
In-Reply-To: <CAOYmi+nYV6Rr9BY4YfYyVdiQ5TzMZray6QPXwiO3pYSaow+-Tg@mail.gmail.com>
References: <[email protected]>
<CAOYmi+k_YBsO3jnxx9HBcChNzkzRW=Erm4yiPGsKV2_6rU+-4g@mail.gmail.com>
<[email protected]>
<CAOYmi+nYV6Rr9BY4YfYyVdiQ5TzMZray6QPXwiO3pYSaow+-Tg@mail.gmail.com>
> On 4 Dec 2024, at 01:43, Jacob Champion <[email protected]> wrote:
>
> On Tue, Dec 3, 2024 at 5:58 AM Daniel Gustafsson <[email protected]> wrote:
>>> Have you seen any weird behavior like this on your end? I'm starting
>>> to doubt my test setup...
>>
>> I've not been able to reproduce any behaviour like what you describe.
>
> Hm, v2 is different enough that I'm going to need to check my notes
> and try to reproduce again. At first glance, I am still seeing strange
> reload behavior (e.g. issuing `pg_ctl reload` a couple of times in a
> row leads to the server disappearing without any log messages
> indicating why).
>
>>> On the plus side, I now have a handful of
>>> debugging patches for a future commitfest.
>>
>> Do you have them handy for running tests on this version?
>
> I'll work on cleaning them up. I'd meant to contribute them
> individually by now, but I got a bit sidetracked...
No worries, I know you have a big path on your plate right now. The attached
v3 fixes a small buglet in the tests and adds silly reload testing to try and
stress out any issues.
--
Daniel Gustafsson
Attachments:
[application/octet-stream] v3-0001-Serverside-SNI-support-for-libpq.patch (45.3K, 2-v3-0001-Serverside-SNI-support-for-libpq.patch)
download | inline diff:
From 2033491062b359cd62df4ec1560947ae21f0868c Mon Sep 17 00:00:00 2001
From: Daniel Gustafsson <[email protected]>
Date: Mon, 4 Nov 2024 13:52:23 +0100
Subject: [PATCH v3] 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 | 62 ++++
doc/src/sgml/runtime.sgml | 50 +++
src/backend/Makefile | 1 +
src/backend/libpq/be-secure-common.c | 203 ++++++++++-
src/backend/libpq/be-secure-openssl.c | 315 ++++++++++++++++--
src/backend/libpq/be-secure.c | 8 +-
src/backend/libpq/meson.build | 1 +
src/backend/libpq/pg_hosts.conf.sample | 5 +
src/backend/utils/misc/guc.c | 26 ++
src/backend/utils/misc/guc_tables.c | 31 ++
src/backend/utils/misc/postgresql.conf.sample | 1 +
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 +
src/test/ssl/meson.build | 1 +
src/test/ssl/t/004_sni.pl | 135 ++++++++
src/tools/pgindent/typedefs.list | 2 +
19 files changed, 843 insertions(+), 48 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 e0c8325a39..1cecaccee7 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1652,6 +1652,68 @@ 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. Any connection specifying
+ <xref linkend="libpq-connect-sslsni"/> to <literal>1</literal>
+ a hostname which is missing in <filename>pg_hosts.conf</filename>
+ will be attempted using the default configuration. 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.
+ </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 94135e9d5e..0ac79ae28d 100644
--- a/doc/src/sgml/runtime.sgml
+++ b/doc/src/sgml/runtime.sgml
@@ -2444,6 +2444,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>
@@ -2571,6 +2577,50 @@ 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
+ SSL certificate, key and CA certificate to use for the connection.
+ </para>
+
+ <para>
+ SNI configuration is defined in the hosts configuration file, which is
+ named <filename>pg_hosts.conf</filename> and 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>
+ </sect2>
</sect1>
<sect1 id="gssapi-enc">
diff --git a/src/backend/Makefile b/src/backend/Makefile
index 84302cc6da..bc8accbde0 100644
--- a/src/backend/Makefile
+++ b/src/backend/Makefile
@@ -186,6 +186,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 0cb201acb1..e483ca7944 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 91a86d62a3..5acfdeb625 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,16 @@ 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 *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 +110,155 @@ static const char *cert_errdetail;
int
be_tls_init(bool isServerStart)
+{
+ SSL_CTX *ctx;
+ List *sni_hosts = NIL;
+
+ /*
+ * If there are contexts loaded when we init they should be released. This
+ * should only be possible when reloading, but to keep any subtle bugs at
+ * arms length we check unconditionally with an assert for non-production
+ * builds.
+ */
+ if (contexts != NIL)
+ {
+ Assert(isServerStart == false);
+ free_contexts();
+ }
+
+ /*
+ * When ssl_snimode is off or default we load the certificate and key
+ * specified in postgresql.conf and set that as the default host.
+ */
+ if (ssl_snimode == SSL_SNIMODE_OFF || ssl_snimode == SSL_SNIMODE_DEFAULT)
+ {
+ HostContext *host_context;
+ HostsLine line;
+
+ 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;
+ }
+
+ host_context = palloc0(sizeof(HostContext));
+
+ host_context->hostname = pstrdup("*");
+ host_context->context = ctx;
+ host_context->default_host = true;
+
+ /*
+ * Set flag to remember whether CA store has been loaded into
+ * SSL_context.
+ */
+ if (ssl_ca_file[0])
+ host_context->ssl_loaded_verify_locations = true;
+
+ /*
+ * The contexts list is not used in ssl_snimode off but we add the
+ * entry there anyways for consistency with the other modes.
+ */
+ contexts = lappend(contexts, host_context);
+
+ /*
+ * Install the default certificate which for ssl_snimode default can
+ * be overridden in the callback if a hostname match is found.
+ */
+ SSL_context = ctx;
+ Host_context = host_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 a working pg_hosts file,
+ */
+ if (sni_hosts == NIL && ssl_snimode == SSL_SNIMODE_STRICT)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("could not load pg_hosts.conf file"));
+ return -1;
+ }
+
+ foreach(line, sni_hosts)
+ {
+ HostContext *host_context;
+ HostsLine *host = lfirst(line);
+
+ SSL_context = ssl_init_context(isServerStart, host);
+ if (SSL_context == NULL)
+ {
+ ereport(isServerStart ? FATAL : LOG,
+ errcode(ERRCODE_CONFIG_FILE_ERROR),
+ errmsg("unable to load certificate from pg_hosts.conf file"));
+ return -1;
+ }
+
+ host_context = palloc(sizeof(HostContext));
+ host_context->hostname = pstrdup(host->hostname);
+ host_context->context = SSL_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
+ * 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 +284,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 +302,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 +320,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 +484,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 +566,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
@@ -1132,7 +1288,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 +1525,60 @@ alpn_cb(SSL *ssl,
}
}
+static int
+sni_servername_cb(SSL *ssl, int *al, void *arg)
+{
+ const char *tlsext_hostname;
+ ListCell *cell;
+ HostContext *host_context;
+
+ Assert(ssl_snimode != SSL_SNIMODE_OFF);
+
+ tlsext_hostname = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
+
+ 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
+ return SSL_TLSEXT_ERR_OK;
+ }
+
+ foreach(cell, contexts)
+ {
+ host_context = lfirst(cell);
+
+ if (strcmp(host_context->hostname, tlsext_hostname) == 0)
+ {
+ Host_context = host_context;
+ SSL_context = host_context->context;
+ SSL_set_SSL_CTX(ssl, SSL_context);
+ return SSL_TLSEXT_ERR_OK;
+ }
+ }
+
+ 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 can return without doing anything since we
+ * already installed the context for the default host when parsing the
+ * hosts file.
+ */
+ Assert(SSL_context);
+ return SSL_TLSEXT_ERR_OK;
+}
/*
* Set DH parameters for generating ephemeral DH keys. The
@@ -1578,6 +1788,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 +1987,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 +2015,22 @@ default_openssl_tls_init(SSL_CTX *context, bool isServerStart)
SSL_CTX_set_default_passwd_cb(context, dummy_ssl_passwd_cb);
}
}
+
+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 2139f81f24..ad3066b63c 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 7c65314512..1c6269262c 100644
--- a/src/backend/libpq/meson.build
+++ b/src/backend/libpq/meson.build
@@ -30,5 +30,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 0000000000..608210686e
--- /dev/null
+++ b/src/backend/libpq/pg_hosts.conf.sample
@@ -0,0 +1,5 @@
+# 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 c10c0844ab..b3e1cf0b25 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 8cf1afbad2..dca5a15f5c 100644
--- a/src/backend/utils/misc/guc_tables.c
+++ b/src/backend/utils/misc/guc_tables.c
@@ -474,6 +474,13 @@ static const struct config_enum_entry wal_compression_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
*/
@@ -538,6 +545,7 @@ char *cluster_name = "";
char *ConfigFileName;
char *HbaFileName;
char *IdentFileName;
+char *HostsFileName;
char *external_pid_file;
char *application_name;
@@ -4562,6 +4570,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."),
@@ -5204,6 +5223,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 a2ac7575ca..902c4eccef 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -120,6 +120,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 9a91830783..984763da08 100644
--- a/src/bin/initdb/initdb.c
+++ b/src/bin/initdb/initdb.c
@@ -176,6 +176,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;
@@ -1512,6 +1513,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);
@@ -2771,6 +2780,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");
@@ -2786,12 +2796,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);
}
@@ -2799,6 +2810,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 8ea837ae82..d1eb750368 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -146,6 +146,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 9109b2c334..cf0e87a28c 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -314,6 +314,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
@@ -326,7 +327,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 07e5b12536..98da8b61eb 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 840b0fe57f..4c33ba7ca4 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -284,6 +284,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/ssl/meson.build b/src/test/ssl/meson.build
index b3c5503f79..55f5887d9c 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 0000000000..1e7e397080
--- /dev/null
+++ b/src/test/ssl/t/004_sni.pl
@@ -0,0 +1,135 @@
+
+# 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 =
+ "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");
+
+ok(unlink($node->data_dir . '/pg_hosts.conf'));
+$node->append_conf('pg_hosts.conf', "localhost server.crt server.key root.crt");
+$node->append_conf('postgresql.conf', "ssl_snimode=strict");
+$node->reload;
+
+$node->connect_fails(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require",
+ expected_stderr => qr/unexpected eof/);
+
+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->reload;
+
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require");
+
+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');
+
+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');
+
+$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_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "1 connect with correct server CA cert file sslmode=require");
+
+# Stress testing during patch debugging and review, unlikely to be merged in
+# this state.
+for (my $i = 0; $i < 100; $i++)
+{
+ if (int(rand(10)) < 3)
+ {
+ 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');
+ }
+ $node->append_conf('pg_hosts.conf', 'localhost_' . $i . ' server-cn-only.crt server-password.key root+client_ca.crt "echo secret1" on');
+ $node->reload;
+ $node->reload;
+ $node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require during reload loop");
+}
+$node->append_conf('pg_hosts.conf', 'localhost server-cn-only.crt server-password.key root+client_ca.crt "echo secret1" on');
+$node->connect_ok(
+ "$connstr sslrootcert=ssl/root+server_ca.crt sslmode=require",
+ "connect with correct server CA cert file sslmode=require after reload loop");
+
+# 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/unexpected eof/);
+
+done_testing();
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 2d4c870423..15ccf4cf46 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1158,6 +1158,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]
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