summaryrefslogtreecommitdiffstats
path: root/src/interfaces/libpq/fe-secure-common.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/interfaces/libpq/fe-secure-common.c')
-rw-r--r--src/interfaces/libpq/fe-secure-common.c307
1 files changed, 307 insertions, 0 deletions
diff --git a/src/interfaces/libpq/fe-secure-common.c b/src/interfaces/libpq/fe-secure-common.c
new file mode 100644
index 0000000..3ecc7bf
--- /dev/null
+++ b/src/interfaces/libpq/fe-secure-common.c
@@ -0,0 +1,307 @@
+/*-------------------------------------------------------------------------
+ *
+ * fe-secure-common.c
+ *
+ * common implementation-independent SSL support code
+ *
+ * While fe-secure.c contains the interfaces that the rest of libpq call, this
+ * file contains support routines that are used by the library-specific
+ * implementations such as fe-secure-openssl.c.
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ * src/interfaces/libpq/fe-secure-common.c
+ *
+ *-------------------------------------------------------------------------
+ */
+
+#include "postgres_fe.h"
+
+#include <arpa/inet.h>
+
+#include "fe-secure-common.h"
+
+#include "libpq-int.h"
+#include "pqexpbuffer.h"
+
+/*
+ * Check if a wildcard certificate matches the server hostname.
+ *
+ * The rule for this is:
+ * 1. We only match the '*' character as wildcard
+ * 2. We match only wildcards at the start of the string
+ * 3. The '*' character does *not* match '.', meaning that we match only
+ * a single pathname component.
+ * 4. We don't support more than one '*' in a single pattern.
+ *
+ * This is roughly in line with RFC2818, but contrary to what most browsers
+ * appear to be implementing (point 3 being the difference)
+ *
+ * Matching is always case-insensitive, since DNS is case insensitive.
+ */
+static bool
+wildcard_certificate_match(const char *pattern, const char *string)
+{
+ int lenpat = strlen(pattern);
+ int lenstr = strlen(string);
+
+ /* If we don't start with a wildcard, it's not a match (rule 1 & 2) */
+ if (lenpat < 3 ||
+ pattern[0] != '*' ||
+ pattern[1] != '.')
+ return false;
+
+ /* If pattern is longer than the string, we can never match */
+ if (lenpat > lenstr)
+ return false;
+
+ /*
+ * If string does not end in pattern (minus the wildcard), we don't match
+ */
+ if (pg_strcasecmp(pattern + 1, string + lenstr - lenpat + 1) != 0)
+ return false;
+
+ /*
+ * If there is a dot left of where the pattern started to match, we don't
+ * match (rule 3)
+ */
+ if (strchr(string, '.') < string + lenstr - lenpat)
+ return false;
+
+ /* String ended with pattern, and didn't have a dot before, so we match */
+ return true;
+}
+
+/*
+ * Check if a name from a server's certificate matches the peer's hostname.
+ *
+ * Returns 1 if the name matches, and 0 if it does not. On error, returns
+ * -1, and sets the libpq error message.
+ *
+ * The name extracted from the certificate is returned in *store_name. The
+ * caller is responsible for freeing it.
+ */
+int
+pq_verify_peer_name_matches_certificate_name(PGconn *conn,
+ const char *namedata, size_t namelen,
+ char **store_name)
+{
+ char *name;
+ int result;
+ char *host = conn->connhost[conn->whichhost].host;
+
+ *store_name = NULL;
+
+ if (!(host && host[0] != '\0'))
+ {
+ libpq_append_conn_error(conn, "host name must be specified");
+ return -1;
+ }
+
+ /*
+ * There is no guarantee the string returned from the certificate is
+ * NULL-terminated, so make a copy that is.
+ */
+ name = malloc(namelen + 1);
+ if (name == NULL)
+ {
+ libpq_append_conn_error(conn, "out of memory");
+ return -1;
+ }
+ memcpy(name, namedata, namelen);
+ name[namelen] = '\0';
+
+ /*
+ * Reject embedded NULLs in certificate common or alternative name to
+ * prevent attacks like CVE-2009-4034.
+ */
+ if (namelen != strlen(name))
+ {
+ free(name);
+ libpq_append_conn_error(conn, "SSL certificate's name contains embedded null");
+ return -1;
+ }
+
+ if (pg_strcasecmp(name, host) == 0)
+ {
+ /* Exact name match */
+ result = 1;
+ }
+ else if (wildcard_certificate_match(name, host))
+ {
+ /* Matched wildcard name */
+ result = 1;
+ }
+ else
+ {
+ result = 0;
+ }
+
+ *store_name = name;
+ 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'))
+ {
+ libpq_append_conn_error(conn, "host name must be specified");
+ 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.
+ */
+ libpq_append_conn_error(conn, "certificate contains IP address with invalid length %zu",
+ 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)
+ {
+ libpq_append_conn_error(conn, "could not convert certificate's IP address to string: %s",
+ 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.
+ *
+ * The certificate's Common Name and Subject Alternative Names are considered.
+ */
+bool
+pq_verify_peer_name_matches_certificate(PGconn *conn)
+{
+ char *host = conn->connhost[conn->whichhost].host;
+ int rc;
+ int names_examined = 0;
+ char *first_name = NULL;
+
+ /*
+ * If told not to verify the peer name, don't do it. Return true
+ * indicating that the verification was successful.
+ */
+ if (strcmp(conn->sslmode, "verify-full") != 0)
+ return true;
+
+ /* Check that we have a hostname to compare with. */
+ if (!(host && host[0] != '\0'))
+ {
+ libpq_append_conn_error(conn, "host name must be specified for a verified SSL connection");
+ return false;
+ }
+
+ rc = pgtls_verify_peer_name_matches_certificate_guts(conn, &names_examined, &first_name);
+
+ if (rc == 0)
+ {
+ /*
+ * No match. Include the name from the server certificate in the error
+ * message, to aid debugging broken configurations. If there are
+ * multiple names, only print the first one to avoid an overly long
+ * error message.
+ */
+ if (names_examined > 1)
+ {
+ appendPQExpBuffer(&conn->errorMessage,
+ libpq_ngettext("server certificate for \"%s\" (and %d other name) does not match host name \"%s\"",
+ "server certificate for \"%s\" (and %d other names) does not match host name \"%s\"",
+ names_examined - 1),
+ first_name, names_examined - 1, host);
+ appendPQExpBufferChar(&conn->errorMessage, '\n');
+ }
+ else if (names_examined == 1)
+ {
+ libpq_append_conn_error(conn, "server certificate for \"%s\" does not match host name \"%s\"",
+ first_name, host);
+ }
+ else
+ {
+ libpq_append_conn_error(conn, "could not get server's host name from server certificate");
+ }
+ }
+
+ /* clean up */
+ free(first_name);
+
+ return (rc == 1);
+}