changeset 28707:c1d41b7484ff

jabber: Complete (though untested) SCRAM implementation. Client proof calculations function properly, but parsing is untested.
author Paul Aurich <paul@darkrain42.org>
date Mon, 09 Nov 2009 03:42:26 +0000
parents 2b4465db73f1
children 39fae7199fa9
files libpurple/protocols/jabber/auth.c libpurple/protocols/jabber/auth.h libpurple/protocols/jabber/auth_scram.c libpurple/protocols/jabber/auth_scram.h libpurple/protocols/jabber/jabber.c libpurple/protocols/jabber/jabber.h libpurple/tests/test_jabber_scram.c
diffstat 7 files changed, 552 insertions(+), 21 deletions(-) [+]
line wrap: on
line diff
--- a/libpurple/protocols/jabber/auth.c	Sun Nov 08 18:39:30 2009 +0000
+++ b/libpurple/protocols/jabber/auth.c	Mon Nov 09 03:42:26 2009 +0000
@@ -485,11 +485,18 @@
 
 void jabber_auth_init(void)
 {
+	JabberSaslMech **tmp;
+	gint count, i;
+
 	auth_mechs = g_slist_insert_sorted(auth_mechs, jabber_auth_get_plain_mech(), compare_mech);
 	auth_mechs = g_slist_insert_sorted(auth_mechs, jabber_auth_get_digest_md5_mech(), compare_mech);
 #ifdef HAVE_CYRUS_SASL
 	auth_mechs = g_slist_insert_sorted(auth_mechs, jabber_auth_get_cyrus_mech(), compare_mech);
 #endif
+
+	tmp = jabber_auth_get_scram_mechs(&count);
+	for (i = 0; i < count; ++i)
+		auth_mechs = g_slist_insert_sorted(auth_mechs, tmp[i], compare_mech);
 }
 
 void jabber_auth_uninit(void)
--- a/libpurple/protocols/jabber/auth.h	Sun Nov 08 18:39:30 2009 +0000
+++ b/libpurple/protocols/jabber/auth.h	Mon Nov 09 03:42:26 2009 +0000
@@ -48,6 +48,7 @@
 
 JabberSaslMech *jabber_auth_get_plain_mech(void);
 JabberSaslMech *jabber_auth_get_digest_md5_mech(void);
+JabberSaslMech **jabber_auth_get_scram_mechs(gint *count);
 #ifdef HAVE_CYRUS_SASL
 JabberSaslMech *jabber_auth_get_cyrus_mech(void);
 #endif
--- a/libpurple/protocols/jabber/auth_scram.c	Sun Nov 08 18:39:30 2009 +0000
+++ b/libpurple/protocols/jabber/auth_scram.c	Mon Nov 09 03:42:26 2009 +0000
@@ -29,16 +29,33 @@
 #include "debug.h"
 
 static const struct {
+	const char *mech_substr;
+	const char *hash;
+} mech_hashes[] = {
+	{ "-SHA-1-", "sha1" },
+};
+
+static const struct {
 	const char *hash;
 	guint size;
 } hash_sizes[] = {
 	{ "sha1", 20 },
-	{ "sha224", 28 },
-	{ "sha256", 32 },
-	{ "sha384", 48 },
-	{ "sha512", 64 }
 };
 
+static const gchar *mech_to_hash(const char *mech)
+{
+	int i;
+
+	g_return_val_if_fail(mech != NULL && *mech != '\0', NULL);
+
+	for (i = 0; i < G_N_ELEMENTS(mech_hashes); ++i) {
+		if (strstr(mech, mech_hashes[i].mech_substr))
+			return mech_hashes[i].hash;
+	}
+
+	return NULL;
+}
+
 static guint hash_to_output_len(const gchar *hash)
 {
 	int i;
@@ -55,11 +72,11 @@
 	return 0;
 }
 
