Thanks for the detailed review and the SPIFFE perspective.  v3 addresses all of 
it


On 26/09/2026 01:19, Agustín Martínez Fayó wrote:
+$node->connect_fails(
+    "$uri_connstr user=ssltestuser sslcert=ssl/client-uri-multi.crt "
+      . sslkey('client-uri-multi.key'),
+    "certificate authorization fails when client certificate has multiple URI 
SANs",
+    expected_stderr =>
+      qr/certificate authentication failed for user "ssltestuser"/);
I think this does not exercise the count check. client-uri-multi.config
lists the non-matching URI first and the code keeps the first URI, so
without the check the connection is authenticated as
spiffe://.../not-the-user and then fails on the usermap, which produces
the same client error. I deleted the check and the suite still passes
286/286. I wonder if putting the matching URI first in the fixture, or
asserting the server log reason with log_like, would make the test fail
for the right cause.

Ok changed, now it lists the matching URI first and asserts the
server-side "exactly one URI SAN" message, so the test fails for the
right reason if the count check is removed.

+        /* fail closed if the URI could not be converted */
+        if (port->peer_uri_count > 0 && port->peer_uri == NULL)
+            port->peer_uri_count = 0;
This seems to log a COMMERROR while the handshake still completes. I
signed a certificate with the suite's client CA whose single URI SAN
contains a null byte. On a clientname=CN line the log shows "URI subject
alternative name contains embedded null" and the connection is then
authenticated, with SELECT system_user returning cert:CN=ssltestuser. On
a clientname=URI line the same log line is followed by the single-URI
rejection message, which points at the wrong cause. Returning false
here, as the CN path does for an embedded null, would also reject such
certificates on CN and DN lines, which never looked at URI SANs before
this patch. I think that is a reasonable price for consistency, but the
alternative of failing only on URI lines with an accurate error message
would work too.

The extraction now happens in CheckCertAuth(), only for
clientname=URI lines, so CN/DN lines never look at URI SANs.  A URI
that cannot be represented as a string logs the precise cause and
fails with its own error message, instead of the misleading
"exactly one URI SAN" message.

+    if (X509_NAME_print_ex(bio, x509name, 0, XN_FLAG_RFC2253) == -1 ||
+        BIO_get_mem_ptr(bio, &bio_buf) < 0)
I wonder whether relaxing this from <= 0 was needed. With OpenSSL 3.x
and 1.1.1, X509_NAME_print_ex returns 0 for an empty name and
BIO_get_mem_ptr still returns 1. If BIO_get_mem_ptr ever returned 0,
bio_buf would stay NULL for the dereference below, so the original check
seems safer to me.

Done, BIO_get_mem_ptr() is back to <= 0.

+       contain exactly one URI subject alternative name.  The comparison is
+       case-sensitive and uses the exact URI as it appears in the
+       certificate.
Minor. Since the URI is matched as an opaque string, I wonder if a
sentence advising that regular-expression maps anchor the full scheme
and authority would help avoid patterns that match a URI from an
unexpected authority.
+       Note that PostgreSQL does not verify any relationship between a
+       certificate's URI and the certificate authority that signed the
+       certificate.  If <literal>ssl_ca_file</literal> contains certificates
+       for more than one trust domain, any of them can issue a certificate
+       for a URI in another trust domain.  For SPIFFE deployments, configure
Minor. This uses "trust domain" before SPIFFE is introduced. Maybe it
could say that any CA in the file can issue a certificate carrying any
URI, and keep the trust domain wording for the SPIFFE sentence that
follows.

Changed to advise anchoring regex maps to the scheme and authority,
introduce "trust domain" only in the SPIFFE sentence, reference
clientname from the CN-only descriptions, and include a worked
pg_hba.conf/pg_ident.conf example.

Cheers,
Florin

--
Florin Irion

www.enterprisedb.com





From 0d8f844328ca5ca2c287e15057ce673965760b33 Mon Sep 17 00:00:00 2001
From: Florin Irion <[email protected]>
Date: Thu, 17 Sep 2026 00:06:27 +0200
Subject: [PATCH v3] libpq: Support URI SANs in certificate authentication

Allow certificate authentication to use a URI subject alternative name
as the client identity via clientname=URI in pg_hba.conf.

With clientname=URI, the username is matched against the URI SAN of
the client certificate, which must carry exactly one URI SAN, keeping
the authenticated identity unambiguous.  The matched URI, rather than
the Subject DN, is recorded as the authenticated identity in the
connection log.  URI SANs are extracted only for clientname=URI
connections, and a URI that cannot be represented as a string, such as
one containing an embedded null byte, is rejected with its own error
message.

Client certificates with an empty subject are accepted when a URI SAN
is present, as issued by Istio's istiod for example.  clientname=CN
and clientname=DN still reject such certificates, since there is no
name to compare.

The documentation describes clientname=URI, including the single URI
SAN requirement, the caveat that ssl_ca_file should hold a single
trust domain's CA bundle for SPIFFE deployments, and an example
configuration.  Places that previously described cn (Common Name) as
the only comparable certificate field now reference the clientname
option.

TAP coverage is added for URI SAN matching: success with a single URI
SAN (identity logged as the URI), rejection when the certificate has no
URI SAN, rejection with multiple URI SANs, and success with an
empty-subject certificate.
---
 doc/src/sgml/client-auth.sgml                 |  57 +++-
 doc/src/sgml/runtime.sgml                     |  13 +-
 src/backend/libpq/auth.c                      |  70 +++--
 src/backend/libpq/be-secure-openssl.c         | 246 +++++++++++++-----
 src/backend/libpq/hba.c                       |   4 +
 src/include/libpq/hba.h                       |   1 +
 src/include/libpq/libpq-be.h                  |   3 +
 src/test/ssl/conf/client-uri-multi.config     |  22 ++
 src/test/ssl/conf/client-uri-nosubject.config |  15 ++
 src/test/ssl/conf/client-uri.config           |  17 ++
 src/test/ssl/ssl/client-uri-multi.crt         |  22 ++
 src/test/ssl/ssl/client-uri-multi.key         |  28 ++
 src/test/ssl/ssl/client-uri-nosubject.crt     |  20 ++
 src/test/ssl/ssl/client-uri-nosubject.key     |  28 ++
 src/test/ssl/ssl/client-uri.crt               |  21 ++
 src/test/ssl/ssl/client-uri.key               |  28 ++
 src/test/ssl/sslfiles.mk                      |  11 +-
 src/test/ssl/t/001_ssltests.pl                |  54 ++++
 src/test/ssl/t/SSL/Backend/OpenSSL.pm         |   3 +-
 src/test/ssl/t/SSL/Server.pm                  |   6 +-
 20 files changed, 569 insertions(+), 100 deletions(-)
 create mode 100644 src/test/ssl/conf/client-uri-multi.config
 create mode 100644 src/test/ssl/conf/client-uri-nosubject.config
 create mode 100644 src/test/ssl/conf/client-uri.config
 create mode 100644 src/test/ssl/ssl/client-uri-multi.crt
 create mode 100644 src/test/ssl/ssl/client-uri-multi.key
 create mode 100644 src/test/ssl/ssl/client-uri-nosubject.crt
 create mode 100644 src/test/ssl/ssl/client-uri-nosubject.key
 create mode 100644 src/test/ssl/ssl/client-uri.crt
 create mode 100644 src/test/ssl/ssl/client-uri.key

diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index e4e65f8feb..c776bb2a89 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -680,8 +680,9 @@ include_dir         <replaceable>directory</replaceable>
        <literal>verify-full</literal>. Both options require the client
        to present a valid (trusted) SSL certificate, while
        <literal>verify-full</literal> additionally enforces that the
-       <literal>cn</literal> (Common Name) in the certificate matches
-       the username or an applicable mapping.
+       username or an applicable mapping matches the certificate field
+       selected by <literal>clientname</literal>, <literal>cn</literal>
+       (Common Name) by default.
        This behavior is similar to the <literal>cert</literal> authentication
        method (see <xref linkend="auth-cert"/>) but enables pairing
        the verification of client certificates with any authentication
