On Mon, 2022-02-07 at 17:29 +0900, Kyotaro Horiguchi wrote:
> At Fri, 4 Feb 2022 17:06:53 +0000, Jacob Champion <pchamp...@vmware.com> 
> wrote in 
> > That works a lot better than what I had in my head. Done that way in
> > v4. Thanks!
> 
> Thanks!
> 
> 0002:
> 
> +#define PGSQL_AF_INET  (AF_INET + 0)
> +#define PGSQL_AF_INET6 (AF_INET + 1)
> ..
> -#define PGSQL_AF_INET  (AF_INET + 0)
> -#define PGSQL_AF_INET6 (AF_INET + 1)
> 
> I feel this should be a part of 0001.  (But the patches will be
> finally merged so maybe no need to bother moving it).

Okay. I can move it easily if you feel like it would help review, but
for now I've kept it in 0002.

> > * The use of inet_aton() instead of inet_pton() is deliberate; the
> > * latter cannot handle alternate IPv4 notations ("numbers-and-dots").
> 
> I think we should be consistent in handling IP addresses.  We have
> both inet_pton and inet_aton to parse IPv4 addresses.
> 
> We use inet_pton in the inet type (network_in).
> We use inet_aton in server addresses.
> 
> # Hmm. I'm surprised to see listen_addresses accepts "0x7f.1".
> # I think we should accept the same by network_in but it is another
> # issue.

Yeah, that's an interesting inconsistency.

> So, inet_aton there seems to be the right choice but the comment
> doesn't describe the reason for that behavior. I think we should add
> an explanation about the reason for the behavior, maybe something like
> this:
> 
> > We accept alternative IPv4 address notations that are accepted by
> > inet_aton but not by inet_pton as server address.

I've pulled this wording into the comment in v5, attached.

> +        * GEN_IPADD is an OCTET STRING containing an IP address in network 
> byte
> +        * order.
> 
> +       /* OK to cast from unsigned to plain char, since it's all ASCII. */
> +       return pq_verify_peer_name_matches_certificate_ip(conn, (const char 
> *) addrdata, len, store_name);
> 
> Aren't the two comments contradicting each other? The retruned general
> name looks like an octet array, which is not a subset of ASCII
> string. So pq_verify_peer_name_matches_certificate_ip should receive
> addrdata as "const unsigned char *", without casting.

Bad copy-paste on my part; thanks for the catch. Fixed.

> +                       if (name->type == host_type)
> +                               check_cn = false;
> 
> Don't we want a concise coment for this?

Added one; see what you think.

> -       if (*names_examined == 0)
> +       if ((rc == 0) && check_cn)
> 
> To me, it seems a bit hard to understand.  We can set false to
> check_cn in the rc != 0 path in the loop on i, like this:
> 
> >                       if (rc != 0)
> > +                     {
> > +                             /*
> > +                              * don't fall back to CN when we have a match 
> > or have an error
> > +                              */
> > +                             check_cn = false;
> >                               break;
> > +                     }
> ...
> > -     if ((rc == 0) && check_cn)
> > +     if (check_cn)

If I understand right, that's not quite equivalent (and the new tests
fail if I implement it that way). We have to disable fallback if the
SAN exists, whether it matches or not.

> The following existing code (CN fallback)
> 
> >       rc = openssl_verify_peer_name_matches_certificate_name(conn,
> >       X509_NAME_ENTRY_get_data(X509_NAME_get_entry(subject_name, cn_index)),
> >       first_name);
> 
> is expecting that first_name has not been set when it is visited.
> However, with this patch, first_name can be set when the cert has any
> SAN of unmatching type (DNS/IPADD) and the already-set name leaks.  We
> need to avoid that memory leak since the path can be visited multiple
> times from the user-program of libpq. I came up with two directions.
> 
> 1. Completely ignore type-unmatching entries. first_name is not set by
>  such entries.  Such unmatching entreis doesn't increase
>  *names_examined.
> 
> 2. Avoid overwriting first_name there.
> 
> I like 1, but since we don't make distinction between DNS and IPADDR
> in the error message emited by the caller, we would take 2?

Great catch, thanks! I implemented option 2 to start. Option 1 might
make things difficult to debug if you're connecting to a server by IP
address but its certificate only has DNS names.

Thanks!
--Jacob

commit 8c427e3289a28cb683eff5d05b2e8770c3c07662
Author: Jacob Champion <pchamp...@vmware.com>
Date:   Tue Feb 8 16:26:27 2022 -0800

    squash! libpq: allow IP address SANs in server certs