-GString *jabber_auth_scram_hi(const gchar *hash, const GString *str,
-                              GString *salt, guint iterations)
+guchar *jabber_scram_hi(const gchar *hash, const GString *str,
+                        GString *salt, guint iterations)
 {
 	PurpleCipherContext *context;
-	GString *result;
+	guchar *result;
 	guint i;
 	guint hash_len;
 	guchar *prev, *tmp;
@@ -74,6 +91,7 @@
 
 	prev = g_new0(guint8, hash_len);
 	tmp = g_new0(guint8, hash_len);
+	result = g_new0(guint8, hash_len);
 
 	context = purple_cipher_context_new_by_name("hmac", NULL);
 
@@ -81,15 +99,13 @@
 	 * octet first. */
 	g_string_append_len(salt, "\0\0\0\1", 4);
 
-	result = g_string_sized_new(hash_len);
-
 	/* Compute U0 */
 	purple_cipher_context_set_option(context, "hash", (gpointer)hash);
 	purple_cipher_context_set_key_with_len(context, (guchar *)str->str, str->len);
 	purple_cipher_context_append(context, (guchar *)salt->str, salt->len);
-	purple_cipher_context_digest(context, hash_len, (guchar *)result->str, &(result->len));
+	purple_cipher_context_digest(context, hash_len, result, NULL);
 
-	memcpy(prev, result->str, hash_len);
+	memcpy(prev, result, hash_len);
 
 	/* Compute U1...Ui */
 	for (i = 1; i < iterations; ++i) {
@@ -100,7 +116,7 @@
 		purple_cipher_context_digest(context, hash_len, tmp, NULL);
 
 		for (j = 0; j < hash_len; ++j)
-			result->str[j] ^= tmp[j];
+			result[j] ^= tmp[j];
 
 		memcpy(prev, tmp, hash_len);
 	}
@@ -110,3 +126,431 @@
 	g_free(prev);
 	return result;
 }
