/src/mod_auth_openidc/test/fuzz/fuzz_jwt.c
Line | Count | Source |
1 | | /* |
2 | | * Licensed to the Apache Software Foundation (ASF) under one or more |
3 | | * contributor license agreements. Licensed under the Apache License, |
4 | | * Version 2.0 (the "License"); you may not use this file except in |
5 | | * compliance with the License. You may obtain a copy of the License at |
6 | | * |
7 | | * http://www.apache.org/licenses/LICENSE-2.0 |
8 | | * |
9 | | * Copyright (C) 2017-2026 ZmartZone Holding BV - hans.zandbelt@openidc.com |
10 | | * |
11 | | * Fuzz target for oidc_jwt_parse() + oidc_jwt_verify(): parsing of |
12 | | * attacker-controlled compact JWT/JWS/JWE serializations, followed by |
13 | | * signature verification against a fixed key set. The key set is the one the |
14 | | * unit tests carry for their RFC vectors, so the seeds in corpus/jwt that are |
15 | | * real tokens verify successfully and the success paths past the signature |
16 | | * check -- claim extraction, re-serialization -- stay reachable (the two JWE |
17 | | * vectors decrypt, but carry a plain-text rather than a JSON payload, so they |
18 | | * stop at the payload decode; they still drive the key-unwrap and AEAD code): |
19 | | * |
20 | | * oct HS256 draft-ietf-oauth-json-web-token-20 §3.1 (no kid) |
21 | | * EC ES256 kid "f6qtj" |
22 | | * RSA RS256 the x5t-headed id_token vector (no kid) |
23 | | * RSA RSA-OAEP private key, RFC 7516 §A.1 (JWE decryption) |
24 | | * oct A128KW, draft-ietf-jose-json-web-encryption-40 §A.3 (JWE decryption) |
25 | | * |
26 | | * A token without a kid is tried against every key of a compatible type, so |
27 | | * mutations of the seeds keep hitting the verification code rather than only |
28 | | * the kid lookup. |
29 | | */ |
30 | | |
31 | | #include "fuzz.h" |
32 | | /* util.h first: see the include-order note in fuzz_metadata.c */ |
33 | | /* clang-format off */ |
34 | | #include "util.h" /* test fixture */ |
35 | | #include "json.h" |
36 | | #include "jose.h" |
37 | | #include "util/util.h" /* oidc_json_decode_object */ |
38 | | /* clang-format on */ |
39 | | |
40 | | #include <apr_hash.h> |
41 | | #include <apr_pools.h> |
42 | | #include <apr_strings.h> |
43 | | #include <stdlib.h> |
44 | | |
45 | | static int g_ready = 0; |
46 | | static apr_hash_t *g_keys = NULL; |
47 | | |
48 | | /* |
49 | | * LeakSanitizer anchors for the key objects. |
50 | | * |
51 | | * The JWKs are pool-allocated and point at cjose/OpenSSL objects on the heap. |
52 | | * APR allocates its pool nodes with mmap() on the distributions the sanitizer |
53 | | * builds run on, and LSan does not scan anonymous mappings for pointers, so |
54 | | * from its point of view those heap objects are referenced by nothing. That is |
55 | | * harmless until libFuzzer runs its mid-run leak check -- which it does after |
56 | | * any input whose malloc count exceeded its free count, and an RSA verify |
57 | | * does exactly that the first time it caches a Montgomery context on the key |
58 | | * -- and then every key, and everything lazily attached to it, is reported as |
59 | | * a leak and the run stops. Keeping the heap handles in a global (a root LSan |
60 | | * does scan) makes the whole key graph reachable again; the keys are released |
61 | | * from an atexit handler so the end-of-run check stays clean as well. |
62 | | */ |
63 | 28 | #define FUZZ_JWT_MAX_KEYS 8 |
64 | | static void *g_cjose_keys[FUZZ_JWT_MAX_KEYS]; |
65 | | |
66 | 2 | static void fuzz_jwt_keys_destroy(void) { |
67 | 2 | if (g_keys != NULL) |
68 | 2 | oidc_jwk_list_destroy_hash(g_keys); |
69 | 2 | g_keys = NULL; |
70 | 18 | for (int i = 0; i < FUZZ_JWT_MAX_KEYS; i++) |
71 | 16 | g_cjose_keys[i] = NULL; |
72 | 2 | } |
73 | | |
74 | | static const struct { |
75 | | const char *name; /* hash key: the kid where the token carries one, descriptive otherwise */ |
76 | | const char *json; |
77 | | } fuzz_jwt_keys[] = { |
78 | | {"hs256", "{\"kty\":\"oct\",\"k\":\"AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-" |
79 | | "1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow\"}"}, |
80 | | {"f6qtj", "{\"kty\":\"EC\",\"kid\":\"f6qtj\",\"use\":\"sig\"," |
81 | | "\"x\":\"iARwFlN3B3xa8Zn_O-CVfqry68tXIhO9DckKo1yrNg0\"," |
82 | | "\"y\":\"583S_mPS7YVZtLCjx2O69G_JzQPnMxjieOli-9cc_6Q\",\"crv\":\"P-256\"}"}, |
83 | | {"rs256", "{\"kty\":\"RSA\",\"e\":\"AQAB\"," |
84 | | "\"n\":\"3lDyn_ZvG32Pw5kYbRuVxHsPfe9Xt8s9vOXnt8z7_T-hZZvealNhCxz9VEwTJ7TsZ9CLi5c30FjoEJYFkKdd" |
85 | | "LAdxKo0oOXWc_AWrQvPwht9a-o6dX2fL_9CmXW1hGHXMH0qiLMrFqMSzZeh-GUY6F1woE_eKsAo6LOhP8X77FlEQT2Eu" |
86 | | "71wu8KC4B3sH_9QTco50KNw14-bRY5j2V2TZelvsXJnvrN4lXtEVYWFkREKeXzMH8DhDyZzh0NcHa7dFBa7rDusyfIHj" |
87 | | "uP6uAju_Ao6hhdOGjlKePMVtfusWBAI7MWDChLTqiCTvlZnCpkpTTh5m-i7TbE1TwmdbLceq1w\"}"}, |
88 | | {"rsa-oaep", |
89 | | "{\"kty\":\"RSA\"," |
90 | | "\"n\":\"oahUIoWw0K0usKNuOR6H4wkf4oBUXHTxRvgb48E-BVvxkeDNjbC4he8rUWcJoZmds2h7M70imEVhRU5djINXtqllXI4D" |
91 | | "FqcI1DgjT9LewND8MW2Krf3Spsk_ZkoFnilakGygTwpZ3uesH-PFABNIUYpOiN15dsQRkgr0vEhxN92i2asbOenSZeyaxziK72Uw" |
92 | | "xrrKoExv6kc5twXTq4h-QChLOln0_mtUZwfsRaMStPs6mS6XrgxnxbWhojf663tuEQueGC-FCMfra36C9knDFGzKsNa7LZK2djYg" |
93 | | "yD3JR_MB_4NUJW_TqOQtwHYbxevoJArm-L5StowjzGy-_bq6Gw\"," |
94 | | "\"e\":\"AQAB\"," |
95 | | "\"d\":\"kLdtIj6GbDks_ApCSTYQtelcNttlKiOyPzMrXHeI-yk1F7-kpDxY4-WY5NWV5KntaEeXS1j82E375xxhWMHXyvjYecPT" |
96 | | "9fpwR_M9gV8n9Hrh2anTpTD93Dt62ypW3yDsJzBnTnrYu1iwWRgBKrEYY46qAZIrA2xAwnm2X7uGR1hghkqDp0Vqj3kbSCz1XyfC" |
97 | | "s6_LehBwtxHIyh8Ripy40p24moOAbgxVw3rxT_vlt3UVe4WO3JkJOzlpUf-KTVI2Ptgm-dARxTEtE-id-4OJr0h-K-VFs3VSndVT" |
98 | | "IznSxfyrj8ILL6MG_Uv8YAu7VILSB3lOW085-4qE3DzgrTjgyQ\"," |
99 | | "\"p\":\"1r52Xk46c-LsfB5P442p7atdPUrxQSy4mti_tZI3Mgf2EuFVbUoDBvaRQ-SWxkbkmoEzL7JXroSBjSrK3YIQgYdMgyAEPT" |
100 | | "PjXv_hI2_1eTSPVZfzL0lffNn03IXqWF5MDFuoUYE0hzb2vhrlN_rKrbfDIwUbTrjjgieRbwC6Cl0\"," |
101 | | "\"q\":\"wLb35x7hmQWZsWJmB_vle87ihgZ19S8lBEROLIsZG4ayZVe9Hi9gDVCOBmUDdaDYVTSNx_8Fyw1YYa9XGrGnDew00J28cR" |
102 | | "UoeBB_jKI1oma0Orv1T9aXIWxKwd4gvxFImOWr3QRL9KEBRzk2RatUBnmDZJTIAfwTs0g68UZHvtc\"," |
103 | | "\"dp\":\"ZK-YwE7diUh0qR1tR7w8WHtolDx3MZ_OTowiFvgfeQ3SiresXjm9gZ5KLhMXvo-uz-KUJWDxS5pFQ_M0evdo1dKiRTjV" |
104 | | "w_x4NyqyXPM5nULPkcpU827rnpZzAJKpdhWAgqrXGKAECQH0Xt4taznjnd_zVpAmZZq60WPMBMfKcuE\"," |
105 | | "\"dq\":\"Dq0gfgJ1DdFGXiLvQEZnuKEN0UUmsJBxkjydc3j4ZYdBiMRAy86x0vHCjywcMlYYg4yoC4YZa9hNVcsjqA3FeiL19rk8" |
106 | | "g6Qn29Tt0cj8qqyFpz9vNDBUfCAiJVeESOjJDZPYHdHY8v1b-o-Z2X5tvLx-TCekf7oxyeKDUqKWjis\"," |
107 | | "\"qi\":\"VIMpMYbPf47dT1w_zDUXfPimsSegnMOA1zTaX7aGk_8urY6R8-ZW1FxU7AlWAyLWybqq6t16VFd7hQd0y6flUK4SlOyd" |
108 | | "B61gwanOsXGOAOv82cHq0E3eL4HrtZkUuKvnPrMnsUUFlfUdybVzxyjz9JF_XyaY14ardLSjf4L_FNY\"}"}, |
109 | | {"a128kw", "{\"kty\":\"oct\",\"k\":\"GawgguFyGrWKav7AX4VKUg\"}"}, |
110 | | {NULL, NULL}, |
111 | | }; |
112 | | |
113 | | /* engine-called one-time init, pre-forkserver on AFL++: see fuzz.h */ |
114 | 2 | int LLVMFuzzerInitialize(int *argc, char ***argv) { |
115 | 2 | (void)argc; |
116 | 2 | (void)argv; |
117 | 2 | if (!g_ready) { |
118 | 2 | oidc_test_setup(); |
119 | | /* the keys live for the whole run, in the fixture's pool */ |
120 | 2 | apr_pool_t *pool = oidc_test_pool_get(); |
121 | 2 | request_rec *r = oidc_test_request_get(); |
122 | 2 | g_keys = apr_hash_make(pool); |
123 | 12 | for (int i = 0; (fuzz_jwt_keys[i].name != NULL) && (i < FUZZ_JWT_MAX_KEYS); i++) { |
124 | 10 | oidc_json_t *json = NULL; |
125 | 10 | oidc_jwk_t *jwk = NULL; |
126 | 10 | oidc_jose_error_t err; |
127 | 10 | if ((oidc_json_decode_object(r, fuzz_jwt_keys[i].json, &json) == TRUE) && (json != NULL)) { |
128 | 10 | if ((oidc_jwk_parse_json(pool, json, &jwk, &err) == TRUE) && (jwk != NULL)) { |
129 | 10 | apr_hash_set(g_keys, fuzz_jwt_keys[i].name, APR_HASH_KEY_STRING, jwk); |
130 | 10 | g_cjose_keys[i] = jwk->cjose_jwk; |
131 | 10 | } |
132 | 10 | oidc_json_decref(json); |
133 | 10 | } |
134 | 10 | } |
135 | 2 | atexit(fuzz_jwt_keys_destroy); |
136 | 2 | g_ready = 1; |
137 | 2 | } |
138 | 2 | return 0; |
139 | 2 | } |
140 | | |
141 | 6.00k | int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { |
142 | 6.00k | if (!g_ready) |
143 | 0 | LLVMFuzzerInitialize(NULL, NULL); |
144 | | |
145 | 6.00k | apr_pool_t *pool = NULL; |
146 | 6.00k | apr_pool_create(&pool, oidc_test_pool_get()); |
147 | | |
148 | 6.00k | char *s = apr_pstrmemdup(pool, (const char *)data, size); |
149 | 6.00k | oidc_jwt_t *jwt = NULL; |
150 | 6.00k | oidc_jose_error_t err; |
151 | | |
152 | | /* parse (and decrypt, when the input is a JWE for one of the keys) ... */ |
153 | 6.00k | if ((oidc_jwt_parse(pool, s, &jwt, g_keys, FALSE, &err) == TRUE) && (jwt != NULL)) { |
154 | | /* ... then verify the signature against the key set, and run the claim |
155 | | * accessors and the serializer over the parsed token either way */ |
156 | 232 | oidc_jwt_verify(pool, jwt, g_keys, &err); |
157 | 232 | oidc_jwt_hdr_get(jwt, "typ"); |
158 | 232 | oidc_jwt_hdr_get(jwt, "cty"); |
159 | 232 | char *value = NULL; |
160 | 232 | oidc_jose_get_string(pool, jwt->payload.value.json, "aud", FALSE, &value, &err); |
161 | 232 | oidc_jose_get_string(pool, jwt->payload.value.json, "nonce", FALSE, &value, &err); |
162 | 232 | double ts = 0; |
163 | 232 | oidc_jose_get_timestamp(jwt->payload.value.json, "nbf", FALSE, &ts, &err); |
164 | 232 | oidc_jose_jwt_serialize(pool, jwt, &err); |
165 | 232 | } |
166 | | /* a JWT wraps cjose/OpenSSL objects allocated outside the pool */ |
167 | 6.00k | if (jwt != NULL) |
168 | 232 | oidc_jwt_destroy(jwt); |
169 | | |
170 | 6.00k | apr_pool_destroy(pool); |
171 | 6.00k | return 0; |
172 | 6.00k | } |