On Tue, Mar 21, 2023 at 11:01 PM Michael Paquier <mich...@paquier.xyz> wrote:
> -      # Function introduced in OpenSSL 1.0.2.
> +      # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of 
> these.
>        ['X509_get_signature_nid'],
> +      ['SSL_CTX_set_cert_cb'],
>
> From what I can see, X509_get_signature_nid() is in LibreSSL, but not
> SSL_CTX_set_cert_cb().  Perhaps that's worth having two different
> comments?

I took a stab at that in v18. I diverged a bit between Meson and
Autoconf, which you may not care for.

> +           <para>
> +            a certificate may be sent, if the server requests one and it has
> +            been provided via <literal>sslcert</literal>
> +           </para>
>
> It seems to me that this description is not completely exact.  The
> default is to look at ~/.postgresql/postgresql.crt, so sslcert is not
> mandatory.  There could be a certificate even without sslcert set.

Reworded.

> +           libpq_append_conn_error(conn, "sslcertmode value \"%s\" invalid 
> when SSL support is not compiled in",
> +                                   conn->sslcertmode);
>
> This string could be combined with the same one used for sslmode,
> saving a bit in translation effortm by making the connection parameter
> name a value of the string ("%s value \"%s\" invalid ..").

Done.

> +        * figure out if a certficate was actually requested, so "require" is
> s/certficate/certificate/.

Heh, fixed. I need new glasses, clearly.

> contrib/sslinfo/ has ssl_client_cert_present(), that we could use in
> the tests to make sure that the client has actually sent a
> certificate?  How about adding some of these tests to 003_sslinfo.pl
> for the "allow" and "require" cases?

Added; see what you think.

