mirror of
https://github.com/postgres/postgres.git
synced 2025-07-11 10:01:57 +03:00
libpq: Allow IP address SANs in server certificates
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.
Author: Jacob Champion <pchampion@vmware.com>
Co-authored-by: Kyotaro Horiguchi <horikyota.ntt@gmail.com>
Co-authored-by: Daniel Gustafsson <daniel@yesql.se>
Discussion: https://www.postgresql.org/message-id/flat/9f5f20974cd3a4091a788cf7f00ab663d5fcdffe.camel@vmware.com
This commit is contained in:
@ -19,6 +19,8 @@
|
||||
|
||||
#include "postgres_fe.h"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
|
||||
#include "fe-secure-common.h"
|
||||
|
||||
#include "libpq-int.h"
|
||||
@ -144,6 +146,108 @@ 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 = 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;
|
||||
}
|
||||
}
|
||||
/*
|
||||
* If they don't have inet_pton(), skip this. Then, an IPv6 address in a
|
||||
* certificate will cause an error.
|
||||
*/
|
||||
#ifdef HAVE_INET_PTON
|
||||
else if (iplen == 16)
|
||||
{
|
||||
/* IPv6 */
|
||||
struct in6_addr addr;
|
||||
|
||||
family = AF_INET6;
|
||||
|
||||
if (inet_pton(AF_INET6, host, &addr) == 1)
|
||||
{
|
||||
if (memcmp(ipdata, &addr.s6_addr, iplen) == 0)
|
||||
match = 1;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
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.
|
||||
*
|
||||
|
@ -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 */
|
||||
|
@ -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,56 @@ 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;
|
||||
#ifdef HAVE_INET_PTON
|
||||
struct in6_addr dummy6;
|
||||
#endif
|
||||
|
||||
return inet_aton(host, &dummy4)
|
||||
#ifdef HAVE_INET_PTON
|
||||
|| (inet_pton(AF_INET6, host, &dummy6) == 1)
|
||||
#endif
|
||||
;
|
||||
}
|
||||
|
||||
/*
|
||||
* Verify that the server certificate matches the hostname we connected to.
|
||||
*
|
||||
@ -522,6 +575,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 +620,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 == 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)
|
||||
{
|
||||
char *alt_name;
|
||||
|
||||
(*names_examined)++;
|
||||
rc = openssl_verify_peer_name_matches_certificate_name(conn,
|
||||
name->d.dNSName,
|
||||
&alt_name);
|
||||
|
||||
if (alt_name)
|
||||
{
|
||||
if (!*first_name)
|
||||
*first_name = alt_name;
|
||||
else
|
||||
free(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 (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 +688,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user