+
+/*
+ * Helper functions for doing the SCRAM calculations. The first argument
+ * is the hash algorithm and the second (len) is the length of the output
+ * buffer and key/data (the fourth argument).
+ * "str" is a NULL-terminated string for hmac().
+ *
+ * Needless to say, these are fragile.
+ */
+static void
+hmac(const gchar *hash_alg, gsize len, guchar *out, const guchar *key, const gchar *str)
+{
+	PurpleCipherContext *context;
+
+	context = purple_cipher_context_new_by_name("hmac", NULL);
+	purple_cipher_context_set_option(context, "hash", (gpointer)hash_alg);
+	purple_cipher_context_set_key_with_len(context, key, len);
+	purple_cipher_context_append(context, (guchar *)str, strlen(str));
+	purple_cipher_context_digest(context, len, out, NULL);
+	purple_cipher_context_destroy(context);
+}
+
+static void
+hash(const gchar *hash_alg, gsize len, guchar *out, const guchar *data)
+{
+	PurpleCipherContext *context;
+
+	context = purple_cipher_context_new_by_name(hash_alg, NULL);
+	purple_cipher_context_append(context, data, len);
+	purple_cipher_context_digest(context, len, out, NULL);
+	purple_cipher_context_destroy(context);
+}
+
+gboolean
+jabber_scram_calc_proofs(JabberScramData *data, const char *password,
+                         GString *salt, guint iterations)
+{
+	guint hash_len = hash_to_output_len(data->hash);
+	guint i;
+
+	GString *pass = g_string_new(password);
+
+	guchar *salted_password;
+	guchar client_key[hash_len];
+	guchar stored_key[hash_len];
+	guchar client_signature[hash_len];
+	guchar server_key[hash_len];
+
+	data->client_proof = g_string_sized_new(hash_len);
+	data->client_proof->len = hash_len;
+	data->server_signature = g_string_sized_new(hash_len);
+	data->server_signature->len = hash_len;
+
+	salted_password = jabber_scram_hi(data->hash, pass, salt, iterations);
+	g_string_free(pass, TRUE);
+	if (!salted_password)
+		return FALSE;
+
+	/* client_key = HMAC(salted_password, "Client Key") */
+	hmac(data->hash, hash_len, client_key, salted_password, "Client Key");
+	/* server_key = HMAC(salted_password, "Server Key") */
+	hmac(data->hash, hash_len, server_key, salted_password, "Server Key");
+	g_free(salted_password);
+
+	/* stored_key = HASH(client_key) */
+	hash(data->hash, hash_len, stored_key, client_key);
+
+	/* client_signature = HMAC(stored_key, auth_message) */
+	hmac(data->hash, hash_len, client_signature, stored_key, data->auth_message->str);
+	/* server_signature = HMAC(server_key, auth_message) */
+	hmac(data->hash, hash_len, (guchar *)data->server_signature->str, server_key, data->auth_message->str);
+
+	/* client_proof = client_key XOR client_signature */
+	for (i = 0; i < hash_len; ++i)
+		data->client_proof->str[i] = client_key[i] ^ client_signature[i];
+
+	return TRUE;
+}
+
+static gboolean
+parse_challenge(JabberScramData *data, const char *challenge,
+                gchar **out_nonce, GString **out_salt, guint *out_iterations)
+{
+	gsize cnonce_len;
+	const char *cur;
+	const char *end;
+	const char *val_start, *val_end;
+	char *tmp, *decoded;
+	gsize decoded_len;
+	char *nonce;
+	GString *salt;
+	guint iterations;
+
+	cur = challenge;
+	end = challenge + strlen(challenge);
+
+	if (cur[0] != 'r' || cur[1] != '=')
+		return FALSE;
+
+	val_start = cur + 2;
+	val_end = strchr(val_start, ',');
+	if (val_end == NULL)
+		return FALSE;
+
+	/* Ensure that the first cnonce_len bytes of the nonce are the original
+	 * cnonce we sent to the server.
+	 */
+	cnonce_len = strlen(data->cnonce);
+	if ((val_end - val_start + 1) <= cnonce_len ||
+			strncmp(data->cnonce, val_start, cnonce_len) != 0)
+		return FALSE;
+
+	nonce = g_strndup(val_start, val_end - val_start + 1);
+
+	/* The Salt, base64-encoded */
+	cur = val_end + 1;
+	if (cur[0] != 's' || cur[1] != '=') {
+		g_free(nonce);
+		return FALSE;
+	}
+
+	val_start = cur + 2;
+	val_end = strchr(val_start, ',');
+	if (val_end == NULL) {
+		g_free(nonce);
+		return FALSE;
+	}
+
+	tmp = g_strndup(val_start, val_end - val_start + 1);
+	decoded = (gchar *)purple_base64_decode(tmp, &decoded_len);
+	g_free(tmp);
+	salt = g_string_new_len(decoded, decoded_len);
+	g_free(decoded);
+
+	/* The iteration count */
+	cur = val_end + 1;
+	if (cur[0] != 'i' || cur[1] != '=') {
+		g_free(nonce);
+		g_string_free(salt, TRUE);
+		return FALSE;
+	}
+
+	val_start = cur + 2;
+	val_end = strchr(val_start, ',');
+	if (val_end == NULL)
+		/* There could be extensions. This should possibly be a hard fail. */
+		val_end = end - 1;
+
+	/* Validate the string */
+	for (tmp = (gchar *)val_start; tmp != val_end; ++tmp) {
+		if (!g_ascii_isdigit(*tmp)) {
+			g_free(nonce);
+			g_string_free(salt, TRUE);
+			return FALSE;
+		}
+	}
+
+	tmp = g_strndup(val_start, val_end - val_start + 1);
+	iterations = strtoul(tmp, NULL, 10);
+	g_free(tmp);
+
+	*out_nonce = nonce;
+	*out_salt = salt;
+	*out_iterations = iterations;
+	return TRUE;
+}
+
+static gboolean
+parse_success(JabberScramData *data, const char *success,
+              gchar **out_verifier)
+{
+	const char *cur;
+	const char *val_start, *val_end;
+	const char *end;
+
+	char *verifier;
+
+	g_return_val_if_fail(data != NULL, FALSE);
+	g_return_val_if_fail(success != NULL, FALSE);
+	g_return_val_if_fail(out_verifier != NULL, FALSE);
+
+	cur = success;
+	end = cur + strlen(cur);
+
+	if (cur[0] != 'v' || cur[1] != '=') {
+		/* TODO: Error handling */
+		return FALSE;
+	}
+
+	val_start = cur + 2;
+	val_end = strchr(val_start, ',');
+	if (val_end == NULL)
+		/* TODO: Maybe make this a strict check on not having any extensions? */
+		val_end = end - 1;
+
+	verifier = g_strndup(val_start, val_end - val_start + 1);
+
+	*out_verifier = verifier;
+	return TRUE;
+}
+
+static xmlnode *scram_start(JabberStream *js, xmlnode *mechanisms)
+{
+	xmlnode *reply;
+	JabberScramData *data;
+	guint64 cnonce;
+#ifdef CHANNEL_BINDING
+	gboolean binding_supported = TRUE;
+#endif
+	gchar *dec_out, *enc_out;
+
+	data = js->auth_mech_data = g_new0(JabberScramData, 1);
+	data->hash = mech_to_hash(js->auth_mech->name);
+
+#ifdef CHANNEL_BINDING
+	if (strstr(js->auth_mech_name, "-PLUS"))
+		data->channel_binding = TRUE;
+#endif
+	cnonce = ((guint64)g_random_int() << 32) | g_random_int();
+	data->cnonce = purple_base64_encode((guchar *)cnonce, sizeof(cnonce));
+
+	data->auth_message = g_string_new(NULL);
+	g_string_printf(data->auth_message, "n=%s,r=%s",
+			js->user->node /* TODO: SaslPrep */,
+			data->cnonce);
+
+	reply = xmlnode_new("auth");
+	xmlnode_set_namespace(reply, "urn:ietf:params:xml:ns:xmpp-sasl");
+	xmlnode_set_attrib(reply, "mechanism", js->auth_mech->name);
+
+	/* TODO: Channel binding */
+	dec_out = g_strdup_printf("%c,,%s", 'n', data->auth_message->str);
+	enc_out = purple_base64_encode((guchar *)dec_out, strlen(dec_out));
+	purple_debug_misc("jabber", "initial SCRAM message '%s'\n", dec_out);
+
+	xmlnode_insert_data(reply, enc_out, -1);
+
+	g_free(enc_out);
+	g_free(dec_out);
+
+	return reply;
+}
+
+static xmlnode *scram_handle_challenge(JabberStream *js, xmlnode *challenge)
+{
+	JabberScramData *data = js->auth_mech_data;
+	xmlnode *reply;
+
+	gchar *enc_in, *dec_in;
+	gchar *enc_out, *dec_out;
+	gsize decoded_size;
+
+	gchar *enc_proof;
+
+	gchar *nonce;
+	GString *salt;
+	guint iterations;
+
+	g_return_val_if_fail(data != NULL, NULL);
+
+	enc_in = xmlnode_get_data(challenge);
+	/* TODO: Error handling */
+	g_return_val_if_fail(enc_in != NULL && *enc_in != '\0', NULL);
+
+	dec_in = (gchar *)purple_base64_decode(enc_in, &decoded_size);
+	g_free(enc_in);
+	if (!dec_in || decoded_size != strlen(dec_in)) {
+		/* Danger afoot; SCRAM shouldn't contain NUL bytes */
+		/* TODO: Error handling */
+		g_free(dec_in);
+		return NULL;
+	}
+
+	purple_debug_misc("jabber", "decoded challenge (%" G_GSIZE_FORMAT "): %s\n",
+			decoded_size, dec_in);
+
+	g_string_append_c(data->auth_message, ',');
+	g_string_append(data->auth_message, dec_in);
+
+	if (!parse_challenge(data, dec_in, &nonce, &salt, &iterations)) {
+		/* TODO: Error handling */
+		return NULL;
+	}
+
+	g_string_append_c(data->auth_message, ',');
+	/* "biwsCg==" is the base64 encoding of "n,,". I promise. */
+	g_string_append_printf(data->auth_message, "c=%s,r=%s", "biwsCg==", nonce);
+#ifdef CHANNEL_BINDING
+	#error fix this
+#endif
+
+	if (!jabber_scram_calc_proofs(data, purple_connection_get_password(js->gc), salt, iterations)) {
+		/* TODO: Error handling */
+		return NULL;
+	}
+
+	reply = xmlnode_new("response");
+	xmlnode_set_namespace(reply, "urn:ietf:params:xml:ns:xmpp-sasl");
+
+	enc_proof = purple_base64_encode((guchar *)data->client_proof->str, data->client_proof->len);
+	dec_out = g_strdup_printf("c=%s,r=%s,p=%s", "biwsCg==", nonce, enc_proof);
+	enc_out = purple_base64_encode((guchar *)dec_out, strlen(dec_out));
+
+	purple_debug_misc("jabber", "decoded response (%" G_GSIZE_FORMAT "): %s\n",
+			strlen(dec_out), dec_out);
+
+	xmlnode_insert_data(reply, enc_out, -1);
+
+	g_free(enc_out);
+	g_free(dec_out);
+	g_free(enc_proof);
+
+	return reply;
+}
+
+static gboolean scram_handle_success(JabberStream *js, xmlnode *packet)
+{
+	JabberScramData *data = js->auth_mech_data;
+	char *enc_in, *dec_in;
+	char *enc_server_signature;
+	guchar *server_signature;
+	gsize decoded_size;
+
+	enc_in = xmlnode_get_data(packet);
+	/* TODO: Error handling */
+	g_return_val_if_fail(enc_in != NULL && *enc_in != '\0', FALSE);
+
+	dec_in = (gchar *)purple_base64_decode(enc_in, &decoded_size);
+	g_free(enc_in);
+	if (!dec_in || decoded_size != strlen(dec_in)) {
+		/* Danger afoot; SCRAM shouldn't contain NUL bytes */
+		/* TODO: Error handling */
+		g_free(dec_in);
+		return FALSE;
+	}
+
+	purple_debug_misc("jabber", "decoded success (%" G_GSIZE_FORMAT "): %s\n",
+			decoded_size, dec_in);
+
+	if (!parse_success(data, dec_in, &enc_server_signature)) {
+		/* TODO: Error handling */
+		return FALSE;
+	}
+
+	server_signature = purple_base64_decode(enc_server_signature, &decoded_size);
+	if (server_signature == NULL) {
+		/* TODO: Error handling */
+		return FALSE;
+	}
+
+	if (decoded_size != data->server_signature->len) {
+		/* TODO: Error handling */
+		purple_debug_error("jabber", "SCRAM server signature wrong length (was "
+				"(was %" G_GSIZE_FORMAT ", expected %" G_GSIZE_FORMAT ")\n",
+				decoded_size, data->server_signature->len);
+		return FALSE;
+	}
+
+	if (0 != memcmp(server_signature, data->server_signature->str, decoded_size)) {
+		/* TODO: Error handling */
+		purple_debug_error("jabber", "SCRAM server signature did not match!\n");
+		return FALSE;
+	}
+
+	/* Hooray */
+	return TRUE;
+}
+
+static void scram_dispose(JabberStream *js)
+{
+	if (js->auth_mech_data) {
+		JabberScramData *data = js->auth_mech_data;
+
+		g_free(data->cnonce);
+		if (data->auth_message)
+			g_string_free(data->auth_message, TRUE);
+		if (data->client_proof)
+			g_string_free(data->client_proof, TRUE);
+		if (data->server_signature)
+			g_string_free(data->server_signature, TRUE);
+		g_free(data);
+		js->auth_mech_data = NULL;
+	}
+}
+
+static JabberSaslMech scram_sha1_mech = {
+	50, /* priority */
+	"SCRAM-SHA-1", /* name */
+	scram_start,
+	scram_handle_challenge,
+	scram_handle_success,
+	NULL, /* handle_failure */
+	scram_dispose
+};
+
+#ifdef CHANNEL_BINDING
+/* With channel binding */
+static JabberSaslMech scram_sha1_plus_mech = {
+	scram_sha1_mech.priority + 1, /* priority */
+	"SCRAM-SHA-1-PLUS", /* name */
+	scram_start,
+	scram_handle_challenge,
+	scram_handle_success,
+	NULL, /* handle_failure */
+	scram_dispose
+};
+#endif
+
+/* For tests */
+JabberSaslMech *jabber_scram_get_sha1(void)
+{
+	return &scram_sha1_mech;
+}
+
+JabberSaslMech **jabber_auth_get_scram_mechs(gint *count)
+{
+	static JabberSaslMech *mechs[] = {
+		&scram_sha1_mech,
+#ifdef CHANNEL_BINDING
+		&scram_sha1_plus_mech,
+#endif
+	};
+
+	g_return_val_if_fail(count != NULL, NULL);
+
+	*count = G_N_ELEMENTS(mechs);
+	return mechs;
+}
--- a/libpurple/protocols/jabber/auth_scram.h	Sun Nov 08 18:39:30 2009 +0000
+++ b/libpurple/protocols/jabber/auth_scram.h	Mon Nov 09 03:42:26 2009 +0000
@@ -24,6 +24,29 @@
 #ifndef PURPLE_JABBER_AUTH_SCRAM_H_
 #define PURPLE_JABBER_AUTH_SCRAM_H_
 
