aboutsummaryrefslogtreecommitdiff
path: root/src/interfaces/libpq/fe-secure-openssl.c
diff options
context:
space:
mode:
authorHeikki Linnakangas <heikki.linnakangas@iki.fi>2014-09-12 17:12:11 +0300
committerHeikki Linnakangas <heikki.linnakangas@iki.fi>2014-09-12 17:17:05 +0300
commitacd08d764a361dcebd346227281ff0ca62b60936 (patch)
tree7061cbd18b063731d13646ca32ae4d9b39a4f78e /src/interfaces/libpq/fe-secure-openssl.c
parent774a78ffe47557313d69c2b27e7d61480a3b9d1f (diff)
downloadpostgresql-acd08d764a361dcebd346227281ff0ca62b60936.tar.gz
postgresql-acd08d764a361dcebd346227281ff0ca62b60936.zip
Support Subject Alternative Names in SSL server certificates.
This patch makes libpq check the server's hostname against DNS names listed in the X509 subjectAltName extension field in the server certificate. This allows the same certificate to be used for multiple domain names. If there are no SANs in the certificate, the Common Name field is used, like before this patch. If both are given, the Common Name is ignored. That is a bit surprising, but that's the behavior mandated by the relevant RFCs, and it's also what the common web browsers do. This also adds a libpq_ngettext helper macro to allow plural messages to be translated in libpq. Apparently this happened to be the first plural message in libpq, so it was not needed before. Alexey Klyukin, with some kibitzing by me.
Diffstat (limited to 'src/interfaces/libpq/fe-secure-openssl.c')
-rw-r--r--src/interfaces/libpq/fe-secure-openssl.c238
1 files changed, 183 insertions, 55 deletions
diff --git a/src/interfaces/libpq/fe-secure-openssl.c b/src/interfaces/libpq/fe-secure-openssl.c
index f950fc3bc3c..98d02b6b634 100644
--- a/src/interfaces/libpq/fe-secure-openssl.c
+++ b/src/interfaces/libpq/fe-secure-openssl.c
@@ -60,9 +60,13 @@
#ifdef USE_SSL_ENGINE
#include <openssl/engine.h>
#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,
+ ASN1_STRING *name,
+ char **store_name);
static void destroy_ssl_system(void);
static int initialize_SSL(PGconn *conn);
static PostgresPollingStatusType open_client_SSL(PGconn *);
@@ -473,98 +477,222 @@ wildcard_certificate_match(const char *pattern, const char *string)
/*
- * Verify that common name resolves to peer.
+ * 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.
*/
-static bool
-verify_peer_name_matches_certificate(PGconn *conn)
+static int
+verify_peer_name_matches_certificate_name(PGconn *conn, ASN1_STRING *name_entry,
+ char **store_name)
{
- char *peer_cn;
- int r;
int len;
- bool result;
+ char *name;
+ unsigned char *namedata;
+ int result;
- /*
- * 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;
+ *store_name = NULL;
+
+ /* Should not happen... */
+ if (name_entry == NULL)
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("SSL certificate's name entry is missing\n"));
+ return -1;
+ }
/*
- * Extract the common name from the certificate.
+ * GEN_DNS can be only IA5String, equivalent to US ASCII.
*
- * XXX: Should support alternate names here
+ * There is no guarantee the string returned from the certificate is
+ * NULL-terminated, so make a copy that is.
*/
- /* First find out the name's length and allocate a buffer for it. */
- len = X509_NAME_get_text_by_NID(X509_get_subject_name(conn->peer),
- NID_commonName, NULL, 0);
- if (len == -1)
+ namedata = ASN1_STRING_data(name_entry);
+ len = ASN1_STRING_length(name_entry);
+ name = malloc(len + 1);
+ if (name == NULL)
{
printfPQExpBuffer(&conn->errorMessage,
- libpq_gettext("could not get server common name from server certificate\n"));
- return false;
+ libpq_gettext("out of memory\n"));
+ return -1;
}
- peer_cn = malloc(len + 1);
- if (peer_cn == NULL)
+ 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("out of memory\n"));
- return false;
+ libpq_gettext("SSL certificate's name contains embedded null\n"));
+ return -1;
}
- r = X509_NAME_get_text_by_NID(X509_get_subject_name(conn->peer),
- NID_commonName, peer_cn, len + 1);
- if (r != len)
+ if (pg_strcasecmp(name, conn->pghost) == 0)
{
- /* Got different length than on the first call. Shouldn't happen. */
- printfPQExpBuffer(&conn->errorMessage,
- libpq_gettext("could not get server common name from server certificate\n"));
- free(peer_cn);
- return false;
+ /* Exact name match */
+ result = 1;
+ }
+ else if (wildcard_certificate_match(name, conn->pghost))
+ {
+ /* Matched wildcard name */
+ result = 1;
+ }
+ else
+ {
+ result = 0;
}
- peer_cn[len] = '\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.
+ */
+static bool
+verify_peer_name_matches_certificate(PGconn *conn)
+{
+ 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;
/*
- * Reject embedded NULLs in certificate common name to prevent attacks
- * like CVE-2009-4034.
+ * If told not to verify the peer name, don't do it. Return true
+ * indicating that the verification was successful.
*/
- if (len != strlen(peer_cn))
+ if (strcmp(conn->sslmode, "verify-full") != 0)
+ return true;
+
+ /* Check that we have a hostname to compare with. */
+ if (!(conn->pghost && conn->pghost[0] != '\0'))
{
printfPQExpBuffer(&conn->errorMessage,
- libpq_gettext("SSL certificate's common name contains embedded null\n"));
- free(peer_cn);
+ libpq_gettext("host name must be specified for a verified SSL connection\n"));
return false;
}
/*
- * We got the peer's common name. Now compare it against the originally
- * given hostname.
+ * First, get the Subject Alternative Names (SANs) from the certificate,
+ * and compare them against the originally given hostname.
*/
- if (!(conn->pghost && conn->pghost[0] != '\0'))
+ peer_san = (STACK_OF(GENERAL_NAME) *)
+ X509_get_ext_d2i(conn->peer, NID_subject_alt_name, NULL, NULL);
+
+ if (peer_san)
{
- printfPQExpBuffer(&conn->errorMessage,
- libpq_gettext("host name must be specified for a verified SSL connection\n"));
- result = false;
+ int san_len = sk_GENERAL_NAME_num(peer_san);
+
+ for (i = 0; i < san_len; i++)
+ {
+ const GENERAL_NAME *name = sk_GENERAL_NAME_value(peer_san, i);
+
+ if (name->type == GEN_DNS)
+ {
+ char *alt_name;
+
+ names_examined++;
+ rc = 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;
+ else
+ free(alt_name);
+ }
+ }
+ if (found_match || got_error)
+ break;
+ }
+ sk_GENERAL_NAME_free(peer_san);
}
+ /*
+ * If there is no subjectAltName extension, check the Common Name.
+ *
+ * (Per RFC 2818 and RFC 6125, if the subjectAltName extension is present,
+ * the CN must be ignored.)
+ */
else
{
- if (pg_strcasecmp(peer_cn, conn->pghost) == 0)
- /* Exact name match */
- result = true;
- else if (wildcard_certificate_match(peer_cn, conn->pghost))
- /* Matched wildcard certificate */
- result = true;
+ X509_NAME *subject_name;
+
+ subject_name = X509_get_subject_name(conn->peer);
+ if (subject_name != NULL)
+ {
+ int cn_index;
+
+ cn_index = X509_NAME_get_index_by_NID(subject_name,
+ NID_commonName, -1);
+ if (cn_index >= 0)
+ {
+ names_examined++;
+ rc = 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;
+ }
+ }
+ }
+
+ 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, conn->pghost);
+ }
+ else if (names_examined == 1)
+ {
+ printfPQExpBuffer(&conn->errorMessage,
+ libpq_gettext("server certificate for \"%s\" does not match host name \"%s\"\n"),
+ first_name, conn->pghost);
+ }
else
{
printfPQExpBuffer(&conn->errorMessage,
- libpq_gettext("server common name \"%s\" does not match host name \"%s\"\n"),
- peer_cn, conn->pghost);
- result = false;
+ libpq_gettext("could not get server's hostname from server certificate\n"));
}
}
- free(peer_cn);
- return result;
+ /* clean up */
+ if (first_name)
+ free(first_name);
+
+ return found_match && !got_error;
}
#ifdef ENABLE_THREAD_SAFETY