> +       if (!conn->ssl_cert_requested)
> +       {
> +           libpq_append_conn_error(conn, "server did not request a 
> certificate");
> +           return false;
> +       }
> +       else if (!conn->ssl_cert_sent)
> +       {
> +           libpq_append_conn_error(conn, "server accepted connection without 
> a valid certificate");
> +           return false;
> +       }
> Perhaps useless question: should this say "SSL certificate"?

I have no objection, so done that way.

> freePGconn() is missing a free(sslcertmode).

Argh, I keep forgetting that. Fixed, thanks!

--Jacob
1:  8d93ca3792 ! 1:  ad71dc8b72 Add sslcertmode option for client certificates
    @@ configure: else
        fi
     -  # Function introduced in OpenSSL 1.0.2.
     -  for ac_func in X509_get_signature_nid
    -+  # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of 
these.
    ++  # Functions introduced in OpenSSL 1.0.2. Note that LibreSSL doesn't have
    ++  # SSL_CTX_set_cert_cb().
     +  for ac_func in X509_get_signature_nid SSL_CTX_set_cert_cb
      do :
     -  ac_fn_c_check_func "$LINENO" "X509_get_signature_nid" 
"ac_cv_func_X509_get_signature_nid"
    @@ configure.ac: if test "$with_ssl" = openssl ; then
        fi
     -  # Function introduced in OpenSSL 1.0.2.
     -  AC_CHECK_FUNCS([X509_get_signature_nid])
    -+  # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all of 
these.
    ++  # Functions introduced in OpenSSL 1.0.2. Note that LibreSSL doesn't have
    ++  # SSL_CTX_set_cert_cb().
     +  AC_CHECK_FUNCS([X509_get_signature_nid SSL_CTX_set_cert_cb])
        # Functions introduced in OpenSSL 1.1.0. We used to check for
        # OPENSSL_VERSION_NUMBER, but that didn't work with 1.1.0, because 
LibreSSL
    @@ doc/src/sgml/libpq.sgml: postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
     +          <term><literal>allow</literal> (default)</term>
     +          <listitem>
     +           <para>
    -+            a certificate may be sent, if the server requests one and it 
has
    -+            been provided via <literal>sslcert</literal>
    ++            a certificate may be sent, if the server requests one and the 
client
    ++            has one to send
     +           </para>
     +          </listitem>
     +         </varlistentry>
    @@ meson.build: if sslopt in ['auto', 'openssl']
            ['SSL_new', {'required': true}],
      
     -      # Function introduced in OpenSSL 1.0.2.
    -+      # Functions introduced in OpenSSL 1.0.2. LibreSSL doesn't have all 
of these.
    ++      # Functions introduced in OpenSSL 1.0.2.
            ['X509_get_signature_nid'],
    -+      ['SSL_CTX_set_cert_cb'],
    ++      ['SSL_CTX_set_cert_cb'], # not in LibreSSL
      
            # Functions introduced in OpenSSL 1.1.0. We used to check for
            # OPENSSL_VERSION_NUMBER, but that didn't work with 1.1.0, because 
LibreSSL
    @@ src/interfaces/libpq/fe-auth.c: check_expected_areq(AuthRequest areq, 
PGconn *co
     +           */
     +          if (!conn->ssl_cert_requested)
     +          {
    -+                  libpq_append_conn_error(conn, "server did not request a 
certificate");
    ++                  libpq_append_conn_error(conn, "server did not request 
an SSL certificate");
     +                  return false;
     +          }
     +          else if (!conn->ssl_cert_sent)
     +          {
    -+                  libpq_append_conn_error(conn, "server accepted 
connection without a valid certificate");
    ++                  libpq_append_conn_error(conn, "server accepted 
connection without a valid SSL certificate");
     +                  return false;
     +          }
     +  }
    @@ src/interfaces/libpq/fe-connect.c: static const internalPQconninfoOption 
PQconni
        {"sslpassword", NULL, NULL, NULL,
                "SSL-Client-Key-Password", "*", 20,
        offsetof(struct pg_conn, sslpassword)},
    +@@ src/interfaces/libpq/fe-connect.c: connectOptions2(PGconn *conn)
    +                   case 'r':                       /* "require" */
    +                   case 'v':                       /* "verify-ca" or 
"verify-full" */
    +                           conn->status = CONNECTION_BAD;
    +-                          libpq_append_conn_error(conn, "sslmode value 
\"%s\" invalid when SSL support is not compiled in",
    +-                                                                          
conn->sslmode);
    ++                          libpq_append_conn_error(conn, "%s value \"%s\" 
invalid when SSL support is not compiled in",
    ++                                                                          
"sslmode", conn->sslmode);
    +                           return false;
    +           }
    + #endif
     @@ src/interfaces/libpq/fe-connect.c: connectOptions2(PGconn *conn)
                return false;
        }
    @@ src/interfaces/libpq/fe-connect.c: connectOptions2(PGconn *conn)
     +          if (strcmp(conn->sslcertmode, "require") == 0)
     +          {
     +                  conn->status = CONNECTION_BAD;
    -+                  libpq_append_conn_error(conn, "sslcertmode value \"%s\" 
invalid when SSL support is not compiled in",
    -+                                                                  
conn->sslcertmode);
    ++                  libpq_append_conn_error(conn, "%s value \"%s\" invalid 
when SSL support is not compiled in",
    ++                                                                  
"sslcertmode", conn->sslcertmode);
     +                  return false;
     +          }
     +#endif
     +#ifndef HAVE_SSL_CTX_SET_CERT_CB
     +          /*
     +           * Without a certificate callback, the current implementation 
can't
    -+           * figure out if a certficate was actually requested, so 
"require" is
    ++           * figure out if a certificate was actually requested, so 
"require" is
     +           * useless.
     +           */
     +          if (strcmp(conn->sslcertmode, "require") == 0)
    @@ src/interfaces/libpq/fe-connect.c: connectOptions2(PGconn *conn)
        /*
         * validate gssencmode option
         */
    +@@ src/interfaces/libpq/fe-connect.c: freePGconn(PGconn *conn)
    +           explicit_bzero(conn->sslpassword, strlen(conn->sslpassword));
    +           free(conn->sslpassword);
    +   }
    ++  free(conn->sslcertmode);
    +   free(conn->sslrootcert);
    +   free(conn->sslcrl);
    +   free(conn->sslcrldir);
     
      ## src/interfaces/libpq/fe-secure-openssl.c ##
     @@ src/interfaces/libpq/fe-secure-openssl.c: verify_cb(int ok, 
X509_STORE_CTX *ctx)
    @@ src/test/ssl/t/001_ssltests.pl: $node->connect_ok(
     +  "$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require 
sslcertmode=require",
     +  "connect with sslcertmode=require fails without a client certificate",
     +  expected_stderr => $supports_sslcertmode_require
    -+          ? qr/server accepted connection without a valid certificate/
    ++          ? qr/server accepted connection without a valid SSL certificate/
     +          : qr/sslcertmode value "require" is not supported/);
     +
      # CRL tests
    @@ src/test/ssl/t/001_ssltests.pl: $node->connect_ok(
      $node->connect_fails(
        "$common_connstr user=ssltestuser sslcert=ssl/client.crt "
     
    + ## src/test/ssl/t/003_sslinfo.pl ##
    +@@ src/test/ssl/t/003_sslinfo.pl: my $SERVERHOSTADDR = '127.0.0.1';
    + # This is the pattern to use in pg_hba.conf to match incoming connections.
    + my $SERVERHOSTCIDR = '127.0.0.1/32';
    + 
    ++# Determine whether build supports sslcertmode=require.
    ++my $supports_sslcertmode_require =
    ++  check_pg_config("#define HAVE_SSL_CTX_SET_CERT_CB 1");
    ++
    + # Allocation of base connection string shared among multiple tests.
    + my $common_connstr;
    + 
    +@@ src/test/ssl/t/003_sslinfo.pl: $result = $node->safe_psql(
    +   connstr => $common_connstr);
    + is($result, 'CA:FALSE|t', 'extract extension from cert');
    + 
    ++# Sanity tests for sslcertmode, using ssl_client_cert_present()
    ++my @cases = (
    ++  { opts => "sslcertmode=allow",                                  present 
=> 't' },
    ++  { opts => "sslcertmode=allow sslcert=invalid",  present => 'f' },
    ++  { opts => "sslcertmode=disable",                                present 
=> 'f' },
    ++);
    ++if ($supports_sslcertmode_require)
    ++{
    ++  push(@cases, { opts => "sslcertmode=require",   present => 't' });
    ++}
    ++
    ++foreach my $c (@cases) {
    ++  $result = $node->safe_psql(
    ++          "trustdb",
    ++          "SELECT ssl_client_cert_present();",
    ++          connstr => "$common_connstr dbname=trustdb $c->{'opts'}"
    ++  );
    ++  is($result, $c->{'present'}, "ssl_client_cert_present() for 
$c->{'opts'}");
    ++}
    ++
    + done_testing();
    +
      ## src/tools/msvc/Solution.pm ##
     @@ src/tools/msvc/Solution.pm: sub GenerateFiles
                HAVE_SETPROCTITLE_FAST                   => undef,
2:  e2343c0089 ! 2:  c9ecdd54ea require_auth: decouple SASL and SCRAM
    @@ src/interfaces/libpq/fe-connect.c: connectOptions2(PGconn *conn)
     +                          free(part);
     +                          continue; /* avoid the bitmask manipulation 
below */
                        }
    -                   else if (strcmp(method, "creds") == 0)
    +                   else if (strcmp(method, "none") == 0)
                        {
     
      ## src/interfaces/libpq/libpq-int.h ##
From ad71dc8b727fc57548693946904de53c2be7defc Mon Sep 17 00:00:00 2001
From: Jacob Champion <jchamp...@timescale.com>
Date: Fri, 24 Jun 2022 15:40:42 -0700
Subject: [PATCH v18 1/2] Add sslcertmode option for client certificates

The sslcertmode option controls whether the server is allowed and/or
required to request a certificate from the client. There are three
modes:

- "allow" is the default and follows the current behavior -- a
  configured  sslcert is sent if the server requests one (which, with
  the current implementation, will happen whenever TLS is negotiated).

- "disable" causes the client to refuse to send a client certificate
  even if an sslcert is configured.

- "require" causes the client to fail if a client certificate is never
  sent and the server opens a connection anyway. This doesn't add any
  additional security, since there is no guarantee that the server is
  validating the certificate correctly, but it may help troubleshoot
  more complicated TLS setups.

sslcertmode=require needs the OpenSSL implementation to support
SSL_CTX_set_cert_cb(). Notably, LibreSSL does not.
---
 configure                                | 12 +++--
 configure.ac                             |  5 +-
 doc/src/sgml/libpq.sgml                  | 64 ++++++++++++++++++++++++
 meson.build                              |  3 +-
 src/include/pg_config.h.in               |  3 ++
 src/interfaces/libpq/fe-auth.c           | 19 +++++++
 src/interfaces/libpq/fe-connect.c        | 56 ++++++++++++++++++++-
 src/interfaces/libpq/fe-secure-openssl.c | 39 ++++++++++++++-
 src/interfaces/libpq/libpq-int.h         |  3 ++
 src/test/ssl/t/001_ssltests.pl           | 43 ++++++++++++++++
 src/test/ssl/t/003_sslinfo.pl            | 24 +++++++++
 src/tools/msvc/Solution.pm               |  9 ++++
 12 files changed, 269 insertions(+), 11 deletions(-)

diff --git a/configure b/configure
index e221dd5b0f..037e8ace54 100755
--- a/configure
+++ b/configure
@@ -12973,13 +12973,15 @@ else
 fi
 
   fi
-  # Function introduced in OpenSSL 1.0.2.
-  for ac_func in X509_get_signature_nid
+  # Functions introduced in OpenSSL 1.0.2. Note that LibreSSL doesn't have
+  # SSL_CTX_set_cert_cb().
+  for ac_func in X509_get_signature_nid SSL_CTX_set_cert_cb
 do :
-  ac_fn_c_check_func "$LINENO" "X509_get_signature_nid" "ac_cv_func_X509_get_signature_nid"
-if test "x$ac_cv_func_X509_get_signature_nid" = xyes; then :
+  as_ac_var=`$as_echo "ac_cv_func_$ac_func" | $as_tr_sh`
+ac_fn_c_check_func "$LINENO" "$ac_func" "$as_ac_var"
+if eval test \"x\$"$as_ac_var"\" = x"yes"; then :
   cat >>confdefs.h <<_ACEOF
-#define HAVE_X509_GET_SIGNATURE_NID 1
+#define `$as_echo "HAVE_$ac_func" | $as_tr_cpp` 1
 _ACEOF
 
 fi
diff --git a/configure.ac b/configure.ac
index 3aa6c15c13..f3ae7569d8 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1373,8 +1373,9 @@ if test "$with_ssl" = openssl ; then
      AC_SEARCH_LIBS(CRYPTO_new_ex_data, [eay32 crypto], [], [AC_MSG_ERROR([library 'eay32' or 'crypto' is required for OpenSSL])])
      AC_SEARCH_LIBS(SSL_new, [ssleay32 ssl], [], [AC_MSG_ERROR([library 'ssleay32' or 'ssl' is required for OpenSSL])])
   fi
-  # Function introduced in OpenSSL 1.0.2.
-  AC_CHECK_FUNCS([X509_get_signature_nid])
+  # Functions introduced in OpenSSL 1.0.2. Note that LibreSSL doesn't have
+  # SSL_CTX_set_cert_cb().
+  AC_CHECK_FUNCS([X509_get_signature_nid SSL_CTX_set_cert_cb])
   # Functions introduced in OpenSSL 1.1.0. We used to check for
   # OPENSSL_VERSION_NUMBER, but that didn't work with 1.1.0, because LibreSSL
   # defines OPENSSL_VERSION_NUMBER to claim version 2.0.0, even though it
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 9ee5532c07..d4cc470be7 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -1810,6 +1810,60 @@ postgresql://%2Fvar%2Flib%2Fpostgresql/dbname
       </listitem>
      </varlistentry>
 
+     <varlistentry id="libpq-connect-sslcertmode" xreflabel="sslcertmode">
+      <term><literal>sslcertmode</literal></term>
+      <listitem>
+       <para>
+        This option determines whether a client certificate may be sent to the
+        server, and whether the server is required to request one. There are
+        three modes:
+
+        <variablelist>
+         <varlistentry>
+          <term><literal>disable</literal></term>
+          <listitem>
+           <para>
+            a client certificate is never sent, even if one is provided via
+            <xref linkend="libpq-connect-sslcert" />
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>allow</literal> (default)</term>
+          <listitem>
+           <para>
+            a certificate may be sent, if the server requests one and the client
+            has one to send
+           </para>
+          </listitem>
+         </varlistentry>
+
+         <varlistentry>
+          <term><literal>require</literal></term>
+          <listitem>
+           <para>
+            the server <emphasis>must</emphasis> request a certificate. The
+            connection will fail if the client does not send a certificate and
+            the server successfully authenticates the client anyway.
+           </para>
+          </listitem>
+         </varlistentry>
+        </variablelist>
+       </para>
+
+       <note>
+        <para>
+         <literal>sslcertmode=require</literal> doesn't add any additional
+         security, since there is no guarantee that the server is validating the
+         certificate correctly; PostgreSQL servers generally request TLS
+         certificates from clients whether they validate them or not. The option
+         may be useful when troubleshooting more complicated TLS setups.
+        </para>
+       </note>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="libpq-connect-sslrootcert" xreflabel="sslrootcert">
       <term><literal>sslrootcert</literal></term>
       <listitem>
@@ -7986,6 +8040,16 @@ myEventProc(PGEventId evtId, void *evtInfo, void *passThrough)
      </para>
     </listitem>
 
+    <listitem>
+     <para>
+      <indexterm>
+       <primary><envar>PGSSLCERTMODE</envar></primary>
+      </indexterm>
+      <envar>PGSSLCERTMODE</envar> behaves the same as the <xref
+      linkend="libpq-connect-sslcertmode"/> connection parameter.
+     </para>
+    </listitem>
+
     <listitem>
      <para>
       <indexterm>
diff --git a/meson.build b/meson.build
index 7f76a101ec..b0bdc17b1d 100644
--- a/meson.build
+++ b/meson.build
@@ -1219,8 +1219,9 @@ if sslopt in ['auto', 'openssl']
       ['CRYPTO_new_ex_data', {'required': true}],
       ['SSL_new', {'required': true}],
 
-      # Function introduced in OpenSSL 1.0.2.
+      # Functions introduced in OpenSSL 1.0.2.
       ['X509_get_signature_nid'],
+      ['SSL_CTX_set_cert_cb'], # not in LibreSSL
 
       # Functions introduced in OpenSSL 1.1.0. We used to check for
       # OPENSSL_VERSION_NUMBER, but that didn't work with 1.1.0, because LibreSSL
diff --git a/src/include/pg_config.h.in b/src/include/pg_config.h.in
index 4882c70559..3665e799e7 100644
--- a/src/include/pg_config.h.in
+++ b/src/include/pg_config.h.in
@@ -394,6 +394,9 @@
 /* Define to 1 if you have spinlocks. */
 #undef HAVE_SPINLOCKS
 
+/* Define to 1 if you have the `SSL_CTX_set_cert_cb' function. */
+#undef HAVE_SSL_CTX_SET_CERT_CB
+
 /* Define to 1 if stdbool.h conforms to C99. */
 #undef HAVE_STDBOOL_H
 
diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index fa95f8e6e9..934e3f4f7c 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -798,6 +798,25 @@ check_expected_areq(AuthRequest areq, PGconn *conn)
 	StaticAssertDecl((sizeof(conn->allowed_auth_methods) * CHAR_BIT) > AUTH_REQ_MAX,
 					 "AUTH_REQ_MAX overflows the allowed_auth_methods bitmask");
 
+	if (conn->sslcertmode[0] == 'r' /* require */
+		&& areq == AUTH_REQ_OK)
+	{
+		/*
+		 * Trade off a little bit of complexity to try to get these error
+		 * messages as precise as possible.
+		 */
+		if (!conn->ssl_cert_requested)
+		{
+			libpq_append_conn_error(conn, "server did not request an SSL certificate");
+			return false;
+		}
+		else if (!conn->ssl_cert_sent)
+		{
+			libpq_append_conn_error(conn, "server accepted connection without a valid SSL certificate");
+			return false;
+		}
+	}
+
 	/*
 	 * If the user required a specific auth method, or specified an allowed
 	 * set, then reject all others here, and make sure the server actually
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index b9f899c552..15515fbd45 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -125,8 +125,10 @@ static int	ldapServiceLookup(const char *purl, PQconninfoOption *options,
 #define DefaultTargetSessionAttrs	"any"
 #ifdef USE_SSL
 #define DefaultSSLMode "prefer"
+#define DefaultSSLCertMode "allow"
 #else
 #define DefaultSSLMode	"disable"
+#define DefaultSSLCertMode "disable"
 #endif
 #ifdef ENABLE_GSS
 #include "fe-gssapi-common.h"
@@ -283,6 +285,10 @@ static const internalPQconninfoOption PQconninfoOptions[] = {
 		"SSL-Client-Key", "", 64,
 	offsetof(struct pg_conn, sslkey)},
 
+	{"sslcertmode", "PGSSLCERTMODE", NULL, NULL,
+		"SSL-Client-Cert-Mode", "", 8, /* sizeof("disable") == 8 */
+	offsetof(struct pg_conn, sslcertmode)},
+
 	{"sslpassword", NULL, NULL, NULL,
 		"SSL-Client-Key-Password", "*", 20,
 	offsetof(struct pg_conn, sslpassword)},
@@ -1457,8 +1463,8 @@ connectOptions2(PGconn *conn)
 			case 'r':			/* "require" */
 			case 'v':			/* "verify-ca" or "verify-full" */
 				conn->status = CONNECTION_BAD;
-				libpq_append_conn_error(conn, "sslmode value \"%s\" invalid when SSL support is not compiled in",
-										conn->sslmode);
+				libpq_append_conn_error(conn, "%s value \"%s\" invalid when SSL support is not compiled in",
+										"sslmode", conn->sslmode);
 				return false;
 		}
 #endif
@@ -1506,6 +1512,51 @@ connectOptions2(PGconn *conn)
 		return false;
 	}
 
+	/*
+	 * validate sslcertmode option
+	 */
+	if (conn->sslcertmode)
+	{
+		if (strcmp(conn->sslcertmode, "disable") != 0 &&
+			strcmp(conn->sslcertmode, "allow") != 0 &&
+			strcmp(conn->sslcertmode, "require") != 0)
+		{
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "invalid %s value: \"%s\"",
+									"sslcertmode", conn->sslcertmode);
+			return false;
+		}
+#ifndef USE_SSL
+		if (strcmp(conn->sslcertmode, "require") == 0)
+		{
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "%s value \"%s\" invalid when SSL support is not compiled in",
+									"sslcertmode", conn->sslcertmode);
+			return false;
+		}
+#endif
+#ifndef HAVE_SSL_CTX_SET_CERT_CB
+		/*
+		 * Without a certificate callback, the current implementation can't
+		 * figure out if a certificate was actually requested, so "require" is
+		 * useless.
+		 */
+		if (strcmp(conn->sslcertmode, "require") == 0)
+		{
+			conn->status = CONNECTION_BAD;
+			libpq_append_conn_error(conn, "sslcertmode value \"%s\" is not supported (check OpenSSL version)",
+									conn->sslcertmode);
+			return false;
+		}
+#endif
+	}
+	else
+	{
+		conn->sslcertmode = strdup(DefaultSSLCertMode);
+		if (!conn->sslcertmode)
+			goto oom_error;
+	}
+
 	/*
 	 * validate gssencmode option
 	 */
@@ -4238,6 +4289,7 @@ freePGconn(PGconn *conn)
 		explicit_bzero(conn->sslpassword, strlen(conn->sslpassword));
 		free(conn->sslpassword);
 	}
