hx509: Add a JWT fuzzer

This commit is contained in:
Nicolas Williams
2026-01-06 18:40:50 -06:00
parent 2ff2cc04b8
commit 76fbb83e86
22 changed files with 355 additions and 0 deletions

94
lib/hx509/FUZZING.md Normal file
View File

@@ -0,0 +1,94 @@
# Fuzzing lib/hx509
This directory contains a fuzzer for JWS/JWT parsing (`jose.c`).
## fuzz_jose
Fuzzes `hx509_jws_verify()` and `hx509_jwt_verify()` with various key types.
Note: This fuzzer primarily exercises the parsing paths (base64url decoding,
JSON header/payload parsing, signature format handling). Signature verification
itself will reject most mutations early, so this is less effective than fuzzing
pure codecs like the JSON parser.
### Building
#### Standalone (for testing)
```bash
cd build
make -C lib/hx509 fuzz_jose
```
#### With libFuzzer + AddressSanitizer (recommended)
```bash
cd build
CC=clang CXX=clang++ \
CFLAGS="-fsanitize=fuzzer-no-link,address -g -O1" \
LDFLAGS="-fsanitize=fuzzer,address" \
../configure --enable-maintainer-mode --enable-developer
make -C lib/hx509 fuzz_jose
```
#### With AFL++
```bash
cd build
CC=afl-clang-fast CXX=afl-clang-fast++ \
../configure --enable-maintainer-mode --enable-developer
make -C lib/hx509 fuzz_jose
```
### Running
#### Standalone mode (reads from files or stdin)
```bash
# Test with corpus files
./lib/hx509/fuzz_jose ../lib/hx509/fuzz_jose_corpus/*.txt
# Test single input
echo 'eyJhbGciOiJSUzI1NiJ9.e30.AA' | ./lib/hx509/fuzz_jose
```
#### libFuzzer mode
```bash
# Basic fuzzing
./lib/hx509/fuzz_jose ../lib/hx509/fuzz_jose_corpus/
# With options
./lib/hx509/fuzz_jose ../lib/hx509/fuzz_jose_corpus/ \
-max_len=65536 \
-timeout=10 \
-jobs=4 \
-workers=4
```
#### AFL++ mode
```bash
afl-fuzz -i ../lib/hx509/fuzz_jose_corpus -o findings -- ./lib/hx509/fuzz_jose @@
```
### Seed Corpus
The `fuzz_jose_corpus/` directory contains seed inputs covering:
- Valid RFC test vectors (RS256, ES256, EdDSA from RFC 7515/8037)
- Various algorithms (RS384, RS512, ES384, ES512, HS256, unknown)
- Edge cases (empty parts, minimal tokens, algorithm "none")
- Malformed inputs (bad base64, wrong signature lengths)
- Long headers, nested JSON, Unicode payloads
### What it tests
1. **JWS verification** with RSA, EC, and Ed25519 public keys
2. **JWT verification** including claims parsing
3. **Base64URL decoding** of header, payload, and signature
4. **JSON parsing** of header and claims
5. **ECDSA signature format** conversion (JWS r||s to DER)
6. **Key type matching** against declared algorithm

View File

@@ -162,6 +162,12 @@ LDADD = libhx509.la
test_soft_pkcs11_LDADD = libhx509.la $(top_builddir)/lib/asn1/libasn1.la
test_name_LDADD = libhx509.la $(LIB_roken) $(top_builddir)/lib/asn1/libasn1.la
# Fuzzer for JWS/JWT parsing (build with: make fuzz_jose CFLAGS="-fsanitize=fuzzer,address")
noinst_PROGRAMS = fuzz_jose
fuzz_jose_SOURCES = fuzz_jose.c
fuzz_jose_LDADD = libhx509.la $(LIB_roken) $(top_builddir)/lib/base/libheimbase.la
fuzz_jose.$(OBJEXT): $(HX509_PROTOS) $(nodist_include_HEADERS)
test_expr_LDADD = libhx509.la $(LIB_roken) $(top_builddir)/lib/asn1/libasn1.la
TESTS = $(SCRIPT_TESTS) $(PROGRAM_TESTS)

234
lib/hx509/fuzz_jose.c Normal file
View File

