libpurple/plugins/ssl/ssl-nss.c

Tue, 07 Mar 2017 00:22:58 -0300

author
dx <dx@dxzone.com.ar>
date
Tue, 07 Mar 2017 00:22:58 -0300
branch
release-2.x.y
changeset 38216
887efbd652d8
parent 38214
b3d0ba7c75f6
child 38231
d49ec3c44a2e
permissions
-rw-r--r--

certificate: Use public key fingerprint to compare certificates

This fixes an issue with google talk's certificates and gnutls, where the root
certificate in the provided chain is a slightly different version of the one
that is usually present in the certificate stores, but the SubjectPublicKeyInfo
section is the same.

This adds a PurpleCertificateScheme function, compare_pubkeys, and its wrapper
purple_certificate_compare_pubkeys().

This is only implemented for gnutls, since the NSS plugin only uses the NSS
certificate validation code. Even if that path was reachable from a plugin that
doesn't implement this method, it would return FALSE and behave as if this bug
was never fixed.

The gnutls implementation uses the gnutls_x509_crt_get_key_id() function,
which returns a hash of the SubjectPublicKeyInfo section of the certificate.

In gnutls versions older than 3.4.1, this may be a SHA1 hash, but after that
version SHA256 support was added (without much fanfare - the documentation
barely mentions this at all), and we just use the constant for the best known
algo, which for current versions is just SHA256. Older versions ignore that
flag parameter.

The whole comparison is modeled after the private _gnutls_check_if_same_key(),
which checks if both certificates have the same DN ("unique id") and does a
memcmp() of the raw SPKI section. We don't have direct access to the raw SPKI
section but comparing their fingerprints is good enough.

/**
 * @file ssl-nss.c Mozilla NSS SSL plugin.
 *
 * purple
 *
 * Copyright (C) 2003 Christian Hammond <chipx86@gnupdate.org>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02111-1301  USA
 */
#include "internal.h"
#include "debug.h"
#include "certificate.h"
#include "plugin.h"
#include "sslconn.h"
#include "util.h"
#include "version.h"

#define SSL_NSS_PLUGIN_ID "ssl-nss"

#ifdef _WIN32
# ifndef HAVE_LONG_LONG
#define HAVE_LONG_LONG
/* WINDDK_BUILD is defined because the checks around usage of
 * intrisic functions are wrong in nspr */
#define WINDDK_BUILD
# endif
#else
/* TODO: Why is this done?
 * This is probably being overridden by <nspr.h> (prcpucfg.h) on *nix OSes */
#undef HAVE_LONG_LONG /* Make Mozilla less angry. If angry, Mozilla SMASH! */
#endif

#include <nspr.h>
#include <nss.h>
#include <nssb64.h>
#include <ocsp.h>
#include <pk11func.h>
#include <prio.h>
#include <secerr.h>
#include <secmod.h>
#include <ssl.h>
#include <sslerr.h>
#include <sslproto.h>

/* There's a bug in some versions of this header that requires that some of
   the headers above be included first. This is true for at least libnss
   3.15.4. */
#include <certdb.h>

/* This is defined in NSPR's <private/pprio.h>, but to avoid including a
 * private header we duplicate the prototype here */
NSPR_API(PRFileDesc*)  PR_ImportTCPSocket(PRInt32 osfd);

typedef struct
{
	PRFileDesc *fd;
	PRFileDesc *in;
	guint handshake_handler;
	guint handshake_timer;
} PurpleSslNssData;

#define PURPLE_SSL_NSS_DATA(gsc) ((PurpleSslNssData *)gsc->private_data)

static const PRIOMethods *_nss_methods = NULL;
static PRDescIdentity _identity;
static PurpleCertificateScheme x509_nss;

/* Thank you, Evolution */
static void
set_errno(int code)
{
	/* FIXME: this should handle more. */
	switch (code) {
	case PR_INVALID_ARGUMENT_ERROR:
		errno = EINVAL;
		break;
	case PR_PENDING_INTERRUPT_ERROR:
		errno = EINTR;
		break;
	case PR_IO_PENDING_ERROR:
		errno = EAGAIN;
		break;
	case PR_WOULD_BLOCK_ERROR:
		errno = EAGAIN;
		/*errno = EWOULDBLOCK; */
		break;
	case PR_IN_PROGRESS_ERROR:
		errno = EINPROGRESS;
		break;
	case PR_ALREADY_INITIATED_ERROR:
		errno = EALREADY;
		break;
	case PR_NETWORK_UNREACHABLE_ERROR:
		errno = EHOSTUNREACH;
		break;
	case PR_CONNECT_REFUSED_ERROR:
		errno = ECONNREFUSED;
		break;
	case PR_CONNECT_TIMEOUT_ERROR:
	case PR_IO_TIMEOUT_ERROR:
		errno = ETIMEDOUT;
		break;
	case PR_NOT_CONNECTED_ERROR:
		errno = ENOTCONN;
		break;
	case PR_CONNECT_RESET_ERROR:
		errno = ECONNRESET;
		break;
	case PR_IO_ERROR:
	default:
		errno = EIO;
		break;
	}
}

static gchar *get_error_text(void)
{
	PRInt32 len = PR_GetErrorTextLength();
	gchar *ret = NULL;

	if (len > 0) {
		ret = g_malloc(len + 1);
		len = PR_GetErrorText(ret);
		ret[len] = '\0';
	}

	return ret;
}

