libpq: Add support for dumping SSL key material to file
authorDaniel Gustafsson <[email protected]>
Thu, 3 Apr 2025 11:16:43 +0000 (13:16 +0200)
committerDaniel Gustafsson <[email protected]>
Thu, 3 Apr 2025 11:16:43 +0000 (13:16 +0200)
This adds a new connection parameter which instructs libpq to
write out keymaterial clientside into a file in order to make
connection debugging with Wireshark and similar tools possible.
The file format used is the standardized NSS format.

Author: Abhishek Chanda <[email protected]>
Co-authored-by: Daniel Gustafsson <[email protected]>
Reviewed-by: Jacob Champion <[email protected]>
Discussion: https://postgr.es/m/CAKiP-K85C8uQbzXKWf5wHQPkuygGUGcufke713iHmYWOe9q2dA@mail.gmail.com

configure
configure.ac
doc/src/sgml/libpq.sgml
meson.build
src/include/pg_config.h.in
src/interfaces/libpq/fe-connect.c
src/interfaces/libpq/fe-secure-openssl.c
src/interfaces/libpq/libpq-int.h
src/test/ssl/t/001_ssltests.pl

index 3c19e7e60ec6d3e2e2d72328a5eb096aac5245fa..11615d1122dea7e05cd330d8d767e480a727ad49 100755 (executable)
--- a/configure
+++ b/configure
@@ -12931,7 +12931,7 @@ fi
 done
 
   # Function introduced in OpenSSL 1.1.1, not in LibreSSL.
-  for ac_func in X509_get_signature_info SSL_CTX_set_num_tickets
+  for ac_func in X509_get_signature_info SSL_CTX_set_num_tickets SSL_CTX_set_keylog_callback
 do :
   as_ac_var=`$as_echo "ac_cv_func_$ac_func" | $as_tr_sh`
 ac_fn_c_check_func "$LINENO" "$ac_func" "$as_ac_var"
index 65db0673f8a1e4a2d82aaec9be04aee2d3055173..debdf1650441b22ccf3c95b29f4f91222bdb83bd 100644 (file)
@@ -1382,7 +1382,7 @@ if test "$with_ssl" = openssl ; then
   # Function introduced in OpenSSL 1.0.2, not in LibreSSL.
   AC_CHECK_FUNCS([SSL_CTX_set_cert_cb])
   # Function introduced in OpenSSL 1.1.1, not in LibreSSL.
-  AC_CHECK_FUNCS([X509_get_signature_info SSL_CTX_set_num_tickets])
+  AC_CHECK_FUNCS([X509_get_signature_info SSL_CTX_set_num_tickets SSL_CTX_set_keylog_callback])
   AC_DEFINE([USE_OPENSSL], 1, [Define to 1 to build with OpenSSL support. (--with-ssl=openssl)])
 elif test "$with_ssl" != no ; then
   AC_MSG_ERROR([--with-ssl must specify openssl])
index d7051190320c8360778444b66232f22541bb4ad7..5e3281ca2eba1555b8e04b77151c69a9455da217 100644 (file)
@@ -1918,6 +1918,30 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-sslkeylogfile" xreflabel="sslkeylogfile">
+      <term><literal>sslkeylogfile</literal></term>
+      <listitem>
+       <para>
+        This parameter specifies the location where <literal>libpq</literal>
+        will log keys used in this SSL context.  This is useful for debugging
+        <productname>PostgreSQL</productname> protocol interactions or client
+        connections using network inspection tools like
+        <productname>Wireshark</productname>.  This parameter is ignored if an
+        SSL connection is not made, or if <productname>LibreSSL</productname>
+        is used (<productname>LibreSSL</productname> does not support key
+        logging).  Keys are logged using the <productname>NSS</productname>
+        format.
+        <warning>
+         <para>
+          Key logging will expose potentially sensitive information in the
+          keylog file.  Keylog files should be handled with the same care as
+          <xref linkend="libpq-connect-sslkey" /> files.
+         </para>
+        </warning>
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-sslpassword" xreflabel="sslpassword">
       <term><literal>sslpassword</literal></term>
       <listitem>