@@ -0,0 +1,234 @@
/*
* Copyright (c) 2025 Kungliga Tekniska Högskolan
* (Royal Institute of Technology, Stockholm, Sweden).
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the Institute nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
/*
* libFuzzer harness for JWS/JWT parsing in jose.c
*
* Build with:
* clang -g -O1 -fno-omit-frame-pointer -fsanitize=fuzzer,address \
* -I... fuzz_jose.c -o fuzz_jose -lhx509 -lroken ...
*
* Run with:
* ./fuzz_jose corpus_dir/
*/
#include <config.h>
#include <stdint.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>
#include <hx509.h>
/* libFuzzer entry points */
int LLVMFuzzerInitialize(int *argc, char ***argv);
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size);
/* Test keys for signature verification fuzzing */
/* RSA-2048 public key */
static const char *rsa_pubkey_pem =
"-----BEGIN PUBLIC KEY-----\n"
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAofgWCuLjybRlzo0tZWJj\n"
"NiuSfb4p4fAkd/wWJcyQoTbji9k0l8W26mPddxHmfHQp+Vaw+4qPCJrcS2mJPMEz\n"
"P1Pt0Bm4d4QlL+yRT+SFd2lZS+pCgNMsD1W/YpRPEwOWvG6b32690r2jZ47soMZo\n"
"9wGzjb/7OMg0LOL+bSf63kpaSHSXndS5z5rexMdbBYUsLA9e+KXBdQOS+UTo7WTB\n"
"EMa2R2CapHg665xsmtdVMTBQY4uDZlxvb3qCo5ZwKh9kG4LT6/I5IhlJH7aGhyxX\n"
"FvUK+DWNmoudF8NAco9/h9iaGNj8q2ethFkMLs91kzk2PAcDTW9gb54h4FRWyuXp\n"
"oQIDAQAB\n"
"-----END PUBLIC KEY-----\n";
/* EC P-256 public key */
static const char *ec_pubkey_pem =
"-----BEGIN PUBLIC KEY-----\n"
"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEf83OJ3D2xF1Bg8vub9tLe1gHMzV7\n"
"6e8Tus9uPHvRVEXH8UTNG72bfocs3+257rn0s2ldbqkLJK2KRiMohYjlrQ==\n"
"-----END PUBLIC KEY-----\n";
/* Ed25519 public key */
static const char *ed25519_pubkey_pem =
"-----BEGIN PUBLIC KEY-----\n"
"MCowBQYDK2VwAyEA11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=\n"
"-----END PUBLIC KEY-----\n";
static hx509_context ctx = NULL;
/*
* Initialize hx509 context once.
* Called by libFuzzer before fuzzing starts.
*/
int LLVMFuzzerInitialize(int *argc, char ***argv)
{
(void)argc;
(void)argv;
if (hx509_context_init(&ctx) != 0) {
ctx = NULL;
}
return 0;
}
/*
* Main fuzzing entry point.
* Input is treated as a potential JWS/JWT token.
*/
int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size)
{
char *token = NULL;
void *payload = NULL;
size_t payload_len = 0;
heim_dict_t claims = NULL;
const char *keys[3];
int ret;
if (ctx == NULL)
return 0;
/* Need at least "a.b.c" for a valid JWS structure */
if (size < 5)
return 0;
/* Limit input size to avoid OOM */
if (size > 1024 * 1024)
return 0;
/* Make null-terminated copy */
token = malloc(size + 1);
if (token == NULL)
return 0;
memcpy(token, data, size);
token[size] = '\0';
/* Set up key array for verification attempts */
keys[0] = rsa_pubkey_pem;
keys[1] = ec_pubkey_pem;
keys[2] = ed25519_pubkey_pem;
/*
* Test 1: JWS verification with multiple key types.
* This exercises:
* - Base64URL decoding of header, payload, signature
* - JSON parsing of header
* - Algorithm detection and validation
* - Key type matching
* - Signature format handling (ECDSA JWS->DER conversion)
*/
ret = hx509_jws_verify(ctx, token, keys, 3, &payload, &payload_len);
if (ret == 0) {
free(payload);
payload = NULL;
}
/*
* Test 2: JWT verification (includes claims parsing).
* This exercises:
* - Everything from JWS verification
* - JSON parsing of claims payload
* - Claims validation (exp, nbf, aud)
*/
ret = hx509_jwt_verify(ctx, token, keys, 3, NULL, 0, &claims);
if (ret == 0 && claims) {
heim_release(claims);
claims = NULL;
}
/*
* Test 3: Try with just one key at a time.
* This ensures we hit different code paths for key type mismatches.
*/
keys[0] = rsa_pubkey_pem;
ret = hx509_jws_verify(ctx, token, keys, 1, &payload, &payload_len);
if (ret == 0) {
free(payload);
payload = NULL;
}
keys[0] = ec_pubkey_pem;
ret = hx509_jws_verify(ctx, token, keys, 1, &payload, &payload_len);
if (ret == 0) {
free(payload);
payload = NULL;
}
keys[0] = ed25519_pubkey_pem;
ret = hx509_jws_verify(ctx, token, keys, 1, &payload, &payload_len);
if (ret == 0) {
free(payload);
payload = NULL;
}
/* Clear any error state */
hx509_clear_error_string(ctx);
free(token);
return 0;
}
#ifndef HAS_LIBFUZZER_MAIN
/*
* Standalone mode for testing without libFuzzer.
* Reads input from stdin or file arguments.
*/
int main(int argc, char **argv)
{
uint8_t buf[1024 * 1024];
size_t len;
FILE *fp;
int i;
LLVMFuzzerInitialize(&argc, &argv);
if (argc < 2) {
/* Read from stdin */
len = fread(buf, 1, sizeof(buf), stdin);
if (len > 0)
LLVMFuzzerTestOneInput(buf, len);
} else {
/* Read from each file argument */
for (i = 1; i < argc; i++) {
fp = fopen(argv[i], "rb");
if (fp == NULL)
continue;
len = fread(buf, 1, sizeof(buf), fp);
fclose(fp);
if (len > 0)
LLVMFuzzerTestOneInput(buf, len);
}
}
if (ctx)
hx509_context_free(&ctx);
return 0;
}
#endif