+	free(conn->sslcertmode);
 	free(conn->sslrootcert);
 	free(conn->sslcrl);
 	free(conn->sslcrldir);
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 6a4431ddfe..b88d9da3e2 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -462,6 +462,33 @@ verify_cb(int ok, X509_STORE_CTX *ctx)
 	return ok;
 }
 
+#ifdef HAVE_SSL_CTX_SET_CERT_CB
+/*
+ * Certificate selection callback
+ *
+ * This callback lets us choose the client certificate we send to the server
+ * after seeing its CertificateRequest. We only support sending a single
+ * hard-coded certificate via sslcert, so we don't actually set any certificates
+ * here; we just use it to record whether or not the server has actually asked
+ * for one and whether we have one to send.
+ */
+static int
+cert_cb(SSL *ssl, void *arg)
+{
+	PGconn *conn = arg;
+	conn->ssl_cert_requested = true;
+
+	/* Do we have a certificate loaded to send back? */
+	if (SSL_get_certificate(ssl))
+		conn->ssl_cert_sent = true;
+
+	/*
+	 * Tell OpenSSL that the callback succeeded; we're not required to actually
+	 * make any changes to the SSL handle.
+	 */
+	return 1;
+}
+#endif
 
 /*
  * OpenSSL-specific wrapper around
@@ -953,6 +980,11 @@ initialize_SSL(PGconn *conn)
 		SSL_CTX_set_default_passwd_cb_userdata(SSL_context, conn);
 	}
 
+#ifdef HAVE_SSL_CTX_SET_CERT_CB
+	/* Set up a certificate selection callback. */
+	SSL_CTX_set_cert_cb(SSL_context, cert_cb, conn);
+#endif
+
 	/* Disable old protocol versions */
 	SSL_CTX_set_options(SSL_context, SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
 
@@ -1107,7 +1139,12 @@ initialize_SSL(PGconn *conn)
 	else
 		fnbuf[0] = '\0';
 
-	if (fnbuf[0] == '\0')
+	if (conn->sslcertmode[0] == 'd') /* disable */
+	{
+		/* don't send a client cert even if we have one */
+		have_cert = false;
+	}
+	else if (fnbuf[0] == '\0')
 	{
 		/* no home directory, proceed without a client cert */
 		have_cert = false;
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index 1dc264fe54..f1f1d973cc 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -384,6 +384,7 @@ struct pg_conn
 	char	   *sslkey;			/* client key filename */
 	char	   *sslcert;		/* client certificate filename */
 	char	   *sslpassword;	/* client key file password */
+	char	   *sslcertmode;	/* client cert mode (require,allow,disable) */
 	char	   *sslrootcert;	/* root certificate filename */
 	char	   *sslcrl;			/* certificate revocation list filename */
 	char	   *sslcrldir;		/* certificate revocation list directory name */
@@ -527,6 +528,8 @@ struct pg_conn
 
 	/* SSL structures */
 	bool		ssl_in_use;
+	bool		ssl_cert_requested;	/* Did the server ask us for a cert? */
+	bool		ssl_cert_sent;		/* Did we send one in reply? */
 
 #ifdef USE_SSL
 	bool		allow_ssl_try;	/* Allowed to try SSL negotiation */
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 3094e27af3..3e5d30c37d 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -42,6 +42,10 @@ my $SERVERHOSTADDR = '127.0.0.1';
 # This is the pattern to use in pg_hba.conf to match incoming connections.
 my $SERVERHOSTCIDR = '127.0.0.1/32';
 
+# Determine whether build supports sslcertmode=require.
+my $supports_sslcertmode_require =
+  check_pg_config("#define HAVE_SSL_CTX_SET_CERT_CB 1");
+
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
 
@@ -191,6 +195,22 @@ $node->connect_ok(
 	"$common_connstr sslrootcert=ssl/both-cas-2.crt sslmode=verify-ca",
 	"cert root file that contains two certificates, order 2");
 
+# sslcertmode=allow and =disable should both work without a client certificate.
+$node->connect_ok(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=disable",
+	"connect with sslcertmode=disable");
+$node->connect_ok(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=allow",
+	"connect with sslcertmode=allow");
+
+# sslcertmode=require, however, should fail.
+$node->connect_fails(
+	"$common_connstr sslrootcert=ssl/root+server_ca.crt sslmode=require sslcertmode=require",
+	"connect with sslcertmode=require fails without a client certificate",
+	expected_stderr => $supports_sslcertmode_require
+		? qr/server accepted connection without a valid SSL certificate/
+		: qr/sslcertmode value "require" is not supported/);
+
 # CRL tests
 
 # Invalid CRL filename is the same as no CRL, succeeds
@@ -538,6 +558,29 @@ $node->connect_ok(
 	"certificate authorization succeeds with correct client cert in encrypted DER format"
 );
 
+# correct client cert with required/allowed certificate authentication
+if ($supports_sslcertmode_require)
+{
+	$node->connect_ok(
+		"$common_connstr user=ssltestuser sslcertmode=require sslcert=ssl/client.crt "
+		  . sslkey('client.key'),
+		"certificate authorization succeeds with sslcertmode=require"
+	);
+}
+$node->connect_ok(
+	"$common_connstr user=ssltestuser sslcertmode=allow sslcert=ssl/client.crt "
+	  . sslkey('client.key'),
+	"certificate authorization succeeds with sslcertmode=allow"
+);
+
+# client cert isn't sent if certificate authentication is disabled
+$node->connect_fails(
+	"$common_connstr user=ssltestuser sslcertmode=disable sslcert=ssl/client.crt "
+	  . sslkey('client.key'),
+	"certificate authorization fails with sslcertmode=disable",
+	expected_stderr => qr/connection requires a valid client certificate/
+);
+
 # correct client cert in encrypted PEM with wrong password
 $node->connect_fails(
 	"$common_connstr user=ssltestuser sslcert=ssl/client.crt "
diff --git a/src/test/ssl/t/003_sslinfo.pl b/src/test/ssl/t/003_sslinfo.pl
index 3f498fff70..e63256a3b8 100644
--- a/src/test/ssl/t/003_sslinfo.pl
+++ b/src/test/ssl/t/003_sslinfo.pl
@@ -43,6 +43,10 @@ my $SERVERHOSTADDR = '127.0.0.1';
 # This is the pattern to use in pg_hba.conf to match incoming connections.
 my $SERVERHOSTCIDR = '127.0.0.1/32';
 
+# Determine whether build supports sslcertmode=require.
+my $supports_sslcertmode_require =
+  check_pg_config("#define HAVE_SSL_CTX_SET_CERT_CB 1");
+
 # Allocation of base connection string shared among multiple tests.
 my $common_connstr;
 
@@ -166,4 +170,24 @@ $result = $node->safe_psql(
 	connstr => $common_connstr);
 is($result, 'CA:FALSE|t', 'extract extension from cert');
 
+# Sanity tests for sslcertmode, using ssl_client_cert_present()
+my @cases = (
+	{ opts => "sslcertmode=allow",					present => 't' },
+	{ opts => "sslcertmode=allow sslcert=invalid",	present => 'f' },
+	{ opts => "sslcertmode=disable",				present => 'f' },
+);
+if ($supports_sslcertmode_require)
+{
+	push(@cases, { opts => "sslcertmode=require",	present => 't' });
+}
+
+foreach my $c (@cases) {
+	$result = $node->safe_psql(
+		"trustdb",
+		"SELECT ssl_client_cert_present();",
+		connstr => "$common_connstr dbname=trustdb $c->{'opts'}"
+	);
+	is($result, $c->{'present'}, "ssl_client_cert_present() for $c->{'opts'}");
+}
+
 done_testing();
diff --git a/src/tools/msvc/Solution.pm b/src/tools/msvc/Solution.pm
index b59953e5b5..153be7be11 100644
--- a/src/tools/msvc/Solution.pm
+++ b/src/tools/msvc/Solution.pm
@@ -327,6 +327,7 @@ sub GenerateFiles
 		HAVE_SETPROCTITLE_FAST                   => undef,
 		HAVE_SOCKLEN_T                           => 1,
 		HAVE_SPINLOCKS                           => 1,
+		HAVE_SSL_CTX_SET_CERT_CB                 => undef,
 		HAVE_STDBOOL_H                           => 1,
 		HAVE_STDINT_H                            => 1,
 		HAVE_STDLIB_H                            => 1,
@@ -506,6 +507,14 @@ sub GenerateFiles
 			$define{HAVE_HMAC_CTX_NEW}          = 1;
 			$define{HAVE_OPENSSL_INIT_SSL}      = 1;
 		}
+
+		# Symbols needed with OpenSSL 1.0.2 and above.
+		if (   ($digit1 >= '3' && $digit2 >= '0' && $digit3 >= '0')
+			|| ($digit1 >= '1' && $digit2 >= '1' && $digit3 >= '0')
+			|| ($digit1 >= '1' && $digit2 >= '0' && $digit3 >= '2'))
+		{
+			$define{HAVE_SSL_CTX_SET_CERT_CB} = 1;
+		}
 	}
 
 	$self->GenerateConfigHeader('src/include/pg_config.h',     \%define, 1);
-- 
2.25.1

From c9ecdd54eaa7a9dd9e9eb34ea1fbafcd22a7c2bc Mon Sep 17 00:00:00 2001
From: Jacob Champion <jchamp...@timescale.com>
Date: Tue, 18 Oct 2022 16:55:36 -0700
Subject: [PATCH v18 2/2] require_auth: decouple SASL and SCRAM

Rather than assume that an AUTH_REQ_SASL* code refers to SCRAM-SHA-256,
future-proof by separating the single allowlist into a list of allowed
authentication request codes and a list of allowed SASL mechanisms.

The require_auth check is now separated into two tiers. The
AUTH_REQ_SASL* codes are always allowed. If the server sends one,
responsibility for the check then falls to pg_SASL_init(), which
compares the selected mechanism against the list of allowed mechanisms.
(Other SCRAM code is already responsible for rejecting unexpected or
out-of-order AUTH_REQ_SASL_* codes, so that's not explicitly handled
with this addition.)

Since there's only one recognized SASL mechanism, conn->sasl_mechs
currently only points at static hardcoded lists. Whenever a second
mechanism is added, the list will need to be managed dynamically.
---
 src/interfaces/libpq/fe-auth.c            | 34 +++++++++++++++++++
 src/interfaces/libpq/fe-connect.c         | 41 +++++++++++++++++++----
 src/interfaces/libpq/libpq-int.h          |  3 +-
 src/test/authentication/t/001_password.pl | 14 +++++---
 src/test/ssl/t/002_scram.pl               |  6 ++++
 5 files changed, 86 insertions(+), 12 deletions(-)

diff --git a/src/interfaces/libpq/fe-auth.c b/src/interfaces/libpq/fe-auth.c
index 934e3f4f7c..7ce0e16b18 100644
--- a/src/interfaces/libpq/fe-auth.c
+++ b/src/interfaces/libpq/fe-auth.c
@@ -522,6 +522,40 @@ pg_SASL_init(PGconn *conn, int payloadlen)
 		goto error;
 	}
 