index e8b872d29ad70a5b2f8830278b8e9a6538f01465..454ed81f5ead9ed49c309631d27e1081e30031cf 100644 (file)
@@ -1479,6 +1479,7 @@ if sslopt in ['auto', 'openssl']
       # Function introduced in OpenSSL 1.1.1, not in LibreSSL.
       ['X509_get_signature_info'],
       ['SSL_CTX_set_num_tickets'],
+      ['SSL_CTX_set_keylog_callback'],
     ]
 
     are_openssl_funcs_complete = true
index 2ac6157588369e128f7a36f2080a804d7ed4fdec..2428e4f854f189dacc18a8417f3df46f44de256f 100644 (file)
 /* Define to 1 if you have the `SSL_CTX_set_ciphersuites' function. */
 #undef HAVE_SSL_CTX_SET_CIPHERSUITES
 
+/* Define to 1 if you have the `SSL_CTX_set_keylog_callback' function. */
+#undef HAVE_SSL_CTX_SET_KEYLOG_CALLBACK
+
 /* Define to 1 if you have the `SSL_CTX_set_num_tickets' function. */
 #undef HAVE_SSL_CTX_SET_NUM_TICKETS
 
index 715b5d5aff45d7a2e5bf9dae30a1aa4020ace186..0258d9ace3c9b4bbaf46e2109881a959f087c0bb 100644 (file)
@@ -401,6 +401,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
        "OAuth-Scope", "", 15,
    offsetof(struct pg_conn, oauth_scope)},
 
+   {"sslkeylogfile", NULL, NULL, NULL,
+       "SSL-Key-Log-File", "", 0,  /* sizeof("") = 0 */
+   offsetof(struct pg_conn, sslkeylogfile)},
+
    /* Terminating entry --- MUST BE LAST */
    {NULL, NULL, NULL, NULL,
    NULL, NULL, 0}
index 5bb9d9779d82803c6394b72e2d7e71a8ea9139a2..4bfd8e0447ce9f017a1900a4dd2a3e13a0ab465d 100644 (file)
@@ -57,6 +57,7 @@
  * include <wincrypt.h>, but some other Windows headers do.)
  */
 #include "common/openssl.h"
+#include <openssl/ssl.h>
 #include <openssl/conf.h>
 #ifdef USE_SSL_ENGINE
 #include <openssl/engine.h>
