> On Tue, May 12, 2026 at 10:48:20PM +0100, Zsolt Parragi wrote:
> +#define HAVE_SSL_GROUPS \
> + defined(HAVE_DECL_SSL_GET1_GROUPS) && \
> + defined(HAVE_DECL_SSL_GET_NEGOTIATED_GROUP) && \
> + defined(HAVE_SSL_GROUP_TO_NAME)
> 
> I don't think this check works properly
> 1. autoconf/meson always defines HAVE_DECL to 0/1, so defined() always
> returns true
> 2. in practice it should work, but using defined() in a macro
> expansion is undefined behavior

That's indeed a faux pas. I was testing this on openssl 1.1.1w, which
was lacking both SSL_get_negotiated_group and SSL_group_to_name, and
this was hiding the problem. The new version should address this
problem, and includes documentation fix from Cary.
>From 65984bd9a56559d4cc3c2877f0af777f624a8cdd Mon Sep 17 00:00:00 2001
From: Dmitrii Dolgov <[email protected]>
Date: Wed, 13 May 2026 19:51:22 +0200
Subject: [PATCH v6] contrib/sslinfo: Add ssl_group_info

Add a new function to sslinfo ssl_group_info to show SSL groups,
including negotiated, supported and shared. It's useful for diagnostic
purposes, to identify what's being used and supported, e.g. which key
share is being negotiated. Few examples, for openssl 3.2.4:

    select * from ssl_group_info();
        type    |        name
    ------------+--------------------
     negotiated | X25519MLKEM768
     shared     | X25519MLKEM768
     shared     | x25519
     supported  | X25519MLKEM768
     supported  | x25519
     [...]

The implementation is inspired by ssl_print_groups from openssl.

Reviewed-by: Daniel Gustafsson <[email protected]>
Reviewed-by: Jacob Champion <[email protected]>
Reviewed-by: Cary Huang <[email protected]>
Reviewed-by: Zsolt Parragi <[email protected]>
---
 configure                             |  36 +++++
 configure.ac                          |   4 +
 contrib/sslinfo/Makefile              |   2 +-
 contrib/sslinfo/meson.build           |   1 +
 contrib/sslinfo/sslinfo--1.2--1.3.sql |  10 ++
 contrib/sslinfo/sslinfo.c             | 192 +++++++++++++++++++++++++-
 contrib/sslinfo/sslinfo.control       |   2 +-
 doc/src/sgml/sslinfo.sgml             |  45 ++++++
 meson.build                           |   5 +
 src/include/pg_config.h.in            |  11 ++
 src/tools/pgindent/typedefs.list      |   1 +
 11 files changed, 306 insertions(+), 3 deletions(-)
 create mode 100644 contrib/sslinfo/sslinfo--1.2--1.3.sql

diff --git a/configure b/configure
index f66c1054a7a..2a708df5afd 100755
--- a/configure
+++ b/configure
@@ -13189,6 +13189,42 @@ _ACEOF
 fi
 done
 
+  # Functions for groups support, some of them are real functions, some are
+  # just wrappers around SSL_ctrl.
+  for ac_func in SSL_group_to_name
+do :
+  ac_fn_c_check_func "$LINENO" "SSL_group_to_name" 
"ac_cv_func_SSL_group_to_name"
+if test "x$ac_cv_func_SSL_group_to_name" = xyes; then :
+  cat >>confdefs.h <<_ACEOF
+#define HAVE_SSL_GROUP_TO_NAME 1
+_ACEOF
+
+fi
+done
+
+ac_fn_c_check_decl "$LINENO" "SSL_get1_groups" 
"ac_cv_have_decl_SSL_get1_groups" "#include <openssl/ssl.h>
+"
+if test "x$ac_cv_have_decl_SSL_get1_groups" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_SSL_GET1_GROUPS $ac_have_decl
+_ACEOF
+ac_fn_c_check_decl "$LINENO" "SSL_get_negotiated_group" 
"ac_cv_have_decl_SSL_get_negotiated_group" "#include <openssl/ssl.h>
+"
+if test "x$ac_cv_have_decl_SSL_get_negotiated_group" = xyes; then :
+  ac_have_decl=1
+else
+  ac_have_decl=0
+fi
+
+cat >>confdefs.h <<_ACEOF
+#define HAVE_DECL_SSL_GET_NEGOTIATED_GROUP $ac_have_decl
+_ACEOF
+
 
 $as_echo "#define USE_OPENSSL 1" >>confdefs.h
 
diff --git a/configure.ac b/configure.ac
index 8d176bd3468..2641a17d317 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1444,6 +1444,10 @@ if test "$with_ssl" = openssl ; then
   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 
