Subject: Re: [PATCH] Accept IP addresses in server certificate SANs

From: Kyotaro Horiguchi <[email-masked]>
Date: Wed, 23 Mar 2022 14:20:07 +0900 (JST)

At Tue, 22 Mar 2022 20:42:37 +0000, Jacob Champion <[email-masked]> wrote in 
> Thanks, looks like I had some old header dependencies left over from
> several versions ago. Fixed in v9.

Thanks!  Looks perfect.

> v9 contains the bare minimum but I don't think it's quite enough. How
> much of the behavior (and edge cases) do you think we should detail
> here? All of it?

I tried to write out the doc part.  What do you think about it?

regards.

-- 
Kyotaro Horiguchi
NTT Open Source Software Center
diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 3998b1781b..13e3e63768 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -8342,16 +8342,31 @@ ldap://ldap.acme.com/cn=dbserver,cn=hosts?pgconnectinfo?base?(objectclass=*)
 
   <para>
    In <literal>verify-full</literal> mode, the host name is matched against the
-   certificate's Subject Alternative Name attribute(s), or against the
-   Common Name attribute if no Subject Alternative Name of type <literal>dNSName</literal> is
+   certificate's Subject Alternative Name attribute(s) (SAN), or against the
+   Common Name attribute if no SAN of type <literal>dNSName</literal> is
    present.  If the certificate's name attribute starts with an asterisk
    (<literal>*</literal>), the asterisk will be treated as
    a wildcard, which will match all characters <emphasis>except</emphasis> a dot
    (<literal>.</literal>). This means the certificate will not match subdomains.
    If the connection is made using an IP address instead of a host name, the
-   IP address will be matched (without doing any DNS lookups).
+   IP address will be matched (without doing any DNS lookups) against SANs of
+   type <literal>iPAddress</literal> or <literal>dNSName</literal>.  If no
+   <literal>ipAddress</literal> SAN is present and no
+   matching <literal>dNSName</literal> SAN is present, the host IP address is
+   matched against the Common Name attribute.
   </para>
 
+  <note>
+    <para>
+      For backward compatibility with earlier versions of PostgreSQL, the host
+      IP address is verified in a manner different
+      from <ulink url="https://tools.ietf.org/html/rfc6125">RFC 6125</ulink>.
+      The host IP address is always matched against <literal>dNSName</literal>
+      SANs as well as <literal>iPAdress</literal> SANs, and can be matched
+      against the Common Name attribute for a certain condition.
+   </para>
+  </note>
+
   <para>
    To allow server certificate verification, one or more root certificates
    must be placed in the file <filename>~/.postgresql/root.crt</filename>

---

From: Jacob Champion <[email-masked]>
Date: Wed, 23 Mar 2022 23:52:06 +0000

> I tried to write out the doc part.  What do you think about it?

I like it, thanks! I've applied that in v10, with a tweak to two
iPAddress spellings and a short expansion of the condition in the Note,
and I've added you as a co-author to 0002.

--Jacob
From 84a107da33b7d901cb318f8c2fd140644fbc5100 Mon Sep 17 00:00:00 2001
From: Jacob Champion <[email-masked]>
Date: Wed, 24 Nov 2021 14:46:11 -0800
Subject: [PATCH v10 1/3] Move inet_net_pton() to src/port

This will be helpful for IP address verification in libpq.
---
 src/backend/utils/adt/Makefile                  |  1 -
 src/include/port.h                              |  3 +++
 src/include/utils/builtins.h                    |  4 ----
 src/port/Makefile                               |  1 +
 src/{backend/utils/adt => port}/inet_net_pton.c | 16 +++++++++++++++-
 src/tools/msvc/Mkvcbuild.pm                     |  2 +-
 6 files changed, 20 insertions(+), 7 deletions(-)
 rename src/{backend/utils/adt => port}/inet_net_pton.c (96%)

diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 41b486bcef..d173d52157 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -43,7 +43,6 @@ OBJS = \
 	geo_selfuncs.o \
 	geo_spgist.o \
 	inet_cidr_ntop.o \
-	inet_net_pton.o \
 	int.o \
 	int8.o \
 	json.o \
diff --git a/src/include/port.h b/src/include/port.h
index 3d103a2b31..2852e5b58b 100644
--- a/src/include/port.h
+++ b/src/include/port.h
@@ -515,6 +515,9 @@ extern int	pg_codepage_to_encoding(UINT cp);
 extern char *pg_inet_net_ntop(int af, const void *src, int bits,
 							  char *dst, size_t size);
 
+/* port/inet_net_pton.c */
+extern int	pg_inet_net_pton(int af, const char *src, void *dst, size_t size);
+
 /* port/pg_strong_random.c */
 extern void pg_strong_random_init(void);
 extern bool pg_strong_random(void *buf, size_t len);
diff --git a/src/include/utils/builtins.h b/src/include/utils/builtins.h
index 666e545496..67b9326dc8 100644
--- a/src/include/utils/builtins.h
+++ b/src/include/utils/builtins.h
@@ -93,10 +93,6 @@ extern int	xidLogicalComparator(const void *arg1, const void *arg2);
 extern char *pg_inet_cidr_ntop(int af, const void *src, int bits,
 							   char *dst, size_t size);
 
-/* inet_net_pton.c */
-extern int	pg_inet_net_pton(int af, const char *src,
-							 void *dst, size_t size);
-
 /* network.c */
 extern double convert_network_to_scalar(Datum value, Oid typid, bool *failure);
 extern Datum network_scan_first(Datum in);
diff --git a/src/port/Makefile b/src/port/Makefile
index bfe1feb0d4..c3ae7b3d5c 100644
--- a/src/port/Makefile
+++ b/src/port/Makefile
@@ -43,6 +43,7 @@ OBJS = \
 	bsearch_arg.o \
 	chklocale.o \
 	inet_net_ntop.o \
+	inet_net_pton.o \
 	noblock.o \
 	path.o \
 	pg_bitutils.o \
diff --git a/src/backend/utils/adt/inet_net_pton.c b/src/port/inet_net_pton.c
similarity index 96%
rename from src/backend/utils/adt/inet_net_pton.c
rename to src/port/inet_net_pton.c
index d3221a1313..bae50ba67e 100644
--- a/src/backend/utils/adt/inet_net_pton.c
+++ b/src/port/inet_net_pton.c
@@ -14,14 +14,18 @@
  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
  * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  *
- *	  src/backend/utils/adt/inet_net_pton.c
+ *	  src/port/inet_net_pton.c
  */
 
 #if defined(LIBC_SCCS) && !defined(lint)
 static const char rcsid[] = "Id: inet_net_pton.c,v 1.4.2.3 2004/03/17 00:40:11 marka Exp $";
 #endif
 
+#ifndef FRONTEND
 #include "postgres.h"
+#else
+#include "postgres_fe.h"
+#endif
 
 #include <sys/socket.h>
 #include <netinet/in.h>
@@ -29,9 +33,19 @@ static const char rcsid[] = "Id: inet_net_pton.c,v 1.4.2.3 2004/03/17 00:40:11 m
 #include <assert.h>
 #include <ctype.h>
 
+#ifndef FRONTEND
 #include "utils/builtins.h" /* pgrminclude ignore */	/* needed on some
 														 * platforms */
 #include "utils/inet.h"
+#else
+/*
+ * In a frontend build, we can't include inet.h, but we still need to have
+ * sensible definitions of these two constants.  Note that pg_inet_net_ntop()
+ * assumes that PGSQL_AF_INET is equal to AF_INET.
+ */
+#define PGSQL_AF_INET	(AF_INET + 0)
+#define PGSQL_AF_INET6	(AF_INET + 1)
+#endif

 static int	inet_net_pton_ipv4(const char *src, u_char *dst);
diff --git a/src/tools/msvc/Mkvcbuild.pm b/src/tools/msvc/Mkvcbuild.pm
index 441d6ae6bf..667b173ec3 100644
--- a/src/tools/msvc/Mkvcbuild.pm
+++ b/src/tools/msvc/Mkvcbuild.pm
@@ -100,7 +100,7 @@ sub mkvcbuild
 
 	our @pgportfiles = qw(
 	  chklocale.c explicit_bzero.c fls.c getpeereid.c getrusage.c inet_aton.c
-	  getaddrinfo.c gettimeofday.c inet_net_ntop.c kill.c open.c
+	  getaddrinfo.c gettimeofday.c inet_net_ntop.c inet_net_pton.c kill.c open.c
 	  snprintf.c strlcat.c strlcpy.c dirmod.c noblock.c path.c
 	  dirent.c dlopen.c getopt.c getopt_long.c link.c
 	  pread.c preadv.c pwrite.c pwritev.c pg_bitutils.c
-- 
2.25.1

From 6ad0b390f11a025045be278a789d72547775fe77 Mon Sep 17 00:00:00 2001
From: Jacob Champion <[email-masked]>
Date: Thu, 18 Nov 2021 15:36:18 -0800
Subject: [PATCH v10 2/3] libpq: allow IP address SANs in server certs

The current implementation supports exactly one IP address in a server
certificate's Common Name, which is brittle (the strings must match
exactly).  This patch adds support for IPv4 and IPv6 addresses in a
server's Subject Alternative Names.

Per discussion on-list:

