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;
 }
 

Reply via email to