{"repro_id":"REPRO-2026-00139","version":8,"title":"libjwt: JWT algorithm-confusion authentication bypass via RSA JWK without alg","repro_type":"security","status":"published","severity":"critical","description":"`libjwt` parses JSON Web Keys (JWKs) into an internal union key structure. When\nan RSA JWK that **lacks an `alg` parameter** is supplied as the verification key\nfor an HMAC-signed token (`HS256`/`HS384`/`HS512`), libjwt through `3.3.2`\naccepts that RSA key for HMAC verification. The HMAC octet (secret) field of the\nunion is left **zero-length**, so HMAC verification runs with an **empty key**.\n\nBecause the public JWKS is, by definition, public, an attacker who knows only\nthe published RSA JWK can forge an HMAC token signed with an empty key and have\nit verify successfully — a full **authentication bypass**.","root_cause":"# Root Cause Analysis: CVE-2026-44699 / GHSA-q843-6q5f-w55g\n\n## Summary\n\n`libjwt` versions 3.0.0 through 3.3.2 suffer from an algorithm confusion attack that allows JWT forgery using only a public RSA JWK. When an RSA JWK lacking an `alg` parameter is supplied as the verification key for an HMAC-signed token (`HS256`/`HS384`/`HS512`), the library incorrectly accepts the RSA key for HMAC verification. Because the internal `jwk_item_t` union shares storage between `provider_data` (an RSA `EVP_PKEY*`) and `oct.{key,len}` (HMAC bytes/length), the HMAC path reads a zero-length key and produces a successful verification for any token signed as `HMAC(\"\", header.payload)`. An attacker who knows only the published JWKS can forge valid tokens and bypass authentication.\n\n## Impact\n\n- **Package**: `libjwt` (C library for JWT processing)\n- **Affected versions**: 3.0.0 through 3.3.2 (inclusive)\n- **Fixed version**: 3.3.3\n- **Risk level**: Critical (CVSS 3.1 base 9.1)\n- **Consequences**: Full authentication bypass. Any attacker with access to the public JWKS (which is public by definition) can forge HMAC-signed JWTs that the vulnerable library accepts as valid.\n\n## Root Cause\n\nThe root cause is a failure to validate that a JWK's actual key type (`kty`) is compatible with the JWT algorithm being used for verification. The library previously relied on the optional `alg` parameter in the JWK to determine compatibility. When `alg` was absent (`JWT_ALG_NONE`), `__setkey_check()` in `jwt-common.c` allowed the key to be used with any caller-specified algorithm.\n\nBecause `jwk_item_t` is implemented as a union, setting an RSA key populates `provider_data` while leaving the HMAC `oct` fields uninitialized (effectively zero-length). When the HMAC verification backend later runs, it reads `oct.key` and `oct.len`, producing an HMAC with an empty key. Since `HMAC(\"\", data)` is deterministic, an attacker can pre-compute the signature and forge a valid token.\n\n### Fix commit\n\n- Commit `49c730a` in the libjwt repository: \"Fix algorithm confusion that allows JWT forgery via RSA JWK as HMAC key\"\n- The fix introduces `jwt_alg_required_kty()` which maps each JWA algorithm to its required JWK key type (e.g., `HS256` requires `kty=oct`).\n- `__setkey_check()` now rejects any setkey/verify callback that pairs a non-none algorithm with an incompatible `kty`.\n- `__verify_config_post()` adds a defensive re-check once `jwt->alg` is bound from the token header.\n- `__check_hmac()` adds a final backstop refusing any non-oct key for HMAC algorithms.\n\n## Reproduction Steps\n\n1. Execute `repro/reproduction_steps.sh`\n2. The script builds two versions of `libjwt`:\n   - `v3.3.2` (vulnerable)\n   - `v3.3.3` (fixed)\n3. It compiles a small C harness against each version.\n4. The harness:\n   - parses an RSA public JWK **without** an `alg` field,\n   - calls `jwt_checker_setkey(checker, JWT_ALG_HS256, rsa_jwk)`,\n   - verifies a forged `HS256` token whose signature is `HMAC-SHA256(\"\", header.payload)`.\n5. On the vulnerable build, the forged token verifies successfully (harness exits `0`).\n6. On the fixed build, `setkey` is rejected immediately with \"Key type does not match algorithm\" (harness exits `2`).\n\n## Evidence\n\n### Log locations\n- `logs/vuln_out.txt` — output from harness linked against `v3.3.2`\n- `logs/fix_out.txt` — output from harness linked against `v3.3.3`\n- `logs/results.txt` — combined summary\n- `repro/validation_verdict.json` — structured verdict\n\n### Key excerpts\n\n**Vulnerable build (v3.3.2)**\n```\nExit code: 0\nVERIFY SUCCEEDED (VULNERABLE)\n```\n\n**Fixed build (v3.3.3)**\n```\nExit code: 2\nsetkey rejected: Key type does not match algorithm\n```\n\nThese outputs confirm the bug and its remediation: the vulnerable library silently accepts an RSA JWK for HMAC verification and validates the forged token, while the fixed library rejects the key/algorithm mismatch at `setkey` time.\n\n### Environment\n- OS: Ubuntu Noble (24.04)\n- Compiler: GCC 13.3.0\n- CMake: 3.28.3\n- OpenSSL: 3.0.13\n- Jansson: 2.14\n- libjwt built from source at `external/libjwt`\n\n## Recommendations / Next Steps\n\n1. **Upgrade immediately** to `libjwt >= 3.3.3`.\n2. **Review JWKS handling** in applications that use `libjwt`. Ensure that verification callbacks do not blindly trust the `alg` field from an attacker-controlled JWT header without also checking the JWK's `kty`.\n3. **Regression tests**: The fix commit includes a comprehensive test suite in `tests/jwt_security.c` covering RSA, EC, and OKP keys mis-targeted at HMAC algorithms, as well as malformed JWKs where `alg` disagrees with `kty`. Maintainers should ensure these tests are run in CI.\n4. **Defense in depth**: Even after upgrading, applications should enforce algorithm whitelisting (e.g., only accept `RS256` if the JWKS contains RSA keys) at the application layer to reduce the impact of any future confusion bugs.\n\n## Additional Notes\n\n- **Idempotency**: The reproduction script was run twice consecutively with identical results, confirming the build and test process is deterministic.\n- **Edge cases**: The advisory notes the bug specifically requires an RSA JWK *without* an `alg` parameter. An RSA JWK that explicitly declares `\"alg\": \"RS256\"` would also be rejected for HMAC after the fix, because the fix binds algorithm acceptance to the JWK's `kty`, not its optional `alg` hint.\n- **Limitations**: The reproduction script focuses on `HS256` with a 2048-bit RSA JWK. The fix commit's tests confirm the same issue exists for `HS384` and `HS512`, as well as for EC and OKP keys misused as HMAC keys.\n","ghsa_id":"GHSA-q843-6q5f-w55g","cve_id":"CVE-2026-44699","cwe_id":"CWE-347 (Improper Verification of Cryptographic Signature)","package":{"name":"libjwt","ecosystem":"c","affected_versions":">= 3.0.0, <= 3.3.2","fixed_version":"3.3.3"},"reproduced_at":"2026-05-22T10:07:50.777092+00:00","duration_secs":806.960330247879,"tool_calls":106,"turns":87,"handoffs":2,"total_cost_usd":0.9390145600000004,"agent_costs":{"repro":0.26888331,"support":0.02422484,"vuln_variant":0.6459064099999999},"cost_breakdown":{"repro":{"accounts/fireworks/models/kimi-k2p6":0.26888331},"support":{"accounts/fireworks/models/kimi-k2p6":0.02422484},"vuln_variant":{"accounts/fireworks/models/kimi-k2p6":0.6459064099999999}},"quality":{"idempotent_verified":false,"community_verifications":0},"published_at":"2026-05-22T10:07:52.631306+00:00","retracted":false,"artifacts":[{"path":"repro/rca_report.md","filename":"rca_report.md","size":5566,"category":"analysis"},{"path":"repro/reproduction_steps.sh","filename":"reproduction_steps.sh","size":3125,"category":"reproduction_script"},{"path":"vuln_variant/rca_report.md","filename":"rca_report.md","size":7537,"category":"analysis"},{"path":"vuln_variant/reproduction_steps.sh","filename":"reproduction_steps.sh","size":2474,"category":"reproduction_script"},{"path":"bundle/context.json","filename":"context.json","size":2815,"category":"other"},{"path":"bundle/metadata.json","filename":"metadata.json","size":709,"category":"other"},{"path":"bundle/ticket.md","filename":"ticket.md","size":3244,"category":"ticket"},{"path":"repro/harness_vuln","filename":"harness_vuln","size":16544,"category":"other"},{"path":"repro/harness_fix","filename":"harness_fix","size":16544,"category":"other"},{"path":"repro/validation_verdict.json","filename":"validation_verdict.json","size":311,"category":"other"},{"path":"repro/harness.c","filename":"harness.c","size":2351,"category":"other"},{"path":"vuln_variant/variant_harness.c","filename":"variant_harness.c","size":4978,"category":"other"},{"path":"vuln_variant/harness_vuln","filename":"harness_vuln","size":16768,"category":"other"},{"path":"vuln_variant/patch_analysis.md","filename":"patch_analysis.md","size":6002,"category":"documentation"},{"path":"vuln_variant/variant_manifest.json","filename":"variant_manifest.json","size":2579,"category":"other"},{"path":"vuln_variant/harness_fix","filename":"harness_fix","size":16768,"category":"other"},{"path":"vuln_variant/validation_verdict.json","filename":"validation_verdict.json","size":1525,"category":"other"},{"path":"logs/results.txt","filename":"results.txt","size":189,"category":"other"},{"path":"logs/fix_out.txt","filename":"fix_out.txt","size":51,"category":"other"},{"path":"logs/vuln_variant/results.txt","filename":"results.txt","size":70,"category":"other"},{"path":"logs/vuln_variant/fix_out.txt","filename":"fix_out.txt","size":620,"category":"other"},{"path":"logs/vuln_variant/vuln_out.txt","filename":"vuln_out.txt","size":581,"category":"other"},{"path":"logs/vuln_out.txt","filename":"vuln_out.txt","size":30,"category":"other"}]}