- If the client's expected host is an IP address, we allow fallback to
  the Subject Common Name if an iPAddress SAN is not present, even if
  a dNSName is present. This matches the behavior of NSS, in violation
  of the relevant RFCs.

- We also, counter-intuitively, match IP addresses embedded in dNSName
  SANs. From inspection this appears to have been the behavior since the
  SAN matching feature was introduced in acd08d76.

- Unlike NSS, we don't map IPv4 to IPv6 addresses, or vice-versa.

- Move PGSQL_AF_INET* to inet-common.h to reduce copy-paste.

Co-authored-by: Kyotaro Horiguchi <[email-masked]>
---
 doc/src/sgml/libpq.sgml                       |  21 ++-
 src/include/common/inet-common.h              |  35 +++++
 src/include/utils/inet.h                      |  13 +-
 src/interfaces/libpq/fe-secure-common.c       | 104 +++++++++++++
 src/interfaces/libpq/fe-secure-common.h       |   4 +
 src/interfaces/libpq/fe-secure-openssl.c      | 139 ++++++++++++++++--
 src/port/inet_net_ntop.c                      |  12 +-
 src/port/inet_net_pton.c                      |  10 +-
 .../conf/server-cn-and-ip-alt-names.config    |  24 +++
 src/test/ssl/conf/server-ip-alt-names.config  |  19 +++
 .../conf/server-ip-cn-and-alt-names.config    |  21 +++
 .../server-ip-cn-and-dns-alt-names.config     |  21 +++
 src/test/ssl/conf/server-ip-cn-only.config    |  12 ++
 src/test/ssl/conf/server-ip-in-dnsname.config |  18 +++
 .../ssl/ssl/server-cn-and-ip-alt-names.crt    |  20 +++
 .../ssl/ssl/server-cn-and-ip-alt-names.key    |  27 ++++
 src/test/ssl/ssl/server-ip-alt-names.crt      |  19 +++
 src/test/ssl/ssl/server-ip-alt-names.key      |  27 ++++
 .../ssl/ssl/server-ip-cn-and-alt-names.crt    |  19 +++
 .../ssl/ssl/server-ip-cn-and-alt-names.key    |  27 ++++
 .../ssl/server-ip-cn-and-dns-alt-names.crt    |  20 +++
 .../ssl/server-ip-cn-and-dns-alt-names.key    |  27 ++++
 src/test/ssl/ssl/server-ip-cn-only.crt        |  18 +++
 src/test/ssl/ssl/server-ip-cn-only.key        |  27 ++++
 src/test/ssl/ssl/server-ip-in-dnsname.crt     |  18 +++
 src/test/ssl/ssl/server-ip-in-dnsname.key     |  27 ++++
 src/test/ssl/sslfiles.mk                      |   6 +
 src/test/ssl/t/001_ssltests.pl                | 106 ++++++++++++-
 28 files changed, 792 insertions(+), 49 deletions(-)
 create mode 100644 src/include/common/inet-common.h
 create mode 100644 src/test/ssl/conf/server-cn-and-ip-alt-names.config
 create mode 100644 src/test/ssl/conf/server-ip-alt-names.config
 create mode 100644 src/test/ssl/conf/server-ip-cn-and-alt-names.config
 create mode 100644 src/test/ssl/conf/server-ip-cn-and-dns-alt-names.config
 create mode 100644 src/test/ssl/conf/server-ip-cn-only.config
 create mode 100644 src/test/ssl/conf/server-ip-in-dnsname.config
 create mode 100644 src/test/ssl/ssl/server-cn-and-ip-alt-names.crt
 create mode 100644 src/test/ssl/ssl/server-cn-and-ip-alt-names.key
 create mode 100644 src/test/ssl/ssl/server-ip-alt-names.crt
 create mode 100644 src/test/ssl/ssl/server-ip-alt-names.key
 create mode 100644 src/test/ssl/ssl/server-ip-cn-and-alt-names.crt
 create mode 100644 src/test/ssl/ssl/server-ip-cn-and-alt-names.key
 create mode 100644 src/test/ssl/ssl/server-ip-cn-and-dns-alt-names.crt
 create mode 100644 src/test/ssl/ssl/server-ip-cn-and-dns-alt-names.key
 create mode 100644 src/test/ssl/ssl/server-ip-cn-only.crt
 create mode 100644 src/test/ssl/ssl/server-ip-cn-only.key
 create mode 100644 src/test/ssl/ssl/server-ip-in-dnsname.crt
 create mode 100644 src/test/ssl/ssl/server-ip-in-dnsname.key

index 3998b1781b..103f4240c3 100644
 
   <para>
+   <literal>iPAddress</literal> SAN is present and no
 
+  <note>
+    <para>
+      For backward compatibility with earlier versions of PostgreSQL, the host
+      IP address is verified in a manner different
+      from <ulink url="https://tools.ietf.org/html/rfc6125">RFC 6125</ulink>.
+      The host IP address is always matched against <literal>dNSName</literal>
+      SANs as well as <literal>iPAddress</literal> SANs, and can be matched
+      against the Common Name attribute if no relevant SANs exist.
+   </para>
+  </note>
+
   <para>
    To allow server certificate verification, one or more root certificates
    must be placed in the file <filename>~/.postgresql/root.crt</filename>
diff --git a/src/include/common/inet-common.h b/src/include/common/inet-common.h
new file mode 100644
index 0000000000..3ad72261b6
--- /dev/null
+++ b/src/include/common/inet-common.h
@@ -0,0 +1,35 @@
+/*-------------------------------------------------------------------------
+ *
+ * inet-common.h
+ *
+ *	  Common code for clients of the inet_* APIs.
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/common/inet-common.h
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#ifndef INET_COMMON_H
+#define INET_COMMON_H
+
+/*
+ * We use these values for the "family" field of inet_struct.
+ *
+ * Referencing all of the non-AF_INET types to AF_INET lets us work on
+ * machines which may not have the appropriate address family (like
+ * inet6 addresses when AF_INET6 isn't present) but doesn't cause a
+ * dump/reload requirement.  Pre-7.4 databases used AF_INET for the family
+ * type on disk.
+ *
+ * In a frontend build, we can't include inet.h, but we still need to have
+ * sensible definitions of these two constants.  (Frontend clients should
+ * include this header directly.)  Note that pg_inet_net_ntop()
+ * assumes that PGSQL_AF_INET is equal to AF_INET.
+ */
+#define PGSQL_AF_INET	(AF_INET + 0)
+#define PGSQL_AF_INET6	(AF_INET + 1)
+
+#endif							/* INET_COMMON_H */
diff --git a/src/include/utils/inet.h b/src/include/utils/inet.h
index 3073c0307e..c56c016987 100644
--- a/src/include/utils/inet.h
+++ b/src/include/utils/inet.h
@@ -14,6 +14,7 @@
 #ifndef INET_H
 #define INET_H
 
+#include "common/inet-common.h"
 #include "fmgr.h"
 
 /*
@@ -27,18 +28,6 @@ typedef struct
 	unsigned char ipaddr[16];	/* up to 128 bits of address */
 } inet_struct;
 
