diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 3a8fc7d803c..f18d2b3353a 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -1313,6 +1313,66 @@ include_dir 'conf.d' + + + ssl_passphrase_command (string) + + ssl_passphrase_command configuration parameter + + + + + 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. + + + The command must print the passphrase to the standard output and exit + with code 0. In the parameter value, %p is + replaced by a prompt string. (Write %% for a + 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. + + + 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. + + + This parameter can only be set in the postgresql.conf + file or on the server command line. + + + + + + ssl_passphrase_command_supports_reload (boolean) + + ssl_passphrase_command_supports_reload configuration parameter + + + + + This setting determines whether the passphrase command set by + ssl_passphrase_command will also be called during a + configuration reload if a key file needs a passphrase. If this + setting is false (the default), then + ssl_passphrase_command 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. + + + This parameter can only be set in the postgresql.conf + file or on the server command line. + + + diff --git a/src/backend/libpq/Makefile b/src/backend/libpq/Makefile index 7fa2b027433..3dbec23e30a 100644 --- a/src/backend/libpq/Makefile +++ b/src/backend/libpq/Makefile @@ -14,7 +14,7 @@ include $(top_builddir)/src/Makefile.global # 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) diff --git a/src/backend/libpq/be-secure-common.c b/src/backend/libpq/be-secure-common.c new file mode 100644 index 00000000000..d1740967f19 --- /dev/null +++ b/src/backend/libpq/be-secure-common.c @@ -0,0 +1,120 @@ +/*------------------------------------------------------------------------- + * + * 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; +} diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c index 567cf7d4550..75ee5456cc1 100644 --- a/src/backend/libpq/be-secure-openssl.c +++ b/src/backend/libpq/be-secure-openssl.c @@ -52,7 +52,8 @@ static int my_SSL_set_fd(Port *port, int fd); 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); @@ -63,7 +64,8 @@ static char *X509_NAME_to_cstring(X509_NAME *name); 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; /* ------------------------------------------------------------ */ @@ -111,14 +113,28 @@ be_tls_init(bool isServerStart) 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 @@ -138,13 +154,13 @@ be_tls_init(bool isServerStart) /* * 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", @@ -839,7 +855,21 @@ load_dh_buffer(const char *buffer, size_t len) } /* - * 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. @@ -848,10 +878,10 @@ load_dh_buffer(const char *buffer, size_t len) * 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'; diff --git a/src/backend/libpq/be-secure.c b/src/backend/libpq/be-secure.c index 76c0a9e39b5..fb1f6b5bbe7 100644 --- a/src/backend/libpq/be-secure.c +++ b/src/backend/libpq/be-secure.c @@ -45,6 +45,8 @@ char *ssl_key_file; 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; diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c index 4685cef5122..7a7ac479c14 100644 --- a/src/backend/utils/misc/guc.c +++ b/src/backend/utils/misc/guc.c @@ -988,6 +988,15 @@ static struct config_bool ConfigureNamesBool[] = 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."), @@ -3655,6 +3664,16 @@ static struct config_string ConfigureNamesString[] = 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."), diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample index 39272925fb7..048bf4cccd7 100644 --- a/src/backend/utils/misc/postgresql.conf.sample +++ b/src/backend/utils/misc/postgresql.conf.sample @@ -104,6 +104,8 @@ #ssl_prefer_server_ciphers = on #ssl_ecdh_curve = 'prime256v1' #ssl_dh_params_file = '' +#ssl_passphrase_command = '' +#ssl_passphrase_command_supports_reload = off #------------------------------------------------------------------------------ diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h index 255222acd7a..997947b0917 100644 --- a/src/include/libpq/libpq.h +++ b/src/include/libpq/libpq.h @@ -80,6 +80,8 @@ extern char *ssl_key_file; 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); @@ -101,4 +103,10 @@ extern char *SSLCipherSuites; 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 */ diff --git a/src/test/ssl/Makefile b/src/test/ssl/Makefile index 5cd2c5a404e..df477f1d401 100644 --- a/src/test/ssl/Makefile +++ b/src/test/ssl/Makefile @@ -22,6 +22,7 @@ CERTIFICATES := server_ca server-cn-and-alt-names \ 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 \ @@ -71,6 +72,10 @@ ssl/server-ss.crt: ssl/server-cn-only.key ssl/server-cn-only.crt server-cn-only. 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 diff --git a/src/test/ssl/README b/src/test/ssl/README index 0be06e755cf..5e8bf641ba4 100644 --- a/src/test/ssl/README +++ b/src/test/ssl/README @@ -48,6 +48,9 @@ server-no-names 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. diff --git a/src/test/ssl/ssl/server-password.key b/src/test/ssl/ssl/server-password.key new file mode 100644 index 00000000000..adcd38ab882 --- /dev/null +++ b/src/test/ssl/ssl/server-password.key @@ -0,0 +1,18 @@ +-----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----- diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl index 34df5e9dbb7..91feac613eb 100644 --- a/src/test/ssl/t/001_ssltests.pl +++ b/src/test/ssl/t/001_ssltests.pl @@ -8,7 +8,7 @@ use File::Copy; if ($ENV{with_openssl} eq 'yes') { - plan tests => 62; + plan tests => 64; } else { @@ -38,7 +38,7 @@ chmod 0600, "ssl/client-revoked_tmp.key"; 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'); @@ -50,9 +50,32 @@ $ENV{PGHOST} = $node->host; $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 @@ -60,6 +83,8 @@ switch_server_cert($node, 'server-cn-only'); 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"; @@ -235,7 +260,7 @@ test_connect_fails($common_connstr, qr/SSL error/, "does not connect with client-side CRL"); -### Part 2. Server-side tests. +### Server-side tests. ### ### Test certificate authorization. diff --git a/src/tools/msvc/Mkvcbuild.pm b/src/tools/msvc/Mkvcbuild.pm index 72976f44d8e..ef315d88f94 100644 --- a/src/tools/msvc/Mkvcbuild.pm +++ b/src/tools/msvc/Mkvcbuild.pm @@ -182,6 +182,7 @@ sub mkvcbuild # 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'); }