static const PRUint16 default_ciphers[] = {
#if NSS_VMAJOR > 3 || ( NSS_VMAJOR == 3 && NSS_VMINOR > 15 ) \
		|| ( NSS_VMAJOR == 3 && NSS_VMINOR == 15 && NSS_VPATCH >= 1 )
	TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
	TLS_DHE_RSA_WITH_AES_128_CBC_SHA256,
	TLS_DHE_RSA_WITH_AES_256_CBC_SHA256,
# if NSS_VMAJOR > 3 || ( NSS_VMAJOR == 3 && NSS_VMINOR > 15 ) \
		|| ( NSS_VMAJOR == 3 && NSS_VMINOR == 15 && NSS_VPATCH >= 2 )
	TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
	TLS_DHE_RSA_WITH_AES_128_GCM_SHA256,
	TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
# endif
#endif
	TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
	TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,

	TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
	TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,

	TLS_DHE_RSA_WITH_AES_128_CBC_SHA,

	TLS_DHE_RSA_WITH_AES_256_CBC_SHA,

	TLS_DHE_DSS_WITH_AES_128_CBC_SHA, /* deprecated (DSS) */
	/* TLS_DHE_DSS_WITH_AES_256_CBC_SHA, false }, // deprecated (DSS) */

	TLS_ECDHE_RSA_WITH_RC4_128_SHA,		/* deprecated (RC4) */
	TLS_ECDHE_ECDSA_WITH_RC4_128_SHA, 	/* deprecated (RC4) */

	/* RFC 6120 Mandatory */
	TLS_RSA_WITH_AES_128_CBC_SHA,		/* deprecated (RSA key exchange) */
	TLS_RSA_WITH_AES_256_CBC_SHA,		/* deprecated (RSA key exchange) */
	/* TLS_RSA_WITH_3DES_EDE_CBC_SHA, 	 deprecated (RSA key exchange, 3DES) */

	0 /* end marker */
};

/* It's unfortunate we need to manage these manually,
 * ideally NSS would choose good defaults.
 * This is mostly based on FireFox's list:
 * https://hg.mozilla.org/mozilla-central/log/default/security/manager/ssl/src/nsNSSComponent.cpp */
static void ssl_nss_init_ciphers(void) {
	/* Disable any ciphers that NSS might have enabled by default */
	const PRUint16 *cipher;
	for (cipher = SSL_GetImplementedCiphers(); *cipher != 0; ++cipher) {
		SSL_CipherPrefSetDefault(*cipher, PR_FALSE);
	}

	/* Now only set SSL/TLS ciphers we knew about at compile time */
	for (cipher = default_ciphers; *cipher != 0; ++cipher) {
		SSL_CipherPrefSetDefault(*cipher, PR_TRUE);
	}

	/* Now log the available and enabled Ciphers */
	for (cipher = SSL_GetImplementedCiphers(); *cipher != 0; ++cipher) {
		const PRUint16 suite = *cipher;
		SECStatus rv;
		PRBool enabled;
		SSLCipherSuiteInfo info;

		rv = SSL_CipherPrefGetDefault(suite, &enabled);
		if (rv != SECSuccess) {
			gchar *error_txt = get_error_text();
			purple_debug_warning("nss",
					"SSL_CipherPrefGetDefault didn't like value 0x%04x: %s\n",
					suite, error_txt);
			g_free(error_txt);
			continue;
		}
		rv = SSL_GetCipherSuiteInfo(suite, &info, (int)(sizeof info));
		if (rv != SECSuccess) {
			gchar *error_txt = get_error_text();
			purple_debug_warning("nss",
					"SSL_GetCipherSuiteInfo didn't like value 0x%04x: %s\n",
					suite, error_txt);
			g_free(error_txt);
			continue;
		}
		purple_debug_info("nss", "Cipher - %s: %s\n",
				info.cipherSuiteName,
				enabled ? "Enabled" : "Disabled");
	}
}

static void
ssl_nss_init_nss(void)
{
#if NSS_VMAJOR > 3 || ( NSS_VMAJOR == 3 && NSS_VMINOR >= 14 )
	SSLVersionRange supported, enabled;
#endif /* NSS >= 3.14 */

	PR_Init(PR_SYSTEM_THREAD, PR_PRIORITY_NORMAL, 1);
	NSS_NoDB_Init(".");
#if (NSS_VMAJOR == 3 && (NSS_VMINOR < 15 || (NSS_VMINOR == 15 && NSS_VPATCH < 2)))
	NSS_SetDomesticPolicy();
#endif /* NSS < 3.15.2 */

	ssl_nss_init_ciphers();

#if NSS_VMAJOR > 3 || ( NSS_VMAJOR == 3 && NSS_VMINOR >= 14 )
	/* Get the ranges of supported and enabled SSL versions */
	if ((SSL_VersionRangeGetSupported(ssl_variant_stream, &supported) == SECSuccess) &&
			(SSL_VersionRangeGetDefault(ssl_variant_stream, &enabled) == SECSuccess)) {
		purple_debug_info("nss", "TLS supported versions: "
				"0x%04hx through 0x%04hx\n", supported.min, supported.max);
		purple_debug_info("nss", "TLS versions allowed by default: "
				"0x%04hx through 0x%04hx\n", enabled.min, enabled.max);

		/* Make sure all versions of TLS supported by the local library are
		   enabled. (For some reason NSS doesn't enable newer versions of TLS
		   by default -- more context in ticket #15909.) */
		if (supported.max > enabled.max) {
			enabled.max = supported.max;
			if (SSL_VersionRangeSetDefault(ssl_variant_stream, &enabled) == SECSuccess) {
				purple_debug_info("nss", "Changed allowed TLS versions to "
						"0x%04hx through 0x%04hx\n", enabled.min, enabled.max);
			} else {
				purple_debug_error("nss", "Error setting allowed TLS versions to "
						"0x%04hx through 0x%04hx\n", enabled.min, enabled.max);
			}
		}
	}
#endif /* NSS >= 3.14 */

	/** Disable OCSP Checking until we can make that use our HTTP & Proxy stuff */
	CERT_EnableOCSPChecking(PR_FALSE);

	_identity = PR_GetUniqueIdentity("Purple");
	_nss_methods = PR_GetDefaultIOMethods();

}