-/*
- * We use these values for the "family" field.
- *
- * Referencing all of the non-AF_INET types to AF_INET lets us work on
- * machines which may not have the appropriate address family (like
- * inet6 addresses when AF_INET6 isn't present) but doesn't cause a
- * dump/reload requirement.  Pre-7.4 databases used AF_INET for the family
- * type on disk.
- */
-#define PGSQL_AF_INET	(AF_INET + 0)
-#define PGSQL_AF_INET6	(AF_INET + 1)
-
 /*
  * Both INET and CIDR addresses are represented within Postgres as varlena
  * objects, ie, there is a varlena header in front of the struct type
diff --git a/src/interfaces/libpq/fe-secure-common.c b/src/interfaces/libpq/fe-secure-common.c
index bd46f08fae..2c0af62afe 100644
--- a/src/interfaces/libpq/fe-secure-common.c
+++ b/src/interfaces/libpq/fe-secure-common.c
@@ -19,9 +19,13 @@
 
 #include "postgres_fe.h"
 
+#include <arpa/inet.h>
+
+#include "common/inet-common.h"
 #include "fe-secure-common.h"
 
 #include "libpq-int.h"
+#include "port.h"
 #include "pqexpbuffer.h"
 
 /*
@@ -144,6 +148,106 @@ pq_verify_peer_name_matches_certificate_name(PGconn *conn,
 	return result;
 }
 
+/*
+ * Check if an IP address from a server's certificate matches the peer's
+ * hostname (which must itself be an IPv4/6 address).
+ *
+ * Returns 1 if the address matches, and 0 if it does not. On error, returns
+ * -1, and sets the libpq error message.
+ *
+ * A string representation of the certificate's IP address is returned in
+ * *store_name. The caller is responsible for freeing it.
+ */
+int
+pq_verify_peer_name_matches_certificate_ip(PGconn *conn,
+										   const unsigned char *ipdata,
+										   size_t iplen,
+										   char **store_name)
+{
+	char	   *addrstr;
+	int			match = 0;
+	char	   *host = conn->connhost[conn->whichhost].host;
+	int			family;
+	char		tmp[sizeof "ffff:ffff:ffff:ffff:ffff:ffff:255.255.255.255"];
+	char		sebuf[PG_STRERROR_R_BUFLEN];
+
+	*store_name = NULL;
+
+	if (!(host && host[0] != '\0'))
+	{
+		appendPQExpBufferStr(&conn->errorMessage,
+							 libpq_gettext("host name must be specified\n"));
+		return -1;
+	}
+
+	/*
+	 * The data from the certificate is in network byte order. Convert our host
+	 * string to network-ordered bytes as well, for comparison. (The host string
+	 * isn't guaranteed to actually be an IP address, so if this conversion
+	 * fails we need to consider it a mismatch rather than an error.)
+	 */
+	if (iplen == 4)
+	{
+		/* IPv4 */
+		struct in_addr addr;
+
+		family = PGSQL_AF_INET;
+
+		/*
+		 * The use of inet_aton() is deliberate; we accept alternative IPv4
+		 * address notations that are accepted by inet_aton() but not
+		 * inet_pton() as server addresses.
+		 */
+		if (inet_aton(host, &addr))
+		{
+			if (memcmp(ipdata, &addr.s_addr, iplen) == 0)
+				match = 1;
+		}
+	}
+	else if (iplen == 16)
+	{
+		/* IPv6 */
+		unsigned char addr[16];
+
+		family = PGSQL_AF_INET6;
+
+		/*
+		 * pg_inet_net_pton() will accept CIDR masks, which we don't want to
+		 * match, so skip the comparison if the host string contains a slash.
+		 */
+		if (!strchr(host, '/')
+			&& pg_inet_net_pton(PGSQL_AF_INET6, host, addr, -1) == 128)
+		{
+			if (memcmp(ipdata, addr, iplen) == 0)
+				match = 1;
+		}
+	}
+	else
+	{
+		/*
+		 * Not IPv4 or IPv6. We could ignore the field, but leniency seems wrong
+		 * given the subject matter.
+		 */
+		appendPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("certificate contains IP address with invalid length %lu\n"),
+						  (unsigned long) iplen);
+		return -1;
+	}
+
+	/* Generate a human-readable representation of the certificate's IP. */
+	addrstr = pg_inet_net_ntop(family, ipdata, 8 * iplen, tmp, sizeof(tmp));
+	if (!addrstr)
+	{
+		appendPQExpBuffer(&conn->errorMessage,
+						  libpq_gettext("could not convert certificate's IP address to string: %s\n"),
+						  strerror_r(errno, sebuf, sizeof(sebuf)));
+		return -1;
+	}
+
+	*store_name = strdup(addrstr);
+	return match;
+}
+
 /*
  * Verify that the server certificate matches the hostname we connected to.
  *
diff --git a/src/interfaces/libpq/fe-secure-common.h b/src/interfaces/libpq/fe-secure-common.h
index 1cca6d785a..d18db7138c 100644
--- a/src/interfaces/libpq/fe-secure-common.h
+++ b/src/interfaces/libpq/fe-secure-common.h
@@ -21,6 +21,10 @@
 extern int	pq_verify_peer_name_matches_certificate_name(PGconn *conn,
 														 const char *namedata, size_t namelen,
 														 char **store_name);
+extern int	pq_verify_peer_name_matches_certificate_ip(PGconn *conn,
+													   const unsigned char *addrdata,
+													   size_t addrlen,
+													   char **store_name);
 extern bool pq_verify_peer_name_matches_certificate(PGconn *conn);
 
 #endif							/* FE_SECURE_COMMON_H */
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index d81218a4cc..7656c3a75e 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -26,6 +26,7 @@
 #include <fcntl.h>
 #include <ctype.h>
 
+#include "common/inet-common.h"
 #include "libpq-fe.h"
 #include "fe-auth.h"
 #include "fe-secure-common.h"
@@ -72,6 +73,9 @@ static int	verify_cb(int ok, X509_STORE_CTX *ctx);
 static int	openssl_verify_peer_name_matches_certificate_name(PGconn *conn,
 															  ASN1_STRING *name,
 															  char **store_name);
+static int	openssl_verify_peer_name_matches_certificate_ip(PGconn *conn,
+															ASN1_OCTET_STRING *addr_entry,
+															char **store_name);
 static void destroy_ssl_system(void);
 static int	initialize_SSL(PGconn *conn);
 static PostgresPollingStatusType open_client_SSL(PGconn *);
@@ -509,6 +513,51 @@ openssl_verify_peer_name_matches_certificate_name(PGconn *conn, ASN1_STRING *nam
 	return pq_verify_peer_name_matches_certificate_name(conn, (const char *) namedata, len, store_name);
 }
 
+/*
+ * OpenSSL-specific wrapper around
+ * pq_verify_peer_name_matches_certificate_ip(), converting the
+ * ASN1_OCTET_STRING into a plain C string.
+ */
+static int
+openssl_verify_peer_name_matches_certificate_ip(PGconn *conn,
+												ASN1_OCTET_STRING *addr_entry,
+												char **store_name)
+{
+	int			len;
+	const unsigned char *addrdata;
+
+	/* Should not happen... */
+	if (addr_entry == NULL)
+	{
+		appendPQExpBufferStr(&conn->errorMessage,
+							 libpq_gettext("SSL certificate's address entry is missing\n"));
+		return -1;
+	}
+
+	/*
+	 * GEN_IPADD is an OCTET STRING containing an IP address in network byte
+	 * order.
+	 */
+#ifdef HAVE_ASN1_STRING_GET0_DATA
+	addrdata = ASN1_STRING_get0_data(addr_entry);
+#else
+	addrdata = ASN1_STRING_data(addr_entry);
+#endif
+	len = ASN1_STRING_length(addr_entry);
+
+	return pq_verify_peer_name_matches_certificate_ip(conn, addrdata, len, store_name);
+}
+
+static bool
+is_ip_address(const char *host)
+{
+	struct in_addr	dummy4;
+	unsigned char	dummy6[16];
+
+	return inet_aton(host, &dummy4)
+		|| (pg_inet_net_pton(PGSQL_AF_INET6, host, dummy6, -1) == 128);
+}
+
 /*
  *	Verify that the server certificate matches the hostname we connected to.
  *
@@ -522,6 +571,36 @@ pgtls_verify_peer_name_matches_certificate_guts(PGconn *conn,
 	STACK_OF(GENERAL_NAME) * peer_san;
 	int			i;
 	int			rc = 0;
+	char	   *host = conn->connhost[conn->whichhost].host;
+	int			host_type;
+	bool		check_cn = true;
+
+	Assert(host && host[0]); /* should be guaranteed by caller */
+
+	/*
+	 * We try to match the NSS behavior here, which is a slight departure from
+	 * the spec but seems to make more intuitive sense:
+	 *
+	 * If connhost contains a DNS name, and the certificate's SANs contain any
+	 * dNSName entries, then we'll ignore the Subject Common Name entirely;
+	 * otherwise, we fall back to checking the CN. (This behavior matches the
+	 * RFC.)
+	 *
+	 * If connhost contains an IP address, and the SANs contain iPAddress
+	 * entries, we again ignore the CN. Otherwise, we allow the CN to match,
+	 * EVEN IF there is a dNSName in the SANs. (RFC 6125 prohibits this: "A
+	 * client MUST NOT seek a match for a reference identifier of CN-ID if the
+	 * presented identifiers include a DNS-ID, SRV-ID, URI-ID, or any
+	 * application-specific identifier types supported by the client.")
+	 *
+	 * NOTE: Prior versions of libpq did not consider iPAddress entries at all,
+	 * so this new behavior might break a certificate that has different IP
+	 * addresses in the Subject CN and the SANs.
+	 */
+	if (is_ip_address(host))
+		host_type = GEN_IPADD;
+	else
+		host_type = GEN_DNS;
 
 	/*
 	 * First, get the Subject Alternative Names (SANs) from the certificate,
@@ -537,38 +616,62 @@ pgtls_verify_peer_name_matches_certificate_guts(PGconn *conn,
 		for (i = 0; i < san_len; i++)
 		{
 			const GENERAL_NAME *name = sk_GENERAL_NAME_value(peer_san, i);
+			char	   *alt_name = NULL;
 
-			if (name->type == GEN_DNS)
+			if (name->type == host_type)
 			{
-				char	   *alt_name;
+				/*
+				 * This SAN is of the same type (IP or DNS) as our host name, so
+				 * don't allow a fallback check of the CN.
+				 */
+				check_cn = false;
+			}
 
+			if (name->type == GEN_DNS)
+			{
 				(*names_examined)++;
 				rc = openssl_verify_peer_name_matches_certificate_name(conn,
 																	   name->d.dNSName,
 																	   &alt_name);
+			}
+			else if (name->type == GEN_IPADD)
+			{
+				(*names_examined)++;
+				rc = openssl_verify_peer_name_matches_certificate_ip(conn,
+																	 name->d.iPAddress,
+																	 &alt_name);
+			}
 
-				if (alt_name)
-				{
-					if (!*first_name)
-						*first_name = alt_name;
-					else
-						free(alt_name);
-				}
+			if (alt_name)
+			{
+				if (!*first_name)
+					*first_name = alt_name;
+				else
+					free(alt_name);
 			}
+
 			if (rc != 0)
