Hi

Here is the first version of the patch to support URI SAN in certificate
authentication (clientname=URI in pg_hba.conf). We can start a discussion
on the actual code now.The patch can also be reviewed on my “postgres”
project fork [1].

Together with Leonardo Cecchi, CloudNativePG (CNPG) maintainer, we also
tested the patch using the cnpg-i-spiffe plugin[2] that he wrote for the
occasion and the generated container image for CloudNativePG [3]. Everything
worked smoothly, and workloads can now authenticate natively with SPIFFE IDs.

Given that both CloudNativePG and SPIFFE/SPIRE are CNCF projects, we contacted
Agustín Martínez Fayó, a SPIRE maintainer, to verify the integration between
the two projects and to begin collaborating on the topic. We decided to
strictly allow only a single URI SAN per certificate. This follows the
X509-SVID spec, avoiding weird edge cases with user mapping, and making
connection logging very clear.

Cheers,
Florin

[1] - https://github.com/irionr/postgres/pull/7

[2] - https://github.com/leonardoce/cnpg-i-spiffe

[3] - https://github.com/leonardoce/postgres-trunk-containers/actions/runs/35070429177
From 1a974a7a8f5bc3c0bc6029bb5a254c9e5b56253d Mon Sep 17 00:00:00 2001
From: Florin Irion <[email protected]>
Date: Thu, 17 Sep 2026 00:06:27 +0200
Subject: [PATCH v1] 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.

URI SANs are extracted from the client certificate during TLS setup and
stored on the Port, alongside the existing CN and DN.  Following the
SPIFFE X.509-SVID specification, authentication requires the certificate
to carry exactly one URI SAN: the username is matched against it, and
the matched URI, rather than the Subject DN, becomes the authenticated
identity recorded in the connection log.

Client certificates with an empty subject are now accepted as long as a
URI SAN is present.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 and the caveat that, for SPIFFE deployments, ssl_ca_file
should hold a single trust domain's CA bundle.

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                 |  24 +++-
 src/backend/libpq/auth.c                      |  71 ++++++---
 src/backend/libpq/be-secure-openssl.c         | 135 +++++++++++++++---
 src/backend/libpq/hba.c                       |   5 +-
 src/include/libpq/hba.h                       |   1 +
 src/include/libpq/libpq-be.h                  |   2 +
 src/test/ssl/conf/client-uri-multi.config     |  19 +++
 src/test/ssl/conf/client-uri-nosubject.config |  14 ++
 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                      |  22 ++-
 src/test/ssl/t/001_ssltests.pl                |  51 +++++++
 src/test/ssl/t/SSL/Backend/OpenSSL.pm         |   3 +-
 src/test/ssl/t/SSL/Server.pm                  |   6 +-
 19 files changed, 472 insertions(+), 45 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 e4e65f8feb1..cf62bd64eab 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -693,11 +693,17 @@ 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.
        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 +715,22 @@ 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
+       for more than one trust domain, any of them can issue a certificate
+       for a URI in another trust domain.  For SPIFFE deployments, configure
+       <literal>ssl_ca_file</literal> with a single trust domain's bundle;
+      </para>
      </listitem>
     </varlistentry>
 
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 12bf153d66f..bf28e004a39 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -2746,11 +2746,26 @@ CheckCertAuth(Port *port)
                        break;
                case clientCertCN:
                        peer_username = port->peer_cn;
+                       break;
+               case clientCertURI:
+                       /*
+                        * The SPIFFE X.509-SVID specification requires an SVID 
to carry
+                        * exactly one URI SAN, so refuse both none and more 
than one: the
+                        * authenticated identity must be unambiguous.
+                        */
+                       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;
+                       }
+                       peer_username = port->peer_uri;
+                       break;
        }
 
        /* Make sure we have received a username in the certificate */