static SECStatus
ssl_auth_cert(void *arg, PRFileDesc *socket, PRBool checksig, PRBool is_server)
{
	/* We just skip cert verification here, and will verify the whole chain
	 * in ssl_nss_handshake_cb, after the handshake is complete.
	 *
	 * The problem is, purple_certificate_verify is asynchronous and
	 * ssl_auth_cert should return the result synchronously (it may ask the
	 * user, if an unknown certificate should be trusted or not).
	 *
	 * Ideally, SSL_AuthCertificateHook/ssl_auth_cert should decide
	 * immediately, if the certificate chain is already trusted and possibly
	 * SSL_BadCertHook to deal with unknown certificates.
	 *
	 * Current implementation may not be ideal, but is no less secure in
	 * terms of MITM attack.
	 */
	return SECSuccess;
}

static gboolean
ssl_nss_init(void)
{
   return TRUE;
}

static void
ssl_nss_uninit(void)
{
	NSS_Shutdown();
	PR_Cleanup();

	_nss_methods = NULL;
}

static void
ssl_nss_verified_cb(PurpleCertificateVerificationStatus st,
		       gpointer userdata)
{
	PurpleSslConnection *gsc = (PurpleSslConnection *) userdata;

	if (st == PURPLE_CERTIFICATE_VALID) {
		/* Certificate valid? Good! Do the connection! */
		gsc->connect_cb(gsc->connect_cb_data, gsc, PURPLE_INPUT_READ);
	} else {
		/* Otherwise, signal an error */
		if(gsc->error_cb != NULL)
			gsc->error_cb(gsc, PURPLE_SSL_CERTIFICATE_INVALID,
				      gsc->connect_cb_data);
		purple_ssl_close(gsc);
	}
}

/** Transforms an NSS containing an X.509 certificate into a Certificate instance
 *
 * @param cert   Certificate to transform
 * @return A newly allocated Certificate
 */
static PurpleCertificate *
x509_import_from_nss(CERTCertificate* cert)
{
	/* New certificate to return */
	PurpleCertificate * crt;

	/* Allocate the certificate and load it with data */
	crt = g_new0(PurpleCertificate, 1);
	crt->scheme = &x509_nss;
	crt->data = CERT_DupCertificate(cert);

	return crt;
}

static GList *
ssl_nss_get_peer_certificates(PRFileDesc *socket, PurpleSslConnection * gsc)
{
	CERTCertificate *curcert;
	CERTCertificate *issuerCert;
	PurpleCertificate * newcrt;

	/* List of Certificate instances to return */
	GList * peer_certs = NULL;
	int count;
	int64 now = PR_Now();

	curcert = SSL_PeerCertificate(socket);
	if (curcert == NULL) {
		purple_debug_error("nss", "could not DupCertificate\n");
		return NULL;
	}

	for (count = 0 ; count < CERT_MAX_CERT_CHAIN ; count++) {
		purple_debug_info("nss", "subject=%s issuer=%s\n", curcert->subjectName,
						  curcert->issuerName  ? curcert->issuerName : "(null)");
		newcrt = x509_import_from_nss(curcert);
		peer_certs = g_list_append(peer_certs, newcrt);

		if (curcert->isRoot) {
			break;
		}
		issuerCert = CERT_FindCertIssuer(curcert, now, certUsageSSLServer);
		if (!issuerCert) {
			purple_debug_error("nss", "partial certificate chain\n");
			break;
		}
		CERT_DestroyCertificate(curcert);
		curcert = issuerCert;
	}
	CERT_DestroyCertificate(curcert);

	return peer_certs;
}

/*
 * Ideally this information would be exposed to the UI somehow, but for now we
 * just print it to the debug log
 */
static void
print_security_info(PRFileDesc *fd)
{
	SECStatus result;
	SSLChannelInfo channel;
	SSLCipherSuiteInfo suite;

	result = SSL_GetChannelInfo(fd, &channel, sizeof channel);
	if (result == SECSuccess && channel.length == sizeof channel
			&& channel.cipherSuite) {
		result = SSL_GetCipherSuiteInfo(channel.cipherSuite,
				&suite, sizeof suite);

		if (result == SECSuccess) {
			purple_debug_info("nss", "SSL version %d.%d using "
					"%d-bit %s with %d-bit %s MAC\n"
					"Server Auth: %d-bit %s, "
					"Key Exchange: %d-bit %s, "
					"Compression: %s\n"
					"Cipher Suite Name: %s\n",
					channel.protocolVersion >> 8,
					channel.protocolVersion & 0xff,
					suite.effectiveKeyBits,
					suite.symCipherName,
					suite.macBits,
					suite.macAlgorithmName,
					channel.authKeyBits,
					suite.authAlgorithmName,
					channel.keaKeyBits, suite.keaTypeName,
					channel.compressionMethodName,
					suite.cipherSuiteName);
		}
	}
}