+			{
+				/*
+				 * Either we hit an error or a match, and either way we should
+				 * not fall back to the CN.
+				 */
+				check_cn = false;
 				break;
+			}
 		}
 		sk_GENERAL_NAME_pop_free(peer_san, GENERAL_NAME_free);
 	}
 
 	/*
-	 * If there is no subjectAltName extension of type dNSName, check the
+	 * If there is no subjectAltName extension of the matching type, check the
 	 * Common Name.
 	 *
 	 * (Per RFC 2818 and RFC 6125, if the subjectAltName extension of type
-	 * dNSName is present, the CN must be ignored.)
+	 * dNSName is present, the CN must be ignored. We break this rule if host is
+	 * an IP address; see the comment above.)
 	 */
-	if (*names_examined == 0)
+	if (check_cn)
 	{
 		X509_NAME  *subject_name;
 
@@ -581,10 +684,20 @@ pgtls_verify_peer_name_matches_certificate_guts(PGconn *conn,
 												  NID_commonName, -1);
 			if (cn_index >= 0)
 			{
+				char   *common_name = NULL;
+
 				(*names_examined)++;
 				rc = openssl_verify_peer_name_matches_certificate_name(conn,
 																	   X509_NAME_ENTRY_get_data(X509_NAME_get_entry(subject_name, cn_index)),
-																	   first_name);
+																	   &common_name);
+
+				if (common_name)
+				{
+					if (!*first_name)
+						*first_name = common_name;
+					else
+						free(common_name);
+				}
 			}
 		}
 	}
diff --git a/src/port/inet_net_ntop.c b/src/port/inet_net_ntop.c
index b8ad69c390..af28056132 100644
--- a/src/port/inet_net_ntop.c
+++ b/src/port/inet_net_ntop.c
@@ -31,17 +31,7 @@ static const char rcsid[] = "Id: inet_net_ntop.c,v 1.1.2.2 2004/03/09 09:17:27 m
 #include <netinet/in.h>
 #include <arpa/inet.h>
 
-#ifndef FRONTEND
-#include "utils/inet.h"
-#else
-/*
- * In a frontend build, we can't include inet.h, but we still need to have
- * sensible definitions of these two constants.  Note that pg_inet_net_ntop()
- * assumes that PGSQL_AF_INET is equal to AF_INET.
- */
-#define PGSQL_AF_INET	(AF_INET + 0)
-#define PGSQL_AF_INET6	(AF_INET + 1)
-#endif
+#include "common/inet-common.h"

 #define NS_IN6ADDRSZ 16
diff --git a/src/port/inet_net_pton.c b/src/port/inet_net_pton.c
index bae50ba67e..5bae811c6f 100644
--- a/src/port/inet_net_pton.c
+++ b/src/port/inet_net_pton.c
@@ -33,18 +33,10 @@ static const char rcsid[] = "Id: inet_net_pton.c,v 1.4.2.3 2004/03/17 00:40:11 m
 #include <assert.h>
 #include <ctype.h>
 
+#include "common/inet-common.h"
 #ifndef FRONTEND
 #include "utils/builtins.h" /* pgrminclude ignore */	/* needed on some
 														 * platforms */
-#include "utils/inet.h"
-#else
-/*
- * In a frontend build, we can't include inet.h, but we still need to have
- * sensible definitions of these two constants.  Note that pg_inet_net_ntop()
- * assumes that PGSQL_AF_INET is equal to AF_INET.
- */
-#define PGSQL_AF_INET	(AF_INET + 0)
-#define PGSQL_AF_INET6	(AF_INET + 1)
 #endif

diff --git a/src/test/ssl/conf/server-cn-and-ip-alt-names.config b/src/test/ssl/conf/server-cn-and-ip-alt-names.config
new file mode 100644
index 0000000000..a6fa09bad3
--- /dev/null
+++ b/src/test/ssl/conf/server-cn-and-ip-alt-names.config
@@ -0,0 +1,24 @@
+# An OpenSSL format CSR config file for creating a server certificate.
+#
+# This certificate contains a CN and SANs for both IPv4 and IPv6.
+
+
+[ req ]
+distinguished_name     = req_distinguished_name
+req_extensions         = v3_req
+prompt                 = no
+
+[ req_distinguished_name ]
+# Note: According to RFC 2818 and 6125, the CN is ignored, when DNS names are
+# present in the SANs. But they are silent on whether the CN is checked when IP
+# addresses are present.
+CN = common-name.pg-ssltest.test
+OU = PostgreSQL test suite
+
+# For Subject Alternative Names
+[ v3_req ]
+subjectAltName = @alt_names
+
+[ alt_names ]
+IP.1 = 192.0.2.1
+IP.2 = 2001:DB8::1
diff --git a/src/test/ssl/conf/server-ip-alt-names.config b/src/test/ssl/conf/server-ip-alt-names.config
new file mode 100644
index 0000000000..c22f22951a
--- /dev/null
+++ b/src/test/ssl/conf/server-ip-alt-names.config
@@ -0,0 +1,19 @@
+# An OpenSSL format CSR config file for creating a server certificate.
+#
+# This certificate has a two IP-address SANs, and no CN.
+
+[ req ]
+distinguished_name     = req_distinguished_name
+req_extensions         = v3_req
+prompt                 = no
+
+[ req_distinguished_name ]
+OU = PostgreSQL test suite
+
+# For Subject Alternative Names
+[ v3_req ]
+subjectAltName = @alt_names
+
+[ alt_names ]
+IP.1 = 192.0.2.1
+IP.2 = 2001:DB8::1
diff --git a/src/test/ssl/conf/server-ip-cn-and-alt-names.config b/src/test/ssl/conf/server-ip-cn-and-alt-names.config
new file mode 100644
index 0000000000..a4087f0a18
--- /dev/null
+++ b/src/test/ssl/conf/server-ip-cn-and-alt-names.config
@@ -0,0 +1,21 @@
+# An OpenSSL format CSR config file for creating a server certificate.
+#
+# This certificate contains both a CN and SANs in IP address format.
+
+
+[ req ]
+distinguished_name     = req_distinguished_name
+req_extensions         = v3_req
+prompt                 = no
+
+[ req_distinguished_name ]
+CN = 192.0.2.1
+OU = PostgreSQL test suite
+
+# For Subject Alternative Names
+[ v3_req ]
+subjectAltName = @alt_names
+
+[ alt_names ]
+IP.1 = 192.0.2.2
+IP.2 = 2001:DB8::1
diff --git a/src/test/ssl/conf/server-ip-cn-and-dns-alt-names.config b/src/test/ssl/conf/server-ip-cn-and-dns-alt-names.config
new file mode 100644
index 0000000000..7121803b49
--- /dev/null
+++ b/src/test/ssl/conf/server-ip-cn-and-dns-alt-names.config
@@ -0,0 +1,21 @@
+# An OpenSSL format CSR config file for creating a server certificate.
+#
+# This certificate contains both a CN and SANs in IP address format.
+
+
+[ req ]
+distinguished_name     = req_distinguished_name
+req_extensions         = v3_req
+prompt                 = no
+
+[ req_distinguished_name ]
+CN = 192.0.2.1
+OU = PostgreSQL test suite
+
+# For Subject Alternative Names
+[ v3_req ]
+subjectAltName = @alt_names
+
+[ alt_names ]
+DNS.1 = dns1.alt-name.pg-ssltest.test
+DNS.2 = dns2.alt-name.pg-ssltest.test
diff --git a/src/test/ssl/conf/server-ip-cn-only.config b/src/test/ssl/conf/server-ip-cn-only.config
new file mode 100644
index 0000000000..585d8bdae8
--- /dev/null
+++ b/src/test/ssl/conf/server-ip-cn-only.config
@@ -0,0 +1,12 @@
+# An OpenSSL format CSR config file for creating a server certificate.
+#
+
+[ req ]
+distinguished_name     = req_distinguished_name
+prompt                 = no
+
+[ req_distinguished_name ]
+CN = 192.0.2.1
+OU = PostgreSQL test suite
+
+# No Subject Alternative Names
diff --git a/src/test/ssl/conf/server-ip-in-dnsname.config b/src/test/ssl/conf/server-ip-in-dnsname.config
new file mode 100644
index 0000000000..b15649aef7
--- /dev/null
+++ b/src/test/ssl/conf/server-ip-in-dnsname.config
@@ -0,0 +1,18 @@
+# An OpenSSL format CSR config file for creating a server certificate.
+#
+
+[ req ]
+distinguished_name     = req_distinguished_name
+req_extensions         = v3_req
+prompt                 = no
+
+[ req_distinguished_name ]
+OU = PostgreSQL test suite
+
+# For Subject Alternative Names
+[ v3_req ]
+subjectAltName = @alt_names
+
+# Normally IP addresses should not go into a dNSName.
+[ alt_names ]
+DNS.1 = 192.0.2.1
diff --git a/src/test/ssl/ssl/server-cn-and-ip-alt-names.crt b/src/test/ssl/ssl/server-cn-and-ip-alt-names.crt
new file mode 100644
index 0000000000..4e58c85ccb
--- /dev/null
+++ b/src/test/ssl/ssl/server-cn-and-ip-alt-names.crt
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+-----END CERTIFICATE-----
diff --git a/src/test/ssl/ssl/server-cn-and-ip-alt-names.key b/src/test/ssl/ssl/server-cn-and-ip-alt-names.key
new file mode 100644
index 0000000000..837eef996d
--- /dev/null
+++ b/src/test/ssl/ssl/server-cn-and-ip-alt-names.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+-----END RSA PRIVATE KEY-----
diff --git a/src/test/ssl/ssl/server-ip-alt-names.crt b/src/test/ssl/ssl/server-ip-alt-names.crt
new file mode 100644
index 0000000000..8a1bc620bb
--- /dev/null
+++ b/src/test/ssl/ssl/server-ip-alt-names.crt
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+-----END CERTIFICATE-----
diff --git a/src/test/ssl/ssl/server-ip-alt-names.key b/src/test/ssl/ssl/server-ip-alt-names.key
new file mode 100644
index 0000000000..b210b3a991
--- /dev/null
+++ b/src/test/ssl/ssl/server-ip-alt-names.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+-----END RSA PRIVATE KEY-----
diff --git a/src/test/ssl/ssl/server-ip-cn-and-alt-names.crt b/src/test/ssl/ssl/server-ip-cn-and-alt-names.crt
new file mode 100644
index 0000000000..2be02feb03
--- /dev/null
+++ b/src/test/ssl/ssl/server-ip-cn-and-alt-names.crt
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+-----END CERTIFICATE-----
diff --git a/src/test/ssl/ssl/server-ip-cn-and-alt-names.key b/src/test/ssl/ssl/server-ip-cn-and-alt-names.key
new file mode 100644
index 0000000000..54fe80fc68
--- /dev/null
+++ b/src/test/ssl/ssl/server-ip-cn-and-alt-names.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+-----END RSA PRIVATE KEY-----
diff --git a/src/test/ssl/ssl/server-ip-cn-and-dns-alt-names.crt b/src/test/ssl/ssl/server-ip-cn-and-dns-alt-names.crt
new file mode 100644
index 0000000000..23c06da01c
--- /dev/null
+++ b/src/test/ssl/ssl/server-ip-cn-and-dns-alt-names.crt
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+-----END CERTIFICATE-----
diff --git a/src/test/ssl/ssl/server-ip-cn-and-dns-alt-names.key b/src/test/ssl/ssl/server-ip-cn-and-dns-alt-names.key
new file mode 100644
index 0000000000..0ace41e0a1
--- /dev/null
+++ b/src/test/ssl/ssl/server-ip-cn-and-dns-alt-names.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+-----END RSA PRIVATE KEY-----
diff --git a/src/test/ssl/ssl/server-ip-cn-only.crt b/src/test/ssl/ssl/server-ip-cn-only.crt
new file mode 100644
index 0000000000..9bf015cf18
--- /dev/null
+++ b/src/test/ssl/ssl/server-ip-cn-only.crt
@@ -0,0 +1,18 @@
+-----BEGIN CERTIFICATE-----
+-----END CERTIFICATE-----
diff --git a/src/test/ssl/ssl/server-ip-cn-only.key b/src/test/ssl/ssl/server-ip-cn-only.key
new file mode 100644
index 0000000000..1966530e72
--- /dev/null
+++ b/src/test/ssl/ssl/server-ip-cn-only.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+-----END RSA PRIVATE KEY-----
diff --git a/src/test/ssl/ssl/server-ip-in-dnsname.crt b/src/test/ssl/ssl/server-ip-in-dnsname.crt
new file mode 100644
index 0000000000..78ad8d99c8
--- /dev/null
+++ b/src/test/ssl/ssl/server-ip-in-dnsname.crt
@@ -0,0 +1,18 @@
+-----BEGIN CERTIFICATE-----
+-----END CERTIFICATE-----
diff --git a/src/test/ssl/ssl/server-ip-in-dnsname.key b/src/test/ssl/ssl/server-ip-in-dnsname.key
new file mode 100644
index 0000000000..ba319b001e
--- /dev/null
+++ b/src/test/ssl/ssl/server-ip-in-dnsname.key
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+-----END RSA PRIVATE KEY-----
diff --git a/src/test/ssl/sslfiles.mk b/src/test/ssl/sslfiles.mk
index 7ed3a30f5c..d6a3870079 100644
--- a/src/test/ssl/sslfiles.mk
+++ b/src/test/ssl/sslfiles.mk
@@ -22,9 +22,15 @@
 # key/certificate pair will be generated for you, signed by the appropriate CA.
 #
 SERVERS := server-cn-and-alt-names \
