From eb91afc87462d0054a09a988b6c7b4678b7a621a Mon Sep 17 00:00:00 2001 From: Abhishek Chanda Date: Tue, 7 Jan 2025 21:54:08 -0600 Subject: [PATCH v4] Add support for dumping SSL keylog to a file This patch adds a new connection parameter which is used by libpq to write keys used in a SSL context --- .gitignore | 1 + doc/src/sgml/libpq.sgml | 22 ++++++++++++ src/interfaces/libpq/fe-connect.c | 5 +++ src/interfaces/libpq/fe-secure-openssl.c | 26 ++++++++++++++ src/interfaces/libpq/libpq-int.h | 1 + src/test/perl/PostgreSQL/Test/Utils.pm | 46 ++++++++++++++++++++++++ src/test/regress/pg_regress.c | 1 + src/test/ssl/t/001_ssltests.pl | 7 ++++ 8 files changed, 109 insertions(+) diff --git a/.gitignore b/.gitignore index 4e911395fe..025d6115da 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,7 @@ win32ver.rc *.exe lib*dll.def lib*.pc +./src/test/ssl/key.txt # Local excludes in root directory /GNUmakefile diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml index c49e975b08..1573cd865a 100644 --- a/doc/src/sgml/libpq.sgml +++ b/doc/src/sgml/libpq.sgml @@ -1909,6 +1909,18 @@ 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 postgres + protocol using tools like wireshark. This parameter is ignored if an + SSL connection is not made. + + + + sslpassword @@ -9060,6 +9072,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough) + + + + PGSSLKEYLOGFILE + + PGSSLKEYLOGFILE behaves the same as the connection parameter. + + + diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 85d1ca2864..9172bd4752 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -373,6 +373,11 @@ static const internalPQconninfoOption PQconninfoOptions[] = { {"scram_server_key", NULL, NULL, NULL, "SCRAM-Server-Key", "D", SCRAM_MAX_KEY_LEN * 2, offsetof(struct pg_conn, scram_server_key)}, + {"sslkeylogfile", "PGSSLKEYLOGFILE", + "", 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 5bb9d9779d..326257ce47 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 @@ -86,6 +87,7 @@ static pthread_mutex_t ssl_config_mutex = PTHREAD_MUTEX_INITIALIZER; static PQsslKeyPassHook_OpenSSL_type PQsslKeyPassHook = NULL; static int ssl_protocol_version_to_openssl(const char *protocol); +static void SSL_CTX_keylog_cb(const SSL *ssl, const char *line); /* ------------------------------------------------------------ */ /* Procedures common to all secure sessions */ @@ -684,6 +686,27 @@ 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; +/* This is a callback that writes to a given ssl key log file */ +static void SSL_CTX_keylog_cb(const SSL *ssl, const char *line) { + FILE *log_file; + PGconn *conn = SSL_get_app_data(ssl); + if (conn == NULL) + return; + + log_file = fopen(conn->sslkeylogfile, "a"); + if (log_file == NULL) { + libpq_append_conn_error(conn, "could not open ssl key log file %s: %s", conn->sslkeylogfile, pg_strerror(errno)); + return; + } + + if (chmod(conn->sslkeylogfile, 0600) == -1) { + libpq_append_conn_error(conn, "could not chmod ssl key log file %s: %s", conn->sslkeylogfile, pg_strerror(errno)); + return; + } + fprintf(log_file, "%s\n", line); + fclose(log_file); +} + /* * Create per-connection SSL object, and load the client certificate, * private key, and trusted CA certs. @@ -1000,6 +1023,9 @@ initialize_SSL(PGconn *conn) } conn->ssl_in_use = true; + if (conn->sslkeylogfile && strlen(conn->sslkeylogfile) > 0) + SSL_CTX_set_keylog_callback(SSL_context, SSL_CTX_keylog_cb); + /* * 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 2546f9f8a5..f35cd538cf 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -432,6 +432,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 key logs */ bool cancelRequest; /* true if this connection is used to send a * cancel request, instead of being a normal diff --git a/src/test/perl/PostgreSQL/Test/Utils.pm b/src/test/perl/PostgreSQL/Test/Utils.pm index efe0321a4e..65f7ed764a 100644 --- a/src/test/perl/PostgreSQL/Test/Utils.pm +++ b/src/test/perl/PostgreSQL/Test/Utils.pm @@ -68,6 +68,7 @@ our @EXPORT = qw( slurp_file append_to_file string_replace_file + check_file_mode check_mode_recursive chmod_recursive check_pg_config @@ -131,6 +132,7 @@ BEGIN PGSSLCRL PGSSLCRLDIR PGSSLKEY + PGSSLKEYLOGFILE PGSSLMAXPROTOCOLVERSION PGSSLMINPROTOCOLVERSION PGSSLMODE @@ -588,6 +590,50 @@ sub string_replace_file =pod +=item check_file_mode(path, expected_file_mode) + +Check that the given file exists and mode match the expected values. + +=cut + +sub check_file_mode { + my ($path, $expected_file_mode) = @_; + + unless (-e $path) { + die "Error: File '$path' does not exist\n"; + } + + my $file_stat = stat($path); + unless (defined($file_stat)) { + my $is_ENOENT = $!{ENOENT}; + my $msg = "unable to stat $path: $!"; + if ($is_ENOENT) { + warn $msg; + return 0; + } + else { + die $msg; + } + } + + # Skip file mode check on Windows + return 1 if $windows_os; + + my $file_mode = S_IMODE($file_stat->mode); + + if (S_ISREG($file_stat->mode)) { + if ($file_mode != $expected_file_mode) { + print STDERR sprintf("%s mode must be %04o\n", + $path, $expected_file_mode); + return 0; + } + } + + return 1; +} + +=pod + =item check_mode_recursive(dir, expected_dir_mode, expected_file_mode, ignore_list) Check that all file/dir modes in a directory match the expected values, diff --git a/src/test/regress/pg_regress.c b/src/test/regress/pg_regress.c index 5d85dcc62f..381a7266cb 100644 --- a/src/test/regress/pg_regress.c +++ b/src/test/regress/pg_regress.c @@ -834,6 +834,7 @@ initialize_environment(void) unsetenv("PGSSLCRL"); unsetenv("PGSSLCRLDIR"); unsetenv("PGSSLKEY"); + unsetenv("PGSSLKEYLOGFILE"); unsetenv("PGSSLMAXPROTOCOLVERSION"); unsetenv("PGSSLMINPROTOCOLVERSION"); unsetenv("PGSSLMODE"); diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl index 5422511d4a..da4a591393 100644 --- a/src/test/ssl/t/001_ssltests.pl +++ b/src/test/ssl/t/001_ssltests.pl @@ -147,6 +147,13 @@ my $default_ssl_connstr = $common_connstr = "$default_ssl_connstr user=ssltestuser dbname=trustdb hostaddr=$SERVERHOSTADDR host=common-name.pg-ssltest.test"; +# Connect should work with a given sslkeylogfile +$node->connect_ok( + "$common_connstr sslrootcert=ssl/root+server_ca.crt sslkeylogfile=key.txt sslmode=require", + "connect with server root certand sslkeylogfile=key.txt"); + +ok(check_file_mode("key.txt", 0600), 'check key.txt mode'); + # The server should not accept non-SSL connections. $node->connect_fails( "$common_connstr sslmode=disable", -- 2.34.1