+/*
+ * Every function in this file is ONLY exposed for tests.
+ * DO NOT USE ANYTHING HERE OR YOU WILL BE SENT TO THE PIT OF DESPAIR.
+ */
+
+/* Per-connection state stored between messages.
+ * This is stored in js->auth_data_mech.
+ */
+
+typedef struct {
+	const char *hash;
+	char *cnonce;
+	GString *auth_message;
+
+	GString *client_proof;
+	GString *server_signature;
+	gboolean channel_binding;
+} JabberScramData;
+
+#include "auth.h"
+
+JabberSaslMech *jabber_scram_get_sha1(void);
+
 /**
  * Implements the Hi() function as described in the SASL-SCRAM I-D.
  *
@@ -34,9 +57,26 @@
  * @param salt The salt.
  * @param iterations The number of iterations to perform.
  *
- * @returns A newly allocated string containing the result.
+ * @returns A newly allocated string containing the result. The string is
+ *          NOT null-terminated and its length is the length of the binary
+ *          output of the hash function in-use.
  */
-GString *jabber_auth_scram_hi(const char *hash, const GString *str,
-                              GString *salt, guint iterations);
+guchar *jabber_scram_hi(const char *hash, const GString *str,
+                        GString *salt, guint iterations);
+
+/**
+ * Calculates the proofs as described in Section 3 of the SASL-SCRAM I-D.
+ *
+ * @param data A JabberScramData structure. hash and auth_message must be
+ *             set. client_proof and server_signature will be set as a result
+ *             of this function.
+ * @param password   The user's password.
+ * @param salt       The salt (as specified by the server)
+ * @param iterations The number of iterations to perform.
+ *
+ * @returns TRUE if the proofs were successfully calculated. FALSE otherwise.
+ */
+gboolean jabber_scram_calc_proofs(JabberScramData *data, const char *password,
+                                  GString *salt, guint iterations);
 
 #endif /* PURPLE_JABBER_AUTH_SCRAM_H_ */
