# HG changeset patch # User William Ehlhardt # Date 1183868822 0 # Node ID 2e064085b7df5c54bce990481d5fdd3b1468d405 # Parent 45865fb3f4f93c9a6214c8e27eea70c5405a92f5# Parent d4065b26dcac8bd2f0df79ba21d93e512b0d20cb propagate from branch 'im.pidgin.pidgin' (head 1ede3f4c61fab6e6979e40b2169494d27af5f528) to branch 'im.pidgin.soc.2007.certmgr' (head 27afef9aea7ea6baa4c0bcc8ddc0dcb3282d747e) diff -r 45865fb3f4f9 -r 2e064085b7df libpurple/Makefile.am --- a/libpurple/Makefile.am Sat Jul 07 04:03:41 2007 +0000 +++ b/libpurple/Makefile.am Sun Jul 08 04:27:02 2007 +0000 @@ -36,6 +36,7 @@ accountopt.c \ blist.c \ buddyicon.c \ + certificate.c \ cipher.c \ circbuffer.c \ cmds.c \ @@ -85,6 +86,7 @@ accountopt.h \ blist.h \ buddyicon.h \ + certificate.h \ cipher.h \ circbuffer.h \ cmds.h \ diff -r 45865fb3f4f9 -r 2e064085b7df libpurple/certificate.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libpurple/certificate.c Sun Jul 08 04:27:02 2007 +0000 @@ -0,0 +1,851 @@ +/** + * @file certificate.h Public-Key Certificate API + * @ingroup core + */ + +/* + * + * purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include + +#include "certificate.h" +#include "debug.h" +#include "internal.h" +#include "request.h" +#include "util.h" + +/** List holding pointers to all registered certificate schemes */ +static GList *cert_schemes = NULL; +/** List of registered Verifiers */ +static GList *cert_verifiers = NULL; +/** List of registered Pools */ +static GList *cert_pools = NULL; + +void +purple_certificate_verify (PurpleCertificateVerifier *verifier, + const gchar *subject_name, GList *cert_chain, + PurpleCertificateVerifiedCallback cb, + gpointer cb_data) +{ + PurpleCertificateVerificationRequest *vrq; + PurpleCertificateScheme *scheme; + + g_return_if_fail(subject_name != NULL); + /* If you don't have a cert to check, why are you requesting that it + be verified? */ + g_return_if_fail(cert_chain != NULL); + g_return_if_fail(cb != NULL); + + /* Look up the CertificateScheme */ + scheme = purple_certificate_find_scheme(verifier->scheme_name); + g_return_if_fail(scheme); + + /* Check that at least the first cert in the chain matches the + Verifier scheme */ + g_return_if_fail(scheme == + ((PurpleCertificate *) (cert_chain->data))->scheme); + + /* Construct and fill in the request fields */ + vrq = g_new0(PurpleCertificateVerificationRequest, 1); + vrq->verifier = verifier; + vrq->scheme = scheme; + vrq->subject_name = g_strdup(subject_name); + vrq->cert_chain = cert_chain; + vrq->cb = cb; + vrq->cb_data = cb_data; + + /* Initiate verification */ + (verifier->start_verification)(vrq); +} + +void +purple_certificate_verify_destroy (PurpleCertificateVerificationRequest *vrq) +{ + PurpleCertificateVerifier *vr; + + if (NULL == vrq) return; + + /* Fetch the Verifier responsible... */ + vr = vrq->verifier; + /* ...and order it to KILL */ + (vr->destroy_request)(vrq); + + /* Now the internals have been cleaned up, so clean up the libpurple- + created elements */ + g_free(vrq->subject_name); + purple_certificate_destroy_list(vrq->cert_chain); + + g_free(vrq); +} + + +void +purple_certificate_destroy (PurpleCertificate *crt) +{ + PurpleCertificateScheme *scheme; + + if (NULL == crt) return; + + scheme = crt->scheme; + + (scheme->destroy_certificate)(crt); +} + +void +purple_certificate_destroy_list (GList * crt_list) +{ + PurpleCertificate *crt; + GList *l; + + for (l=crt_list; l; l = l->next) { + crt = (PurpleCertificate *) l->data; + purple_certificate_destroy(crt); + } + + g_list_free(crt_list); +} + +PurpleCertificate * +purple_certificate_import(PurpleCertificateScheme *scheme, const gchar *filename) +{ + g_return_val_if_fail(scheme, NULL); + g_return_val_if_fail(scheme->import_certificate, NULL); + g_return_val_if_fail(filename, NULL); + + return (scheme->import_certificate)(filename); +} + +gboolean +purple_certificate_export(const gchar *filename, PurpleCertificate *crt) +{ + PurpleCertificateScheme *scheme; + + g_return_val_if_fail(filename, FALSE); + g_return_val_if_fail(crt, FALSE); + g_return_val_if_fail(crt->scheme, FALSE); + + scheme = crt->scheme; + g_return_val_if_fail(scheme->export_certificate, FALSE); + + return (scheme->export_certificate)(filename, crt); +} + +GByteArray * +purple_certificate_get_fingerprint_sha1(PurpleCertificate *crt) +{ + PurpleCertificateScheme *scheme; + GByteArray *fpr; + + g_return_val_if_fail(crt, NULL); + g_return_val_if_fail(crt->scheme, NULL); + + scheme = crt->scheme; + + g_return_val_if_fail(scheme->get_fingerprint_sha1, NULL); + + fpr = (scheme->get_fingerprint_sha1)(crt); + + return fpr; +} + +gchar * +purple_certificate_get_subject_name(PurpleCertificate *crt) +{ + PurpleCertificateScheme *scheme; + gchar *subject_name; + + g_return_val_if_fail(crt, NULL); + g_return_val_if_fail(crt->scheme, NULL); + + scheme = crt->scheme; + + g_return_val_if_fail(scheme->get_subject_name, NULL); + + subject_name = (scheme->get_subject_name)(crt); + + return subject_name; +} + +gchar * +purple_certificate_pool_mkpath(PurpleCertificatePool *pool, const gchar *id) +{ + gchar *path; + + g_return_val_if_fail(pool, NULL); + g_return_val_if_fail(pool->scheme_name, NULL); + g_return_val_if_fail(pool->name, NULL); + + path = g_build_filename(purple_user_dir(), + "certificates", /* TODO: constantize this? */ + pool->scheme_name, + pool->name, + id, + NULL); + return path; +} + +gboolean +purple_certificate_pool_contains(PurpleCertificatePool *pool, const gchar *id) +{ + g_return_val_if_fail(pool, FALSE); + g_return_val_if_fail(id, FALSE); + g_return_val_if_fail(pool->cert_in_pool, FALSE); + + return (pool->cert_in_pool)(id); +} + +PurpleCertificate * +purple_certificate_pool_retrieve(PurpleCertificatePool *pool, const gchar *id) +{ + g_return_val_if_fail(pool, NULL); + g_return_val_if_fail(id, NULL); + g_return_val_if_fail(pool->get_cert, NULL); + + return (pool->get_cert)(id); +} + +gboolean +purple_certificate_pool_store(PurpleCertificatePool *pool, const gchar *id, PurpleCertificate *crt) +{ + g_return_val_if_fail(pool, FALSE); + g_return_val_if_fail(id, FALSE); + g_return_val_if_fail(pool->put_cert, FALSE); + + /* TODO: Should this just be someone else's problem? */ + /* Whether crt->scheme matches find_scheme(pool->scheme_name) is not + relevant... I think... */ + g_return_val_if_fail( + g_ascii_strcasecmp(pool->scheme_name, crt->scheme->name) == 0, + FALSE); + + return (pool->put_cert)(id, crt); +} + +/****************************************************************************/ +/* Builtin Verifiers, Pools, etc. */ +/****************************************************************************/ + +static void +x509_singleuse_verify_cb (PurpleCertificateVerificationRequest *vrq, gint id) +{ + g_return_if_fail(vrq); + + purple_debug_info("certificate/x509_singleuse", + "VRQ on cert from %s gave %d\n", + vrq->subject_name, id); + + /* Signal what happened back to the caller */ + if (1 == id) { + /* Accepted! */ + (vrq->cb)(PURPLE_CERTIFICATE_VALID, vrq->cb_data); + } else { + /* Not accepted */ + (vrq->cb)(PURPLE_CERTIFICATE_INVALID, vrq->cb_data); + } + + /* Now clean up the request */ + purple_certificate_verify_destroy(vrq); +} + +static void +x509_singleuse_start_verify (PurpleCertificateVerificationRequest *vrq) +{ + gchar *sha_asc; + GByteArray *sha_bin; + gchar *cn; + const gchar *cn_match; + gchar *primary, *secondary; + PurpleCertificate *crt = (PurpleCertificate *) vrq->cert_chain->data; + + /* Pull out the SHA1 checksum */ + sha_bin = purple_certificate_get_fingerprint_sha1(crt); + /* Now decode it for display */ + sha_asc = purple_base16_encode_chunked(sha_bin->data, + sha_bin->len); + + /* Get the cert Common Name */ + cn = purple_certificate_get_subject_name(crt); + + /* Determine whether the name matches */ + /* TODO: Worry about strcmp safety? */ + if (!strcmp(cn, vrq->subject_name)) { + cn_match = _(""); + } else { + cn_match = _("(DOES NOT MATCH)"); + } + + /* Make messages */ + primary = g_strdup_printf(_("%s has presented the following certificate for just-this-once use:"), vrq->subject_name); + secondary = g_strdup_printf(_("Common name: %s %s\nFingerprint (SHA1): %s"), cn, cn_match, sha_asc); + + /* Make a semi-pretty display */ + purple_request_accept_cancel( + vrq->cb_data, /* TODO: Find what the handle ought to be */ + _("Single-use Certificate Verification"), + primary, + secondary, + 1, /* Accept by default */ + NULL, /* No account */ + NULL, /* No other user */ + NULL, /* No associated conversation */ + vrq, + x509_singleuse_verify_cb, + x509_singleuse_verify_cb ); + + /* Cleanup */ + g_free(primary); + g_free(secondary); + g_free(sha_asc); + g_byte_array_free(sha_bin, TRUE); +} + +static void +x509_singleuse_destroy_request (PurpleCertificateVerificationRequest *vrq) +{ + /* I don't do anything! */ +} + +PurpleCertificateVerifier x509_singleuse = { + "x509", /* Scheme name */ + "singleuse", /* Verifier name */ + x509_singleuse_start_verify, /* start_verification function */ + x509_singleuse_destroy_request /* Request cleanup operation */ +}; + + + + +static PurpleCertificatePool x509_tls_peers; + +static gboolean +x509_tls_peers_init(void) +{ + gchar *poolpath; + int ret; + + /* Set up key cache here if it isn't already done */ + poolpath = purple_certificate_pool_mkpath(&x509_tls_peers, NULL); + ret = purple_build_dir(poolpath, 0700); /* Make it this user only */ + + g_free(poolpath); + + g_return_val_if_fail(ret == 0, FALSE); + return TRUE; +} + +static gboolean +x509_tls_peers_cert_in_pool(const gchar *id) +{ + gchar *keypath; + gboolean ret = FALSE; + + g_return_val_if_fail(id, FALSE); + + keypath = purple_certificate_pool_mkpath(&x509_tls_peers, id); + + ret = g_file_test(keypath, G_FILE_TEST_IS_REGULAR); + + g_free(keypath); + return ret; +} + +static PurpleCertificate * +x509_tls_peers_get_cert(const gchar *id) +{ + PurpleCertificateScheme *x509; + PurpleCertificate *crt; + gchar *keypath; + + g_return_val_if_fail(id, NULL); + + /* Is it in the pool? */ + if ( !x509_tls_peers_cert_in_pool(id) ) { + return NULL; + } + + /* Look up the X.509 scheme */ + x509 = purple_certificate_find_scheme("x509"); + g_return_val_if_fail(x509, NULL); + + /* Okay, now find and load that key */ + keypath = purple_certificate_pool_mkpath(&x509_tls_peers, id); + crt = purple_certificate_import(x509, keypath); + + g_free(keypath); + + return crt; +} + +static gboolean +x509_tls_peers_put_cert(const gchar *id, PurpleCertificate *crt) +{ + gboolean ret = FALSE; + gchar *keypath; + + g_return_val_if_fail(crt, FALSE); + g_return_val_if_fail(crt->scheme, FALSE); + /* Make sure that this is some kind of X.509 certificate */ + /* TODO: Perhaps just check crt->scheme->name instead? */ + g_return_val_if_fail(crt->scheme == purple_certificate_find_scheme(x509_tls_peers.scheme_name), FALSE); + + /* Work out the filename and export */ + keypath = purple_certificate_pool_mkpath(&x509_tls_peers, id); + ret = purple_certificate_export(keypath, crt); + + g_free(keypath); + return ret; +} + +static PurpleCertificatePool x509_tls_peers = { + "x509", /* Scheme name */ + "tls_peers", /* Pool name */ + N_("SSL Peers Cache"), /* User-friendly name */ + NULL, /* Internal data */ + x509_tls_peers_init, /* init */ + NULL, /* uninit not required */ + x509_tls_peers_cert_in_pool, /* Certificate exists? */ + x509_tls_peers_get_cert, /* Cert retriever */ + x509_tls_peers_put_cert /* Cert writer */ +}; + + + +static PurpleCertificateVerifier x509_tls_cached; + +static void +x509_tls_cached_unknown_peer_cb (PurpleCertificateVerificationRequest *vrq, gint id) +{ + PurpleCertificatePool *tls_peers; + + g_return_if_fail(vrq); + + tls_peers = purple_certificate_find_pool("x509","tls_peers"); + + if (1 == id) { + gchar *cache_id = vrq->subject_name; + purple_debug_info("certificate/x509/tls_cached", + "User ACCEPTED cert\nCaching first in chain for future use as %s...\n", + cache_id); + + purple_certificate_pool_store(tls_peers, cache_id, + vrq->cert_chain->data); + + (vrq->cb)(PURPLE_CERTIFICATE_VALID, vrq->cb_data); + } else { + purple_debug_info("certificate/x509/tls_cached", + "User REJECTED cert\n"); + (vrq->cb)(PURPLE_CERTIFICATE_INVALID, vrq->cb_data); + } + + /* Finish off the request */ + purple_certificate_verify_destroy(vrq); +} + +static void +x509_tls_cached_unknown_peer(PurpleCertificateVerificationRequest *vrq) +{ + gchar *sha_asc; + GByteArray *sha_bin; + gchar *cn; + const gchar *cn_match; + gchar *primary, *secondary; + PurpleCertificate *crt = (PurpleCertificate *) vrq->cert_chain->data; + + /* Pull out the SHA1 checksum */ + sha_bin = purple_certificate_get_fingerprint_sha1(crt); + /* Now decode it for display */ + sha_asc = purple_base16_encode_chunked(sha_bin->data, + sha_bin->len); + + /* Get the cert Common Name */ + cn = purple_certificate_get_subject_name(crt); + + /* Determine whether the name matches */ + /* TODO: Worry about strcmp safety? */ + if (!strcmp(cn, vrq->subject_name)) { + cn_match = _(""); + } else { + cn_match = _("(DOES NOT MATCH)"); + } + + /* Make messages */ + primary = g_strdup_printf(_("%s has presented the following certificate:"), vrq->subject_name); + secondary = g_strdup_printf(_("Common name: %s %s\nFingerprint (SHA1): %s"), cn, cn_match, sha_asc); + + /* Make a semi-pretty display */ + purple_request_accept_cancel( + vrq->cb_data, /* TODO: Find what the handle ought to be */ + _("SSL Certificate Verification"), + primary, + secondary, + 1, /* Accept by default */ + NULL, /* No account */ + NULL, /* No other user */ + NULL, /* No associated conversation */ + vrq, + x509_tls_cached_unknown_peer_cb, + x509_tls_cached_unknown_peer_cb ); + + /* Cleanup */ + g_free(primary); + g_free(secondary); + g_free(sha_asc); + g_byte_array_free(sha_bin, TRUE); +} + +static void +x509_tls_cached_peer_cert_changed(PurpleCertificateVerificationRequest *vrq) +{ + /* TODO: Prompt the user, etc. */ + + (vrq->cb)(PURPLE_CERTIFICATE_INVALID, vrq->cb_data); + /* Okay, we're done here */ + purple_certificate_verify_destroy(vrq); + return; +} + +static void +x509_tls_cached_start_verify(PurpleCertificateVerificationRequest *vrq) +{ + PurpleCertificate *peer_crt = (PurpleCertificate *) vrq->cert_chain->data; + const gchar *tls_peers_name = "tls_peers"; /* Name of local cache */ + PurpleCertificatePool *tls_peers; + + g_return_if_fail(vrq); + + purple_debug_info("certificate/x509/tls_cached", + "Starting verify for %s\n", + vrq->subject_name); + + tls_peers = purple_certificate_find_pool(x509_tls_cached.scheme_name,tls_peers_name); + + /* TODO: This should probably just prompt the user instead of throwing + an angry fit */ + if (!tls_peers) { + purple_debug_error("certificate/x509/tls_cached", + "Couldn't find local peers cache %s\nReturning INVALID to callback\n", + tls_peers_name); + (vrq->cb)(PURPLE_CERTIFICATE_INVALID, vrq->cb_data); + purple_certificate_verify_destroy(vrq); + return; + } + + /* Check if the peer has a certificate cached already */ + purple_debug_info("certificate/x509/tls_cached", + "Checking for cached cert...\n"); + if (purple_certificate_pool_contains(tls_peers, vrq->subject_name)) { + PurpleCertificate *cached_crt; + GByteArray *peer_fpr, *cached_fpr; + + purple_debug_info("certificate/x509/tls_cached", + "...Found cached cert\n"); + + /* Load up the cached certificate */ + cached_crt = purple_certificate_pool_retrieve( + tls_peers, vrq->subject_name); + + /* Now get SHA1 sums for both and compare them */ + /* TODO: This is not an elegant way to compare certs */ + peer_fpr = purple_certificate_get_fingerprint_sha1(peer_crt); + cached_fpr = purple_certificate_get_fingerprint_sha1(cached_crt); + if (!memcmp(peer_fpr->data, cached_fpr->data, peer_fpr->len)) { + purple_debug_info("certificate/x509/tls_cached", + "Peer cert matched cached\n"); + (vrq->cb)(PURPLE_CERTIFICATE_VALID, vrq->cb_data); + + /* vrq is now finished */ + purple_certificate_verify_destroy(vrq); + } else { + purple_debug_info("certificate/x509/tls_cached", + "Peer cert did NOT match cached\n"); + /* vrq now becomes the problem of cert_changed */ + x509_tls_cached_peer_cert_changed(vrq); + } + + purple_certificate_destroy(cached_crt); + g_byte_array_free(peer_fpr, TRUE); + g_byte_array_free(cached_fpr, TRUE); + } else { + /* TODO: Prompt the user, etc. */ + purple_debug_info("certificate/x509/tls_cached", + "...Not in cache\n"); + /* vrq now becomes the problem of unknown_peer */ + x509_tls_cached_unknown_peer(vrq); + } +} + +static void +x509_tls_cached_destroy_request(PurpleCertificateVerificationRequest *vrq) +{ + g_return_if_fail(vrq); +} + +static PurpleCertificateVerifier x509_tls_cached = { + "x509", /* Scheme name */ + "tls_cached", /* Verifier name */ + x509_tls_cached_start_verify, /* Verification begin */ + x509_tls_cached_destroy_request /* Request cleanup */ +}; + +/****************************************************************************/ +/* Subsystem */ +/****************************************************************************/ +void +purple_certificate_init(void) +{ + /* Register builtins */ + purple_certificate_register_verifier(&x509_singleuse); + purple_certificate_register_pool(&x509_tls_peers); + purple_certificate_register_verifier(&x509_tls_cached); +} + +void +purple_certificate_uninit(void) +{ + /* Unregister the builtins */ + purple_certificate_unregister_verifier(&x509_singleuse); + purple_certificate_unregister_pool(&x509_tls_peers); + purple_certificate_unregister_verifier(&x509_tls_cached); + + /* TODO: Unregistering everything would be good... */ +} + +PurpleCertificateScheme * +purple_certificate_find_scheme(const gchar *name) +{ + PurpleCertificateScheme *scheme = NULL; + GList *l; + + g_return_val_if_fail(name, NULL); + + /* Traverse the list of registered schemes and locate the + one whose name matches */ + for(l = cert_schemes; l; l = l->next) { + scheme = (PurpleCertificateScheme *)(l->data); + + /* Name matches? that's our man */ + if(!g_ascii_strcasecmp(scheme->name, name)) + return scheme; + } + + purple_debug_warning("certificate", + "CertificateScheme %s requested but not found.\n", + name); + + /* TODO: Signalling and such? */ + + return NULL; +} + +gboolean +purple_certificate_register_scheme(PurpleCertificateScheme *scheme) +{ + g_return_val_if_fail(scheme != NULL, FALSE); + + /* Make sure no scheme is registered with the same name */ + if (purple_certificate_find_scheme(scheme->name) != NULL) { + return FALSE; + } + + /* Okay, we're golden. Register it. */ + cert_schemes = g_list_prepend(cert_schemes, scheme); + + /* TODO: Signalling and such? */ + return TRUE; +} + +gboolean +purple_certificate_unregister_scheme(PurpleCertificateScheme *scheme) +{ + if (NULL == scheme) { + purple_debug_warning("certificate", + "Attempting to unregister NULL scheme\n"); + return FALSE; + } + + /* TODO: signalling? */ + + /* TODO: unregister all CertificateVerifiers for this scheme?*/ + /* TODO: unregister all CertificatePools for this scheme? */ + /* Neither of the above should be necessary, though */ + cert_schemes = g_list_remove(cert_schemes, scheme); + + return TRUE; +} + +PurpleCertificateVerifier * +purple_certificate_find_verifier(const gchar *scheme_name, const gchar *ver_name) +{ + PurpleCertificateVerifier *vr = NULL; + GList *l; + + g_return_val_if_fail(scheme_name, NULL); + g_return_val_if_fail(ver_name, NULL); + + /* Traverse the list of registered verifiers and locate the + one whose name matches */ + for(l = cert_verifiers; l; l = l->next) { + vr = (PurpleCertificateVerifier *)(l->data); + + /* Scheme and name match? */ + if(!g_ascii_strcasecmp(vr->scheme_name, scheme_name) && + !g_ascii_strcasecmp(vr->name, ver_name)) + return vr; + } + + purple_debug_warning("certificate", + "CertificateVerifier %s, %s requested but not found.\n", + scheme_name, ver_name); + + /* TODO: Signalling and such? */ + + return NULL; +} + + +gboolean +purple_certificate_register_verifier(PurpleCertificateVerifier *vr) +{ + g_return_val_if_fail(vr != NULL, FALSE); + + /* Make sure no verifier is registered with the same scheme/name */ + if (purple_certificate_find_verifier(vr->scheme_name, vr->name) != NULL) { + return FALSE; + } + + /* Okay, we're golden. Register it. */ + cert_verifiers = g_list_prepend(cert_verifiers, vr); + + /* TODO: Signalling and such? */ + return TRUE; +} + +gboolean +purple_certificate_unregister_verifier(PurpleCertificateVerifier *vr) +{ + if (NULL == vr) { + purple_debug_warning("certificate", + "Attempting to unregister NULL verifier\n"); + return FALSE; + } + + /* TODO: signalling? */ + + cert_verifiers = g_list_remove(cert_verifiers, vr); + + return TRUE; +} + +PurpleCertificatePool * +purple_certificate_find_pool(const gchar *scheme_name, const gchar *pool_name) +{ + PurpleCertificatePool *pool = NULL; + GList *l; + + g_return_val_if_fail(scheme_name, NULL); + g_return_val_if_fail(pool_name, NULL); + + /* Traverse the list of registered pools and locate the + one whose name matches */ + for(l = cert_pools; l; l = l->next) { + pool = (PurpleCertificatePool *)(l->data); + + /* Scheme and name match? */ + if(!g_ascii_strcasecmp(pool->scheme_name, scheme_name) && + !g_ascii_strcasecmp(pool->name, pool_name)) + return pool; + } + + purple_debug_warning("certificate", + "CertificatePool %s, %s requested but not found.\n", + scheme_name, pool_name); + + /* TODO: Signalling and such? */ + + return NULL; + +} + + +gboolean +purple_certificate_register_pool(PurpleCertificatePool *pool) +{ + gboolean success = FALSE; + g_return_val_if_fail(pool, FALSE); + g_return_val_if_fail(pool->scheme_name, FALSE); + g_return_val_if_fail(pool->name, FALSE); + g_return_val_if_fail(pool->fullname, FALSE); + + /* Make sure no pools are registered under this name */ + if (purple_certificate_find_pool(pool->scheme_name, pool->name)) { + return FALSE; + } + + /* Initialize the pool if needed */ + if (pool->init) { + success = pool->init(); + } else { + success = TRUE; + } + + if (success) { + /* Register the Pool */ + cert_pools = g_list_prepend(cert_pools, pool); + + return TRUE; + } else { + return FALSE; + } + + /* Control does not reach this point */ +} + +gboolean +purple_certificate_unregister_pool(PurpleCertificatePool *pool) +{ + /* TODO: Better error checking? */ + if (NULL == pool) { + purple_debug_warning("certificate", + "Attempting to unregister NULL pool\n"); + return FALSE; + } + + /* Check that the pool is registered */ + if (!g_list_find(cert_pools, pool)) { + purple_debug_warning("certificate", + "Pool to unregister isn't registered!\n"); + + return FALSE; + } + + /* Uninit the pool if needed */ + if (pool->uninit) { + pool->uninit(); + } + + cert_pools = g_list_remove(cert_pools, pool); + + /* TODO: Signalling? */ + + return TRUE; +} diff -r 45865fb3f4f9 -r 2e064085b7df libpurple/certificate.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libpurple/certificate.h Sun Jul 08 04:27:02 2007 +0000 @@ -0,0 +1,579 @@ +/** + * @file certificate.h Public-Key Certificate API + * @ingroup core + */ + +/* + * + * purple + * + * Purple is the legal property of its developers, whose names are too numerous + * to list here. Please refer to the COPYRIGHT file distributed with this + * source distribution. + * + * 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#ifndef _PURPLE_CERTIFICATE_H +#define _PURPLE_CERTIFICATE_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + + +typedef enum +{ + PURPLE_CERTIFICATE_INVALID = 0, + PURPLE_CERTIFICATE_VALID = 1 +} PurpleCertificateVerificationStatus; + +typedef struct _PurpleCertificate PurpleCertificate; +typedef struct _PurpleCertificatePool PurpleCertificatePool; +typedef struct _PurpleCertificateScheme PurpleCertificateScheme; +typedef struct _PurpleCertificateVerifier PurpleCertificateVerifier; +typedef struct _PurpleCertificateVerificationRequest PurpleCertificateVerificationRequest; + +/** + * Callback function for the results of a verification check + * @param st Status code + * @param userdata User-defined data + */ +typedef void (*PurpleCertificateVerifiedCallback) + (PurpleCertificateVerificationStatus st, + gpointer userdata); + +/** A certificate instance + * + * An opaque data structure representing a single certificate under some + * CertificateScheme + */ +struct _PurpleCertificate +{ + /** Scheme this certificate is under */ + PurpleCertificateScheme * scheme; + /** Opaque pointer to internal data */ + gpointer data; +}; + +/** + * Database for retrieval or storage of Certificates + * + * More or less a hash table; all lookups and writes are controlled by a string + * key. + */ +struct _PurpleCertificatePool +{ + /** Scheme this Pool operates for */ + gchar *scheme_name; + /** Internal name to refer to the pool by */ + gchar *name; + + /** User-friendly name for this type + * ex: N_("SSL Servers") + * When this is displayed anywhere, it should be i18ned + * ex: _(pool->fullname) + */ + gchar *fullname; + + /** Internal pool data */ + gpointer data; + + /** + * Set up the Pool's internal state + * + * Upon calling purple_certificate_register_pool() , this function will + * be called. May be NULL. + * @return TRUE if the initialization succeeded, otherwise FALSE + */ + gboolean (* init)(void); + + /** + * Uninit the Pool's internal state + * + * Will be called by purple_certificate_unregister_pool() . May be NULL + */ + void (* uninit)(void); + + /** Check for presence of a certificate in the pool using unique ID */ + gboolean (* cert_in_pool)(const gchar *id); + /** Retrieve a PurpleCertificate from the pool */ + PurpleCertificate * (* get_cert)(const gchar *id); + /** Add a certificate to the pool. Must overwrite any other + * certificates sharing the same ID in the pool. + * @return TRUE if the operation succeeded, otherwise FALSE + */ + gboolean (* put_cert)(const gchar *id, PurpleCertificate *crt); +}; + +/** A certificate type + * + * A CertificateScheme must implement all of the fields in the structure, + * and register it using purple_certificate_register_scheme() + * + * There may be only ONE CertificateScheme provided for each certificate + * type, as specified by the "name" field. + */ +struct _PurpleCertificateScheme +{ + /** Name of the certificate type + * ex: "x509", "pgp", etc. + * This must be globally unique - you may not register more than one + * CertificateScheme of the same name at a time. + */ + gchar * name; + + /** User-friendly name for this type + * ex: N_("X.509 Certificates") + * When this is displayed anywhere, it should be i18ned + * ex: _(scheme->fullname) + */ + gchar * fullname; + + /** Imports a certificate from a file + * + * @param filename File to import the certificate from + * @return Pointer to the newly allocated Certificate struct + * or NULL on failure. + */ + PurpleCertificate * (* import_certificate)(const gchar * filename); + + /** + * Exports a certificate to a file + * + * @param filename File to export the certificate to + * @param crt Certificate to export + * @return TRUE if the export succeeded, otherwise FALSE + * @see purple_certificate_export() + */ + gboolean (* export_certificate)(const gchar *filename, PurpleCertificate *crt); + + /** Destroys and frees a Certificate structure + * + * Destroys a Certificate's internal data structures and calls + * free(crt) + * + * @param crt Certificate instance to be destroyed. It WILL NOT be + * destroyed if it is not of the correct + * CertificateScheme. Can be NULL + */ + void (* destroy_certificate)(PurpleCertificate * crt); + + /** + * Retrieves the certificate public key fingerprint using SHA1 + * + * @param crt Certificate instance + * @return Binary representation of SHA1 hash - must be freed using + * g_byte_array_free() + */ + GByteArray * (* get_fingerprint_sha1)(PurpleCertificate *crt); + + /** + * Reads "who the certificate is assigned to" + * + * For SSL X.509 certificates, this is something like + * "gmail.com" or "jabber.org" + * + * @param crt Certificate instance + * @return Newly allocated string specifying "whose certificate this + * is" + */ + gchar * (* get_certificate_subject)(PurpleCertificate *crt); + + /** + * Retrieves a unique certificate identifier + * + * @param crt Certificate instance + * @return Newly allocated string that can be used to uniquely + * identify the certificate. + */ + gchar * (* get_unique_id)(PurpleCertificate *crt); + + /** + * Retrieves a unique identifier for the certificate's issuer + * + * @param crt Certificate instance + * @return Newly allocated string that can be used to uniquely + * identify the issuer's certificate. + */ + gchar * (* get_issuer_unique_id)(PurpleCertificate *crt); + + /** + * Gets the certificate subject's name + * + * For X.509, this is the "Common Name" field, as we're only using it + * for hostname verification at the moment + * + * @see purple_certificate_get_subject_name() + * + * @param crt Certificate instance + * @return Newly allocated string with the certificate subject. + */ + gchar * (* get_subject_name)(PurpleCertificate *crt); + /* TODO: Fill out this structure */ +}; + +/** A set of operations used to provide logic for verifying a Certificate's + * authenticity. + * + * A Verifier provider must fill out these fields, then register it using + * purple_certificate_register_verifier() + * + * The (scheme_name, name) value must be unique for each Verifier - you may not + * register more than one Verifier of the same name for each Scheme + */ +struct _PurpleCertificateVerifier +{ + /** Name of the scheme this Verifier operates on + * + * The scheme will be looked up by name when a Request is generated + * using this Verifier + */ + gchar *scheme_name; + + /** Name of the Verifier - case insensitive */ + gchar *name; + + /** + * Start the verification process + * + * To be called from purple_certificate_verify once it has + * constructed the request. This will use the information in the + * given VerificationRequest to check the certificate and callback + * the requester with the verification results. + * + * @param vrq Request to process + */ + void (* start_verification)(PurpleCertificateVerificationRequest *vrq); + + /** + * Destroy a completed Request under this Verifier + * The function pointed to here is only responsible for cleaning up + * whatever PurpleCertificateVerificationRequest::data points to. + * It should not call free(vrq) + * + * @param vrq Request to destroy + */ + void (* destroy_request)(PurpleCertificateVerificationRequest *vrq); +}; + +/** Structure for a single certificate request + * + * Useful for keeping track of the state of a verification that involves + * several steps + */ +struct _PurpleCertificateVerificationRequest +{ + /** Reference to the verification logic used */ + PurpleCertificateVerifier *verifier; + /** Reference to the scheme used. + * + * This is looked up from the Verifier when the Request is generated + */ + PurpleCertificateScheme *scheme; + + /** + * Name to check that the certificate is issued to + * + * For X.509 certificates, this is the Common Name + */ + gchar *subject_name; + + /** List of certificates in the chain to be verified (such as that returned by purple_ssl_get_peer_certificates ) + * + * This is most relevant for X.509 certificates used in SSL sessions. + * The list order should be: certificate, issuer, issuer's issuer, etc. + */ + GList *cert_chain; + + /** Internal data used by the Verifier code */ + gpointer data; + + /** Function to call with the verification result */ + PurpleCertificateVerifiedCallback cb; + /** Data to pass to the post-verification callback */ + gpointer cb_data; +}; + +/*****************************************************************************/ +/** @name Certificate Verification Functions */ +/*****************************************************************************/ +/*@{*/ + +/** + * Constructs a verification request and passed control to the specified Verifier + * + * It is possible that the callback will be called immediately upon calling + * this function. Plan accordingly. + * + * @param verifier Verification logic to use. + * @see purple_certificate_find_verifier() + * + * @param subject_name Name that should match the first certificate in the + * chain for the certificate to be valid. Will be strdup'd + * into the Request struct + * + * @param cert_chain Certificate chain to check. If there is more than one + * certificate in the chain (X.509), the peer's + * certificate comes first, then the issuer/signer's + * certificate, etc. + * + * @param cb Callback function to be called with whether the + * certificate was approved or not. + * @param cb_data User-defined data for the above. + */ +void +purple_certificate_verify (PurpleCertificateVerifier *verifier, + const gchar *subject_name, GList *cert_chain, + PurpleCertificateVerifiedCallback cb, + gpointer cb_data); + +/** + * Disposes of a VerificationRequest once it is complete + * + * @param vrq Request to destroy. Will be free()'d. + * The certificate chain involved will also be destroyed. + */ +void +purple_certificate_verify_destroy (PurpleCertificateVerificationRequest *vrq); + +/*@}*/ + +/*****************************************************************************/ +/** @name Certificate Functions */ +/*****************************************************************************/ +/*@{*/ + +/** + * Destroys and free()'s a Certificate + * + * @param crt Instance to destroy. May be NULL. + */ +void +purple_certificate_destroy (PurpleCertificate *crt); + +/** + * Destroy an entire list of Certificate instances and the containing list + * + * @param crt_list List of certificates to destroy. May be NULL. + */ +void +purple_certificate_destroy_list (GList * crt_list); + +/** + * Imports a PurpleCertificate from a file + * + * @param scheme Scheme to import under + * @param filename File path to import from + * @return Pointer to a new PurpleCertificate, or NULL on failure + */ +PurpleCertificate * +purple_certificate_import(PurpleCertificateScheme *scheme, const gchar *filename); + +/** + * Exports a PurpleCertificate to a file + * + * @param filename File to export the certificate to + * @param crt Certificate to export + * @return TRUE if the export succeeded, otherwise FALSE + */ +gboolean +purple_certificate_export(const gchar *filename, PurpleCertificate *crt); + + +/** + * Retrieves the certificate public key fingerprint using SHA1. + * + * @param crt Certificate instance + * @return Binary representation of the hash. You are responsible for free()ing + * this. + * @see purple_base16_encode_chunked() + */ +GByteArray * +purple_certificate_get_fingerprint_sha1(PurpleCertificate *crt); + + +/** + * Gets the certificate subject's name + * + * For X.509, this is the "Common Name" field, as we're only using it + * for hostname verification at the moment + * + * @param crt Certificate instance + * @return Newly allocated string with the certificate subject. + */ +gchar * +purple_certificate_get_subject_name(PurpleCertificate *crt); + +/*@}*/ + +/*****************************************************************************/ +/** @name Certificate Pool Functions */ +/*****************************************************************************/ +/*@{*/ +/** + * Helper function for generating file paths in ~/.purple/certificates for + * CertificatePools that use them. + * + * @todo Passing in filesystem-unfriendly characters will cause breakage! + * @param pool CertificatePool to build a path for + * @param id Key to look up a Certificate by. May be NULL. + * @return A newly allocated path of the form + * ~/.purple/certificates/scheme_name/pool_name/unique_id + */ +gchar * +purple_certificate_pool_mkpath(PurpleCertificatePool *pool, const gchar *id); + +/** + * Check for presence of an ID in a pool. + * @param pool Pool to look in + * @param id ID to look for + * @return TRUE if the ID is in the pool, else FALSE + */ +gboolean +purple_certificate_pool_contains(PurpleCertificatePool *pool, const gchar *id); + +/** + * Retrieve a certificate from a pool. + * @param pool Pool to fish in + * @param id ID to look up + * @return Retrieved certificate, or NULL if it wasn't there + */ +PurpleCertificate * +purple_certificate_pool_retrieve(PurpleCertificatePool *pool, const gchar *id); + +/** + * Add a certificate to a pool + * + * Any pre-existing certificate of the same ID will be overwritten. + * + * @param pool Pool to add to + * @param id ID to store the certificate with + * @param crt Certificate to store + * @return TRUE if the operation succeeded, otherwise FALSE + */ +gboolean +purple_certificate_pool_store(PurpleCertificatePool *pool, const gchar *id, PurpleCertificate *crt); + +/*@}*/ + +/*****************************************************************************/ +/** @name Certificate Subsystem API */ +/*****************************************************************************/ +/*@{*/ + +/** + * Initialize the certificate system + */ +void +purple_certificate_init(void); + +/** + * Un-initialize the certificate system + */ +void +purple_certificate_uninit(void); + +/** Look up a registered CertificateScheme by name + * @param name The scheme name. Case insensitive. + * @return Pointer to the located Scheme, or NULL if it isn't found. + */ +PurpleCertificateScheme * +purple_certificate_find_scheme(const gchar *name); + +/** Register a CertificateScheme with libpurple + * + * No two schemes can be registered with the same name; this function enforces + * that. + * + * @param scheme Pointer to the scheme to register. + * @return TRUE if the scheme was successfully added, otherwise FALSE + */ +gboolean +purple_certificate_register_scheme(PurpleCertificateScheme *scheme); + +/** Unregister a CertificateScheme from libpurple + * + * @param scheme Scheme to unregister. + * If the scheme is not registered, this is a no-op. + * + * @return TRUE if the unregister completed successfully + */ +gboolean +purple_certificate_unregister_scheme(PurpleCertificateScheme *scheme); + +/** Look up a registered PurpleCertificateVerifier by scheme and name + * @param scheme_name Scheme name. Case insensitive. + * @param ver_name The verifier name. Case insensitive. + * @return Pointer to the located Verifier, or NULL if it isn't found. + */ +PurpleCertificateVerifier * +purple_certificate_find_verifier(const gchar *scheme_name, const gchar *ver_name); + + +/** + * Register a CertificateVerifier with libpurple + * + * @param vr Verifier to register. + * @return TRUE if register succeeded, otherwise FALSE + */ +gboolean +purple_certificate_register_verifier(PurpleCertificateVerifier *vr); + +/** + * Unregister a CertificateVerifier with libpurple + * + * @param vr Verifier to unregister. + * @return TRUE if unregister succeeded, otherwise FALSE + */ +gboolean +purple_certificate_unregister_verifier(PurpleCertificateVerifier *vr); + +/** Look up a registered PurpleCertificatePool by scheme and name + * @param scheme_name Scheme name. Case insensitive. + * @param pool_name Pool name. Case insensitive. + * @return Pointer to the located Pool, or NULL if it isn't found. + */ +PurpleCertificatePool * +purple_certificate_find_pool(const gchar *scheme_name, const gchar *pool_name); + +/** + * Register a CertificatePool with libpurple and call its init function + * + * @param pool Pool to register. + * @return TRUE if the register succeeded, otherwise FALSE + */ +gboolean +purple_certificate_register_pool(PurpleCertificatePool *pool); + +/** + * Unregister a CertificatePool with libpurple and call its uninit function + * + * @param pool Pool to unregister. + * @return TRUE if the unregister succeeded, otherwise FALSE + */ +gboolean +purple_certificate_unregister_pool(PurpleCertificatePool *pool); + +/*@}*/ + + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + +#endif /* _PURPLE_CERTIFICATE_H */ diff -r 45865fb3f4f9 -r 2e064085b7df libpurple/core.c --- a/libpurple/core.c Sat Jul 07 04:03:41 2007 +0000 +++ b/libpurple/core.c Sun Jul 08 04:27:02 2007 +0000 @@ -24,6 +24,7 @@ */ #include "internal.h" #include "cipher.h" +#include "certificate.h" #include "connection.h" #include "conversation.h" #include "core.h" @@ -141,6 +142,7 @@ purple_accounts_init(); purple_savedstatuses_init(); purple_notify_init(); + purple_certificate_init(); purple_connections_init(); purple_conversations_init(); purple_blist_init(); @@ -191,6 +193,7 @@ purple_notify_uninit(); purple_conversations_uninit(); purple_connections_uninit(); + purple_certificate_uninit(); purple_buddy_icons_uninit(); purple_accounts_uninit(); purple_savedstatuses_uninit(); diff -r 45865fb3f4f9 -r 2e064085b7df libpurple/plugins/ssl/ssl-gnutls.c --- a/libpurple/plugins/ssl/ssl-gnutls.c Sat Jul 07 04:03:41 2007 +0000 +++ b/libpurple/plugins/ssl/ssl-gnutls.c Sun Jul 08 04:27:02 2007 +0000 @@ -21,15 +21,18 @@ */ #include "internal.h" #include "debug.h" +#include "certificate.h" #include "plugin.h" #include "sslconn.h" #include "version.h" +#include "util.h" #define SSL_GNUTLS_PLUGIN_ID "ssl-gnutls" #ifdef HAVE_GNUTLS #include +#include typedef struct { @@ -44,9 +47,25 @@ static void ssl_gnutls_init_gnutls(void) { + /* Configure GnuTLS to use glib memory management */ + /* I expect that this isn't really necessary, but it may prevent + some bugs */ + /* TODO: It may be necessary to wrap this allocators for GnuTLS. + If there are strange bugs, perhaps look here (yes, I am a + hypocrite) */ + gnutls_global_set_mem_functions( + (gnutls_alloc_function) g_malloc0, /* malloc */ + (gnutls_alloc_function) g_malloc0, /* secure malloc */ + NULL, /* mem_is_secure */ + (gnutls_realloc_function) g_realloc, /* realloc */ + (gnutls_free_function) g_free /* free */ + ); + gnutls_global_init(); gnutls_certificate_allocate_credentials(&xcred); + + /* TODO: I can likely remove this */ gnutls_certificate_set_x509_trust_file(xcred, "ca.pem", GNUTLS_X509_FMT_PEM); } @@ -65,6 +84,25 @@ gnutls_certificate_free_credentials(xcred); } +static void +ssl_gnutls_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); + } +} + + static void ssl_gnutls_handshake_cb(gpointer data, gint source, PurpleInputCondition cond) @@ -73,7 +111,7 @@ PurpleSslGnutlsData *gnutls_data = PURPLE_SSL_GNUTLS_DATA(gsc); ssize_t ret; - purple_debug_info("gnutls", "Handshaking\n"); + purple_debug_info("gnutls", "Handshaking with %s\n", gsc->host); ret = gnutls_handshake(gnutls_data->session); if(ret == GNUTLS_E_AGAIN || ret == GNUTLS_E_INTERRUPTED) @@ -94,7 +132,116 @@ } else { purple_debug_info("gnutls", "Handshake complete\n"); - gsc->connect_cb(gsc->connect_cb_data, gsc, cond); + /* TODO: Remove all this debugging babble */ + /* Now we are cooking with gas! */ + PurpleSslOps *ops = purple_ssl_get_ops(); + GList * peers = ops->get_peer_certificates(gsc); + + PurpleCertificateScheme *x509 = + purple_certificate_find_scheme("x509"); + + GList * l; + for (l=peers; l; l = l->next) { + PurpleCertificate *crt = l->data; + GByteArray *z = + x509->get_fingerprint_sha1(crt); + gchar * fpr = + purple_base16_encode_chunked(z->data, + z->len); + + purple_debug_info("gnutls/x509", + "Key print: %s\n", + fpr); + + /* Kill the cert! */ + x509->destroy_certificate(crt); + + g_free(fpr); + g_byte_array_free(z, TRUE); + } + g_list_free(peers); + + { + const gnutls_datum_t *cert_list; + unsigned int cert_list_size = 0; + gnutls_session_t session=gnutls_data->session; + + cert_list = + gnutls_certificate_get_peers(session, &cert_list_size); + + purple_debug_info("gnutls", + "Peer provided %d certs\n", + cert_list_size); + int i; + for (i=0; iverifier) { + GList *peers; + /* First, get the peer cert chain */ + peers = purple_ssl_get_peer_certificates(gsc); + + /* Now kick off the verification process */ + purple_certificate_verify(gsc->verifier, + gsc->host, + peers, + ssl_gnutls_verified_cb, + gsc); + + } else { + /* Otherwise, just call the "connection complete" + callback */ + gsc->connect_cb(gsc->connect_cb_data, gsc, cond); + } } } @@ -213,6 +360,372 @@ return s; } +/* Forward declarations are fun! + TODO: This is a stupid place for this */ +static PurpleCertificate * +x509_import_from_datum(const gnutls_datum_t dt, gnutls_x509_crt_fmt_t mode); + +static GList * +ssl_gnutls_get_peer_certificates(PurpleSslConnection * gsc) +{ + PurpleSslGnutlsData *gnutls_data = PURPLE_SSL_GNUTLS_DATA(gsc); + + /* List of Certificate instances to return */ + GList * peer_certs = NULL; + + /* List of raw certificates as given by GnuTLS */ + const gnutls_datum_t *cert_list; + unsigned int cert_list_size = 0; + + unsigned int i; + + /* This should never, ever happen. */ + g_return_val_if_fail( gnutls_certificate_type_get (gnutls_data->session) == GNUTLS_CRT_X509, NULL); + + /* Get the certificate list from GnuTLS */ + /* TODO: I am _pretty sure_ this doesn't block or do other exciting things */ + cert_list = gnutls_certificate_get_peers(gnutls_data->session, + &cert_list_size); + + /* Convert each certificate to a Certificate and append it to the list */ + for (i = 0; i < cert_list_size; i++) { + PurpleCertificate * newcrt = x509_import_from_datum(cert_list[i], + GNUTLS_X509_FMT_DER); + /* Append is somewhat inefficient on linked lists, but is easy + to read. If someone complains, I'll change it. + TODO: Is anyone complaining? (Maybe elb?) */ + peer_certs = g_list_append(peer_certs, newcrt); + } + + /* cert_list shouldn't need free()-ing */ + /* TODO: double-check this */ + + return peer_certs; +} + +/************************************************************************/ +/* X.509 functionality */ +/************************************************************************/ +const gchar * SCHEME_NAME = "x509"; + +static PurpleCertificateScheme x509_gnutls; + +/** Transforms a gnutls_datum_t containing an X.509 certificate into a Certificate instance under the x509_gnutls scheme + * + * @param dt Datum to transform + * @param mode GnuTLS certificate format specifier (GNUTLS_X509_FMT_PEM for + * reading from files, and GNUTLS_X509_FMT_DER for converting + * "over the wire" certs for SSL) + * + * @return A newly allocated Certificate structure of the x509_gnutls scheme + */ +static PurpleCertificate * +x509_import_from_datum(const gnutls_datum_t dt, gnutls_x509_crt_fmt_t mode) +{ + /* Internal certificate data structure */ + gnutls_x509_crt_t *certdat; + /* New certificate to return */ + PurpleCertificate * crt; + + /* Allocate and prepare the internal certificate data */ + certdat = g_new0(gnutls_x509_crt_t, 1); + gnutls_x509_crt_init(certdat); + + /* Perform the actual certificate parse */ + /* Yes, certdat SHOULD be dereferenced */ + gnutls_x509_crt_import(*certdat, &dt, mode); + + /* Allocate the certificate and load it with data */ + crt = g_new0(PurpleCertificate, 1); + crt->scheme = &x509_gnutls; + crt->data = certdat; + + return crt; +} + +/** 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_gnutls scheme + */ +static PurpleCertificate * +x509_import_from_file(const gchar * filename) +{ + PurpleCertificate *crt; /* Certificate being constructed */ + gchar *buf; /* Used to load the raw file data */ + gsize buf_sz; /* Size of the above */ + gnutls_datum_t dt; /* Struct to pass down to GnuTLS */ + + purple_debug_info("gnutls", + "Attempting to load X.509 certificate from %s\n", + filename); + + /* Next, we'll simply yank the entire contents of the file + into memory */ + /* TODO: Should I worry about very large files here? */ + /* TODO: Error checking */ + g_file_get_contents(filename, + &buf, + &buf_sz, + NULL /* No error checking for now */ + ); + + /* Load the datum struct */ + dt.data = (unsigned char *) buf; + dt.size = buf_sz; + + /* Perform the conversion */ + crt = x509_import_from_datum(dt, + GNUTLS_X509_FMT_PEM); // files should be in PEM format + + /* Cleanup */ + g_free(buf); + + return crt; +} + +/** + * 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 + */ +static gboolean +x509_export_certificate(const gchar *filename, PurpleCertificate *crt) +{ + gnutls_x509_crt_t crt_dat; /* GnuTLS cert struct */ + int ret; + gchar * out_buf; /* Data to output */ + size_t out_size; /* Output size */ + gboolean success = FALSE; + + /* Paranoia paranoia paranoia! */ + g_return_val_if_fail(filename, FALSE); + g_return_val_if_fail(crt, FALSE); + g_return_val_if_fail(crt->scheme == &x509_gnutls, FALSE); + g_return_val_if_fail(crt->data, FALSE); + + crt_dat = *( (gnutls_x509_crt_t *) crt->data); + + /* Obtain the output size required */ + out_size = 0; + ret = gnutls_x509_crt_export(crt_dat, GNUTLS_X509_FMT_PEM, + NULL, /* Provide no buffer yet */ + &out_size /* Put size here */ + ); + g_return_val_if_fail(ret == GNUTLS_E_SHORT_MEMORY_BUFFER, FALSE); + + /* Now allocate a buffer and *really* export it */ + out_buf = g_new0(gchar, out_size); + ret = gnutls_x509_crt_export(crt_dat, GNUTLS_X509_FMT_PEM, + out_buf, /* Export to our new buffer */ + &out_size /* Put size here */ + ); + if (ret != 0) { + purple_debug_error("gnutls/x509", + "Failed to export cert to buffer with code %d\n", + ret); + g_free(out_buf); + return FALSE; + } + + /* Write it out to an actual file */ + /* TODO: THIS IS A COMPATIBILITY VIOLATION + Look into util.c write_data_to_file. */ + success = g_file_set_contents(filename, + out_buf, + out_size, + NULL); + + + g_free(out_buf); + g_return_val_if_fail(success, FALSE); + return success; +} + +/** 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) +{ + /* TODO: Issue a warning here? */ + if (NULL == crt) return; + + /* Check that the scheme is x509_gnutls */ + if ( crt->scheme != &x509_gnutls ) { + purple_debug_error("gnutls", + "destroy_certificate attempted on certificate of wrong scheme (scheme was %s, expected %s)\n", + crt->scheme->name, + SCHEME_NAME); + return; + } + + /* TODO: Different error checking? */ + g_return_if_fail(crt->data != NULL); + g_return_if_fail(crt->scheme != NULL); + + /* Destroy the GnuTLS-specific data */ + gnutls_x509_crt_deinit( *( (gnutls_x509_crt_t *) crt->data ) ); + g_free(crt->data); + + /* TODO: Reference counting here? */ + + /* Kill the structure itself */ + 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_certificate_signed_by(PurpleCertificate * crt, + PurpleCertificate * issuer) +{ + gnutls_x509_crt_t crt_dat; + gnutls_x509_crt_t issuer_dat; + unsigned int verify; /* used to store details from GnuTLS verifier */ + int ret; + + /* TODO: Change this error checking? */ + g_return_val_if_fail(crt, FALSE); + g_return_val_if_fail(issuer, FALSE); + + /* Verify that both certs are the correct scheme */ + g_return_val_if_fail(crt->scheme == &x509_gnutls, FALSE); + g_return_val_if_fail(issuer->scheme == &x509_gnutls, FALSE); + + /* TODO: check for more nullness? */ + + crt_dat = *((gnutls_x509_crt_t *) crt->data); + issuer_dat = *((gnutls_x509_crt_t *) issuer->data); + + /* First, let's check that crt.issuer is actually issuer */ + ret = gnutls_x509_crt_check_issuer(crt_dat, issuer_dat); + if (ret <= 0) { + + if (ret < 0) { + purple_debug_error("gnutls/x509", + "GnuTLS error %d while checking certificate issuer match.", + ret); + } + + /* The issuer is not correct, or there were errors */ + return FALSE; + } + + /* Now, check the signature */ + /* The second argument is a ptr to an array of "trusted" issuer certs, + but we're only using one trusted one */ + ret = gnutls_x509_crt_verify(crt_dat, &issuer_dat, 1, 0, &verify); + + if (ret > 0) { + /* The certificate is good. */ + return TRUE; + } + else if (ret < 0) { + purple_debug_error("gnutls/x509", + "Attempted certificate verification caused a GnuTLS error code %d. I will just say the signature is bad, but you should look into this.\n", ret); + return FALSE; + } + else { + /* Signature didn't check out, but at least + there were no errors*/ + return FALSE; + } /* if (ret, etc.) */ + + /* Control does not reach this point */ +} + +static GByteArray * +x509_sha1sum(PurpleCertificate *crt) +{ + size_t hashlen = 20; /* SHA1 hashes are 20 bytes */ + size_t tmpsz = hashlen; /* Throw-away variable for GnuTLS to stomp on*/ + gnutls_x509_crt_t crt_dat; + GByteArray *hash; /**< Final hash container */ + guchar hashbuf[hashlen]; /**< Temporary buffer to contain hash */ + + g_return_val_if_fail(crt, NULL); + + crt_dat = *( (gnutls_x509_crt_t *) crt->data ); + + /* Extract the fingerprint */ + /* TODO: Errorcheck? */ + gnutls_x509_crt_get_fingerprint(crt_dat, GNUTLS_MAC_SHA, + hashbuf, &tmpsz); + + /* This shouldn't happen */ + g_return_val_if_fail(tmpsz == hashlen, NULL); + + /* Okay, now create and fill hash array */ + hash = g_byte_array_new(); + g_byte_array_append(hash, hashbuf, hashlen); + + return hash; +} + +static gchar * +x509_common_name (PurpleCertificate *crt) +{ + gnutls_x509_crt_t cert_dat; + gchar *cn = NULL; + size_t cn_size; + + g_return_val_if_fail(crt, NULL); + g_return_val_if_fail(crt->scheme == &x509_gnutls, NULL); + + cert_dat = *( (gnutls_x509_crt_t *) crt->data ); + + /* TODO: Not return values? */ + + /* Figure out the length of the Common Name */ + /* Claim that the buffer is size 0 so GnuTLS just tells us how much + space it needs */ + cn_size = 0; + gnutls_x509_crt_get_dn_by_oid(cert_dat, + GNUTLS_OID_X520_COMMON_NAME, + 0, /* First CN found, please */ + 0, /* Not in raw mode */ + cn, &cn_size); + + /* Now allocate and get the Common Name */ + cn = g_new0(gchar, cn_size); + gnutls_x509_crt_get_dn_by_oid(cert_dat, + GNUTLS_OID_X520_COMMON_NAME, + 0, /* First CN found, please */ + 0, /* Not in raw mode */ + cn, &cn_size); + + return cn; +} + +/* X.509 certificate operations provided by this plugin */ +/* TODO: Flesh this out! */ +static PurpleCertificateScheme x509_gnutls = { + "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_destroy_certificate, /* Destroy cert */ + x509_sha1sum, /* SHA1 fingerprint */ + NULL, /* Subject */ + NULL, /* Unique ID */ + NULL, /* Issuer Unique ID */ + x509_common_name /* Subject name */ +}; + static PurpleSslOps ssl_ops = { ssl_gnutls_init, @@ -221,11 +734,11 @@ ssl_gnutls_close, ssl_gnutls_read, ssl_gnutls_write, + ssl_gnutls_get_peer_certificates, /* padding */ NULL, NULL, - NULL, NULL }; @@ -235,6 +748,10 @@ plugin_load(PurplePlugin *plugin) { #ifdef HAVE_GNUTLS + /* Register that we're providing an X.509 CertScheme */ + /* @TODO : error checking */ + purple_certificate_register_scheme( &x509_gnutls ); + if(!purple_ssl_get_ops()) { purple_ssl_set_ops(&ssl_ops); } @@ -255,6 +772,8 @@ if(purple_ssl_get_ops() == &ssl_ops) { purple_ssl_set_ops(NULL); } + + purple_certificate_unregister_scheme( &x509_gnutls ); #endif return TRUE; diff -r 45865fb3f4f9 -r 2e064085b7df libpurple/prefs.h --- a/libpurple/prefs.h Sat Jul 07 04:03:41 2007 +0000 +++ b/libpurple/prefs.h Sun Jul 08 04:27:02 2007 +0000 @@ -55,7 +55,9 @@ #endif /**************************************************************************/ -/** @name Prefs API */ +/** @name Prefs API + Preferences are named according to a directory-like structure. + Example: "/plugins/core/potato/is_from_idaho" (probably a boolean) */ /**************************************************************************/ /*@{*/ diff -r 45865fb3f4f9 -r 2e064085b7df libpurple/protocols/irc/irc.c --- a/libpurple/protocols/irc/irc.c Sat Jul 07 04:03:41 2007 +0000 +++ b/libpurple/protocols/irc/irc.c Sun Jul 08 04:27:02 2007 +0000 @@ -433,14 +433,7 @@ irc->gsc = NULL; - switch(error) { - case PURPLE_SSL_CONNECT_FAILED: - purple_connection_error(gc, _("Connection Failed")); - break; - case PURPLE_SSL_HANDSHAKE_FAILED: - purple_connection_error(gc, _("SSL Handshake Failed")); - break; - } + purple_connection_error(gc, purple_ssl_strerror(error)); } static void irc_close(PurpleConnection *gc) diff -r 45865fb3f4f9 -r 2e064085b7df libpurple/protocols/jabber/jabber.c --- a/libpurple/protocols/jabber/jabber.c Sat Jul 07 04:03:41 2007 +0000 +++ b/libpurple/protocols/jabber/jabber.c Sun Jul 08 04:27:02 2007 +0000 @@ -494,29 +494,20 @@ js = gc->proto_data; js->gsc = NULL; - switch(error) { - case PURPLE_SSL_CONNECT_FAILED: - purple_connection_error(gc, _("Connection Failed")); - break; - case PURPLE_SSL_HANDSHAKE_FAILED: - purple_connection_error(gc, _("SSL Handshake Failed")); - break; - } + purple_connection_error(gc, purple_ssl_strerror(error)); } static void tls_init(JabberStream *js) { purple_input_remove(js->gc->inpa); js->gc->inpa = 0; - js->gsc = purple_ssl_connect_fd(js->gc->account, js->fd, - jabber_login_callback_ssl, jabber_ssl_connect_failure, js->gc); + js->gsc = purple_ssl_connect_with_host_fd(js->gc->account, js->fd, + jabber_login_callback_ssl, jabber_ssl_connect_failure, js->serverFQDN, js->gc); } static void jabber_login_connect(JabberStream *js, const char *fqdn, const char *host, int port) { -#ifdef HAVE_CYRUS_SASL js->serverFQDN = g_strdup(fqdn); -#endif if (purple_proxy_connect(js->gc, js->gc->account, host, port, jabber_login_callback, js->gc) == NULL) @@ -1025,9 +1016,9 @@ g_string_free(js->sasl_mechs, TRUE); if(js->sasl_cb) g_free(js->sasl_cb); +#endif if(js->serverFQDN) g_free(js->serverFQDN); -#endif g_free(js->server_name); g_free(js->gmail_last_time); g_free(js->gmail_last_tid); diff -r 45865fb3f4f9 -r 2e064085b7df libpurple/protocols/jabber/jabber.h --- a/libpurple/protocols/jabber/jabber.h Sat Jul 07 04:03:41 2007 +0000 +++ b/libpurple/protocols/jabber/jabber.h Sun Jul 08 04:27:02 2007 +0000 @@ -136,6 +136,8 @@ char *gmail_last_time; char *gmail_last_tid; + char *serverFQDN; + /* OK, this stays at the end of the struct, so plugins can depend * on the rest of the stuff being in the right place */ @@ -150,7 +152,6 @@ int sasl_state; int sasl_maxbuf; GString *sasl_mechs; - char *serverFQDN; gboolean vcard_fetched; diff -r 45865fb3f4f9 -r 2e064085b7df libpurple/sslconn.c --- a/libpurple/sslconn.c Sat Jul 07 04:03:41 2007 +0000 +++ b/libpurple/sslconn.c Sun Jul 08 04:27:02 2007 +0000 @@ -24,6 +24,7 @@ */ #include "internal.h" +#include "certificate.h" #include "debug.h" #include "sslconn.h" @@ -117,6 +118,9 @@ gsc->connect_cb = func; gsc->error_cb = error_func; + /* TODO: Move this elsewhere */ + gsc->verifier = purple_certificate_find_verifier("x509","tls_cached"); + gsc->connect_data = purple_proxy_connect(NULL, account, host, port, purple_ssl_connect_cb, gsc); if (gsc->connect_data == NULL) @@ -151,10 +155,37 @@ gsc->inpa = purple_input_add(gsc->fd, PURPLE_INPUT_READ, recv_cb, gsc); } +const gchar * +purple_ssl_strerror(PurpleSslErrorType error) +{ + switch(error) { + case PURPLE_SSL_CONNECT_FAILED: + return _("SSL Connection Failed"); + case PURPLE_SSL_HANDSHAKE_FAILED: + return _("SSL Handshake Failed"); + case PURPLE_SSL_CERTIFICATE_INVALID: + return _("SSL peer presented an invalid certificate"); + default: + purple_debug_warning("sslconn", "Unknown SSL error code %d\n", error); + return _("Unknown SSL error"); + } +} + PurpleSslConnection * purple_ssl_connect_fd(PurpleAccount *account, int fd, PurpleSslInputFunction func, - PurpleSslErrorFunction error_func, void *data) + PurpleSslErrorFunction error_func, + void *data) +{ + return purple_ssl_connect_with_host_fd(account, fd, func, error_func, NULL, data); +} + +PurpleSslConnection * +purple_ssl_connect_with_host_fd(PurpleAccount *account, int fd, + PurpleSslInputFunction func, + PurpleSslErrorFunction error_func, + const char *host, + void *data) { PurpleSslConnection *gsc; PurpleSslOps *ops; @@ -175,7 +206,13 @@ gsc->connect_cb = func; gsc->error_cb = error_func; gsc->fd = fd; + if(host) + gsc->host = g_strdup(host); + /* TODO: Move this elsewhere */ + gsc->verifier = purple_certificate_find_verifier("x509","tls_cached"); + + ops = purple_ssl_get_ops(); ops->connectfunc(gsc); @@ -231,6 +268,17 @@ return (ops->write)(gsc, data, len); } +GList * +purple_ssl_get_peer_certificates(PurpleSslConnection *gsc) +{ + PurpleSslOps *ops; + + g_return_val_if_fail(gsc != NULL, NULL); + + ops = purple_ssl_get_ops(); + return (ops->get_peer_certificates)(gsc); +} + void purple_ssl_set_ops(PurpleSslOps *ops) { diff -r 45865fb3f4f9 -r 2e064085b7df libpurple/sslconn.h --- a/libpurple/sslconn.h Sat Jul 07 04:03:41 2007 +0000 +++ b/libpurple/sslconn.h Sun Jul 08 04:27:02 2007 +0000 @@ -25,6 +25,7 @@ #ifndef _PURPLE_SSLCONN_H_ #define _PURPLE_SSLCONN_H_ +#include "certificate.h" #include "proxy.h" #define PURPLE_SSL_DEFAULT_PORT 443 @@ -32,7 +33,8 @@ typedef enum { PURPLE_SSL_HANDSHAKE_FAILED = 1, - PURPLE_SSL_CONNECT_FAILED = 2 + PURPLE_SSL_CONNECT_FAILED = 2, + PURPLE_SSL_CERTIFICATE_INVALID = 3 } PurpleSslErrorType; typedef struct _PurpleSslConnection PurpleSslConnection; @@ -69,6 +71,9 @@ /** Internal connection data managed by the SSL backend (GnuTLS/LibNSS/whatever) */ void *private_data; + + /** Verifier to use in authenticating the peer */ + PurpleCertificateVerifier *verifier; }; /** @@ -107,8 +112,17 @@ * @return The number of bytes written (may be less than len) or <0 on error */ size_t (*write)(PurpleSslConnection *gsc, const void *data, size_t len); - - void (*_purple_reserved1)(void); + /** Obtains the certificate chain provided by the peer + * + * @param gsc Connection context + * @return A newly allocated list containing the certificates + * the peer provided. + * @see PurpleCertificate + * @todo Decide whether the ordering of certificates in this + * list can be guaranteed. + */ + GList * (* get_peer_certificates)(PurpleSslConnection * gsc); + void (*_purple_reserved2)(void); void (*_purple_reserved3)(void); void (*_purple_reserved4)(void); @@ -131,6 +145,14 @@ gboolean purple_ssl_is_supported(void); /** + * Returns a human-readable string for an SSL error + * + * @param error Error code + * @return Human-readable error explanation + */ +const gchar * purple_ssl_strerror(PurpleSslErrorType error); + +/** * Makes a SSL connection to the specified host and port. The caller * should keep track of the returned value and use it to cancel the * connection, if needed. @@ -154,6 +176,7 @@ /** * Makes a SSL connection using an already open file descriptor. + * DEPRECATED. Use purple_ssl_connect_with_host_fd instead. * * @param account The account making the connection. * @param fd The file descriptor. @@ -166,7 +189,25 @@ PurpleSslConnection *purple_ssl_connect_fd(PurpleAccount *account, int fd, PurpleSslInputFunction func, PurpleSslErrorFunction error_func, - void *data); + void *data); + +/** + * Makes a SSL connection using an already open file descriptor. + * + * @param account The account making the connection. + * @param fd The file descriptor. + * @param func The SSL input handler function. + * @param error_func The SSL error handler function. + * @param host The hostname of the other peer (to verify the CN) + * @param data User-defined data. + * + * @return The SSL connection handle. + */ +PurpleSslConnection *purple_ssl_connect_with_host_fd(PurpleAccount *account, int fd, + PurpleSslInputFunction func, + PurpleSslErrorFunction error_func, + const char *host, + void *data); /** * Adds an input watcher for the specified SSL connection. @@ -208,6 +249,16 @@ */ size_t purple_ssl_write(PurpleSslConnection *gsc, const void *buffer, size_t len); +/** + * Obtains the peer's presented certificates + * + * @param gsc The SSL connection handle + * + * @return The peer certificate chain, in the order of certificate, issuer, + * issuer's issuer, etc. NULL if no certificates have been provided, + */ +GList * purple_ssl_get_peer_certificates(PurpleSslConnection *gsc); + /*@}*/ /**************************************************************************/