SSL_CTX_set_keylog_callback SSL_CTX_set_client_hello_cb])
+  # Functions for groups support, some of them are real functions, some are
+  # just wrappers around SSL_ctrl.
+  AC_CHECK_FUNCS([SSL_group_to_name])
+  AC_CHECK_DECLS([SSL_get1_groups, SSL_get_negotiated_group], [], [], 
[#include <openssl/ssl.h>])
   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/contrib/sslinfo/Makefile b/contrib/sslinfo/Makefile
index 14305594e2d..d968ef2abfd 100644
--- a/contrib/sslinfo/Makefile
+++ b/contrib/sslinfo/Makefile
@@ -6,7 +6,7 @@ OBJS = \
        sslinfo.o
 
 EXTENSION = sslinfo
-DATA = sslinfo--1.2.sql sslinfo--1.1--1.2.sql sslinfo--1.0--1.1.sql
+DATA = sslinfo--1.2.sql sslinfo--1.2--1.3.sql sslinfo--1.1--1.2.sql 
sslinfo--1.0--1.1.sql
 PGFILEDESC = "sslinfo - information about client SSL certificate"
 
 ifdef USE_PGXS
diff --git a/contrib/sslinfo/meson.build b/contrib/sslinfo/meson.build
index 6e9cb96430a..27737562925 100644
--- a/contrib/sslinfo/meson.build
+++ b/contrib/sslinfo/meson.build
@@ -26,6 +26,7 @@ install_data(
   'sslinfo--1.0--1.1.sql',
   'sslinfo--1.1--1.2.sql',
   'sslinfo--1.2.sql',
+  'sslinfo--1.2--1.3.sql',
   'sslinfo.control',
   kwargs: contrib_data_args,
 )
diff --git a/contrib/sslinfo/sslinfo--1.2--1.3.sql 
b/contrib/sslinfo/sslinfo--1.2--1.3.sql
new file mode 100644
index 00000000000..40fd0ea2b9c
--- /dev/null
+++ b/contrib/sslinfo/sslinfo--1.2--1.3.sql
@@ -0,0 +1,10 @@
+/* contrib/sslinfo/sslinfo--1.2--1.3.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION sslinfo UPDATE TO '1.3'" to load this file. \quit
+
+CREATE FUNCTION
+ssl_group_info(OUT group_type text, OUT name text
+) RETURNS SETOF record
+AS 'MODULE_PATHNAME', 'ssl_group_info'
+LANGUAGE C STRICT PARALLEL RESTRICTED;
diff --git a/contrib/sslinfo/sslinfo.c b/contrib/sslinfo/sslinfo.c
index 2b9eb90b093..8807f50c908 100644
--- a/contrib/sslinfo/sslinfo.c
+++ b/contrib/sslinfo/sslinfo.c
@@ -19,6 +19,14 @@
 #include "miscadmin.h"
 #include "utils/builtins.h"
 
+#if HAVE_DECL_SSL_GET1_GROUPS && \
+       HAVE_DECL_SSL_GET_NEGOTIATED_GROUP && \
+       defined(HAVE_SSL_GROUP_TO_NAME) \
+
+#define HAVE_SSL_GROUPS
+
+#endif
+
 PG_MODULE_MAGIC_EXT(
                                        .name = "sslinfo",
                                        .version = PG_VERSION
@@ -28,13 +36,28 @@ static Datum X509_NAME_field_to_text(X509_NAME *name, text 
*fieldName);
 static Datum ASN1_STRING_to_text(ASN1_STRING *str);
 
 /*
- * Function context for data persisting over repeated calls.
+ * Function context for data persisting over repeated calls of
+ * ssl_extension_info.
  */
 typedef struct
 {
        TupleDesc       tupdesc;
 } SSLExtensionInfoContext;
 
+/*
+ * Function context for data persisting over repeated calls of
+ * ssl_group_info.
+ */
+typedef struct
+{
+       TupleDesc       tupdesc;
+       int                     nshared;
+       int                     nsupported;
+
+       /* Supported groups have to be stored separately */
+       int                *supported_groups;
+} SSLGroupInfoContext;
+
 /*
  * Indicates whether current session uses SSL
  *
@@ -474,3 +497,170 @@ ssl_extension_info(PG_FUNCTION_ARGS)
        /* All done */
        SRF_RETURN_DONE(funcctx);
 }
+
+/*
+ * Returns information about TLS groups.
+ *
+ * Returns setof record made of the following values:
+ * - type of the group: negotiated, shared, supported.
+ * - name of the group.
+ */
+PG_FUNCTION_INFO_V1(ssl_group_info);
+Datum
+ssl_group_info(PG_FUNCTION_ARGS)
+{
+#ifdef HAVE_SSL_GROUPS
+       /*
+        * If we lack SSL groups API, we still need to do SRF stuff. Thus the
+        * condition doesn't cover the whole function, but only parts of it. 
This
+        * particular one is only to avoid unused variable warning, in case if
+        * HAVE_SSL_GROUPS is false.
+        */
+       SSL                *ssl = MyProcPort->ssl;
+#endif
+
+       FuncCallContext *funcctx;
+       int                     call_cntr = 0;
+       int                     max_calls = 0;
+       MemoryContext oldcontext;
+       SSLGroupInfoContext *fctx;
+
+       if (SRF_IS_FIRSTCALL())
+       {
+
+               TupleDesc       tupdesc;
+
+               /* create a function context for cross-call persistence */
+               funcctx = SRF_FIRSTCALL_INIT();
+
+               /*
+                * Switch to memory context appropriate for multiple function 
calls
+                */
+               oldcontext = 
MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+
+               /* Create a user function context for cross-call persistence */
+               fctx = palloc_object(SSLGroupInfoContext);
+
+               /* Construct tuple descriptor */
+               if (get_call_result_type(fcinfo, NULL, &tupdesc) != 
TYPEFUNC_COMPOSITE)
+                       ereport(ERROR,
+                                       (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                                        errmsg("function returning record 
called in context that cannot accept type record")));
+               fctx->tupdesc = BlessTupleDesc(tupdesc);
+
+               if (!MyProcPort->ssl_in_use)
+               {
+                       /* fast track when no results */
+                       MemoryContextSwitchTo(oldcontext);
+                       SRF_RETURN_DONE(funcctx);
+               }
+
+#ifdef HAVE_SSL_GROUPS
+               if (ssl != NULL)
+               {
+                       fctx->nsupported = SSL_get1_groups(ssl, NULL);
+                       fctx->nshared = SSL_get_shared_group(ssl, -1);
+
+                       fctx->supported_groups =
+                               palloc(fctx->nsupported * 
sizeof(*fctx->supported_groups));
+                       SSL_get1_groups(ssl, fctx->supported_groups);
+
+                       /*
+                        * Set max_calls as the number of supported groups plus 
the number
+                        * of shared groups plus one negotiated group.
+                        */
+                       max_calls = fctx->nsupported + fctx->nshared + 1;
+               }
+
+               if (max_calls > 0)
+               {
+                       /* got results, keep track of them */
+                       funcctx->max_calls = max_calls;
+                       funcctx->user_fctx = fctx;
+               }
+               else
+               {
+                       /* fast track when no results */
+                       MemoryContextSwitchTo(oldcontext);
+                       SRF_RETURN_DONE(funcctx);
+               }
+#else
+               /* SSL groups API is not present, skip */
+               MemoryContextSwitchTo(oldcontext);
+               SRF_RETURN_DONE(funcctx);
+#endif
+
+               MemoryContextSwitchTo(oldcontext);
+       }
+
+       /* stuff done on every call of the function */
+       funcctx = SRF_PERCALL_SETUP();
+
+       /*
+        * Initialize per-call variables.
+        */
+       call_cntr = funcctx->call_cntr;
+       max_calls = funcctx->max_calls;
+       fctx = funcctx->user_fctx;
+
+       /* do while there are more left to send */
+       if (call_cntr < max_calls)
+       {
+#ifdef HAVE_SSL_GROUPS
+               Datum           values[2];
+               bool            nulls[2];
+               HeapTuple       tuple;
+               Datum           result,
+                                       group_type;
+               int                     nid;
+               const char *group_name;
+
+               /* Send the negotiated group first */
+               if (call_cntr == 0)
+               {
+                       nid = SSL_get_negotiated_group(ssl);
+                       group_type = CStringGetTextDatum("negotiated");
+               }
+               /* Then the shared groups */
+               else if (call_cntr < fctx->nshared + 1)
+               {
+                       nid = SSL_get_shared_group(ssl, call_cntr - 1);
+                       group_type = CStringGetTextDatum("shared");
+               }
+               /* And finally the supported groups */
+               else if (call_cntr < fctx->nsupported + fctx->nshared + 1)
+               {
+                       nid = fctx->supported_groups[call_cntr - fctx->nshared 
- 1];
+                       group_type = CStringGetTextDatum("supported");
+               }
+               else
+                       SRF_RETURN_DONE(funcctx);
+
+               /*
+                * SSL_group_to_name can return NULL in case of an error, e.g. 
when no
+                * such name was registered for some reason.
+                */
+               group_name = SSL_group_to_name(ssl, nid);
+               if (group_name == NULL)
+                       ereport(ERROR,
+                                       (errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+                                        errmsg("unknown OpenSSL group at 
position %d",
+                                                       call_cntr)));
+
+               values[0] = group_type;
+               nulls[0] = false;
+
+               values[1] = CStringGetTextDatum(group_name);
+               nulls[1] = false;
+
+               /* Build tuple */
+               tuple = heap_form_tuple(fctx->tupdesc, values, nulls);
+               result = HeapTupleGetDatum(tuple);
+
+               SRF_RETURN_NEXT(funcctx, result);
+#endif
+       }
+
+       /* All done */
+       SRF_RETURN_DONE(funcctx);
+}
diff --git a/contrib/sslinfo/sslinfo.control b/contrib/sslinfo/sslinfo.control
index c7754f924cf..b53e95b7da8 100644
--- a/contrib/sslinfo/sslinfo.control
+++ b/contrib/sslinfo/sslinfo.control
@@ -1,5 +1,5 @@
 # sslinfo extension
 comment = 'information about SSL certificates'
-default_version = '1.2'
+default_version = '1.3'
 module_pathname = '$libdir/sslinfo'
 relocatable = true
diff --git a/doc/src/sgml/sslinfo.sgml b/doc/src/sgml/sslinfo.sgml
index 85d49f66537..34fc187e4e4 100644
--- a/doc/src/sgml/sslinfo.sgml
+++ b/doc/src/sgml/sslinfo.sgml
@@ -240,6 +240,51 @@ emailAddress
     </para>
     </listitem>
    </varlistentry>
+
+   <varlistentry>
+    <term>
+     <function>ssl_group_info() returns setof record</function>
+     <indexterm>
+      <primary>ssl_group_info</primary>
+     </indexterm>
+    </term>
+    <listitem>
+    <para>
+     Provide information about TLS groups: group type and group name.
+     The group type value could be one of the following:
+
+     <variablelist>
+      <varlistentry id="ssl-group-info-negotiated">
+       <term><literal>negotiated</literal></term>
+       <listitem>
+        <para>
+         The group used for the handshake key exchange process.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry id="ssl-group-info-shared">
+       <term><literal>shared</literal></term>
+       <listitem>
+        <para>
+         List of named groups shared with the server side.
+        </para>
+       </listitem>
+      </varlistentry>
+
+      <varlistentry id="ssl-group-info-supported">
+       <term><literal>supported</literal></term>
+       <listitem>
+        <para>
+         list of named groups supported by the client for key exchange in the
+         form of "supported_groups" extension.
+        </para>
+       </listitem>
+      </varlistentry>
+     </variablelist>
+    </para>
+    </listitem>
+   </varlistentry>
   </variablelist>
  </sect2>
 
diff --git a/meson.build b/meson.build
index 20b887f1a1b..90e99705656 100644
--- a/meson.build
+++ b/meson.build
@@ -1677,6 +1677,9 @@ if sslopt in ['auto', 'openssl']
       ['SSL_CTX_set_num_tickets'],
       ['SSL_CTX_set_keylog_callback'],
       ['SSL_CTX_set_client_hello_cb'],
+
+      # Function for groups support.
+      ['SSL_group_to_name'],
     ]
 
     are_openssl_funcs_complete = true
@@ -2901,6 +2904,8 @@ decl_checks = [
   ['strlcpy', 'string.h'],
   ['strsep',  'string.h'],
   ['timingsafe_bcmp',  'string.h'],
+  ['SSL_get1_groups',           'openssl/ssl.h'],
+  ['SSL_get_negotiated_group',  'openssl/ssl.h'],
 ]
 
 # Need to check for function declarations for these functions, because
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 4f8113c144b..67090821722 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -101,6 +101,14 @@
    don't. */
 #undef HAVE_DECL_PWRITEV
 
+/* Define to 1 if you have the declaration of `SSL_get1_groups', and to 0 if
+   you don't. */
+#undef HAVE_DECL_SSL_GET1_GROUPS
+
+/* Define to 1 if you have the declaration of `SSL_get_negotiated_group', and
+   to 0 if you don't. */
+#undef HAVE_DECL_SSL_GET_NEGOTIATED_GROUP
+
 /* Define to 1 if you have the declaration of `strchrnul', and to 0 if you
    don't. */
 #undef HAVE_DECL_STRCHRNUL
@@ -384,6 +392,9 @@
 /* Define to 1 if you have the `SSL_CTX_set_num_tickets' function. */
 #undef HAVE_SSL_CTX_SET_NUM_TICKETS
 
+/* Define to 1 if you have the `SSL_group_to_name' function. */
+#undef HAVE_SSL_GROUP_TO_NAME
+
 /* Define to 1 if you have the <stdint.h> header file. */
 #undef HAVE_STDINT_H
 
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index cbd9e10fc1d..cb8b4ea306e 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2777,6 +2777,7 @@ SQLValueFunction
 SQLValueFunctionOp
 SSL
 SSLExtensionInfoContext
+SSLGroupInfoContext
 SSL_CTX
 STARTUPINFO
 STRLEN

base-commit: 3bf63730cb041a834c618082ba3d5e8bf96a31a5
-- 
2.52.0

Reply via email to