</para>
</listitem>
</varlistentry>
+
+ <varlistentry id="guc-ssl-passphrase-command" xreflabel="ssl_passphrase_command">
+ <term><varname>ssl_passphrase_command</varname> (<type>string</type>)
+ <indexterm>
+ <primary><varname>ssl_passphrase_command</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ Sets an external command to be invoked when a passphrase for
+ decrypting an SSL file such as a private key needs to be obtained. By
+ default, this parameter is empty, which means the built-in prompting
+ mechanism is used.
+ </para>
+ <para>
+ The command must print the passphrase to the standard output and exit
+ with code 0. In the parameter value, <literal>%p</literal> is
+ replaced by a prompt string. (Write <literal>%%</literal> for a
+ literal <literal>%</literal>.) Note that the prompt string will
+ probably contain whitespace, so be sure to quote adequately. A single
+ newline is stripped from the end of the output if present.
+ </para>
+ <para>
+ The command does not actually have to prompt the user for a
+ passphrase. It can read it from a file, obtain it from a keychain
+ facility, or similar. It is up to the user to make sure the chosen
+ mechanism is adequately secure.
+ </para>
+ <para>
+ This parameter can only be set in the <filename>postgresql.conf</filename>
+ file or on the server command line.
+ </para>
+ </listitem>
+ </varlistentry>
+
+ <varlistentry id="guc-ssl-passphrase-command-supports-reload" xreflabel="ssl_passphrase_command_supports_reload">
+ <term><varname>ssl_passphrase_command_supports_reload</varname> (<type>boolean</type>)
+ <indexterm>
+ <primary><varname>ssl_passphrase_command_supports_reload</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ This setting determines whether the passphrase command set by
+ <varname>ssl_passphrase_command</varname> will also be called during a
+ configuration reload if a key file needs a passphrase. If this
+ setting is false (the default), then
+ <varname>ssl_passphrase_command</varname> will be ignored during a
+ reload and the SSL configuration will not be reloaded if a passphrase
+ is needed. That setting is appropriate for a command that requires a
+ TTY for prompting, which might not be available when the server is
+ running. Setting this to true might be appropriate if the passphrase
+ is obtained from a file, for example.
+ </para>
+ <para>
+ This parameter can only be set in the <filename>postgresql.conf</filename>
+ file or on the server command line.
+ </para>
+ </listitem>
+ </varlistentry>
</variablelist>
</sect2>
</sect1>
# be-fsstubs is here for historical reasons, probably belongs elsewhere
-OBJS = be-fsstubs.o be-secure.o auth.o crypt.o hba.o ifaddr.o pqcomm.o \
+OBJS = be-fsstubs.o be-secure.o be-secure-common.o auth.o crypt.o hba.o ifaddr.o pqcomm.o \
pqformat.o pqmq.o pqsignal.o auth-scram.o
ifeq ($(with_openssl),yes)
--- /dev/null
+/*-------------------------------------------------------------------------
+ *
+ * be-secure-common.c
+ *
+ * common implementation-independent SSL support code
+ *
+ * While be-secure.c contains the interfaces that the rest of the
+ * communications code calls, this file contains support routines that are
+ * used by the library-specific implementations such as be-secure-openssl.c.
+ *
+ * Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ * src/backend/libpq/be-secure-common.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "libpq/libpq.h"
+#include "storage/fd.h"
+
+/*
+ * Run ssl_passphrase_command
+ *
+ * prompt will be substituted for %p. is_server_start determines the loglevel
+ * of error messages.
+ *
+ * The result will be put in buffer buf, which is of size size. The return
+ * value is the length of the actual result.
+ */
+int
+run_ssl_passphrase_command(const char *prompt, bool is_server_start, char *buf, int size)
+{
+ int loglevel = is_server_start ? ERROR : LOG;
+ StringInfoData command;
+ char *p;
+ FILE *fh;
+ int pclose_rc;
+ size_t len = 0;
+
+ Assert(prompt);
+ Assert(size > 0);
+ buf[0] = '\0';
+
+ initStringInfo(&command);
+
+ for (p = ssl_passphrase_command; *p; p++)
+ {
+ if (p[0] == '%')
+ {
+ switch (p[1])
+ {
+ case 'p':
+ appendStringInfoString(&command, prompt);
+ p++;
+ break;
+ case '%':
+ appendStringInfoChar(&command, '%');
+ p++;
+ break;
+ default:
+ appendStringInfoChar(&command, p[0]);
+ }
+ }
+ else
+ appendStringInfoChar(&command, p[0]);
+ }
+
+ fh = OpenPipeStream(command.data, "r");
+ if (fh == NULL)
+ {
+ ereport(loglevel,
+ (errcode_for_file_access(),
+ errmsg("could not execute command \"%s\": %m",
+ command.data)));
+ goto error;
+ }
+
+ if (!fgets(buf, size, fh))
+ {
+ if (ferror(fh))
+ {
+ ereport(loglevel,
+ (errcode_for_file_access(),
+ errmsg("could not read from command \"%s\": %m",
+ command.data)));
+ goto error;
+ }
+ }
+
+ pclose_rc = ClosePipeStream(fh);
+ if (pclose_rc == -1)
+ {
+ ereport(loglevel,
+ (errcode_for_file_access(),
+ errmsg("could not close pipe to external command: %m")));
+ goto error;
+ }
+ else if (pclose_rc != 0)
+ {
+ ereport(loglevel,
+ (errcode_for_file_access(),
+ errmsg("command \"%s\" failed",
+ command.data),
+ errdetail_internal("%s", wait_result_to_str(pclose_rc))));
+ goto error;
+ }
+
+ /* strip trailing newline */
+ len = strlen(buf);
+ if (buf[len - 1] == '\n')
+ buf[len-- -1] = '\0';
+
+error:
+ pfree(command.data);
+ return len;
+}
static DH *load_dh_file(char *filename, bool isServerStart);
static DH *load_dh_buffer(const char *, size_t);
-static int ssl_passwd_cb(char *buf, int size, int rwflag, void *userdata);
+static int ssl_external_passwd_cb(char *buf, int size, int rwflag, void *userdata);
+static int dummy_ssl_passwd_cb(char *buf, int size, int rwflag, void *userdata);
static int verify_cb(int, X509_STORE_CTX *);
static void info_cb(const SSL *ssl, int type, int args);
static bool initialize_dh(SSL_CTX *context, bool isServerStart);
static SSL_CTX *SSL_context = NULL;
static bool SSL_initialized = false;
-static bool ssl_passwd_cb_called = false;
+static bool dummy_ssl_passwd_cb_called = false;
+static bool ssl_is_server_start;
/* ------------------------------------------------------------ */
SSL_CTX_set_mode(context, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
/*
- * If reloading, override OpenSSL's default handling of
- * passphrase-protected files, because we don't want to prompt for a
- * passphrase in an already-running server. (Not that the default
- * handling is very desirable during server start either, but some people
- * insist we need to keep it.)
+ * Set password callback
*/
- if (!isServerStart)
- SSL_CTX_set_default_passwd_cb(context, ssl_passwd_cb);
+ if (isServerStart)
+ {
+ if (ssl_passphrase_command[0])
+ SSL_CTX_set_default_passwd_cb(context, ssl_external_passwd_cb);
+ }
+ else
+ {
+ if (ssl_passphrase_command[0] && ssl_passphrase_command_supports_reload)
+ SSL_CTX_set_default_passwd_cb(context, ssl_external_passwd_cb);
+ else
+ /*
+ * If reloading and no external command is configured, override
+ * OpenSSL's default handling of passphrase-protected files,
+ * because we don't want to prompt for a passphrase in an
+ * already-running server.
+ */
+ SSL_CTX_set_default_passwd_cb(context, dummy_ssl_passwd_cb);
+ }
+ /* used by the callback */
+ ssl_is_server_start = isServerStart;
/*
* Load and verify server's certificate and private key
/*
* OK, try to load the private key file.
*/
- ssl_passwd_cb_called = false;
+ dummy_ssl_passwd_cb_called = false;
if (SSL_CTX_use_PrivateKey_file(context,
ssl_key_file,
SSL_FILETYPE_PEM) != 1)
{
- if (ssl_passwd_cb_called)
+ 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",
}
/*
- * Passphrase collection callback
+ * Passphrase collection callback using ssl_passphrase_command
+ */
+static int
+ssl_external_passwd_cb(char *buf, int size, int rwflag, void *userdata)
+{
+ /* same prompt as OpenSSL uses internally */
+ const char *prompt = "Enter PEM pass phrase:";
+
+ Assert(rwflag == 0);
+
+ return run_ssl_passphrase_command(prompt, ssl_is_server_start, buf, size);
+}
+
+/*
+ * Dummy passphrase callback
*
* If OpenSSL is told to use a passphrase-protected server key, by default
* it will issue a prompt on /dev/tty and try to read a key from there.
* function that just returns an empty passphrase, guaranteeing failure.
*/
static int
-ssl_passwd_cb(char *buf, int size, int rwflag, void *userdata)
+dummy_ssl_passwd_cb(char *buf, int size, int rwflag, void *userdata)
{
/* Set flag to change the error message we'll report */
- ssl_passwd_cb_called = true;
+ dummy_ssl_passwd_cb_called = true;
/* And return empty string */
Assert(size > 0);
buf[0] = '\0';
char *ssl_ca_file;
char *ssl_crl_file;
char *ssl_dh_params_file;
+char *ssl_passphrase_command;
+bool ssl_passphrase_command_supports_reload;
#ifdef USE_SSL
bool ssl_loaded_verify_locations = false;
false,
check_ssl, NULL, NULL
},
+ {
+ {"ssl_passphrase_command_supports_reload", PGC_SIGHUP, CONN_AUTH_SSL,
+ gettext_noop("Also use ssl_passphrase_command during server reload."),
+ NULL
+ },
+ &ssl_passphrase_command_supports_reload,
+ false,
+ NULL, NULL, NULL
+ },
{
{"ssl_prefer_server_ciphers", PGC_SIGHUP, CONN_AUTH_SSL,
gettext_noop("Give priority to server ciphersuite order."),
NULL, NULL, NULL
},
+ {
+ {"ssl_passphrase_command", PGC_SIGHUP, CONN_AUTH_SSL,
+ gettext_noop("Command to obtain passphrases for SSL."),
+ NULL
+ },
+ &ssl_passphrase_command,
+ "",
+ NULL, NULL, NULL
+ },
+
{
{"application_name", PGC_USERSET, LOGGING_WHAT,
gettext_noop("Sets the application name to be reported in statistics and logs."),
#ssl_prefer_server_ciphers = on
#ssl_ecdh_curve = 'prime256v1'
#ssl_dh_params_file = ''
+#ssl_passphrase_command = ''
+#ssl_passphrase_command_supports_reload = off
#------------------------------------------------------------------------------
extern char *ssl_ca_file;
extern char *ssl_crl_file;
extern char *ssl_dh_params_file;
+extern char *ssl_passphrase_command;
+extern bool ssl_passphrase_command_supports_reload;
extern int secure_initialize(bool isServerStart);
extern bool secure_loaded_verify_locations(void);
extern char *SSLECDHCurve;
extern bool SSLPreferServerCiphers;
+/*
+ * 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);
+
#endif /* LIBPQ_H */
root_ca
SSLFILES := $(CERTIFICATES:%=ssl/%.key) $(CERTIFICATES:%=ssl/%.crt) \
+ ssl/server-password.key \
ssl/client.crl ssl/server.crl ssl/root.crl \
ssl/both-cas-1.crt ssl/both-cas-2.crt \
ssl/root+server_ca.crt ssl/root+server.crl \
openssl x509 -req -days 10000 -in ssl/server-ss.csr -signkey ssl/server-cn-only.key -out ssl/server-ss.crt -extensions v3_req -extfile server-cn-only.config
rm ssl/server-ss.csr
+# Password-protected version of server-cn-only.key
+ssl/server-password.key: ssl/server-cn-only.key
+ openssl rsa -des -in $< -out $@ -passout 'pass:secret1'
+
# Client certificate, signed by the client CA:
ssl/client.crt: ssl/client.key ssl/client_ca.crt
openssl req -new -key ssl/client.key -out ssl/client.csr -config client.config
server-ss
same as server-cn-only, but self-signed.
+server-password
+ same as server-cn-only, but password-protected.
+
client
a client certificate, for user "ssltestuser". Signed by client_ca.
--- /dev/null
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: DES-CBC,2FAEFD1C1B2C881C
+
+PGi9r3pm05iUwz5QbZik+ZNu0fHNaX8LJFZqpOhg0TV38csLtQ2PRjZ0Q/diBlVT
+SD8JJnIvwPoIWXyMMTax/krFL0CpbFqgAzD4CEgfWxGNhwnMD1DkNaYp/UF/NfuF
+7TqXomUlcH/pVaZlu7G0wrIo5rnjef70I7GEY2vwT5adSLsUBAgrs/u3MAAx/Wh4
+PkVxZELmyiH/8MdIevodjRcJrgIzRheEph39eHrWKgWeSbO0DEQK91vv3prICwo2
+w2iU0Zohf92QuquA2MKZWruCHb4A4HusUZf3Zc14Yueu/HyztSrHmFeBp0amlWep
+/o6mx274XVj7IpanOPPM4qEhrF97LHdaSEPn9HwxvvV4GFJDNCVEBl4zuaHo0N8C
+85GPazIxUWB3CB9PrtXduxeI22lwrIiUdmzA68EXHD7Wg8R90397MNMOomLgfNcu
+rXarrTXmTNgOa20hc1Ue5AXg9fVS9V/5GP4Dn9SX/CdaE1rz0b73N/ViQzVrS9Ne
+n04qYPbnf+MQmFWnzMXctZbYG6jDCbuGFIGP4i/LG+wOE8Rntu8Re9re+HANu5VJ
+Ht20wYOGZIpNwo4YenxvPeTTlbB0Qcma2lnw2bt19owpNQVIeTnRQXxZs3/Y3a+A
++/B8VvIkQ0u0EpnSVLBetEmJqtOQvBz7c4Z+0Cl+DL1bTqrDn54MxUBap6dgU+/1
+R6pxx1F0ZTtQauVmO8n3rWKwOGG5NeMhf4iId2JWpw39VtRk8LNtnGUbUAbL5znY
+rkUVyJstQg6U6kNTgDWQ1nBxCzlRz2xpHyghnyxLkMpW5ECpmwwLDQ==
+-----END RSA PRIVATE KEY-----
if ($ENV{with_openssl} eq 'yes')
{
- plan tests => 62;
+ plan tests => 64;
}
else
{
copy("ssl/client.key", "ssl/client_wrongperms_tmp.key");
chmod 0644, "ssl/client_wrongperms_tmp.key";
-#### Part 0. Set up the server.
+#### Set up the server.
note "setting up data directory";
my $node = get_new_node('master');
$ENV{PGPORT} = $node->port;
$node->start;
configure_test_server_for_ssl($node, $SERVERHOSTADDR, 'trust');
-switch_server_cert($node, 'server-cn-only');
-### Part 1. Run client-side tests.
+note "testing password-protected keys";
+
+open my $sslconf, '>', $node->data_dir."/sslconfig.conf";
+print $sslconf "ssl=on\n";
+print $sslconf "ssl_cert_file='server-cn-only.crt'\n";
+print $sslconf "ssl_key_file='server-password.key'\n";
+print $sslconf "ssl_passphrase_command='echo wrongpassword'\n";
+close $sslconf;
+
+command_fails(['pg_ctl', '-D', $node->data_dir, '-l', $node->logfile, 'restart'],
+ 'restart fails with password-protected key file with wrong password');
+$node->_update_pid(0);
+
+open $sslconf, '>', $node->data_dir."/sslconfig.conf";
+print $sslconf "ssl=on\n";
+print $sslconf "ssl_cert_file='server-cn-only.crt'\n";
+print $sslconf "ssl_key_file='server-password.key'\n";
+print $sslconf "ssl_passphrase_command='echo secret1'\n";
+close $sslconf;
+
+command_ok(['pg_ctl', '-D', $node->data_dir, '-l', $node->logfile, 'restart'],
+ 'restart succeeds with password-protected key file');
+$node->_update_pid(1);
+
+### Run client-side tests.
###
### Test that libpq accepts/rejects the connection correctly, depending
### on sslmode and whether the server's certificate looks correct. No
note "running client tests";
+switch_server_cert($node, 'server-cn-only');
+
$common_connstr =
"user=ssltestuser dbname=trustdb sslcert=invalid hostaddr=$SERVERHOSTADDR host=common-name.pg-ssltest.test";
qr/SSL error/,
"does not connect with client-side CRL");
-### Part 2. Server-side tests.
+### Server-side tests.
###
### Test certificate authorization.
# if building without OpenSSL
if (!$solution->{options}->{openssl})
{
+ $postgres->RemoveFile('src/backend/libpq/be-secure-common.c');
$postgres->RemoveFile('src/backend/libpq/be-secure-openssl.c');
}