blob: 2aea84a1e3815937882c14a34e80fad0994053d5 [file] [edit]
/* Public domain, no copyright. Use at your own risk. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "jwt_tests.h"
/* @rfc{7515,7.2} JWS JSON Serialization (multi-signature) + keyring/policy
* verification (issue #308). Every test runs under each compiled crypto backend
* via the SET_OPS() loop, and the suite is built under both JSON backends. */
/* Private signing keys in tests/keys/jwks_keyring.json (used for kid tests). */
#define KID_ES256 "ee30e68c-a5e9-4067-864e-cf7dc9ffd2c1"
#define KID_RS256 "1971c0aa-4369-435f-aaf3-d4ca34a23ddb"
static jwk_set_t *load_one(const char *file)
{
char *path;
jwk_set_t *set;
int ret;
ret = asprintf(&path, KEYDIR "/%s", file);
ck_assert_int_gt(ret, 0);
set = jwks_create_fromfile(path);
free(path);
ck_assert_ptr_nonnull(set);
return set;
}
/* A keyring of the standard EC P-256 + RSA-2048 private test keys (no kids; a
* signature without a "kid" is matched by scanning the ring). */
static jwk_set_t *load_ec_rsa_ring(void)
{
jwk_set_t *ring = jwks_create(NULL);
ck_assert_ptr_nonnull(ring);
ck_assert_ptr_nonnull(jwks_load_fromfile(ring,
KEYDIR "/ec_key_prime256v1.json"));
ck_assert_ptr_nonnull(jwks_load_fromfile(ring,
KEYDIR "/rsa_key_2048.json"));
ck_assert_uint_eq(jwks_item_count(ring), 2);
return ring;
}
/* ---- Flattened round-trip, single key, across alg families ---- */
START_TEST(test_flat_roundtrip)
{
static const struct {
const char *file;
jwt_alg_t alg;
} cases[] = {
{ "ec_key_prime256v1.json", JWT_ALG_ES256 },
{ "rsa_key_2048.json", JWT_ALG_RS256 },
{ "oct_key_256.json", JWT_ALG_HS256 },
};
size_t i;
SET_OPS();
for (i = 0; i < ARRAY_SIZE(cases); i++) {
jwt_builder_auto_t *builder = NULL;
jwt_checker_auto_t *checker = NULL;
char_auto *tok = NULL;
jwk_set_t *ks = load_one(cases[i].file);
const jwk_item_t *key = jwks_item_get(ks, 0);
builder = jwt_builder_new();
ck_assert_int_eq(jwt_builder_setkey(builder, cases[i].alg, key), 0);
ck_assert_int_eq(jwt_builder_set_format(builder,
JWT_FORMAT_JSON_FLAT), 0);
tok = jwt_builder_generate(builder);
ck_assert_ptr_nonnull(tok);
ck_assert_int_eq(tok[0], '{'); /* it is JSON, not compact */
checker = jwt_checker_new();
ck_assert_int_eq(jwt_checker_setkey(checker, cases[i].alg, key), 0);
ck_assert_int_eq(jwt_checker_verify(checker, tok), 0);
ck_assert_uint_eq(jwt_checker_sig_count(checker), 1);
ck_assert_int_eq(jwt_checker_sig_verified(checker, 0), 1);
jwks_free(ks);
}
}
END_TEST
/* ---- General round-trip: RS256 + ES256, verified against a 2-key ring ---- */
START_TEST(test_general_roundtrip)
{
jwt_builder_auto_t *builder = NULL;
char_auto *tok = NULL;
jwk_set_t *eks, *rks, *ring;
const jwk_item_t *ec, *rsa;
jwt_verify_policy_t pol[] = { JWT_VERIFY_POLICY_ANY, JWT_VERIFY_POLICY_ALL };
size_t i;
SET_OPS();
eks = load_one("ec_key_prime256v1.json");
rks = load_one("rsa_key_2048.json");
ec = jwks_item_get(eks, 0);
rsa = jwks_item_get(rks, 0);
ring = load_ec_rsa_ring();
builder = jwt_builder_new();
ck_assert_int_eq(jwt_builder_setkey(builder, JWT_ALG_RS256, rsa), 0);
ck_assert_ptr_nonnull(jwt_builder_add_signature(builder, JWT_ALG_ES256, ec));
tok = jwt_builder_generate(builder);
ck_assert_ptr_nonnull(tok);
/* Both ANY and ALL accept: every signature verifies against the ring. */
for (i = 0; i < ARRAY_SIZE(pol); i++) {
jwt_checker_auto_t *checker = jwt_checker_new();
ck_assert_int_eq(jwt_checker_setkeyring(checker, ring, pol[i]), 0);
ck_assert_int_eq(jwt_checker_verify(checker, tok), 0);
ck_assert_uint_eq(jwt_checker_sig_count(checker), 2);
ck_assert_int_eq(jwt_checker_sig_verified(checker, 0), 1);
ck_assert_int_eq(jwt_checker_sig_verified(checker, 1), 1);
ck_assert_ptr_nonnull(jwt_checker_sig_key(checker, 0));
}
jwks_free(eks);
jwks_free(rks);
jwks_free(ring);
}
END_TEST
/* ---- Policy: a ring missing one signer's key. ANY passes, ALL fails. ---- */
START_TEST(test_policy_partial_ring)
{
jwt_builder_auto_t *builder = NULL;
jwt_checker_auto_t *c_any = NULL, *c_all = NULL;
char_auto *tok = NULL;
jwk_set_t *eks, *rks, *ring;
const jwk_item_t *ec, *rsa;
SET_OPS();
eks = load_one("ec_key_prime256v1.json");
rks = load_one("rsa_key_2048.json");
ec = jwks_item_get(eks, 0);
rsa = jwks_item_get(rks, 0);
/* Ring with ONLY the EC key. */
ring = jwks_create(NULL);
ck_assert_ptr_nonnull(jwks_load_fromfile(ring,
KEYDIR "/ec_key_prime256v1.json"));
builder = jwt_builder_new();
ck_assert_int_eq(jwt_builder_setkey(builder, JWT_ALG_RS256, rsa), 0);
ck_assert_ptr_nonnull(jwt_builder_add_signature(builder, JWT_ALG_ES256, ec));
tok = jwt_builder_generate(builder);
ck_assert_ptr_nonnull(tok);
/* ANY: the ES256 signature verifies; the RS256 one has no key. */
c_any = jwt_checker_new();
ck_assert_int_eq(jwt_checker_setkeyring(c_any, ring,
JWT_VERIFY_POLICY_ANY), 0);
ck_assert_int_eq(jwt_checker_verify(c_any, tok), 0);
ck_assert_int_eq(jwt_checker_sig_verified(c_any, 0), 0); /* RS256 */
ck_assert_int_eq(jwt_checker_sig_verified(c_any, 1), 1); /* ES256 */
/* ALL: not every signature verified -> reject. */
c_all = jwt_checker_new();
ck_assert_int_eq(jwt_checker_setkeyring(c_all, ring,
JWT_VERIFY_POLICY_ALL), 0);
ck_assert_int_ne(jwt_checker_verify(c_all, tok), 0);
jwks_free(eks);
jwks_free(rks);
jwks_free(ring);
}
END_TEST
/* ---- kid-driven key selection from a multi-key ring ---- */
START_TEST(test_kid_match)
{
jwt_builder_auto_t *builder = NULL;
jwt_checker_auto_t *checker = NULL;
char_auto *tok = NULL;
jwk_set_t *ring;
jwk_item_t *ec, *rsa;
jwt_signature_t *s_rsa, *s_ec;
SET_OPS();
ring = jwks_create_fromfile(KEYDIR "/jwks_keyring.json");
ck_assert_ptr_nonnull(ring);
ec = jwks_find_bykid(ring, KID_ES256);
rsa = jwks_find_bykid(ring, KID_RS256);
ck_assert_ptr_nonnull(ec);
ck_assert_ptr_nonnull(rsa);
/* Both signers added with add_signature so each can carry its own kid. */
builder = jwt_builder_new();
s_rsa = jwt_builder_add_signature(builder, JWT_ALG_RS256, rsa);
ck_assert_ptr_nonnull(s_rsa);
ck_assert_int_eq(jwt_signature_add_protected_json(s_rsa, "kid",
"\"" KID_RS256 "\""), 0);
s_ec = jwt_builder_add_signature(builder, JWT_ALG_ES256, ec);
ck_assert_ptr_nonnull(s_ec);
ck_assert_int_eq(jwt_signature_add_protected_json(s_ec, "kid",
"\"" KID_ES256 "\""), 0);
tok = jwt_builder_generate(builder);
ck_assert_ptr_nonnull(tok);
checker = jwt_checker_new();
ck_assert_int_eq(jwt_checker_setkeyring(checker, ring,
JWT_VERIFY_POLICY_ALL), 0);
ck_assert_int_eq(jwt_checker_verify(checker, tok), 0);
ck_assert_uint_eq(jwt_checker_sig_count(checker), 2);
/* Each signature verified against the key its kid named. */
ck_assert_str_eq(jwks_item_kid(jwt_checker_sig_key(checker, 0)), KID_RS256);
ck_assert_str_eq(jwks_item_kid(jwt_checker_sig_key(checker, 1)), KID_ES256);
jwks_free(ring);
}
END_TEST
/* ---- Tampering a signature is rejected ---- */
START_TEST(test_tamper)
{
jwt_builder_auto_t *builder = NULL;
jwt_checker_auto_t *checker = NULL;
char_auto *tok = NULL;
jwk_set_t *eks;
const jwk_item_t *ec;
char *p;
SET_OPS();
eks = load_one("ec_key_prime256v1.json");
ec = jwks_item_get(eks, 0);
builder = jwt_builder_new();
ck_assert_int_eq(jwt_builder_setkey(builder, JWT_ALG_ES256, ec), 0);
ck_assert_int_eq(jwt_builder_set_format(builder, JWT_FORMAT_JSON_FLAT), 0);
tok = jwt_builder_generate(builder);
ck_assert_ptr_nonnull(tok);
/* Flip the first base64url char of the signature. */
p = strstr(tok, "\"signature\":\"");
ck_assert_ptr_nonnull(p);
p += strlen("\"signature\":\"");
*p = (*p == 'A') ? 'B' : 'A';
checker = jwt_checker_new();
ck_assert_int_eq(jwt_checker_setkey(checker, JWT_ALG_ES256, ec), 0);
ck_assert_int_ne(jwt_checker_verify(checker, tok), 0);
jwks_free(eks);
}
END_TEST
/* ---- Malformed JSON serializations are rejected, never crash (json-c) ---- */
START_TEST(test_malformed)
{
/* "e30" is base64url({}). Each of these must be rejected structurally. */
static const char * const bad[] = {
"{}", /* no payload */
"{\"payload\":123}", /* non-string payload */
"{\"payload\":\"e30\"}", /* no signature(s) */
"{\"payload\":\"e30\",\"signatures\":[]}", /* empty array */
"{\"payload\":\"e30\",\"signatures\":\"x\"}", /* non-array */
/* both General and Flattened members */
"{\"payload\":\"e30\",\"signatures\":[{\"protected\":\"e30\","
"\"signature\":\"x\"}],\"signature\":\"y\"}",
/* flattened, missing signature */
"{\"payload\":\"e30\",\"protected\":\"e30\"}",
/* alg:none in protected (base64url of {"alg":"none"}) */
"{\"payload\":\"e30\",\"protected\":\"eyJhbGciOiJub25lIn0\","
"\"signature\":\"\"}",
"{ this is not json",
};
jwk_set_t *eks;
const jwk_item_t *ec;
size_t i;
SET_OPS();
eks = load_one("ec_key_prime256v1.json");
ec = jwks_item_get(eks, 0);
for (i = 0; i < ARRAY_SIZE(bad); i++) {
jwt_checker_auto_t *checker = jwt_checker_new();
ck_assert_int_eq(jwt_checker_setkey(checker, JWT_ALG_ES256, ec), 0);
ck_assert_int_ne(jwt_checker_verify(checker, bad[i]), 0);
}
jwks_free(eks);
}
END_TEST
/* ---- "none" is refused on the builder side too ---- */
START_TEST(test_none_rejected_builder)
{
jwt_builder_auto_t *builder = jwt_builder_new();
SET_OPS();
ck_assert_ptr_null(jwt_builder_add_signature(builder, JWT_ALG_NONE, NULL));
}
END_TEST
static int cb_allow(jwt_t *jwt, jwt_config_t *config)
{
(void)jwt;
(void)config;
return 0;
}
static int cb_reject(jwt_t *jwt, jwt_config_t *config)
{
(void)jwt;
(void)config;
return 1;
}
/* ---- Per-signature unprotected header + assorted edge/error paths ---- */
START_TEST(test_edges)
{
jwt_builder_auto_t *b1 = NULL, *b2 = NULL, *b3 = NULL;
jwt_checker_auto_t *c1 = NULL, *c2 = NULL, *c3 = NULL;
jwt_signature_t *sig;
char_auto *tok = NULL;
jwk_set_t *eks;
const jwk_item_t *ec;
SET_OPS();
eks = load_one("ec_key_prime256v1.json");
ec = jwks_item_get(eks, 0);
/* (a) An unprotected per-signature "header" round-trips and verifies. */
b1 = jwt_builder_new();
sig = jwt_builder_add_signature(b1, JWT_ALG_ES256, ec);
ck_assert_ptr_nonnull(sig);
ck_assert_int_eq(jwt_signature_add_header_json(sig, "kid", "\"hint\""), 0);
ck_assert_int_eq(jwt_builder_set_format(b1, JWT_FORMAT_JSON_FLAT), 0);
tok = jwt_builder_generate(b1);
ck_assert_ptr_nonnull(tok);
ck_assert_ptr_nonnull(strstr(tok, "\"header\""));
c1 = jwt_checker_new();
ck_assert_int_eq(jwt_checker_setkey(c1, JWT_ALG_ES256, ec), 0);
ck_assert_int_eq(jwt_checker_verify(c1, tok), 0);
/* Out-of-range introspection is safe. */
ck_assert_int_eq(jwt_checker_sig_verified(c1, 99), 0);
ck_assert_ptr_null(jwt_checker_sig_key(c1, 99));
/* (b) A header parameter in both protected and unprotected is rejected. */
b2 = jwt_builder_new();
sig = jwt_builder_add_signature(b2, JWT_ALG_ES256, ec);
ck_assert_int_eq(jwt_signature_add_protected_json(sig, "kid", "\"a\""), 0);
ck_assert_int_eq(jwt_signature_add_header_json(sig, "kid", "\"b\""), 0);
ck_assert_int_eq(jwt_builder_set_format(b2, JWT_FORMAT_JSON_FLAT), 0);
ck_assert_ptr_null(jwt_builder_generate(b2));
/* (c) "none" cannot be emitted in a JSON serialization. */
b3 = jwt_builder_new();
ck_assert_int_eq(jwt_builder_setkey(b3, JWT_ALG_NONE, NULL), 0);
ck_assert_int_eq(jwt_builder_set_format(b3, JWT_FORMAT_JSON_FLAT), 0);
ck_assert_ptr_null(jwt_builder_generate(b3));
/* (d) A verify callback runs per signature: allow then reject. */
c2 = jwt_checker_new();
ck_assert_int_eq(jwt_checker_setkey(c2, JWT_ALG_ES256, ec), 0);
ck_assert_int_eq(jwt_checker_setcb(c2, cb_allow, NULL), 0);
ck_assert_int_eq(jwt_checker_verify(c2, tok), 0);
c3 = jwt_checker_new();
ck_assert_int_eq(jwt_checker_setkey(c3, JWT_ALG_ES256, ec), 0);
ck_assert_int_eq(jwt_checker_setcb(c3, cb_reject, NULL), 0);
ck_assert_int_ne(jwt_checker_verify(c3, tok), 0);
/* (e) setkeyring rejects a NULL ring and a bogus policy. */
{
jwt_checker_auto_t *c4 = jwt_checker_new();
ck_assert_int_ne(jwt_checker_setkeyring(c4, NULL,
JWT_VERIFY_POLICY_ANY), 0);
ck_assert_int_ne(jwt_checker_setkeyring(c4, eks,
(jwt_verify_policy_t)42), 0);
}
jwks_free(eks);
}
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("jws_json");
tcase_add_loop_test(tc_core, test_flat_roundtrip, 0, i);
tcase_add_loop_test(tc_core, test_general_roundtrip, 0, i);
tcase_add_loop_test(tc_core, test_policy_partial_ring, 0, i);
tcase_add_loop_test(tc_core, test_kid_match, 0, i);
tcase_add_loop_test(tc_core, test_tamper, 0, i);
tcase_add_loop_test(tc_core, test_malformed, 0, i);
tcase_add_loop_test(tc_core, test_none_rejected_builder, 0, i);
tcase_add_loop_test(tc_core, test_edges, 0, i);
tcase_set_timeout(tc_core, 60);
suite_add_tcase(s, tc_core);
return s;
}
int main(void)
{
JWT_TEST_MAIN("LibJWT JWS JSON Serialization (#308)");
}