From 2da74d8d6400975bf73fb0df97c3943ad3ed9a36 Mon Sep 17 00:00:00 2001 From: Daniel Gustafsson Date: Thu, 3 Apr 2025 13:16:43 +0200 Subject: [PATCH] libpq: Add support for dumping SSL key material to file 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 Co-authored-by: Daniel Gustafsson Reviewed-by: Jacob Champion Discussion: https://postgr.es/m/CAKiP-K85C8uQbzXKWf5wHQPkuygGUGcufke713iHmYWOe9q2dA@mail.gmail.com --- configure | 2 +- configure.ac | 2 +- doc/src/sgml/libpq.sgml | 24 ++++++++++ meson.build | 1 + src/include/pg_config.h.in | 3 ++ src/interfaces/libpq/fe-connect.c | 4 ++ src/interfaces/libpq/fe-secure-openssl.c | 58 ++++++++++++++++++++++++ src/interfaces/libpq/libpq-int.h | 1 + src/test/ssl/t/001_ssltests.pl | 27 +++++++++++ 9 files changed, 120 insertions(+), 2 deletions(-) diff --git a/configure b/configure index 3c19e7e60ec..11615d1122d 100755 --- 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" diff --git a/configure.ac b/configure.ac index 65db0673f8a..debdf165044 100644 --- a/configure.ac +++ b/configure.ac @@ -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]) diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml index d7051190320..5e3281ca2eb 100644 --- a/doc/src/sgml/libpq.sgml +++ b/doc/src/sgml/libpq.sgml @@ -1918,6 +1918,30 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname + + sslkeylogfile + + + This parameter specifies the location where libpq + will log keys used in this SSL context. This is useful for debugging + PostgreSQL protocol interactions or client + connections using network inspection tools like + Wireshark. This parameter is ignored if an + SSL connection is not made, or if LibreSSL + is used (LibreSSL does not support key + logging). Keys are logged using the NSS + format. + + + Key logging will expose potentially sensitive information in the + keylog file. Keylog files should be handled with the same care as + files. + + + + + + sslpassword diff --git a/meson.build b/meson.build index e8b872d29ad..454ed81f5ea 100644 --- a/meson.build +++ b/meson.build @@ -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 diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in index 2ac61575883..2428e4f854f 100644 --- a/src/include/pg_config.h.in +++ b/src/include/pg_config.h.in @@ -368,6 +368,9 @@ /* 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 diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 715b5d5aff4..0258d9ace3c 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -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} diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c index 5bb9d9779d8..4bfd8e0447c 100644 --- a/src/interfaces/libpq/fe-secure-openssl.c +++ b/src/interfaces/libpq/fe-secure-openssl.c @@ -57,6 +57,7 @@ * include , but some other Windows headers do.) */ #include "common/openssl.h" +#include #include #ifdef USE_SSL_ENGINE #include @@ -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 diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index 25de3e95055..9369c217fb5 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -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 diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl index 5422511d4ab..086abf3b8b3 100644 --- a/src/test/ssl/t/001_ssltests.pl +++ b/src/test/ssl/t/001_ssltests.pl @@ -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",