static void
ssl_nss_handshake_cb(gpointer data, int fd, PurpleInputCondition cond)
{
	PurpleSslConnection *gsc = (PurpleSslConnection *)data;
	PurpleSslNssData *nss_data = gsc->private_data;

	/* I don't think this the best way to do this...
	 * It seems to work because it'll eventually use the cached value
	 */
	if(SSL_ForceHandshake(nss_data->in) != SECSuccess) {
		gchar *error_txt;
		set_errno(PR_GetError());
		if (errno == EAGAIN || errno == EWOULDBLOCK)
			return;

		error_txt = get_error_text();
		purple_debug_error("nss", "Handshake failed %s (%d)\n", error_txt ? error_txt : "", PR_GetError());
		g_free(error_txt);

		if (gsc->error_cb != NULL)
			gsc->error_cb(gsc, PURPLE_SSL_HANDSHAKE_FAILED, gsc->connect_cb_data);

		purple_ssl_close(gsc);

		return;
	}

	print_security_info(nss_data->in);

	purple_input_remove(nss_data->handshake_handler);
	nss_data->handshake_handler = 0;

	/* If a Verifier was given, hand control over to it */
	if (gsc->verifier) {
		GList *peers;
		/* First, get the peer cert chain */
		peers = ssl_nss_get_peer_certificates(nss_data->in, gsc);

		/* Now kick off the verification process */
		purple_certificate_verify(gsc->verifier,
				gsc->host,
				peers,
				ssl_nss_verified_cb,
				gsc);

		purple_certificate_destroy_list(peers);
	} else {
		/* Otherwise, just call the "connection complete"
		 * callback. The verification was already done with
		 * SSL_AuthCertificate, the default verifier
		 * (SSL_AuthCertificateHook was not called in ssl_nss_connect).
		 */
		gsc->connect_cb(gsc->connect_cb_data, gsc, cond);
	}
}

static gboolean
start_handshake_cb(gpointer data)
{
	PurpleSslConnection *gsc = data;
	PurpleSslNssData *nss_data = PURPLE_SSL_NSS_DATA(gsc);

	nss_data->handshake_timer = 0;

	ssl_nss_handshake_cb(gsc, gsc->fd, PURPLE_INPUT_READ);
	return FALSE;
}

static void
ssl_nss_connect(PurpleSslConnection *gsc)
{
	PurpleSslNssData *nss_data = g_new0(PurpleSslNssData, 1);
	PRSocketOptionData socket_opt;

	gsc->private_data = nss_data;

	nss_data->fd = PR_ImportTCPSocket(gsc->fd);

	if (nss_data->fd == NULL)
	{
		purple_debug_error("nss", "nss_data->fd == NULL!\n");

		if (gsc->error_cb != NULL)
			gsc->error_cb(gsc, PURPLE_SSL_CONNECT_FAILED, gsc->connect_cb_data);

		purple_ssl_close((PurpleSslConnection *)gsc);

		return;
	}

	socket_opt.option = PR_SockOpt_Nonblocking;
	socket_opt.value.non_blocking = PR_TRUE;

	if (PR_SetSocketOption(nss_data->fd, &socket_opt) != PR_SUCCESS) {
		gchar *error_txt = get_error_text();
		purple_debug_warning("nss", "unable to set socket into non-blocking mode: %s (%d)\n", error_txt ? error_txt : "", PR_GetError());
		g_free(error_txt);
	}

	nss_data->in = SSL_ImportFD(NULL, nss_data->fd);

	if (nss_data->in == NULL)
	{
		purple_debug_error("nss", "nss_data->in == NUL!\n");

		if (gsc->error_cb != NULL)
			gsc->error_cb(gsc, PURPLE_SSL_CONNECT_FAILED, gsc->connect_cb_data);

		purple_ssl_close((PurpleSslConnection *)gsc);

		return;
	}

	SSL_OptionSet(nss_data->in, SSL_SECURITY,            PR_TRUE);
	SSL_OptionSet(nss_data->in, SSL_HANDSHAKE_AS_CLIENT, PR_TRUE);

	/* If we have our internal verifier set up, use it. Otherwise,
	 * use default. */
	if (gsc->verifier != NULL)
		SSL_AuthCertificateHook(nss_data->in, ssl_auth_cert, NULL);

	if(gsc->host)
		SSL_SetURL(nss_data->in, gsc->host);

#if 0
	/* This seems like it'd the be the correct way to implement the
	nonblocking stuff, but it doesn't seem to work */
	SSL_HandshakeCallback(nss_data->in,
		(SSLHandshakeCallback) ssl_nss_handshake_cb, gsc);
#endif
	SSL_ResetHandshake(nss_data->in, PR_FALSE);

	nss_data->handshake_handler = purple_input_add(gsc->fd,
		PURPLE_INPUT_READ, ssl_nss_handshake_cb, gsc);

	nss_data->handshake_timer = purple_timeout_add(0, start_handshake_cb, gsc);
}

static void
ssl_nss_close(PurpleSslConnection *gsc)
{
	PurpleSslNssData *nss_data = PURPLE_SSL_NSS_DATA(gsc);

	if(!nss_data)
		return;

	if (nss_data->in) {
		PR_Close(nss_data->in);
		gsc->fd = -1;
	} else if (nss_data->fd) {
		PR_Close(nss_data->fd);
		gsc->fd = -1;
	}

	if (nss_data->handshake_handler)
		purple_input_remove(nss_data->handshake_handler);

	if (nss_data->handshake_timer)
		purple_timeout_remove(nss_data->handshake_timer);

	g_free(nss_data);
	gsc->private_data = NULL;
}

static size_t
ssl_nss_read(PurpleSslConnection *gsc, void *data, size_t len)
{
	PRInt32 ret;
	PurpleSslNssData *nss_data = PURPLE_SSL_NSS_DATA(gsc);

	ret = PR_Read(nss_data->in, data, len);

	if (ret == -1)
		set_errno(PR_GetError());

	return ret;
}

static size_t
ssl_nss_write(PurpleSslConnection *gsc, const void *data, size_t len)
{
	PRInt32 ret;
	PurpleSslNssData *nss_data = PURPLE_SSL_NSS_DATA(gsc);

	if(!nss_data)
		return 0;

	ret = PR_Write(nss_data->in, data, len);

	if (ret == -1)
		set_errno(PR_GetError());

	return ret;
}