@@ -693,11 +694,20 @@ include_dir         <replaceable>directory</replaceable>
        using the <literal>clientcert</literal> option), you can specify
        which part of the client certificate credentials to match using
        the <literal>clientname</literal> option. This option can have one
-       of two values. If you specify <literal>clientname=CN</literal>, which
+       of three values. If you specify <literal>clientname=CN</literal>, which
        is the default, the username is matched against the certificate's
        <literal>Common Name (CN)</literal>. If instead you specify
        <literal>clientname=DN</literal> the username is matched against the
        entire <literal>Distinguished Name (DN)</literal> of the certificate.
+       Finally, if you specify <literal>clientname=URI</literal>, the username
+       is matched against the certificate's URI
+       <literal>Subject Alternative Name</literal>; the certificate must
+       contain exactly one URI subject alternative name.  The comparison is
+       case-sensitive and uses the exact URI as it appears in the
+       certificate.  When using regular expression user name maps with
+       <literal>clientname=URI</literal>, anchor the pattern to the URI's
+       scheme and authority so that it cannot match a URI from a different
+       authority.
        This option is probably best used in conjunction with a username map.
        The comparison is done with the <literal>DN</literal> in
        <ulink url="https://datatracker.ietf.org/doc/html/rfc2253";>RFC 
2253</ulink>
@@ -709,6 +719,35 @@ openssl x509 -in myclient.crt -noout -subject -nameopt 
RFC2253 | sed "s/^subject
         Care needs to be taken when using this option, especially when using
         regular expression matching against the <literal>DN</literal>.
       </para>
+
+      <para>
+       With <literal>clientname=URI</literal>, the matched URI (rather than
+       the Subject DN) is recorded as the authenticated identity, and appears
+       in the <literal>connection authenticated: identity=...</literal> log
+       line.
+      </para>
+
+      <para>
+       Note that PostgreSQL does not verify any relationship between a
+       certificate's URI and the certificate authority that signed the
+       certificate.  If <literal>ssl_ca_file</literal> contains certificates
+       from more than one CA, any of them can issue a certificate carrying
+       any URI.  For SPIFFE deployments, configure
+       <literal>ssl_ca_file</literal> with a single trust domain's CA bundle;
+       SPIFFE federation is not supported.
+      </para>
+
+      <para>
+       For example, this <filename>pg_hba.conf</filename> entry authenticates
+       users by SPIFFE ID:
+<programlisting>
+hostssl mydb appuser 192.168.1.0/24 cert clientname=URI map=spiffe-map
+</programlisting>
+       with a corresponding entry in <filename>pg_ident.conf</filename>:
+<programlisting>
+spiffe-map spiffe://example.org/ns/default/sa/appuser appuser
+</programlisting>
+      </para>
      </listitem>
     </varlistentry>
 
@@ -2093,11 +2132,13 @@ host ... ldap ldapbasedn="dc=example,dc=net"
     see <xref linkend="ssl-openssl-config"/> for SSL configuration 
instructions.
     When using this authentication method, the server will require that
     the client provide a valid, trusted certificate.  No password prompt
-    will be sent to the client.  The <literal>cn</literal> (Common Name)
-    attribute of the certificate
-    will be compared to the requested database user name, and if they match
-    the login will be allowed.  User name mapping can be used to allow
-    <literal>cn</literal> to be different from the database user name.
+    will be sent to the client.  By default, the <literal>cn</literal>
+    (Common Name) attribute of the certificate will be compared to the
+    requested database user name, and if they match the login will be
+    allowed.  The <literal>clientname</literal> option selects which part
+    of the certificate is compared, as described in
+    <xref linkend="auth-pg-hba-conf"/>.  User name mapping can be used to
+    allow <literal>cn</literal> to be different from the database user name.
    </para>
 
    <para>
diff --git a/doc/src/sgml/runtime.sgml b/doc/src/sgml/runtime.sgml
index d9984910cc..7a8a2f64e9 100644
--- a/doc/src/sgml/runtime.sgml
+++ b/doc/src/sgml/runtime.sgml
@@ -2366,7 +2366,9 @@ pg_dumpall -p 5432 | psql -d postgres -p 5433
    certificate authorities. If <literal>clientcert=verify-full</literal>
    is specified, the server will not only verify the certificate
    chain, but it will also check whether the username or its mapping
-   matches the <literal>cn</literal> (Common Name) of the provided certificate.
+   matches the certificate field selected by <literal>clientname</literal>
+   (see <xref linkend="auth-pg-hba-conf"/>), <literal>cn</literal>
+   (Common Name) by default.
    Note that certificate chain validation is always ensured when the
    <literal>cert</literal> authentication method is used
    (see <xref linkend="auth-cert"/>).
@@ -2402,7 +2404,9 @@ pg_dumpall -p 5432 | psql -d postgres -p 5433
    (It is not necessary to specify any <literal>clientcert</literal> options
    explicitly when using the <literal>cert</literal> authentication method.)
    In this case, the <literal>cn</literal> (Common Name) provided in
-   the certificate is checked against the user name or an applicable mapping.
+   the certificate is checked against the user name or an applicable mapping;
+   the <literal>clientname</literal> option selects which certificate field
+   is compared (see <xref linkend="auth-pg-hba-conf"/>).
   </para>
 
   <para>
@@ -2411,8 +2415,9 @@ pg_dumpall -p 5432 | psql -d postgres -p 5433
    <literal>clientcert</literal> authentication option to 
<literal>verify-ca</literal>
    or <literal>verify-full</literal>. The former option only enforces that
    the certificate is valid, while the latter also ensures that the
-   <literal>cn</literal> (Common Name) in the certificate matches
-   the user name or an applicable mapping.
+   user name or an applicable mapping matches the certificate field
+   selected by <literal>clientname</literal> (see
+   <xref linkend="auth-pg-hba-conf"/>).
   </para>
   </sect2>
 
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 12bf153d66..36ea8e5780 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -2746,6 +2746,30 @@ CheckCertAuth(Port *port)
                        break;
                case clientCertCN:
                        peer_username = port->peer_cn;
+                       break;
+               case clientCertURI:
+                       /* the certificate must contain exactly one URI SAN */
+                       be_tls_get_peer_uri(port);
+                       if (port->peer_uri_count != 1)
+                       {
+                               ereport(LOG,
+                                               (errmsg("certificate 
authentication failed for user \"%s\": client certificate must contain exactly 
one URI subject alternative name",
+                                                               
port->user_name)));
+                               return STATUS_ERROR;
+                       }
+                       if (port->peer_uri == NULL)
+                       {
+                               /*
+                                * the URI SAN is invalid; the reason has 
already been
+                                * reported
+                                */
+                               ereport(LOG,
+                                               (errmsg("certificate 
authentication failed for user \"%s\": client certificate's URI subject 
alternative name is invalid",
+                                                               
port->user_name)));
+                               return STATUS_ERROR;
+                       }
+                       peer_username = port->peer_uri;
+                       break;
        }
 
        /* Make sure we have received a username in the certificate */