View File

@@ -0,0 +1,3 @@
Seed corpus for fuzz_jose
These files contain various JWS/JWT tokens to seed the fuzzer.

View File

@@ -0,0 +1 @@
eyJhbGciOiJub25lIn0..

View File

@@ -0,0 +1 @@
!!!.@@@.###

View File

@@ -0,0 +1 @@
eyJhbGciOiJFZERTQSJ9.RXhhbXBsZSBvZiBFZDI1NTE5IHNpZ25pbmc.hgyY0il_MGCjP0JzlnLWG1PPOt7-09PGcvMg3AIbQR6dWbhijcNR4ki4iylGjg5BhVsPt9g7sVvpAr_MuM0KAg

View File

@@ -0,0 +1 @@
...

View File

@@ -0,0 +1 @@
eyJhbGciOiJFUzI1NiJ9.e30.QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUE

View File

@@ -0,0 +1 @@
eyJhbGciOiJFUzI1NiJ9.e30.AAAA

View File

@@ -0,0 +1 @@
eyJhbGciOiJFUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.DtEhU3ljbEg8L38VWAfUAqOyKAM6-Xx-F4GawxaepmXFCgfTjDxw5djxLa8ISlSApmWQxfKTUJqPP3-Kg6NU1Q

View File

@@ -0,0 +1 @@
eyJhbGciOiJFUzM4NCJ9.e30.AA

View File

@@ -0,0 +1 @@
eyJhbGciOiJFUzUxMiJ9.e30.AA

View File

@@ -0,0 +1 @@
eyJhbGciOiJIUzI1NiJ9.e30.AA

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
e30.e30.AA

View File

@@ -0,0 +1 @@
eyJhbGciOiJSUzI1NiIsImV4dHJhIjp7Im5lc3RlZCI6eyJkZWVwIjp0cnVlfX19.e30.AA

View File

@@ -0,0 +1 @@
eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.cC4hiUPoj9Eetdgtv3hF80EGrhuB__dzERat0XF9g2VtQgr9PJbu3XOiZj5RZmh7AAuHIm4Bh-0Qc_lF5YKt_O8W2Fp5jujGbds9uJdbF9CUAr7t1dnZcAcQjbKBYNX4BAynRFdiuB--f_nZLgrnbyTyWzO75vRK5h6xBArLIARNPvkSjtQBMHlb1L07Qe7K0GarZRmB_eSN9383LcOLn6_dO--xi12jzDwusC-eOkHWEsqtFZESc6BfI7noOPqvhJ1phCnvWh6IeYI2w9QOYEUipUTI8np6LbgGY9Fs98rqVt5AXLIhWkWywlVmtVrBp0igcN_IoypGlUPQGe77Rw

View File

@@ -0,0 +1 @@
eyJhbGciOiJSUzM4NCJ9.e30.AA

View File

@@ -0,0 +1 @@
eyJhbGciOiJSUzUxMiJ9.e30.AA

View File

@@ -0,0 +1 @@
eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiLnlKjmiLciLCJpc3MiOiLlj5HooYzogIUifQ.AA

View File

@@ -0,0 +1 @@
eyJhbGciOiJ1bmtub3duIn0.e30.AA