This time with attachment.
On 10/31/19 6:33 PM, Andrew Dunstan wrote: > This patch provides for an sslpassword parameter for libpq, and a hook > that a client can fill in for a callback function to set the password. > > > This provides similar facilities to those already available in the JDBC > driver. > > > There is also a function to fetch the sslpassword from the connection > parameters, in the same way that other settings can be fetched. > > > This is mostly the excellent work of my colleague Craig Ringer, with a > few embellishments from me. > > > Here are his notes: > > > Allow libpq to non-interactively decrypt client certificates that > are stored > encrypted by adding a new "sslpassword" connection option. > > The sslpassword option offers a middle ground between a cleartext > key and > setting up advanced key mangement via openssl engines, PKCS#11, USB > crypto > offload and key escrow, etc. > > Previously use of encrypted client certificate keys only worked if > the user > could enter the key's password interactively on stdin, in response > to openssl's > default prompt callback: > > Enter PEM passhprase: > > That's infesible in many situations, especially things like use from > postgres_fdw. > > This change also allows admins to prevent libpq from ever prompting > for a > password by calling: > > PQsetSSLKeyPassHook(PQdefaultSSLKeyPassHook); > > which is useful since OpenSSL likes to open /dev/tty to prompt for a > password, > so even closing stdin won't stop it blocking if there's no user > input available. > Applications may also override or extend SSL password fetching with > their own > callback. > > There is deliberately no environment variable equivalent for the > sslpassword > option. > > > cheers > > > andrew > > -- Andrew Dunstan https://www.2ndQuadrant.com PostgreSQL Development, 24x7 Support, Remote DBA, Training & Services
diff --git a/contrib/dblink/expected/dblink.out b/contrib/dblink/expected/dblink.out index 6ceabb453c..6516d4f131 100644 --- a/contrib/dblink/expected/dblink.out +++ b/contrib/dblink/expected/dblink.out @@ -879,7 +879,7 @@ $d$; CREATE USER MAPPING FOR public SERVER fdtest OPTIONS (server 'localhost'); -- fail, can't specify server here ERROR: invalid option "server" -HINT: Valid options in this context are: user, password +HINT: Valid options in this context are: user, password, sslpassword CREATE USER MAPPING FOR public SERVER fdtest OPTIONS (user :'USER'); GRANT USAGE ON FOREIGN SERVER fdtest TO regress_dblink_user; GRANT EXECUTE ON FUNCTION dblink_connect_u(text, text) TO regress_dblink_user; diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml index c58527b0c3..a606680182 100644 --- a/doc/src/sgml/libpq.sgml +++ b/doc/src/sgml/libpq.sgml @@ -776,6 +776,72 @@ PGPing PQping(const char *conninfo); </listitem> </varlistentry> + <varlistentry id="libpq-pqsetsslkeypasshook"> + <term><function>PQsetSSLKeyPassHook</function><indexterm><primary>PQsetSSLKeyPassHook</primary></indexterm></term> + <listitem> + <para> + <function>PQsetSSLKeyPassHook</function> lets an application override + <literal>libpq</literal>'s <link linkend="libpq-ssl-clientcert">default + handling of encrypted client certificate key files</link> using + <xref linkend="libpq-connect-sslpassword"/> or interactive prompting. + +<synopsis> +void PQsetSSLKeyPassHook(PQsslKeyPassHook_type hook); +</synopsis> + + The application passes a pointer to a callback function with signature: + <programlisting> + int callback_fn(char *buf, int size, PGconn *conn); + </programlisting> + which <literal>libpq</literal> will then call <emphasis>instead of</emphasis> + its default <function>PQdefaultSSLKeyPassHook</function> handler. The callback + should determine the password for the key and copy it to result-buffer + <literal>buf</literal> of size <literal>size</literal>. The string in <literal> + buf</literal> must be null-terminated. The calback must return the length of + the password stored in <literal>buf</literal> excluding the null terminator. + On failure, the callback should set <literal>buf[0] = '\0'</literal> and return 0. + See <function>PQdefaultSSLKeyPassHook</function> in <literal>libpq</literal>'s + source code for an example. + </para> + + <para> + If the user specified an explicit key location, + its path will be in <literal>conn->pgsslkey</literal> when the callback + is invoked. This will be empty if the default key path is being used. + For keys that are engine specifiers, it is up to engine implementations + whether they use the OpenSSL password callback or define their own handling. + </para> + + <para> + The app callback may choose to delegate unhandled cases to + <function>PQdefaultSSLKeyPassHook</function>, + or call it first and try something else if it returns 0, or completely override it. + </para> + + <para> + The callback <emphasis>must not</emphasis> escape normal flow control with exceptions, + <function>longjmp(...)</function>, etc. It must return normally. + </para> + + </listitem> + </varlistentry> + + <varlistentry id="libpq-pqgetsslkeypasshook"> + <term><function>PQgetSSLKeyPassHook</function><indexterm><primary>PQgetSSLKeyPassHook</primary></indexterm></term> + <listitem> + <para> + <function>PQgetSSLKeyPassHook</function> returns the current + client certificate key password hook, or <literal>NULL</literal> + if none has been set. + +<synopsis> +PQsslKeyPassHook_type PQgetSSLKeyPassHook(void); +</synopsis> + </para> + + </listitem> + </varlistentry> + </variablelist> </para> @@ -1586,6 +1652,36 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname </listitem> </varlistentry> + <varlistentry id="libpq-connect-sslpassword" xreflabel="sslpassword"> + <term><literal>sslpassword</literal></term> + <listitem> + <para> + This parameter specifies the password for the secret key specified in + <literal>sslkey</literal>, allowing client certificate private keys + to be stored in encrypted form on disk even when interactive passphrase + input is not practical. + </para> + <para> + Specifying this parameter with any non-empty value suppresses the + <literal>Enter PEM passphrase:</literal> + prompt that OpenSSL will emit by default when an encrypted client + certificate key is provided to <literal>libpq</literal>. + </para> + <para> + If the key is not encrypted this parameter is ignored. The parameter has no + effect on keys specified by OpenSSL engines unless the engine uses the + OpenSSL password callback mechanism for prompts. + </para> + <para> + There is no environment variable equivalent to this option, and no + facility for looking it up in <filename>.pgpass</filename>. It can be + used in a service file connection definition. Users with + more sophisticated uses should consider using openssl engines and + tools like PKCS#11 or USB crypto offload devices. + </para> + </listitem> + </varlistentry> + <varlistentry id="libpq-connect-sslrootcert" xreflabel="sslrootcert"> <term><literal>sslrootcert</literal></term> <listitem> @@ -7499,6 +7595,26 @@ ldap://ldap.acme.com/cn=dbserver,cn=hosts?pgconnectinfo?base?(objectclass=*) certificates on the server (<xref linkend="guc-ssl-ca-file"/>). </para> + <para> + The certificate and key may be in PEM or ASN.1 DER format. + </para> + + <para> + The key may be + stored in cleartext or encrypted with a passphrase using any algorithm supported + by OpenSSL, like AES-128. If the key is stored encrypted, then the passphrase + may be provided in the <xref linkend="libpq-connect-sslpassword"/> connection + option. If an encrypted key is supplied and the <literal>sslpassword</literal> + option is absent or blank, a password will be prompted for interactively by + OpenSSL with a + <programlisting> + Enter PEM Passphrase: + </programlisting> + prompt if a TTY is available. Applications can override the client certificate + prompt and the handling of the <literal>sslpassword</literal> parameter by supplying + their own key password callback; see <xref linkend="libpq-pqsetsslkeypasshook"/>. + </para> + <para> For instructions on creating certificates, see <xref linkend="ssl-certificate-creation"/>. diff --git a/doc/src/sgml/postgres-fdw.sgml b/doc/src/sgml/postgres-fdw.sgml index fb4ceb355a..bdd6ff8ae4 100644 --- a/doc/src/sgml/postgres-fdw.sgml +++ b/doc/src/sgml/postgres-fdw.sgml @@ -112,7 +112,7 @@ <itemizedlist spacing="compact"> <listitem> <para> - <literal>user</literal> and <literal>password</literal> (specify these + <literal>user</literal>, <literal>password</literal> and <literal>sslpassword</literal> (specify these in a user mapping, instead) </para> </listitem> diff --git a/src/interfaces/libpq/exports.txt b/src/interfaces/libpq/exports.txt index ccec59919b..53f8ee0fc4 100644 --- a/src/interfaces/libpq/exports.txt +++ b/src/interfaces/libpq/exports.txt @@ -176,3 +176,7 @@ PQresultMemorySize 173 PQhostaddr 174 PQgssEncInUse 175 PQgetgssctx 176 +PQsslpassword 177 +PQsetSSLKeyPassHook 178 +PQgetSSLKeyPassHook 179 +PQdefaultSSLKeyPassHook 180 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 99051231a6..e6c9cfda01 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -353,6 +353,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = { "Target-Session-Attrs", "", 11, /* sizeof("read-write") = 11 */ offsetof(struct pg_conn, target_session_attrs)}, + {"sslpassword", NULL, NULL, NULL, + "SSL-Client-Key-Password", "*", 20, + offsetof(struct pg_conn, sslpassword)}, + /* Terminating entry --- MUST BE LAST */ {NULL, NULL, NULL, NULL, NULL, NULL, 0} @@ -4028,6 +4032,8 @@ freePGconn(PGconn *conn) free(conn->target_session_attrs); termPQExpBuffer(&conn->errorMessage); termPQExpBuffer(&conn->workBuffer); + if (conn->sslpassword) + free(conn->sslpassword); free(conn); @@ -6546,6 +6552,14 @@ PQport(const PGconn *conn) return ""; } +char * +PQsslpassword(const PGconn *conn) +{ + if (!conn) + return NULL; + return conn->sslpassword; +} + char * PQtty(const PGconn *conn) { diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c index c8dddfb5fd..cba81f63c0 100644 --- a/src/interfaces/libpq/fe-secure-openssl.c +++ b/src/interfaces/libpq/fe-secure-openssl.c @@ -70,6 +70,7 @@ static int initialize_SSL(PGconn *conn); static PostgresPollingStatusType open_client_SSL(PGconn *); static char *SSLerrmessage(unsigned long ecode); static void SSLerrfree(char *buf); +static int PQssl_passwd_cb(char *buf, int size, int rwflag, void *userdata); static int my_sock_read(BIO *h, char *buf, int size); static int my_sock_write(BIO *h, const char *buf, int size); @@ -93,6 +94,7 @@ static long win32_ssl_create_mutex = 0; #endif #endif /* ENABLE_THREAD_SAFETY */ +static PQsslKeyPassHook_type PQsslKeyPassHook = NULL; /* ------------------------------------------------------------ */ /* Procedures common to all secure sessions */ @@ -818,6 +820,26 @@ initialize_SSL(PGconn *conn) return -1; } + /* + * Delegate the client cert password prompt to the libpq wrapper + * callback if any is defined. + * + * If the application hasn't installed its own and the sslpassword + * parameter is non-null, we install ours now to make sure we + * supply PGconn->sslpassword to OpenSSL instead of letting it + * prompt on stdin. + * + * This will replace OpenSSL's default PEM_def_callback (which + * prompts on stdin), but we're only setting it for this SSL + * context so it's harmless. + */ + if (PQsslKeyPassHook + || (conn->sslpassword && strlen(conn->sslpassword) > 0)) + { + SSL_CTX_set_default_passwd_cb(SSL_context, PQssl_passwd_cb); + SSL_CTX_set_default_passwd_cb_userdata(SSL_context, conn); + } + /* Disable old protocol versions */ SSL_CTX_set_options(SSL_context, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3); @@ -1123,11 +1145,29 @@ initialize_SSL(PGconn *conn) { char *err = SSLerrmessage(ERR_get_error()); - printfPQExpBuffer(&conn->errorMessage, - libpq_gettext("could not load private key file \"%s\": %s\n"), - fnbuf, err); + /* + * We'll try to load the file in DER (binary ASN.1) format, and if + * that fails too, report the original error. This could mask + * issues where there's something wrong with a DER-format cert, but + * we'd have to duplicate openssl's format detection to be smarter + * than this. We can't just probe for a leading -----BEGIN because + * PEM can have leading non-matching lines and blanks. OpenSSL + * doesn't expose its get_name(...) and its PEM routines don't + * differentiate between failure modes in enough detail to let us + * tell the difference between "not PEM, try DER" and "wrong + * password". + */ + if (SSL_use_PrivateKey_file(conn->ssl, fnbuf, SSL_FILETYPE_ASN1) != 1) + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("could not load private key file \"%s\": %s\n"), + fnbuf, err); + SSLerrfree(err); + return -1; + } + SSLerrfree(err); - return -1; + } } @@ -1580,3 +1620,54 @@ my_SSL_set_fd(PGconn *conn, int fd) err: return ret; } + +/* + * This is the default handler to return a client cert password from + * conn->sslpassword. Apps may install it explicitly if they want to + * prevent openssl from ever prompting on stdin. + */ +int +PQdefaultSSLKeyPassHook(char *buf, int size, PGconn *conn) +{ + if (conn->sslpassword) + { + if (strlen(conn->sslpassword) + 1 > size) + fprintf(stderr, libpq_gettext("WARNING: sslpassword truncated")); + strncpy(buf, conn->sslpassword, size); + buf[size-1] = '\0'; + return strlen(buf); + } + else + { + buf[0] = '\0'; + return 0; + } +} + +PQsslKeyPassHook_type +PQgetSSLKeyPassHook(void) +{ + return PQsslKeyPassHook; +} + +void +PQsetSSLKeyPassHook(PQsslKeyPassHook_type hook) +{ + PQsslKeyPassHook = hook; +} + +/* + * Supply a password to decrypt a client certificate. + * + * This must match OpenSSL type pem_passwd_cb. + */ +static int +PQssl_passwd_cb(char *buf, int size, int rwflag, void *userdata) +{ + PGconn *conn = userdata; + + if (PQsslKeyPassHook) + return PQsslKeyPassHook(buf, size, conn); + else + return PQdefaultSSLKeyPassHook(buf, size, conn); +} diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index 5f65db30e4..4d52fd7994 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -317,6 +317,7 @@ extern char *PQpass(const PGconn *conn); extern char *PQhost(const PGconn *conn); extern char *PQhostaddr(const PGconn *conn); extern char *PQport(const PGconn *conn); +extern char *PQsslpassword(const PGconn *conn); extern char *PQtty(const PGconn *conn); extern char *PQoptions(const PGconn *conn); extern ConnStatusType PQstatus(const PGconn *conn); @@ -617,6 +618,19 @@ extern int pg_char_to_encoding(const char *name); extern const char *pg_encoding_to_char(int encoding); extern int pg_valid_server_encoding_id(int encoding); +/* == in fe-secure-openssl.c === */ + +/* + * Support for overriding sslpassword handling with a callback. + * + * 2ndQPostgres extension. If you need to be compatible with unpatched libpq + * you must dlsym() these. + */ +typedef int (*PQsslKeyPassHook_type)(char *buf, int size, PGconn *conn); +extern PQsslKeyPassHook_type PQgetSSLKeyPassHook(void); +extern void PQsetSSLKeyPassHook(PQsslKeyPassHook_type hook); +extern int PQdefaultSSLKeyPassHook(char *buf, int size, PGconn *conn); + #ifdef __cplusplus } #endif diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 64468ab4da..7f5be7db7a 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -512,6 +512,8 @@ struct pg_conn /* Buffer for receiving various parts of messages */ PQExpBufferData workBuffer; /* expansible string */ + + char *sslpassword; /* client key file password */ }; /* PGcancel stores all data necessary to cancel a connection. A copy of this diff --git a/src/test/ssl/Makefile b/src/test/ssl/Makefile index 3b53972f6f..cea5ace7aa 100644 --- a/src/test/ssl/Makefile +++ b/src/test/ssl/Makefile @@ -27,9 +27,14 @@ SSLFILES := $(CERTIFICATES:%=ssl/%.key) $(CERTIFICATES:%=ssl/%.crt) \ ssl/both-cas-1.crt ssl/both-cas-2.crt \ ssl/root+server_ca.crt ssl/root+server.crl \ ssl/root+client_ca.crt ssl/root+client.crl \ - ssl/client+client_ca.crt + ssl/client+client_ca.crt ssl/client-der.key \ + ssl/client-encrypted-pem.key ssl/client-encrypted-der.key -# This target generates all the key and certificate files. +# This target re-generates all the key and certificate files. Usually we just +# use the ones that are committed to the tree without rebuilding them. +# +# This target will fail unless preceded by sslfiles-clean. +# sslfiles: $(SSLFILES) # OpenSSL requires a directory to put all generated certificates in. We don't @@ -90,6 +95,18 @@ ssl/client-revoked.crt: ssl/client-revoked.key ssl/client_ca.crt client.config openssl x509 -in ssl/temp.crt -out ssl/client-revoked.crt # to keep just the PEM cert rm ssl/client-revoked.csr ssl/temp.crt +# Convert the key to DER, to test our behaviour there too +ssl/client-der.key: ssl/client.key + openssl rsa -in ssl/client.key -outform DER -out ssl/client-der.key + +# Convert the existing key to encrypted PEM (X.509 text) and DER (X.509 ASN.1) formats +# to test libpq's support for the sslpassword= option. +ssl/client-encrypted-pem.key: ssl/client.key + openssl rsa -in ssl/client.key -outform PEM -aes128 -passout 'pass:dUmmyP^#+' -out ssl/client-encrypted-pem.key + +ssl/client-encrypted-der.key: ssl/client.key + openssl rsa -in ssl/client.key -outform DER -aes128 -passout 'pass:dUmmyP^#+' -out ssl/client-encrypted-der.key + # Root certificate files that contains both CA certificates, for testing # that multiple certificates can be used. ssl/both-cas-1.crt: ssl/root_ca.crt ssl/client_ca.crt ssl/server_ca.crt @@ -138,6 +155,7 @@ clean distclean maintainer-clean: rm -rf tmp_check rm -rf ssl/*.old ssl/new_certs_dir ssl/client*_tmp.key +# Doesn't depend on $(SSLFILES) because we don't rebuild them by default check: $(prove_check) diff --git a/src/test/ssl/ssl/client-der.key b/src/test/ssl/ssl/client-der.key new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/test/ssl/ssl/client-encrypted-der.key b/src/test/ssl/ssl/client-encrypted-der.key new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/test/ssl/ssl/client-encrypted-pem.key b/src/test/ssl/ssl/client-encrypted-pem.key new file mode 100644 index 0000000000..8a3c7d6e07 --- /dev/null +++ b/src/test/ssl/ssl/client-encrypted-pem.key @@ -0,0 +1,18 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-128-CBC,DFCCEA1226790120AE185F8A42941296 + +jkCJfxg36xJjsat8dyJaIaMkjAWeuJMC1XCoHjIXFbGOno6wkLH0vb+4UhUh3UgU +hq9owmwrwWXzER7FawI1P0cCtusdXoU/35FJwe9KAY0PaozCOf5iS755y/S6/9Y3 +Jb4KBvPw/3OokYRrzMr3tPgSGY61028HPiHrnZbR3Q/eDzcQ9DlsYsvio6I6iHkC +kQyznEH1gpRQGLHg17LZVS1Jr6Cgw7qBTgUWW++AGSdFk9YD+NdyM0ZFhyyXNoCE +MWRzo9HbSNk3jIqBHVMxSW7AbcFWuEWwAO4ycrXzuHDY6qqwbi/phW2MS5hUKprW +aAbNFdA1aS9FIanxVEqsvrSMyR6qsgFgo7mFvt8jKkLMphRvwi5yImxD8Ml4U4GF +37VSfD2OG3i6GMFfscURy1GgagpOhprJsxI35kh36cI15dPrY7FKw6c3PJ7ZbZ6G +HnsdXlDaozlqXkzd5ZtEeqm7YYzwNw7Ih0uhTq4PNiKGMWs0a+s9jUNgL+y9VmOO +thqCzbgES2UdBVclEri/Ucdb7irT1FuFKXNxIMxgaxziJO0QJHgxYLcKzCDWvuK3 +r5jL9oOmeP98XKyvcgZ48oLiNqk1uk72sPJLo4OvMueC9g56n64g0LE9A7klOdRt +xfI33YBG34fk489NjbAesuJM9dlwELOVk4yzjBdwV8235SF+D38VP9N4v0htNc4T +wGnMU8NyTepfKv04LgUr0CQ7VvLNjInz36YC1ZzWf+pUjtxukA5yOCKXsNQ4vAVe +EPksrbgfzD7ASpwHq3ZHVTQWlbrtwHdjC9RX8GE70YX+dKNpo2Y9RZsRLs1n2Xu3 +-----END RSA PRIVATE KEY----- diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl index 67a3a28db6..ec58d45500 100644 --- a/src/test/ssl/t/001_ssltests.pl +++ b/src/test/ssl/t/001_ssltests.pl @@ -13,7 +13,7 @@ use SSLServer; if ($ENV{with_openssl} eq 'yes') { - plan tests => 75; + plan tests => 85; } else { @@ -32,10 +32,17 @@ my $common_connstr; # The client's private key must not be world-readable, so take a copy # of the key stored in the code tree and update its permissions. -copy("ssl/client.key", "ssl/client_tmp.key"); -chmod 0600, "ssl/client_tmp.key"; -copy("ssl/client-revoked.key", "ssl/client-revoked_tmp.key"); -chmod 0600, "ssl/client-revoked_tmp.key"; +# +# This changes ssl/client.key to ssl/client_tmp.key etc for the rest +# of the tests. +my @keys = ("client", "client-revoked", "client-der", "client-encrypted-pem", "client-encrypted-der"); +foreach my $key (@keys) +{ + copy("ssl/${key}.key", "ssl/${key}_tmp.key") + or die "couldn't copy ssl/${key}.key to ssl/${key}_tmp.key for permissions change: $!"; + chmod 0600, "ssl/${key}_tmp.key" + or die "failed to change permissions on ssl/${key}_tmp.key: $!"; +} # Also make a copy of that explicitly world-readable. We can't # necessarily rely on the file in the source tree having those @@ -344,11 +351,52 @@ test_connect_fails( qr/connection requires a valid client certificate/, "certificate authorization fails without client cert"); -# correct client cert +# correct client cert in unencrypted PEM test_connect_ok( $common_connstr, "user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client_tmp.key", - "certificate authorization succeeds with correct client cert"); + "certificate authorization succeeds with correct client cert in PEM format"); + +# correct client cert in unencrypted DER +test_connect_ok( + $common_connstr, + "user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client-der_tmp.key", + "certificate authorization succeeds with correct client cert in DER format"); + +# correct client cert in encrypted PEM +test_connect_ok( + $common_connstr, + "user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client-encrypted-pem_tmp.key sslpassword='dUmmyP^#+'", + "certificate authorization succeeds with correct client cert in encrypted PEM format"); + +# correct client cert in encrypted DER +test_connect_ok( + $common_connstr, + "user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client-encrypted-der_tmp.key sslpassword='dUmmyP^#+'", + "certificate authorization succeeds with correct client cert in encrypted DER format"); + +# correct client cert in encrypted PEM with wrong password +test_connect_fails( + $common_connstr, + "user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client-encrypted-pem_tmp.key sslpassword='wrong'", + qr!\Qprivate key file "ssl/client-encrypted-pem_tmp.key": bad decrypt\E!, + "certificate authorization fails with correct client cert and wrong password in encrypted PEM format"); + +# correct client cert in encrypted PEM with empty password +test_connect_fails( + $common_connstr, + "user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client-encrypted-pem_tmp.key sslpassword=''", + qr!\Qprivate key file "ssl/client-encrypted-pem_tmp.key": processing error\E!, + "certificate authorization fails with correct client cert and empty password in encrypted PEM format", + close_stdin => 1); + +# correct client cert in encrypted PEM with no password +test_connect_fails( + $common_connstr, + "user=ssltestuser sslcert=ssl/client.crt sslkey=ssl/client-encrypted-pem_tmp.key", + qr!\Qprivate key file "ssl/client-encrypted-pem_tmp.key": processing error\E!, + "certificate authorization fails with correct client cert and no password in encrypted PEM format", + close_stdin => 1); # pg_stat_ssl command_like( @@ -436,5 +484,7 @@ test_connect_fails($common_connstr, "sslmode=require sslcert=ssl/client.crt", qr/SSL error/, "intermediate client certificate is missing"); # clean up -unlink("ssl/client_tmp.key", "ssl/client_wrongperms_tmp.key", - "ssl/client-revoked_tmp.key"); +foreach my $key (@keys) +{ + unlink("ssl/${key}_tmp.key"); +} diff --git a/src/test/ssl/t/SSLServer.pm b/src/test/ssl/t/SSLServer.pm index 005955a2ff..a081cc3ab2 100644 --- a/src/test/ssl/t/SSLServer.pm +++ b/src/test/ssl/t/SSLServer.pm @@ -66,7 +66,7 @@ sub test_connect_fails { local $Test::Builder::Level = $Test::Builder::Level + 1; - my ($common_connstr, $connstr, $expected_stderr, $test_name) = @_; + my ($common_connstr, $connstr, $expected_stderr, $test_name, %kwargs) = @_; my $cmd = [ 'psql', '-X', '-A', '-t', '-c', @@ -74,7 +74,18 @@ sub test_connect_fails '-d', "$common_connstr $connstr" ]; - command_fails_like($cmd, $expected_stderr, $test_name); + my @extra_ipcrun_opts = (); + if ($kwargs{'close_stdin'}) + { + my $eof_in = "\x04"; + # We use IPC::Run's pty support here because OpenSSL likes to use + # /dev/tty to write its prompt and read the result instead of using + # stdio. So we can't stop it blocking the test just by closing stdin. + push(@extra_ipcrun_opts, '<pty<', \$eof_in); + } + + command_fails_like($cmd, $expected_stderr, $test_name, + extra_ipcrun_opts => \@extra_ipcrun_opts); return; }