@@ -2761,29 +2785,37 @@ CheckCertAuth(Port *port)
        if (port->hba->auth_method == uaCert)
        {
                /*
-                * For cert auth, the client's Subject DN is always our 
authenticated
-                * identity, even if we're only using its CN for authorization. 
 Set
-                * it now, rather than waiting for check_usermap() below, 
because
+                * For cert auth, the client's Subject DN is our authenticated
+                * identity, except when clientname=URI, in which case the URI 
SAN is.
+                * Set it now, rather than waiting for check_usermap() below, 
because
                 * authentication has already succeeded and we want the log 
file to
                 * reflect that.
                 */
-               if (!port->peer_dn)
+               if (port->hba->clientcertname == clientCertURI)
+                       set_authn_id(port, peer_username);
+               else
                {
-                       /*
-                        * This should not happen as both peer_dn and peer_cn 
should be
-                        * set in this context.
-                        */
-                       ereport(LOG,
-                                       (errmsg("certificate authentication 
failed for user \"%s\": unable to retrieve subject DN",
-                                                       port->user_name)));
-                       return STATUS_ERROR;
-               }
+                       if (!port->peer_dn)
+                       {
+                               /*
+                                * This should not happen as both peer_dn and 
peer_cn should
+                                * be set in this context.
+                                */
+                               ereport(LOG,
+                                               (errmsg("certificate 
authentication failed for user \"%s\": unable to retrieve subject DN",
+                                                               
port->user_name)));
+                               return STATUS_ERROR;
+                       }
 
-               set_authn_id(port, port->peer_dn);
+                       set_authn_id(port, port->peer_dn);
+               }
        }
 