-       if (peer_username == NULL ||
-               strlen(peer_username) <= 0)
+       if (peer_username == NULL || strlen(peer_username) <= 0)
        {
                ereport(LOG,
                                (errmsg("certificate authentication failed for 
user \"%s\": client certificate contains no user name",
@@ -2761,29 +2776,39 @@ 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
-                * authentication has already succeeded and we want the log 
file to
-                * reflect that.
+                * For cert auth, the client's Subject DN is our authenticated
+                * identity, except when clientname=URI, in which case the URI 
SAN is
+                * the authenticated identity.  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, unless the 
certificate has an empty
+                                * subject (which is allowed when a URI SAN is 
present, but
+                                * not here, since we're not matching on the 
URI).
+                                */
+                               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 +2829,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 173623d1f6a..dbecef5a71f 100644
--- a/src/backend/libpq/be-secure-openssl.c
+++ b/src/backend/libpq/be-secure-openssl.c
@@ -106,6 +106,7 @@ 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 SSL_CTX *SSL_context = NULL;
 static MemoryContext SSL_hosts_memcxt = NULL;
@@ -1072,9 +1073,14 @@ aloop:
        /* Get client certificate, if available. */
        port->peer = SSL_get_peer_certificate(port->ssl);
 
-       /* and extract the Common Name and Distinguished Name from it. */
+       /*
+        * and extract the Common Name, Distinguished Name, and Subject 
Alternate
+        * Name 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)
        {
@@ -1085,6 +1091,8 @@ aloop:
                BUF_MEM    *bio_buf = NULL;
                int                     index;
 
+               STACK_OF(GENERAL_NAME) * peer_san;
+
                index = X509_NAME_get_index_by_NID(unconstify(X509_NAME *, 
x509name), NID_commonName, -1);
                if (index >= 0)
                {
@@ -1137,7 +1145,7 @@ aloop:
                 * 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_get_mem_ptr(bio, &bio_buf) < 0)
                {
                        BIO_free(bio);
                        if (port->peer_cn != NULL)
@@ -1147,26 +1155,74 @@ aloop:
                        }
                        return -1;
                }
-               peer_dn = MemoryContextAlloc(TopMemoryContext, bio_buf->length 
+ 1);
-               memcpy(peer_dn, bio_buf->data, bio_buf->length);
                len = bio_buf->length;
-               BIO_free(bio);
-               peer_dn[len] = '\0';
-               if (len != strlen(peer_dn))
+               if (len > 0)
                {
-                       ereport(COMMERROR,
-                                       (errcode(ERRCODE_PROTOCOL_VIOLATION),
-                                        errmsg("SSL certificate's 
distinguished name contains embedded null")));
-                       pfree(peer_dn);
-                       if (port->peer_cn != NULL)
+                       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))
                        {
-                               pfree(port->peer_cn);
-                               port->peer_cn = NULL;
+                               ereport(COMMERROR,
+                                               
(errcode(ERRCODE_PROTOCOL_VIOLATION),
+                                                errmsg("SSL certificate's 
distinguished name contains embedded null")));
+                               pfree(peer_dn);
+                               if (port->peer_cn != NULL)
+                               {
+                                       pfree(port->peer_cn);
+                                       port->peer_cn = NULL;
+                               }
+                               return -1;
                        }
-                       return -1;
+
+                       port->peer_dn = peer_dn;
                }
+               else
+               {
+                       /*
+                        * An empty subject means there is no DN to record, so 
leave
+                        * peer_dn NULL.  The X.509-SVID specification allows 
this when a
+                        * (critical) URI subjectAltName is present.
+                        * clientname=CN or clientname=DN authentication will 
reject such
+                        * a certificate later in CheckCertAuth(), since there 
is no name to compare.
+                        */
+                       BIO_free(bio);
+               }
+
+               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;
+
+                       /*
+                        * Count the URI subjectAltNames directly in 
peer_uri_count and
+                        * keep the first one.  Only one URI is ever accepted 
(see
+                        * CheckCertAuth()), so later URIs are never used as an 
identity;
+                        * the count alone is enough to reject certificates 
with more than
+                        * one.
+                        */
+                       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);
+                       }
 
-               port->peer_dn = peer_dn;
+                       /*
+                        * If a URI was present but could not be converted 
(e.g. embedded
+                        * NUL), fail closed: reset the count so the 
certificate is
+                        * rejected.
+                        */
+                       if (port->peer_uri_count > 0 && port->peer_uri == NULL)
+                               port->peer_uri_count = 0;
+
+                       sk_GENERAL_NAME_pop_free(peer_san, GENERAL_NAME_free);
+               }
 
                port->peer_cert_valid = true;
        }