static GList *
ssl_nss_peer_certs(PurpleSslConnection *gsc)
{
#if 0
	PurpleSslNssData *nss_data = PURPLE_SSL_NSS_DATA(gsc);
	CERTCertificate *cert;
/*
	GList *chain = NULL;
	void *pinArg;
	SECStatus status;
*/

	/* TODO: this is a blind guess */
	cert = SSL_PeerCertificate(nss_data->fd);

	if (cert)
		CERT_DestroyCertificate(cert);
#endif



	return NULL;
}

/************************************************************************/
/* X.509 functionality                                                  */
/************************************************************************/
static PurpleCertificateScheme x509_nss;

/** Helpr macro to retrieve the NSS certdata from a PurpleCertificate */
#define X509_NSS_DATA(pcrt) ( (CERTCertificate * ) (pcrt->data) )

/** Imports a PEM-formatted X.509 certificate from the specified file.
 * @param filename Filename to import from. Format is PEM
 *
 * @return A newly allocated Certificate structure of the x509_nss scheme
 */
static PurpleCertificate *
x509_import_from_file(const gchar *filename)
{
	gchar *rawcert;
	gsize len = 0;
	CERTCertificate *crt_dat;
	PurpleCertificate *crt;

	g_return_val_if_fail(filename != NULL, NULL);

	purple_debug_info("nss/x509",
			  "Loading certificate from %s\n",
			  filename);

	/* Load the raw data up */
	if (!g_file_get_contents(filename,
				 &rawcert, &len,
				 NULL)) {
		purple_debug_error("nss/x509", "Unable to read certificate file.\n");
		return NULL;
	}

	if (len == 0) {
		purple_debug_error("nss/x509",
				"Certificate file has no contents!\n");
		if (rawcert)
			g_free(rawcert);
		return NULL;
	}

	/* Decode the certificate */
	crt_dat = CERT_DecodeCertFromPackage(rawcert, len);
	g_free(rawcert);

	g_return_val_if_fail(crt_dat != NULL, NULL);

	crt = g_new0(PurpleCertificate, 1);
	crt->scheme = &x509_nss;
	crt->data = crt_dat;

	return crt;
}

/** Imports a number of PEM-formatted X.509 certificates from the specified file.
 * @param filename Filename to import from. Format is PEM
 *
 * @return A GSList of newly allocated Certificate structures of the x509_nss scheme
 */
static GSList *
x509_importcerts_from_file(const gchar *filename)
{
	gchar *rawcert, *begin, *end;
	gsize len = 0;
	GSList *crts = NULL;
	CERTCertificate *crt_dat;
	PurpleCertificate *crt;

	g_return_val_if_fail(filename != NULL, NULL);

	purple_debug_info("nss/x509",
			  "Loading certificate from %s\n",
			  filename);

	/* Load the raw data up */
	if (!g_file_get_contents(filename,
				 &rawcert, &len,
				 NULL)) {
		purple_debug_error("nss/x509", "Unable to read certificate file.\n");
		return NULL;
	}

	if (len == 0) {
		purple_debug_error("nss/x509",
				"Certificate file has no contents!\n");
		if (rawcert)
			g_free(rawcert);
		return NULL;
	}

	begin = rawcert;
	while((end = strstr(begin, "-----END CERTIFICATE-----")) != NULL) {
		end += sizeof("-----END CERTIFICATE-----")-1;
		/* Decode the certificate */
		crt_dat = CERT_DecodeCertFromPackage(begin, (end-begin));

		g_return_val_if_fail(crt_dat != NULL, NULL);

		crt = g_new0(PurpleCertificate, 1);
		crt->scheme = &x509_nss;
		crt->data = crt_dat;
		crts = g_slist_prepend(crts, crt);
		begin = end;
	}
	g_free(rawcert);

	return crts;
}
/**
 * Exports a PEM-formatted X.509 certificate to the specified file.
 * @param filename Filename to export to. Format will be PEM
 * @param crt      Certificate to export
 *
 * @return TRUE if success, otherwise FALSE
 */
/* This function should not be so complicated, but NSS doesn't seem to have a
   "convert yon certificate to PEM format" function. */
static gboolean
x509_export_certificate(const gchar *filename, PurpleCertificate *crt)
{
	CERTCertificate *crt_dat;
	SECItem *dercrt;
	gchar *b64crt;
	gchar *pemcrt;
	gboolean ret = FALSE;

	g_return_val_if_fail(filename, FALSE);
	g_return_val_if_fail(crt, FALSE);
	g_return_val_if_fail(crt->scheme == &x509_nss, FALSE);

	crt_dat = X509_NSS_DATA(crt);
	g_return_val_if_fail(crt_dat, FALSE);

	purple_debug_info("nss/x509",
			  "Exporting certificate to %s\n", filename);

	/* First, use NSS voodoo to create a DER-formatted certificate */
	dercrt = SEC_ASN1EncodeItem(NULL, NULL, crt_dat,
				    SEC_ASN1_GET(SEC_SignedCertificateTemplate));
	g_return_val_if_fail(dercrt != NULL, FALSE);

	/* Now encode it to b64 */
	b64crt = NSSBase64_EncodeItem(NULL, NULL, 0, dercrt);
	SECITEM_FreeItem(dercrt, PR_TRUE);
	g_return_val_if_fail(b64crt, FALSE);

	/* Wrap it in nice PEM header things */
	pemcrt = g_strdup_printf("-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----\n", b64crt);
	PORT_Free(b64crt); /* Notice that b64crt was allocated by an NSS
			      function; hence, we'll let NSPR free it. */

	/* Finally, dump the silly thing to a file. */
	ret =  purple_util_write_data_to_file_absolute(filename, pemcrt, -1);

	g_free(pemcrt);

	return ret;
}