--- a/libpurple/protocols/jabber/jabber.c	Sun Nov 08 18:39:30 2009 +0000
+++ b/libpurple/protocols/jabber/jabber.c	Mon Nov 09 03:42:26 2009 +0000
@@ -1501,6 +1501,8 @@
 	purple_circ_buffer_destroy(js->write_buffer);
 	if(js->writeh)
 		purple_input_remove(js->writeh);
+	if (js->auth_mech && js->auth_mech->dispose)
+		js->auth_mech->dispose(js);
 #ifdef HAVE_CYRUS_SASL
 	if(js->sasl)
 		sasl_dispose(&js->sasl);
--- a/libpurple/protocols/jabber/jabber.h	Sun Nov 08 18:39:30 2009 +0000
+++ b/libpurple/protocols/jabber/jabber.h	Mon Nov 09 03:42:26 2009 +0000
@@ -107,6 +107,7 @@
 	} protocol_version;
 
 	JabberSaslMech *auth_mech;
+	gpointer auth_mech_data;
 	char *stream_id;
 	JabberStreamState state;
 
--- a/libpurple/tests/test_jabber_scram.c	Sun Nov 08 18:39:30 2009 +0000
+++ b/libpurple/tests/test_jabber_scram.c	Mon Nov 09 03:42:26 2009 +0000
@@ -4,14 +4,14 @@
 #include "../util.h"
 #include "../protocols/jabber/auth_scram.h"
 