+	server-cn-and-ip-alt-names \
 	server-cn-only \
+	server-ip-cn-only \
+	server-ip-cn-and-alt-names \
+	server-ip-cn-and-dns-alt-names \
+	server-ip-in-dnsname \
 	server-single-alt-name \
 	server-multiple-alt-names \
+	server-ip-alt-names \
 	server-no-names \
 	server-revoked
 CLIENTS := client client-dn client-revoked client_ext
diff --git a/src/test/ssl/t/001_ssltests.pl b/src/test/ssl/t/001_ssltests.pl
index 5c5b16fbe7..07e5b71d8c 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -249,6 +249,30 @@ $node->connect_fails(
 	  qr/\Qserver certificate for "common-name.pg-ssltest.test" does not match host name "wronghost.test"\E/
 );
 
+# Test with an IP address in the Common Name. This is a strange corner case that
+# nevertheless is supported, as long as the address string matches exactly.
+switch_server_cert($node, 'server-ip-cn-only');
+
+$common_connstr =
+  "user=ssltestuser dbname=trustdb sslcert=invalid sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
+
+$node->connect_ok("$common_connstr host=192.0.2.1",
+	"IP address in the Common Name");
+
+$node->connect_fails(
+	"$common_connstr host=192.000.002.001",
+	"mismatch between host name and server certificate IP address",
+	expected_stderr =>
+	  qr/\Qserver certificate for "192.0.2.1" does not match host name "192.000.002.001"\E/
+);
+
+# Similarly, we'll also match an IP address in a dNSName SAN. (This is
+# long-standing behavior.)
+switch_server_cert($node, 'server-ip-in-dnsname');
+
+$node->connect_ok("$common_connstr host=192.0.2.1",
+	"IP address in a dNSName");
+
 # Test Subject Alternative Names.
 switch_server_cert($node, 'server-multiple-alt-names');
 
@@ -301,7 +325,54 @@ $node->connect_fails(
 	  qr/\Qserver certificate for "single.alt-name.pg-ssltest.test" does not match host name "deep.subdomain.wildcard.pg-ssltest.test"\E/
 );
 
-# Test server certificate with a CN and SANs. Per RFCs 2818 and 6125, the CN
+# Test certificate with IP addresses in the SANs.
+switch_server_cert($node, 'server-ip-alt-names');
+
+$node->connect_ok(
+	"$common_connstr host=192.0.2.1",
+	"host matching an IPv4 address (Subject Alternative Name 1)");
+
+$node->connect_ok(
+	"$common_connstr host=192.000.002.001",
+	"host matching an IPv4 address in alternate form (Subject Alternative Name 1)");
+
+$node->connect_fails(
+	"$common_connstr host=192.0.2.2",
+	"host not matching an IPv4 address (Subject Alternative Name 1)",
+	expected_stderr =>
+	  qr/\Qserver certificate for "192.0.2.1" (and 1 other name) does not match host name "192.0.2.2"\E/
+);
+
+$node->connect_fails(
+	"$common_connstr host=192.0.2.1/32",
+	"IPv4 host with CIDR mask does not match",
+	expected_stderr =>
+	  qr/\Qserver certificate for "192.0.2.1" (and 1 other name) does not match host name "192.0.2.1\/32"\E/
+);
+
+$node->connect_ok(
+	"$common_connstr host=2001:DB8::1",
+	"host matching an IPv6 address (Subject Alternative Name 2)");
+
+$node->connect_ok(
+	"$common_connstr host=2001:db8:0:0:0:0:0:1",
+	"host matching an IPv6 address in alternate form (Subject Alternative Name 2)");
+
+$node->connect_fails(
+	"$common_connstr host=::1",
+	"host not matching an IPv6 address (Subject Alternative Name 2)",
+	expected_stderr =>
+	  qr/\Qserver certificate for "192.0.2.1" (and 1 other name) does not match host name "::1"\E/
+);
+
+$node->connect_fails(
+	"$common_connstr host=2001:DB8::1/128",
+	"IPv6 host with CIDR mask does not match",
+	expected_stderr =>
+	  qr/\Qserver certificate for "192.0.2.1" (and 1 other name) does not match host name "2001:DB8::1\/128"\E/
+);
+
+# Test server certificate with a CN and DNS SANs. Per RFCs 2818 and 6125, the CN
 # should be ignored when the certificate has both.
 switch_server_cert($node, 'server-cn-and-alt-names');
 
@@ -319,6 +390,39 @@ $node->connect_fails(
 	  qr/\Qserver certificate for "dns1.alt-name.pg-ssltest.test" (and 1 other name) does not match host name "common-name.pg-ssltest.test"\E/
 );
 