static PurpleCertificate *
x509_copy_certificate(PurpleCertificate *crt)
{
	CERTCertificate *crt_dat;
	PurpleCertificate *newcrt;

	g_return_val_if_fail(crt, NULL);
	g_return_val_if_fail(crt->scheme == &x509_nss, NULL);

	crt_dat = X509_NSS_DATA(crt);
	g_return_val_if_fail(crt_dat, NULL);

	/* Create the certificate copy */
	newcrt = g_new0(PurpleCertificate, 1);
	newcrt->scheme = &x509_nss;
	/* NSS does refcounting automatically */
	newcrt->data = CERT_DupCertificate(crt_dat);

	return newcrt;
}

/** Frees a Certificate
 *
 *  Destroys a Certificate's internal data structures and frees the pointer
 *  given.
 *  @param crt  Certificate instance to be destroyed. It WILL NOT be destroyed
 *              if it is not of the correct CertificateScheme. Can be NULL
 *
 */
static void
x509_destroy_certificate(PurpleCertificate * crt)
{
	CERTCertificate *crt_dat;

	g_return_if_fail(crt);
	g_return_if_fail(crt->scheme == &x509_nss);

	crt_dat = X509_NSS_DATA(crt);
	g_return_if_fail(crt_dat);

	/* Finally we have the certificate. So let's kill it */
	/* NSS does refcounting automatically */
	CERT_DestroyCertificate(crt_dat);

	/* Delete the PurpleCertificate as well */
	g_free(crt);
}

/** Determines whether one certificate has been issued and signed by another
 *
 * @param crt       Certificate to check the signature of
 * @param issuer    Issuer's certificate
 *
 * @return TRUE if crt was signed and issued by issuer, otherwise FALSE
 * @TODO  Modify this function to return a reason for invalidity?
 */
static gboolean
x509_signed_by(PurpleCertificate * crt,
	       PurpleCertificate * issuer)
{
	CERTCertificate *subjectCert;
	CERTCertificate *issuerCert;
	SECStatus st;

	issuerCert = X509_NSS_DATA(issuer);
	g_return_val_if_fail(issuerCert, FALSE);

	subjectCert = X509_NSS_DATA(crt);
	g_return_val_if_fail(subjectCert, FALSE);

	if (subjectCert->issuerName == NULL || issuerCert->subjectName == NULL
			|| PORT_Strcmp(subjectCert->issuerName, issuerCert->subjectName) != 0)
		return FALSE;
	st = CERT_VerifySignedData(&subjectCert->signatureWrap, issuerCert, PR_Now(), NULL);
	return st == SECSuccess;
}

static GByteArray *
x509_shasum(PurpleCertificate *crt, SECOidTag algo)
{
	CERTCertificate *crt_dat;
	size_t hashlen = (algo == SEC_OID_SHA1) ? 20 : 32;
	GByteArray *hash;
	SECItem *derCert; /* DER representation of the cert */
	SECStatus st;

	g_return_val_if_fail(crt, NULL);
	g_return_val_if_fail(crt->scheme == &x509_nss, NULL);

	crt_dat = X509_NSS_DATA(crt);
	g_return_val_if_fail(crt_dat, NULL);

	/* Get the certificate DER representation */
	derCert = &(crt_dat->derCert);

	/* Make a hash! */
	hash = g_byte_array_sized_new(hashlen);
	/* glib leaves the size as 0 by default */
	hash->len = hashlen;

	st = PK11_HashBuf(algo, hash->data,
			  derCert->data, derCert->len);

	/* Check for errors */
	if (st != SECSuccess) {
		g_byte_array_free(hash, TRUE);
		purple_debug_error("nss/x509",
				   "Error: hashing failed!\n");
		return NULL;
	}

	return hash;
}

static GByteArray *
x509_sha1sum(PurpleCertificate *crt)
{
	return x509_shasum(crt, SEC_OID_SHA1);
}

static GByteArray *
x509_sha256sum(PurpleCertificate *crt)
{
	return x509_shasum(crt, SEC_OID_SHA256);
}

static gchar *
x509_dn (PurpleCertificate *crt)
{
	CERTCertificate *crt_dat;

	g_return_val_if_fail(crt, NULL);
	g_return_val_if_fail(crt->scheme == &x509_nss, NULL);

	crt_dat = X509_NSS_DATA(crt);
	g_return_val_if_fail(crt_dat, NULL);

	return g_strdup(crt_dat->subjectName);
}

static gchar *
x509_issuer_dn (PurpleCertificate *crt)
{
	CERTCertificate *crt_dat;

	g_return_val_if_fail(crt, NULL);
	g_return_val_if_fail(crt->scheme == &x509_nss, NULL);

	crt_dat = X509_NSS_DATA(crt);
	g_return_val_if_fail(crt_dat, NULL);

	return g_strdup(crt_dat->issuerName);
}

static gchar *
x509_common_name (PurpleCertificate *crt)
{
	CERTCertificate *crt_dat;
	char *nss_cn;
	gchar *ret_cn;

	g_return_val_if_fail(crt, NULL);
	g_return_val_if_fail(crt->scheme == &x509_nss, NULL);

	crt_dat = X509_NSS_DATA(crt);
	g_return_val_if_fail(crt_dat, NULL);

	/* Q:
	   Why get a newly allocated string out of NSS, strdup it, and then
	   return the new copy?

	   A:
	   The NSS LXR docs state that I should use the NSPR free functions on
	   the strings that the NSS cert functions return. Since the libpurple
	   API expects a g_free()-able string, we make our own copy and return
	   that.

	   NSPR is something of a prima donna. */

	nss_cn = CERT_GetCommonName( &(crt_dat->subject) );
	ret_cn = g_strdup(nss_cn);
	PORT_Free(nss_cn);

	return ret_cn;
}

