blob: 161cd26f83a3dc63a4a6fbd9bec81aeadde02623 [file] [edit]
/* Public domain, no copyright. Use at your own risk. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "jwt_tests.h"
static const char PT[] = "{\"sub\":\"1234567890\",\"name\":\"Jane Doe\"}";
/* Count the dots in a compact token. */
static int count_dots(const char *s)
{
int n = 0;
for (; *s; s++)
if (*s == '.')
n++;
return n;
}
static void roundtrip(const char *keyfile, jwe_enc_t enc)
{
jwe_builder_auto_t *builder = NULL;
jwe_checker_auto_t *checker = NULL;
char_auto *tok = NULL;
unsigned char *pt = NULL;
size_t pt_len = 0;
read_json(keyfile);
builder = jwe_builder_new();
ck_assert_ptr_nonnull(builder);
ck_assert_int_eq(jwe_builder_setkey(builder, JWE_ALG_DIR, enc, g_item),
0);
tok = jwe_builder_generate(builder, (const unsigned char *)PT,
strlen(PT));
ck_assert_ptr_nonnull(tok);
ck_assert_int_eq(jwe_builder_error(builder), 0);
/* Five parts (4 dots), with an empty Encrypted Key segment for dir. */
ck_assert_int_eq(count_dots(tok), 4);
ck_assert_ptr_nonnull(strstr(tok, ".."));
checker = jwe_checker_new();
ck_assert_ptr_nonnull(checker);
ck_assert_int_eq(jwe_checker_setkey(checker, JWE_ALG_DIR, enc, g_item),
0);
pt = jwe_checker_decrypt(checker, tok, &pt_len);
ck_assert_ptr_nonnull(pt);
ck_assert_int_eq(jwe_checker_error(checker), 0);
ck_assert_int_eq(pt_len, strlen(PT));
ck_assert_mem_eq(pt, PT, pt_len);
free(pt);
free_key();
}
START_TEST(roundtrip_128)
{
SET_OPS();
roundtrip("oct_dir_128.json", JWE_ENC_A128GCM);
}
END_TEST
START_TEST(roundtrip_192)
{
SET_OPS();
roundtrip("oct_dir_192.json", JWE_ENC_A192GCM);
}
END_TEST
START_TEST(roundtrip_256)
{
SET_OPS();
roundtrip("oct_dir_256.json", JWE_ENC_A256GCM);
}
END_TEST
START_TEST(tamper_ct_fails)
{
jwe_builder_auto_t *builder = NULL;
jwe_checker_auto_t *checker = NULL;
char_auto *tok = NULL;
unsigned char *pt = NULL;
size_t pt_len = 0;
char *ct_seg;
SET_OPS();
read_json("oct_dir_256.json");
builder = jwe_builder_new();
ck_assert_int_eq(jwe_builder_setkey(builder, JWE_ALG_DIR,
JWE_ENC_A256GCM, g_item), 0);
tok = jwe_builder_generate(builder, (const unsigned char *)PT,
strlen(PT));
ck_assert_ptr_nonnull(tok);
/* Corrupt the first character of the ciphertext (4th of 5 segments:
* hdr . EK . iv . CT . tag). Changing the base64url char to a clearly
* distinct value guarantees a different decoded byte, so the GCM tag
* must fail to verify. */
ct_seg = tok;
ct_seg = strchr(ct_seg, '.') + 1; /* start of EK (empty) */
ct_seg = strchr(ct_seg, '.') + 1; /* start of iv */
ct_seg = strchr(ct_seg, '.') + 1; /* start of ct */
ck_assert_int_ne(*ct_seg, '\0');
ck_assert_int_ne(*ct_seg, '.');
*ct_seg = (*ct_seg == 'Q') ? 'R' : 'Q';
checker = jwe_checker_new();
ck_assert_int_eq(jwe_checker_setkey(checker, JWE_ALG_DIR,
JWE_ENC_A256GCM, g_item), 0);
pt = jwe_checker_decrypt(checker, tok, &pt_len);
ck_assert_ptr_null(pt);
ck_assert_int_eq(jwe_checker_error(checker), 1);
free_key();
}
END_TEST
/* A GCM token whose IV is not 96 bits must be rejected (RFC 7518 5.3 fixes the
* GCM IV at 12 bytes). With the central iv_len gate the token is rejected as
* malformed before the cipher runs; without it the wrong IV would change J0 and
* the GCM tag check would fail anyway, so the observable outcome is the same.
* This is therefore a conformance regression guard rather than a bypass test:
* it pins that a non-12-byte IV never decrypts. Replace the 12-byte IV segment
* with a 16-byte one (still valid base64url) and require rejection. */
START_TEST(gcm_wrong_iv_len_fails)
{
jwe_builder_auto_t *builder = NULL;
jwe_checker_auto_t *checker = NULL;
char_auto *tok = NULL;
char *bad = NULL;
unsigned char *pt = NULL;
size_t pt_len = 0;
char *iv_start, *iv_end;
/* base64url of 16 bytes "abcdefghijklmnop" (no padding). */
static const char iv16[] = "YWJjZGVmZ2hpamtsbW5vcA";
SET_OPS();
read_json("oct_dir_256.json");
builder = jwe_builder_new();
ck_assert_int_eq(jwe_builder_setkey(builder, JWE_ALG_DIR,
JWE_ENC_A256GCM, g_item), 0);
tok = jwe_builder_generate(builder, (const unsigned char *)PT,
strlen(PT));
ck_assert_ptr_nonnull(tok);
/* Locate the IV segment (3rd of 5: hdr . EK . iv . ct . tag). */
iv_start = strchr(tok, '.') + 1; /* start of EK (empty) */
iv_start = strchr(iv_start, '.') + 1; /* start of iv */
iv_end = strchr(iv_start, '.'); /* end of iv */
ck_assert_ptr_nonnull(iv_end);
/* Rebuild the token with the oversized IV in place of the original. */
ck_assert_int_gt(asprintf(&bad, "%.*s%s%s",
(int)(iv_start - tok), tok, iv16, iv_end), 0);
checker = jwe_checker_new();
ck_assert_int_eq(jwe_checker_setkey(checker, JWE_ALG_DIR,
JWE_ENC_A256GCM, g_item), 0);
pt = jwe_checker_decrypt(checker, bad, &pt_len);
ck_assert_ptr_null(pt);
ck_assert_int_eq(jwe_checker_error(checker), 1);
free(bad);
free_key();
}
END_TEST
/* A JWE whose protected header carries a "crit" parameter must be rejected:
* libjwt understands no critical JWE header parameters (RFC 7516 4.1.13). The
* crit check runs before tag verification, so even though replacing the header
* invalidates the tag, the failure is reported as the crit error. */
START_TEST(crit_header_rejected)
{
jwe_builder_auto_t *builder = NULL;
jwe_checker_auto_t *checker = NULL;
char_auto *tok = NULL;
char *bad = NULL;
unsigned char *pt = NULL;
size_t pt_len = 0;
char *rest;
/* {"alg":"dir","enc":"A256GCM","crit":["myparam"],"myparam":1} */
static const char crit_hdr[] =
"eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwiY3JpdCI6WyJteXBhcmFtIl0"
"sIm15cGFyYW0iOjF9";
SET_OPS();
read_json("oct_dir_256.json");
builder = jwe_builder_new();
ck_assert_int_eq(jwe_builder_setkey(builder, JWE_ALG_DIR,
JWE_ENC_A256GCM, g_item), 0);
tok = jwe_builder_generate(builder, (const unsigned char *)PT,
strlen(PT));
ck_assert_ptr_nonnull(tok);
/* Each of these protected headers must be rejected by the crit check:
* a well-formed crit (unknown param), a non-array crit, an empty crit,
* and a crit array with a non-string entry. */
static const char *const bad_hdrs[] = {
crit_hdr,
/* {"alg":"dir","enc":"A256GCM","crit":"x"} */
"eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwiY3JpdCI6IngifQ",
/* {"alg":"dir","enc":"A256GCM","crit":[]} */
"eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwiY3JpdCI6W119",
/* {"alg":"dir","enc":"A256GCM","crit":[1]} */
"eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwiY3JpdCI6WzFdfQ",
};
size_t bi;
rest = strchr(tok, '.');
ck_assert_ptr_nonnull(rest);
checker = jwe_checker_new();
ck_assert_int_eq(jwe_checker_setkey(checker, JWE_ALG_DIR,
JWE_ENC_A256GCM, g_item), 0);
for (bi = 0; bi < ARRAY_SIZE(bad_hdrs); bi++) {
/* Swap the protected header (1st segment) for the bad one. */
ck_assert_int_gt(asprintf(&bad, "%s%s", bad_hdrs[bi], rest), 0);
pt = jwe_checker_decrypt(checker, bad, &pt_len);
ck_assert_ptr_null(pt);
ck_assert_int_eq(jwe_checker_error(checker), 1);
ck_assert_ptr_nonnull(strstr(jwe_checker_error_msg(checker),
"crit"));
jwe_checker_error_clear(checker);
free(bad);
bad = NULL;
}
free_key();
}
END_TEST
START_TEST(wrong_key_fails)
{
jwe_builder_auto_t *builder = NULL;
jwe_checker_auto_t *checker = NULL;
char_auto *tok = NULL;
unsigned char *pt = NULL;
size_t pt_len = 0;
SET_OPS();
/* Encrypt with one 256-bit key... */
read_json("oct_dir_256.json");
builder = jwe_builder_new();
ck_assert_int_eq(jwe_builder_setkey(builder, JWE_ALG_DIR,
JWE_ENC_A256GCM, g_item), 0);
tok = jwe_builder_generate(builder, (const unsigned char *)PT,
strlen(PT));
ck_assert_ptr_nonnull(tok);
free_key();
/* ...decrypt with a different 256-bit key. */
read_json("oct_key_256_enc.json");
checker = jwe_checker_new();
ck_assert_int_eq(jwe_checker_setkey(checker, JWE_ALG_DIR,
JWE_ENC_A256GCM, g_item), 0);
pt = jwe_checker_decrypt(checker, tok, &pt_len);
ck_assert_ptr_null(pt);
ck_assert_int_eq(jwe_checker_error(checker), 1);
free_key();
}
END_TEST
START_TEST(dir_wrong_len)
{
jwe_builder_auto_t *builder = NULL;
char_auto *tok = NULL;
SET_OPS();
/* A 256-bit key cannot be used as a dir CEK for A128GCM (needs 128). */
read_json("oct_dir_256.json");
builder = jwe_builder_new();
ck_assert_int_eq(jwe_builder_setkey(builder, JWE_ALG_DIR,
JWE_ENC_A128GCM, g_item), 0);
tok = jwe_builder_generate(builder, (const unsigned char *)PT,
strlen(PT));
ck_assert_ptr_null(tok);
ck_assert_int_eq(jwe_builder_error(builder), 1);
free_key();
}
END_TEST
START_TEST(reject_non_jwe)
{
jwe_checker_auto_t *checker = NULL;
unsigned char *pt = NULL;
size_t pt_len = 0;
SET_OPS();
read_json("oct_dir_256.json");
checker = jwe_checker_new();
ck_assert_int_eq(jwe_checker_setkey(checker, JWE_ALG_DIR,
JWE_ENC_A256GCM, g_item), 0);
/* A 3-part JWS-shaped token has the wrong number of parts. */
pt = jwe_checker_decrypt(checker,
"eyJhbGciOiJub25lIn0.eyJhIjoxfQ.", &pt_len);
ck_assert_ptr_null(pt);
ck_assert_int_eq(jwe_checker_error(checker), 1);
jwe_checker_error_clear(checker);
/* Garbage / empty. */
pt = jwe_checker_decrypt(checker, "", &pt_len);
ck_assert_ptr_null(pt);
free_key();
}
END_TEST
START_TEST(alg_enc_mismatch)
{
jwe_builder_auto_t *builder = NULL;
jwe_checker_auto_t *checker = NULL;
char_auto *tok = NULL;
unsigned char *pt = NULL;
size_t pt_len = 0;
SET_OPS();
read_json("oct_dir_256.json");
builder = jwe_builder_new();
ck_assert_int_eq(jwe_builder_setkey(builder, JWE_ALG_DIR,
JWE_ENC_A256GCM, g_item), 0);
tok = jwe_builder_generate(builder, (const unsigned char *)PT,
strlen(PT));
ck_assert_ptr_nonnull(tok);
/* Checker configured for a different enc than the token carries. */
read_json("oct_dir_128.json");
checker = jwe_checker_new();
ck_assert_int_eq(jwe_checker_setkey(checker, JWE_ALG_DIR,
JWE_ENC_A128GCM, g_item), 0);
pt = jwe_checker_decrypt(checker, tok, &pt_len);
ck_assert_ptr_null(pt);
ck_assert_int_eq(jwe_checker_error(checker), 1);
free_key();
}
END_TEST
START_TEST(generate_errors)
{
jwe_builder_auto_t *builder = NULL;
char *tok;
SET_OPS();
/* NULL object. */
ck_assert_ptr_null(jwe_builder_generate(NULL,
(const unsigned char *)PT, strlen(PT)));
/* No setkey -> no key/alg set. */
builder = jwe_builder_new();
tok = jwe_builder_generate(builder, (const unsigned char *)PT,
strlen(PT));
ck_assert_ptr_null(tok);
ck_assert_int_eq(jwe_builder_error(builder), 1);
/* NULL plaintext with non-zero length. */
read_json("oct_dir_256.json");
jwe_builder_error_clear(builder);
ck_assert_int_eq(jwe_builder_setkey(builder, JWE_ALG_DIR,
JWE_ENC_A256GCM, g_item), 0);
tok = jwe_builder_generate(builder, NULL, 5);
ck_assert_ptr_null(tok);
free_key();
}
END_TEST
START_TEST(decrypt_errors)
{
jwe_checker_auto_t *checker = NULL;
unsigned char *pt;
size_t pt_len = 0;
SET_OPS();
/* NULL object. */
ck_assert_ptr_null(jwe_checker_decrypt(NULL, "a.b.c.d.e", &pt_len));
/* No setkey. */
checker = jwe_checker_new();
pt = jwe_checker_decrypt(checker, "a.b.c.d.e", &pt_len);
ck_assert_ptr_null(pt);
ck_assert_int_eq(jwe_checker_error(checker), 1);
read_json("oct_dir_256.json");
ck_assert_int_eq(jwe_checker_setkey(checker, JWE_ALG_DIR,
JWE_ENC_A256GCM, g_item), 0);
/* Header that is not valid base64url / not JSON. */
jwe_checker_error_clear(checker);
pt = jwe_checker_decrypt(checker, "!!!!..aa.bb.cc", &pt_len);
ck_assert_ptr_null(pt);
/* Valid base64url header that is not a JSON object. */
jwe_checker_error_clear(checker);
/* base64url("123") = "MTIz" -> parses as a JSON number, no alg/enc. */
pt = jwe_checker_decrypt(checker, "MTIz..aa.bb.cc", &pt_len);
ck_assert_ptr_null(pt);
free_key();
}
END_TEST
START_TEST(decrypt_header_cases)
{
jwe_checker_auto_t *checker = NULL;
unsigned char *pt;
size_t pt_len = 0;
SET_OPS();
read_json("oct_dir_256.json");
checker = jwe_checker_new();
ck_assert_int_eq(jwe_checker_setkey(checker, JWE_ALG_DIR,
JWE_ENC_A256GCM, g_item), 0);
/* A header with no "enc" (a JWS-style header) -> not a JWE.
* base64url('{"alg":"dir"}') computed below by encoding via builder is
* overkill; use a known-good encoding. */
/* {"alg":"dir"} */
pt = jwe_checker_decrypt(checker,
"eyJhbGciOiJkaXIifQ..aa.bb.cc", &pt_len);
ck_assert_ptr_null(pt);
jwe_checker_error_clear(checker);
/* A header carrying "zip" must be rejected. Build a normal token,
* then splice in a zip header. Easiest: craft the header directly.
* {"alg":"dir","enc":"A256GCM","zip":"DEF"} */
pt = jwe_checker_decrypt(checker,
"eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwiemlwIjoiREVGIn0..aa.bb.cc",
&pt_len);
ck_assert_ptr_null(pt);
jwe_checker_error_clear(checker);
/* @rfc{7516} dir must carry an empty Encrypted Key. A token whose
* header is dir+A256GCM but with a non-empty EK segment must be
* rejected at that check. Header is base64url({"alg":"dir","enc":
* "A256GCM"}) (sorted keys, as the builder emits). */
pt = jwe_checker_decrypt(checker,
"eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0.QUJD.aa.bb.cc",
&pt_len);
ck_assert_ptr_null(pt);
jwe_checker_error_clear(checker);
free_key();
}
END_TEST
START_TEST(decrypt_cek_cases)
{
jwe_checker_auto_t *checker = NULL;
unsigned char *pt;
size_t pt_len = 0;
SET_OPS();
/* dir + A128GCM token, but checker holds a 256-bit key: the dir CEK
* length will not match what A128GCM requires. Header is
* base64url({"alg":"dir","enc":"A128GCM"}). */
read_json("oct_dir_256.json");
checker = jwe_checker_new();
ck_assert_int_eq(jwe_checker_setkey(checker, JWE_ALG_DIR,
JWE_ENC_A128GCM, g_item), 0);
pt = jwe_checker_decrypt(checker,
"eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4R0NNIn0..aa.bb.cc", &pt_len);
ck_assert_ptr_null(pt);
ck_assert_int_eq(jwe_checker_error(checker), 1);
free_key();
}
END_TEST
START_TEST(decrypt_bad_components)
{
jwe_checker_auto_t *checker = NULL;
unsigned char *pt;
size_t pt_len = 0;
SET_OPS();
read_json("oct_dir_256.json");
checker = jwe_checker_new();
ck_assert_int_eq(jwe_checker_setkey(checker, JWE_ALG_DIR,
JWE_ENC_A256GCM, g_item), 0);
/* Valid dir+A256GCM header but an empty IV segment -> decode yields a
* zero-length IV, which is rejected. Header is
* base64url({"alg":"dir","enc":"A256GCM"}). */
pt = jwe_checker_decrypt(checker,
"eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0...bb.cc", &pt_len);
ck_assert_ptr_null(pt);
ck_assert_int_eq(jwe_checker_error(checker), 1);
free_key();
}
END_TEST
static Suite *libjwt_suite(const char *title)
{
Suite *s;
TCase *tc_core;
int i = ARRAY_SIZE(jwt_test_ops);
s = suite_create(title);
tc_core = tcase_create("JWE dir+GCM");
tcase_add_loop_test(tc_core, roundtrip_128, 0, i);
tcase_add_loop_test(tc_core, roundtrip_192, 0, i);
tcase_add_loop_test(tc_core, roundtrip_256, 0, i);
tcase_add_loop_test(tc_core, tamper_ct_fails, 0, i);
tcase_add_loop_test(tc_core, gcm_wrong_iv_len_fails, 0, i);
tcase_add_loop_test(tc_core, crit_header_rejected, 0, i);
tcase_add_loop_test(tc_core, wrong_key_fails, 0, i);
tcase_add_loop_test(tc_core, dir_wrong_len, 0, i);
tcase_add_loop_test(tc_core, reject_non_jwe, 0, i);
tcase_add_loop_test(tc_core, alg_enc_mismatch, 0, i);
tcase_add_loop_test(tc_core, generate_errors, 0, i);
tcase_add_loop_test(tc_core, decrypt_errors, 0, i);
tcase_add_loop_test(tc_core, decrypt_header_cases, 0, i);
tcase_add_loop_test(tc_core, decrypt_cek_cases, 0, i);
tcase_add_loop_test(tc_core, decrypt_bad_components, 0, i);
tcase_set_timeout(tc_core, 30);
suite_add_tcase(s, tc_core);
return s;
}
int main(void)
{
JWT_TEST_MAIN("LibJWT JWE dir+GCM");
}