Commit acd08d76 authored by Heikki Linnakangas's avatar Heikki Linnakangas

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.
parent 774a78ff
......@@ -1210,14 +1210,14 @@ PQenv2encoding(void)
#ifdef ENABLE_NLS
char *
libpq_gettext(const char *msgid)
static void
libpq_binddomain()
{
static bool already_bound = false;
if (!already_bound)
{
/* dgettext() preserves errno, but bindtextdomain() doesn't */
/* bindtextdomain() does not preserve errno */
#ifdef WIN32
int save_errno = GetLastError();
#else
......@@ -1237,8 +1237,20 @@ libpq_gettext(const char *msgid)
errno = save_errno;
#endif
}
}
char *
libpq_gettext(const char *msgid)
{
libpq_binddomain();
return dgettext(PG_TEXTDOMAIN("libpq"), msgid);
}
char *
libpq_ngettext(const char *msgid, const char *msgid_plural, unsigned long n)
{
libpq_binddomain();
return dngettext(PG_TEXTDOMAIN("libpq"), msgid, msgid_plural, n);
}
#endif /* ENABLE_NLS */
......@@ -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
......
......@@ -653,8 +653,12 @@ extern ssize_t pgtls_write(PGconn *conn, const void *ptr, size_t len);
extern char *
libpq_gettext(const char *msgid)
__attribute__((format_arg(1)));
extern char *
libpq_ngettext(const char *msgid, const char *msgid_plural, unsigned long n)
__attribute__((format_arg(1))) __attribute__((format_arg(2)));
#else
#define libpq_gettext(x) (x)
#define libpq_ngettext(s, p, n) ((n) == 1 ? (s) : (p))
#endif
/*
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment