|
14 | 14 |
|
15 | 15 | #include "catch2/catch_test_macros.hpp" |
16 | 16 | #include "catch2/matchers/catch_matchers_string.hpp" |
| 17 | +#include <catch2/generators/catch_generators.hpp> |
17 | 18 | #include <cstring> |
18 | 19 | #include <zt_internal.h> |
19 | 20 | #include "internal_model.h" |
@@ -94,6 +95,92 @@ TEST_CASE("zt_jwt_parse","[model]") { |
94 | 95 | zt_jwt_drop(&jwt_struct); |
95 | 96 | } |
96 | 97 |
|
| 98 | +TEST_CASE("zt_jwt_parse negative", "[model]") { |
| 99 | + auto tc = GENERATE(table<const char*, const char*>({ |
| 100 | + // structure errors |
| 101 | + {"empty string", ""}, |
| 102 | + {"no dots", "eyJhbGciOiJSUzI1NiJ9"}, |
| 103 | + {"only one dot", "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJ0ZXN0In0"}, |
| 104 | + {"dots only", "..."}, |
| 105 | + {"empty segments", ".."}, |
| 106 | + |
| 107 | + // header decode/parse errors |
| 108 | + {"invalid base64 header", "!!!invalid!!!.cGF5bG9hZA.c2ln"}, |
| 109 | + {"header not json", "bm90IGpzb24.cGF5bG9hZA.c2ln"}, // "not json" |
| 110 | + |
| 111 | + // payload decode/parse errors |
| 112 | + {"invalid base64 payload", "eyJhbGciOiJSUzI1NiJ9.!!!invalid!!!.c2ln"}, |
| 113 | + {"payload not json", "eyJhbGciOiJSUzI1NiJ9.bm90IGpzb24.c2ln"}, // "not json" |
| 114 | + {"payload is json array", "eyJhbGciOiJSUzI1NiJ9.WzEsIDJd.c2ln"}, // [1, 2] |
| 115 | + {"empty json payload", "eyJhbGciOiJSUzI1NiJ9.e30.c2ln"}, // {} |
| 116 | + |
| 117 | + // iss claim errors |
| 118 | + {"missing iss claim", "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ0ZXN0In0.c2ln"}, // {"sub":"test"} |
| 119 | + {"iss not a string", "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOjEyM30.c2ln"}, // {"iss":123} |
| 120 | + {"iss is null", "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiBudWxsfQ.c2ln"}, // {"iss": null} |
| 121 | + {"iss is boolean", "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiB0cnVlfQ.c2ln"}, // {"iss": true} |
| 122 | + {"iss is array", "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiBbImEiXX0.c2ln"}, // {"iss": ["a"]} |
| 123 | + {"iss is object", "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiB7fX0.c2ln"}, // {"iss": {}} |
| 124 | + |
| 125 | + // exp claim errors |
| 126 | + {"exp not an int", "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiZXhwIjoibm90YW5pbnQifQ.c2ln"}, // {"iss":"test","exp":"notanint"} |
| 127 | + {"exp is boolean", "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiAidGVzdCIsICJleHAiOiB0cnVlfQ.c2ln"}, // {"iss":"test","exp":true} |
| 128 | + {"exp is float", "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiAidGVzdCIsICJleHAiOiAxLjV9.c2ln"}, // {"iss":"test","exp":1.5} |
| 129 | + })); |
| 130 | + |
| 131 | + DYNAMIC_SECTION(std::get<0>(tc)) { |
| 132 | + zt_jwt jwt_struct{}; |
| 133 | + int rc = zt_jwt_parse(std::get<1>(tc), &jwt_struct); |
| 134 | + CHECK(rc != 0); |
| 135 | + } |
| 136 | +} |
| 137 | + |
| 138 | +TEST_CASE("zt_jwt_parse valid", "[model]") { |
| 139 | + struct valid_case { |
| 140 | + const char *label; |
| 141 | + const char *input; |
| 142 | + const char *expected_iss; |
| 143 | + uint64_t expected_exp; |
| 144 | + }; |
| 145 | + auto tc = GENERATE(values<valid_case>({ |
| 146 | + // {"iss":"https://example.com","exp":1704661695} |
| 147 | + {"with exp", |
| 148 | + "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIiwiZXhwIjoxNzA0NjYxNjk1fQ.c2ln", |
| 149 | + "https://example.com", 1704661695}, |
| 150 | + // {"iss":"https://example.com"} |
| 151 | + {"without exp", |
| 152 | + "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL2V4YW1wbGUuY29tIn0.c2ln", |
| 153 | + "https://example.com", 0}, |
| 154 | + // json-c represents JSON null as NULL pointer, parser treats it as absent exp |
| 155 | + // {"iss":"test","exp":null} |
| 156 | + {"exp is null", |
| 157 | + "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiAidGVzdCIsICJleHAiOiBudWxsfQ.c2ln", |
| 158 | + "test", 0}, |
| 159 | + // parser doesn't validate header structure, only that it's valid JSON |
| 160 | + // header: [1, 2], payload: {"iss":"test"} |
| 161 | + {"header is json array", |
| 162 | + "WzEsIDJd.eyJpc3MiOiJ0ZXN0In0.c2ln", |
| 163 | + "test", 0}, |
| 164 | + })); |
| 165 | + |
| 166 | + DYNAMIC_SECTION(tc.label) { |
| 167 | + zt_jwt jwt_struct{}; |
| 168 | + int rc = zt_jwt_parse(tc.input, &jwt_struct); |
| 169 | + CHECK(rc == 0); |
| 170 | + CHECK_THAT(cstr_str(&jwt_struct.issuer), Catch::Matchers::Equals(tc.expected_iss)); |
| 171 | + CHECK(jwt_struct.expiration == tc.expected_exp); |
| 172 | + zt_jwt_drop(&jwt_struct); |
| 173 | + } |
| 174 | +} |
| 175 | + |
| 176 | +TEST_CASE("zt_jwt_parse extra dots", "[model]") { |
| 177 | + zt_jwt jwt_struct{}; |
| 178 | + // header.payload.sig.extra — extra segment; verify no crash |
| 179 | + int rc = zt_jwt_parse("eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJ0ZXN0In0.c2ln.ZXh0cmE", &jwt_struct); |
| 180 | + (void)rc; |
| 181 | + zt_jwt_drop(&jwt_struct); |
| 182 | +} |
| 183 | + |
97 | 184 | TEST_CASE("jwt cred", "[model]") { |
98 | 185 | ziti_credential_t *cred = nullptr; |
99 | 186 | int rc = ziti_credential_from_jwt(jwt, &cred); |
|
0 commit comments