# HG changeset patch # User Paul Aurich # Date 1257738146 0 # Node ID c1d41b7484ffb4318449e9b4f52fa314f45fd993 # Parent 2b4465db73f1c97da74e6f2ba66fc00560413fee jabber: Complete (though untested) SCRAM implementation. Client proof calculations function properly, but parsing is untested. diff -r 2b4465db73f1 -r c1d41b7484ff libpurple/protocols/jabber/auth.c --- 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) diff -r 2b4465db73f1 -r c1d41b7484ff libpurple/protocols/jabber/auth.h --- 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 diff -r 2b4465db73f1 -r c1d41b7484ff libpurple/protocols/jabber/auth_scram.c --- 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; +} diff -r 2b4465db73f1 -r c1d41b7484ff libpurple/protocols/jabber/auth_scram.h --- 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_ */ diff -r 2b4465db73f1 -r c1d41b7484ff libpurple/protocols/jabber/jabber.c --- 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); diff -r 2b4465db73f1 -r c1d41b7484ff libpurple/protocols/jabber/jabber.h --- 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; diff -r 2b4465db73f1 -r c1d41b7484ff libpurple/tests/test_jabber_scram.c --- 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; }