diff options
author | Peter Eisentraut <peter_e@gmx.net> | 2018-01-27 13:47:52 -0500 |
---|---|---|
committer | Peter Eisentraut <peter_e@gmx.net> | 2018-01-30 22:56:24 -0500 |
commit | f75a95915528646cbfaf238fb48b3ffa17969383 (patch) | |
tree | 39ae8490eadc1b2c965b569be2e43abe77ca1c49 /src | |
parent | 38d485fdaa5739627b642303cc172acc1487b90a (diff) | |
download | postgresql-f75a95915528646cbfaf238fb48b3ffa17969383.tar.gz |
Refactor client-side SSL certificate checking code
Separate the parts specific to the SSL library from the general logic.
The previous code structure was
open_client_SSL()
calls verify_peer_name_matches_certificate()
calls verify_peer_name_matches_certificate_name()
calls wildcard_certificate_match()
and was completely in fe-secure-openssl.c. The new structure is
open_client_SSL() [openssl]
calls pq_verify_peer_name_matches_certificate() [generic]
calls pgtls_verify_peer_name_matches_certificate_guts() [openssl]
calls openssl_verify_peer_name_matches_certificate_name() [openssl]
calls pq_verify_peer_name_matches_certificate_name() [generic]
calls wildcard_certificate_match() [generic]
Move the generic functions into a new file fe-secure-common.c, so the
calls generally go fe-connect.c -> fe-secure.c -> fe-secure-${impl}.c ->
fe-secure-common.c, although there is a bit of back-and-forth between
the last two.
Reviewed-by: Michael Paquier <michael.paquier@gmail.com>
Diffstat (limited to 'src')
-rw-r--r-- | src/interfaces/libpq/Makefile | 2 | ||||
-rw-r--r-- | src/interfaces/libpq/fe-secure-common.c | 204 | ||||
-rw-r--r-- | src/interfaces/libpq/fe-secure-common.h | 26 | ||||
-rw-r--r-- | src/interfaces/libpq/fe-secure-openssl.c | 210 | ||||
-rw-r--r-- | src/interfaces/libpq/libpq-int.h | 13 | ||||
-rw-r--r-- | src/interfaces/libpq/nls.mk | 2 | ||||
-rw-r--r-- | src/tools/msvc/Mkvcbuild.pm | 1 |
7 files changed, 271 insertions, 187 deletions
diff --git a/src/interfaces/libpq/Makefile b/src/interfaces/libpq/Makefile index 0bf1e7ef04..abe0a50e98 100644 --- a/src/interfaces/libpq/Makefile +++ b/src/interfaces/libpq/Makefile @@ -52,7 +52,7 @@ OBJS += encnames.o wchar.o OBJS += base64.o ip.o md5.o scram-common.o saslprep.o unicode_norm.o ifeq ($(with_openssl),yes) -OBJS += fe-secure-openssl.o sha2_openssl.o +OBJS += fe-secure-openssl.o fe-secure-common.o sha2_openssl.o else OBJS += sha2.o endif diff --git a/src/interfaces/libpq/fe-secure-common.c b/src/interfaces/libpq/fe-secure-common.c new file mode 100644 index 0000000000..40203f3b64 --- /dev/null +++ b/src/interfaces/libpq/fe-secure-common.c @@ -0,0 +1,204 @@ +/*------------------------------------------------------------------------- + * + * 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-2018, 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 "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 = PQhost(conn); + + *store_name = NULL; + + /* + * 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) + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("out of memory\n")); + 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); + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("SSL certificate's name contains embedded null\n")); + 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; +} + +/* + * 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 = PQhost(conn); + 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')) + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("host name must be specified for a verified SSL connection\n")); + 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) + { + printfPQExpBuffer(&conn->errorMessage, + libpq_ngettext("server certificate for \"%s\" (and %d other name) does not match host name \"%s\"\n", + "server certificate for \"%s\" (and %d other names) does not match host name \"%s\"\n", + names_examined - 1), + first_name, names_examined - 1, host); + } + else if (names_examined == 1) + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("server certificate for \"%s\" does not match host name \"%s\"\n"), + first_name, host); + } + else + { + printfPQExpBuffer(&conn->errorMessage, + libpq_gettext("could not get server's host name from server certificate\n")); + } + } + + /* clean up */ + if (first_name) + free(first_name); + + return (rc == 1); +} diff --git a/src/interfaces/libpq/fe-secure-common.h b/src/interfaces/libpq/fe-secure-common.h new file mode 100644 index 0000000000..980a58af25 --- /dev/null +++ b/src/interfaces/libpq/fe-secure-common.h @@ -0,0 +1,26 @@ +/*------------------------------------------------------------------------- + * + * fe-secure-common.h + * + * common implementation-independent SSL support code + * + * Portions Copyright (c) 1996-2018, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * IDENTIFICATION + * src/interfaces/libpq/fe-secure-common.h + * + *------------------------------------------------------------------------- + */ + +#ifndef FE_SECURE_COMMON_H +#define FE_SECURE_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 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 9ab317320a..cade4e157c 100644 --- a/src/interfaces/libpq/fe-secure-openssl.c +++ b/src/interfaces/libpq/fe-secure-openssl.c @@ -28,6 +28,7 @@ #include "libpq-fe.h" #include "fe-auth.h" +#include "fe-secure-common.h" #include "libpq-int.h" #ifdef WIN32 @@ -60,9 +61,8 @@ #endif #include <openssl/x509v3.h> -static bool verify_peer_name_matches_certificate(PGconn *); static int verify_cb(int ok, X509_STORE_CTX *ctx); -static int verify_peer_name_matches_certificate_name(PGconn *conn, +static int openssl_verify_peer_name_matches_certificate_name(PGconn *conn, ASN1_STRING *name, char **store_name); static void destroy_ssl_system(void); @@ -492,76 +492,16 @@ verify_cb(int ok, X509_STORE_CTX *ctx) /* - * 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 int -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 0; - - if (lenpat > lenstr) - /* If pattern is longer than the string, we can never match */ - return 0; - - if (pg_strcasecmp(pattern + 1, string + lenstr - lenpat + 1) != 0) - - /* - * If string does not end in pattern (minus the wildcard), we don't - * match - */ - return 0; - - if (strchr(string, '.') < string + lenstr - lenpat) - - /* - * If there is a dot left of where the pattern started to match, we - * don't match (rule 3) - */ - return 0; - - /* String ended with pattern, and didn't have a dot before, so we match */ - return 1; -} - -/* - * 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. + * OpenSSL-specific wrapper around + * pq_verify_peer_name_matches_certificate_name(), converting the ASN1_STRING + * into a plain C string. */ static int -verify_peer_name_matches_certificate_name(PGconn *conn, ASN1_STRING *name_entry, - char **store_name) +openssl_verify_peer_name_matches_certificate_name(PGconn *conn, ASN1_STRING *name_entry, + char **store_name) { int len; - char *name; const unsigned char *namedata; - int result; - char *host = PQhost(conn); - - *store_name = NULL; /* Should not happen... */ if (name_entry == NULL) @@ -573,9 +513,6 @@ verify_peer_name_matches_certificate_name(PGconn *conn, ASN1_STRING *name_entry, /* * GEN_DNS can be only IA5String, equivalent to US ASCII. - * - * There is no guarantee the string returned from the certificate is - * NULL-terminated, so make a copy that is. */ #ifdef HAVE_ASN1_STRING_GET0_DATA namedata = ASN1_STRING_get0_data(name_entry); @@ -583,45 +520,9 @@ verify_peer_name_matches_certificate_name(PGconn *conn, ASN1_STRING *name_entry, namedata = ASN1_STRING_data(name_entry); #endif len = ASN1_STRING_length(name_entry); - name = malloc(len + 1); - if (name == NULL) - { - printfPQExpBuffer(&conn->errorMessage, - libpq_gettext("out of memory\n")); - return -1; - } - memcpy(name, namedata, len); - name[len] = '\0'; - - /* - * Reject embedded NULLs in certificate common or alternative name to - * prevent attacks like CVE-2009-4034. - */ - if (len != strlen(name)) - { - free(name); - printfPQExpBuffer(&conn->errorMessage, - libpq_gettext("SSL certificate's name contains embedded null\n")); - 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; + /* OK to cast from unsigned to plain char, since it's all ASCII. */ + return pq_verify_peer_name_matches_certificate_name(conn, (const char *) namedata, len, store_name); } /* @@ -629,33 +530,14 @@ verify_peer_name_matches_certificate_name(PGconn *conn, ASN1_STRING *name_entry, * * The certificate's Common Name and Subject Alternative Names are considered. */ -static bool -verify_peer_name_matches_certificate(PGconn *conn) +int +pgtls_verify_peer_name_matches_certificate_guts(PGconn *conn, + int *names_examined, + char **first_name) { - int names_examined = 0; - bool found_match = false; - bool got_error = false; - char *first_name = NULL; - STACK_OF(GENERAL_NAME) *peer_san; int i; - int rc; - char *host = PQhost(conn); - - /* - * 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')) - { - printfPQExpBuffer(&conn->errorMessage, - libpq_gettext("host name must be specified for a verified SSL connection\n")); - return false; - } + int rc = 0; /* * First, get the Subject Alternative Names (SANs) from the certificate, @@ -676,24 +558,20 @@ verify_peer_name_matches_certificate(PGconn *conn) { char *alt_name; - names_examined++; - rc = verify_peer_name_matches_certificate_name(conn, + (*names_examined)++; + rc = openssl_verify_peer_name_matches_certificate_name(conn, name->d.dNSName, &alt_name); - if (rc == -1) - got_error = true; - if (rc == 1) - found_match = true; if (alt_name) { - if (!first_name) - first_name = alt_name; + if (!*first_name) + *first_name = alt_name; else free(alt_name); } } - if (found_match || got_error) + if (rc != 0) break; } sk_GENERAL_NAME_free(peer_san); @@ -706,7 +584,7 @@ verify_peer_name_matches_certificate(PGconn *conn) * (Per RFC 2818 and RFC 6125, if the subjectAltName extension of type * dNSName is present, the CN must be ignored.) */ - if (names_examined == 0) + if (*names_examined == 0) { X509_NAME *subject_name; @@ -719,55 +597,17 @@ verify_peer_name_matches_certificate(PGconn *conn) NID_commonName, -1); if (cn_index >= 0) { - names_examined++; - rc = verify_peer_name_matches_certificate_name( + (*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); - - if (rc == -1) - got_error = true; - else if (rc == 1) - found_match = true; + first_name); } } } - if (!found_match && !got_error) - { - /* - * 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) - { - printfPQExpBuffer(&conn->errorMessage, - libpq_ngettext("server certificate for \"%s\" (and %d other name) does not match host name \"%s\"\n", - "server certificate for \"%s\" (and %d other names) does not match host name \"%s\"\n", - names_examined - 1), - first_name, names_examined - 1, host); - } - else if (names_examined == 1) - { - printfPQExpBuffer(&conn->errorMessage, - libpq_gettext("server certificate for \"%s\" does not match host name \"%s\"\n"), - first_name, host); - } - else - { - printfPQExpBuffer(&conn->errorMessage, - libpq_gettext("could not get server's host name from server certificate\n")); - } - } - - /* clean up */ - if (first_name) - free(first_name); - - return found_match && !got_error; + return rc; } #if defined(ENABLE_THREAD_SAFETY) && defined(HAVE_CRYPTO_LOCK) @@ -1441,7 +1281,7 @@ open_client_SSL(PGconn *conn) return PGRES_POLLING_FAILED; } - if (!verify_peer_name_matches_certificate(conn)) + if (!pq_verify_peer_name_matches_certificate(conn)) { pgtls_close(conn); return PGRES_POLLING_FAILED; diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index b3492b033a..eba23dcecc 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -732,6 +732,19 @@ extern char *pgtls_get_finished(PGconn *conn, size_t *len); */ extern char *pgtls_get_peer_certificate_hash(PGconn *conn, size_t *len); +/* + * Verify that the server certificate matches the host name we connected to. + * + * The certificate's Common Name and Subject Alternative Names are considered. + * + * Returns 1 if the name matches, and 0 if it does not. On error, returns + * -1, and sets the libpq error message. + * + */ +extern int pgtls_verify_peer_name_matches_certificate_guts(PGconn *conn, + int *names_examined, + char **first_name); + /* === miscellaneous macros === */ /* diff --git a/src/interfaces/libpq/nls.mk b/src/interfaces/libpq/nls.mk index 2c5659e262..4196870b49 100644 --- a/src/interfaces/libpq/nls.mk +++ b/src/interfaces/libpq/nls.mk @@ -1,6 +1,6 @@ # src/interfaces/libpq/nls.mk CATALOG_NAME = libpq AVAIL_LANGUAGES = cs de es fr he it ja ko pl pt_BR ru sv tr zh_CN zh_TW -GETTEXT_FILES = fe-auth.c fe-auth-scram.c fe-connect.c fe-exec.c fe-lobj.c fe-misc.c fe-protocol2.c fe-protocol3.c fe-secure.c fe-secure-openssl.c win32.c +GETTEXT_FILES = fe-auth.c fe-auth-scram.c fe-connect.c fe-exec.c fe-lobj.c fe-misc.c fe-protocol2.c fe-protocol3.c fe-secure.c fe-secure-common.c fe-secure-openssl.c win32.c GETTEXT_TRIGGERS = libpq_gettext pqInternalNotice:2 GETTEXT_FLAGS = libpq_gettext:1:pass-c-format pqInternalNotice:2:c-format diff --git a/src/tools/msvc/Mkvcbuild.pm b/src/tools/msvc/Mkvcbuild.pm index 93f364a9f2..d8c279ab92 100644 --- a/src/tools/msvc/Mkvcbuild.pm +++ b/src/tools/msvc/Mkvcbuild.pm @@ -242,6 +242,7 @@ sub mkvcbuild # building with OpenSSL. if (!$solution->{options}->{openssl}) { + $libpq->RemoveFile('src/interfaces/libpq/fe-secure-common.c'); $libpq->RemoveFile('src/interfaces/libpq/fe-secure-openssl.c'); $libpq->RemoveFile('src/common/sha2_openssl.c'); } |