+# But we will fall back to check the CN if the SANs contain only IP addresses.
+switch_server_cert($node, 'server-cn-and-ip-alt-names');
+
+$node->connect_ok("$common_connstr host=common-name.pg-ssltest.test",
+	"certificate with both a CN and IP SANs matches CN");
+$node->connect_ok("$common_connstr host=192.0.2.1",
+	"certificate with both a CN and IP SANs matches SAN 1");
+$node->connect_ok("$common_connstr host=2001:db8::1",
+	"certificate with both a CN and IP SANs matches SAN 2");
+
+# And now the same tests, but with IP addresses and DNS names swapped.
+switch_server_cert($node, 'server-ip-cn-and-alt-names');
+
+$node->connect_ok("$common_connstr host=192.0.2.2",
+	"certificate with both an IP CN and IP SANs 1");
+$node->connect_ok("$common_connstr host=2001:db8::1",
+	"certificate with both an IP CN and IP SANs 2");
+$node->connect_fails(
+	"$common_connstr host=192.0.2.1",
+	"certificate with both an IP CN and IP SANs ignores CN",
+	expected_stderr =>
+	  qr/\Qserver certificate for "192.0.2.2" (and 1 other name) does not match host name "192.0.2.1"\E/
+);
+
+switch_server_cert($node, 'server-ip-cn-and-dns-alt-names');
+
+$node->connect_ok("$common_connstr host=192.0.2.1",
+	"certificate with both an IP CN and DNS SANs matches CN");
+$node->connect_ok("$common_connstr host=dns1.alt-name.pg-ssltest.test",
+	"certificate with both an IP CN and DNS SANs matches SAN 1");
+$node->connect_ok("$common_connstr host=dns2.alt-name.pg-ssltest.test",
+	"certificate with both an IP CN and DNS SANs matches SAN 2");
+
 # Finally, test a server certificate that has no CN or SANs. Of course, that's
 # not a very sensible certificate, but libpq should handle it gracefully.
 switch_server_cert($node, 'server-no-names');
-- 
2.25.1

From 1a070a1ac821c8eda4d3a2856e2721b2fa97aaa1 Mon Sep 17 00:00:00 2001
From: Jacob Champion <[email-masked]>
Date: Wed, 5 Jan 2022 15:47:03 -0800
Subject: [PATCH v10 3/3] squash! libpq: allow IP address SANs in server certs

Per review, provide a pg_inet_pton() interface for IPv6 and refactor the
internals accordingly. IPv4 is not yet implemented. The call sites that
just want standard addresses without a CIDR mask have been updated to
use the new function.

Internally, the IPv6 parser now takes an allow_cidr boolean. I've
plumbed that all the way down to getbits() in order to minimize the
amount of code touched, at the expense of an unnecessary function call
in the failing case.
---
 src/include/port.h                       |  1 +
 src/interfaces/libpq/fe-secure-common.c  |  7 +--
 src/interfaces/libpq/fe-secure-openssl.c |  2 +-
 src/port/inet_net_pton.c                 | 66 ++++++++++++++++++------
 4 files changed, 52 insertions(+), 24 deletions(-)

diff --git a/src/include/port.h b/src/include/port.h
index 2852e5b58b..fd046f4c24 100644
--- a/src/include/port.h
+++ b/src/include/port.h
@@ -517,6 +517,7 @@ extern char *pg_inet_net_ntop(int af, const void *src, int bits,
 
 /* port/inet_net_pton.c */
 extern int	pg_inet_net_pton(int af, const char *src, void *dst, size_t size);
+extern int	pg_inet_pton(int af, const char *src, void *dst);
 
 /* port/pg_strong_random.c */
 extern void pg_strong_random_init(void);
diff --git a/src/interfaces/libpq/fe-secure-common.c b/src/interfaces/libpq/fe-secure-common.c
index 2c0af62afe..9c408df369 100644
--- a/src/interfaces/libpq/fe-secure-common.c
+++ b/src/interfaces/libpq/fe-secure-common.c
@@ -211,12 +211,7 @@ pq_verify_peer_name_matches_certificate_ip(PGconn *conn,
 
 		family = PGSQL_AF_INET6;
 
-		/*
-		 * pg_inet_net_pton() will accept CIDR masks, which we don't want to
-		 * match, so skip the comparison if the host string contains a slash.
-		 */
-		if (!strchr(host, '/')
-			&& pg_inet_net_pton(PGSQL_AF_INET6, host, addr, -1) == 128)
+		if (pg_inet_pton(PGSQL_AF_INET6, host, addr) == 1)
 		{
 			if (memcmp(ipdata, addr, iplen) == 0)
 				match = 1;
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index 7656c3a75e..b70bd2eb19 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -555,7 +555,7 @@ is_ip_address(const char *host)
 	unsigned char	dummy6[16];
 
 	return inet_aton(host, &dummy4)
-		|| (pg_inet_net_pton(PGSQL_AF_INET6, host, dummy6, -1) == 128);
+		|| (pg_inet_pton(PGSQL_AF_INET6, host, dummy6) == 1);
 }
 
 /*
diff --git a/src/port/inet_net_pton.c b/src/port/inet_net_pton.c
index 5bae811c6f..ae0d1fd2e3 100644
--- a/src/port/inet_net_pton.c
+++ b/src/port/inet_net_pton.c
@@ -42,8 +42,8 @@ static const char rcsid[] = "Id: inet_net_pton.c,v 1.4.2.3 2004/03/17 00:40:11 m
 
 static int	inet_net_pton_ipv4(const char *src, u_char *dst);
 static int	inet_cidr_pton_ipv4(const char *src, u_char *dst, size_t size);
-static int	inet_net_pton_ipv6(const char *src, u_char *dst);
-static int	inet_cidr_pton_ipv6(const char *src, u_char *dst, size_t size);
+static int	inet_pton_ipv6_internal(const char *src, u_char *dst, size_t size,
+									bool allow_cidr);

 /*
@@ -74,9 +74,9 @@ pg_inet_net_pton(int af, const char *src, void *dst, size_t size)
 				inet_net_pton_ipv4(src, dst) :
 				inet_cidr_pton_ipv4(src, dst, size);
 		case PGSQL_AF_INET6:
-			return size == -1 ?
-				inet_net_pton_ipv6(src, dst) :
-				inet_cidr_pton_ipv6(src, dst, size);
+			return inet_pton_ipv6_internal(src, dst,
+										   (size == -1) ? 16 : size,
+										   true /* allow CIDR */);
 		default:
 			errno = EAFNOSUPPORT;
 			return -1;
@@ -352,13 +352,22 @@ emsgsize:
 }
 
 static int
-getbits(const char *src, int *bitsp)
+getbits(const char *src, int *bitsp, bool allow_cidr)
 {
 	static const char digits[] = "0123456789";
 	int			n;
 	int			val;
 	char		ch;
 
+	if (!allow_cidr)
+	{
+		/*
+		 * If we got here, the top-level call is to pg_inet_pton() and the
+		 * caller supplied a CIDR mask, which isn't allowed.
+		 */
+		return 0;
+	}
+
 	val = 0;
 	n = 0;
 	while ((ch = *src++) != '\0')
@@ -385,7 +394,7 @@ getbits(const char *src, int *bitsp)
 }
 
 static int
-getv4(const char *src, u_char *dst, int *bitsp)
+getv4(const char *src, u_char *dst, int *bitsp, bool allow_cidr)
 {
 	static const char digits[] = "0123456789";
 	u_char	   *odst = dst;
@@ -416,7 +425,7 @@ getv4(const char *src, u_char *dst, int *bitsp)
 				return 0;
 			*dst++ = val;
 			if (ch == '/')
-				return getbits(src, bitsp);
+				return getbits(src, bitsp, allow_cidr);
 			val = 0;
 			n = 0;
 			continue;
@@ -431,18 +440,12 @@ getv4(const char *src, u_char *dst, int *bitsp)
 	return 1;
 }
 
-static int
-inet_net_pton_ipv6(const char *src, u_char *dst)
-{
-	return inet_cidr_pton_ipv6(src, dst, 16);
-}
-
 #define NS_IN6ADDRSZ 16
 #define NS_INT16SZ 2
 #define NS_INADDRSZ 4
 
 static int
-inet_cidr_pton_ipv6(const char *src, u_char *dst, size_t size)
+inet_pton_ipv6_internal(const char *src, u_char *dst, size_t size, bool allow_cidr)
 {
 	static const char xdigits_l[] = "0123456789abcdef",
 				xdigits_u[] = "0123456789ABCDEF";
@@ -510,13 +513,13 @@ inet_cidr_pton_ipv6(const char *src, u_char *dst, size_t size)
 			continue;
 		}
 		if (ch == '.' && ((tp + NS_INADDRSZ) <= endp) &&
-			getv4(curtok, tp, &bits) > 0)
+			getv4(curtok, tp, &bits, allow_cidr) > 0)
 		{
 			tp += NS_INADDRSZ;
 			saw_xdigit = 0;
 			break;				/* '\0' was seen by inet_pton4(). */
 		}
-		if (ch == '/' && getbits(src, &bits) > 0)
+		if (ch == '/' && getbits(src, &bits, allow_cidr) > 0)
 			break;
 		goto enoent;
 	}
@@ -530,6 +533,9 @@ inet_cidr_pton_ipv6(const char *src, u_char *dst, size_t size)
 	if (bits == -1)
 		bits = 128;
 
+	/* Without a CIDR mask, the above should guarantee a complete address. */
+	Assert(allow_cidr || bits == 128);
+
 	endp = tmp + 16;
 
 	if (colonp != NULL)
@@ -568,3 +574,29 @@ emsgsize:
 	errno = EMSGSIZE;
 	return -1;
 }