+static JabberSaslMech *scram_sha1_mech = NULL;
+
 #define assert_pbkdf2_equal(password, salt, count, expected) { \
 	GString *p = g_string_new(password); \
 	GString *s = g_string_new(salt); \
-	GString *result = jabber_auth_scram_hi("sha1", p, s, count); \
+	guchar *result = jabber_scram_hi("sha1", p, s, count); \
 	fail_if(result == NULL, "Hi() returned NULL"); \
-	fail_if(result->len != 20, "Hi() returned with unexpected length %u", result->len); \
-	fail_if(0 != memcmp(result->str, expected, 20), "Hi() returned invalid result"); \
-	g_string_free(result, TRUE); \
+	fail_if(0 != memcmp(result, expected, 20), "Hi() returned invalid result"); \
 	g_string_free(s, TRUE); \
 	g_string_free(p, TRUE); \
 }
@@ -19,9 +19,7 @@
 START_TEST(test_pbkdf2)
 {
 	assert_pbkdf2_equal("password", "salt", 1, "\x0c\x60\xc8\x0f\x96\x1f\x0e\x71\xf3\xa9\xb5\x24\xaf\x60\x12\x06\x2f\xe0\x37\xa6");
-	
 	assert_pbkdf2_equal("password", "salt", 2, "\xea\x6c\x01\x4d\xc7\x2d\x6f\x8c\xcd\x1e\xd9\x2a\xce\x1d\x41\xf0\xd8\xde\x89\x57");
-
 	assert_pbkdf2_equal("password", "salt", 4096, "\x4b\x00\x79\x01\xb7\x65\x48\x9a\xbe\xad\x49\xd9\x26\xf7\x21\xd0\x65\xa4\x29\xc1");
 
 #if 0
@@ -31,6 +29,40 @@
 }
 END_TEST
 