+	/*
+	 * Before going ahead with any SASL exchange, ensure that the user has
+	 * allowed (or, alternatively, has not forbidden) this particular mechanism.
+	 *
+	 * In a hypothetical future where a server responds with multiple SASL
+	 * mechanism families, we would need to instead consult this list up above,
+	 * during mechanism negotiation. We don't live in that world yet. The server
+	 * presents one auth method and we decide whether that's acceptable or not.
+	 */
+	if (conn->sasl_mechs)
+	{
+		const char **mech;
+		bool		found = false;
+
+		Assert(conn->require_auth);
+
+		for (mech = conn->sasl_mechs; *mech; mech++)
+		{
+			if (strcmp(*mech, selected_mechanism) == 0)
+			{
+				found = true;
+				break;
+			}
+		}
+
+		if ((conn->sasl_mechs_denied && found)
+			|| (!conn->sasl_mechs_denied && !found))
+		{
+			libpq_append_conn_error(conn, "auth method \"%s\" requirement failed: server requested unacceptable SASL mechanism \"%s\"",
+									conn->require_auth, selected_mechanism);
+			goto error;
+		}
+	}
+
 	if (conn->channel_binding[0] == 'r' &&	/* require */
 		strcmp(selected_mechanism, SCRAM_SHA_256_PLUS_NAME) != 0)
 	{
diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c
index 15515fbd45..c90a23fd2a 100644
--- a/src/interfaces/libpq/fe-connect.c
+++ b/src/interfaces/libpq/fe-connect.c
@@ -1258,12 +1258,25 @@ connectOptions2(PGconn *conn)
 					more;
 		bool		negated = false;
 
+		static const uint32 default_methods = (
+			1 << AUTH_REQ_SASL
+			| 1 << AUTH_REQ_SASL_CONT
+			| 1 << AUTH_REQ_SASL_FIN
+		);
+		static const char *no_mechs[] = { NULL };
+
 		/*
-		 * By default, start from an empty set of allowed options and add to
+		 * By default, start from a minimum set of allowed options and add to
 		 * it.
+		 *
+		 * NB: The SASL method codes are always "allowed" here. If the server
+		 * requests SASL auth, pg_SASL_init() will enforce adherence to the
+		 * sasl_mechs list, which by default is empty.
 		 */
 		conn->auth_required = true;
-		conn->allowed_auth_methods = 0;
+		conn->allowed_auth_methods = default_methods;
+		conn->sasl_mechs = no_mechs;
+		conn->sasl_mechs_denied = false;
 
 		for (first = true, more = true; more; first = false)
 		{
@@ -1290,6 +1303,9 @@ connectOptions2(PGconn *conn)
 					 */
 					conn->auth_required = false;
 					conn->allowed_auth_methods = -1;
+
+					/* conn->sasl_mechs is now a list of denied mechanisms. */
+					conn->sasl_mechs_denied = true;
 				}
 				else if (!negated)
 				{
@@ -1334,10 +1350,23 @@ connectOptions2(PGconn *conn)
 			}
 			else if (strcmp(method, "scram-sha-256") == 0)
 			{
-				/* This currently assumes that SCRAM is the only SASL method. */
-				bits = (1 << AUTH_REQ_SASL);
-				bits |= (1 << AUTH_REQ_SASL_CONT);
-				bits |= (1 << AUTH_REQ_SASL_FIN);
+				static const char *scram_mechs[] = {
+					SCRAM_SHA_256_NAME,
+					SCRAM_SHA_256_PLUS_NAME,
+					NULL /* list terminator */
+				};
+
+				/*
+				 * This currently assumes that SCRAM is the only SASL method.
+				 * Once a second mechanism is added, this code will need to add
+				 * to the list instead of replacing it wholesale.
+				 */
+				if (conn->sasl_mechs[0])
+					goto duplicate;
+				conn->sasl_mechs = scram_mechs;
+
+				free(part);
+				continue; /* avoid the bitmask manipulation below */
 			}
 			else if (strcmp(method, "none") == 0)
 			{
diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h
index f1f1d973cc..ab26292586 100644
--- a/src/interfaces/libpq/libpq-int.h
+++ b/src/interfaces/libpq/libpq-int.h
@@ -465,7 +465,8 @@ struct pg_conn
 										 * codes */
 	bool		client_finished_auth;	/* have we finished our half of the
 										 * authentication exchange? */
-
+	const char **sasl_mechs;	/* list of allowed/denied SASL mechanisms */
+	bool		sasl_mechs_denied;	/* is the sasl_mechs list forbidden? */
 
 	/* Transient state needed while establishing connection */
 	PGTargetServerType target_server_type;	/* desired session properties */
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index cba5d7d648..015532893c 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -301,30 +301,34 @@ $node->connect_fails(
 	"user=scram_role require_auth=password",
 	"password authentication required, fails with SCRAM auth",
 	expected_stderr =>
-	  qr/auth method "password" requirement failed: server requested SASL authentication/
+	  qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/
 );
 $node->connect_fails(
 	"user=scram_role require_auth=md5",
 	"md5 authentication required, fails with SCRAM auth",
 	expected_stderr =>
-	  qr/auth method "md5" requirement failed: server requested SASL authentication/
+	  qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/
 );
 $node->connect_fails(
 	"user=scram_role require_auth=none",
 	"all authentication forbidden, fails with SCRAM auth",
 	expected_stderr =>
-	  qr/auth method "none" requirement failed: server requested SASL authentication/
+	  qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/
 );
 
 # Authentication fails if SCRAM authentication is forbidden.
 $node->connect_fails(
 	"user=scram_role require_auth=!scram-sha-256",
 	"SCRAM authentication forbidden, fails with SCRAM auth",
-	expected_stderr => qr/server requested SASL authentication/);
+	expected_stderr =>
+	  qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/
+);
 $node->connect_fails(
 	"user=scram_role require_auth=!password,!md5,!scram-sha-256",
 	"multiple authentication types forbidden, fails with SCRAM auth",
-	expected_stderr => qr/server requested SASL authentication/);
+	expected_stderr =>
+	  qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256"/
+);
 
 # Test that bad passwords are rejected.
 $ENV{"PGPASSWORD"} = 'badpass';
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index 8038135697..173ac8d86b 100644
--- a/src/test/ssl/t/002_scram.pl
+++ b/src/test/ssl/t/002_scram.pl
@@ -157,6 +157,12 @@ if ($supports_tls_server_end_point)
 		"$common_connstr user=ssltestuser channel_binding=require require_auth=scram-sha-256",
 		"SCRAM with SSL, channel_binding=require, and require_auth=scram-sha-256"
 	);
+	$node->connect_fails(
+		"$common_connstr user=ssltestuser channel_binding=require require_auth=password",
+		"SCRAM with SSL, channel_binding=require, and require_auth=password",
+		expected_stderr =>
+		  qr/server requested unacceptable SASL mechanism "SCRAM-SHA-256-PLUS"/
+	);
 }
 else
 {
-- 
2.25.1

Reply via email to