+
+/*
+ * Converts a network address in presentation format to network format. The main
+ * difference between this and pg_inet_net_pton() above is that CIDR notation is
+ * not allowed.
+ *
+ * The memory pointed to by dst depends on the address family:
+ *
+ *     PGSQL_AF_INET:  not implemented yet (returns -1 with EAFNOSUPPORT)
+ *     PGSQL_AF_INET6: *dst must be a u_char[16]
+ *
+ * Over and above the standard inet_pton() functionality, we also set errno on
+ * parse failure, with the same meanings as with pg_inet_net_pton().
+ */
+int
+pg_inet_pton(int af, const char *src, void *dst)
+{
+	if (af != PGSQL_AF_INET6)
+	{
+		/* Currently only IPv6 is implemented. */
+		errno = EAFNOSUPPORT;
+		return -1;
+	}
+
+	return (inet_pton_ipv6_internal(src, dst, 16, false /* no CIDR */) == 128);
+}
-- 
2.25.1

---

From: Kyotaro Horiguchi <[email-masked]>
Date: Thu, 24 Mar 2022 17:10:54 +0900 (JST)

At Wed, 23 Mar 2022 23:52:06 +0000, Jacob Champion <[email-masked]> wrote in 
> and I've added you as a co-author to 0002.

I'm fine with it. Thanks. I marked it as Ready-for-Commiter.

Note for the patch set:

0001 is preliminary patch to move inet_pton out of src/backend tree. 

0002 is the main patch of this patchset

0003 is optional, which introduces pg_inet_pton() only works for IPv6
     addresses. 0002 gets the same effect by the following use of
     pg_inet_net_pton().

      > if (!strchr(host, '/')
      >     && pg_inet_net_pton(PGSQL_AF_INET6, host, addr, -1) == 128)

regards.

---

From: Jacob Champion <[email-masked]>
Date: Thu, 24 Mar 2022 15:36:51 +0000

> I'm fine with it. Thanks. I marked it as Ready-for-Commiter.

Thank you for the reviews and feedback!

--Jacob

---

From: Tom Lane <[email-masked]>
Date: Sun, 27 Mar 2022 17:19:27 -0400

Jacob Champion <[email-masked]> writes:
> [ v10-0001-Move-inet_net_pton-to-src-port.patch etc ]

There is something broken about the ssl tests as modified by
this patch.  The cfbot doesn't provide a lot of evidence about
why it's failing, but I applied the patchset locally and what
I see is

...
ok 47 - mismatch between host name and server certificate sslmode=verify-full: m
atches
Odd number of elements in hash assignment at /home/postgres/pgsql/src/test/ssl/t
/SSL/Server.pm line 288.
Use of uninitialized value in concatenation (.) or string at /home/postgres/pgsq
l/src/test/ssl/t/SSL/Backend/OpenSSL.pm line 178.
Use of uninitialized value in concatenation (.) or string at /home/postgres/pgsq
l/src/test/ssl/t/SSL/Backend/OpenSSL.pm line 178.
### Restarting node "primary"
# Running: pg_ctl -w -D /home/postgres/pgsql/src/test/ssl/tmp_check/t_001_ssltes
ts_primary_data/pgdata -l /home/postgres/pgsql/src/test/ssl/tmp_check/log/001_ss
ltests_primary.log restart
waiting for server to shut down.... done
server stopped
waiting for server to start.... stopped waiting
pg_ctl: could not start server

The tail end of the server log is

2022-03-27 17:13:11.482 EDT [551720] FATAL:  could not load server certificate file ".crt": No such file or directory

so it seems pretty clear that something's fouling up computation of
a certificate file name.  This may be caused by 9ca234bae or
4a7e964fc.

			regards, tom lane

---

From: Daniel Gustafsson <[email-masked]>
Date: Mon, 28 Mar 2022 00:44:07 +0200

> On 27 Mar 2022, at 23:19, Tom Lane <[email-masked]> wrote:

> This may be caused by 9ca234bae or 4a7e964fc.

I'd say 4a7e964fc is the culprit here.  From a quick skim the the
switch_server_cert() calls need to be changed along the lines of:

  from: switch_server_cert($node, 'server-ip-in-dnsname');
    to: switch_server_cert($node, certfile => 'server-ip-in-dnsname');

There migth be more changes required, that was the one that stood out.  Unless
someone beats me to it I'll take a look at fixing up the test in this patch
tomorrow.

--
Daniel Gustafsson		https://vmware.com/

---

From: Andres Freund <[email-masked]>
Date: Sun, 27 Mar 2022 17:54:51 -0700

Hi,

On 2022-03-27 17:19:27 -0400, Tom Lane wrote:
> The cfbot doesn't provide a lot of evidence about

FWIW - and I agree that's not nice user interface wise - just below the cpu /
memory graphs there's a "directory browser", allowing you to navigate to the
saved log files. Navigating to log/src/test/ssl/tmp_check/log allows you to
download
https://api.cirrus-ci.com/v1/artifact/task/5261015175659520/log/src/test/ssl/tmp_check/log/regress_log_001_ssltests
https://api.cirrus-ci.com/v1/artifact/task/5261015175659520/log/src/test/ssl/tmp_check/log/001_ssltests_primary.log

Greetings,

Andres Freund

---

From: Daniel Gustafsson <[email-masked]>
Date: Mon, 28 Mar 2022 11:17:25 +0200

> On 28 Mar 2022, at 00:44, Daniel Gustafsson <[email-masked]> wrote:

> I'll take a look at fixing up the test in this patch tomorrow.

Fixing up the switch_server_cert() calls and using default_ssl_connstr makes
the test pass for me.  The required fixes are in the supplied 0004 diff, I kept
them separate to allow the original author to incorporate them without having
to dig them out to see what changed (named to match the git format-patch output
since I think the CFBot just applies the patches in alphabetical order).

---

From: Greg Stark <[email-masked]>
Date: Mon, 28 Mar 2022 15:43:43 -0400

On Mon, 28 Mar 2022 at 05:17, Daniel Gustafsson <[email-masked]> wrote:
>
> named to match the git format-patch output

The first patch doesn't seem to actually apply though so it doesn't
get to the subsequent patches.

http://cfbot.cputube.org/patch_37_3458.log

=== Applying patches on top of PostgreSQL commit ID
e26114c817b610424010cfbe91a743f591246ff1 ===
=== applying patch ./v10-0001-Move-inet_net_pton-to-src-port.patch
patching file src/backend/utils/adt/Makefile
Hunk #1 FAILED at 44.
1 out of 1 hunk FAILED -- saving rejects to file
src/backend/utils/adt/Makefile.rej
patching file src/include/port.h
patching file src/include/utils/builtins.h
patching file src/port/Makefile
patching file src/port/inet_net_pton.c (renamed from
src/backend/utils/adt/inet_net_pton.c)
patching file src/tools/msvc/Mkvcbuild.pm

-- 
greg

---

From: Jacob Champion <[email-masked]>
Date: Mon, 28 Mar 2022 20:21:17 +0000

> since I think the CFBot just applies the patches in alphabetical order).

Thanks! Those changes look good to me; I've folded them into v11. This
is rebased on a newer HEAD so it should fix the apply failures that
Greg pointed out.

--Jacob
From 0025dcecab2d05c30e118105ea7d461ce98a8466 Mon Sep 17 00:00:00 2001
From: Jacob Champion <[email-masked]>
Date: Wed, 24 Nov 2021 14:46:11 -0800
Subject: [PATCH v11 1/3] Move inet_net_pton() to src/port

diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 7c722ea2ce..ccaa46babf 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -44,7 +44,6 @@ OBJS = \
 	geo_spgist.o \
 	hbafuncs.o \
 	inet_cidr_ntop.o \
-	inet_net_pton.o \
 	int.o \
 	int8.o \
 	json.o \
diff --git a/src/include/port.h b/src/include/port.h
index 3d103a2b31..2852e5b58b 100644
--- a/src/include/port.h
+++ b/src/include/port.h
@@ -515,6 +515,9 @@ extern int	pg_codepage_to_encoding(UINT cp);
 extern char *pg_inet_net_ntop(int af, const void *src, int bits,
 							  char *dst, size_t size);

-/* inet_net_pton.c */

+#ifndef FRONTEND
 #include "postgres.h"
+#else
 
 #include <sys/socket.h>
 #include <netinet/in.h>
 
+#ifndef FRONTEND