static gboolean
x509_check_name (PurpleCertificate *crt, const gchar *name)
{
	CERTCertificate *crt_dat;
	SECStatus st;

	g_return_val_if_fail(crt, FALSE);
	g_return_val_if_fail(crt->scheme == &x509_nss, FALSE);

	crt_dat = X509_NSS_DATA(crt);
	g_return_val_if_fail(crt_dat, FALSE);

	st = CERT_VerifyCertName(crt_dat, name);

	if (st == SECSuccess) {
		return TRUE;
	}
	else if (st == SECFailure) {
		return FALSE;
	}

	/* If we get here...bad things! */
	purple_debug_error("nss/x509",
			   "x509_check_name fell through where it shouldn't "
			   "have.\n");
	return FALSE;
}

static gboolean
x509_times (PurpleCertificate *crt, time_t *activation, time_t *expiration)
{
	CERTCertificate *crt_dat;
	PRTime nss_activ, nss_expir;

	g_return_val_if_fail(crt, FALSE);
	g_return_val_if_fail(crt->scheme == &x509_nss, FALSE);

	crt_dat = X509_NSS_DATA(crt);
	g_return_val_if_fail(crt_dat, FALSE);

	/* Extract the times into ugly PRTime thingies */
	/* TODO: Maybe this shouldn't throw an error? */
	g_return_val_if_fail(
		SECSuccess == CERT_GetCertTimes(crt_dat,
						&nss_activ, &nss_expir),
		FALSE);

	/* NSS's native PRTime type *almost* corresponds to time_t; however,
	   it measures *microseconds* since the epoch, not seconds. Hence
	   the funny conversion. */
	nss_activ = nss_activ / 1000000;
	nss_expir = nss_expir / 1000000;

	if (activation) {
		*activation = nss_activ;
#if SIZEOF_TIME_T == 4
		/** Hack to deal with dates past the 32-bit barrier.
		    Handling is different for signed vs unsigned 32-bit types.
		 */
		if (*activation != nss_activ) {
			if (nss_activ < 0) {
				purple_debug_warning("nss",
					"Setting Activation Date to epoch to handle pre-epoch value\n");
				*activation = 0;
			} else {
				purple_debug_error("nss",
					"Activation date past 32-bit barrier, forcing invalidity\n");
				return FALSE;
			}
		}
#endif
	}
	if (expiration) {
		*expiration = nss_expir;
#if SIZEOF_TIME_T == 4
		if (*expiration != nss_expir) {
			if (*expiration < nss_expir) {
				if (*expiration < 0) {
					purple_debug_warning("nss",
						"Setting Expiration Date to 32-bit signed max\n");
					*expiration = PR_INT32_MAX;
				} else {
					purple_debug_warning("nss",
						"Setting Expiration Date to 32-bit unsigned max\n");
					*expiration = PR_UINT32_MAX;
				}
			} else {
				purple_debug_error("nss",
					"Expiration date prior to unix epoch, forcing invalidity\n");
				return FALSE;
			}
		}
#endif
	}

	return TRUE;
}

static gboolean
x509_register_trusted_tls_cert(PurpleCertificate *crt, gboolean ca)
{
	CERTCertDBHandle *certdb = CERT_GetDefaultCertDB();
	CERTCertificate *crt_dat;
	CERTCertTrust trust;

	g_return_val_if_fail(crt, FALSE);
	g_return_val_if_fail(crt->scheme == &x509_nss, FALSE);

	crt_dat = X509_NSS_DATA(crt);
	g_return_val_if_fail(crt_dat, FALSE);

	purple_debug_info("nss", "Trusting %s\n", crt_dat->subjectName);

	if (ca && !CERT_IsCACert(crt_dat, NULL)) {
		purple_debug_error("nss",
			"Refusing to set non-CA cert as trusted CA\n");
		return FALSE;
	}

	if (crt_dat->isperm) {
		purple_debug_info("nss",
			"Skipping setting trust for cert in permanent DB\n");
		return TRUE;
	}

	if (ca) {
		trust.sslFlags = CERTDB_TRUSTED_CA | CERTDB_TRUSTED_CLIENT_CA;
	} else {
		trust.sslFlags = CERTDB_TRUSTED;
	}
	trust.emailFlags = 0;
	trust.objectSigningFlags = 0;

	CERT_ChangeCertTrust(certdb, crt_dat, &trust);

	return TRUE;
}