@@ -1202,6 +1258,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
@@ -2433,6 +2496,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 c0d8a9d8a00..852364d1ba2 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,
@@ -2835,7 +2839,6 @@ check_usermap(const char *usermap_name,
        return found_entry ? STATUS_OK : STATUS_ERROR;
 }
 
-
 /*
  * Read the ident config file and create a List of IdentLine records for
  * the contents.
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 4aa6258a345..58e497e018b 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 921b2daa4ff..545626e2389 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;
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 00000000000..ae0ed1c400c
--- /dev/null
+++ b/src/test/ssl/conf/client-uri-multi.config
@@ -0,0 +1,19 @@
+# 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.
+
+[ 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/not-the-user
+URI.2                  = spiffe://postgresql.example/ns/default/sa/ssltestuser
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 00000000000..cee1db84398
--- /dev/null
+++ b/src/test/ssl/conf/client-uri-nosubject.config
@@ -0,0 +1,14 @@
+# An OpenSSL format CSR config file for creating a client certificate with
+# an empty subject and a single, critical URI subject alternative name,
+# as issued by Istio's istiod, for example.  The X.509-SVID specification
+# requires the URI SAN to be critical when the subject is empty.
+
+[ 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 00000000000..ea7175e0410
--- /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 00000000000..2783df70d4e
--- /dev/null
+++ b/src/test/ssl/ssl/client-uri-multi.crt
@@ -0,0 +1,22 @@
+-----BEGIN CERTIFICATE-----
+MIIDmjCCAoKgAwIBAgIIICYIMRc4BgUwDQYJKoZIhvcNAQELBQAwQjFAMD4GA1UE
+Aww3VGVzdCBDQSBmb3IgUG9zdGdyZVNRTCBTU0wgcmVncmVzc2lvbiB0ZXN0IGNs
+aWVudCBjZXJ0czAgFw0yMzA2MjkwMTAxMDFaGA8yMDUwMDEwMTAxMDEwMVowFjEU
+MBIGA1UEAwwLc3NsdGVzdHVzZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
+AoIBAQC1DIM8v/RJlfpjl8Sb7M3xY27Tv+m+YlJ5NxNWDUnlGiBokzsoKZ9chaoQ
+yDP9fq5fOFpDFhWwgo5WskHfb/RXjuDaro2myZsM+mMuZcNWM0ATZpexlDd9Sh79
+Pz+eU0kP34ZcPr9QwhmgHRsv4sAbf0DTC8ICnH57IryeTa2fwJ6wsySTzf3e9lus
+3svhyZzXW3EsXH212KPtWlWRqcqbi6DKPFfrWuOpi1dNHDK+pmK94S5N9eSYsUap
+kh0U6t8GO1Z83jqvkgAt4Lhsu+cfOpEJOzRlinC2m3a+X3pXKMXx8yFPK7ksin+i
+OLz1/115pRYwyL1wHAsbK9VpuI+/AgMBAAGjgb0wgboweAYDVR0RBHEwb4Y2c3Bp
+ZmZlOi8vcG9zdGdyZXNxbC5leGFtcGxlL25zL2RlZmF1bHQvc2Evbm90LXRoZS11
+c2VyhjVzcGlmZmU6Ly9wb3N0Z3Jlc3FsLmV4YW1wbGUvbnMvZGVmYXVsdC9zYS9z
+c2x0ZXN0dXNlcjAdBgNVHQ4EFgQUJpgtW87f9rqDisg1391xThjWsOkwHwYDVR0j
+BBgwFoAUn6Tr0smZ3rADsDA0IoFhFUu9rTYwDQYJKoZIhvcNAQELBQADggEBACwc
+JuplInvK+z6Zf6HPPYc0S+owaVi4hy5BBH2gvm6s1HTAJFInXji9UVdG8cBCukXX
+DiOudCcf2C/sl4VqXnmEECyyRctKrW9NFhYs07VUueIJ86wVWrntsks+qTT7ZDK4
+WjO2+UrI71zQZB/QTiraPRJlLtz/AS68K8w95+sNcwuMIqn+3XG8Qs3dQoXv5Z1f
+Y/mN7e9hovW067cL4UTprC6MOB6+UbqjbVM6mWaI/PvSe5rxawD3sdidKWnWIwIB
+m5g9sBbmWOFWM3KHCXIHYDPKM0wNb1n2GEA1Z90L8hUfskq5aYIvYpqnFNfiFFBH
+h1d/rf++/z/LsZwDEEA=
+-----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 00000000000..ae05e25b045
--- /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 00000000000..2391c820c12
--- /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 00000000000..53f00038725
--- /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 00000000000..612dd96e41d
--- /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 00000000000..ae05e25b045
--- /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 f32c53a76a1..c310003730f 100644
--- a/src/test/ssl/sslfiles.mk
+++ b/src/test/ssl/sslfiles.mk
@@ -35,20 +35,21 @@ 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
 
 #
 # To add a new non-standard certificate, add it to SPECIAL_CERTS and then add
 # a recipe for creating it to the "Special-case certificates" section below.
 #
-SPECIAL_CERTS := ssl/server-rsapss.crt
+SPECIAL_CERTS := ssl/server-rsapss.crt ssl/client-uri-nosubject.crt
 
 # Likewise for non-standard keys
 SPECIAL_KEYS := ssl/server-password.key \
        ssl/client-der.key \
        ssl/client-encrypted-pem.key \
        ssl/client-encrypted-der.key \
-       ssl/server-rsapss.key
+       ssl/server-rsapss.key \
+       ssl/client-uri-nosubject.key
 
 #
 # These files are just concatenations of other files. You can add new ones to
@@ -116,6 +117,11 @@ ssl/server-password.key: ssl/server-cn-only.key
 ssl/server-rsapss.key:
        $(OPENSSL) genpkey -algorithm rsa-pss -out $@
 
+# Key for the client certificate with an empty subject (client-uri-nosubject)
+ssl/client-uri-nosubject.key:
+       $(OPENSSL) genrsa -out $@ 2048
+       chmod 0600 $@
+
 # DER-encoded version of client.key
 ssl/client-der.key: ssl/client.key
        $(OPENSSL) rsa -in $< -outform DER -out $@
@@ -196,8 +202,16 @@ $(SERVER_CERTS): ssl/%.crt: ssl/%.csr conf/%.config 
conf/cas.config ssl/server_c
 $(CLIENT_CERTS): ssl/%.crt: ssl/%.csr conf/%.config conf/cas.config 
ssl/client_ca.crt | ssl/new_certs_dir $(client_ca_state_files)
        $(OPENSSL) ca -batch -config conf/cas.config -name client_ca -notext 
-in $< -out $@
 
+# Client certificate with an empty subject and a single, critical URI
+# subjectAltName.  Unlike the standard client certificates, this needs
+# an explicit "-subj /" when generating the CSR, since the config file
+# alone cannot express an empty subject.
+ssl/client-uri-nosubject.crt: ssl/client-uri-nosubject.key 
conf/client-uri-nosubject.config conf/cas.config ssl/client_ca.crt | 
ssl/new_certs_dir $(client_ca_state_files)
+       $(OPENSSL) req -new -utf8 -key $< -subj / -out 
ssl/client-uri-nosubject.csr -config conf/client-uri-nosubject.config
+       $(OPENSSL) ca -batch -config conf/cas.config -name client_ca -notext 
-in ssl/client-uri-nosubject.csr -out $@
+
 # The CSRs don't need to persist after a build.
-.INTERMEDIATE: $(CERTIFICATES:%=ssl/%.csr)
+.INTERMEDIATE: $(CERTIFICATES:%=ssl/%.csr) ssl/client-uri-nosubject.csr
 ssl/%.csr: ssl/%.key conf/%.config
        $(OPENSSL) req -new -utf8 -key $< -out $@ -config conf/$*.config
 
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index cb7c2a06193..74e35e39087 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -1007,6 +1007,57 @@ $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"/);
+
+$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 6060771c1a8..91b0cbd24d5 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 4400a432f42..1c79c1abc5a 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.45.1

Reply via email to