diff --git a/doc/src/sgml/ssl.sgml b/doc/src/sgml/ssl.sgml index c7a2693..16082af 100644 --- a/doc/src/sgml/ssl.sgml +++ b/doc/src/sgml/ssl.sgml @@ -220,6 +220,36 @@ + + 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 SSL file will not be loaded if passphrase is required. + + + 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 at server start. + + + + diff --git a/src/config/pool_config_variables.c b/src/config/pool_config_variables.c index 1c1736e..1196b91 100644 --- a/src/config/pool_config_variables.c +++ b/src/config/pool_config_variables.c @@ -1100,6 +1100,17 @@ static struct config_string ConfigureNamesString[] = }, { + {"ssl_passphrase_command", CFGCXT_INIT, SSL_CONFIG, + "Path to the Diffie-Hellman parameters contained file", + CONFIG_VAR_TYPE_STRING, false, 0 + }, + &g_pool_config.ssl_passphrase_command, + "", + NULL, NULL, NULL, NULL + }, + + + { {"memqcache_oiddir", CFGCXT_INIT, CACHE_CONFIG, "Tempory directory to record table oids.", CONFIG_VAR_TYPE_STRING, false, 0 diff --git a/src/include/pool_config.h b/src/include/pool_config.h index ec1bbf7..287f81f 100644 --- a/src/include/pool_config.h +++ b/src/include/pool_config.h @@ -366,6 +366,7 @@ typedef struct bool ssl_prefer_server_ciphers; /*Use SSL cipher preferences, rather than the client's*/ char *ssl_ecdh_curve; /* the curve to use in ECDH key exchange */ char *ssl_dh_params_file; /* path to the Diffie-Hellman parameters contained file */ + char *ssl_passphrase_command; /* path to the Diffie-Hellman parameters contained file */ int64 relcache_expire; /* relation cache life time in seconds */ int relcache_size; /* number of relation cache life entry */ CHECK_TEMP_TABLE_OPTION check_temp_table; /* how to check temporary table */ diff --git a/src/sample/pgpool.conf.sample-logical b/src/sample/pgpool.conf.sample-logical index 44060d8..7480389 100644 --- a/src/sample/pgpool.conf.sample-logical +++ b/src/sample/pgpool.conf.sample-logical @@ -141,6 +141,10 @@ ssl_ecdh_curve = 'prime256v1' ssl_dh_params_file = '' # Name of the file containing Diffie-Hellman parameters used # for so-called ephemeral DH family of SSL cipher. +#ssl_passphrase_command='' + # Sets an external command to be invoked when a passphrase + # for decrypting an SSL file needs to be obtained + # (change requires restart) #------------------------------------------------------------------------------ # POOLS diff --git a/src/sample/pgpool.conf.sample-raw b/src/sample/pgpool.conf.sample-raw index 3c7845d..90612f8 100644 --- a/src/sample/pgpool.conf.sample-raw +++ b/src/sample/pgpool.conf.sample-raw @@ -142,6 +142,10 @@ ssl_ecdh_curve = 'prime256v1' ssl_dh_params_file = '' # Name of the file containing Diffie-Hellman parameters used # for so-called ephemeral DH family of SSL cipher. +#ssl_passphrase_command='' + # Sets an external command to be invoked when a passphrase + # for decrypting an SSL file needs to be obtained + # (change requires restart) #------------------------------------------------------------------------------ # POOLS diff --git a/src/sample/pgpool.conf.sample-replication b/src/sample/pgpool.conf.sample-replication index ce5e4da..a670829 100644 --- a/src/sample/pgpool.conf.sample-replication +++ b/src/sample/pgpool.conf.sample-replication @@ -137,6 +137,10 @@ ssl_ecdh_curve = 'prime256v1' ssl_dh_params_file = '' # Name of the file containing Diffie-Hellman parameters used # for so-called ephemeral DH family of SSL cipher. +#ssl_passphrase_command='' + # Sets an external command to be invoked when a passphrase + # for decrypting an SSL file needs to be obtained + # (change requires restart) #------------------------------------------------------------------------------ # POOLS diff --git a/src/sample/pgpool.conf.sample-slony b/src/sample/pgpool.conf.sample-slony index f34b40f..836d4f6 100644 --- a/src/sample/pgpool.conf.sample-slony +++ b/src/sample/pgpool.conf.sample-slony @@ -138,6 +138,10 @@ ssl_ecdh_curve = 'prime256v1' ssl_dh_params_file = '' # Name of the file containing Diffie-Hellman parameters used # for so-called ephemeral DH family of SSL cipher. +#ssl_passphrase_command='' + # Sets an external command to be invoked when a passphrase + # for decrypting an SSL file needs to be obtained + # (change requires restart) #------------------------------------------------------------------------------ # POOLS diff --git a/src/sample/pgpool.conf.sample-stream b/src/sample/pgpool.conf.sample-stream index 8474240..469c72b 100644 --- a/src/sample/pgpool.conf.sample-stream +++ b/src/sample/pgpool.conf.sample-stream @@ -142,6 +142,10 @@ ssl_ecdh_curve = 'prime256v1' ssl_dh_params_file = '' # Name of the file containing Diffie-Hellman parameters used # for so-called ephemeral DH family of SSL cipher. +#ssl_passphrase_command='' + # Sets an external command to be invoked when a passphrase + # for decrypting an SSL file needs to be obtained + # (change requires restart) #------------------------------------------------------------------------------ # POOLS diff --git a/src/test/regression/tests/072.cert_passphrase/cert.sh b/src/test/regression/tests/072.cert_passphrase/cert.sh new file mode 100755 index 0000000..2c6c310 --- /dev/null +++ b/src/test/regression/tests/072.cert_passphrase/cert.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +# Create root cert +openssl req -new -x509 -nodes -out root.crt -keyout root.key -days 365 -subj /CN=MyRootCA +# PostgreSQL/Pgpool cert with password +openssl genrsa -aes256 -out server.key -passout pass:pgpoolsecret 2048 +openssl req -new -out server.req -key server.key -subj "/CN=postgresql" -passin pass:pgpoolsecret +openssl x509 -req -in server.req -CAkey root.key -CA root.crt -days 365 -CAcreateserial -out server.crt +# Frontend Cert +openssl req -new -out postgresql.req -keyout frontend.key -nodes -subj "/CN=$USER" +openssl x509 -req -in postgresql.req -CAkey root.key -CA root.crt -days 365 -CAcreateserial -out frontend.crt diff --git a/src/test/regression/tests/072.cert_passphrase/test.sh b/src/test/regression/tests/072.cert_passphrase/test.sh new file mode 100755 index 0000000..236a980 --- /dev/null +++ b/src/test/regression/tests/072.cert_passphrase/test.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +#------------------------------------------------------------------- +# test script for cert authentication for: frontend <--> Pgpool-II. +# +source $TESTLIBS +TESTDIR=testdir +PSQL=$PGBIN/psql +PG_CTL=$PGBIN/pg_ctl +export PGDATABASE=test + +# Generate certifications +./cert.sh + +dir=`pwd` +SSL_KEY=$dir/server.key +SSL_CRT=$dir/server.crt +ROOT_CRT=$dir/root.crt +FRONTEND_KEY=$dir/frontend.key +FRONTEND_CRT=$dir/frontend.crt +chmod 600 *.key +CERTPW=pgpoolsecret + +rm -fr $TESTDIR +mkdir $TESTDIR +cd $TESTDIR + +# create test environment. Number of backend node is 1 is enough. +echo -n "creating test environment..." +$PGPOOL_SETUP -m s -n 1 || exit 1 +echo "done." + +dir=`pwd` + +echo "ssl = on" >> etc/pgpool.conf +echo "ssl_key = '$SSL_KEY'" >> etc/pgpool.conf +echo "ssl_cert = '$SSL_CRT'" >> etc/pgpool.conf +echo "ssl_ca_cert = '$ROOT_CRT'" >> etc/pgpool.conf +echo "enable_pool_hba = on" >> etc/pgpool.conf + +# allow to access IPv6 localhost +echo "hostssl all all 127.0.0.1/32 cert" >> etc/pool_hba.conf +echo "hostssl all all ::1/128 cert" >> etc/pool_hba.conf + +sed -i "/^host.*trust$/d" etc/pool_hba.conf + +source ./bashrc.ports + +# +# Without Pass Phrase entry +# + +./startall + +export PGPORT=$PGPOOL_PORT + +wait_for_pgpool_startup + +export PGSSLCERT=$FRONTEND_CRT +export PGSSLKEY=$FRONTEND_KEY + +$PSQL -h localhost -c "select 1" test + +grep "cannot be reloaded because it requires a passphrase" log/pgpool.log +if [ $? != 0 ];then + echo "Checking cert without password failed." + ./shutdownall + exit 1 +fi +echo "Checking cert without password was ok." + +./shutdownall + +# +# Valid Pass Phrase +# +echo "ssl_passphrase_command = 'echo \"$CERTPW\"'" >> etc/pgpool.conf +grep "ssl_passphrase_command" etc/pgpool.conf + +./startall +wait_for_pgpool_startup + +$PSQL -h localhost -c "select 1" test + +grep "SSL certificate authentication for user" log/pgpool.log +if [ $? != 0 ];then + echo "Checking cert with valid password failed." + ./shutdownall + exit 1 +fi +echo "Checking cert with valid password was ok." + +./shutdownall + +# +# Invalid Pass Phrase +# +sed -i 's/ssl_passphrase_command/#ssl_passphrase_command/' etc/pgpool.conf +echo "ssl_passphrase_command = 'echo \"incorrectpw\"'" >> etc/pgpool.conf +grep "ssl_passphrase_command" etc/pgpool.conf + +./startall +wait_for_pgpool_startup + +$PSQL -h localhost -c "select 1" test + +grep "could not load private key file" log/pgpool.log|grep "bad decrypt" +if [ $? != 0 ];then + echo "Checking cert with invalid password failed." + ./shutdownall + exit 1 +fi +echo "Checking cert with invalid password was ok." + +./shutdownall + +exit 0 diff --git a/src/utils/pool_process_reporting.c b/src/utils/pool_process_reporting.c index 25728b0..652f20d 100644 --- a/src/utils/pool_process_reporting.c +++ b/src/utils/pool_process_reporting.c @@ -279,6 +279,11 @@ get_config(int *nrows) StrNCpy(status[i].desc, "path to the Diffie-Hellman parameters contained file", POOLCONFIG_MAXDESCLEN); i++; + StrNCpy(status[i].name, "ssl_passphrase_command", POOLCONFIG_MAXNAMELEN); + snprintf(status[i].value, POOLCONFIG_MAXVALLEN, "%s", pool_config->ssl_passphrase_command); + StrNCpy(status[i].desc, "external command to be invoked when a passphrase for decrypting an SSL file such as a private key needs to be obtained", POOLCONFIG_MAXDESCLEN); + i++; + /* POOLS */ /* - Pool size - */ diff --git a/src/utils/pool_ssl.c b/src/utils/pool_ssl.c index 346477e..945d626 100644 --- a/src/utils/pool_ssl.c +++ b/src/utils/pool_ssl.c @@ -40,8 +40,9 @@ static SSL_CTX *SSL_frontend_context = NULL; static bool SSL_initialized = false; -static bool ssl_passwd_cb_called = false; -static int ssl_passwd_cb(char *buf, int size, int rwflag, void *userdata); +static bool dummy_ssl_passwd_cb_called = false; +static int dummy_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 verify_cb(int ok, X509_STORE_CTX *ctx); static const char *SSLerrmessage(unsigned long ecode); static void fetch_pool_ssl_cert(POOL_CONNECTION * cp); @@ -49,6 +50,7 @@ static DH *load_dh_file(char *filename); static DH *load_dh_buffer(const char *, size_t); static bool initialize_dh(SSL_CTX *context); static bool initialize_ecdh(SSL_CTX *context); +static int run_ssl_passphrase_command(const char *prompt, char *buf, int size); #define SSL_RETURN_VOID_IF(cond, msg) \ do { \ @@ -474,10 +476,10 @@ fetch_pool_ssl_cert(POOL_CONNECTION * cp) * 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'; @@ -485,6 +487,20 @@ ssl_passwd_cb(char *buf, int size, int rwflag, void *userdata) } /* + * 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, buf, size); +} + +/* * Certificate verification callback * * This callback allows us to log intermediate problems during @@ -557,7 +573,10 @@ SSL_ServerSide_init(void) /* * prompt for password for passphrase-protected files */ - SSL_CTX_set_default_passwd_cb(context, ssl_passwd_cb); + if(pool_config->ssl_passphrase_command && strlen(pool_config->ssl_passphrase_command)) + SSL_CTX_set_default_passwd_cb(context, ssl_external_passwd_cb); + else + SSL_CTX_set_default_passwd_cb(context, dummy_ssl_passwd_cb); /* * Load and verify server's certificate and private key @@ -611,13 +630,13 @@ SSL_ServerSide_init(void) /* * 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, pool_config->ssl_key, SSL_FILETYPE_PEM) != 1) { - if (ssl_passwd_cb_called) + if (dummy_ssl_passwd_cb_called) ereport(WARNING, (errmsg("private key file \"%s\" cannot be reloaded because it requires a passphrase", pool_config->ssl_key))); @@ -865,6 +884,95 @@ load_dh_buffer(const char *buffer, size_t len) return dh; } +/* + * Run ssl_passphrase_command + * + * 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, char *buf, int size) +{ + int loglevel = ERROR; + 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 = pool_config->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 = popen(command.data, "r"); + if (fh == NULL) + { + ereport(loglevel, + (errmsg("could not execute command \"%s\": %m", + command.data))); + goto error; + } + + if (!fgets(buf, size, fh)) + { + if (ferror(fh)) + { + ereport(loglevel, + (errmsg("could not read from command \"%s\": %m", + command.data))); + goto error; + } + } + + pclose_rc = pclose(fh); + if (pclose_rc == -1) + { + ereport(loglevel, + (errmsg("could not close pipe to external command: %m"))); + goto error; + } + else if (pclose_rc != 0) + { + ereport(loglevel, + (errmsg("command \"%s\" failed", + command.data))); + goto error; + } + + /* strip trailing newline */ + len = strlen(buf); + if (len > 0 && buf[len - 1] == '\n') + buf[--len] = '\0'; + +error: + pfree(command.data); + return len; +} + #else /* USE_SSL: wrap / no-op ssl functionality if * it's not available */