static void x509_verify_cert(PurpleCertificateVerificationRequest *vrq, PurpleCertificateInvalidityFlags *flags)
{
	CERTCertDBHandle *certdb = CERT_GetDefaultCertDB();
	CERTCertificate *crt_dat;
	PRTime now = PR_Now();
	SECStatus rv;
	PurpleCertificate *first_cert = vrq->cert_chain->data;
	CERTVerifyLog log;
	gboolean self_signed = FALSE;

	crt_dat = X509_NSS_DATA(first_cert);

	log.arena = PORT_NewArena(512);
	log.head = log.tail = NULL;
	log.count = 0;
	rv = CERT_VerifyCert(certdb, crt_dat, PR_TRUE, certUsageSSLServer, now, NULL, &log);

	if (rv != SECSuccess || log.count > 0) {
		CERTVerifyLogNode *node   = NULL;
		unsigned int depth = (unsigned int)-1;

		if (crt_dat->isRoot) {
			self_signed = TRUE;
			*flags |= PURPLE_CERTIFICATE_SELF_SIGNED;
		}

		/* Handling of untrusted, etc. modeled after
		 * source/security/manager/ssl/src/TransportSecurityInfo.cpp in Firefox
		 */
		for (node = log.head; node; node = node->next) {
			if (depth != node->depth) {
				depth = node->depth;
				purple_debug_error("nss", "CERT %d. %s %s:\n", depth,
					node->cert->subjectName,
					depth ? "[Certificate Authority]": "");
			}
			purple_debug_error("nss", "  ERROR %ld: %s\n", node->error,
				PR_ErrorToName(node->error));
			switch (node->error) {
				case SEC_ERROR_EXPIRED_CERTIFICATE:
					*flags |= PURPLE_CERTIFICATE_EXPIRED;
					break;
				case SEC_ERROR_REVOKED_CERTIFICATE:
					*flags |= PURPLE_CERTIFICATE_REVOKED;
					break;
				case SEC_ERROR_UNKNOWN_ISSUER:
				case SEC_ERROR_UNTRUSTED_ISSUER:
					if (!self_signed) {
						*flags |= PURPLE_CERTIFICATE_CA_UNKNOWN;
					}
					break;
				case SEC_ERROR_CA_CERT_INVALID:
				case SEC_ERROR_EXPIRED_ISSUER_CERTIFICATE:
				case SEC_ERROR_UNTRUSTED_CERT:
#ifdef SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED
				case SEC_ERROR_CERT_SIGNATURE_ALGORITHM_DISABLED:
#endif
					if (!self_signed) {
						*flags |= PURPLE_CERTIFICATE_INVALID_CHAIN;
					}
					break;
				case SEC_ERROR_BAD_SIGNATURE:
				default:
					*flags |= PURPLE_CERTIFICATE_INVALID_CHAIN;
			}
			if (node->cert)
				CERT_DestroyCertificate(node->cert);
		}
	}

	rv = CERT_VerifyCertName(crt_dat, vrq->subject_name);
	if (rv != SECSuccess) {
		purple_debug_error("nss", "subject name not verified\n");
		*flags |= PURPLE_CERTIFICATE_NAME_MISMATCH;
	}

	PORT_FreeArena(log.arena, PR_FALSE);
}

static PurpleCertificateScheme x509_nss = {
	"x509",                          /* Scheme name */
	N_("X.509 Certificates"),        /* User-visible scheme name */
	x509_import_from_file,           /* Certificate import function */
	x509_export_certificate,         /* Certificate export function */
	x509_copy_certificate,           /* Copy */
	x509_destroy_certificate,        /* Destroy cert */
	x509_signed_by,                  /* Signed-by */
	x509_sha1sum,                    /* SHA1 fingerprint */
	x509_dn,                         /* Unique ID */
	x509_issuer_dn,                  /* Issuer Unique ID */
	x509_common_name,                /* Subject name */
	x509_check_name,                 /* Check subject name */
	x509_times,                      /* Activation/Expiration time */
	x509_importcerts_from_file,      /* Multiple certificate import function */
	x509_register_trusted_tls_cert,  /* Register a certificate as trusted for TLS */
	x509_verify_cert,                /* Verify that the specified cert chain is trusted */
	sizeof(PurpleCertificateScheme), /* struct_size */
	x509_sha256sum,                  /* SHA256 fingerprint */
	NULL,
};

static PurpleSslOps ssl_ops =
{
	ssl_nss_init,
	ssl_nss_uninit,
	ssl_nss_connect,
	ssl_nss_close,
	ssl_nss_read,
	ssl_nss_write,
	ssl_nss_peer_certs,

	/* padding */
	NULL,
	NULL,
	NULL
};


static gboolean
plugin_load(PurplePlugin *plugin)
{
	if (!purple_ssl_get_ops()) {
		purple_ssl_set_ops(&ssl_ops);
	}

	/* Init NSS now, so others can use it even if sslconn never does */
	ssl_nss_init_nss();

	/* Register the X.509 functions we provide */
	purple_certificate_register_scheme(&x509_nss);

	return TRUE;
}

static gboolean
plugin_unload(PurplePlugin *plugin)
{
	if (purple_ssl_get_ops() == &ssl_ops) {
		purple_ssl_set_ops(NULL);
	}

	/* Unregister our X.509 functions */
	purple_certificate_unregister_scheme(&x509_nss);

	return TRUE;
}

static PurplePluginInfo info =
{
	PURPLE_PLUGIN_MAGIC,
	PURPLE_MAJOR_VERSION,
	PURPLE_MINOR_VERSION,
	PURPLE_PLUGIN_STANDARD,                             /**< type           */
	NULL,                                             /**< ui_requirement */
	PURPLE_PLUGIN_FLAG_INVISIBLE,                       /**< flags          */
	NULL,                                             /**< dependencies   */
	PURPLE_PRIORITY_DEFAULT,                            /**< priority       */

	SSL_NSS_PLUGIN_ID,                             /**< id             */
	N_("NSS"),                                        /**< name           */
	DISPLAY_VERSION,                                  /**< version        */
	                                                  /**  summary        */
	N_("Provides SSL support through Mozilla NSS."),
	                                                  /**  description    */
	N_("Provides SSL support through Mozilla NSS."),
	"Christian Hammond <chipx86@gnupdate.org>",
	PURPLE_WEBSITE,                                     /**< homepage       */

	plugin_load,                                      /**< load           */
	plugin_unload,                                    /**< unload         */
	NULL,                                             /**< destroy        */

	NULL,                                             /**< ui_info        */
	NULL,                                             /**< extra_info     */
	NULL,                                             /**< prefs_info     */
	NULL,                                             /**< actions        */

	/* padding */
	NULL,
	NULL,
	NULL,
	NULL
};

static void
init_plugin(PurplePlugin *plugin)
{
}

PURPLE_INIT_PLUGIN(ssl_nss, init_plugin, info)

mercurial