-       /* Just pass the certificate cn/dn to the usermap check */
-       status_check_usermap = check_usermap(port->hba->usermap, 
port->user_name, peer_username, false);
+       /* Just pass the certificate cn/dn/uri to the usermap check */
+       status_check_usermap = check_usermap(port->hba->usermap,
+                                                                               
 port->user_name,
+                                                                               
 peer_username,
+                                                                               
 false);
        if (status_check_usermap != STATUS_OK)
        {
                /*
@@ -2804,6 +2836,12 @@ CheckCertAuth(Port *port)
                                        ereport(LOG,
                                                        (errmsg("certificate 
validation (clientcert=verify-full) failed for user \"%s\": CN mismatch",
                                                                        
port->user_name)));
+                                       break;
+                               case clientCertURI:
+                                       ereport(LOG,
+                                                       (errmsg("certificate 
validation (clientcert=verify-full) failed for user \"%s\": URI SAN mismatch",
+                                                                       
port->user_name)));
+                                       break;
                        }
                }
        }
diff --git a/src/backend/libpq/be-secure-openssl.c 
b/src/backend/libpq/be-secure-openssl.c
index b7ded8a025..0501531861 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -106,6 +106,8 @@ static int  sni_clienthello_cb(SSL *ssl, int *al, void 
*arg);
 #endif
 
 static char *X509_NAME_to_cstring(const X509_NAME *name);
+static char *X509_URI_to_cstring(const ASN1_STRING *uri);
+static bool be_tls_extract_peer_cert_names(Port *port, X509 *peer);
 
 static SSL_CTX *SSL_context = NULL;
 static MemoryContext SSL_hosts_memcxt = NULL;
@@ -1072,84 +1074,105 @@ aloop:
        /* Get client certificate, if available. */
        port->peer = SSL_get_peer_certificate(port->ssl);
 
-       /* and extract the Common Name and Distinguished Name from it. */
+       /* extract the Common Name, Distinguished Name, and URI SAN from it */
        port->peer_cn = NULL;
        port->peer_dn = NULL;
+       port->peer_uri = NULL;
+       port->peer_uri_count = 0;
        port->peer_cert_valid = false;
+
        if (port->peer != NULL)
        {
-               int                     len;
-               const X509_NAME *x509name = X509_get_subject_name(port->peer);
-               char       *peer_dn;
-               BIO                *bio = NULL;
-               BUF_MEM    *bio_buf = NULL;
-               int                     index;
-
-               index = X509_NAME_get_index_by_NID(unconstify(X509_NAME *, 
x509name), NID_commonName, -1);
-               if (index >= 0)
-               {
-                       const X509_NAME_ENTRY *entry;
-                       const ASN1_STRING *peer_cn_asn1;
-                       const unsigned char *peer_cn_internal;
-                       char       *peer_cn;
-
-                       entry = X509_NAME_get_entry(unconstify(X509_NAME *, 
x509name), index);
-                       peer_cn_asn1 = X509_NAME_ENTRY_get_data(entry);
-                       len = ASN1_STRING_length(peer_cn_asn1);
-                       peer_cn_internal = ASN1_STRING_get0_data(peer_cn_asn1);
+               if (!be_tls_extract_peer_cert_names(port, port->peer))
+                       return -1;
+               port->peer_cert_valid = true;
+       }
 
-                       peer_cn = MemoryContextAlloc(TopMemoryContext, len + 1);
-                       memcpy(peer_cn, peer_cn_internal, len);
-                       peer_cn[len] = '\0';
+       return 0;
+}
 
-                       /*
-                        * Reject embedded NULLs in certificate common name to 
prevent
-                        * attacks like CVE-2009-4034.
-                        */
-                       if (len != strlen(peer_cn))
-                       {
-                               ereport(COMMERROR,
-                                               
(errcode(ERRCODE_PROTOCOL_VIOLATION),
-                                                errmsg("SSL certificate's 
common name contains embedded null")));
-                               pfree(peer_cn);
-                               return -1;
-                       }
+/*
+ * Extract the Common Name and Distinguished Name from a client certificate,
+ * storing them on the Port.  Returns false if the certificate is malformed
+ * and the connection should be rejected.
+ */
+static bool
+be_tls_extract_peer_cert_names(Port *port, X509 *peer)
+{
+       int                     len;
+       const X509_NAME *x509name = X509_get_subject_name(peer);
+       char       *peer_dn;
+       BIO                *bio = NULL;
+       BUF_MEM    *bio_buf = NULL;
+       int                     index;
+
+       index = X509_NAME_get_index_by_NID(unconstify(X509_NAME *, x509name), 
NID_commonName, -1);
+       if (index >= 0)
+       {
+               const X509_NAME_ENTRY *entry;
+               const ASN1_STRING *peer_cn_asn1;
+               const unsigned char *peer_cn_internal;
+               char       *peer_cn;
+
+               entry = X509_NAME_get_entry(unconstify(X509_NAME *, x509name), 
index);
+               peer_cn_asn1 = X509_NAME_ENTRY_get_data(entry);
+               len = ASN1_STRING_length(peer_cn_asn1);
+               peer_cn_internal = ASN1_STRING_get0_data(peer_cn_asn1);
+
+               peer_cn = MemoryContextAlloc(TopMemoryContext, len + 1);
+               memcpy(peer_cn, peer_cn_internal, len);
+               peer_cn[len] = '\0';
 
-                       port->peer_cn = peer_cn;
+               /*
+                * Reject embedded NULLs in certificate common name to prevent 
attacks
+                * like CVE-2009-4034.
+                */
+               if (len != strlen(peer_cn))
+               {
+                       ereport(COMMERROR,
+                                       (errcode(ERRCODE_PROTOCOL_VIOLATION),
+                                        errmsg("SSL certificate's common name 
contains embedded null")));
+                       pfree(peer_cn);
+                       return false;
                }
 
-               bio = BIO_new(BIO_s_mem());
-               if (!bio)
+               port->peer_cn = peer_cn;
+       }
+
+       bio = BIO_new(BIO_s_mem());
+       if (!bio)
+       {
+               if (port->peer_cn != NULL)
                {
-                       if (port->peer_cn != NULL)
-                       {
-                               pfree(port->peer_cn);
-                               port->peer_cn = NULL;
-                       }
-                       return -1;
+                       pfree(port->peer_cn);
+                       port->peer_cn = NULL;
                }
+               return false;
+       }
 
-               /*
-                * RFC2253 is the closest thing to an accepted standard format 
for
-                * DNs. We have documented how to produce this format from a
-                * certificate. It uses commas instead of slashes for 
delimiters,
-                * which make regular expression matching a bit easier. Also 
note that
-                * it prints the Subject fields in reverse order.
-                */
-               if (X509_NAME_print_ex(bio, x509name, 0, XN_FLAG_RFC2253) == -1 
||
-                       BIO_get_mem_ptr(bio, &bio_buf) <= 0)
+       /*
+        * RFC2253 is the closest thing to an accepted standard format for DNs. 
We
+        * have documented how to produce this format from a certificate. It 
uses
+        * commas instead of slashes for delimiters, which make regular 
expression
+        * matching a bit easier. Also note that it prints the Subject fields in
+        * reverse order.
+        */
+       if (X509_NAME_print_ex(bio, x509name, 0, XN_FLAG_RFC2253) == -1 ||
+               BIO_get_mem_ptr(bio, &bio_buf) <= 0)
+       {
+               BIO_free(bio);
+               if (port->peer_cn != NULL)
                {
-                       BIO_free(bio);
-                       if (port->peer_cn != NULL)
-                       {
-                               pfree(port->peer_cn);
-                               port->peer_cn = NULL;
-                       }
-                       return -1;
+                       pfree(port->peer_cn);
+                       port->peer_cn = NULL;
                }
-               peer_dn = MemoryContextAlloc(TopMemoryContext, bio_buf->length 
+ 1);
-               memcpy(peer_dn, bio_buf->data, bio_buf->length);
-               len = bio_buf->length;
+               return false;
+       }
+       len = bio_buf->length;
+       if (len > 0)
+       {
+               peer_dn = MemoryContextAlloc(TopMemoryContext, len + 1);
+               memcpy(peer_dn, bio_buf->data, len);
                BIO_free(bio);
                peer_dn[len] = '\0';
                if (len != strlen(peer_dn))
@@ -1163,15 +1186,22 @@ aloop:
                                pfree(port->peer_cn);
                                port->peer_cn = NULL;
                        }
-                       return -1;
+                       return false;
                }
 
                port->peer_dn = peer_dn;
-
-               port->peer_cert_valid = true;
+       }
+       else
+       {
+               /*
+                * An empty subject means there is no DN to record, so leave 
peer_dn
+                * NULL.  RFC 5280 allows this when the subjectAltName 
extension is
+                * present.
+                */
+               BIO_free(bio);
        }
 
-       return 0;
+       return true;
 }
 
 void
@@ -1202,6 +1232,13 @@ be_tls_close(Port *port)
                pfree(port->peer_dn);
                port->peer_dn = NULL;
        }
+
+       if (port->peer_uri)
+       {
+               pfree(port->peer_uri);
+               port->peer_uri = NULL;
+               port->peer_uri_count = 0;
+       }
 }
 
 ssize_t
@@ -2277,6 +2314,41 @@ be_tls_get_peer_serial(Port *port, char *ptr, size_t len)
                ptr[0] = '\0';
 }
 
+/*
+ * Extract the URI subjectAltNames from the client certificate, storing the
+ * total count and the first URI on the Port.  Called only for
+ * clientname=URI authentication.  An unrepresentable URI is reported by
+ * X509_URI_to_cstring() and leaves peer_uri NULL.
+ */
+void
+be_tls_get_peer_uri(Port *port)
+{
+       if (port->peer)
+       {
+               STACK_OF(GENERAL_NAME) * peer_san;
+
+               peer_san = (STACK_OF(GENERAL_NAME) *)
+                       X509_get_ext_d2i(port->peer, NID_subject_alt_name, 
NULL, NULL);
+               if (peer_san)
+               {
+                       int                     san_len = 
sk_GENERAL_NAME_num(peer_san);
+                       int                     i;
+
+                       for (i = 0; i < san_len; i++)
+                       {
+                               const GENERAL_NAME *name = 
sk_GENERAL_NAME_value(peer_san, i);
+
+                               if (name->type == GEN_URI &&
+                                       port->peer_uri_count++ == 0)
+                                       port->peer_uri =
+                                               
X509_URI_to_cstring(name->d.uniformResourceIdentifier);
+                       }
+
+                       sk_GENERAL_NAME_pop_free(peer_san, GENERAL_NAME_free);
+               }
+       }
+}
+
 char *
 be_tls_get_certificate_hash(Port *port, size_t *len)
 {
@@ -2433,6 +2505,46 @@ X509_NAME_to_cstring(const X509_NAME *name)
        return result;
 }
 
+/*
+ * Convert an X509 URI subjectAltName to a cstring.
+ */
+static char *
+X509_URI_to_cstring(const ASN1_STRING *uri)
+{
+       int                     len;
+       const unsigned char *data;
+       char       *result;
+
+       if (uri == NULL)
+       {
+               ereport(COMMERROR,
+                               (errcode(ERRCODE_PROTOCOL_VIOLATION),
+                                errmsg("SSL certificate's URI subject 
alternative name is invalid")));
+               return NULL;
+       }
+
+       len = ASN1_STRING_length(uri);
+       data = ASN1_STRING_get0_data(uri);
+       result = MemoryContextAlloc(TopMemoryContext, len + 1);
+       memcpy(result, data, len);
+       result[len] = '\0';
+
+       /*
+        * Reject embedded NULLs in certificate URI SANs to prevent confusion
+        * between PostgreSQL's cstring handling and the certificate contents.
+        */
+       if (len != strlen(result))
+       {
+               pfree(result);
+               ereport(COMMERROR,
+                               (errcode(ERRCODE_PROTOCOL_VIOLATION),
+                                errmsg("SSL certificate's URI subject 
alternative name contains embedded null")));
+               return NULL;
+       }
+
+       return result;
+}
+
 /*
  * Convert TLS protocol version GUC enum to OpenSSL values
  *
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index c0d8a9d8a0..eb07cc3c9d 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -2081,6 +2081,10 @@ parse_hba_auth_opt(char *name, char *val, HbaLine 
*hbaline,
                {
                        hbaline->clientcertname = clientCertDN;
                }
+               else if (strcmp(val, "URI") == 0)
+               {
+                       hbaline->clientcertname = clientCertURI;
+               }
                else
                {
                        ereport(elevel,
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 4aa6258a34..58e497e018 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -75,6 +75,7 @@ typedef enum ClientCertName
 {
        clientCertCN,
        clientCertDN,
+       clientCertURI,
 } ClientCertName;
 
 /*
diff --git a/src/include/libpq/libpq-be.h b/src/include/libpq/libpq-be.h
index 921b2daa4f..5020a04a27 100644
--- a/src/include/libpq/libpq-be.h
+++ b/src/include/libpq/libpq-be.h
@@ -208,6 +208,8 @@ typedef struct Port
        bool            ssl_in_use;
        char       *peer_cn;
        char       *peer_dn;
+       char       *peer_uri;
+       int                     peer_uri_count;
        bool            peer_cert_valid;
        bool            alpn_used;
        bool            last_read_was_eof;
@@ -320,6 +322,7 @@ extern const char *be_tls_get_cipher(Port *port);
 extern void be_tls_get_peer_subject_name(Port *port, char *ptr, size_t len);
 extern void be_tls_get_peer_issuer_name(Port *port, char *ptr, size_t len);
 extern void be_tls_get_peer_serial(Port *port, char *ptr, size_t len);
+extern void be_tls_get_peer_uri(Port *port);
 
 /*
  * Get the server certificate hash for SCRAM channel binding type
diff --git a/src/test/ssl/conf/client-uri-multi.config 
b/src/test/ssl/conf/client-uri-multi.config
new file mode 100644
index 0000000000..113c3e8489
--- /dev/null
+++ b/src/test/ssl/conf/client-uri-multi.config
@@ -0,0 +1,22 @@
+# An OpenSSL format CSR config file for creating a client certificate.
+# The certificate is for user "ssltestuser" with multiple URI subject
+# alternative names.  With clientname=URI, PostgreSQL rejects certificates
+# with more than one URI SAN.
+# The URI matching ssltestuser is listed first, so that without the
+# one-URI check, matching the first URI alone would authenticate this
+# certificate.
+
+[ req ]
+distinguished_name     = req_distinguished_name
+req_extensions         = v3_req
+prompt                 = no
+
+[ req_distinguished_name ]
+CN                     = ssltestuser
+
+[ v3_req ]
+subjectAltName         = @alt_names
+
+[ alt_names ]
+URI.1                  = spiffe://postgresql.example/ns/default/sa/ssltestuser
+URI.2                  = spiffe://postgresql.example/ns/default/sa/not-the-user
diff --git a/src/test/ssl/conf/client-uri-nosubject.config 
b/src/test/ssl/conf/client-uri-nosubject.config
new file mode 100644
index 0000000000..b536b94f7b
--- /dev/null
+++ b/src/test/ssl/conf/client-uri-nosubject.config
@@ -0,0 +1,15 @@
+# An OpenSSL format CSR config file for creating a client certificate with
+# an empty subject and a single URI subject alternative name.  RFC 5280
+# requires the subjectAltName extension to be critical when the subject is
+# empty, and the CSR recipe in sslfiles.mk passes "-subj /" to express the
+# empty subject.
+
+[ req ]
+distinguished_name     = req_distinguished_name
+req_extensions         = v3_req
+prompt                 = no
+
+[ req_distinguished_name ]
+
+[ v3_req ]
+subjectAltName         = 
critical,URI:spiffe://postgresql.example/ns/default/sa/ssltestuser
diff --git a/src/test/ssl/conf/client-uri.config 
b/src/test/ssl/conf/client-uri.config
new file mode 100644
index 0000000000..ea7175e041
--- /dev/null
+++ b/src/test/ssl/conf/client-uri.config
@@ -0,0 +1,17 @@
+# An OpenSSL format CSR config file for creating a client certificate.
+# The certificate is for user "ssltestuser" with a single URI subject
+# alternative name.
+
+[ req ]
+distinguished_name     = req_distinguished_name
+req_extensions         = v3_req
+prompt                 = no
+
+[ req_distinguished_name ]
+CN                     = ssltestuser
+
+[ v3_req ]
+subjectAltName         = @alt_names
+
+[ alt_names ]
+URI.1                  = spiffe://postgresql.example/ns/default/sa/ssltestuser
diff --git a/src/test/ssl/ssl/client-uri-multi.crt 
b/src/test/ssl/ssl/client-uri-multi.crt
new file mode 100644
index 0000000000..22a5149a22
--- /dev/null
+++ b/src/test/ssl/ssl/client-uri-multi.crt
@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDmjCCAoKgAwIBAgIIICYJKQkTQQAwDQYJKoZIhvcNAQELBQAwQjFAMD4GA1UE
+Aww3VGVzdCBDQSBmb3IgUG9zdGdyZVNRTCBTU0wgcmVncmVzc2lvbiB0ZXN0IGNs
+aWVudCBjZXJ0czAgFw0yMzA2MjkwMTAxMDFaGA8yMDUwMDEwMTAxMDEwMVowFjEU
+MBIGA1UEAwwLc3NsdGVzdHVzZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
+AoIBAQC1DIM8v/RJlfpjl8Sb7M3xY27Tv+m+YlJ5NxNWDUnlGiBokzsoKZ9chaoQ
+yDP9fq5fOFpDFhWwgo5WskHfb/RXjuDaro2myZsM+mMuZcNWM0ATZpexlDd9Sh79
+Pz+eU0kP34ZcPr9QwhmgHRsv4sAbf0DTC8ICnH57IryeTa2fwJ6wsySTzf3e9lus
+3svhyZzXW3EsXH212KPtWlWRqcqbi6DKPFfrWuOpi1dNHDK+pmK94S5N9eSYsUap
+kh0U6t8GO1Z83jqvkgAt4Lhsu+cfOpEJOzRlinC2m3a+X3pXKMXx8yFPK7ksin+i
+OLz1/115pRYwyL1wHAsbK9VpuI+/AgMBAAGjgb0wgboweAYDVR0RBHEwb4Y1c3Bp
+ZmZlOi8vcG9zdGdyZXNxbC5leGFtcGxlL25zL2RlZmF1bHQvc2Evc3NsdGVzdHVz
+ZXKGNnNwaWZmZTovL3Bvc3RncmVzcWwuZXhhbXBsZS9ucy9kZWZhdWx0L3NhL25v
+dC10aGUtdXNlcjAdBgNVHQ4EFgQUJpgtW87f9rqDisg1391xThjWsOkwHwYDVR0j
+BBgwFoAUn6Tr0smZ3rADsDA0IoFhFUu9rTYwDQYJKoZIhvcNAQELBQADggEBAB/c
+Ovjq7Nhs0yEUxlI15cwT7yAher5Q+1kUuCS8cWDMOFT9V8KaN8X09G7JgUzUr6qK
+7gVV42hXPhq2oagWhGB1uP2Aiqwi3uxLB/TWhrj2bawRap47/1Xr3AqsfU3H/C78
+toNaKdr1O6974LAZ8/hayezf/tBWjBrhDuXjGUl8sBpB8N//PB9/Nsu/ZPu63lYT
+laH165ByUL9n1uMPr25MrNUmLCnhaiLEyrB+/FlhsZRodjryXPFCI1HH/YzgMDqw
+GGCQptdZHCoGbc/VEncyU+nV6AaWdV3Ejthrqc+S320YuzqW7FvZWHCDWvVSWoSQ
+29we52Jdv6FvTMuKbW8=
+-----END CERTIFICATE-----
diff --git a/src/test/ssl/ssl/client-uri-multi.key 
b/src/test/ssl/ssl/client-uri-multi.key
new file mode 100644
index 0000000000..ae05e25b04
--- /dev/null
+++ b/src/test/ssl/ssl/client-uri-multi.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC1DIM8v/RJlfpj
+l8Sb7M3xY27Tv+m+YlJ5NxNWDUnlGiBokzsoKZ9chaoQyDP9fq5fOFpDFhWwgo5W
+skHfb/RXjuDaro2myZsM+mMuZcNWM0ATZpexlDd9Sh79Pz+eU0kP34ZcPr9Qwhmg
+HRsv4sAbf0DTC8ICnH57IryeTa2fwJ6wsySTzf3e9lus3svhyZzXW3EsXH212KPt
+WlWRqcqbi6DKPFfrWuOpi1dNHDK+pmK94S5N9eSYsUapkh0U6t8GO1Z83jqvkgAt
+4Lhsu+cfOpEJOzRlinC2m3a+X3pXKMXx8yFPK7ksin+iOLz1/115pRYwyL1wHAsb
+K9VpuI+/AgMBAAECggEAJxVF6WHYtt/o9wHmv/A9PuxDmvN1XpN0EVW511w0BQCA
+WYLbBN8DV2JFZa0KSCFGPCj6lzvXv8xXNNDzVmwhF5uw35RJ4OTpk0IkEfqG0f9r
+SCTf/0YrOmE7UlkKfz+kaIhMxXIIM8NK690Mpugwp17vm/+QSKcGyMclZ5kGL5N0
+h7J6gFC230oN3Ucty+c3pkRU7yU8c87tUXmuoAef/2J2A4XguALzsvPEVRnQLKiW
+aa8yQ2rw+WEH3mdKvFHX740QEdSIBiHZggywY+F+1GW9HPvnolsEKYOE4XT1XAOK
+tN7iGgOhBJkvkCN6Q9mM+Hv0UdfAoN8UbQrviHFZXQKBgQDqGXsqZLa2ckC6dVn3
+DdpEAawcW3KnL9keMWsfXcajj0Bo1wVgdiyT6KQJP31F0F9U9MbadMCzDB/jbJ1P
+WGIYHJyShJtHF4V/FLJmwPnS/7i7lEwO6aRZTKEV6SV7R2/1x24huponqr4HQPLo
+EX52nw6UAKyFc8eI6lnWHvCnkwKBgQDF/IFy32xptKgladcpnrCg6Fv9pwwiqquD
+hOznHUXjRdPtu8FadTbJB0af4/s91+hFROhHC0zfdsypoQ4m/1cAe+RGiWQmSxoS
+s8EnDkdfQ8lauRIhj8qmZEG4UGAd7p5okHl9ir9094x3DZwHewDo1/vMm4oTCtat
+2hWG0436pQKBgQCmOkHC8JDtLGPaAspHK4b5E1brK+RV4xwA3IZ8V1JdgbLyvvwK
+at9sh50zE+oYUMXgxY8OQCk0+j8Kdm2dkdzV0js7rv2zlXgtrLyhShYbRYofaEY5
+sJ4K6ubXcB41U5ykoWAKgz5DPHKDJBNXxlROVtM7NN9MQ9JD3mS8LmwkEQKBgGck
+etO7baFMCmUjVIJMN7w8EOB+DCZrP1sO/tQQPf5+vD+Xls1nTxk/fx2BCbxYJJsh
+oNiSHIQ2Qb0FOHg8gbiw0mWk4dXJPlYL605LdAc/K0DmNXnijTJN/KbmyIwPadsT
+mRc8Fy36YOUM5AJJpFmpW3ZmeXIeLWT4vko1IDw5AoGASYUc0EeLqd5av1PQJxMv
+KkuQBIL8QGgry6UVqJ3rRRLNNRtSt4F8Af/VdA0QDK6m7lFtKaIbwjjkZvq5HQy6
+6eFXZfimjLnOzRw+iqufxYxqLwS0hB3uH0vEGNZFlrP/GjllGsIE5enlyl5S4ZvM
+3k/eoDSThehbhyZV2fAmHb8=
+-----END PRIVATE KEY-----
diff --git a/src/test/ssl/ssl/client-uri-nosubject.crt 
b/src/test/ssl/ssl/client-uri-nosubject.crt
new file mode 100644
index 0000000000..2391c820c1
--- /dev/null
+++ b/src/test/ssl/ssl/client-uri-nosubject.crt
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDTzCCAjegAwIBAgIIICYJFiMIWQEwDQYJKoZIhvcNAQELBQAwQjFAMD4GA1UE
+Aww3VGVzdCBDQSBmb3IgUG9zdGdyZVNRTCBTU0wgcmVncmVzc2lvbiB0ZXN0IGNs
+aWVudCBjZXJ0czAgFw0yMzA2MjkwMTAxMDFaGA8yMDUwMDEwMTAxMDEwMVowADCC
+ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ70eCm2orWuCp9UWf0VBc8Z
+666KIkdhr9XzSomoPPk1wddwTJPyCKAZ8yCGh78RmCfpeuIjvAnsUF9kJESRP1V/
+CEMz9tueFuj226ni37sOySKTUhda6o68fP1dXffImOhWZbyQxSWG0pIs21h+7hh4
+XZgBa0QepFzlx0u2u0KTu540ILjtOafBsdBgwE/WaN+xzE6HnujkNKei74QsEIoH
+5S00P6B13kl7JUm/Q0CyxI10QOhHfjYCsrNLsRPQHdxusUAZSb7J4JEcn8HyhzJy
+GuPcVrsy8obLTVifVClCn+S0470mi5SKJo/mtPblvHXugOY15qoVAz3Gtccnc/0C
+AwEAAaOBiDCBhTBDBgNVHREBAf8EOTA3hjVzcGlmZmU6Ly9wb3N0Z3Jlc3FsLmV4
+YW1wbGUvbnMvZGVmYXVsdC9zYS9zc2x0ZXN0dXNlcjAdBgNVHQ4EFgQUpWRpbmwi
+gV2R6L8uTZZ3A0LUQNQwHwYDVR0jBBgwFoAUn6Tr0smZ3rADsDA0IoFhFUu9rTYw
+DQYJKoZIhvcNAQELBQADggEBAK1bX5+0CqutAtces5YPoNmoZQvB7WYqR0n9OlDO
+u+6LxUB5SAAlIACrCVYAcTJ+K66faLTCgeqtU6fcoq9iFWG5vVRdhv00Hdt6p3MX
+b0vVrrxbnqDM4ynHCuSJv5A9whsfqMqAlZIqs4AwmkYglrdK//jnTWYyY6ZQPxVb
+D5dSXFT06HoQSijZhheYFgZovrv/oY6Pe0HaidiiAc6ZyeM5xZgxiGV3UwWFt0Jr
+y59t7oOBLQQGTVzv8AVCwcq4p+z0FvCMIimIU5JbrDxiMDsfdGSjYNG62SFFdrIa
+kePzfIJ873DkPgFPwU7/0Yq1WlIqVNDBzSAAkqGdlrjY4aM=
+-----END CERTIFICATE-----
diff --git a/src/test/ssl/ssl/client-uri-nosubject.key 
b/src/test/ssl/ssl/client-uri-nosubject.key
new file mode 100644
index 0000000000..53f0003872
--- /dev/null
+++ b/src/test/ssl/ssl/client-uri-nosubject.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCe9HgptqK1rgqf
+VFn9FQXPGeuuiiJHYa/V80qJqDz5NcHXcEyT8gigGfMghoe/EZgn6XriI7wJ7FBf
+ZCREkT9VfwhDM/bbnhbo9tup4t+7Dskik1IXWuqOvHz9XV33yJjoVmW8kMUlhtKS
+LNtYfu4YeF2YAWtEHqRc5cdLtrtCk7ueNCC47TmnwbHQYMBP1mjfscxOh57o5DSn
+ou+ELBCKB+UtND+gdd5JeyVJv0NAssSNdEDoR342ArKzS7ET0B3cbrFAGUm+yeCR
+HJ/B8ocychrj3Fa7MvKGy01Yn1QpQp/ktOO9JouUiiaP5rT25bx17oDmNeaqFQM9
+xrXHJ3P9AgMBAAECggEADNhUnYpUu7nEwftfCkSYo5PEp+YhvwL14qQ2cclWpAzk
+DTTV+16js8xlCUvVzGrvkModVAjvOR4PoKXHCIN0SsRpNoUMfdLYpxrLxX6/9a3M
+My8Ugy6lpcM1loPlBBEBykSxE/ve8pliis11LEGVnASeC5qRwH95aMhowoWRvF9z
+K6AuZB+KfJEoXR6BarLQ+ZS1U2/h9Sh3TYBfb+6Taup7Kb6RoHvBuuPJUN/T1M+e
+4N30tiJ9H9j/1NlXaY1qA+s5NDmvqSwrEtbm2gBXafTxB4DNvb08rZFgxxmqWtsr
++lx+U2OZIGeowGxoqka0P/2wYPk4CAt8kqZZET1iAQKBgQDYKYvR3SW2GfKny40w
+UjbPGhroMX++rOcdXVG5+Jt4HyGvrPsgvvdlus5w9YKv7HMZFgm4830qsim/63b5
+OFhDHNcRO4Xvd8bVtUps7gF0zOk3cu0oAVq8ZuDII0L7lHfLn9j/t0SSCa6AMide
+rOTdbsGPpxEx8HlPmxw8uWUzmQKBgQC8P+c+aKQBCP36okVTODLCKdvHGnVwnntU
+Gf6aEwIgFGDjUJMPdVexd/qdrLy8GIdrP+Ij/PMo3UR36Wrw8zXxHOwQ9oOyTvKY
+cP6WjfyaywcygFBhEXur70j08ZGkKD9IyXoFSROkRfx2zBidHrkD1AlY8uKHzlIB
+ER93I+BCBQKBgB/XjuctQn7et6YMEBJMKhK777bAg+bcpXbn5kAU5SH+xAGS47Nh
+LiRoLjzpjYTIufO6EViTVZ7Se9/vmakAqc/JEc1SDVrHNB0LBZmiPcis3rXyUgkQ
+mgMizH3u49EXf8YZF+gjYRB6KKBtwurpYRVVWWIF5DyNBfG6EaDIVqYRAoGBAJNW
+eMkx4VsmDJMEOro9vAyX8npNTSnOALz8c3dn9Tvid0qzH8bzkqVGQJL0Rev6TtM0
+duyv6ClNtW8c9CSOVuPWPTWxm7YNcHa7yadQjishSQrPvxmaM2+Io1ODVvhiv9Va
+S/SjE4p5dHYOnB0tlKVYadCCmaatZyWKn1QCcl15AoGALE7qV+BDTav16IX608N2
+s5V/br936JbIPgVF5FqXbnpjAwfn4tB75iNHznEb04s4Y4TKreyFx5BQLmIAK1Xr
+O2lF95PZlcsubVNFMOBxluMdw4d6MKCS/QuZncfy+gxsjeAFAmPVgX+7GJp8h7je
+a+3HOha5d2smUGG3/PyxbXQ=
+-----END PRIVATE KEY-----
diff --git a/src/test/ssl/ssl/client-uri.crt b/src/test/ssl/ssl/client-uri.crt
new file mode 100644
index 0000000000..612dd96e41
--- /dev/null
+++ b/src/test/ssl/ssl/client-uri.crt
@@ -0,0 +1,21 @@
+-----BEGIN CERTIFICATE-----
+MIIDYjCCAkqgAwIBAgIIICYJFiMIWQAwDQYJKoZIhvcNAQELBQAwQjFAMD4GA1UE
+Aww3VGVzdCBDQSBmb3IgUG9zdGdyZVNRTCBTU0wgcmVncmVzc2lvbiB0ZXN0IGNs
+aWVudCBjZXJ0czAgFw0yMzA2MjkwMTAxMDFaGA8yMDUwMDEwMTAxMDEwMVowFjEU
+MBIGA1UEAwwLc3NsdGVzdHVzZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
+AoIBAQC1DIM8v/RJlfpjl8Sb7M3xY27Tv+m+YlJ5NxNWDUnlGiBokzsoKZ9chaoQ
+yDP9fq5fOFpDFhWwgo5WskHfb/RXjuDaro2myZsM+mMuZcNWM0ATZpexlDd9Sh79
+Pz+eU0kP34ZcPr9QwhmgHRsv4sAbf0DTC8ICnH57IryeTa2fwJ6wsySTzf3e9lus
+3svhyZzXW3EsXH212KPtWlWRqcqbi6DKPFfrWuOpi1dNHDK+pmK94S5N9eSYsUap
+kh0U6t8GO1Z83jqvkgAt4Lhsu+cfOpEJOzRlinC2m3a+X3pXKMXx8yFPK7ksin+i
+OLz1/115pRYwyL1wHAsbK9VpuI+/AgMBAAGjgYUwgYIwQAYDVR0RBDkwN4Y1c3Bp
+ZmZlOi8vcG9zdGdyZXNxbC5leGFtcGxlL25zL2RlZmF1bHQvc2Evc3NsdGVzdHVz
+ZXIwHQYDVR0OBBYEFCaYLVvO3/a6g4rINd/dcU4Y1rDpMB8GA1UdIwQYMBaAFJ+k
+69LJmd6wA7AwNCKBYRVLva02MA0GCSqGSIb3DQEBCwUAA4IBAQAE2ImAAqHqwNkT
+vocEO5c5iJFXvSW3arB9FYZ8Py06LwctaQl7OydzEgqUpgKRwLFALWOqMT8ZAjbZ
+LxzXwGIDgIC/ez7FoJYwUJfw6P2GmvRkKw+NySq1M8cZ9u6hpsZbFG4Vgz0QICAD
+h59mIm/T8pnP8mMCUrg9GdikeZgAbXgcLpHlfVspUb5MkywQfxnFe6vS/HMYdDBc
+Rt7vxmrTVSniEH8tq++/PWjLvZ3ObjlfD/QLCyfaBN2jyBaaxUrKcJpgCFdQF4mM
+amSWDwnBIv9wDDspp5O5uw7RhCNmVl8Rqb8IegHx5o2L4ATW/OZsf1cy7LV/nHck
+8xOZ2d8x
+-----END CERTIFICATE-----
diff --git a/src/test/ssl/ssl/client-uri.key b/src/test/ssl/ssl/client-uri.key
new file mode 100644
index 0000000000..ae05e25b04
--- /dev/null
+++ b/src/test/ssl/ssl/client-uri.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC1DIM8v/RJlfpj
+l8Sb7M3xY27Tv+m+YlJ5NxNWDUnlGiBokzsoKZ9chaoQyDP9fq5fOFpDFhWwgo5W
+skHfb/RXjuDaro2myZsM+mMuZcNWM0ATZpexlDd9Sh79Pz+eU0kP34ZcPr9Qwhmg
+HRsv4sAbf0DTC8ICnH57IryeTa2fwJ6wsySTzf3e9lus3svhyZzXW3EsXH212KPt
+WlWRqcqbi6DKPFfrWuOpi1dNHDK+pmK94S5N9eSYsUapkh0U6t8GO1Z83jqvkgAt
+4Lhsu+cfOpEJOzRlinC2m3a+X3pXKMXx8yFPK7ksin+iOLz1/115pRYwyL1wHAsb
+K9VpuI+/AgMBAAECggEAJxVF6WHYtt/o9wHmv/A9PuxDmvN1XpN0EVW511w0BQCA
+WYLbBN8DV2JFZa0KSCFGPCj6lzvXv8xXNNDzVmwhF5uw35RJ4OTpk0IkEfqG0f9r
+SCTf/0YrOmE7UlkKfz+kaIhMxXIIM8NK690Mpugwp17vm/+QSKcGyMclZ5kGL5N0
+h7J6gFC230oN3Ucty+c3pkRU7yU8c87tUXmuoAef/2J2A4XguALzsvPEVRnQLKiW
+aa8yQ2rw+WEH3mdKvFHX740QEdSIBiHZggywY+F+1GW9HPvnolsEKYOE4XT1XAOK
+tN7iGgOhBJkvkCN6Q9mM+Hv0UdfAoN8UbQrviHFZXQKBgQDqGXsqZLa2ckC6dVn3
+DdpEAawcW3KnL9keMWsfXcajj0Bo1wVgdiyT6KQJP31F0F9U9MbadMCzDB/jbJ1P
+WGIYHJyShJtHF4V/FLJmwPnS/7i7lEwO6aRZTKEV6SV7R2/1x24huponqr4HQPLo
+EX52nw6UAKyFc8eI6lnWHvCnkwKBgQDF/IFy32xptKgladcpnrCg6Fv9pwwiqquD
+hOznHUXjRdPtu8FadTbJB0af4/s91+hFROhHC0zfdsypoQ4m/1cAe+RGiWQmSxoS
+s8EnDkdfQ8lauRIhj8qmZEG4UGAd7p5okHl9ir9094x3DZwHewDo1/vMm4oTCtat
+2hWG0436pQKBgQCmOkHC8JDtLGPaAspHK4b5E1brK+RV4xwA3IZ8V1JdgbLyvvwK
+at9sh50zE+oYUMXgxY8OQCk0+j8Kdm2dkdzV0js7rv2zlXgtrLyhShYbRYofaEY5
+sJ4K6ubXcB41U5ykoWAKgz5DPHKDJBNXxlROVtM7NN9MQ9JD3mS8LmwkEQKBgGck
+etO7baFMCmUjVIJMN7w8EOB+DCZrP1sO/tQQPf5+vD+Xls1nTxk/fx2BCbxYJJsh
+oNiSHIQ2Qb0FOHg8gbiw0mWk4dXJPlYL605LdAc/K0DmNXnijTJN/KbmyIwPadsT
+mRc8Fy36YOUM5AJJpFmpW3ZmeXIeLWT4vko1IDw5AoGASYUc0EeLqd5av1PQJxMv
+KkuQBIL8QGgry6UVqJ3rRRLNNRtSt4F8Af/VdA0QDK6m7lFtKaIbwjjkZvq5HQy6
+6eFXZfimjLnOzRw+iqufxYxqLwS0hB3uH0vEGNZFlrP/GjllGsIE5enlyl5S4ZvM
+3k/eoDSThehbhyZV2fAmHb8=
+-----END PRIVATE KEY-----
diff --git a/src/test/ssl/sslfiles.mk b/src/test/ssl/sslfiles.mk
index f32c53a76a..1fdc7d27b5 100644
--- a/src/test/ssl/sslfiles.mk
+++ b/src/test/ssl/sslfiles.mk
@@ -35,7 +35,7 @@ SERVERS := server-cn-and-alt-names \
        server-no-names \
        server-revoked
 CLIENTS := client client-dn client-revoked client_ext client-long \
-       client-revoked-utf8
+       client-revoked-utf8 client-uri client-uri-multi client-uri-nosubject
 
 #
 # To add a new non-standard certificate, add it to SPECIAL_CERTS and then add
@@ -198,8 +198,15 @@ $(CLIENT_CERTS): ssl/%.crt: ssl/%.csr conf/%.config 
conf/cas.config ssl/client_c
 
 # The CSRs don't need to persist after a build.
 .INTERMEDIATE: $(CERTIFICATES:%=ssl/%.csr)
+
+# A certificate with an empty subject needs "-subj /" to be passed to
+# `openssl req`, since the config file alone cannot express an empty
+# subject.  REQ_SUBJ is empty by default.
+REQ_SUBJ :=
 ssl/%.csr: ssl/%.key conf/%.config
-       $(OPENSSL) req -new -utf8 -key $< -out $@ -config conf/$*.config
+       $(OPENSSL) req -new -utf8 -key $< $(REQ_SUBJ) -out $@ -config 
conf/$*.config
+
+ssl/client-uri-nosubject.csr: REQ_SUBJ = -subj /
 
 #
 # CA State
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index cb7c2a0619..d8dcfa2f8f 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -1007,6 +1007,60 @@ $node->connect_fails(
                qr{Failed certificate data \(unverified\): subject 
"/CN=\\xce\\x9f\\xce\\xb4\\xcf\\x85\\xcf\\x83\\xcf\\x83\\xce\\xad\\xce\\xb1\\xcf\\x82",
 serial number \d+, issuer "/CN=Test CA for PostgreSQL SSL regression test 
client certs"},
        ]);
 
+# same thing but using URI subject alternative names
+my $uri_connstr = "$common_connstr dbname=certdb_uri";
+
+$node->connect_ok(
+       "$uri_connstr user=ssltestuser sslcert=ssl/client-uri.crt "
+         . sslkey('client-uri.key'),
+       "certificate authorization succeeds with URI SAN mapping",
+       # the matched URI should be used as the authenticated identity
+       log_like => [
+               qr{connection authenticated: 
identity="spiffe://postgresql\.example/ns/default/sa/ssltestuser" method=cert}
+       ],);
+
+$node->connect_fails(
+       "$uri_connstr user=anotheruser sslcert=ssl/client-uri.crt "
+         . sslkey('client-uri.key'),
+       "certificate authorization fails when the URI SAN does not map to the 
requested user",
+       expected_stderr =>
+         qr/certificate authentication failed for user "anotheruser"/);
+
+$node->connect_fails(
+       "$uri_connstr user=ssltestuser sslcert=ssl/client.crt "
+         . sslkey('client.key'),
+       "certificate authorization fails when client certificate has no URI 
SAN",
+       expected_stderr =>
+         qr/certificate authentication failed for user "ssltestuser"/);
+
+$node->connect_fails(
+       "$uri_connstr user=ssltestuser sslcert=ssl/client-uri-multi.crt "
+         . sslkey('client-uri-multi.key'),
+       "certificate authorization fails when client certificate has multiple 
URI SANs",
+       expected_stderr =>
+         qr/certificate authentication failed for user "ssltestuser"/,
+       log_like => [
+               qr{certificate authentication failed for user "ssltestuser": 
client certificate must contain exactly one URI subject alternative name}
+       ],);
+
+$node->connect_ok(
+       "$uri_connstr user=ssltestuser sslcert=ssl/client-uri-nosubject.crt "
+         . sslkey('client-uri-nosubject.key'),
+       "certificate authorization succeeds with URI SAN mapping when the 
certificate has no subject",
+       log_like => [
+               qr{connection authenticated: 
identity="spiffe://postgresql\.example/ns/default/sa/ssltestuser" method=cert}
+       ],);
+
+# With clientname=CN, a certificate without a subject has no CN to match
+# against, and authentication must fail cleanly rather than aborting the
+# TLS handshake.
+$node->connect_fails(
+       "$common_connstr dbname=certdb_cn user=ssltestuser 
sslcert=ssl/client-uri-nosubject.crt "
+         . sslkey('client-uri-nosubject.key'),
+       "certificate authorization with clientname=CN fails when the 
certificate has no subject",
+       expected_stderr =>
+         qr/certificate authentication failed for user "ssltestuser"/);
+
 SKIP:
 {
        skip "sslmode require not supported in this build", 4
diff --git a/src/test/ssl/t/SSL/Backend/OpenSSL.pm 
b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
index 6060771c1a..91b0cbd24d 100644
--- a/src/test/ssl/t/SSL/Backend/OpenSSL.pm
+++ b/src/test/ssl/t/SSL/Backend/OpenSSL.pm
@@ -91,7 +91,8 @@ sub init
                "client-der.key", "client-encrypted-pem.key",
                "client-encrypted-der.key", "client-dn.key",
                "client_ext.key", "client-long.key",
-               "client-revoked-utf8.key");
+               "client-revoked-utf8.key", "client-uri.key",
+               "client-uri-multi.key", "client-uri-nosubject.key");
        foreach my $keyfile (@keys)
        {
                copy("ssl/$keyfile", "$cert_tempdir/$keyfile")
diff --git a/src/test/ssl/t/SSL/Server.pm b/src/test/ssl/t/SSL/Server.pm
index 4400a432f4..1c79c1abc5 100644
--- a/src/test/ssl/t/SSL/Server.pm
+++ b/src/test/ssl/t/SSL/Server.pm
@@ -134,7 +134,7 @@ sub sslkey
 
 Configure the cluster specified by B<node> or listening on SSL connections.
 The following databases will be created in the cluster: trustdb, certdb,
-certdb_dn, certdb_dn_re, certdb_cn, verifydb. The following users will be
+certdb_dn, certdb_dn_re, certdb_cn, certdb_uri, verifydb. The following users 
will be
 created in the cluster: ssltestuser, md5testuser, anotheruser, yetanotheruser.
 If B<< $params{password} >> is set, it will be used as password for all users
 with the password encoding B<< $params{password_enc} >> (except for md5testuser
@@ -153,7 +153,7 @@ sub configure_test_server_for_ssl
 
        my @databases = (
                'trustdb', 'certdb', 'certdb_dn', 'certdb_dn_re',
-               'certdb_cn', 'verifydb');
+               'certdb_cn', 'certdb_uri', 'verifydb');
 
        # Create test users and databases
        $node->psql('postgres', "CREATE USER ssltestuser");
@@ -366,6 +366,7 @@ hostssl certdb        all             $servercidr   cert
 hostssl certdb_dn     all             $servercidr   cert clientname=DN map=dn
 hostssl certdb_dn_re  all             $servercidr   cert clientname=DN map=dnre
 hostssl certdb_cn     all             $servercidr   cert clientname=CN map=cn
+hostssl certdb_uri    all             $servercidr   cert clientname=URI map=uri
 EOF
        );
 
@@ -377,6 +378,7 @@ EOF
 dn        "CN=ssltestuser-dn,OU=Testing,OU=Engineering,O=PGDG"    ssltestuser
 dnre      "/^.*OU=Testing,.*\$"                                   ssltestuser
 cn        ssltestuser-dn                                          ssltestuser
+uri       spiffe://postgresql.example/ns/default/sa/ssltestuser   ssltestuser
 EOF
        );
        return;
-- 
2.53.0

Reply via email to