index de8676d339..8491e31adb 100644
 
 	our @pgportfiles = qw(

From 43a0ecebf3ccabcb25b735494eadefeb2a56ffd6 Mon Sep 17 00:00:00 2001
From: Jacob Champion <[email-masked]>
Date: Thu, 18 Nov 2021 15:36:18 -0800
Subject: [PATCH v11 2/3] libpq: allow IP address SANs in server certs

Per discussion on-list:

Co-authored-by: Daniel Gustafsson <[email-masked]>

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 70233aa872..cf69567c21 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -8344,16 +8344,31 @@ ldap://ldap.acme.com/cn=dbserver,cn=hosts?pgconnectinfo?base?(objectclass=*)
 
   <para>
 
+  <note>
+    <para>

 /*
 
-/*
 
 #include "postgres_fe.h"
 
+#include <arpa/inet.h>
+
 
 #include "libpq-int.h"
+#include "port.h"
 #include "pqexpbuffer.h"
 
 /*
 
+/*

 
+/*
 
 	/*

 
-				if (alt_name)
-				{
-					if (!*first_name)
 
 	/*

-#ifndef FRONTEND
-#include "utils/inet.h"
-#else
-/*

 #define NS_IN6ADDRSZ 16

index d8eeb085da..9cc6fed51f 100644
@@ -229,6 +229,30 @@ $node->connect_fails(
 
+# Test with an IP address in the Common Name. This is a strange corner case that
+# nevertheless is supported, as long as the address string matches exactly.
+switch_server_cert($node, certfile => 'server-ip-cn-only');
+
+$common_connstr =
+  "$default_ssl_connstr user=ssltestuser dbname=trustdb sslrootcert=ssl/root+server_ca.crt hostaddr=$SERVERHOSTADDR sslmode=verify-full";
+
+$node->connect_ok("$common_connstr host=192.0.2.1",
+	"IP address in the Common Name");
+
+$node->connect_fails(
+	"$common_connstr host=192.000.002.001",
+	"mismatch between host name and server certificate IP address",
+	expected_stderr =>
+	  qr/\Qserver certificate for "192.0.2.1" does not match host name "192.000.002.001"\E/
+);
+
+# Similarly, we'll also match an IP address in a dNSName SAN. (This is
+# long-standing behavior.)
+switch_server_cert($node, certfile => 'server-ip-in-dnsname');
+
+$node->connect_ok("$common_connstr host=192.0.2.1",
+	"IP address in a dNSName");
+
 # Test Subject Alternative Names.
 switch_server_cert($node, certfile => 'server-multiple-alt-names');
 
@@ -281,7 +305,54 @@ $node->connect_fails(
 	  qr/\Qserver certificate for "single.alt-name.pg-ssltest.test" does not match host name "deep.subdomain.wildcard.pg-ssltest.test"\E/
 );
 
+switch_server_cert($node, certfile => 'server-ip-alt-names');
 switch_server_cert($node, certfile => 'server-cn-and-alt-names');
 
@@ -299,6 +370,39 @@ $node->connect_fails(
 	  qr/\Qserver certificate for "dns1.alt-name.pg-ssltest.test" (and 1 other name) does not match host name "common-name.pg-ssltest.test"\E/
 );
 
+switch_server_cert($node, certfile => 'server-cn-and-ip-alt-names');
+switch_server_cert($node, certfile => 'server-ip-cn-and-alt-names');
+switch_server_cert($node, certfile => 'server-ip-cn-and-dns-alt-names');
 switch_server_cert($node, certfile => 'server-no-names');
-- 
2.25.1

From 44734feef1404d56849cb87e0bbbaef6d1291031 Mon Sep 17 00:00:00 2001
From: Jacob Champion <[email-masked]>
Date: Wed, 5 Jan 2022 15:47:03 -0800
Subject: [PATCH v11 3/3] squash! libpq: allow IP address SANs in server certs

 

 		family = PGSQL_AF_INET6;
 
-		/*

 /*

 /*
 
 static int
 
+	if (!allow_cidr)
+	{
+		/*
 
 static int
 
-static int
 
 static int

 	if (colonp != NULL)

---

From: Peter Eisentraut <[email-masked]>
Date: Wed, 30 Mar 2022 13:37:31 +0200

> is rebased on a newer HEAD so it should fix the apply failures that

I'm not happy about how inet_net_pton.o is repurposed here.  That code 
is clearly meant to support backend data types with specifically.  Code like

+ /*

indicates that we are fighting against the API.  Also, if someone ever 
wants to change how those backend data types work, we then have to check 
a bunch of other code as well.

I think we should be using inet_ntop()/inet_pton() directly here.  We 
can throw substitute implementations into libpgport if necessary, that's 
not so difficult.

---

From: Jacob Champion <[email-masked]>
Date: Wed, 30 Mar 2022 16:17:18 +0000

On Wed, 2022-03-30 at 13:37 +0200, Peter Eisentraut wrote:
> indicates that we are fighting against the API.

Horiguchi-san had the same concern upthread, I think. I replaced that
code in the next patch, but it was enough net-new stuff that I kept the
patches separate instead of merging them. I can change that if it's not
helpful for review.

> Also, if someone ever

Is this request satisfied by the implementation of pg_inet_pton() in
patch 0003? If not, what needs to change?

Thanks,
--Jacob

---

From: Peter Eisentraut <[email-masked]>
Date: Thu, 31 Mar 2022 16:32:27 +0200

> patch 0003? If not, what needs to change?

Why add a (failry complicated) pg_inet_pton() when a perfectly 
reasonable inet_pton() exists?

I would get rid of all that refactoring and just have your code call 
inet_pton()/inet_ntop() directly.

If you're worried about portability, and you don't want to go through 
the effort of proving libpgport substitutes, just have your code raise 
an error in the "#else" code paths.  We can fill that in later if there 
is demand.

---

From: Jacob Champion <[email-masked]>
Date: Thu, 31 Mar 2022 18:15:25 +0000

> reasonable inet_pton() exists?

I think it was mostly just that inet_aton() and pg_inet_net_ntop() both
had ports, and I figured I might as well port the other one since we
already had the implementation. (I don't have a good intuition yet for
the community's preference for port vs dependency.)

> an error in the "#else" code paths.  We can fill that in later if there

Switched to inet_pton() in v12, with no #if/else for now. I think this
should work with Winsock as-is; let's see if the bot agrees...

Thanks,
--Jacob
From a973663f1b4b98a427692bb6291ddc2c3ce9bb4f Mon Sep 17 00:00:00 2001
From: Jacob Champion <[email-masked]>
Date: Thu, 18 Nov 2021 15:36:18 -0800
Subject: [PATCH v12] libpq: allow IP address SANs in server certs

Per discussion on-list:

 src/backend/utils/adt/inet_net_pton.c         |   2 +-
 src/interfaces/libpq/fe-secure-common.c       |  99 +++++++++++++
 src/test/ssl/t/001_ssltests.pl                | 110 +++++++++++++-
 28 files changed, 791 insertions(+), 41 deletions(-)

diff --git a/doc/src/sgml/libpq.sgml b/doc/src/sgml/libpq.sgml
index 0b2a8720f0..b785008045 100644
--- a/doc/src/sgml/libpq.sgml
+++ b/doc/src/sgml/libpq.sgml
@@ -8356,16 +8356,31 @@ ldap://ldap.acme.com/cn=dbserver,cn=hosts?pgconnectinfo?base?(objectclass=*)
 
   <para>
 
+  <note>
+    <para>
+      For backward compatibility with earlier versions of PostgreSQL, the host
+      IP address is verified in a manner different
+      from <ulink url="https://tools.ietf.org/html/rfc6125">RFC 6125</ulink>.
+      The host IP address is always matched against <literal>dNSName</literal>
+      SANs as well as <literal>iPAddress</literal> SANs, and can be matched
+      against the Common Name attribute if no relevant SANs exist.
+   </para>
+  </note>
+
   <para>
    To allow server certificate verification, one or more root certificates
    must be placed in the file <filename>~/.postgresql/root.crt</filename>
diff --git a/src/backend/utils/adt/inet_net_pton.c b/src/backend/utils/adt/inet_net_pton.c
index d3221a1313..7752012ad9 100644
--- a/src/backend/utils/adt/inet_net_pton.c
+++ b/src/backend/utils/adt/inet_net_pton.c
@@ -29,9 +29,9 @@ static const char rcsid[] = "Id: inet_net_pton.c,v 1.4.2.3 2004/03/17 00:40:11 m
 #include <assert.h>
 #include <ctype.h>

 
 
 /*
 
-/*
index bd46f08fae..6628eb8318 100644
 
 #include "postgres_fe.h"
 
+#include <arpa/inet.h>
+
 
 #include "libpq-int.h"
+#include "port.h"
 #include "pqexpbuffer.h"
 
 /*
@@ -144,6 +148,101 @@ pq_verify_peer_name_matches_certificate_name(PGconn *conn,
 	return result;
 }
 
+/*
+		struct in6_addr addr;
+		if (inet_pton(AF_INET6, host, &addr) == 1)
+			if (memcmp(ipdata, &addr.s6_addr, iplen) == 0)
 
index 0cba5c5cf2..1828a3cb8c 100644

+/*
+	struct in6_addr	dummy6;
+		|| (inet_pton(AF_INET6, host, &dummy6) == 1);
 
 	/*

 
-				if (alt_name)
-				{
-					if (!*first_name)
 
 	/*

-#ifndef FRONTEND
-#include "utils/inet.h"
-#else
-/*

 #define NS_IN6ADDRSZ 16
index d8eeb085da..051de4efb0 100644

@@ -281,7 +305,58 @@ $node->connect_fails(
 	  qr/\Qserver certificate for "single.alt-name.pg-ssltest.test" does not match host name "deep.subdomain.wildcard.pg-ssltest.test"\E/
 );
 
+	"$common_connstr host=2001:db8::0.0.0.1",
+	"host matching an IPv6 address in mixed form (Subject Alternative Name 2)");
 
@@ -299,6 +374,39 @@ $node->connect_fails(
 	  qr/\Qserver certificate for "dns1.alt-name.pg-ssltest.test" (and 1 other name) does not match host name "common-name.pg-ssltest.test"\E/
 );