@@ -684,6 +685,49 @@ pgtls_verify_peer_name_matches_certificate_guts(PGconn *conn,
 /* See pqcomm.h comments on OpenSSL implementation of ALPN (RFC 7301) */
 static unsigned char alpn_protos[] = PG_ALPN_PROTOCOL_VECTOR;
 
+#ifdef HAVE_SSL_CTX_SET_KEYLOG_CALLBACK
+/*
+ * SSL Key Logging callback
+ *
+ * This callback lets the user store all key material to a file for debugging
+ * purposes.  The file will be written using the NSS keylog format.  LibreSSL
+ * 3.5 introduced stub function to set the callback for OpenSSL compatibility
+ * but the callback is never invoked.
+ */
+static void
+SSL_CTX_keylog_cb(const SSL *ssl, const char *line)
+{
+   int         fd;
+   mode_t      old_umask;
+   ssize_t     rc;
+   PGconn     *conn = SSL_get_app_data(ssl);
+
+   if (conn == NULL)
+       return;
+
+   old_umask = umask(077);
+   fd = open(conn->sslkeylogfile, O_WRONLY | O_APPEND | O_CREAT, 0600);
+   umask(old_umask);
+
+   if (fd == -1)
+   {
+       libpq_append_conn_error(conn, "could not open ssl keylog file %s: %s",
+                               conn->sslkeylogfile, pg_strerror(errno));
+       return;
+   }
+
+   /* line is guaranteed by OpenSSL to be NUL terminated */
+   rc = write(fd, line, strlen(line));
+   if (rc < 0)
+       libpq_append_conn_error(conn, "could not write to ssl keylog file %s: %s",
+                               conn->sslkeylogfile, pg_strerror(errno));
+   else
+       rc = write(fd, "\n", 1);
+   (void) rc;                  /* silence compiler warnings */
+   close(fd);
+}
+#endif
+
 /*
  * Create per-connection SSL object, and load the client certificate,
  * private key, and trusted CA certs.
@@ -1000,6 +1044,20 @@ initialize_SSL(PGconn *conn)
    }
    conn->ssl_in_use = true;
 
+   if (conn->sslkeylogfile && strlen(conn->sslkeylogfile) > 0)
+   {
+#ifdef HAVE_SSL_CTX_SET_KEYLOG_CALLBACK
+       SSL_CTX_set_keylog_callback(SSL_context, SSL_CTX_keylog_cb);
+#else
+#ifdef LIBRESSL_VERSION_NUMBER
+       fprintf(stderr, libpq_gettext("WARNING: sslkeylogfile support requires OpenSSL\n"));
+#else
+       fprintf(stderr, libpq_gettext("WARNING: libpq was not built with sslkeylogfile support\n"));
+#endif
+#endif
+   }
+
+
    /*
     * SSL contexts are reference counted by OpenSSL. We can free it as soon
     * as we have created the SSL object, and it will stick around for as long
index 25de3e950551c3292d90a0d6534ccb31d7ae7a5b..9369c217fb56e402e4f5f20ff6f8ef3cbcb98616 100644 (file)
@@ -426,6 +426,7 @@ struct pg_conn
    char       *load_balance_hosts; /* load balance over hosts */
    char       *scram_client_key;   /* base64-encoded SCRAM client key */
    char       *scram_server_key;   /* base64-encoded SCRAM server key */
+   char       *sslkeylogfile;  /* where should the client write ssl keylogs */
 
    bool        cancelRequest;  /* true if this connection is used to send a
                                 * cancel request, instead of being a normal
index 5422511d4ab73da4c6bb963a90efbfbbebf2e7b1..086abf3b8b3943c4d230e1a33e6fc2000e1b9b12 100644 (file)
@@ -147,6 +147,33 @@ my $default_ssl_connstr =
 $common_connstr =
   "$default_ssl_connstr user=ssltestuser dbname=trustdb hostaddr=$SERVERHOSTADDR host=common-name.pg-ssltest.test";
 
+SKIP:
+{
+   skip "Keylogging is not supported with LibreSSL", 5 if $libressl;
+
+   my $tempdir = PostgreSQL::Test::Utils::tempdir;
+   my @status;
+
+   # Properly escape backslashes in the path
+   $tempdir =~ s/\\/\\\\/g;
+
+   # Connect should work with a given sslkeylogfile
+   $node->connect_ok(
+       "$common_connstr sslrootcert=ssl/root+server_ca.crt sslkeylogfile=$tempdir/key.txt sslmode=require",
+       "connect with server root cert and sslkeylogfile=$tempdir/key.txt");
+
+   # Verify the key file exists
+   ok(-f "$tempdir/key.txt", "keylog file exists at: $tempdir/key.txt");
+
+   # Skip permission checks on Windows/Cygwin
+   skip "Permissions check not enforced on Windows", 2
+     if ($windows_os || $Config::Config{osname} eq 'cygwin');
+
+   ok( (@status = stat("$tempdir/key.txt")),
+       "keylog file exists and returned status");
+   ok(@status && !($status[2] & 0006), "keylog file is not world readable");
+}
+
 # The server should not accept non-SSL connections.
 $node->connect_fails(
    "$common_connstr sslmode=disable",