diff --git a/src/interfaces/libpq/fe-secure-common.c 
b/src/interfaces/libpq/fe-secure-common.c
index cfdde58e67..4d78715756 100644
--- a/src/interfaces/libpq/fe-secure-common.c
+++ b/src/interfaces/libpq/fe-secure-common.c
@@ -160,7 +160,8 @@ pq_verify_peer_name_matches_certificate_name(PGconn *conn,
  */
 int
 pq_verify_peer_name_matches_certificate_ip(PGconn *conn,
-                                                                               
   const char *ipdata, size_t iplen,
+                                                                               
   const unsigned char *ipdata,
+                                                                               
   size_t iplen,
                                                                                
   char **store_name)
 {
        char       *addrstr;
@@ -193,8 +194,9 @@ pq_verify_peer_name_matches_certificate_ip(PGconn *conn,
                family = PGSQL_AF_INET;
 
                /*
-                * The use of inet_aton() instead of inet_pton() is deliberate; 
the
-                * latter cannot handle alternate IPv4 notations 
("numbers-and-dots").
+                * 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))
                {
diff --git a/src/interfaces/libpq/fe-secure-common.h 
b/src/interfaces/libpq/fe-secure-common.h
index 4316c3d1b7..20ff9ba5db 100644
--- a/src/interfaces/libpq/fe-secure-common.h
+++ b/src/interfaces/libpq/fe-secure-common.h
@@ -23,7 +23,8 @@ 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 char *addrdata, size_t addrlen,
+                                                                               
                           const unsigned char *addrdata,
+                                                                               
                           size_t addrlen,
                                                                                
                           char **store_name);
 extern bool pq_verify_peer_name_matches_certificate(PGconn *conn);
 
diff --git a/src/interfaces/libpq/fe-secure-openssl.c 
b/src/interfaces/libpq/fe-secure-openssl.c
index 7173ea0d73..00e297db17 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -544,8 +544,7 @@ openssl_verify_peer_name_matches_certificate_ip(PGconn 
*conn,
 #endif
        len = ASN1_STRING_length(addr_entry);
 
-       /* OK to cast from unsigned to plain char, since it's all ASCII. */
-       return pq_verify_peer_name_matches_certificate_ip(conn, (const char *) 
addrdata, len, store_name);
+       return pq_verify_peer_name_matches_certificate_ip(conn, addrdata, len, 
store_name);
 }
 
 static bool
@@ -619,7 +618,13 @@ pgtls_verify_peer_name_matches_certificate_guts(PGconn 
*conn,
                        char       *alt_name = NULL;
 
                        if (name->type == host_type)
+                       {
+                               /*
+                                * 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)
                        {
@@ -671,10 +676,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);
+                               }
                        }
                }
        }
From 476017589454fac0bef2eaf5f9098b2e363429f7 Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchamp...@vmware.com>
Date: Wed, 24 Nov 2021 14:46:11 -0800
Subject: [PATCH v5 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 d8f05a7df3..22ebbfda17 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 a310bcb28c..b566a4aed7 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 66f543e350df166cc8acced1567e805842572f8e Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchamp...@vmware.com>
Date: Thu, 18 Nov 2021 15:36:18 -0800
Subject: [PATCH v5 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.

- 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.
---
 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       |   5 +
 src/interfaces/libpq/fe-secure-openssl.c      | 131 ++++++++++++++++--
 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 ++
 .../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/sslfiles.mk                      |   5 +
 src/test/ssl/t/001_ssltests.pl                | 101 +++++++++++++-
 24 files changed, 697 insertions(+), 47 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/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

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..2b8f2d6b24 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 <netinet/in.h>
+#include <arpa/inet.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..20ff9ba5db 100644
--- a/src/interfaces/libpq/fe-secure-common.h
+++ b/src/interfaces/libpq/fe-secure-common.h
@@ -16,11 +16,16 @@
 #ifndef FE_SECURE_COMMON_H
 #define FE_SECURE_COMMON_H
 
+#include "common/inet-common.h"
 #include "libpq-fe.h"
 
 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 9f735ba437..55bd55ae6e 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -72,6 +72,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 +512,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 +570,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,24 +615,40 @@ 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)
 				break;
 		}
@@ -562,13 +656,14 @@ pgtls_verify_peer_name_matches_certificate_guts(PGconn *conn,
 	}
 
 	/*
-	 * 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 ((rc == 0) && check_cn)
 	{
 		X509_NAME  *subject_name;
 
@@ -581,10 +676,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/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-----
+MIIDLzCCAhegAwIBAgIIICERKRE1UQAwDQYJKoZIhvcNAQELBQAwQjFAMD4GA1UE
+Aww3VGVzdCBDQSBmb3IgUG9zdGdyZVNRTCBTU0wgcmVncmVzc2lvbiB0ZXN0IHNl
+cnZlciBjZXJ0czAeFw0yMTExMjkxOTM1NTFaFw00OTA0MTYxOTM1NTFaMEYxHjAc
+BgNVBAsMFVBvc3RncmVTUUwgdGVzdCBzdWl0ZTEkMCIGA1UEAwwbY29tbW9uLW5h
+bWUucGctc3NsdGVzdC50ZXN0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC
+AQEA6+8IYKAFnZ7V+fDo1cyMpbGBLzCfJOQ/1o2jOGP4+GjpsZgv6S6UT2MheC8M
+iiEFrYwdsSIZyYc3jEZrluy/UuR0bCGtqU92BCqa0iBLhvHOgjR588u253eLxQtQ
+8iJn11QPrKMk35nMkmY8GfHt4sGFbvBL6+GpipHq7a6cde3Z+v4kCB5dKMYDUDtm
+3mJmviuGNAu5wOqItk2Yi5dwJs1054007KNH0Il43urxiOfnkLS0cG5kehboPf86
+vxBt3iHByrU/9/DY5IvQCfSXVNa6rb5w5/pGja9aCei6Mv1jQY/V8SMQTga+MOsA
+0WB9akxMi2NxwS2+BQ4k/McPlwIDAQABoyUwIzAhBgNVHREEGjAYhwTAAAIBhxAg
+AQ24AAAAAAAAAAAAAAABMA0GCSqGSIb3DQEBCwUAA4IBAQAQLo2RzC07dG9p+J3A
+W6C0p3Y+Os/YE2D9wfp4TIDTZxcRUQZ0S6ahF1N6sp8l9KHBJHPU1cUpRAU1oD+Y
+SqmnP/VJRRDTTj9Ytdc/Vuo2jeLpSYhVKrCqtjqIrCwYJFoYRmMoxTtJGlwA0hSd
+kwo3XYrALPUQWUErTYPvNfDNIuUwqUXNfS0CXuIOVN3LJ+shegg6Pwbh9B5T9NHx
+kH+HswajhdpdnZIgh0FYTlTCPILDrB49aOWwqLa54AUA6WXa35hPsP8SoqL9Eucq
+ifPhBYyadsjOb+70N8GbbAsDPN1jCX9L8RuNcEkxSCKCYx91cWXh7K5KMPuGlzB7
+j8xB
+-----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-----
+MIIEpAIBAAKCAQEA6+8IYKAFnZ7V+fDo1cyMpbGBLzCfJOQ/1o2jOGP4+GjpsZgv
+6S6UT2MheC8MiiEFrYwdsSIZyYc3jEZrluy/UuR0bCGtqU92BCqa0iBLhvHOgjR5
+88u253eLxQtQ8iJn11QPrKMk35nMkmY8GfHt4sGFbvBL6+GpipHq7a6cde3Z+v4k
+CB5dKMYDUDtm3mJmviuGNAu5wOqItk2Yi5dwJs1054007KNH0Il43urxiOfnkLS0
+cG5kehboPf86vxBt3iHByrU/9/DY5IvQCfSXVNa6rb5w5/pGja9aCei6Mv1jQY/V
+8SMQTga+MOsA0WB9akxMi2NxwS2+BQ4k/McPlwIDAQABAoIBAQCuNFKVNdKvrUYF
+RLJGmsAG3+eo9lern7TbML2ht39vu9dBwEMwA6qSa3mdCfBSVUuh9uE9lxY/TU3g
+j2aFi81A4VptNPjLGNblAKhMGnhp7UUzspeRQYuNoSFcnpxoDKtrvK/OIq/pQeBh
+AIfECHRDh+yEG32Tb44FuPQkB1eTYl8xbMEImrhNUaSjJk7tTsmydHy0DjmqHVKX
+HUj0TREfDBDOBiHtY0XV6Pu3bnqDH/TKLTfUf3UdfTuay3Yai9aEcRPWp9GrMO7G
+axsKCifTz6177gyr6Fv8HLeMZMh9rMZRn3e0zfaF6vrH1QnZZOts5jpUa0KugSCd
+//uC0iNxAoGBAPXVc3b+o3hY5gcwwpaW6JtsarDrmNRxrizqIDG7NgpqwdFXgTi6
+6q0t2pjv81ATqij69IcPkNSissyR4OEKnu/OFJWzreg8yLi75WHKi0E/6msHpwRk
+d1yP0Zgd05ots/yOjDSp593RagaPVvHBxMECZ/Tm3B+Tq55Azudd/zvLAoGBAPWw
+xf0oUEJl6NdUZD6K7eFc6jf8yrpD85dldeko6LeN8x0XlKKWvUDJ2+3oizXoQvCm
+8by6KOYEIo4MrtXuy9MmtPWfNvRBr+hsUHchIj7IgFa9bKXyK2FnJqu/8CbEymli
+eZu7hoOhelurhnFy1zSqwNO4GC+kw60Y/BO3Z1nlAoGAVOyYJtNwxXJwhKtjjYI0
+ePzLHrNE6J8c/Ick+AkkchTPP/JqwZ5Q0+KzUYITG+avMdkAAGhwMATEn8cFWLjC
+jzUyB0U7Hq9g5/CBHXdLBA+Ae9j46ZuLYH6OeW5UWz7OnsDfzpGjeA2QAxQhhQLb
+ZZHfN8tI39+zucfJskPWmGECgYEAg9guF1Fn6InJrqwR82IYj6SN6CeXHufSM392
+C/4xDDd3rDf4QlwECV2J0RzGf9I5Ae2EshNwWScE6Be0RweTh6cw2tJq6h7J6D8f
+2x4Dw49TF7klMdRIJUf2f5pLpHJccLswqTqzz7V69PCSABVxmUi8m6EiEYconp5W
+v7nfE2UCgYALrEqzncuSIX3q6TVAjnzT7gO4h8h2TUekIWdHQFldFx8R7Kncggnd
+48gQqhewchNR83UCcd7pPsCcTqu6UR1QRdq/DV5P6J3xdZ2iS/2gCM6hvWIvKZEv
+/ClnkyFCOW7zX6RKIXtRYZTV1kz3TajApi34RTIeIMTieaCarnBJbA==
+-----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-----
+MIIDCTCCAfGgAwIBAgIIICERKREEUAAwDQYJKoZIhvcNAQELBQAwQjFAMD4GA1UE
+Aww3VGVzdCBDQSBmb3IgUG9zdGdyZVNRTCBTU0wgcmVncmVzc2lvbiB0ZXN0IHNl
+cnZlciBjZXJ0czAeFw0yMTExMjkxOTA0NTBaFw00OTA0MTYxOTA0NTBaMCAxHjAc
+BgNVBAsMFVBvc3RncmVTUUwgdGVzdCBzdWl0ZTCCASIwDQYJKoZIhvcNAQEBBQAD
+ggEPADCCAQoCggEBAOM8yB6aVWb17ujr3ayU62mxHQoqn4CvG9yXlJvGOGv/ursW
+Vs0UYJdc96LsNZN1szdm9ayNzCIw3eja+ULsjxCi6+3LM4pO76IORL/XFamlTPYb
+BZ4pHdZVB0nnZAAnWCZPyXdnjOKQ5+8unVXkfibkjj8UELBJ2snehsOa+CTkOBez
+zxYMqxAgbywLIYsW448brun7UXpWmqbGK+SsdGaIZ5Sb7Zezc5lt6CrLemTZTHHK
+7l4WZFCCEi4t3sgO8o1vDELD/IE5G8lyXvIdgJg6t8ssper7iCw6S8x+okhjiSjT
+vDLU2g4AanqZRZB49aPwTo0QUcJA2BCJxL9xLy8CAwEAAaMlMCMwIQYDVR0RBBow
+GIcEwAACAYcQIAENuAAAAAAAAAAAAAAAATANBgkqhkiG9w0BAQsFAAOCAQEAwZJ+
+8KpABTlMEgKnHIYb35ItGhtFiTLQta9RkXx7vaeDwpOdPP/IvuvpjpQZkobRgBsk
+bNM0KuJpd2mSTphQAt6eKQIdcPrkzvc/Yh9OK3YNLUAbu/ZhBUnBvFnUL4wn2f1U
+mfO+m8P/LxybwqKx7r1mbaB+tP3RTxxLcIMvm9ECPQEoBntfEL325Wdoj+WuQH5Y
+IvcM6FaCTkQsNIPbaBD5l5MhMLHRULZujbDjXqGSvRMQfns6np/biMjNdQA8NZ5z
+STeUFvkQbCxoA0YYLgoSHL5KhZjXrg2g+T+2TUyCTR/91xf9OoOjBZdixR0S0DzJ
+B1+5vnUjZaCfnSEA7A==
+-----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-----
+MIIEpQIBAAKCAQEA4zzIHppVZvXu6OvdrJTrabEdCiqfgK8b3JeUm8Y4a/+6uxZW
+zRRgl1z3ouw1k3WzN2b1rI3MIjDd6Nr5QuyPEKLr7cszik7vog5Ev9cVqaVM9hsF
+nikd1lUHSedkACdYJk/Jd2eM4pDn7y6dVeR+JuSOPxQQsEnayd6Gw5r4JOQ4F7PP
+FgyrECBvLAshixbjjxuu6ftRelaapsYr5Kx0ZohnlJvtl7NzmW3oKst6ZNlMccru
+XhZkUIISLi3eyA7yjW8MQsP8gTkbyXJe8h2AmDq3yyyl6vuILDpLzH6iSGOJKNO8
+MtTaDgBqeplFkHj1o/BOjRBRwkDYEInEv3EvLwIDAQABAoIBACp3uY6+mSdc3wF4
+0zzlt/lQuHSl8plCIJrhWUyjhvfoGyXLzv0Uydh/72frbTfZz1yTSWauOXBKYa6a
+/eqb+0DIsf8G8uLuTaqjsAWKVOoXkoKMGkistn7P9UTCkdXVhIvkbWp7V8EgA7iX
+pZ/fzBPIsyzmuxe3NcR0ags0cxuxkNuu+YXDv1oTedmT2wS3CZq1d/T1Y/EOVIf8
+Iznd2aOverlsnt6iiQ3ZWdG/W5F8FhnrR/rrBdYsdCv6TH/KUYexnDOUYpayjDbu
+oAKnifPp6UqiOM4SuBL83OAz19jptp5vpF370BEVRs3eK0q+zo/mETjv9HsXdolZ
+lfoXA0ECgYEA/7nb2azbq/2muvXCh1ZxCEbn3mt8KXoJP/xkx/v9eEc/cc5Q9e0V
+2oGfjC2hSE+bjOWMwiUMD6uU+iRjhz5A3IvUxnoSdoL7H9p0hTqLMyP7dTDkoVF5
+aEuLMaiI5YEnfAFu9L5h8ZKieoQTBoscT06wnGjh9pBV9bthfTKA7ksCgYEA43sb
+55m9WL4kWCPwOAp3vdEAFyxzmZHlO26sEQOU/m5aN01pumYybBruziEXMI96yfTj
+VmXKReeYb6XUiCcs3fLSipD/+8/8CsjO4uMORtxWumXe8AbKZfysGFzL7wJlByGT
+38AGQwIG/XD8cKnaiEMX4E/3Owbcoxwixo3WZC0CgYEAovaqJ9mEU+Jc8h/TS7PG
+bGPjN1Z/1V6zrlcFUnw/Vvrwb3HvHglsN8cLCaW6df5lPjC6tq4tNX8+fPnbg0Ak
+zWc+vQzl3ygxKGdqgcyBEKIJiPETgcoN+GzL02V3d+oKY3f2YXlBqVSsvi6UgUL9
+U3zuB36/IQVyAhrbUZFxoGkCgYEAnaFAO+Nvrp/LhXwZyGuQf+rkmipGTIMpil5t
+QzjtNMV5JFszSWPpyrl7A0Ew1YiG+I0GP2c3m+sY2TzbIiGrWH0b4cMKbw63Qy3V
+FqlpyjaCrpVKv56k/7jv883RzuQk56Uf1+szK5mrCFITy2oXsVZ0pA4lbjSaDTjA
+7D968V0CgYEA+qKqXKL98+c5CMPnpf+0B1x2zgyUym1ouPfon2x5fhK84T53zDMA
+zfdUJ/SOZw6/c9vRF7RL8h+ZfFdIyoAXv4Tt6mIiZe7P+AUVg6XgJ0ce2MUSeWjI
+W8D4WdSi0jyqr99TuVBWhbTZJviMB3pHqKaHQ07hnd/lPtvzsiH12qk=
+-----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-----
+MIIDHTCCAgWgAwIBAgIIICIBBBQ2MQAwDQYJKoZIhvcNAQELBQAwQjFAMD4GA1UE
+Aww3VGVzdCBDQSBmb3IgUG9zdGdyZVNRTCBTU0wgcmVncmVzc2lvbiB0ZXN0IHNl
+cnZlciBjZXJ0czAeFw0yMjAxMDQyMjM2MzFaFw00OTA1MjIyMjM2MzFaMDQxHjAc
+BgNVBAsMFVBvc3RncmVTUUwgdGVzdCBzdWl0ZTESMBAGA1UEAwwJMTkyLjAuMi4x
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwmqTdQJfs2Ti9tPitYp2
+27I0HvL/kNSgA6egFr0foRo0BorwJNIzdbV0+EnsfiBNTWL5It26gqO7UP3ms8t2
+vHD5gkXfT+f6ts0lVJEcIOkUD/8ws4Ic9Y4uPqb4gN+pUKqcxtmLW1TYk84MBK59
+Xz4yPPS6N+G/DMMeFHTNkM9EQwn/+DC3fDsWdGYM2GRWDTJGg1A5tSUcF+seu7i1
+Vg7XajBfsvgAUAsrAxV+X/sLZh94HY+paD6wfaI99mY2OXVc/XW/z1r9WQznor65
+ZkonNCaPfavqPG5vqnab9AyQcqPqmX8hf/xrniASBAkqNCctbASrFCIYvCJfGfmX
+EQIDAQABoyUwIzAhBgNVHREEGjAYhwTAAAIChxAgAQ24AAAAAAAAAAAAAAABMA0G
+CSqGSIb3DQEBCwUAA4IBAQBf7kmYfRYfnWk1OUfY3N1kaNg9piBBlFr9g+OQn9KU
+zirkN7s0ZQbCGxV1uJQBKS58NyE414Vorau77379emgYDcCBpDIYpkLiNujVrIOr
+ggRFKsFRgxu4/mw0BSgCcV8RPe9SWHZ90Mos7TMCnW/PdxOCD1wD0YMkcs0rwB3l
+0Kzc7jDnfOEvmgw/Ysm7v67ps+05Uq5VskQ6WrpSAw6kPD/QMuuBAX8ATPczIaox
+zAMyncq1IiSIwG93f3EoQQThdQ70C6G9vLcu9TtL6JAsEMFEzR99gt1Wsqvmgl9W
+kStzj1yjIWeo5gIsa4Jgcke1lZviWyrTxHDfyunYE5i5
+-----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-----
+MIIEpAIBAAKCAQEAwmqTdQJfs2Ti9tPitYp227I0HvL/kNSgA6egFr0foRo0Borw
+JNIzdbV0+EnsfiBNTWL5It26gqO7UP3ms8t2vHD5gkXfT+f6ts0lVJEcIOkUD/8w
+s4Ic9Y4uPqb4gN+pUKqcxtmLW1TYk84MBK59Xz4yPPS6N+G/DMMeFHTNkM9EQwn/
++DC3fDsWdGYM2GRWDTJGg1A5tSUcF+seu7i1Vg7XajBfsvgAUAsrAxV+X/sLZh94
+HY+paD6wfaI99mY2OXVc/XW/z1r9WQznor65ZkonNCaPfavqPG5vqnab9AyQcqPq
+mX8hf/xrniASBAkqNCctbASrFCIYvCJfGfmXEQIDAQABAoIBAB6GgVST5NbT9lbu
++d+rN/JSzqA1Yy8oU19/iEFJvJec96I3WnFNl8rZjN4XLUy4YarO6XMyAUDV2Gll
+FD4Sqjf4PRTZR7DSKaleGIhoqFP6hK3mUY091rIves9XhBkoBPunbipCqgDTF5ZN
+edGaXBECQP0VJ8/yX/7u++AWXthnjDis9X0taZfFg/PYbV7SCJ1Hg1O/wEsgXlnC
+7mbL6wkCW0f6700B0x1kKbZqJY95xRqp6Ipq2lIQbJDdGywoj0WzKqNltf9cer+r
+cXl8WjeiMvvvpl4uGhckAbzUifUzxN6A3f1fu/XKtOmabMi9t7J4MRfgOgedgtQB
+0jaZGSkCgYEA+lBLnNY6M48HX2mdtr86+n41gh69v8Z7oNikJFDZkodrvI8uqE0i
+0XwnYPFddt8NbmuUhhuzI2M8RKhGLgdlbKpkSSVafnMfcxRmX2EAtWQgdvX1Iult
+752LWdBgSuw2vlzvy3T/GYnjMrXSCGput4amqojMEbvUGvIdSUMdHGMCgYEAxtU1
+WixKPL6aEnYy1f4bybzcNgGtl8PBRz9xw+P46g+ijOPoaG9O73Tr7An11AO003Ot
+DHhMW+b8yHLyxoKwS2sU2cN/lKB8xNQYZc1D61RNJlzgnHMXnA0lcH0I3M35fqKr
+/71pD1ZP40SSJS+od/KEjW80XzuOdyiXg8q81vsCgYEAnUPLbbsuj+whzrFVlFZr
+IKwgxCK6Rn3WeIUEA4kEWUpZxvsSbk0gPgtJ1l9uwFt9Xc2bX/KRRv93Aw/SH+Mn
+tvEK1uXwCBgePzgm5W/VeSFyQCthm1CbcHtD7Oa9SPVFo65SPjrAd3QpWVfgoMb1
+zrp7hhMyW0XuCgvpmHjhFk8CgYEAxq/thXM2p+bLLWGhwQcRG5G299zLbBl4PUsf
+0uEvLi17gJCKADoiRdSvoAn/9eHSQ26XYRuhKkDzHxcGlOmpY2PYzRa3mXyZ0VIk
+Iy5wDWwLQCeVZ6D22cClRfgb8BF/nFTPzVmn72SPpgoyhChQj7PvUynpyrRH07jj
+VxYziBsCgYAFr37Xbl0VnXVK+XU+vMwUZjcF4jpoCr7SFZqgRbW2GbYSUoMuPXns
+RnJh+Fvi1NUei+E5s1H4P1pVq4p0jFxP4GvH/qvNjnIn/Er3bbqvpox6dWUJXprq
+qTQSDIeoDC/V8cyRoIfqPvTVqY8Rgew6GEkv0bAImdxhoSng7vIseg==
+-----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-----
+MIIDQzCCAiugAwIBAgIIICIBBBQ2MQEwDQYJKoZIhvcNAQELBQAwQjFAMD4GA1UE
+Aww3VGVzdCBDQSBmb3IgUG9zdGdyZVNRTCBTU0wgcmVncmVzc2lvbiB0ZXN0IHNl
+cnZlciBjZXJ0czAeFw0yMjAxMDQyMjM2MzFaFw00OTA1MjIyMjM2MzFaMDQxHjAc
+BgNVBAsMFVBvc3RncmVTUUwgdGVzdCBzdWl0ZTESMBAGA1UEAwwJMTkyLjAuMi4x
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8xddbo/x2TOSIa/br8BN
+o/URdTr9+l2R5YojiZKDuLxiQVkgC30PJ2/CNFKIh2nHhRrzknI6sETVtrxZ+9V2
+qRc1yShVu462u0DHPRMIZnZIOZg3hlNB0cRWbOglUKlttIARNEQUcTUyPOtyo4/v
++u0Ej5NTNcHFbFT01vdD9MjQiCO3jKdAwPIb14jTg4C71EpZ+LuelDo4DzF2/XgG
+WqUTrgD/XnBU/60PU9Iy3G0nVpx21q6ppn9G7a9R+i8FjBcwW1T+cfsBDWhAv+bi
+RmSAkENf8L8TwOlDQUwROkfz3Hz36vuJjdkreQJsiqL0HnrnH5T5G9UzJO86FvZQ
+5wIDAQABo0swSTBHBgNVHREEQDA+gh1kbnMxLmFsdC1uYW1lLnBnLXNzbHRlc3Qu
+dGVzdIIdZG5zMi5hbHQtbmFtZS5wZy1zc2x0ZXN0LnRlc3QwDQYJKoZIhvcNAQEL
+BQADggEBAF+mfaw6iBPzpCgqq830pHRa3Yzm1aezt8SkeRohUYHNv/yCnDSRaqtj
+xbENih3lJMSTBL3g0wtTOHfH8ViC/h+lvYELHzXKic7gkjV7H5XETKGr0ZsjBBT2
+4cZQKbD9e0x0HrENXMYgGpBf747qL6uTOVJdG0s15hwpLq47bY5WUjXathejbpxW
+prmF8F+xaC52N9P/1VnqguQB909F4x1pyOK7D7tjFu+Y8Je7PHKbb6WY5K6xAv6t
+R17CY0749/FotlphquElUR2bs5Zzv5YrjUHPTcbwKvcH5cdNi93/u6NJt2xNAoYf
+aZERhX5TA9DYk4gC8OY0yGaYCIj3Dd4=
+-----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-----
+MIIEogIBAAKCAQEA8xddbo/x2TOSIa/br8BNo/URdTr9+l2R5YojiZKDuLxiQVkg
+C30PJ2/CNFKIh2nHhRrzknI6sETVtrxZ+9V2qRc1yShVu462u0DHPRMIZnZIOZg3
+hlNB0cRWbOglUKlttIARNEQUcTUyPOtyo4/v+u0Ej5NTNcHFbFT01vdD9MjQiCO3
+jKdAwPIb14jTg4C71EpZ+LuelDo4DzF2/XgGWqUTrgD/XnBU/60PU9Iy3G0nVpx2
+1q6ppn9G7a9R+i8FjBcwW1T+cfsBDWhAv+biRmSAkENf8L8TwOlDQUwROkfz3Hz3
+6vuJjdkreQJsiqL0HnrnH5T5G9UzJO86FvZQ5wIDAQABAoIBAGv0BFoFMrHyZQLw
+xe7Wx6P4QTh+aiu1QgVdw0pk9nojrr62hbSUZRZuWyBBRsBcCW7i+Sgf8lA1QXNV
+UeC0e228EPa0It6YEi42JkTJHwHhpVFud7n/X0t4lajnryqTE1UFSp6bXTipFxZW
+uSJJ2ZjliRD5rApDcxkY4WJVjKg3aEt7P/DiM8iKGfyE6stq72VjEbJjdViMEcOP
+BNf0TiREZz5Mp7jAVWhpen0ebbLOBVWV4/ONNcL+yqR4mCEDUSFGewrTVX4zHL0A
+hYk198C5F8sFvEDnFkPco9sXMVanmLoI8sbhP4IIz9g4+GU6kFuj7fUKp11Azqv+
+3WQDKYECgYEA/XG4mmG/g8FG44y42mfZpUXWi1pwU4CQIrhkoU5j7EPQrvRboOOE
+Rv95jSwyZu4vCqjyI5FN1jCGTdhmt++R1e//zH6Hqa9Smo+jw7DtAFrCYd1JnCf1
+ToOwsYPHv4P7A8q8kc5vCNIv+AQSlP/wqdVNo3grdf7cGXkMtEY4F9UCgYEA9Yrq
+zWdnNGPATuSBqL6TSjQ37oR+dBD6WnGsiDenQkOzyDPFZ3CT1DjJghjEtxc8EfNf
+Oo8dMMR2q+5FZQo7WuqONEgyzKePiNR8RK2gOYpgdjN9bih1sAhHR10D26cpwlDJ
+bx7D5ZzENLbdZmfEiWwKswnaIhN4yMalgE0mP8sCgYAhzJy12ftUct4lUosEdX0N
+EXc/NlxshmSyfKzO5kllJNYbvvLJTg5B+agYL6C5IWKcpVNFcwdSXT5L+2QXe5eT
+VGJkvysQchUuD6HjYyD4PyJVMtGyRZHtWpqh0dU9sTg0lUD4oPMl1gIXrVNdE5Tg
+0VV9S3VgUxC/ROlw0TyB0QKBgGsVE0NS9hJF8mc1hko2GnwA++d8Rr2NbfElo+2f
+/8SJTA1ibpOm6AFkZpTjAl8qtdrKPVyHb16GP47Jkd/3r1z979hjKCxSYul0aWF2
+KusNKvZBjFEPOgv0AEniCb2wUCjbHI3mZ95qGLM4kKOJW4/m21+rS0MTJNjCsQic
+HLMzAoGAeCsY09d3m8xGeU+DuTPC6GH7Sgy/NBYqS5VaVNjb2jnuZlW2SSW2oiID
+4tXTi4ruKmHC898BfyFxhSMqub+tg3pVqIYADC71rnJLrVyc1SzoWzL7yMT3qFj7
+C7ZYZYmfG9agcZb5NkqKPTfCxkBhWbdgTTgBKVO/xQst8EUgko8=
+-----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-----
+MIIC8TCCAdkCCCAhESkRN1IAMA0GCSqGSIb3DQEBCwUAMEIxQDA+BgNVBAMMN1Rl
+c3QgQ0EgZm9yIFBvc3RncmVTUUwgU1NMIHJlZ3Jlc3Npb24gdGVzdCBzZXJ2ZXIg
+Y2VydHMwHhcNMjExMTI5MTkzNzUyWhcNNDkwNDE2MTkzNzUyWjA0MR4wHAYDVQQL
+DBVQb3N0Z3JlU1FMIHRlc3Qgc3VpdGUxEjAQBgNVBAMMCTE5Mi4wLjIuMTCCASIw
+DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANWs1uUL71nHYF9Zj6p+M3MpYDvx
+32iCjVdtH5a2qpSWHXTg0rR8dLX0y92cvOYvMXHRajZT1avpHr8dooPYSVaXpGMK
+NvF/Qi+WFYovRbP2vmd1yv1cgW/FggbwJFWVobizIz4seyA4d0B2j9fqoi2OFBNP
+huW664SjF0u3p21tDy+43i2LNUMAKf6dnRR5Vqenath87LEU41tSLudu6NXgbFMk
+jvfNkl4d0w7YCzeXmklmSI+uaX3PlJJ4NzQO2j8w5BvnKVhNVD0KjgrXZ6nB/8F7
+Pg3XY+d7rJlwRgXemU6resWQDJ7+UaC9u7I4EIP+9lzCR/nNBqUktpHRmHUCAwEA
+ATANBgkqhkiG9w0BAQsFAAOCAQEAos1JncV8Yf4UaKl6h1GdYtcVtzFyJvBEnhRD
+07ldL+TYnfZiX8wK2ssBtM3cg/C78y5bzdUa5XGS83ZKQJFFdhE7PSnrvyNqyIqY
+ZgNBxto3gyvir+EjO1u9BAB0NP3r3gYoHRDZS1xOPPzt4WgjuUgTLM9k82GsqAbO
+UrOTOdRnkIqC5xLpa05EnRyJPRsR1w1PRJC2XXKnHIuFjMb4v7UuPwyCcX1P5ioc
+rQszQcORy/L+k0ezCkyweORg68htjYbBHuwOuiGfok6yKKDMzrTvD3lIslls6eX7
+4sI3XWqzkPmG9Vsxm9Vu9/Ma+PRO76VyCoIwBd+Ufg5vNXhMmw==
+-----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-----
+MIIEowIBAAKCAQEA1azW5QvvWcdgX1mPqn4zcylgO/HfaIKNV20flraqlJYddODS
+tHx0tfTL3Zy85i8xcdFqNlPVq+kevx2ig9hJVpekYwo28X9CL5YVii9Fs/a+Z3XK
+/VyBb8WCBvAkVZWhuLMjPix7IDh3QHaP1+qiLY4UE0+G5brrhKMXS7enbW0PL7je
+LYs1QwAp/p2dFHlWp6dq2HzssRTjW1Iu527o1eBsUySO982SXh3TDtgLN5eaSWZI
+j65pfc+Ukng3NA7aPzDkG+cpWE1UPQqOCtdnqcH/wXs+Dddj53usmXBGBd6ZTqt6
+xZAMnv5RoL27sjgQg/72XMJH+c0GpSS2kdGYdQIDAQABAoIBAQDNXviU4WnF8rmQ
+K7bH+dBdqbETLKC8BG7xTrMD2sINWlMpmUUrsEtE7+paMGHnJAj0CoF5gg5m0wN4
+UXV4H5QtpEad4p14dAYbUreVP2ZRWKEdM7xM1HKcCUu2e22QzObJbXQ8N+iHyX3k
++Y+7yYrjGiH1hYR0nbnsnAyx++zyYBSQeqzpdQwf/BLY5xZmyYWNfqbckiMpEqMs
+EmZmGXnCjIipzEC0LQHoSW9PNa92Z9bvuxOKYl8iHYDDXjvMRFoZBSiMXpzHQocb
+QlQ5F4ayfW2OrOhpNbY7niYM9GN3Bk9TgMP+0BkJE6uuktLYW35LY1M78CCPWcWb
+npJNK3QBAoGBAOxkGrhAHAysSmtirIyMdvySb76wb/Ukfi+AULKz20FI5j4/GXm9
+qCb2GeT+FFSUHeSC8f0EFnosRYkdBGruqeZioI+5rUkboYFJPspAHAuvg9kgtfF+
+kvphD4O4P/foYsEZRx66FHozDbhrrR5UXc7KzqRIASc/D3FOx2UFJLb1AoGBAOdm
+WcaMvYygl9ZW+ThWAR1xG1X70AGKwrlrpF2hBkWYxSurxSMXnD0DUzC9Nb4EyCaM
+c2uSqEZOKdW+XfXtK2DnqXKfb3YCVEoGN4gVfyuW/vxii/+ZxLo3md/b3vrkZEVp
+pfkXy/HoZ71YN7bNpcDpOnhml6vvuCRCYFnI1WuBAoGAC0shB6pwbJ6Sk5zMN47C
+ZICufAK75o9OxAAyWsdC81SDQ3gKRImuDeZ2CD2nRP8qim9DFl5qoH2a+Nj9DArI
+7SvLFfK9958tURrpuAnmDRzehLIOXzI33WRjtFxKGhLtHOKTRkGHlur3fdcPF0La
+lHWV971E6NYXa8diuU3Mmj0CgYBYd+ka3/QYL83dRKNDxp3mg7fPx9ZewI5yFZVh
+to6PTTkU2Tclk4FIUl0b5TsGyw06r7fxCMENIBUegwmpXGOZSPifuhUDKSDQrE/O
+12knYTNbitG7hy6Pg3JxA77cbTVo1FuAQHjYo+IFohSq7zTP7FtObOrP8XaVZksw
+CHiQAQKBgBW4EiA9AAnZ1LOpifAvM7bs0NHg95qTwtAL52WKom2ga2H+lMhxeu6Y
+hUSytC/f9kALVcYloZhkLYpO07x1gXmy7f4parMjA4Ex+4vfu3kPd8GiNGZ+AUJD
+nnJ1OINY9ziXJZfju7FpVWpkiuPzWCh6y/o3gZ/veq5mIUxuDMVa
+-----END RSA PRIVATE KEY-----
diff --git a/src/test/ssl/sslfiles.mk b/src/test/ssl/sslfiles.mk
index 7ed3a30f5c..f9be5ffdba 100644
--- a/src/test/ssl/sslfiles.mk
+++ b/src/test/ssl/sslfiles.mk
@@ -22,9 +22,14 @@
 # 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-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 b1fb15ce80..e5a8080dc3 100644
--- a/src/test/ssl/t/001_ssltests.pl
+++ b/src/test/ssl/t/001_ssltests.pl
@@ -21,7 +21,7 @@ if ($ENV{with_ssl} ne 'openssl')
 }
 else
 {
-	plan tests => 110;
+	plan tests => 135;
 }
 
 #### Some configuration
@@ -253,6 +253,23 @@ $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/
+);
+
 # Test Subject Alternative Names.
 switch_server_cert($node, 'server-multiple-alt-names');
 
@@ -305,7 +322,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');
 
@@ -323,6 +387,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 6b4080e0db528a863feacf9c3cb89ca81948ef9c Mon Sep 17 00:00:00 2001
From: Jacob Champion <pchamp...@vmware.com>
Date: Wed, 5 Jan 2022 15:47:03 -0800
Subject: [PATCH v5 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 2b8f2d6b24..4d78715756 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 55bd55ae6e..00e297db17 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -554,7 +554,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

Reply via email to