On 18/09/2026 17:58, Jacob Champion wrote:
On Thu, Sep 17, 2026 at 3:10 AM Florin Irion <[email protected]> wrote:
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.
I'd like opinions from the crowd on the use of X509-SVID as a
governing spec here. I don't mind the 1:1 requirement for now; it
simplifies things. But if someone comes forward proposing multiple-URI
support, are we going to reject it since the original use case was
SPIFFE? Also, this patch doesn't seem to perform the other required
checks for X509-SVID validation [1], so it seems a little weird to
cite SPIFFE in the comments and tests and documentation.
The single-URI requirement is a design simplification, not a spec
requirement.
SPIFFE motivated the feature but doesn't necessarily govern it. If someone
later proposes matching against multiple URI SANs, it can be evaluated
on its
own merits.
v2 drops all X509-SVID citations from comments, tests, and docs, since
we don't
implement the other X509-SVID validation checks.
Partial review follows (I have not read in detail):
+ * 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.
From a skim, it looks like other parts of the code assume either
peer_dn or peer_cn is set; they'll need to be changed too.
With V2 I don't see any other places where we assume peer_dn or peer_cn
is set.
The growing indentation in be_tls_open_server is, I think, a smell.
We've reached rule of three (or at least 2.5 depending on your view of
peer_dn and peer_cn) and I think this code is going to need to be
restructured a bit in order to absorb the new use case. I don't have
anything in particular in mind yet.
I restructured the code adding be_tls_extract_peer_cert_names()
The X.509-SVID specification
+# requires the URI SAN to be critical when the subject is empty.
We can cite RFC 5280 for the criticality requirement (X509-SVID defers
to it anyway).
done.
+# 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.
That's fun. Let's just pull the `-subj /` into a default-empty
variable that the new CSR can tweak. That should get rid of most of
the new copy-paste boilerplate, I think.
done.
For SPIFFE deployments, configure
+ <literal>ssl_ca_file</literal> with a single trust domain's bundle;
+ </para>
Looks like this was cut off?
Yup, fixed.
+ /*
+ * 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)
I think it's really important that we keep unedited LLMisms out of the
security code. (And elsewhere too.)
Like, definitely push back if I've misattributed this, but I imagine
that you wouldn't have willingly typed three lines of comments in
order to say "we require exactly one URI", and I don't really want to
read three lines of comments that say that. We need the
signal-to-noise ratio to be very high here.
True.
I tend to be very succinct and sometimes I fear I don't give enough context,
but I agree here the LLM added way too much.
Fixed.
- if (peer_username == NULL ||
- strlen(peer_username) <= 0)
+ if (peer_username == NULL || strlen(peer_username) <= 0)
Drive-by cleanup is usually very welcome (thank you), but for security
patches I think it's good to keep unrelated code motion separate from
new implementation.
Makes sense. Removed.
Thank you for checking this!
V2 attached.
Florin
--
_/**/__/**/__/**/__/**/__/* Florin Irion */__/**/__/**/__/**/__/**/__/**/_
/https://www.enterprisedb.com/
From e6fa099780af641603321fa0c2308abfaac96639 Mon Sep 17 00:00:00 2001
From: Florin Irion <[email protected]>
Date: Thu, 17 Sep 2026 00:06:27 +0200
Subject: [PATCH v2] 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. Authentication
requires the certificate to carry exactly one URI SAN, so that the
authenticated identity is unambiguous: 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, 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 and the caveat that 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 | 25 +-
src/backend/libpq/auth.c | 58 +++--
src/backend/libpq/be-secure-openssl.c | 230 +++++++++++++-----
src/backend/libpq/be-secure.c | 5 +-
src/backend/libpq/hba.c | 4 +
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 | 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 | 51 ++++
src/test/ssl/t/SSL/Backend/OpenSSL.pm | 3 +-
src/test/ssl/t/SSL/Server.pm | 6 +-
20 files changed, 506 insertions(+), 88 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..75eeaaa7d2 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,23 @@ 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 CA bundle;
+ SPIFFE federation is not supported.
+ </para>
</listitem>
</varlistentry>
diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c
index 12bf153d66..cf0cd3cbe8 100644
--- a/src/backend/libpq/auth.c
+++ b/src/backend/libpq/auth.c
@@ -2746,6 +2746,18 @@ CheckCertAuth(Port *port)
break;
case clientCertCN:
peer_username = port->peer_cn;
+ break;
+ case clientCertURI:
+ /* the certificate must contain exactly one URI SAN */
+ 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 */
@@ -2761,29 +2773,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 +2824,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..4ade3b1733 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,107 @@ 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;
+ if (!be_tls_extract_peer_cert_names(port, port->peer))
+ return -1;
+ port->peer_cert_valid = true;
+ }
- 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;
+ return 0;
+}
- 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);
+/*
+ * Extract the Common Name, Distinguished Name, and first URI subjectAltName
+ * 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;
- peer_cn = MemoryContextAlloc(TopMemoryContext, len + 1);
- memcpy(peer_cn, peer_cn_internal, len);
- peer_cn[len] = '\0';
+ STACK_OF(GENERAL_NAME) * peer_san;
- /*
- * 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;
- }
+ 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);
- port->peer_cn = peer_cn;
+ peer_cn = MemoryContextAlloc(TopMemoryContext, len + 1);
+ memcpy(peer_cn, peer_cn_internal, len);
+ peer_cn[len] = '\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 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 +1188,45 @@ aloop:
pfree(port->peer_cn);
port->peer_cn = NULL;
}
- return -1;
+ return false;
}
port->peer_dn = peer_dn;
+ }
+ 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);
+ }
- port->peer_cert_valid = true;
+ peer_san = (STACK_OF(GENERAL_NAME) *) X509_get_ext_d2i(peer,
NID_subject_alt_name, NULL, NULL);
+ if (peer_san)
+ {
+ int san_len = sk_GENERAL_NAME_num(peer_san);
+ int i;
+
+ /* count the URI SANs and keep the first 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);
+ }
+
+ /* fail closed if the URI could not be converted */
+ 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);
}
- return 0;
+ return true;
}
void
@@ -1202,6 +1257,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 +2495,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/be-secure.c b/src/backend/libpq/be-secure.c
index 86ceea72e6..d018fcc587 100644
--- a/src/backend/libpq/be-secure.c
+++ b/src/backend/libpq/be-secure.c
@@ -155,9 +155,10 @@ secure_open_server(Port *port)
}
ereport(DEBUG2,
- (errmsg_internal("SSL connection from DN:\"%s\"
CN:\"%s\"",
+ (errmsg_internal("SSL connection from DN:\"%s\"
CN:\"%s\" URI:\"%s\"",
port->peer_dn ?
port->peer_dn : "(anonymous)",
- port->peer_cn ?
port->peer_cn : "(anonymous)")));
+ port->peer_cn ?
port->peer_cn : "(anonymous)",
+ port->peer_uri ?
port->peer_uri : "(anonymous)")));
return r;
#else
return 0;
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..545626e238 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 0000000000..ae0ed1c400
--- /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 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..2783df70d4
--- /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 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..7cd4126445 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 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