+START_TEST(test_proofs)
+{
+	JabberScramData *data = g_new0(JabberScramData, 1);
+	gboolean ret;
+	GString *salt;
+	const char *client_proof;
+/*	const char *server_signature; */
+
+	data->hash = "sha1";
+	data->auth_message = g_string_new("n=username@jabber.org,r=8jLxB5515dhFxBil5A0xSXMH,"
+			"r=8jLxB5515dhFxBil5A0xSXMHabc,s=c2FsdA==,i=1,"
+			"c=biws,r=8jLxB5515dhFxBil5A0xSXMHabc");
+	client_proof = "\x48\x61\x30\xa5\x61\x0b\xae\xb9\xe4\x11\xa8\xfd\xa5\xcd\x34\x1d\x8a\x3c\x28\x17";
+
+	salt = g_string_new("salt");
+	ret = jabber_scram_calc_proofs(data, "password", salt, 1);
+	fail_if(ret == FALSE, "Failed to calculate SCRAM proofs!");
+
+	fail_unless(0 == memcmp(client_proof, data->client_proof->str, 20));
+	g_string_free(salt, TRUE);
+	g_string_free(data->auth_message, TRUE);
+	g_free(data);
+}
+END_TEST
+
+#if 0
+START_TEST(test_mech)
+{
+	scram_sha1_mech = jabber_scram_get_sha1();
+
+}
+END_TEST
+#endif
+
 Suite *
 jabber_scram_suite(void)
 {
@@ -40,5 +72,9 @@
 	tcase_add_test(tc, test_pbkdf2);
 	suite_add_tcase(s, tc);
 
+	tc = tcase_create("SCRAM Proofs");
+	tcase_add_test(tc, test_proofs);
+	suite_add_tcase(s, tc);
+
 	return s;
 }